本文就纯 OD 手工完成算法分析,过程比较累,你有什么更好的办法请不吝分享。如果有猪蹄汤、排骨汤应该提前服用。
为了节省篇幅,本文直接拿事后算出来的注册码作为测试输入,更直观。功力有限,有些地方分析不到位,请见谅。
另外,我对本题作者无任何不敬之意,权当学习交流了。
一、首先,算法很绕,能破解出来也有一定的运气成分。看到群里有人提到是否可以直接修改CrackMe汇编代码,让它自己完成穷举,我觉得这个思路值得探索,事后补上了这章。开头先讲讲如何利用 ODbgScript 实现自动化穷举代码的写入和结果显示,就当开拓思维吧,以后在其他场合还是有用武之地的。
// Ctf.pediy.com crackme 2# Ollydbg 穷举脚本
// 本例测试穷举效率不高,每秒几千到一万多次,可能跟Crackme的算法很绕有关
// 所以直接通过本Crackme自身写入代码进行穷举需要几个小时时间才能完成
// OdbgScript 1.83
// Written by 爱琴海
// 2017/06/04
CMP $VERSION, "1.82"
//比较是否大于1.82版,脚本基于1.82版本以上
JAE Initialize
MSG "需要1.82版本以上的ODbgScript才能支持本程序,请更新!"
RET
Initialize:
//定义变量
VAR Sn_address
//储存注册码地址
VAR Eip_bak
//备份恢复原始Eip
PUSHA
//高版本OdbgScript才支持该指令,保存寄存器现场
Set_break:
//设置相关断点,提示用户
BP 4010BC
BPGOTO 4010BC,Catch_break
//设置4010BC断点,接管算法流程
BP 401257
BPGOTO 401257,Succes
//设置401257断点,找到注册码时候接管流程
MSG "请在CrackMe输入窗口输入8位非0数字并回车继续"
RUN
Catch_break:
//捕捉到4010BC断点,让程序自己完成一些初始化工作再接管
BC 4010BC
//取消4010BC断点,有始有终
MOV Eip_bak,eip
//保存当前eip,穷举结束时得恢复eip
Start:
//循环穷举主程序,为提高运行效率采用补丁方式,交给程序自己完成
MOV Sn_address,ecx
//储存注册码地址,成功时需要调用显示
MOV [ecx],#313131313131313000#
//我已经测试CrackMe自身穷举效率较低,穷举需要几个小时才能完成
//穷举初值11111110(接下里的累加1,将实际初值变为11111111)
Inc_check:
//写入代码,修改程序流程,实现穷举注册码功能
MOV [4010BC],#E95F660000909090909090#
// JMP 00407720 以及 nop 填充,接管算法流程
MOV [40122B],#909090909090E976FEFFFF#
// NOP JE 004010FA
// JMP 004010AC 错误时继续
MOV [407700],#60B80800000083F8017413807C08FF397609C64408FF31FE4408FE48EBE861C3FE4107E8D8FFFFFF8039397601C3E99499FFFF#
//累加、检查进位子程序,选择407700位置写入代码
// 407700:
// PUSHAD
// MOV EAX,0x8
// CMP EAX,0x1
// JE 0040771E
// CMP BYTE PTR DS:[EAX+ECU-0x1],0x39 不是ECU而是ECX,显示乱码搞不懂
// JBE 0040771B
// MOV BYTE PTR DS:[EAX+ECU-0x1],0x31 不是ECU而是ECX,显示乱码搞不懂
// INC BYTE PTR DS:[EAX+ECU-0x2] 不是ECU而是ECX,显示乱码搞不懂
// DEC EAX
// JMP 00407706
// POPAD
// RETN
// 407720:
// INC BYTE PTR DS:[ECX+0x7]
// CALL 00407700
// CMP BYTE PTR DS:[ECX],0x39 最高位超过9就结束
// JBE 0040772E
// RETN
// 40772E:
// JMP 004010C7
RUN
Succes:
//成功穷举到注册码
BC 401257
//取消401257断点,有始有终
MSG [Sn_address]
//显示注册码
End:
//收尾工作
POPA
//高版本OdbgScript才支持该指令,彻底恢复寄存器现场
MOV eip,Eip_bak
//穷举结束恢复eip
MSG "穷举结束"
ret
//结束脚本
你猜结果如何? 虽然能穷举到注册码,但每秒几千次到上万次,效率比较低,需进一步分析算法实现破解。这章事后补充的,希望能有启发。
二、查壳,Microsoft Visual C/C++(6.0)[libc],木有壳。
三、OD载入,查找字符串,定位到:“WELL DONE!”,往上翻翻就到了主要代码。
四、在401053设断点,运行。动态跟踪时发现有检测GetTickCount,记得设置返回为0,避免干扰分析。
五、输入“97654321”之后回车断下,一步步跟踪:
要求注册码为:8-20位,且1-9(非0数字)
六、跟踪到 4010CF:
此时感觉 CALL 4014E0 可疑,单步进入:
原来此段代码是将输入字符当做十进制,逐位存放,逐位检查格式、进位,出题作者自己实现了最大0x400位十进制计算功能。比如输入“97654321”得到 12345679。 CALL 00401970 各位可以自己跟踪看看,这里省点篇幅以免过于啰嗦。
七、返回到上一步,继续:
此处 CALL 401730 明显得跟踪进入:
跟踪 CALL 00401540:
留意到401567、40156B分别处理了当前计算结果所储存的地址和内容,为了方便后续分析,在这两处分别下条件记录断点记录
ECX+EBP*4+0x8、EBX,并命名“地址、密码”:
继续跟进 CALL 004016E0 定位核心功能:
可以看到:
0040170C y |. 0FAFDF |IMUL EBX,EDI ; *9 *7 *6 ......*3 *2 *1
这句就是作者实现自定义乘法的“乘法之根本法”,后续频繁调用这个子程序,为了减少劳动量,请设置
401708、40170C、40170F 条件记录断点,永不中断,分别永远记录 EBX、EDI、EBX 值,并标明表达式名称为“x、y、x *
y”,以免后续搞混了。
在4017B5下断,清空记录窗口,运行,查看记录窗口:
Log data
地址 消息
00401567 COND: 地址 = 001289B0
0040156B COND: 密码 = 00000009
00401567 COND: 地址 = 00128664
0040156B COND: 密码 = 00000007
00401567 COND: 地址 = 0012823C
0040156B COND: 密码 = 00000006
00401567 COND: 地址 = 00128D00
0040156B COND: 密码 = 00000005
00401567 COND: 地址 = 00128234
0040156B COND: 密码 = 00000004
00401567 COND: 地址 = 001284E0
0040156B COND: 密码 = 00000003
00401567 COND: 地址 = 00127F94
0040156B COND: 密码 = 00000002
00401567 COND: 地址 = 00128C14
0040156B COND: 密码 = 00000001
00401708 COND: x = 00000009
0040170C COND: y = 00000009
0040170F COND: x * y = 00000051
00401708 COND: x = 00000007
0040170C COND: y = 00000009
0040170F COND: x * y = 0000003F
00401708 COND: x = 00000006
0040170C COND: y = 00000009
0040170F COND: x * y = 00000036
00401708 COND: x = 00000005
0040170C COND: y = 00000009
0040170F COND: x * y = 0000002D
00401708 COND: x = 00000004
0040170C COND: y = 00000009
0040170F COND: x * y = 00000024
00401708 COND: x = 00000003
0040170C COND: y = 00000009
0040170F COND: x * y = 0000001B
00401708 COND: x = 00000002
0040170C COND: y = 00000009
0040170F COND: x * y = 00000012
00401708 COND: x = 00000001
0040170C COND: y = 00000009
0040170F COND: x * y = 00000009
004017B5 断点位于 2.004017B5
取消 4017B5 断点,保留条件记录断点,跟踪 CALL 004015E0:
可以看到:
00401641 b |. 012A |ADD DWORD PTR DS:[EDX],EBP ; 加法
这句就是作者实现自定义加法的“加法之根本法”,后续频繁调用这个子程序,为了减少劳动量,请设置40163D、401641、401643条件记录断点,永不中断,分别永远记录EBP、DWORD
PTR DS:[EDX]、条件EDX>400记录DWORD PTR DS:[EDX]值,并标明表达式名称为“a、b、a +
b”,以免后续搞混了。
此时还没到用到加法的时候,所以你也记录不到加法信息,别着急,保留条件记录断点,回到4010EE下断点,清空记录窗口,运行。
断下之后,查看记录窗口:
通过上述记录信息,很快就可以理解本步骤算法含义:12345679 * 9 = 111111111
八、从上文 4010EE 继续:
CALL 401840 和 CALL 401730 是算法的核心,分别实现了逐位乘法、加法,累加得到最终结果。(CALL 401730 在上文中已经分析过了)
清空记录窗口,在 401137 设置断点,运行,此时查看记录窗口正在迅速回显记录,条数很多,此时静下心来逐条查看计算记录,因为条数太多,这里只罗列最后结果那几行,剩下的记录请见附件log:
00401567 COND: 地址 = 0012C728
0040156B COND: 密码 = 00000001
00401567 COND: 地址 = 0012CCFC
0040156B COND: 密码 = 00000002
00401567 COND: 地址 = 0012C584
0040156B COND: 密码 = 00000003
00401567 COND: 地址 = 0012C48C
0040156B COND: 密码 = 00000004
00401567 COND: 地址 = 0012C154
0040156B COND: 密码 = 00000005
00401567 COND: 地址 = 0012C3EC
0040156B COND: 密码 = 00000006
00401567 COND: 地址 = 0012CC8C
0040156B COND: 密码 = 00000007
00401567 COND: 地址 = 0012C220
0040156B COND: 密码 = 00000008
00401567 COND: 地址 = 0012C188
0040156B COND: 密码 = 00000009
00401567 COND: 地址 = 0012C740
0040156B COND: 密码 = 00000008
00401567 COND: 地址 = 0012BFD0
0040156B COND: 密码 = 00000007
00401567 COND: 地址 = 0012C8D8
0040156B COND: 密码 = 00000006
00401567 COND: 地址 = 0012C11C
0040156B COND: 密码 = 00000005
00401567 COND: 地址 = 0012C0BC
0040156B COND: 密码 = 00000004
00401567 COND: 地址 = 0012CBEC
0040156B COND: 密码 = 00000003
00401567 COND: 地址 = 0012C598
0040156B COND: 密码 = 00000002
00401567 COND: 地址 = 0012BFEC
0040156B COND: 密码 = 00000001
00401137 断点位于 2.00401137
整理计算记录、步骤:
根据追踪记录,不难看出本作品是以用户输入倒置为十进制数,自定义了0x400位十进制计算乘法、加法(从低位到高位,逐位乘法、加法并进行进位)
Python 实现该部分算法:
先在 Python 中测试:
输入“11111111”--> 11111111 -- > 16位:9999999800000001
输入“87654321”--> 12345678 -- > 17位:12345676987654404
输入“99999999”--> 99999999 -- > 18位:809999983800000081
输入8位-->16-18位
输入9位-->18-21位
依次类推,先得到大概输入、输出位数的范围,便于后续选择。
九、跟踪关键比较程序:
与计算结果的第7-1位相等
与计算结果的17-11位相等
0040120F |. 03F0 |ADD ESI,EAX
00401211 |. 74 44 |JE X2.00401257 ; 正确出口
其中:
00401168 |. /0F85 A7000000 |JNZ 2.00401215 ; 当输入8位数时,结果16-18位,所以可以假设
17 位
提示了计算结果极有可能是奇数位。
004011A2 |. /75 78 |JNZ X2.0040121C ; 不能跳
提示了用户输入的第1位(十进制个位)== 计算结果的中间位。
CALL 004013E0 关键比较,逐位比较用户输入的第2-8位是否与计算结果的第7-1位相等;用户输入的第8-2位是否与计算结果的17-11位相等:
至此算法部分基本清楚了。
十、算法逆向:
输入:“97654321”--> 12345679
输出:12345678987654321
满足条件1:每个输入字符为1-9数字
满足条件2:输出结果奇数位
满足条件3:输出结果中间位必须等于输入第1位(十进制末位)
满足条件4:用户输入的第2-8位(十进制7-1位)必须与计算结果的第7-1位相等
满足条件5:用户输入的第8-2位(十进制1-7位)必须与计算结果的17-11位相等
考虑先从8位数开始穷举:(也许算法深入分析后会有一些简便运算方法或者效率提升,但是人生苦短,只要出答案即可,不浪费时间)
Python Keygen: