目录
实验简介
bomb lab是csapp课程的一个lab,实验目标是通过输入一系列的字符串以保证程序正确执行并正常退出,要保证不触发炸弹。
实验前准备
要先大概读一遍csapp的第三章。
要了解一些gdb命令使用,过程中我用到了下面这些命令:
- x:查看内存处的值
- i:i r reg可以查看寄存器的值
- b: b + 地址值可以在相应位置打断点
然后需要了解下x86-64下的函数调用惯例,这里放几个mooc的图:
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处的字符串:
所以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看一下:
所以要输入六个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
还是看一下格式化字符串的内容,这里要求我们输入两个整数
继续往下看
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处取地址间接跳转。
switch语句开头先判断了数是否小于8,所以switch里面case的值应该在0到7之间,这里看一下0x402470处的8个四字内容。
可以看到这些地址与phase_3代码的地址对应:
输入的第一个数是跳转表的索引,用于决定要跳转到哪里去执行,如果我们第一个数输入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>
开一下格式化字符串内容,还是要求输入两个整数。
接下来判断输入的第一个数是否小于或等于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,此时要满足 才能跳转到0x400ffb处,用input1与c对比,如果input1满足
就可以只调一次func4而不用递归就从func4返回。去上面两个不等式的交集,input1要等于7才就能一次都不递归就返回,此时func4返回值是0。
接下来看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"
再看看0x4024b0处的内容,可以看到是一个字符串:
要想从中取出flyers这个字符串,输入的六个字符的低4位按16进制表示应该分别是,9、F、E、5、6、7,这里可以找个ASCII码表看看:
最后我选取的字符串是:
)?>%&'
按照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处的内容:
可以看到第三段循环中取的是每个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代码的目的是通过用户输入的六个数来将链表组织成降序的顺序!
那么再来看看每个节点的值
如果按照降序的顺序来排的话,那么索引顺序应该要是3、4、5、6、1、2。
但是别忘了,3、4、5、6、1、2应该要是7分别减去每个输入的数后得到的序列(第二段代码里的操作)。
所以最终答案应该是
4 3 2 1 6 5
隐藏阶段
前六个阶段通过后,程序也正常结束了:
但是看bomb.c里作者的注释,还留下了彩蛋:
我们可以注意到,每个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输入的值还一样:
再回到phase_4处打断点看看phase_4处读取到的输入字符串的地址,发现地址就是phase_4输入的字符串就是存储在0x603870处:
但是上面在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处的字符串:
再重新运行一遍bomb程序,试着在phase_4加上字符串DrEvil,发现已经触发了隐藏阶段:
进入到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的值,看一下结构体的格式:
每个结构体有一个值,然后有两个地址值,分别是其它结构体的首地址,可以判断结构体定义如下
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:
那么应该递归到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
总结
现在应该很少需要自己写汇编的场景了,学习汇编只是为了能够大概读懂源码编译后的机器指令,这样可以了解到一些语言机制是如何实现的,还有分析自己写的程序生成的机器码效率如何。
参考资料
实验 2:Bomb Lab | 深入理解计算机系统(CSAPP) (gitbook.io)
计算机系统基础(二):程序的执行和存储访问_南京大学_中国大学MOOC(慕课) (icourse163.org)