avatar

目录
CSAPP BinaryBomb拆弹记录

前言

本来这一次的系统级编程作业是让我们阅读一下各种结构的汇编代码,了解一下汇编,在附件里面除了作业还传上了一个挖雷游戏,让我们尽量先通过第一关。然后想了一下,读代码多没意思啊,怎么不去玩玩紧张刺激的hacking呢?听老师说这玩意很有意思,能让人在无限续杯的咖啡店通宵一晚上。看到舍友在玩,我也按耐不住自己了,于是打开了尘封半年的Ollydbg,开始我的挖雷游戏。

本次拆弹游戏使用的材料是由CSAPP配套提供的二进制炸弹,SCU应该对它进行了一些修改,工具是在Windows系统下,由吾爱破解论坛提供的专用版Ollydbg动态分析工具。

第一个BOMB-逆向基础

先把程序跑起来,随便输,让炸弹爆炸,看看出什么结果。

图1.png

右键使用中文搜索引擎进行智能搜索,可以看到顶上就是BOOM!那个字符串,Enter追踪一下。

图2

可以看到是一个调用两次printf的函数,在函数头部先F2下个断点,重新跑起来,看看返回到哪。

图3

右下角堆栈段的观察栈顶,重新用Enter追踪一下。

图4

发现一个字符串,和eax的值一起被push到栈里面,猜测这个是flag。

输进去,还真是。下个断点F7单步调试,最后发现这块代码是字符串比较函数。

第二个BOMB-循环

和第一个boom一样,首先追根溯源到boom的主调函数

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
00CB25F6    52              push edx
00CB25F7 68 AC7ACB00 push bomb_-_?00CB7AAC ; %d %d %d %d %d %d
00CB25FC 8B45 08 mov eax,dword ptr ss:[ebp+0x8] ; bomb_-_?00CBA6F0
00CB25FF 50 push eax
00CB2600 FF15 ECB2CB00 call dword ptr ds:[<&MSVCR110D.sscanf>] ; msvcr110.sscanf
00CB2606 83C4 20 add esp,0x20
00CB2609 3BF4 cmp esi,esp
00CB260B E8 85EBFFFF call bomb_-_?00CB1195
00CB2610 83F8 06 cmp eax,0x6
00CB2613 7D 05 jge short bomb_-_?00CB261A
00CB2615 E8 D0EBFFFF call bomb_-_?00CB11EA
00CB261A 5F pop edi ; bomb_-_?00CB261A
00CB261B 5E pop esi ; bomb_-_?00CB261A
00CB261C 5B pop ebx ; bomb_-_?00CB261A
00CB261D 81C4 C0000000 add esp,0xC0
00CB2623 3BEC cmp ebp,esp
00CB2625 E8 6BEBFFFF call bomb_-_?00CB1195
00CB262A 8BE5 mov esp,ebp
00CB262C 5D pop ebp ; bomb_-_?00CB261A
00CB262D C3 retn

观察栈顶可得boom的调用点是在0x00CB2615处的指令,在这条指令上方有一个判断,判断eax中的值是否等于6,不等于6就会走这条call,然后boom,再看看前五行代码,OD已经给了明确提示:这里调用了sscanf函数,回忆C语言中的这个函数,其中两个参数为格式化字符串和变量地址,返回值为成功读入的值的个数,这里也依次压栈。调用过后的返回值会存在eax中,所以要是不想在这里boom,必须输入6个整数,才能走下去。

好吧,那随便输6个整数试试,输个666 666 666 666 666 666吧~注意先在与6比较的那条指令那里下个断点,方便调试

果然,6个整数走通了,接下来一直F7单步执行,最后可以来到一片桃花源

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
00CB1B25    83C4 08              add esp,0x8
00CB1B28 B8 04000000 mov eax,0x4
00CB1B2D 6BC0 00 imul eax,eax,0x0
00CB1B30 837C05 E0 01 cmp dword ptr ss:[ebp+eax-0x20],0x1
00CB1B35 74 05 je short bomb_-_?00CB1B3C
00CB1B37 E8 AEF6FFFF call bomb_-_?00CB11EA ; boom
00CB1B3C C745 D4 01000000 mov dword ptr ss:[ebp-0x2C],0x1
00CB1B43 8B45 D4 mov eax,dword ptr ss:[ebp-0x2C] ; [ebp-0x2C]存的是计数变量
00CB1B46 83C0 01 add eax,0x1
00CB1B49 8B4D D4 mov ecx,dword ptr ss:[ebp-0x2C]
00CB1B4C 0FAF448D DC imul eax,dword ptr ss:[ebp+ecx*4-0x24] ; [ebp+ecx*4-0x24]存的是当前的乘积,相当于把i+1和ai相乘
00CB1B51 8B55 D4 mov edx,dword ptr ss:[ebp-0x2C]
00CB1B54 3B4495 E0 cmp eax,dword ptr ss:[ebp+edx*4-0x20] ; 我们输入的第i+1个参数要和刚才的运算结果相等
00CB1B58 74 05 je short bomb_-_?00CB1B5F
00CB1B5A E8 8BF6FFFF call bomb_-_?00CB11EA ; boom
00CB1B5F 8B45 D4 mov eax,dword ptr ss:[ebp-0x2C]
00CB1B62 83C0 01 add eax,0x1
00CB1B65 8945 D4 mov dword ptr ss:[ebp-0x2C],eax
00CB1B68 837D D4 05 cmp dword ptr ss:[ebp-0x2C],0x5
00CB1B6C 7E D5 jle short bomb_-_?00CB1B43

这算是一片比较烧脑的汇编代码了。不过在OD里面可以得到提示说这是一个循环。

可以看到有3个比较跳转指令,入手点应该就是它们。

来到0x00CB1B30,OD提示现在我们在拿0x29A也就是666和1做比较,去栈里面看看这个666是哪来的。

图5

发现栈里面有连续的6个666,不用说了,这铁定是输进来的参数啊!按照栈地址的排列方式,这个就是输入的第一个参数。已经知道下一条call走向boom,当然是要跳过它啦!所以得到输入的第一个参数是1。

往下走,这里是个循环,这个循环是要干嘛呢?首先是给某个局部变量初始化为1,然后开始循环。翻译一下这段汇编代码,其实就是先把这个局部变量拿出来,加一,乘以输入的第i+1个数,所得的结果要和输入的第i+1个数相等才不会boom,接着再把运算结果存回这个变量。那这个局部变量是什么呢?因为汇编代码里面把它放到了ecx寄存器中,猜测它应该是一个计数变量。看看0x00CB1B5F之后的代码,还确实是这样。

思考一下,递推关系就是a(i+1)=a(i)*(i+1)。

这TM不就是阶乘嘛~

那就很简单了,列出16的阶乘,答案出来辣

第三个BOMB-分支

前面思路和第二题一样,不过这次输入的是两个整数、一个字符。校验代码是下面这样的

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
00A81C35    83F8 03               cmp eax,0x3
00A81C38 7D 05 jge short bomb.00A81C3F
00A81C3A E8 ABF5FFFF call bomb.00A811EA ; boom
00A81C3F 8B45 D4 mov eax,dword ptr ss:[ebp-0x2C]
00A81C42 8985 0CFFFFFF mov dword ptr ss:[ebp-0xF4],eax
00A81C48 83BD 0CFFFFFF 07 cmp dword ptr ss:[ebp-0xF4],0x7
00A81C4F 0F87 AE000000 ja bomb.00A81D03
00A81C55 8B8D 0CFFFFFF mov ecx,dword ptr ss:[ebp-0xF4]
00A81C5B FF248D 781DA800 jmp dword ptr ds:[ecx*4+0xA81D78]
00A81C62 C645 E3 71 mov byte ptr ss:[ebp-0x1D],0x71
00A81C66 817D F8 09030000 cmp dword ptr ss:[ebp-0x8],0x309
00A81C6D 74 05 je short bomb.00A81C74
00A81C6F E8 76F5FFFF call bomb.00A811EA ; boom
00A81C74 E9 93000000 jmp bomb.00A81D0C
00A81C79 C645 E3 62 mov byte ptr ss:[ebp-0x1D],0x62
00A81C7D 817D F8 D6000000 cmp dword ptr ss:[ebp-0x8],0xD6
00A81C84 74 05 je short bomb.00A81C8B
00A81C86 E8 5FF5FFFF call bomb.00A811EA ; boom
00A81C8B EB 7F jmp short bomb.00A81D0C
00A81C8D C645 E3 62 mov byte ptr ss:[ebp-0x1D],0x62
00A81C91 817D F8 F3020000 cmp dword ptr ss:[ebp-0x8],0x2F3
00A81C98 74 05 je short bomb.00A81C9F
00A81C9A E8 4BF5FFFF call bomb.00A811EA ; boom
00A81C9F EB 6B jmp short bomb.00A81D0C
00A81CA1 C645 E3 6B mov byte ptr ss:[ebp-0x1D],0x6B
00A81CA5 817D F8 FB000000 cmp dword ptr ss:[ebp-0x8],0xFB
00A81CAC 74 05 je short bomb.00A81CB3
00A81CAE E8 37F5FFFF call bomb.00A811EA ; boom
00A81CB3 EB 57 jmp short bomb.00A81D0C
00A81CB5 C645 E3 6F mov byte ptr ss:[ebp-0x1D],0x6F
00A81CB9 817D F8 A0000000 cmp dword ptr ss:[ebp-0x8],0xA0
00A81CC0 74 05 je short bomb.00A81CC7
00A81CC2 E8 23F5FFFF call bomb.00A811EA ; boom
00A81CC7 EB 43 jmp short bomb.00A81D0C
00A81CC9 C645 E3 74 mov byte ptr ss:[ebp-0x1D],0x74
00A81CCD 817D F8 CA010000 cmp dword ptr ss:[ebp-0x8],0x1CA
00A81CD4 74 05 je short bomb.00A81CDB
00A81CD6 E8 0FF5FFFF call bomb.00A811EA ; boom
00A81CDB EB 2F jmp short bomb.00A81D0C
00A81CDD C645 E3 76 mov byte ptr ss:[ebp-0x1D],0x76
00A81CE1 817D F8 0C030000 cmp dword ptr ss:[ebp-0x8],0x30C
00A81CE8 74 05 je short bomb.00A81CEF
00A81CEA E8 FBF4FFFF call bomb.00A811EA ; boom
00A81CEF C645 E3 62 mov byte ptr ss:[ebp-0x1D],0x62
00A81CF3 817D F8 0C020000 cmp dword ptr ss:[ebp-0x8],0x20C
00A81CFA 74 05 je short bomb.00A81D01
00A81CFC E8 E9F4FFFF call bomb.00A811EA ; boom
00A81D01 EB 09 jmp short bomb.00A81D0C
00A81D03 C645 E3 78 mov byte ptr ss:[ebp-0x1D],0x78
00A81D07 E8 DEF4FFFF call bomb.00A811EA ; boom
00A81D0C 0FBE45 EF movsx eax,byte ptr ss:[ebp-0x11]
00A81D10 0FBE4D E3 movsx ecx,byte ptr ss:[ebp-0x1D]
00A81D14 3BC1 cmp eax,ecx
00A81D16 74 05 je short bomb.00A81D1D
00A81D18 E8 CDF4FFFF call bomb.00A811EA ; boom
00A81D1D 52 push edx

这串代码看起来很长,但其实就是一个跳转表而已,估计是if else实现的。所以我们只要用F7单步执行走下去就可以找到答案。

在每个cmp处可以看到程序在和我们输入的第几个参数进行比较,拿小本本记下来,改改我们的输入参数,就可以得到其中一个答案。(答案应该有好几个)

这里经过调试得到的答案是7 b 524。

第四个BOMB-递归

前面的步骤和以上两题相同,此题要求输入一个整数。这里找到boom的入口点

Code
1
2
3
4
5
00A81E61    E8 08F2FFFF           call bomb.00A8106E     ; 本题核心函数
00A81E66 83C4 04 add esp,0x4
00A81E69 83F8 37 cmp eax,0x37
00A81E6C 74 05 je short bomb.00A81E73
00A81E6E E8 77F3FFFF call bomb.00A811EA

这里call了一个函数,函数结束后将返回值与0x37也就是55比较,也就是函数返回值结果为55才会跳过boom,可以把EIP寄存器改成0x00A81E73,点运行可以看到是成功的,所以现在我们的目标就是让所调用的函数返回值为55就OK了。

按两下Enter追踪一下,就是这题的核心代码了。

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
00A816C0    55                    push ebp
00A816C1 8BEC mov ebp,esp
00A816C3 81EC C0000000 sub esp,0xC0
00A816C9 53 push ebx
00A816CA 56 push esi
00A816CB 57 push edi
00A816CC 8DBD 40FFFFFF lea edi,dword ptr ss:[ebp-0xC0]
00A816D2 B9 30000000 mov ecx,0x30
00A816D7 B8 CCCCCCCC mov eax,0xCCCCCCCC
00A816DC F3:AB rep stos dword ptr es:[edi]
00A816DE 837D 08 01 cmp dword ptr ss:[ebp+0x8],0x1
00A816E2 7E 24 jle short bomb.00A81708
00A816E4 8B45 08 mov eax,dword ptr ss:[ebp+0x8] ; bomb.00A8A790
00A816E7 83E8 01 sub eax,0x1
00A816EA 50 push eax
00A816EB E8 7EF9FFFF call bomb.00A8106E ; 调用f(n-1)
00A816F0 83C4 04 add esp,0x4
00A816F3 8BF0 mov esi,eax ; 将结果暂存到esi寄存器中,一会再用
00A816F5 8B4D 08 mov ecx,dword ptr ss:[ebp+0x8] ; bomb.00A8A790
00A816F8 83E9 02 sub ecx,0x2
00A816FB 51 push ecx
00A816FC E8 6DF9FFFF call bomb.00A8106E ; 调用f(n-2)
00A81701 83C4 04 add esp,0x4
00A81704 03C6 add eax,esi ; 把f(n-1)和f(n-2)相加并作为返回值
00A81706 EB 05 jmp short bomb.00A8170D
00A81708 B8 01000000 mov eax,0x1
00A8170D 5F pop edi
00A8170E 5E pop esi
00A8170F 5B pop ebx
00A81710 81C4 C0000000 add esp,0xC0
00A81716 3BEC cmp ebp,esp
00A81718 E8 78FAFFFF call bomb.00A81195
00A8171D 8BE5 mov esp,ebp
00A8171F 5D pop ebp
00A81720 C3 retn

看到0x00A816EB和0x00A816FC处,这里是两次调用了自己,判断出这是一个递归,这两次调用传入的参数是什么呢?往回倒,第一次是当前传入参数-1,第二次是当前传入参数-2,调用完以后,0x00A81704处把两个结果相加作为返回值。所以递推式为

a(n)=a(n-1)+a(n-2)

这有点像斐波那契数列。再验证一下递归出口,0x00A816DE处和下面一条,是一个if,当传入参数小于或等于1时,跳转到0x00A81708,返回1。很明显了,当n=0或者1的时候直接返回1,加上之前的递推式,只能是斐波那契数列了啊!

从第0项开始,斐波那契数列第9项为55,好了,第四个炸弹拆除了。

第五个BOMB-加密

前面步骤都一样就不说了,这题虽然没有明确提示我们要输入一个字符串,但是在乱输一通之后还是有一些比较奇妙的发现,比如寄存器里面以unicode编码方式显示了输入,大概说明是需要一个字符串了。

这里是boom的入口点

Code
1
2
3
4
5
6
00A81F0C    E8 3EF2FFFF           call bomb.00A8114F                       ; 判断字符串长度为6个字符
00A81F11 83C4 04 add esp,0x4
00A81F14 83F8 06 cmp eax,0x6
00A81F17 74 05 je short bomb.00A81F1E
00A81F19 E8 CCF2FFFF call bomb.00A811EA ; boom
00A81F1E C745 E4 00000000 mov dword ptr ss:[ebp-0x1C],0x0

追踪到第一行的call,F7单步执行,观察eax寄存器的变化,发现是一个取字符串长度的strlen函数,不知道为什么OD没有给出相应的提示。

那么这里意图就很明显了,是在判断我们输入的字符串长度是否为6,没有就boom。所以我们要输入的字符串长度为6。接着往下拉

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
00A81F27    8B45 E4               mov eax,dword ptr ss:[ebp-0x1C]
00A81F2A 83C0 01 add eax,0x1
00A81F2D 8945 E4 mov dword ptr ss:[ebp-0x1C],eax ; bomb.00A8A7E0
00A81F30 837D E4 05 cmp dword ptr ss:[ebp-0x1C],0x5
00A81F34 7F 1B jg short bomb.00A81F51
00A81F36 8B45 08 mov eax,dword ptr ss:[ebp+0x8] ; bomb.00A819C8
00A81F39 0345 E4 add eax,dword ptr ss:[ebp-0x1C]
00A81F3C 0FBE08 movsx ecx,byte ptr ds:[eax]
00A81F3F 83E1 0F and ecx,0xF ; 输入的第i个字符的ASCII码对16取余
00A81F42 8B55 E4 mov edx,dword ptr ss:[ebp-0x1C]
00A81F45 8A81 FCA0A800 mov al,byte ptr ds:[ecx+0xA8A0FC] ; 取余结果作为下标,取串isrveawhobpnutfg中对应的字符
00A81F4B 884415 F0 mov byte ptr ss:[ebp+edx-0x10],al ; 拼接一下字符变成字符串
00A81F4F ^ EB D6 jmp short bomb.00A81F27
00A81F51 B8 01000000 mov eax,0x1
00A81F56 6BC0 06 imul eax,eax,0x6 ; bomb.00A8A7E0
00A81F59 8985 18FFFFFF mov dword ptr ss:[ebp-0xE8],eax ; bomb.00A8A7E0
00A81F5F 83BD 18FFFFFF 08 cmp dword ptr ss:[ebp-0xE8],0x8
00A81F66 73 02 jnb short bomb.00A81F6A
00A81F68 EB 05 jmp short bomb.00A81F6F
00A81F6A E8 5DF2FFFF call bomb.00A811CC
00A81F6F 8B8D 18FFFFFF mov ecx,dword ptr ss:[ebp-0xE8]
00A81F75 C6440D F0 00 mov byte ptr ss:[ebp+ecx-0x10],0x0
00A81F7A 68 187AA800 push bomb.00A87A18 ; giants
00A81F7F 8D45 F0 lea eax,dword ptr ss:[ebp-0x10]
00A81F82 50 push eax ; 我们输入的字符串转化后的结果
00A81F83 E8 30F2FFFF call bomb.00A811B8 ; 字符串比较
00A81F88 83C4 08 add esp,0x8
00A81F8B 85C0 test eax,eax ; bomb.00A8A7E0
00A81F8D 74 05 je short bomb.00A81F94
00A81F8F E8 56F2FFFF call bomb.00A811EA ; boom
00A81F94 52 push edx

看到这里,已经有两个很明显的字符串在那摆着了。0x00A81F45的 isrveawhobpnutfg 和0x00A81F7A的 giants 。无论flag是不是它们两个的其中一个,但是肯定跟它们有关联!

我也表示不能完全看懂汇编代码,但是我们是在动态调试啊,那就执行呗,同样地上F7单步调试。

0x00A81F27到0x00A81F4F是一个循环,条件是某个计数变量从零开始自增的值小于等于5,因为我们的字符串长度是6,所以估计是对字符串的每一位进行操作?

好了,观察寄存器变化,发现在0x00A81F3C处程序会把输入字符串的第i个字符(i是当前循环次数)拿出来,在下一行与0xF进行按位与操作,或者说对16取余。再往下,就是把取余的结果作为下标,在0x00A81F45处取出 isrveawhobpnutfg 中对应下标的字符,拼接起来。

下面的代码看得还不是很清楚,但可以猜想下面call的是一个字符串比较函数,所以拼接得到的结果应该是 giants

我们倒推一下,giants几个字母在上面玄学字符串里面的位置分别是15、0、5、11、13、1,十六进制是F05BD1,所以对于输入的六个字符的要求是ASCII码的十六进制表示低位分别是F05BD1。查阅ASCII码表,可以选择一组合适的输入。我的输入是opukma。

第六个BOMB-排序、链表

这次的输入格式和第二次一样,也是6个整数,似乎连调用的函数都是一样的。

但是进入到核心代码以后就发现比较懵逼了,一共有4个循环,其中前两个循环是双层的。一共找到3个会爆炸的地方,一个在循环1的外层,另一个在循环1的内层,最后一个在循环4里面。试过把EIP寄存器直接改到循环4之后可以直接通关,所以我们只要完整走完四个循环就OK了。先来看看第一个循环

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
00A8205F    C745 A8 00000000      mov dword ptr ss:[ebp-0x58],0x0
00A82066 8B45 A8 mov eax,dword ptr ss:[ebp-0x58]
00A82069 8B4C85 E0 mov ecx,dword ptr ss:[ebp+eax*4-0x20] ; 输入的第i个参数
00A8206D 83E9 01 sub ecx,0x1
00A82070 83F9 05 cmp ecx,0x5 ; 要求减一后小于等于5,也就是本身小于等于6
00A82073 76 05 jbe short bomb.00A8207A
00A82075 E8 70F1FFFF call bomb.00A811EA ; boom
00A8207A 8B45 A8 mov eax,dword ptr ss:[ebp-0x58]
00A8207D 83C0 01 add eax,0x1
00A82080 8945 9C mov dword ptr ss:[ebp-0x64],eax
00A82083 EB 09 jmp short bomb.00A8208E
00A82085 8B45 9C mov eax,dword ptr ss:[ebp-0x64] ; 内层循环开始
00A82088 83C0 01 add eax,0x1
00A8208B 8945 9C mov dword ptr ss:[ebp-0x64],eax
00A8208E 837D 9C 05 cmp dword ptr ss:[ebp-0x64],0x5
00A82092 7F 17 jg short bomb.00A820AB
00A82094 8B45 A8 mov eax,dword ptr ss:[ebp-0x58] ; i
00A82097 8B4D 9C mov ecx,dword ptr ss:[ebp-0x64] ; j
00A8209A 8B5485 E0 mov edx,dword ptr ss:[ebp+eax*4-0x20] ; 输入的第i个参数
00A8209E 3B548D E0 cmp edx,dword ptr ss:[ebp+ecx*4-0x20] ; 和输入的第j个参数进行对比
00A820A2 75 05 jnz short bomb.00A820A9 ; 相等就炸了
00A820A4 E8 41F1FFFF call bomb.00A811EA ; boom
00A820A9 ^ EB DA jmp short bomb.00A82085
00A820AB 8B45 A8 mov eax,dword ptr ss:[ebp-0x58]
00A820AE 83C0 01 add eax,0x1
00A820B1 8945 A8 mov dword ptr ss:[ebp-0x58],eax
00A820B4 837D A8 05 cmp dword ptr ss:[ebp-0x58],0x5
00A820B8 ^ 7E AC jle short bomb.00A82066 ; 判断六个参数互不相等,并且小于等于6的循环

运行的时候查看寄存器可以发现ebp-0x58和ebp-0x64两个地址里面存的是两个局部计数变量,记为i和j。主要看到0x00A82070和0x00A820A2两个判断,第一个判断是让输入的第i个参数减一后与5做比较,小于等于5才跳过boom,这就要求每个输入参数都小于等于6;第二个判断是拿输入的第i和第j个参数进行对比,不等的时候跳转,这就要求每个输入的参数互不相等。所以通过这段代码,我们可以得出的结论是:输入的六个数都要小于等于6,并且互不相等。也就是说答案只可能是1~6的全排列其中的一个。这个范围,一共有120种可能,已经可以用枚举法把答案试一遍了吧。

第二和第三个循环看得我是一脸懵逼,但是里面是没有call boom的,所以为什么不直接跳到第四个会boom的循环呢?这才是动态分析的精髓啊(放屁)!来吧,我们把1 2 3 4 5 6作为输入,跳到第四个循环。

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
00A8214E    C745 A8 00000000      mov dword ptr ss:[ebp-0x58],0x0
...
00A82164 8B45 B4 mov eax,dword ptr ss:[ebp-0x4C] ;
00A82167 8B48 08 mov ecx,dword ptr ds:[eax+0x8]
00A8216A 8B55 B4 mov edx,dword ptr ss:[ebp-0x4C] ;
00A8216D 8B02 mov eax,dword ptr ds:[edx] ; 当前结点数据域
00A8216F 3B01 cmp eax,dword ptr ds:[ecx] ; 和下一个结点数据域对比
00A82171 7D 05 jge short bomb.00A82178 ; 大于就通过
00A82173 E8 72F0FFFF call bomb.00A811EA ; boom
00A82178 8B45 B4 mov eax,dword ptr ss:[ebp-0x4C] ;
00A8217B 8B48 08 mov ecx,dword ptr ds:[eax+0x8]
00A8217E 894D B4 mov dword ptr ss:[ebp-0x4C],ecx ;
00A82181 8B45 A8 mov eax,dword ptr ss:[ebp-0x58]
00A82184 83C0 01 add eax,0x1
00A82187 8945 A8 mov dword ptr ss:[ebp-0x58],eax
00A8218A 837D A8 04 cmp dword ptr ss:[ebp-0x58],0x4
00A8218E ^ 7E D4 jle short bomb.00A82164

先看代码,应该可以直接看出来每次自增的变量[ebp-0x58]是一个计数变量,从0开始,到大于4结束。

在0x00A8214E断一下,监测一下[ebp-0x4C],发现这时候是一个很大的值0x00A8A030,看起来有点像地址。我们可以在数据段窗口里面CTRL+G跳转一下到这个地址。目前这个地址接下来4个字节的数据为D5 02 00 00,由于是小端存储,所以它的值应该为0x2D5。单步执行一下,下一条语句访问了这个地址偏移8后的位置的值,将这个值放入ecx寄存器中。看看寄存器,取到的值为0x00A8A024。如果它是一个地址的话,那离我们刚才这个地址有点近啊,相差12个字节,可以看看。

12个字节算是一个提示吧,我们把这从0x00A8A024开始的12个字节作为整体一起看看。四个字节作为一个单位,可以分成3个值,0x12D 3 0xA8A018,再把0x00A8A030重新看看,可以发现也是类似的结构 0x2D5 2 0x00A8A024,感觉像是连续分配的结构体空间,后面像带了一个地址,所以应该是一个链表的结点。把附近的内存再看看,可以看到从0x00A8A000开始大家都是一样的结构,并且有6个这样的结构。

这六个结构里面,第一个dword是一个不大不小的数,像是数据域,第二个是16的整数,随着地址增加是顺序是从61。第三个是一个地址,如果是链表的话那应该是指向下一个结点。

ADDRESS 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00A8A000 B0 01 00 00 06 00 00 00 00 00 00 00 D4 00 00 00
00A8A010 05 00 00 00 00 A0 A8 00 E5 03 00 00 04 00 00 00
00A8A020 0C A0 A8 00 2D 01 00 00 03 00 00 00 18 A0 A8 00
00A8A030 D5 02 00 00 02 00 00 00 24 A0 A8 00 FD 00 00 00
00A8A040 01 00 00 00 30 A0 A8 00

下面一条语句又重新访问了刚才的0x00A8A030,然后再把这个值放入到edx寄存器中。

所以现在的不boom的条件是访问到的结点的数据域的值要大于它下一个结点的,就是说这个链表的数据域要单调递减。

重新观察一下内存,我们发现上面的每个结点都指向它上一个紧邻它的结点。为了直观一些,把这6个结点的顺序列一下:

  • 1 0xFD
  • 2 0x2D5
  • 3 0x12D
  • 4 0x3E5
  • 5 0xD4
  • 6 0x1B0

想了一下,1 2 3 4 5 6是我们的输入顺序啊,那如果改一下顺序会怎么样?

为了满足上面的数据域递减的条件,排个序就是变成这样

  • 4 0x3E5
  • 2 0x2D5
  • 6 0x1B0
  • 3 0x12D
  • 1 0xFD
  • 5 0xD4

那我们来试试这个输入顺序吧

4 2 6 3 1 5

结果竟然就pass了!动态分析大法好啊~

那么现在倒推回去,就可以猜到上面两个循环在干的事情,应该就是把这6个结点的指针按照我们的输入顺序排序了。

游戏结束!(假的)

隐藏关-递归

听破解完的舍友说这玩意有隐藏关,那我就去找找吧。

打开字符串常量搜索引擎,浏览一下,发现有个字符串带有secret这个关键词

Curses, you’ve found the secret phase!\n

追踪一下,就可以看到这一片代码了。看到这片代码的其中一个跳转:

Code
1
2
00A82298    833D 80A2A800 06      cmp dword ptr ds:[0xA8A280],0x6
00A8229F 0F85 92000000 jnz bomb.00A82337

当传入的参数为6时,不跳转。而这个输入参数可以猜测为是第几关,由此得出第六关的时候才有可能进入隐藏关卡。

接下来又看到一个scanf,输入参数是一个整数和一个字符串。

Code
1
2
3
4
5
6
00A822AF    68 487BA800           push bomb.00A87B48                       ; %d %s
00A822B4 BA 50000000 mov edx,0x50
00A822B9 6BD2 03 imul edx,edx,0x3
00A822BC 81C2 A0A6A800 add edx,bomb.00A8A6A0 ; ASCII "Public speaking is very easy."
00A822C2 52 push edx
00A822C3 FF15 ECB2A800 call dword ptr ds:[<&MSVCR110D.sscanf>] ; msvcr110.sscanf

中间的三行代码翻译成高级语言大概是:

c
1
2
3
edx = 50;
edx = edx * 3;
edx = edx + 0xA8A6A0;

所以edx最后的值应该为0xA8A790,去追踪一下这个地址:

图6

我觉得有点好玩,这完全就是前几关的输入啊!9是第四关的输入,所以我们应该在第四关的输入后面加一点什么。往下看,又是一个合法性判断,再往下

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
00A822D8    68 507BA800           push bomb.00A87B50                       ; austinpowers
00A822DD 8D45 A8 lea eax,dword ptr ss:[ebp-0x58]
00A822E0 50 push eax
00A822E1 E8 D2EEFFFF call bomb.00A811B8 ; strcmp
00A822E6 83C4 08 add esp,0x8
00A822E9 85C0 test eax,eax
00A822EB 75 33 jnz short bomb.00A82320
00A822ED 8BF4 mov esi,esp
00A822EF 68 607BA800 push bomb.00A87B60 ; Curses, you've found the secret phase!\n
00A822F4 FF15 F0B2A800 call dword ptr ds:[<&MSVCR110D.printf>] ; msvcr110.printf
00A822FA 83C4 04 add esp,0x4
00A822FD 3BF4 cmp esi,esp
00A822FF E8 91EEFFFF call bomb.00A81195
00A82304 8BF4 mov esi,esp
00A82306 68 907BA800 push bomb.00A87B90 ; But finding it and solving it are quite different...\n
00A8230B FF15 F0B2A800 call dword ptr ds:[<&MSVCR110D.printf>] ; msvcr110.printf

前面已经标记了那个call是一个字符串比较函数,上面又有一个字符串,所以在第四关后面输入那个字符串就进入了隐藏关。

进入隐藏关后,先是需要一个输入,然后下面调用了一个叫strtol的函数

Code
1
2
3
4
5
6
7
8
9
10
00A8268C    8B45 F8               mov eax,dword ptr ss:[ebp-0x8]
00A8268F 50 push eax ; bomb.00A8A880
00A82690 FF15 E0B2A800 call dword ptr ds:[<&MSVCR110D.strtol>] ; msvcr110.strtol
00A82696 83C4 0C add esp,0xC
...
00A826A6 83E8 01 sub eax,0x1
00A826A9 3D E8030000 cmp eax,0x3E8
00A826AE 76 05 jbe short bomb.00A826B5 ; 要求输入小于等于1001
00A826B0 E8 35EBFFFF call bomb.00A811EA
00A826B5 8B45 EC mov eax,dword ptr ss:[ebp-0x14]

去查了一下,这个函数作用跟atoi差不多,暂且把它理解为提取字符串中的数。下面又是一个比较,要求转化后的整数值小于等于1001,否则要炸。

Code
1
2
3
4
5
6
7
8
9
10
11
00A826B5    8B45 EC               mov eax,dword ptr ss:[ebp-0x14]
00A826B8 50 push eax
00A826B9 68 F0A0A800 push bomb.00A8A0F0 ; 地址内的值为24
00A826BE E8 45EBFFFF call bomb.00A81208
00A826C3 83C4 08 add esp,0x8
00A826C6 83F8 07 cmp eax,0x7 ; 要求函数返回值为7
00A826C9 74 05 je short bomb.00A826D0
00A826CB E8 1AEBFFFF call bomb.00A811EA
00A826D0 8BF4 mov esi,esp
00A826D2 68 207AA800 push bomb.00A87A20 ; Wow! You've defused the secret stage!\n
00A826D7 FF15 F0B2A800 call dword ptr ds:[<&MSVCR110D.printf>] ; msvcr110.printf

接下来是一个函数调用,调用完成后拿返回值和7做对比,不等就boom,相等就pass了。所以把这个call的函数读懂,就可以找到答案了。首先确定这个函数有两个参数,其一为我们输入的数,记为a,其二为它给出的一个地址,这个地址里面目前的值为0x24,把这个地址里面的值记为b吧。追踪到这个函数里面,进行单步调试。

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
00A8162E    837D 08 00            cmp dword ptr ss:[ebp+0x8],0x0
00A81632 75 05 jnz short bomb.00A81639
00A81634 83C8 FF or eax,-0x1 ; 如果数据域为0则将eax置为0xFFFFFF然后返回
00A81637 EB 4A jmp short bomb.00A81683
00A81639 8B45 08 mov eax,dword ptr ss:[ebp+0x8]
00A8163C 8B4D 0C mov ecx,dword ptr ss:[ebp+0xC] ; 输入的数
00A8163F 3B08 cmp ecx,dword ptr ds:[eax]
00A81641 7D 19 jge short bomb.00A8165C
00A81643 8B45 0C mov eax,dword ptr ss:[ebp+0xC]
00A81646 50 push eax
00A81647 8B4D 08 mov ecx,dword ptr ss:[ebp+0x8]
00A8164A 8B51 04 mov edx,dword ptr ds:[ecx+0x4]
00A8164D 52 push edx
00A8164E E8 B5FBFFFF call bomb.00A81208
00A81653 83C4 08 add esp,0x8
00A81656 D1E0 shl eax,1
00A81658 EB 29 jmp short bomb.00A81683
00A8165A EB 27 jmp short bomb.00A81683
00A8165C 8B45 08 mov eax,dword ptr ss:[ebp+0x8]
00A8165F 8B4D 0C mov ecx,dword ptr ss:[ebp+0xC]
00A81662 3B08 cmp ecx,dword ptr ds:[eax]
00A81664 7E 1B jle short bomb.00A81681
00A81666 8B45 0C mov eax,dword ptr ss:[ebp+0xC]
00A81669 50 push eax
00A8166A 8B4D 08 mov ecx,dword ptr ss:[ebp+0x8]
00A8166D 8B51 08 mov edx,dword ptr ds:[ecx+0x8]
00A81670 52 push edx
00A81671 E8 92FBFFFF call bomb.00A81208
00A81676 83C4 08 add esp,0x8
00A81679 8D4400 01 lea eax,dword ptr ds:[eax+eax+0x1]
00A8167D EB 04 jmp short bomb.00A81683
00A8167F EB 02 jmp short bomb.00A81683
00A81681 33C0 xor eax,eax
00A81683 5F pop edi ; bomb.00A826C3
00A81684 5E pop esi ; bomb.00A826C3
00A81685 5B pop ebx ; bomb.00A826C3
00A81686 81C4 C0000000 add esp,0xC0
00A8168C 3BEC cmp ebp,esp
00A8168E E8 02FBFFFF call bomb.00A81195 ;
00A81693 8BE5 mov esp,ebp
00A81695 5D pop ebp ; bomb.00A826C3
00A81696 C3 retn

看到有两个call自己的地方,已经意识到这里又是递归了。首先来分析一下函数的整体结构。

首先是0x00A8162E处的比较,这里比较的是传入的b是否为0,如果为0就返回-1。

然后下面还有两个判断,在0x00A8163F和0x00A81662处,这里是判断传入的两个参数a和b谁大谁小,如果相等就返回0。

讲到这里,其实还没有细看a>b和b>a的情况分别发生了什么。但是我对前面在调用之前给出的地址有点感兴趣,可以在数据段窗口追踪一下。

图7

这里的数据有点乱,但是我们可以看到,这里还像是一个数据加两个指针。找到刚才的0x00A8A0F0,假设这是一个双向链表吧,(似乎并不是,反正后面看上去有点像个交叉链表)偏移4个单位是prev指针,偏移8个单位是next指针,那prev链和next链的数据域如下:

prev: 0x24->0x8->0x6->0x1

next: 0x24->0x32->0x6B->0x3E9

虽然不知道这个有什么用,后面再看看吧。接下来就可以单步调试看看数据的变化了。

我们输入一个值,比如1000,首先进入了第一个判断,拿我们输入的数和它刚才给的0x24进行比较,大了,跳转到下面。发现接下来发生的事情是把0x32的地址和我们输入的数压栈,再递归调用。看到下面0x00A81679一句,这里翻译成C语言就是

c
1
eax = eax * 2 + 1;

eax存储的是调用的返回值,所以我们可以得到一个不完整的递推关系式

c
1
2
3
4
5
6
7
8
9
10
11
int f(int a, Node* bp) {
int b = bp->data;
if(b == 0)
return -1;
if(a > b)
return f(a, bp->next->data) * 2 + 1;
if(a == b)
return 0;
if(a < b)
//还没读代码
}

其实到了这里已经可以不用去读a<b的代码了,我们稍微算一下,就可以算出当递归达到第三层的时候,只要递归出口为return 0那一条,就可以得到返回值7。怎么达到递归出口呢?很明显,当我们输入的值为第三层递归传入的参数的数据即a==b时候,返回0。第三层递归,b的传入值应该为0x3E9,十进制为1001,所以这题的答案为1001。

后语

好了,这下游戏真的结束了。

整个过程大概花了我两天时间,感觉相比之下还是太长了。反正嘛,我是没有去咖啡店通宵续杯挖炸弹的精力的,狗命要紧。

拆完这7个炸弹,基本上对IA-32体系结构的汇编语言和代码执行过程有一定了解了。总结一下,这七道题的知识点主要包括一些逆向基础、指针、分支、循环、递归、索引、排序、结构体和链表。最后我还是没有直接使用IDA进行静态分析直接反编译生成C语言代码,因为……我不会用啊~

心黑鸭.jpg

最后,心疼一下我家心黑鸭,从第一关到隐藏关,一刻不停地盯着电脑屏幕,不离不弃。下次debug一定还要带上ta!

最后的最后,祝大家hacking愉快!

文章作者: LightingX
文章链接: http://lightingx.top/blog/2018/09/22/CSAPP%20BinaryBomb%E6%8B%86%E5%BC%B9%E8%AE%B0%E5%BD%95/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 LightingX

评论