Replace
发布日期:2019年03月13日 类别:reversing 题目来源:reversing.kr 题目链接:http://reversing.kr/challenge.php打开程序后弹出了一个对话框,文本框中似乎只能输入数字,输入错误的话程序会直接退出,有时还会崩溃:
用 cutter 打开,查看一下字符串,发现一个“Correct!”。再查看一下交叉引用,能够看出这个函数应该是对话框的消息处理回调函数:
| 0x00401021 mov ebp, esp
| 0x00401023 cmp dword [arg_ch], 0x111 ; [0xc:4]=-1 ; 273
| ,=< 0x0040102a je 0x401032
| | 0x0040102c xor eax, eax
| | 0x0040102e pop ebp
| | 0x0040102f ret 0x10
| `-> 0x00401032 mov eax, dword [arg_10h] ; [0x10:4]=-1 ; 16
| 0x00401035 and eax, 0xffff
| 0x0040103a sub eax, 2
| ,=< 0x0040103d je loc.00401095
| | 0x0040103f sub eax, 0x3e9
| ,==< 0x00401044 je 0x40104c
| || 0x00401046 xor eax, eax
| || 0x00401048 pop ebp
| || 0x00401049 ret 0x10
| `--> 0x0040104c push esi
| | 0x0040104d mov esi, dword [arg_8h] ; [0x8:4]=-1 ; 8
| | 0x00401050 push 0
| | 0x00401052 push 0
| | 0x00401054 push 0x3ea ; 1002
| | 0x00401059 push esi
| | 0x0040105a call dword [sym.imp.USER32.dll_GetDlgItemInt] ; 0x40509c ; "`U"
| | 0x00401060 mov dword [0x4084d0], eax ; [0x4084d0:4]=0
| | 0x00401065 call fcn.0040466f
| | 0x0040106a xor eax, eax
| ,==< 0x0040106c jmp loc.00404690
| || ;-- fcn.00401071:
\ ,===< 0x00401071 jmp loc.00401084
||| 0x00401073 push str.Correct ; 0x406034 ; "Correct!"
||| 0x00401078 push 0x3e9 ; 1001
||| 0x0040107d push esi
||| 0x0040107e call dword [sym.imp.USER32.dll_SetDlgItemTextA] ; 0x4050a0 ; "NU"
我们感兴趣的地方从 0x0040105a
这里开始。程序首先调用了 GetDlgItemInt
将文本框中的文本按照整数读出,将其存入 0x004084d0
中;而后又调用了一个函数,紧接着是两个 jmp
。此时如果搜索下面压入“Correct”字符串处的地址 0x00401073
,会发现并没有地方直接引用它,很奇怪。不管它了,还是先看一下 fcn.0040466f
处的函数吧:
/ (fcn) fcn.0040466f 26
| fcn.0040466f ();
| : 0x0040466f call 0x40467a
| : 0x00404674 add dword [0x4084d0], 0x601605c7
| : 0x0040467e inc eax
| : 0x0040467f add bl, ch
| |: 0x00404681 pushal
| |: 0x00404682 nop
| |: 0x00404683 popal
\ |: 0x00404684 call fcn.00404689
/ (fcn) fcn.00404689 7
| fcn.00404689 ();
| |: 0x00404689 inc dword [0x4084d0]
\ |: 0x0040468f ret
首先这个函数第一句调用了 0x0040467a
。从旁边的一列地址中我们可以发现,这个地址并不在 cutter 显示出来的这些地址里。这是由于 x86 是变长指令集所导致的——任意地址都可以认为是一条指令的开始。也就是说,如果我们认为 0x00404674
地址为一条指令的开始,那么它就是 cutter 显示出来的 add dword [0x4084d0], 0x601605c7
这一句,这条指令的长度为 8 个字节;可是我们同样也可以认为 0x0040467a
是一条指令的开始,CPU 同样可以从这里继续执行,只不过此时 cutter 没有正确地把指令显示出来而已。
我们可以在 0x00404674
上右键选择“Set to Data”,选择 “…”:
然后在弹出的对话框中填入 6(即 0x0040467a - 0x00404674
),让 r2 将 0x00404674
处的字节解释为长度为 6 字节的数据,从而使得它能够从 0x0040467a
处开始继续反汇编指令。这样这个函数就变成了:
/ (fcn) fcn.0040466f 26
| fcn.0040466f ();
| : 0x0040466f call 0x40467a
| : 0x00404674 hex length=6 delta=0
0x00404674 81 05 d0 84 40 00 ....@.
| : 0x0040467a mov dword [0x406016], 0x619060eb ; [0x406016:4]=0
\ |: 0x00404684 call fcn.00404689
/ (fcn) fcn.00404689 7
| fcn.00404689 ();
| |: 0x00404689 inc dword [0x4084d0]
\ |: 0x0040468f ret
0x0040467a
是一个 mov
,这个地址似乎没什么用,这条指令也不影响程序的逻辑,可以忽略。接下来需要注意的是,call fcn.00404689
直接 call
了下一行指令 0x00404689
,这里即将输入的数字加一;ret
返回后再次回到了 0x00404689
,又执行了一次加一操作,而后函数才返回。此时我们已经将输入加 2 了。
返回后就返回到了 0x0040466f
的下一个字节处,即此时 0x00404674
就是新一条指令的开始了。我们需要在 cutter 里面把刚才的“Set to Data”取消掉——在 0x00404674
上右键,选择“Set as Code”即可:
/ (fcn) fcn.0040466f 26
| fcn.0040466f ();
| : 0x0040466f call 0x40467a
| : 0x00404674 add dword [0x4084d0], 0x601605c7
| : 0x0040467e inc eax
| : 0x0040467f add bl, ch
| |: 0x00404681 pushal
| |: 0x00404682 nop
| |: 0x00404683 popal
\ |: 0x00404684 call fcn.00404689
/ (fcn) fcn.00404689 7
| fcn.00404689 ();
| |: 0x00404689 inc dword [0x4084d0]
\ |: 0x0040468f ret
这样继续执行的话,0x00404674
处将我们的输入加上了 0x601605c7
。随后是几条不影响程序逻辑的指令,然后又到了 0x00404684
这里。刚才我们已经分析过了,这几行代码会将我们的输入加 2。
因此总结一下,fcn.0040466f
这个函数会把我们输入的数字加上 2 + 0x601605c7 + 2
。
回到刚才的消息处理函数中,调用了这个函数后,清空 eax
,然后又 jmp
到了 loc.00404690
处:
| |: 0x00404690 mov eax, dword [0x4084d0] ; [0x4084d0:4]=0
| |: 0x00404695 push 0x40469f
| |: 0x0040469a call fcn.00404689
| |: 0x0040469f mov dword [fcn.0040466f], 0xc39000c6 ; [0x40466f:4]=0x6e8
| |: 0x004046a9 call fcn.0040466f
| |: 0x004046ae inc eax
| |: 0x004046af call fcn.0040466f
| |: 0x004046b4 mov dword [fcn.0040466f], 0x6e8 ; [0x40466f:4]=0x6e8
| |: 0x004046be pop eax
| |: 0x004046bf mov eax, 0xffffffff ; -1
\ |`=< 0x004046c4 jmp fcn.00401071
首先第一行是把我们的数据读入至 eax
。第二行压入了一个立即数,通过后续分析发现倒数第三行 pop eax
将这个数拿回了 eax
,但是随后重新设置了 eax
的值,因此这个 push
也没什么用。第三行调用了 fcn.00404689
将我们输入的数字加一,但要注意我们刚才已经将这个数字读出至 eax
,因此这一次调用不会影响到 eax
的值。
第四行 mov dword [fcn.0040466f], 0xc39000c6
,这条指令将 0040466f
处的数据做了修改,这不就是我们刚才看的函数吗?也就是说,这一句代码在运行时改变了 0x0040466f
处的汇编指令。首先看看没改动之前的情况:
我们把从 0x0040466f
开始的四个字节修改为 0xc6 0x00 0x90 0xc3
,然后重新用 cutter 打开查看反汇编:
/ (fcn) fcn.0040466f 4
| fcn.0040466f ();
| : 0x0040466f mov byte [eax], 0x90 ; [0x90:1]=255 ; 144
\ : 0x00404672 ret
这个函数的作用就变为了向 eax
处写入 nop
。
继续向下看到第五行:
调用了 fcn.0040466f
,随后 eax
加一,再调用一次 fcn.0040466f
。也就是说,我们连续把从 eax
开始的两个字节都写成了 nop
。要记得,eax
中就是根据我们输入的数字计算而得的。
再往下就是把 fcn.0040466f
恢复原状,eax
置 -1 后,跳转到了 fcn.00401071
。
我们看一下 fcn.00401071
:
\ ,===< 0x00401071 jmp loc.00401084
||| 0x00401073 push str.Correct ; 0x406034 ; "Correct!"
||| 0x00401078 push 0x3e9 ; 1001
||| 0x0040107d push esi
||| 0x0040107e call dword [sym.imp.USER32.dll_SetDlgItemTextA] ; 0x4050a0 ; "NU"
|- loc.00401084 17
| loc.00401084 ();
| `---> 0x00401084 mov eax, 1
| || 0x00401089 nop
| || 0x0040108a nop
| || 0x0040108b nop
| || 0x0040108c nop
| || 0x0040108d nop
| || 0x0040108e nop
| || 0x0040108f nop
| || 0x00401090 pop esi
| || 0x00401091 pop ebp
\ || 0x00401092 ret 0x10
刚好在我们的 Correct
上方,一个 jmp
就跳走了。联系刚才那段逻辑可以想到,我们完全可以将 0x00401071
处的两个字节修改为 nop
,这样就能够输出 Correct
了。因此我们输入的数字应为 0x00401071 - (2 + 0x601605c7 + 2) = 0xa02a0aa6
。
这里还需要注意的是,由于 GetDlgItemInt
的返回类型是 UINT
,因此应该以无符号整数解释 0xa02a0aa6
。所以只需要在对话框中填入