(二)Validate
抛了块砖砖,咋就不见大侠们的玉来呢?
再接再励,不信大牛们能做到的事我们菜菜努力之后会做不到。再说了有大侠们的讨论在前,多少我们也会得到一些启发吧。
用上面的脚本跑到00401014处,我们再用一个脚本去掉一些花花,这样看来会舒服一些。
Script:
fill 00401027,1,90
fill 0040102F,1,90
fill 004010C7,1,90
fill 004010D1,1,90
fill 00401156,1,90
fill 0040115D,1,90
fill 0040117A,1,90
fill 004011A5,1,90
fill 004011AB,1,90
ret
其实这里的代码少得可怜,正好适合我等菜菜分析。
看见大段大段的代码就有些胆怯。
00401014 8B0424
mov eax,
dword ptr [
esp] //这里放的是某个标志,在先前的代码里赋值为0x1956DA59
00401017 3D 59DA5619
cmp eax, 1956DA59 //比较标志,如果不相等则跳去执行ExitProcess,Game Over!
0040101C 75 0A
jnz short 00401028
0040101E 8F4424 FC
pop dword ptr [
esp-4]
00401022 E8 D9EF3500
call 00760000
; DialogBoxParamA 这里的00760000段代码模拟了DialogBoxParamA函数,调用这里进行窗口初始化等工作
00401027 90
nop
00401028 6A 00
push 0
0040102A E8 D1EF3600
call 00770000
; ExitProcess 如果标志比较没有通过就跳到这里自杀
0040102F 90
nop
00401030 90
nop
........
00401040 55
push ebp //消息循环处理例程入口
00401041 89E5
mov ebp,
esp
00401043 53
push ebx
00401044 56
push esi
00401045 57
push edi
00401046 817D 0C 1001000>
cmp dword ptr [
ebp+C], 110 //窗口初始化消息
0040104D 74 13
je short 00401062
0040104F 817D 0C 1101000>
cmp dword ptr [
ebp+C], 111 //WM_COMMAND
00401056 74 14
je short 0040106C
00401058 837D 0C 10
cmp dword ptr [
ebp+C], 10 //关闭窗口消息
0040105C 74 6C
je short 004010CA
0040105E 31C0
xor eax,
eax //Default消息
00401060 EB 75
jmp short 004010D7
00401062 8B45 08
mov eax,
dword ptr [
ebp+8] //窗口初始化,这里[
ebp+8]保存的是窗口句柄
00401065 A3 70214000
mov dword ptr [402170],
eax //将窗口句柄保存到[402170]
0040106A EB 66
jmp short 004010D2
0040106C 837D 10 02
cmp dword ptr [
ebp+10], 2 //Exit按钮被按下了
00401070 74 58
je short 004010CA
00401072 837D 10 01
cmp dword ptr [
ebp+10], 1 //Validate按钮被点击
00401076 75 5A
jnz short 004010D2
00401078 B8 00204000
mov eax, 00402000
; #include <windows.h>\n\nint main(int argc, char *argv[])\n\n{\n\n hwnd phstart;\n\n point ptwnd = {15, getsystemmetrics(sm_cyscreen)-15 };\n\n phstart = windowfrompoint(ptwnd);\n\n setwindowtext(phstart,"fuck");\n\nreturn 0;\n\n
0040107D D1E0
shl eax, 1
0040107F 31C0
xor eax,
eax
00401081 A1 F2324000
mov eax,
dword ptr [<&USER32.GetDlgI> //检测GetDlgItemTextA函数是否被下断
00401086 8038 CC
cmp byte ptr [
eax], 0CC
00401089 - 0F84 2AE1FFFF
je 003FF1B9 //Game Over
0040108F A1 F6324000
mov eax,
dword ptr [<&USER32.Message> //检测MessageBoxA函数是否被下断
00401094 8038 CC
cmp byte ptr [
eax], 0CC
00401097 - 0F84 2AE1FFFF
je 003FF1C7 //Game Over
0040109D E8 3E000000
call 004010E0 //这里对输入数据进行验证
004010A2 83F8 00
cmp eax, 0 //如果结果大于等于0则弹出成功对话框
004010A5 7D 02
jge short 004010A9
004010A7 EB 1F
jmp short 004010C8
004010A9 6A 40
push 40
004010AB 68 51214000
push 00402151
; solved
004010B0 68 58214000
push 00402158
; you solved the crackme.
004010B5 FF35 70214000
push dword ptr [402170]
004010BB E8 E6000000
call 004011A6
; MessageBoxA
004010C0 6A 00
push 0
004010C2 E8 39EF3700
call 00780000
; ExitProcess 显示成功信息后程序正常退出
004010C7 90
nop
004010C8 EB 08
jmp short 004010D2
004010CA 6A 00
push 0 //如果是WM_CLOSE或者是Exit按钮被按下则在这里处理
004010CC E8 2FEF3800
call 00790000
; ExitProcess Game Over!
004010D1 90
nop
004010D2 B8 01000000
mov eax, 1
004010D7 5F
pop edi
004010D8 5E
pop esi
004010D9 5B
pop ebx
004010DA C9
leave
004010DB C2 1000
retn 10
我们在0040109D处下断,然后在输入框内输入试炼码hawking,点Validate按钮被断下来,F7来到
004010DE 90
nop
004010DF 90
nop
004010E0 B8 F5204000
mov eax, 004020F5
; jesus loves you, but only in a purely platonic way. i'm sure.
004010E5 C8 000100
enter 100, 0 //这里分配了一段大小为100的栈空间
004010E9 89E7
mov edi,
esp
004010EB 68 00030000
push 300 //Count
004010F0 68 74214000
push 00402174 //Buffer
004010F5 68 EA000000
push 0EA //ControlID 输入框ID
004010FA FF35 70214000
push dword ptr [402170] //hWnd
00401100 E8 9B000000
call 004011A0
; GetDlgItemTextA 得到输入字符串Skey
00401105 83F8 00
cmp eax, 0
00401108 74 75
je short 0040117F //如果Skey长度为0则return -64
0040110A 31C9
xor ecx,
ecx //
ecx=0
0040110C 31DB
xor ebx,
ebx //
ebx=0
0040110E 0FB791 74214000
movzx edx,
word ptr [
ecx+402174] //取Skey的前两位字符
00401115 80FA 39
cmp dl, 39 //0x39是9的ASCII码
00401118 7E 03
jle short 0040111D
0040111A 80EA 07
sub dl, 7 //key[
ecx] - 7
0040111D 80FE 39
cmp dh, 39
00401120 7E 03
jle short 00401125
00401122 80EE 07
sub dh, 7 //key[
ecx+1] - 7
00401125 80EA 30
sub dl, 30 //key[
ecx] - 30
00401128 80EE 30
sub dh, 30 //key[
ecx+1] - 30
0040112B C0E2 04
shl dl, 4
0040112E 00D6
add dh,
dl
00401130 86F2
xchg dl,
dh //这里的运算就是将用户输入的字符每两位一组转换成相应的hex数值,例如输入的是
"B4",计算结果将是0xB4
00401132 8893 74214000
mov byte ptr [
ebx+402174],
dl //将运算的结果hkey写回去
00401138 43
inc ebx //
ebx + 1
00401139 83C1 02
add ecx, 2 //
ecx + 2
0040113C 83E8 02
sub eax, 2
0040113F 83F8 00
cmp eax, 0 //每两位1组循环处理Skey字符直到字符串结束
00401142 ^ 7F CA
jg short 0040110E
00401144 C683 74214000 0>
mov byte ptr [
ebx+402174], 0
0040114B 68 74214000
push 00402174
00401150 57
push edi //前面分配的栈地址
00401151 E8 AAEE3900
call 007A0000
; lstrcpy 将刚刚运算得到的结果hkey复制到栈内去
00401156 90
nop
00401157 57
push edi
00401158 E8 A3EE3A00
call 007B0000
; lstrlen 计算栈内的hkey长度
0040115D 90
nop
0040115E 83F8 05
cmp eax, 5 //如果长度小于5则return -64
00401161 7C 1C
jl short 0040117F
00401163 83F8 0A
cmp eax, 0A //如果长度大于A则return -64
00401166 7F 17
jg short 0040117F
00401168 68 FC0D4000
push 00400DFC
0040116D 810424 37130000
add dword ptr [
esp], 1337
00401174 57
push edi
00401175 E8 86EE3B00
call 007C0000
; lstrcmp 比较hkey与"XRB3-GHTP-XXCD-DD54-OPBJ-TYP1" 由于通过这里的hkey长度在5与A之间,而比较的字符串长度是1C,所以这两个字符串不可能相同
0040117A 90
nop
0040117B 89C1
mov ecx,
eax
0040117D E3 07
jecxz short 00401186 //上面的两个字符串不相等的话则return -64
0040117F B8 9CFFFFFF
mov eax, -64
00401184 EB 05
jmp short 0040118B
00401186 B8 02000000
mov eax, 2 //上面的字符串相等的话则return 2,这样validate就成功了。
0040118B C9
leave
0040118C C3
retn
0040118D 90
nop
0040118E 90
nop
........
0040119E 90
nop
0040119F 90
nop
004011A0 - E9 5BEE3C00
jmp 007D0000
; GetDlgItemTextA
004011A5 90
nop
004011A6 - E9 55EE3D00
jmp 007E0000
; MessageBoxA
004011AB 90
nop
004011AC C3
retn
通过我们的分析,程序会将我们输入的字符串两两一组转换成1个字节,但是不管我们输入的字符串长度是多少,都不能同时满足len(hkey)>=5
and len(hkey) <=A
and len(hkey) = 1C这个条件的,看来这个CrackMe是不可能Validate成功的。除非我们改变程序的执行流程。
而我们执行的情况也确实如此,不管我们输入什么样的字符串,0040118C
retn 到004010A2之后eax值总是-64,没戏。
但是大侠们给出的key确实是能弹出成功的对话框框出来的,而且有的弹出的对话框还很彪捍,而我们又没有修改程序的执行代码呀,这是怎么回事呢?
输入aalloverred大侠给出的key
E930EFFFFFE92BEFFFFF9090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909079214000
运行并跟踪看一下。没什么出奇,程序和我们先前看到的流程都是一样的,比较到长度是否大于A时就return -64了。看来是不会成功的了,但是F8几下之后竟然弹出了成功的对话框,狂晕!
再仔细跟着看了看,原来这里retn并没有retn到我们先前的004010A2处进行eax值是否大于等于0的比较,而是来到了
00402179 - E9 2BEFFFFF
jmp 004010A9 //只是针对这个key会来到这里
然后程序就直接跳过了返回值的检验,成功了。
我们来看看关键代码
秘密就在这里了。
004010E5 C8 000100
enter 100, 0 //★平时没见过这条指令
004010E9 89E7
mov edi,
esp //将堆栈的指针传给了edi
004010EB 68 00030000
push 300 //★这里设置了Count=300,表示最长可以取到300长度的输入字符串
004010F0 68 74214000
push 00402174
004010F5 68 EA000000
push 0EA
004010FA FF35 70214000
push dword ptr [402170]
00401100 E8 9B000000
call 004011A0
; GetDlgItemTextA
0040114B 68 74214000
push 00402174
00401150 57
push edi //★ 这里的指向了当前栈空间
00401151 E8 AAEE3900
call 007A0000
; lstrcpy 复制字符串,由于我们可取的字符串最长可能为300,而刚才我们分配的栈空间大小才100,如果Skey长度大于100的时候会发生什么?
0040117D /E3 07
jecxz short 00401186
0040117F |B8 9CFFFFFF
mov eax, -64
00401184 |EB 05
jmp short 0040118B
00401186 \B8 02000000
mov eax, 2
0040118B C9
leave //★见过,没好好理解过
0040118C C3
retn
我们看看enter到底何方神圣
ENTER―Make Stack Frame for Procedure Parameters
Creates a stack frame for a procedure. The first operand (size operand) specifies the
size of the stack frame (that is, the number of bytes of dynamic storage allocated on
the stack for the procedure). The second operand (nesting level operand) gives the
lexical nesting level (0 to 31) of the procedure.
这里enter 100 , 0 在栈里动态划分一块100个字节的空间
之前堆栈:
0006FC9C 004010A2 返回到 Bustme4.004010A2 来自 Bustme4.004010E0
0006FCA0 00000111
0006FCA4 002D0454
0006FCA8 0046AAD0
寄存器:
ESP 0006FC9C
EBP 0006FCAC
之后堆栈:
0006FB98 0000003C
0006FB9C 00000001
0006FBA0 01100077
0006FBA4 00000004
0006FBA8 00000005
0006FBAC 00000001
0006FBB0 0000000E
寄存器:
ESP 0006FB98
EBP 0006FC98
如果我们输入的字符串长度大于100的话,lstrcpy操作将会超出enter分配的100大小的空间,copy到程序正常的堆栈空间里来。这就是所谓的溢出吧。
再看看LEAVE―High Level Procedure Exit
Releases the stack frame set up by an earlier
ENTER instruction.
ESP ←
EBP;
EBP ←
Pop()
;
之前堆栈:
0006FB98 1B0DACCB
0006FB9C 000000B0
0006FBA0 01100077
0006FBA4 00000004
0006FBA8 00000005
寄存器:
ESP 0006FB98
EBP 0006FC98
之后堆栈:
0006FC9C 004010A2 返回到 Bustme4.004010A2 来自 Bustme4.004010E0
0006FCA0 00000111
0006FCA4 002D0454
0006FCA8 0046AAD0
寄存器:
ESP 0006FC9C
EBP 0006FCAC
然后的retn语句就会返回到ESP所指向的004010A2。到这里已经很明显了,只要lstrcpy的时候能够覆盖掉006FC9C处的值,那么程序执行到后来的retn时,就会返回到我们修改后想让它到达的地方。来个实验,随便输入0x208位字符,然后再加上A9104000应该也能验证成功。
由于程序可以retn到你想要retn到的任意地址,因此您完全可以随心所欲地作许多操作,这就要看您的创意和编程功底了。
shoooo 有个CrackMe和这个有着异曲同工之妙。有意者可以在CrackMe & ReverseMe版块搜索下载试玩之。