CSAPP:Bomb Lab

目录

实验简介

实验前准备

phase_1: 字符串比较

phase_2:循环

phase_3:条件/分支

phase_4:函数调用(递归调用)

phase_5:指针

phase_6:链表/结构体

隐藏阶段

最终答案

总结

参考资料


实验简介

bomb lab是csapp课程的一个lab,实验目标是通过输入一系列的字符串以保证程序正确执行并正常退出,要保证不触发炸弹。

实验前准备

要先大概读一遍csapp的第三章。

要了解一些gdb命令使用,过程中我用到了下面这些命令:

  • x:查看内存处的值
  • i:i r reg可以查看寄存器的值
  • b: b + 地址值可以在相应位置打断点

然后需要了解下x86-64下的函数调用惯例,这里放几个mooc的图

12e221f828f44a7e99ef024edc2766a3.png

ae4b4bf5ea7742d1bc10af7f4a051a3c.png

a18db4af68784905ba9d0d40bae2af48.png

phase_1: 字符串比较

首先阅读以下bomb.c,可以发现程序每个阶段要我们给一个输入,然后通过这个输入去执行相应阶段的函数phase_xx。

这里先反汇编可执行程序得到对应的汇编代码,

objdump -d bomb > bomb.S

phase_1函数的汇编代码如下:

0000000000400ee0 <phase_1>:
  400ee0:	48 83 ec 08          	sub    $0x8,%rsp
  400ee4:	be 00 24 40 00       	mov    $0x402400,%esi   # str2
  400ee9:	e8 4a 04 00 00       	call   401338 <strings_not_equal>
  400eee:	85 c0                	test   %eax,%eax        # 检查返回结果是否为0
  400ef0:	74 05                	je     400ef7 <phase_1+0x17>
  400ef2:	e8 43 05 00 00       	call   40143a <explode_bomb>
  400ef7:	48 83 c4 08          	add    $0x8,%rsp
  400efb:	c3                   	ret    

可以看到phase_1逻辑是将用户输入的字符串和地址0x402400处的字符串比较,如果相等就直接返回,否则会触发炸弹。这里用gdb查看0x402400处的字符串:

ad38d8a300804b1ca5f229c3080f017d.png所以phase_1需要输入的字符串是:

Border relations with Canada have never been better.

phase_2:循环

phase_2的开头一段逻辑,调用了read_six_numbers这个函数,所以phase_2要输入的字符串应该是6个数:

  400efc:	55                   	push   %rbp
  400efd:	53                   	push   %rbx
  400efe:	48 83 ec 28          	sub    $0x28,%rsp
  400f02:	48 89 e6             	mov    %rsp,%rsi
  400f05:	e8 52 05 00 00       	call   40145c <read_six_numbers>

输入的数是什么类型的?看一下read_six_numbers的实现,使用了sscanf来解析输入的数

000000000040145c <read_six_numbers>:
  40145c:	48 83 ec 18          	sub    $0x18,%rsp
  401460:	48 89 f2             	mov    %rsi,%rdx
  401463:	48 8d 4e 04          	lea    0x4(%rsi),%rcx
  401467:	48 8d 46 14          	lea    0x14(%rsi),%rax
  40146b:	48 89 44 24 08       	mov    %rax,0x8(%rsp)
  401470:	48 8d 46 10          	lea    0x10(%rsi),%rax
  401474:	48 89 04 24          	mov    %rax,(%rsp)
  401478:	4c 8d 4e 0c          	lea    0xc(%rsi),%r9
  40147c:	4c 8d 46 08          	lea    0x8(%rsi),%r8
  401480:	be c3 25 40 00       	mov    $0x4025c3,%esi # 格式字符串地址
  401485:	b8 00 00 00 00       	mov    $0x0,%eax
  40148a:	e8 61 f7 ff ff       	call   400bf0 <__isoc99_sscanf@plt>
  40148f:	83 f8 05             	cmp    $0x5,%eax
  401492:	7f 05                	jg     401499 <read_six_numbers+0x3d>
  401494:	e8 a1 ff ff ff       	call   40143a <explode_bomb>
  401499:	48 83 c4 18          	add    $0x18,%rsp
  40149d:	c3                   	ret    

sscanf的函数原型如下:

int sscanf(const char *str, const char *format, ...)

按照调用惯例来看,0x4025c3处存储的是格式化字符串,gdb看一下:

35f1b88d4a3c471b9004c754026d0b4b.png所以要输入六个int类型的整数。

再回到phase_2的逻辑,读到六个数之后返回之后,首先判断读到的第一个数是否为1,不为1会触发炸弹,所以输入的第一个数是1。

  400f0a:	83 3c 24 01          	cmpl   $0x1,(%rsp)
  400f0e:	74 20                	je     400f30 <phase_2+0x34>
  400f10:	e8 25 05 00 00       	call   40143a <explode_bomb>

接下来的一段代码逻辑是判断读取到的剩余数是否满足一个规律,即后一个数是前一个数的两倍,看如下代码:

  400f15:	eb 19                	jmp    400f30 <phase_2+0x34>
  400f17:	8b 43 fc             	mov    -0x4(%rbx),%eax       # 取前一个数
  400f1a:	01 c0                	add    %eax,%eax             # 前一个数乘以2
  400f1c:	39 03                	cmp    %eax,(%rbx)           # 与当前数比较
  400f1e:	74 05                	je     400f25 <phase_2+0x29> # 判断是否相等
  400f20:	e8 15 05 00 00       	call   40143a <explode_bomb>
  400f25:	48 83 c3 04          	add    $0x4,%rbx         # 指针后移
  400f29:	48 39 eb             	cmp    %rbp,%rbx         # 判断是否已经比较完读入的所有数,没有则继续比较
  400f2c:	75 e9                	jne    400f17 <phase_2+0x1b>
  400f2e:	eb 0c                	jmp    400f3c <phase_2+0x40>
  400f30:	48 8d 5c 24 04       	lea    0x4(%rsp),%rbx    # 取读到的第二个数
  400f35:	48 8d 6c 24 18       	lea    0x18(%rsp),%rbp   
  400f3a:	eb db                	jmp    400f17 <phase_2+0x1b>

这里可以尝试写一下phase_2的实现(不一定正确,仅供参考):

void phase_2(const char* input)
{
    int nums[6];
    read_six_numbers(nums);
    if(nums[0] != 1)
    {
        explode_bomb();
    }

    int i = 1;
    int pre = nums[i - 1];
    while(i < 6)
    {
        pre += pre;
        if(pre != nums[i])
        {
            explode_bomb();
        }
        ++ i;
    }
}

那么,在第一个数为1,且要输入六个数,并且满足后一个数是前一个数的两倍的情况下,答案为

1 2 4 8 16 32

phase_3:条件/分支

phase_3开头还是调用了sscanf:

  400f43:	48 83 ec 18          	sub    $0x18,%rsp
  400f47:	48 8d 4c 24 0c       	lea    0xc(%rsp),%rcx
  400f4c:	48 8d 54 24 08       	lea    0x8(%rsp),%rdx
  400f51:	be cf 25 40 00       	mov    $0x4025cf,%esi # 格式化字符串
  400f56:	b8 00 00 00 00       	mov    $0x0,%eax
  400f5b:	e8 90 fc ff ff       	call   400bf0 <__isoc99_sscanf@plt>
  400f60:	83 f8 01             	cmp    $0x1,%eax

还是看一下格式化字符串的内容,这里要求我们输入两个整数

657771d8f3964340a3664d17f742c175.png

继续往下看

400f60:	83 f8 01             	cmp    $0x1,%eax             # eax存储解析成功的数的个数
400f63:	7f 05                	jg     400f6a <phase_3+0x27> # 至少要输入两个数
400f65:	e8 d0 04 00 00       	call   40143a <explode_bomb>
400f6a:	83 7c 24 08 07       	cmpl   $0x7,0x8(%rsp)        # 输入的第一个数大于7就触发bomb
400f6f:	77 3c                	ja     400fad <phase_3+0x6a>
400f71:	8b 44 24 08          	mov    0x8(%rsp),%eax
400f75:	ff 24 c5 70 24 40 00 	jmp    *0x402470(,%rax,8)    # 在0x402470处取地址跳转

看这里一句 jmp *0x402470(,%rax,8),是书里面提到的计算goto的语法,从0x402470处取地址间接跳转。

ec21d4c07ee34ce39a980107b76d186b.png

switch语句开头先判断了数是否小于8,所以switch里面case的值应该在0到7之间,这里看一下0x402470处的8个四字内容。

6d7ffda2bf9c44f0b712fbfb012c9be6.png

可以看到这些地址与phase_3代码的地址对应:

42f8885a53fc4019a1dcf460212b563f.png

输入的第一个数是跳转表的索引,用于决定要跳转到哪里去执行,如果我们第一个数输入0,那会跳转到0x0000000000400f7c去执行,0x0000000000400f7c处将立即数0xcf存储到eax,然后与我们输入的第二个数比较,相等则正常返回,0xcf对应的十进制数是207。

所以phase_3的一个可选答案是

0 207

这里根据输入的第一个数的不同,还可以有其它答案,答案并不唯一。

phase_4:函数调用(递归调用)

phase_4开头还是经典的sscanf:

  40100c:	48 83 ec 18          	sub    $0x18,%rsp
  401010:	48 8d 4c 24 0c       	lea    0xc(%rsp),%rcx
  401015:	48 8d 54 24 08       	lea    0x8(%rsp),%rdx
  40101a:	be cf 25 40 00       	mov    $0x4025cf,%esi # 格式化字符串
  40101f:	b8 00 00 00 00       	mov    $0x0,%eax
  401024:	e8 c7 fb ff ff       	call   400bf0 <__isoc99_sscanf@plt>
  401029:	83 f8 02             	cmp    $0x2,%eax
  40102c:	75 07                	jne    401035 <phase_4+0x29>

开一下格式化字符串内容,还是要求输入两个整数。

f5aaca5f2d5940d4a37815b84a06041e.png

接下来判断输入的第一个数是否小于或等于0xe,即是否小于或等于15,大于15会触发炸弹

  40102e:	83 7c 24 08 0e       	cmpl   $0xe,0x8(%rsp)        # 判断输入的第一个数是否小于或等于15
  401033:	76 05                	jbe    40103a <phase_4+0x2e>
  401035:	e8 00 04 00 00       	call   40143a <explode_bomb>

接下来调用了func4这个函数,看起来这个函数有三个参数

  40103a:	ba 0e 00 00 00       	mov    $0xe,%edx       # arg3
  40103f:	be 00 00 00 00       	mov    $0x0,%esi       # arg2
  401044:	8b 7c 24 08          	mov    0x8(%rsp),%edi  # arg1
  401048:	e8 81 ff ff ff       	call   400fce <func4>

我只看了func4的前一段,发现func4里面又调用了func4,所以func4内存在递归调用,func4的递归出口可以是0x400ffb处如果满足条件就可以结束递归。第一次调用func4时的调用参数是func4(input1, 0, 15),经过开头的计算,得到c应该等于 15 >> 1 = 0x1111 >> 1 = 0x0111,即c等于7,此时要满足 eq?input%20%3C%3D%207才能跳转到0x400ffb处,用input1与c对比,如果input1满足eq?input%20%3E%3D%207就可以只调一次func4而不用递归就从func4返回。去上面两个不等式的交集,input1要等于7才就能一次都不递归就返回,此时func4返回值是0

7b8dc9c2386644fd9812c1f8bdca43a5.png

接下来看phase_4的返回值有无限制。再继续看phase_4的逻辑,可以看到剩余的代码里要求func4返回0,并且要求输入的第二个数是0

  401048:	e8 81 ff ff ff       	call   400fce <func4>        
  40104d:	85 c0                	test   %eax,%eax             # 校验func4返回值是否为0
  40104f:	75 07                	jne    401058 <phase_4+0x4c>
  401051:	83 7c 24 0c 00       	cmpl   $0x0,0xc(%rsp)        # 判断用户输入的第二个数是否为0
  401056:	74 05                	je     40105d <phase_4+0x51>
  401058:	e8 dd 03 00 00       	call   40143a <explode_bomb>

所以一个可选的答案是

7 0

phase_5:指针

phase_5开头第一段,求用户输入字符串的长度,要求输入的字符串长度为6

  401062:	53                   	push   %rbx
  401063:	48 83 ec 20          	sub    $0x20,%rsp
  401067:	48 89 fb             	mov    %rdi,%rbx
  40106a:	64 48 8b 04 25 28 00 	mov    %fs:0x28,%rax
  401071:	00 00 
  401073:	48 89 44 24 18       	mov    %rax,0x18(%rsp)
  401078:	31 c0                	xor    %eax,%eax
  40107a:	e8 9c 02 00 00       	call   40131b <string_length>
  40107f:	83 f8 06             	cmp    $0x6,%eax             # 要求字符串长度为6
  401082:	74 4e                	je     4010d2 <phase_5+0x70>
  401084:	e8 b1 03 00 00       	call   40143a <explode_bomb>

接下来的代码逻辑的解析如下,可以看到这里取了用户输入字符串的每个字符的低4位作为索引,然后从0x4024b0处取出六个字符,然后与0x40245e处的字符比较:

  40108b:	0f b6 0c 03          	movzbl (%rbx,%rax,1),%ecx    # 从输入的字符串里取字符
  40108f:	88 0c 24             	mov    %cl,(%rsp)            
  401092:	48 8b 14 24          	mov    (%rsp),%rdx
  401096:	83 e2 0f             	and    $0xf,%edx             # 字符值与0xf即0x1111按位与
  401099:	0f b6 92 b0 24 40 00 	movzbl 0x4024b0(%rdx),%edx   # 将按位与的结果作为索引从0x4024b0处取值
  4010a0:	88 54 04 10          	mov    %dl,0x10(%rsp,%rax,1) # 存储取到得值
  4010a4:	48 83 c0 01          	add    $0x1,%rax             # 索引后移准备取下一个字符
  4010a8:	48 83 f8 06          	cmp    $0x6,%rax             # 取够六个结束循环
  4010ac:	75 dd                	jne    40108b <phase_5+0x29>
  4010ae:	c6 44 24 16 00       	movb   $0x0,0x16(%rsp)
  4010b3:	be 5e 24 40 00       	mov    $0x40245e,%esi
  4010b8:	48 8d 7c 24 10       	lea    0x10(%rsp),%rdi       
  4010bd:	e8 76 02 00 00       	call   401338 <strings_not_equal> # 比较取得的字符串和0x40245e处的字符串
  4010c2:	85 c0                	test   %eax,%eax             # 不等则触发炸弹
  4010c4:	74 13                	je     4010d9 <phase_5+0x77>
  4010c6:	e8 6f 03 00 00       	call   40143a <explode_bomb>
  4010cb:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)
  4010d0:	eb 07                	jmp    4010d9 <phase_5+0x77>
  4010d2:	b8 00 00 00 00       	mov    $0x0,%eax

看看0x40245e处的字符串,也就是说用输入的六个字符的低四位作为索引,从0x4024b0处取出的六个字符是"flyers"

1404689614ac4518b364c1dd8d9cbccb.png

再看看0x4024b0处的内容,可以看到是一个字符串:

b6e10f42fa0b40c0b873ab2b62aefb17.png

要想从中取出flyers这个字符串,输入的六个字符的低4位按16进制表示应该分别是,9、F、E、5、6、7,这里可以找个ASCII码表看看:

30f6a885cd264a5fa13e7edcf72a255a.png

最后我选取的字符串是:

)?>%&'

按照ASCLL码表来看,答案并不唯一。

phase_6:链表/结构体

phase_6的代码逻辑比之前要复杂许多,这里分多段来看。

第一段代码的逻辑是读取六个数,然后要求这六个数两两不相等:

  4010f4:	41 56                	push   %r14
  4010f6:	41 55                	push   %r13
  4010f8:	41 54                	push   %r12
  4010fa:	55                   	push   %rbp
  4010fb:	53                   	push   %rbx
  4010fc:	48 83 ec 50          	sub    $0x50,%rsp
  401100:	49 89 e5             	mov    %rsp,%r13
  401103:	48 89 e6             	mov    %rsp,%rsi
  401106:	e8 51 03 00 00       	call   40145c <read_six_numbers>
  40110b:	49 89 e6             	mov    %rsp,%r14  # a = rsp;
  40110e:	41 bc 00 00 00 00    	mov    $0x0,%r12d # b = 0
  401114:	4c 89 ed             	mov    %r13,%rbp  # r13应该是6位数数组的首地址,第一个 循环 的入口,总结出来,第一段loop的逻辑是要求6个数都不相等。
  401117:	41 8b 45 00          	mov    0x0(%r13),%eax
  40111b:	83 e8 01             	sub    $0x1,%eax   # 取出第一个数
  40111e:	83 f8 05             	cmp    $0x5,%eax   # 剩下五个数
  401121:	76 05                	jbe    401128 <phase_6+0x34> # 要求读了一个数字后还有五个数字
  401123:	e8 12 03 00 00       	call   40143a <explode_bomb>
  401128:	41 83 c4 01          	add    $0x1,%r12d # b++           
  40112c:	41 83 fc 06          	cmp    $0x6,%r12d # 跟6比,
  401130:	74 21                	je     401153 <phase_6+0x5f> # 相等的话跳走,不然往下执行,应该是要循环六次, 循环结束的地方
  401132:	44 89 e3             	mov    %r12d,%ebx # ebx = b;
  401135:	48 63 c3             	movslq %ebx,%rax  # 符号扩展,rax = b;
  401138:	8b 04 84             	mov    (%rsp,%rax,4),%eax # 取出读取的其他数
  40113b:	39 45 00             	cmp    %eax,0x0(%rbp) # 要求输入的六个数两两不相同
  40113e:	75 05                	jne    401145 <phase_6+0x51>
  401140:	e8 f5 02 00 00       	call   40143a <explode_bomb>
  401145:	83 c3 01             	add    $0x1,%ebx # b++;
  401148:	83 fb 05             	cmp    $0x5,%ebx # 
  40114b:	7e e8                	jle    401135 <phase_6+0x41>
  40114d:	49 83 c5 04          	add    $0x4,%r13
  401151:	eb c1                	jmp    401114 <phase_6+0x20>   # 第一个循环的结尾

第二段看起来有点奇怪,用7减去输入的数,然后存回去

  401160:	89 ca                	mov    %ecx,%edx # edx为7  # 第二个循环开始
  401162:	2b 10                	sub    (%rax),%edx # 7减去数组元素的值
  401164:	89 10                	mov    %edx,(%rax) # 结果存放回数组
  401166:	48 83 c0 04          	add    $0x4,%rax   # 指针后移
  40116a:	48 39 f0             	cmp    %rsi,%rax   # 遍历完数组时rsi和rax指向同一个地方
  40116d:	75 f1                	jne    401160 <phase_6+0x6c>  # 第二个循环结束

第三段循环这里,有两种情况,对于输入的数小于或等于1的情况直接从0x6032d0处取了32位值放到数组处,对于数组元素值大于1的情况则会把rdx让后移,直到找到的值与第i位输入的数相等为止。

 401176:	48 8b 52 08          	mov    0x8(%rdx),%rdx    # 第三个循环开始,取rdx加8处的内容存回rdx,大于1的逻辑,这里是找到以数组元素值为索引的节点,将其对应后继节点的地址存放到数组
  40117a:	83 c0 01             	add    $0x1,%eax         # eax++;  第三个循环的作用是获取所有对应索引处节点的地址。
  40117d:	39 c8                	cmp    %ecx,%eax         # 数组元素值和索引相比,不等于则循环,
  40117f:	75 f5                	jne    401176 <phase_6+0x82>
  401181:	eb 05                	jmp    401188 <phase_6+0x94>
  401183:	ba d0 32 60 00       	mov    $0x6032d0,%edx        # 数组元素值小于1的逻辑
  401188:	48 89 54 74 20       	mov    %rdx,0x20(%rsp,%rsi,2) # rdx的值放到相应数组处
  40118d:	48 83 c6 04          	add    $0x4,%rsi              # 取下一个数组元素值,
  401191:	48 83 fe 18          	cmp    $0x18,%rsi             # 判断是否取完了。
  401195:	74 14                	je     4011ab <phase_6+0xb7>
  401197:	8b 0c 34             	mov    (%rsp,%rsi,1),%ecx   # 取数组元素值
  40119a:	83 f9 01             	cmp    $0x1,%ecx            # 数组元素值与1比
  40119d:	7e e4                	jle    401183 <phase_6+0x8f>
  40119f:	b8 01 00 00 00       	mov    $0x1,%eax              # 大于1的逻辑
  4011a4:	ba d0 32 60 00       	mov    $0x6032d0,%edx         # 地址值赋给edx
  4011a9:	eb cb                	jmp    401176 <phase_6+0x82>  # 第三个循环结束

这里看起来像是访问结构体中的元素的语法,看看0x6032d0处的内容:

292446ca049f49488f77504d4ef17115.png

可以看到第三段循环中取的是每个node的前32位内容,并且要比较用户输入的数是否与这前32位内容相等,node中这些数的值分别是1、2、3、4、5和6,所以用户要输入的6个数只能是1、2、3、4、5和6,只不过顺序还不能确定,并且这里每个node的后64位值都是后一个node的值,所以这应该是下一个node的指针,这里可以推断node是一个结构体,并且定义可以写成下面这样:

struct node
{
    int tag;
    int val;
    node *next;
};

既然知道了node的定义,接下来就好办了,第四段的内容是对node组成的链表按照输入索引取得得顺序重新连接:

  4011ab:	48 8b 5c 24 20       	mov    0x20(%rsp),%rbx   # 第一个数组元素的node的后继的地址
  4011b0:	48 8d 44 24 28       	lea    0x28(%rsp),%rax   # 第二个数组元素的node的后继的地址
  4011b5:	48 8d 74 24 50       	lea    0x50(%rsp),%rsi   # node地址的结尾
  4011ba:	48 89 d9             	mov    %rbx,%rcx        # rcx 存储 第一个数组元素的node的地址
  4011bd:	48 8b 10             	mov    (%rax),%rdx      # 第四个循环开始 # rax存储second node 的value值
  4011c0:	48 89 51 08          	mov    %rdx,0x8(%rcx)   # 把第一个node的后继改为第二个元素的后继
  4011c4:	48 83 c0 08          	add    $0x8,%rax        
  4011c8:	48 39 f0             	cmp    %rsi,%rax
  4011cb:	74 05                	je     4011d2 <phase_6+0xde>
  4011cd:	48 89 d1             	mov    %rdx,%rcx
  4011d0:	eb eb                	jmp    4011bd <phase_6+0xc9> # 第四个循环结束
  4011d2:	48 c7 42 08 00 00 00 	movq   $0x0,0x8(%rdx)        # 此时rdx存储最后一个元素的后继,把最后一个元素的后继置为NULL

接下来最后一段的内容可以分析出来是在判断重新组织后的链表是否已经按照降序的顺序排列!

  4011df:	48 8b 43 08          	mov    0x8(%rbx),%rax     # 第五个循环开始,
  4011e3:	8b 00                	mov    (%rax),%eax
  4011e5:	39 03                	cmp    %eax,(%rbx)        # 第一个数组元素的值与第二个数组元素的node值比较,要求第一大于第二,也就是说最终目的是链表降序排序!
  4011e7:	7d 05                	jge    4011ee <phase_6+0xfa>
  4011e9:	e8 4c 02 00 00       	call   40143a <explode_bomb>
  4011ee:	48 8b 5b 08          	mov    0x8(%rbx),%rbx
  4011f2:	83 ed 01             	sub    $0x1,%ebp
  4011f5:	75 e8                	jne    4011df <phase_6+0xeb>  # 第五个循环结束

也就是说,phase_6代码的目的是通过用户输入的六个数来将链表组织成降序的顺序!

那么再来看看每个节点的值

5c306a0ead70484683c642494375a18b.png

如果按照降序的顺序来排的话,那么索引顺序应该要是3、4、5、6、1、2。

但是别忘了,3、4、5、6、1、2应该要是7分别减去每个输入的数后得到的序列(第二段代码里的操作)。

所以最终答案应该是

4 3 2 1 6 5

隐藏阶段

前六个阶段通过后,程序也正常结束了:

7eb2cb1d85bc4ac2ba5703453be64c36.png

但是看bomb.c里作者的注释,还留下了彩蛋:

d686a2ccef4c4264a75d02bb73aa67b2.png

我们可以注意到,每个phase结束后都调用了phase_defused,这个函数的作用是什么呢?

分析看看,phase_defused的第一段,会查看0x202181(%rip)处的数是否等于6,这应该是用于计算我们通过的阶段数的,然后下面又读取了几个数,这里在0x4015f5打断点看看:

00000000004015c4 <phase_defused>:
  4015c4:	48 83 ec 78          	sub    $0x78,%rsp
  4015c8:	64 48 8b 04 25 28 00 	mov    %fs:0x28,%rax
  4015cf:	00 00 
  4015d1:	48 89 44 24 68       	mov    %rax,0x68(%rsp)
  4015d6:	31 c0                	xor    %eax,%eax
  4015d8:	83 3d 81 21 20 00 06 	cmpl   $0x6,0x202181(%rip)        # 603760 <num_input_strings>
  4015df:	75 5e                	jne    40163f <phase_defused+0x7b>
  4015e1:	4c 8d 44 24 10       	lea    0x10(%rsp),%r8
  4015e6:	48 8d 4c 24 0c       	lea    0xc(%rsp),%rcx
  4015eb:	48 8d 54 24 08       	lea    0x8(%rsp),%rdx
  4015f0:	be 19 26 40 00       	mov    $0x402619,%esi
  4015f5:	bf 70 38 60 00       	mov    $0x603870,%edi
  4015fa:	e8 f1 f5 ff ff       	call   400bf0 <__isoc99_sscanf@plt>
  4015ff:	83 f8 03             	cmp    $0x3,%eax
  401602:	75 31                	jne    401635 <phase_defused+0x71>

这里看到,我们并没有输入字符串,但是sscanf的参数str已经有值了,和我们在phase_4输入的值还一样:

21f02b0cbca547dd9e7db56c96dac15f.png

再回到phase_4处打断点看看phase_4处读取到的输入字符串的地址,发现地址就是phase_4输入的字符串就是存储在0x603870处:

a572f9eb414a404e99df68dd87ac1fd0.png

但是上面在phase_defused里却要求第4阶段输入的字符串是两个整数和一个字符串,这说明在phase_4输入字符串的时候除了两个整数外还要额外再输入一个字符串。

接下来一段代码就是判断phase_4额外输入的字符串是否和0x402622处的字符串相等:

  4015ff:	83 f8 03             	cmp    $0x3,%eax
  401602:	75 31                	jne    401635 <phase_defused+0x71>
  401604:	be 22 26 40 00       	mov    $0x402622,%esi
  401609:	48 8d 7c 24 10       	lea    0x10(%rsp),%rdi
  40160e:	e8 25 fd ff ff       	call   401338 <strings_not_equal>

看一下0x402622处的字符串:

0043ddaf55dd4adcaae5a20d0ab63967.png

再重新运行一遍bomb程序,试着在phase_4加上字符串DrEvil,发现已经触发了隐藏阶段:

4e6b5c0358394086be4e97855738621d.png

进入到secret_phase里,第一段依然是处理用户的输入,用strtol将用户输入的数转为10进制数,并且要求输入的数减一后小于或等于0x3e8,即小于1001,那这样看来本阶段只需输入一个数:

0000000000401242 <secret_phase>:
  401242:	53                   	push   %rbx
  401243:	e8 56 02 00 00       	call   40149e <read_line>
  401248:	ba 0a 00 00 00       	mov    $0xa,%edx   # arg3
  40124d:	be 00 00 00 00       	mov    $0x0,%esi   # arg2
  401252:	48 89 c7             	mov    %rax,%rdi   # arg1
  401255:	e8 76 f9 ff ff       	call   400bd0 <strtol@plt>
  40125a:	48 89 c3             	mov    %rax,%rbx
  40125d:	8d 40 ff             	lea    -0x1(%rax),%eax
  401260:	3d e8 03 00 00       	cmp    $0x3e8,%eax   # 判断用户输入的数是否小于1000
  401265:	76 05                	jbe    40126c <secret_phase+0x2a>
  401267:	e8 ce 01 00 00       	call   40143a <explode_bomb>

接下来调用fun7,传入一个立即数0x6030f0和我们输入的数,这个立即数应该是个地址:

  40126c:	89 de                	mov    %ebx,%esi
  40126e:	bf f0 30 60 00       	mov    $0x6030f0,%edi
  401273:	e8 8c ff ff ff       	call   401204 <fun7>

fun7第一段代码是判断输入的地址值是否为0,是的话就跳到最后的指令返回-1了

  401204:	48 83 ec 08          	sub    $0x8,%rsp
  401208:	48 85 ff             	test   %rdi,%rdi           # 判断arg1是否为NULL
  40120b:	74 2b                	je     401238 <fun7+0x34>
..........
  401238:	b8 ff ff ff ff       	mov    $0xffffffff,%eax    # 返回-1
  40123d:	48 83 c4 08          	add    $0x8,%rsp
  401241:	c3                   	ret   

接下来是arg1不为NULL的情况,可以发现,首先会判断arg1地址处的值是否小于arg2(arg2在第一次调用时是我们输入的数),如果小于或等于的话又会取arg1+8处地址的值作为地址值递归调用fun7,如果大于arg2的话就会取arg1+16地址处的值作为地址值递归调用fun7,这种指令访问形式可以判断这里arg1应该是一个结构体的首地址了。

  40120d:	8b 17                	mov    (%rdi),%edx     # 取arg1地址处的值,32位
  40120f:	39 f2                	cmp    %esi,%edx       # 与arg2比较
  401211:	7e 0d                	jle    401220 <fun7+0x1c>
  401213:	48 8b 7f 08          	mov    0x8(%rdi),%rdi   # 如果大于,取arg1 + 8处的值作为地址值调用fun7,
  401217:	e8 e8 ff ff ff       	call   401204 <fun7>
  40121c:	01 c0                	add    %eax,%eax       # 结果为 2 * fun7(arg1+8, arg2)
  40121e:	eb 1d                	jmp    40123d <fun7+0x39>
  401220:	b8 00 00 00 00       	mov    $0x0,%eax       # 如果arg1地址处的值等于arg2,返回0
  401225:	39 f2                	cmp    %esi,%edx
  401227:	74 14                	je     40123d <fun7+0x39>
  401229:	48 8b 7f 10          	mov    0x10(%rdi),%rdi  # 如果arg1地址处的值小于arg2,取arg+16处的值作为地址值调用fun7
  40122d:	e8 d2 ff ff ff       	call   401204 <fun7>
  401232:	8d 44 00 01          	lea    0x1(%rax,%rax,1),%eax # 结果为 2 * fun7(arg1 + 16, arg2) + 1

这里可以查看一下一开始传入的地址0x6030f0的值,看一下结构体的格式:

dede4ecec70b4a7aae8c20532a0d6768.png

每个结构体有一个值,然后有两个地址值,分别是其它结构体的首地址,可以判断结构体定义如下

struct Node
{
    int val;
    Node* left;
    Node* right;
};

那么也可以根据上面的逻辑写出fun7的C代码了,可以分析出0x6030f0处是一颗二叉排序树:

int fun7(Node *curNode, int target) {
	if(!curNode) {
		return -1;
	}
	int res;
	if(curNode->val < target) {
		res = 2 * fun7(curNode->right, target) + 1;
  } else if(p->val > target) {
		res = 2 * fun7(curNode->left, target);
	} else {
		res = 0;
	}

	return res;
}

现在返回secret_phase,可以看到要求fun7最终的返回值为2,所以我们输入的数要保证能让fun7最终返回2

  401273:	e8 8c ff ff ff       	call   401204 <fun7>
  401278:	83 f8 02             	cmp    $0x2,%eax

看上面的C代码,输入的target要使得fun7访问二叉树的路径为root->left->right,然后就结束递归,才能使得返回值为2,那么输入的target值应为root->left->right->val:

4be33d0e56e44e2e937bb65471157f53.png

那么应该递归到n32节点处就终止递归并返回0,所以本阶段需要输入的数为0x16 = 22

22

最终答案

Border relations with Canada have never been better.
1 2 4 8 16 32
0 207
7 0 DrEvil
)?>%&'
4 3 2 1 6 5
22

39867ddcf8cf4454886e21a2db5d8a6f.png

总结

现在应该很少需要自己写汇编的场景了,学习汇编只是为了能够大概读懂源码编译后的机器指令,这样可以了解到一些语言机制是如何实现的,还有分析自己写的程序生成的机器码效率如何。

参考资料

实验 2:Bomb Lab | 深入理解计算机系统(CSAPP) (gitbook.io)

计算机系统基础(二):程序的执行和存储访问_南京大学_中国大学MOOC(慕课) (icourse163.org)

第 3 章:程序的机器级表示 | 深入理解计算机系统(CSAPP) (gitbook.io)

x86_64汇编之一:AT&T汇编语法_x86_64汇编 at&t-CSDN博客

  • 25
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值