[CSAPP] Lab2 - Bomb Lab Report
Categories 课内 CSAPP
Tags
- 整体思路:利用汇编语言知识结合 gdb 调试一起推导过程,仅调试和仅观察都无法解开问题,必须一边调试一边理解汇编代码,找出答案规律。
Phase 1
汇编代码
0000000000400ee0 <phase_1>:
400ee0: 48 83 ec 08 sub $0x8,%rsp
400ee4: be 00 24 40 00 mov $0x402400,%esi
COMMENT ; 将 0x402400 中的内容作为第二个参数传入 strings_not_equal 函数
400ee9: e8 4a 04 00 00 call 401338 <strings_not_equal>
400eee: 85 c0 test %eax,%eax
400ef0: 74 05 je 400ef7 <phase_1+0x17>
COMMENT ; 如果两个参数相等,跳过炸弹
400ef2: e8 43 05 00 00 call 40143a <explode_bomb>
400ef7: 48 83 c4 08 add $0x8,%rsp
400efb: c3 ret
分析过程
400ee0中,0x402400内存中的内容存到%esi寄存器中,作为第二个参数传入strings_not_equal函数中(第一个是%edi,是用户传入的)。
之后在400ee9调用了函数比较两个字符串的内容,倘若相等,返回0。
接下来400eee和400ef0判断返回值是多少,如果是0,即字符串相等,则跳过炸弹到400ef7,反之到400ef2触发炸弹。
于是,我们现在明白了,只需要知道0x402400里面存放的是什么,然后在gdb中输入这个字符串,就可以解开炸弹了。
最终答案为:
Border relations with Canada have never been better.
伪代码
我们可以得到如下C伪代码便于理解:
int strings_not_equal(string &a, string &b) {
if (a == b) {
return 0;
}
return 1;
}
void phase_1(userInput, *(0x402400)) {
if (string_not_equal(userInput, *(0x402400)) == 0) {
The bomb is defused;
} else {
The bomb explodes;
}
}
运行截图

Phase 2
汇编代码
0000000000400efc <phase_2>:
400efc: 55 push %rbp
400efd: 53 push %rbx
400efe: 48 83 ec 28 sub $0x28,%rsp
400f02: 48 89 e6 mov %rsp,%rsi
COMMENT ; 栈底现在往下 0x28 个单位
400f05: e8 52 05 00 00 call 40145c <read_six_numbers>
COMMENT ; 输入6个数字
400f0a: 83 3c 24 01 cmpl $0x1,(%rsp)
COMMENT ; 第一个数字是1
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
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
COMMENT ; 边界,循环结束的标志
400f3a: eb db jmp 400f17 <phase_2+0x1b>
400f3c: 48 83 c4 28 add $0x28,%rsp
400f40: 5b pop %rbx
400f41: 5d pop %rbp
400f42: c3 ret
分析过程
400f05中,调用了一个获得6个数字的函数read_six_numbers,之后将对这六个数字进行操作。
400f0a后,比较了此时在栈底的%rsp里面的值和1,如果相等就跳到400f30,防止炸弹爆炸。显然,%rsp的值就是1。
400f30后,%rbx指向的是%rsp往上4个位置,%rbp指向的是%rsp往上18个位置,然后跳转到400f17。
在400f17,%rbx向下移动4个位置的值存储在%eax当中,此时就是%rsp的值,为1。之后%eax的值翻倍,并且判断其值是否就是%rbx.由于翻倍后的%eax的值是2,因此%rbx的值也应该是2。
接着两个值的确相同的话,跳转到400f25,%rbx向上移动4个单位,比较此时%rbx和%rbq。这里分为两个分支:如果相同跳转到400f3c,反之跳转到400f17。我们不妨先来分析不相同的情况。
400f17其实已经在上文分析过了,通过刚刚的规律可以知道,这个时候%rbx里面的值总是%rbx往下4个单位里面的值的两倍,因此从栈底到栈顶的方向,我们就知道有这样一串等比数列:
1 2 4 8 16 32 ...
那到底什么时候结束呢?很显然通过read_six_numbers可以知道,我们只要6个数字。这时候又有一个问题:是递增的序列输入还是递减的序列输入呢?我们知道每个新的数字都要是前一个数字的两倍,因此一定是递增数列。投机取巧的方法就是都试一遍,哪个不会炸那个就是对的了。
因此答案是:
1 2 4 8 16 32
伪代码
void phase_2(void) {
int a[6];
read_six_numbers(a);
int n = 6;
for (int i = 1; i < 6; i++) {
if (a[i] != a[i - 1] * 2) {
The bomb explodes;
}
}
The bomb is defused;
}
运行截图

Phase 3
汇编代码
0000000000400f43 <phase_3>:
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
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)
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)
400f7c: b8 cf 00 00 00 mov $0xcf,%eax
400f81: eb 3b jmp 400fbe <phase_3+0x7b>
400f83: b8 c3 02 00 00 mov $0x2c3,%eax
400f88: eb 34 jmp 400fbe <phase_3+0x7b>
400f8a: b8 00 01 00 00 mov $0x100,%eax
400f8f: eb 2d jmp 400fbe <phase_3+0x7b>
400f91: b8 85 01 00 00 mov $0x185,%eax
400f96: eb 26 jmp 400fbe <phase_3+0x7b>
400f98: b8 ce 00 00 00 mov $0xce,%eax
400f9d: eb 1f jmp 400fbe <phase_3+0x7b>
400f9f: b8 aa 02 00 00 mov $0x2aa,%eax
400fa4: eb 18 jmp 400fbe <phase_3+0x7b>
400fa6: b8 47 01 00 00 mov $0x147,%eax
400fab: eb 11 jmp 400fbe <phase_3+0x7b>
400fad: e8 88 04 00 00 call 40143a <explode_bomb>
400fb2: b8 00 00 00 00 mov $0x0,%eax
400fb7: eb 05 jmp 400fbe <phase_3+0x7b>
400fb9: b8 37 01 00 00 mov $0x137,%eax
400fbe: 3b 44 24 0c cmp 0xc(%rsp),%eax
400fc2: 74 05 je 400fc9 <phase_3+0x86>
400fc4: e8 71 04 00 00 call 40143a <explode_bomb>
400fc9: 48 83 c4 18 add $0x18,%rsp
400fcd: c3 ret
分析过程
前面两个炸弹都可以干看看出答案,但是第三个往后明显就困难多了。往后的炸弹我们很难逐条分析汇编代码来分析功能,现在开始我们使用gdb断点来帮助我们了解程序功能。
我们总体上要关注每次出现explode_bomb这个函数前程序做了什么,有助于理解我们传入的参数需要满足什么条件。
400f5b的__isoc99_sscanf@plt函数长得非常奇怪,我们可以使用如下指令去看看其到底是做什么的。
gdb bomb
b 89 # 在 bomb.c 中,这是进入 phase_3 的入口
run ans.txt # 第一和第二个炸弹的答案已经保存了
test
stepi
... # 跳过好多条汇编代码
最终展现如下信息:
__GI___isoc99_sscanf (s=0x603820 <input_strings+160> "test", format=0x4025cf "%d %d") at ./stdio-common/isoc99_sscanf.c:24
说明第三个炸弹需要我们传入两个整型。
我们将ans.txt随意传入两个整数22 23。
使用stepi nexti finish disas等指令,当跳转到:
0x0000000000400f63 <+32>: jg 0x400f6a <phase_3+39>
我们很好奇这上一行400f60中的%eax的值是多少,因为只有这个值比1大,炸弹才不会爆炸。那么我们用info r或者p $eax查看一下寄存器的值,发现是2。大胆猜测,这个值实际上是sscanf的返回值,意味着我们传入了两个参数。验证的方法也很简单,只需要在拆除第三个炸弹时只传入一个参数,这时候确实爆炸了,而且$eax值为1。
让我们仍然传入两个参数22 23。
当运行到:
0x0000000000400f6a <+39>: cmpl $0x7,0x8(%rsp)
我们可以检查一下0x8(%rsp)是什么,因为接下来就要进行函数跳转了。运用命令,得:
(gdb) x/d ($rsp + 8)
0x7fffffffd678: 22
这个22就是我们传入的第一个参数!这个数值一旦比7来得大,就会跳转到400fad,炸弹爆炸。因此我们第一个参数必须小于等于7!我们修改第一个参数为5重新实验。运行到:
0x0000000000400fb9 <+118>: mov $0x137,%eax
0x0000000000400fbe <+123>: cmp 0xc(%rsp),%eax
0x0000000000400fc2 <+127>: je 0x400fc9 <phase_3+134>
我们又不由自主关心起0xc(%rsp)和%eax是什么,因为0xc(%rsp)的值如果不是0x137就会引爆炸弹。我们发现:
(gdb) p $eax
$3 = 206
(gdb) x /d ($rsp + 0xc)
0x7fffffffd67c: 2
这说明炸弹会引爆。我们需要回去逐行分析代码找出规律了。运行到:
0x0000000000400f71 <+46>: mov 0x8(%rsp),%eax
%eax存放了0x8(%rsp)的值,即上文提到的第一个参数值。接下来运行到:
0x0000000000400f98 <+85>: mov $0xce,%eax
这个时候把0xce (206)赋值给%eax,和我们那次即将引爆炸弹的%eax值相等。那我刚刚的分析好像显得有些多余了???
再看这条指令:
(gdb) x /d ($rsp + 0xc)
0x7fffffffd67c: 2
我们似乎并不清楚这里的2是什么含义,但我瞬间有一种很不好的预感:在第二个炸弹的时候,我直接用 Windows 下的 VS Code 编辑我的ans.txt,结果答案正确输入了,炸弹还是爆炸。于是我换成了 Vim 重新编辑了我的答案,炸弹就正常拆除了。我第二个参数传入的是23,我很合理的怀疑这里的2实际上应该是23。于是我抛弃 VS Code ,再重新编辑,这一次我再查看($rsp + 0xc),结果就符合我的意图了。
(gdb) x /d ($rsp + 0xc)
0x7fffffffd67c: 23
我很难排查到究竟是什么原因导致了 Linux 和 Windows 上的格式差距,但这个例子提醒我:尽量在 Linux 环境下进行编辑修改,以免产生不必要的格式方面的麻烦。
那么我这次使用 Vim 修改了答案为5 206,炸弹就被拆除了。
实际上,第二个参数不总是206。当我把第一个参数换成别的值的时候,如果不改变第二个值,炸弹会爆炸。因此两个参数存在某种神奇的关系。但是我们只要能拆除炸弹就好了,所以关系是什么并不重要。
其实这么多的jmp,让我感觉到这个程序实际上实现了switch的功能。
最终答案是:
5 206
伪代码
仅仅是伪代码,忽略语法错误。
void phase_3(int argc, char *argv[]) {
if (argc != 2) {
The bomb explodes;
}
// 当 argv[0] == 5 的时候,argv[1] == 206,不会爆炸。
// 但是我并不知道这两个参数之间有什么关系,总之这样子可以拆除炸弹。
if (stoi(argv[0]) <= 7 && stoi(argv[1]) == ???) {
The bomb is defused;
}
}
运行截图

Phase 4
汇编代码
0000000000400fce <func4>:
400fce: 48 83 ec 08 sub $0x8,%rsp
400fd2: 89 d0 mov %edx,%eax
400fd4: 29 f0 sub %esi,%eax
400fd6: 89 c1 mov %eax,%ecx
400fd8: c1 e9 1f shr $0x1f,%ecx
400fdb: 01 c8 add %ecx,%eax
400fdd: d1 f8 sar $1,%eax
400fdf: 8d 0c 30 lea (%rax,%rsi,1),%ecx
400fe2: 39 f9 cmp %edi,%ecx
400fe4: 7e 0c jle 400ff2 <func4+0x24>
400fe6: 8d 51 ff lea -0x1(%rcx),%edx
400fe9: e8 e0 ff ff ff call 400fce <func4>
400fee: 01 c0 add %eax,%eax
400ff0: eb 15 jmp 401007 <func4+0x39>
400ff2: b8 00 00 00 00 mov $0x0,%eax
400ff7: 39 f9 cmp %edi,%ecx
400ff9: 7d 0c jge 401007 <func4+0x39>
400ffb: 8d 71 01 lea 0x1(%rcx),%esi
400ffe: e8 cb ff ff ff call 400fce <func4>
401003: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax
401007: 48 83 c4 08 add $0x8,%rsp
40100b: c3 ret
000000000040100c <phase_4>:
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>
40102e: 83 7c 24 08 0e cmpl $0xe,0x8(%rsp)
401033: 76 05 jbe 40103a <phase_4+0x2e>
401035: e8 00 04 00 00 call 40143a <explode_bomb>
40103a: ba 0e 00 00 00 mov $0xe,%edx
40103f: be 00 00 00 00 mov $0x0,%esi
401044: 8b 7c 24 08 mov 0x8(%rsp),%edi
401048: e8 81 ff ff ff call 400fce <func4>
40104d: 85 c0 test %eax,%eax
40104f: 75 07 jne 401058 <phase_4+0x4c>
401051: 83 7c 24 0c 00 cmpl $0x0,0xc(%rsp)
401056: 74 05 je 40105d <phase_4+0x51>
401058: e8 dd 03 00 00 call 40143a <explode_bomb>
40105d: 48 83 c4 18 add $0x18,%rsp
401061: c3 ret
分析过程
使用和上题一样的方法,发现:
__GI___isoc99_sscanf (s=0x603870 <input_strings+240> "3", format=0x4025cf "%d %d") at ./stdio-common/isoc99_sscanf.c:24
传入的是两个整型。那我们先输入22 23。再次运行,观察如下代码:
0x0000000000401029 <+29>: cmp $0x2,%eax
0x000000000040102c <+32>: jne 0x401035 <phase_4+41>
这里%eax一定要和0x2相等,否则就爆炸了。因此查看一下%eax的值:
(gdb) p $eax
$1 = 2
再次大胆猜测这里%eax就是传入的参数数量。当输入一个参数时,%eax为1,猜想大概率是正确的。再往下看:
0x000000000040102e <+34>: cmpl $0xe,0x8(%rsp)
0x0000000000401033 <+39>: jbe 0x40103a <phase_4+46>
0x0000000000401035 <+41>: call 0x40143a <explode_bomb>
我们要保证0x8(%rsp)的值要小于等于0xe。因此查看一下0x8(%rsp)。
(gdb) p ($rsp + 8)
$1 = (void *) 0x7fffffffd698
(gdb) p *(int *)($rsp + 8)
$2 = 22
就是我们的第一个参数,那么我们把第一个参数改小一点,改成10。然后再往下,不妨把断点打在*0x000000000040103a:
0x0000000000401048 <+60>: call 0x400fce <func4>
要进入func4了,我们stepi一下,往里看:
0x0000000000400fe2 <+20>: cmp %edi,%ecx
0x0000000000400fe4 <+22>: jle 0x400ff2 <func4+36>
%ecx要小于等于%edi,查看一下这两个:
(gdb) p $edi
$3 = 10
(gdb) p $ecx
$4 = 7
%edi我们显然知道是第一个参数,那%ecx呢?但是此时是满足条件的,我们接着往下走。走多一点吧,中间并没有引爆炸弹的代码,我们放心往下走。这个时候我们跳回到了phase_4,看看这几行:
0x000000000040104d <+65>: test %eax,%eax
0x000000000040104f <+67>: jne 0x401058 <phase_4+76>
这个时候%eax一定要是0。查看一下%eax:
(gdb) p $eax
$5 = 5
很明显不符合我们的要求。这个时候我们得去看看我们这之前的代码了。可不可以不管func4,从前面发现一点规律呢?比如会不会有哪个寄存器的值就是5?看这几行:
40103a: ba 0e 00 00 00 mov $0xe,%edx
40103f: be 00 00 00 00 mov $0x0,%esi
401044: 8b 7c 24 08 mov 0x8(%rsp),%edi
查看一下这几个寄存器的值:%edx肯定是0xe,%esi肯定是0。
(gdb) p $edi
$1 = 10
说明把第一个参数给了0x8(%rsp)。不妨试一下把第二个参数改成0或者是0xe?把断点打在*0x40104d。先试试0,失败了。再试试0xe,也失败了。代码都是卡在之前一样的跳转的位置,说明和第二个参数没有关系。那么再把第一个参数改成0或者e?先试试0 23。这一次通过了原来的比较,查看一下%eax:
(gdb) p $eax
$1 = 0
嗯,确实符合预期了。接下来看:
401051: 83 7c 24 0c 00 cmpl $0x0,0xc(%rsp)
401056: 74 05 je 40105d <phase_4+0x51>
查看一下0xc(%rsp),确保其就是0,炸弹才不会爆炸。
(gdb) x/d $rsp + 12
0x7fffffffd69c: 23
正好是我们第二个参数!我们只要把第二个参数改成0,炸弹就拆除了。
这种解法其实又有点投机取巧,因为我们根本没有分析func4的功能,只是有些盲目地给第一个或者第二个参数赋值为0或者14。
最终答案是:
0 0
运行截图

Phase 5
汇编代码
0000000000401062 <phase_5>:
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
401082: 74 4e je 4010d2 <phase_5+0x70>
401084: e8 b1 03 00 00 call 40143a <explode_bomb>
401089: eb 47 jmp 4010d2 <phase_5+0x70>
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
401099: 0f b6 92 b0 24 40 00 movzbl 0x4024b0(%rdx),%edx
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>
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
4010d7: eb b2 jmp 40108b <phase_5+0x29>
4010d9: 48 8b 44 24 18 mov 0x18(%rsp),%rax
4010de: 64 48 33 04 25 28 00 xor %fs:0x28,%rax
4010e5: 00 00
4010e7: 74 05 je 4010ee <phase_5+0x8c>
4010e9: e8 42 fa ff ff call 400b30 <__stack_chk_fail@plt>
4010ee: 48 83 c4 20 add $0x20,%rsp
4010f2: 5b pop %rbx
4010f3: c3 ret
分析过程
先在line 101打断点,进入程序,随便输入个2asdsads。看到:
0x000000000040107a <+24>: call 0x40131b <string_length>
0x000000000040107f <+29>: cmp $0x6,%eax
0x0000000000401082 <+32>: je 0x4010d2 <phase_5+112>
这里说明%eax要和6相等,我们看看%eax:
(gdb) p $eax
$1 = 8
这刚好是2asdsads的长度,可以推测函数string_length修改了%eax,我们需要确保输入的字符串长度是6。于是改成输入asdfgh。来到:
0x00000000004010a8 <+70>: cmp $0x6,%rax
0x00000000004010ac <+74>: jne 0x40108b <phase_5+41>
如果%rax的值不是6,就会跳回phase_5+41。我们看看%rax。
(gdb) p $rax
$1 = 1
的确不是。那让程序继续运行。运行了很久很久之后,我们再检查一下%rax:
(gdb) p $rax
$2 = 1
来到如下代码区域:
0x00000000004010bd <+91>: call 0x401338 <strings_not_equal>
0x00000000004010c2 <+96>: test %eax,%eax
0x00000000004010c4 <+98>: je 0x4010d9 <phase_5+119>
说明%eax必须是0,否则爆炸。因此查看一下:
(gdb) p $eax
$3 = 1
会爆炸,我们要找寻一下原因。我们是否应该回去想想strings_not_equal是拿来干什么的?重新运行一下试试:
0x00000000004010ae <+76>: movb $0x0,0x16(%rsp)
0x00000000004010b3 <+81>: mov $0x40245e,%esi
0x00000000004010b8 <+86>: lea 0x10(%rsp),%rdi
0x00000000004010bd <+91>: call 0x401338 <strings_not_equal>
运行到<+91>了,我们很有必要查看一下0x10(%rsp)和0x40245e都有些什么。
(gdb) x/s 0x40245e
0x40245e: "flyers"
(gdb) x/s $rsp + 16
0x7fffffffd9b0: "auirsn"
这两个字符串都是长度为6,我们是不是该把我们的输入改成它们呢?先试试"flyers"。断点可以打在*0x4010b8了。然后查看一下%eax是多少:
(gdb) p $eax
$1 = 1
那我们试试"auirsn":
(gdb) p $eax
$3 = 1
还是不行。那我们就很有必要对再往前的代码进行分析了。把断点打在*0x4010d2,然后分析如下代码:
401089: eb 47 jmp 4010d2 <phase_5+0x70>
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
401099: 0f b6 92 b0 24 40 00 movzbl 0x4024b0(%rdx),%edx
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>
4010c2: 85 c0 test %eax,%eax
4010c4: 74 13 je 4010d9 <phase_5+0x77>
4010c6: e8 6f 03 00 00 call 40143a <explode_bomb>
4010d2: b8 00 00 00 00 mov $0x0,%eax
4010d7: eb b2 jmp 40108b <phase_5+0x29>
观察可以知道,程序刚开始跳到了4010d2,之后又跳回上面的代码。我们有必要逐行分析一下。在40108b执行前查看一下%rbx %rax %ecx。
(gdb) p $ecx
$1 = 6
(gdb) x/s ($rax + $rbx)
0x6038c0 <input_strings+320>: "asdfgh"
(gdb) x $rbx
0x6038c0 <input_strings+320>: 97 'a'
(gdb) x $rbx + 1
0x6038c1 <input_strings+321>: 115 's'
(gdb) x $rbx + 2
0x6038c2 <input_strings+322>: 100 'd'
(gdb) x $rbx + 3
0x6038c3 <input_strings+323>: 102 'f'
(gdb) x $rbx + 4
0x6038c4 <input_strings+324>: 103 'g'
(gdb) x $rbx + 5
0x6038c5 <input_strings+325>: 104 'h'
(gdb) x $rbx + 6
0x6038c6 <input_strings+326>: 0 '\000'
寄存器含义不言而喻,$rbx指向了输入的字符串的最低一位。执行后:
(gdb) p $ecx
$4 = 97
97是什么?显然就是a的ASCII码,此时%ecx存放的是输入字符串的首位。其实也很好理解,我们把(%rbx,%rax,1)传给%eax。查看一下%cl(其实就是%ecx):
(gdb) p $cl
$5 = 97
再看到如下代码:
0x000000000040108b <+41>: movzbl (%rbx,%rax,1),%ecx
0x000000000040108f <+45>: mov %cl,(%rsp)
0x0000000000401092 <+48>: mov (%rsp),%rdx
0x0000000000401096 <+52>: and $0xf,%edx
这里还是a的ASCII码。我们发现这个值最终传给了%rdx,并且让其与1111作按位与运算。由于'a'的二进制表示是01100001,按位与的结果应该就是1。我们验证一下:
(gdb) p $rdx
$12 = 1
果然如此!因此这里的作用就是取%rdx的低4位。我们再往下看:
0x0000000000401099 <+55>: movzbl 0x4024b0(%rdx),%edx
0x00000000004010a0 <+62>: mov %dl,0x10(%rsp,%rax,1)
0x00000000004010a4 <+66>: add $0x1,%rax
0x00000000004010a8 <+70>: cmp $0x6,%rax
0x00000000004010ac <+74>: jne 0x40108b <phase_5+41>
首先好奇0x4024b0(%rdx)是什么。我们知道%dl实际上是%edx的低八位寄存器,所以此时里面的值应该和进行按位与运算后的%edx一样,它给到了0x10(%rsp,%rax,1)。
(gdb) x 0x4024b0
0x4024b0 <array.3449>: "maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"
(gdb) x 0x4024b0 + $rdx
0x4024b1 <array.3449+1>: "aduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"
为什么要把*0x4024b0加上%rdx的值呢?我们知道%rdx实际上是我们刚刚输入的第一个字符的低4位,也就是要从"maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"这个字符串的第(输入的字符串的第一个字符的低4位)位,也就是第一位开始取,因此0x4024b0 + $rdx结果符合我们预期。我们可以推断,我们输入的字符串有取字符的作用。我们把取出来的字符存到0x10(%rsp,%rax,1)当中,这是一个全新的内存区域。能取到刚好是一个字符的原因是,%dl是%edx的低八位寄存器,刚好取到一个char类型的长度。这里的$rax有特定的用途,我们知道在之前:
0x00000000004010d2 <+112>: mov $0x0,%eax
这里%eax被置为0,因此可以知道如下的汇编代码实现了特定的功能,保证执行了六次:
0x00000000004010a4 <+66>: add $0x1,%rax
0x00000000004010a8 <+70>: cmp $0x6,%rax
0x00000000004010ac <+74>: jne 0x40108b <phase_5+41>
伪代码如下:
for (int i = 0; i < 6; i++) {
goto: 0x40108b;
}
第二次循环,我们也可以继续观察。
(gdb) p $rax
$37 = 1
(gdb) x $rbx
0x6038c0 <input_strings+320>: "asdfgh"
之后,多亏了八位寄存器%cl,通过%rax的一次偏置,我们可以把字符串的第二个元素存入%rdx当中。
(gdb) p/c $dl
$48 = 115 's'
's'的低四位是0011,我们推测这一次能取到神秘字符串的第3位。我们试一试:
(gdb) p/c $dl
$50 = 117 'u'
没错!"maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"第三位就是u!那我们继续看看运行到第六次之后,新开辟的内存空间0x10(%rsp)里面是不是存放着我们取出来的字符。
(gdb) x/s 16 + $rsp
0x7fffffffd9b0: "auirsn"
那么我们现在很明白我们输入的字符串会被怎么处理了:取出输入的字符串的每个字母的ASCII码的第八位,作为神秘字符串的索引,分别取出字符,存入一个新的内存区域当中。
接下来我们看看新的字符串要有什么用。
0x00000000004010b3 <+81>: mov $0x40245e,%esi
0x00000000004010b8 <+86>: lea 0x10(%rsp),%rdi
0x00000000004010bd <+91>: call 0x401338 <strings_not_equal>
0x00000000004010c2 <+96>: test %eax,%eax
0x00000000004010c4 <+98>: je 0x4010d9 <phase_5+119>
这里调用了一个strings_not_equal函数,很可能就是把我们新的字符串和某个字符串比较是否相等,如果相等就返回0,不相等返回1。我们在最开始已经分析过了,如果是1就爆炸了,因此两个字符串必须是相等的。我们知道:
(gdb) x/s 0x40245e
0x40245e: "flyers"
(gdb) x/s $rsp + 16
0x7fffffffd9b0: "auirsn"
那我们只要能取出"flyers"就能拆除炸弹了吧!我们来试试。对于"maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?",我们要取出第9 15 14 5 6 7 (1001 1111 1110 0101 0110 0111)位即可。通过查询ASCII码表,我们可以试试输入字符串"yONEFG",炸弹确实拆除了。最终答案就是:
"yONEFG"
运行截图

Phase 6
汇编代码
00000000004010f4 <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
40110e: 41 bc 00 00 00 00 mov $0x0,%r12d
401114: 4c 89 ed mov %r13,%rbp
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
40112c: 41 83 fc 06 cmp $0x6,%r12d
401130: 74 21 je 401153 <phase_6+0x5f>
401132: 44 89 e3 mov %r12d,%ebx
401135: 48 63 c3 movslq %ebx,%rax
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
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>
401153: 48 8d 74 24 18 lea 0x18(%rsp),%rsi
401158: 4c 89 f0 mov %r14,%rax
40115b: b9 07 00 00 00 mov $0x7,%ecx
401160: 89 ca mov %ecx,%edx
401162: 2b 10 sub (%rax),%edx
401164: 89 10 mov %edx,(%rax)
401166: 48 83 c0 04 add $0x4,%rax
40116a: 48 39 f0 cmp %rsi,%rax
40116d: 75 f1 jne 401160 <phase_6+0x6c>
40116f: be 00 00 00 00 mov $0x0,%esi
401174: eb 21 jmp 401197 <phase_6+0xa3>
401176: 48 8b 52 08 mov 0x8(%rdx),%rdx
40117a: 83 c0 01 add $0x1,%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
401188: 48 89 54 74 20 mov %rdx,0x20(%rsp,%rsi,2)
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
40119d: 7e e4 jle 401183 <phase_6+0x8f>
40119f: b8 01 00 00 00 mov $0x1,%eax
4011a4: ba d0 32 60 00 mov $0x6032d0,%edx
4011a9: eb cb jmp 401176 <phase_6+0x82>
4011ab: 48 8b 5c 24 20 mov 0x20(%rsp),%rbx
4011b0: 48 8d 44 24 28 lea 0x28(%rsp),%rax
4011b5: 48 8d 74 24 50 lea 0x50(%rsp),%rsi
4011ba: 48 89 d9 mov %rbx,%rcx
4011bd: 48 8b 10 mov (%rax),%rdx
4011c0: 48 89 51 08 mov %rdx,0x8(%rcx)
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)
4011d9: 00
4011da: bd 05 00 00 00 mov $0x5,%ebp
4011df: 48 8b 43 08 mov 0x8(%rbx),%rax
4011e3: 8b 00 mov (%rax),%eax
4011e5: 39 03 cmp %eax,(%rbx)
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>
4011f7: 48 83 c4 50 add $0x50,%rsp
4011fb: 5b pop %rbx
4011fc: 5d pop %rbp
4011fd: 41 5c pop %r12
4011ff: 41 5d pop %r13
401201: 41 5e pop %r14
401203: c3 ret
分析过程
代码很长,如果一条一条分析会很费时费力。我们尝试浏览一下代码,捕获一点关键信息。有:
401106: e8 51 03 00 00 call 40145c <read_six_numbers>
说明我们这里要读入6个数字,那么这个数字的特征尚不明确。我们先试着输入1 3 5 7 9 11,然后在*0x40111b处打断点。因为我们观察到,下面的代码似乎是在验证数字的合法性,否则炸弹就会爆炸。
0x000000000040110b <+23>: mov %rsp,%r14
0x000000000040110e <+26>: mov $0x0,%r12d
0x0000000000401114 <+32>: mov %r13,%rbp
0x0000000000401117 <+35>: mov 0x0(%r13),%eax
0x000000000040111b <+39>: sub $0x1,%eax
0x000000000040111e <+42>: cmp $0x5,%eax
0x0000000000401121 <+45>: jbe 0x401128 <phase_6+52>
0x0000000000401123 <+47>: call 0x40143a <explode_bomb>
我们在每次运行到:
0x0000000000401121 <+45>: jbe 0x401128 <phase_6+52>
查看一下%eax的值,揣测一下有什么规律,这样就不必逐行分析代码了。代码会跳到后面的部分,我们等待运行回到上面的时候记录。接下来我将记录六次的%eax的值。
(gdb) p $eax
$1 = 0
(gdb) p $r12d
$2 = 2
(gdb) p $r12d
$3 = 4
(gdb) p $r12d
$4 = 6
运行了四次炸弹就爆炸了。这意味着我们第四个值开始是有问题的。我们可以看到%eax实际上就是我们每次的input - 1,是否意味着input - 1一定要小于6?因为:
0x0000000000401117 <+35>: mov 0x0(%r13),%eax
0x000000000040111b <+39>: sub $0x1,%eax
0x000000000040111e <+42>: cmp $0x5,%eax
0x0000000000401121 <+45>: jbe 0x401128 <phase_6+52>
0x0000000000401123 <+47>: call 0x40143a <explode_bomb>
这里%eax - 1要小于等于5,否则就会爆炸,因此我们就知道input - 1 <= 5,则我们输入一定要小于等于6!
我们试一下1 2 3 4 5 6,然后这一个炸弹一定是避开了,断点打在*0x40113b,每当运行到:
40113b: 39 45 00 cmp %eax,0x0(%rbp)
40113e: 75 05 jne 401145 <phase_6+0x51>
我们就查看一次%eax和%rbp:
(gdb) p $eax
$1 = 2
(gdb) x/6 $rbp
0x7fffffffd950: 1 2 3 4
0x7fffffffd960: 5 6
$rbp正好存放着我们输入的数字,我们在第二次循环再查看一下:
(gdb) p $eax
$12 = 3
(gdb) x/6 $rbp
0x7fffffffd950: 1 2 3 4
0x7fffffffd960: 5 6
显而易见,$eax是input + 1。我们看看第六次循环:
(gdb) p $eax
$13 = 3
(gdb) x/6 $rbp
0x7fffffffd954: 2 3 4 5
0x7fffffffd964: 6 -134230016
好像超过我们预期的六个数字的界限了?我们看看第五次循环:
(gdb) p $eax
$1 = 6
(gdb) x/6 $rbp
0x7fffffffd950: 1 2 3 4
0x7fffffffd960: 5 6
说明运行到第六次的时候,我们的input由于自增了,超出了6的界限,我们看看第六次跳转的时候这一行:
0x0000000000401145 <+81>: add $0x1,%ebx
0x0000000000401148 <+84>: cmp $0x5,%ebx
0x000000000040114b <+87>: jle 0x401135 <phase_6+65>
查看一下%ebx的值:
(gdb) p $ebx
$2 = 6
这个时候刚好到了6,刚好会跳转了。写下来达到断点的次数(包括最开始的一次)为5 4 3 2 1。
我们看到奇怪的一行:
0x0000000000401183 <+143>: mov $0x6032d0,%edx
这里访问一下0x6032d0:
(gdb) x 0x6032d0
0x6032d0 <node1>: 0x0000014c
我们发现了一个节点?根据节点的特征,节点应该有值和下一个结点的内存地址,我们查看大一点的内容:
(gdb) x/100 0x6032d0
0x6032d0 <node1>: 0x0000014c 0x00000001 0x006032e0 0x00000000
0x6032e0 <node2>: 0x000000a8 0x00000002 0x006032d0 0x00000000
0x6032f0 <node3>: 0x0000039c 0x00000003 0x006032e0 0x00000000
0x603300 <node4>: 0x000002b3 0x00000004 0x006032f0 0x00000000
0x603310 <node5>: 0x000001dd 0x00000005 0x00603300 0x00000000
0x603320 <node6>: 0x000001bb 0x00000006 0x00603310 0x00000000
COMMENT ; 以下省略
可以看到,第二个值是我们输入的内容,第三个值就是下一个结点的内存。我们可以写如下的代码:
typedef ListNode {
int rootVal;
int val;
struct ListNode *next;
} ListNode;
这个结构有两个值和一个指针,其中这两个值一定有某种关系。最最最朴素的一种想法:会不会是排序呢?是从大到小还是从小到大呢?
那我们来尝试一下:从小到大应该输入的是2 1 6 5 4 3,从大到小是3 4 5 6 1 2。结果都不正确。
那会不会是排序的顺序进行了某种处理呢?我们已经限定了输入的数据必须小于等于6,按照排序的顺序应该输入的数值是1 - 6,那么加密后的数据也是1 - 6。这怎么做到的呢?比如加上一个数字模7?或者被7减去?那么我们肯定要找找汇编代码有没有数字7了,通过搜索果然发现:
40115b: b9 07 00 00 00 mov $0x7,%ecx
401160: 89 ca mov %ecx,%edx
401162: 2b 10 sub (%rax),%edx
这个代码比较简单,我们只要查看一下运行到这里的时候%rax是谁就可以了,相信这个寄存器一定与我们的输入有关。我们在*0x401162打一个断点,还是输入1 2 3 4 5 6,每次经过的时候记录一下值。结果如下:
(gdb) x $rax
0x7fffffffd950: 0x00000001
(gdb) x $rax
0x7fffffffd954: 0x00000002
(gdb) x $rax
0x7fffffffd958: 0x00000003
(gdb) x $rax
0x7fffffffd95c: 0x00000004
(gdb) x $rax
0x7fffffffd960: 0x00000005
(gdb) x $rax
0x7fffffffd964: 0x00000006
说明($rax)就是我们的输入。由上面的代码分析,我们知道最终%edx的值是7 - input。我们处理一下我们刚刚的猜测:从小到大应该输入的是5 6 1 2 3 4,从大到小是4 3 2 1 6 5。最终答案就是:
4 3 2 1 6 5
运行截图

Secret Phase
汇编代码
0000000000401204 <fun7>:
401204: 48 83 ec 08 sub $0x8,%rsp
401208: 48 85 ff test %rdi,%rdi
40120b: 74 2b je 401238 <fun7+0x34>
40120d: 8b 17 mov (%rdi),%edx
40120f: 39 f2 cmp %esi,%edx
401211: 7e 0d jle 401220 <fun7+0x1c>
401213: 48 8b 7f 08 mov 0x8(%rdi),%rdi
401217: e8 e8 ff ff ff call 401204 <fun7>
40121c: 01 c0 add %eax,%eax
40121e: eb 1d jmp 40123d <fun7+0x39>
401220: b8 00 00 00 00 mov $0x0,%eax
401225: 39 f2 cmp %esi,%edx
401227: 74 14 je 40123d <fun7+0x39>
401229: 48 8b 7f 10 mov 0x10(%rdi),%rdi
40122d: e8 d2 ff ff ff call 401204 <fun7>
401232: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax
401236: eb 05 jmp 40123d <fun7+0x39>
401238: b8 ff ff ff ff mov $0xffffffff,%eax
40123d: 48 83 c4 08 add $0x8,%rsp
401241: c3 ret
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
40124d: be 00 00 00 00 mov $0x0,%esi
401252: 48 89 c7 mov %rax,%rdi
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
401265: 76 05 jbe 40126c <secret_phase+0x2a>
401267: e8 ce 01 00 00 call 40143a <explode_bomb>
40126c: 89 de mov %ebx,%esi
40126e: bf f0 30 60 00 mov $0x6030f0,%edi
401273: e8 8c ff ff ff call 401204 <fun7>
401278: 83 f8 02 cmp $0x2,%eax
40127b: 74 05 je 401282 <secret_phase+0x40>
40127d: e8 b8 01 00 00 call 40143a <explode_bomb>
401282: bf 38 24 40 00 mov $0x402438,%edi
401287: e8 84 f8 ff ff call 400b10 <puts@plt>
40128c: e8 33 03 00 00 call 4015c4 <phase_defused>
401291: 5b pop %rbx
401292: c3 ret
401293: 90 nop
401294: 90 nop
401295: 90 nop
401296: 90 nop
401297: 90 nop
401298: 90 nop
401299: 90 nop
40129a: 90 nop
40129b: 90 nop
40129c: 90 nop
40129d: 90 nop
40129e: 90 nop
40129f: 90 nop
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>
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>
401613: 85 c0 test %eax,%eax
401615: 75 1e jne 401635 <phase_defused+0x71>
401617: bf f8 24 40 00 mov $0x4024f8,%edi
40161c: e8 ef f4 ff ff call 400b10 <puts@plt>
401621: bf 20 25 40 00 mov $0x402520,%edi
401626: e8 e5 f4 ff ff call 400b10 <puts@plt>
40162b: b8 00 00 00 00 mov $0x0,%eax
401630: e8 0d fc ff ff call 401242 <secret_phase>
401635: bf 58 25 40 00 mov $0x402558,%edi
40163a: e8 d1 f4 ff ff call 400b10 <puts@plt>
40163f: 48 8b 44 24 68 mov 0x68(%rsp),%rax
401644: 64 48 33 04 25 28 00 xor %fs:0x28,%rax
40164b: 00 00
40164d: 74 05 je 401654 <phase_defused+0x90>
40164f: e8 dc f4 ff ff call 400b30 <__stack_chk_fail@plt>
401654: 48 83 c4 78 add $0x78,%rsp
401658: c3 ret
401659: 90 nop
40165a: 90 nop
40165b: 90 nop
40165c: 90 nop
40165d: 90 nop
40165e: 90 nop
40165f: 90 nop
分析过程
我们想要找到secret_phase什么时候被调用。通过搜索得到:
00000000004015c4 <phase_defused>:
COMMENT ; ...
401630: e8 0d fc ff ff call 401242 <secret_phase>
COMMENT ; ...
在phase_defused中找到了我们函数的入口。我们看到:
4015fa: e8 f1 f5 ff ff call 400bf0 <__isoc99_sscanf@plt>
因此可以尝试看看__isoc99_sscanf@plt里面需要我们输入什么。我们给phase_defused和*0x4015fa各打一个断点,以便我们查看其内容。发现在第六个炸弹拆除后调用phase_defused时,出现了如下一行:
__GI___isoc99_sscanf (s=0x603870 <input_strings+240> "0 0", format=0x402619 "%d %d %s") at ./stdio-common/isoc99_sscanf.c:24
这里的0 0显然是我们在phase_4时输入的答案,此时这个函数允许我们输入两个整型和一个字符串,或许我们只要找到这个字符串就能找到入口了?观察到这一行:
0x0000000000401604 <+64>: mov $0x402622,%esi
0x0000000000401609 <+69>: lea 0x10(%rsp),%rdi
0x000000000040160e <+74>: call 0x401338 <strings_not_equal>
0x0000000000401613 <+79>: test %eax,%eax
0x0000000000401615 <+81>: jne 0x401635 <phase_defused+113>
我们不由地想到strings_not_equal函数的用法,如果我们在第三个参数输入了正确的字符串,就不会跳转到0x0000000000401635 <+113>,而是会进入0x0000000000401630 <+108>,进入隐藏炸弹。
0x0000000000401630 <+108>: call 0x401242 <secret_phase>
0x0000000000401635 <+113>: mov $0x402558,%edi
我们尝试获取0x402622所表示的字符串:
(gdb) x /s 0x402622
0x402622: "DrEvil"
这时候我们修改一下我们的ans.txt,在第四个炸弹答案中加上DrEvil。果然显示了如下句子:
Curses, you've found the secret phase!
But finding it and solving it are quite different...
这样就进入了隐藏炸弹。来观察一下我们的secret_phase。先解决一下输入格式:
401243: e8 56 02 00 00 call 40149e <read_line>
COMMENT ; ...
401255: e8 76 f9 ff ff call 400bd0 <strtol@plt>
说明程序读入一个字符串并且转成long int。我们可以推测答案实际上就是一个整数。观察到:
0x000000000040125a <+24>: mov %rax,%rbx
0x000000000040125d <+27>: lea -0x1(%rax),%eax
0x0000000000401260 <+30>: cmp $0x3e8,%eax
0x0000000000401265 <+35>: jbe 0x40126c <secret_phase+42>
0x0000000000401267 <+37>: call 0x40143a <explode_bomb>
我们要判断一下%rax一开始的值,在*0x40125a打断点,并且输入23:
(gdb) p $rbx
$1 = 23
(gdb) p $eax
$2 = 22
说明一开始%rax就存放我们输入的数字,然后自减一次,结果一定要小于等于0x3e8,说明我们输入的数一定要小于等于1001。再往下看:
0x000000000040126c <+42>: mov %ebx,%esi
0x000000000040126e <+44>: mov $0x6030f0,%edi
0x0000000000401273 <+49>: call 0x401204 <fun7>
0x0000000000401278 <+54>: cmp $0x2,%eax
0x000000000040127b <+57>: je 0x401282 <secret_phase+64>
0x000000000040127d <+59>: call 0x40143a <explode_bomb>
我们知道%ebx == %rbx == 23,就是我们输入的数值。查看一下*0x6030f0:
(gdb) p *0x6030f0
$7 = 36
看不出什么规律。那我们接着往下,等调用完func7后看看%eax的值:
(gdb) p $eax
$8 = -10
要让%eax等于2才不会爆炸。我们因此需要分析一下func7了。在fun7打断点,忽略栈空间分配,我们看到:
0x0000000000401208 <+4>: test %rdi,%rdi
0x000000000040120b <+7>: je 0x401238 <fun7+52>
0x000000000040120d <+9>: mov (%rdi),%edx
COMMENT ; ...
0x0000000000401238 <+52>: mov $0xffffffff,%eax
这里查看一下%rdi:
(gdb) p *$rdi
$2 = 36
刚好是我们上面那个不明所以的数字。如果%rdi为0,注意到0x401238,就会给返回值%eax赋值为-1。为什么有-1呢?大概率是这个函数不符合某个条件的时候会返回这个值。那么当%rdi为0就满足了这个条件。在看这个函数,我们可以发现其两次调用了自己本身,因此这是一个递归函数。我们观察一下什么时候会发生递归:
0x0000000000401213 <+15>: mov 0x8(%rdi),%rdi
0x0000000000401217 <+19>: call 0x401204 <fun7>
COMMENT ; ...
0x0000000000401229 <+37>: mov 0x10(%rdi),%rdi
0x000000000040122d <+41>: call 0x401204 <fun7>
可以看出,每次递归都多开辟了8个内存空间,这也许是两个变量的意思。再往前看:
0x000000000040120d <+9>: mov (%rdi),%edx
0x000000000040120f <+11>: cmp %esi,%edx
0x0000000000401211 <+13>: jle 0x401220 <fun7+28>
(%rdi)就是之前不明所以的值。我们查看一下%esi:
(gdb) p $esi
$3 = 23
我们现在是在比较输入的值和这个不明所以的值!如果输入的值大于这个数,就会跳转到:
0x0000000000401220 <+28>: mov $0x0,%eax
0x0000000000401225 <+33>: cmp %esi,%edx
0x0000000000401227 <+35>: je 0x40123d <fun7+57>
满足条件就跳出当前递归了。这个不明所以的值确实很恼人,我们还记得它的地址是0x6030f0,我们可能需要用别的方法看看它是什么。我们不妨一次性看多一点:
(gdb) x/100 0x6030f0
0x6030f0 <n1>: 0x00000024 0x00000000 0x00603110 0x00000000
0x603100 <n1+16>: 0x00603130 0x00000000 0x00000000 0x00000000
0x603110 <n21>: 0x00000008 0x00000000 0x00603190 0x00000000
0x603120 <n21+16>: 0x00603150 0x00000000 0x00000000 0x00000000
0x603130 <n22>: 0x00000032 0x00000000 0x00603170 0x00000000
0x603140 <n22+16>: 0x006031b0 0x00000000 0x00000000 0x00000000
0x603150 <n32>: 0x00000016 0x00000000 0x00603270 0x00000000
0x603160 <n32+16>: 0x00603230 0x00000000 0x00000000 0x00000000
0x603170 <n33>: 0x0000002d 0x00000000 0x006031d0 0x00000000
0x603180 <n33+16>: 0x00603290 0x00000000 0x00000000 0x00000000
0x603190 <n31>: 0x00000006 0x00000000 0x006031f0 0x00000000
0x6031a0 <n31+16>: 0x00603250 0x00000000 0x00000000 0x00000000
0x6031b0 <n34>: 0x0000006b 0x00000000 0x00603210 0x00000000
0x6031c0 <n34+16>: 0x006032b0 0x00000000 0x00000000 0x00000000
0x6031d0 <n45>: 0x00000028 0x00000000 0x00000000 0x00000000
0x6031e0 <n45+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x6031f0 <n41>: 0x00000001 0x00000000 0x00000000 0x00000000
0x603200 <n41+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x603210 <n47>: 0x00000063 0x00000000 0x00000000 0x00000000
0x603220 <n47+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x603230 <n44>: 0x00000023 0x00000000 0x00000000 0x00000000
0x603240 <n44+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x603250 <n42>: 0x00000007 0x00000000 0x00000000 0x00000000
0x603260 <n42+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x603270 <n43>: 0x00000014 0x00000000 0x00000000 0x00000000
这看上去像是某种像链表的数据结构。但如果是链表,应该只会占据16个字节,而这里占据了24个字节。会不会可能是二叉树呢?我们来看看以下结构:
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int val) : val(val), left(nullptr), right(nullptr) {}
};
因此增加8个内存单位就是为了存放一个节点指针。我们可以延续这个假设,然后来逐行分析汇编代码:
0x0000000000401204 <+0>: sub $0x8,%rsp
0x0000000000401208 <+4>: test %rdi,%rdi # 检查是否是NULL,因为NULL == 0
0x000000000040120b <+7>: je 0x401238 <fun7+52> # 如果是NULL,跳到<+52>,返回-1
0x000000000040120d <+9>: mov (%rdi),%edx
0x000000000040120f <+11>: cmp %esi,%edx # 比较target和当前节点val
0x0000000000401211 <+13>: jle 0x401220 <fun7+28> # 如果target >= node->val,跳转到<+28>
0x0000000000401213 <+15>: mov 0x8(%rdi),%rdi
0x0000000000401217 <+19>: call 0x401204 <fun7> # 否则传入node->left
0x000000000040121c <+24>: add %eax,%eax # node->val *= 2
0x000000000040121e <+26>: jmp 0x40123d <fun7+57> # 结束递归
0x0000000000401220 <+28>: mov $0x0,%eax # 赋值为0
0x0000000000401225 <+33>: cmp %esi,%edx # 比较当前节点和target
0x0000000000401227 <+35>: je 0x40123d <fun7+57> # 如果相等就返回,返回值就是上面设置的%eax,为0
0x0000000000401229 <+37>: mov 0x10(%rdi),%rdi
0x000000000040122d <+41>: call 0x401204 <fun7> # 传入node->right
0x0000000000401232 <+46>: lea 0x1(%rax,%rax,1),%eax # node->val = node->val + 1
0x0000000000401236 <+50>: jmp 0x40123d <fun7+57> # 返回
0x0000000000401238 <+52>: mov $0xffffffff,%eax
0x000000000040123d <+57>: add $0x8,%rsp
0x0000000000401241 <+61>: ret
可以写出伪代码:
int func7(int target, TreeNode *node) {
if (node == nullptr) {
return -1;
}
if (target == node->val) {
return 0;
}
if (target > node->val) {
return func7(target, node->right) * 2 + 1;
}
return func7(target, node->left) * 2;
}
我们再生成一次二叉树的结构:
(gdb) x /200 0x6030f0
0x6030f0 <n1>: 0x00000024 0x00000000 0x00603110 0x00000000
0x603100 <n1+16>: 0x00603130 0x00000000 0x00000000 0x00000000
0x603110 <n21>: 0x00000008 0x00000000 0x00603190 0x00000000
0x603120 <n21+16>: 0x00603150 0x00000000 0x00000000 0x00000000
0x603130 <n22>: 0x00000032 0x00000000 0x00603170 0x00000000
0x603140 <n22+16>: 0x006031b0 0x00000000 0x00000000 0x00000000
0x603150 <n32>: 0x00000016 0x00000000 0x00603270 0x00000000
0x603160 <n32+16>: 0x00603230 0x00000000 0x00000000 0x00000000
0x603170 <n33>: 0x0000002d 0x00000000 0x006031d0 0x00000000
0x603180 <n33+16>: 0x00603290 0x00000000 0x00000000 0x00000000
0x603190 <n31>: 0x00000006 0x00000000 0x006031f0 0x00000000
0x6031a0 <n31+16>: 0x00603250 0x00000000 0x00000000 0x00000000
0x6031b0 <n34>: 0x0000006b 0x00000000 0x00603210 0x00000000
0x6031c0 <n34+16>: 0x006032b0 0x00000000 0x00000000 0x00000000
0x6031d0 <n45>: 0x00000028 0x00000000 0x00000000 0x00000000
0x6031e0 <n45+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x6031f0 <n41>: 0x00000001 0x00000000 0x00000000 0x00000000
0x603200 <n41+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x603210 <n47>: 0x00000063 0x00000000 0x00000000 0x00000000
0x603220 <n47+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x603230 <n44>: 0x00000023 0x00000000 0x00000000 0x00000000
0x603240 <n44+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x603250 <n42>: 0x00000007 0x00000000 0x00000000 0x00000000
0x603260 <n42+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x603270 <n43>: 0x00000014 0x00000000 0x00000000 0x00000000
0x603280 <n43+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x603290 <n46>: 0x0000002f 0x00000000 0x00000000 0x00000000
0x6032a0 <n46+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x6032b0 <n48>: 0x000003e9 0x00000000 0x00000000 0x00000000
0x6032c0 <n48+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x6032d0 <node1>: 0x0000014c 0x00000001 0x006032e0 0x00000000
0x6032e0 <node2>: 0x000000a8 0x00000002 0x00000000 0x00000000
0x6032f0 <node3>: 0x0000039c 0x00000003 0x00603300 0x00000000
0x603300 <node4>: 0x000002b3 0x00000004 0x00603310 0x00000000
0x603310 <node5>: 0x000001dd 0x00000005 0x00603320 0x00000000
0x603320 <node6>: 0x000001bb 0x00000006 0x006032d0 0x00000000
根据上面的代码,我们知道这是一个二叉搜索树。
36
/ \
8 50
/ \ / \
6 22 45 107
/ \ / \ / \ / \
1 7 20 35 40 47 99 1001
一直不理解的36实际上就是头节点的值而已。那我们需要函数返回什么呢?看到secret_phase中这几行:
0x0000000000401273 <+49>: call 0x401204 <fun7>
0x0000000000401278 <+54>: cmp $0x2,%eax
0x000000000040127b <+57>: je 0x401282 <secret_phase+64>
0x000000000040127d <+59>: call 0x40143a <explode_bomb>
其中%eax是func7的返回值。我们需要返回值是2才能拆除炸弹。什么时候返回的才能是2呢?比如说一个函数返回了0,然后这个0是右子树来的,所以0 * 2 + 1得到1;当然也可以是从很多个左子树来的,毕竟0 * 2还是0。接着这个1是从左子树来的,因此1 * 2得到了2。反过来思考,我们就是先从根节点往左找,再往右找,就可以找到了,答案是22。也可以再从22的左子树一直找,这样就还能找到一个答案20。
最终答案是:
20 或 22
运行截图


答案
phase_3 phase_4 phase_5 secret_phase答案并不唯一,这里只给出我有直接解出的答案。
Border relations with Canada have never been better.
1 2 4 8 16 32
5 206
0 0 DrEvil
yONEFG
4 3 2 1 6 5
20
or
Border relations with Canada have never been better.
1 2 4 8 16 32
5 206
0 0 DrEvil
yONEFG
4 3 2 1 6 5
22
Page views: Loading... · Visitors: Loading...
Except where otherwise noted, original content on this site is dedicated to the public domain under CC0 1.0.
Powered by Hexo & Theme mdsuper
沪ICP备2026040813号