介绍
(貌似有好多Bomb版本,这个版本是北理工特供版)
本实验要求你使用课程所学知识拆除"binary bombs(二进制炸弹,下文将简称为炸弹)",增强对程序的机器级表示、汇编语言、调试器和逆向工程等方面原理与技能的掌握。 这里的炸弹是一个Linux可执行程序,包含了6个阶段(或层次、关卡)。炸弹运行的每个阶段要求你输入一个特定字符串,你的输入符合程序预期的输入,该阶段的炸弹就被拆除引信即解除了,否则炸弹"爆炸"打印输出 "BOOM!!!"。实验的目标是拆除尽可能多的炸弹关卡。
每个炸弹阶段考察了机器级程序语言的一个不同方面,难度逐级递增:
- 阶段1:字符串比较
- 阶段2:循环
- 阶段3:条件/分支
- 阶段4:递归调用和栈
- 阶段5:指针
- 阶段6:链表/指针/结构
另外还有一个隐藏阶段,只有当你在第4阶段的解后附加一特定字符串后才会出现。
为完成二进制炸弹拆除任务,你需要使用gdb 调试器和objdump来反汇编炸弹的可执行文件并跟踪调试每一阶段的机器代码,从中理解每一汇编语言代码的行为或作用,进而设法推断拆除炸弹所需的目标字符串。比如在每一阶段的开始代码前和引爆炸弹的函数前设置断点。
phase 1
phase_1函数汇编代码:
通过objdump -d bomb进行反汇编,查看汇编代码,其中的phase_1函数部分如下
yaml
0000000000400e6d <phase_1>:
400e6d: 48 83 ec 08 sub $0x8,%rsp
400e71: be d0 23 40 00 mov $0x4023d0,%esi
400e76: e8 cf 04 00 00 call 40134a <strings_not_equal>
400e7b: 85 c0 test %eax,%eax
400e7d: 75 05 jne 400e84 <phase_1+0x17>
400e7f: 48 83 c4 08 add $0x8,%rsp
400e83: c3 ret
400e84: e8 be 05 00 00 call 401447 <explode_bomb>
400e89: eb f4 jmp 400e7f <phase_1+0x12>
在gdb调试时给break phase_1打上断点,并查看disas phase_1
yaml
(gdb) disas phase_1
Dump of assembler code for function phase_1:
=> 0x0000000000400e6d <+0>: sub $0x8,%rsp
0x0000000000400e71 <+4>: mov $0x4023d0,%esi
0x0000000000400e76 <+9>: call 0x40134a <strings_not_equal>
0x0000000000400e7b <+14>: test %eax,%eax
0x0000000000400e7d <+16>: jne 0x400e84 <phase_1+23>
0x0000000000400e7f <+18>: add $0x8,%rsp
0x0000000000400e83 <+22>: ret
0x0000000000400e84 <+23>: call 0x401447 <explode_bomb>
0x0000000000400e89 <+28>: jmp 0x400e7f <phase_1+18>
第二行有一个移动数据的操作 mov ,所以查看这个esi的值:
yaml
(gdb) print(char*)0x4023d0
$1 = 0x4023d0 "Slave, thou hast slain me. Villain, take my purse."
得到答案"Slave, thou hast slain me. Villain, take my purse."
把答案写入 secret.txt的第一行,运行 run secret.txt ,看到"Phase 1 defused. How about the next one?"说明成功完成了第一道题。

phase 2
phase_2函数汇编代码:
yaml
(gdb) disas phase_2
Dump of assembler code for function phase_2:
=> 0x0000000000400e8b <+0>: push %rbx
0x0000000000400e8c <+1>: sub $0x20,%rsp
0x0000000000400e90 <+5>: mov %fs:0x28,%rax
0x0000000000400e99 <+14>: mov %rax,0x18(%rsp)
0x0000000000400e9e <+19>: xor %eax,%eax
0x0000000000400ea0 <+21>: mov %rsp,%rsi
0x0000000000400ea3 <+24>: call 0x401469 <read_six_numbers>
0x0000000000400ea8 <+29>: cmpl $0x0,(%rsp)
0x0000000000400eac <+33>: js 0x400eb5 <phase_2+42>
0x0000000000400eae <+35>: mov $0x1,%ebx
0x0000000000400eb3 <+40>: jmp 0x400ec6 <phase_2+59>
0x0000000000400eb5 <+42>: call 0x401447 <explode_bomb>
0x0000000000400eba <+47>: jmp 0x400eae <phase_2+35>
0x0000000000400ebc <+49>: add $0x1,%rbx
0x0000000000400ec0 <+53>: cmp $0x6,%rbx
0x0000000000400ec4 <+57>: je 0x400ed8 <phase_2+77>
0x0000000000400ec6 <+59>: mov %ebx,%eax
0x0000000000400ec8 <+61>: add -0x4(%rsp,%rbx,4),%eax
0x0000000000400ecc <+65>: cmp %eax,(%rsp,%rbx,4)
0x0000000000400ecf <+68>: je 0x400ebc <phase_2+49>
简单观察这段代码,注意到"phase_2"函数还引用了一个名为"read_six_numbers"的函数,猜测与读取我们的输入有关,于是断点查看该函数的汇编代码:
yaml
(gdb) disas read_six_numbers
Dump of assembler code for function read_six_numbers:
0x0000000000401469 <+0>: sub $0x8,%rsp
0x000000000040146d <+4>: mov %rsi,%rdx
0x0000000000401470 <+7>: lea 0x4(%rsi),%rcx
0x0000000000401474 <+11>: lea 0x14(%rsi),%rax
0x0000000000401478 <+15>: push %rax
0x0000000000401479 <+16>: lea 0x10(%rsi),%rax
0x000000000040147d <+20>: push %rax
0x000000000040147e <+21>: lea 0xc(%rsi),%r9
0x0000000000401482 <+25>: lea 0x8(%rsi),%r8
0x0000000000401486 <+29>: mov $0x4025c3,%esi
0x000000000040148b <+34>: mov $0x0,%eax
0x0000000000401490 <+39>: call 0x400ba0 <__isoc99_sscanf@plt>
0x0000000000401495 <+44>: add $0x10,%rsp
0x0000000000401499 <+48>: cmp $0x5,%eax
0x000000000040149c <+51>: jle 0x4014a3 <read_six_numbers+58>
0x000000000040149e <+53>: add $0x8,%rsp
0x00000000004014a2 <+57>: ret
0x00000000004014a3 <+58>: call 0x401447 <explode_bomb>
果然这段代码有对输入的判断,甚至还会调用"explode_bomb"引爆炸弹。并且在401486行看到一个比较明显的地址0x4025c3,读出其内容为:
(gdb) x/s 0x4025c3
0x4025c3: "%d %d %d %d %d %d"
看到这就知道函数"read_six_numbers"的任务是读取6个整数,那么我们的输入也应该是6个整数.
那么回到函数"phase_2",程序会对输入的第一个值进行判断,若其小于0,则炸弹直接爆炸;之后从400eae行开始进入循环,寄存器ebx存储的值即是循环的计数器,取值从1到6并在取值达到6时退出循环;这个题解题的关键在于400ec6-400ecc三行汇编。因为ebx是循环的计数器,在下面以i代指他的值
yaml
# 将ebx的值赋给eax, 也就是说eax这里的值即为i
400ec6: 89 d8 mov %ebx,%eax
# eax的大小是i这都没问题, 而-0x4(%rsp,%rbx,4)即是M[R(rsp)+4*(i-1)], 也就是我们输入的第i个值
# 那么,这一行代码的意思就是eax += 输入的第i个值
400ec8: 03 44 9c fc add -0x4(%rsp,%rbx,4),%eax
# 判断eax是否与输入的第i+1个值相等
400ecc: 39 04 9c cmp %eax,(%rsp,%rbx,4)
# 总结, 最后判断的是: 输入的第i+1个值 是否与 i+输入的第i个值 相等
所以,需要输入的六个数字需要是,第i个数字是第(i-1)个数字加(i-1)
比如:1 2 4 7 11 16
输入之后也是成功拆解第二个炸弹了:
(gdb) run secret.txt
Starting program: /home/sun/codes/CSAPP/Bomblab/bomb secret.txt
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Phase 1 defused. How about the next one?
That's number 2. Keep going!
最后记录一下完整phase_2汇编代码的注解:
yaml
0000000000400e8b <phase_2>:
400e8b: 53 push %rbx # 保存 %rbx(callee-saved),函数会改变 %rbx 的值
400e8c: 48 83 ec 20 sub $0x20,%rsp # 在栈上分配 0x20(32) 字节的局部空间
400e90: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax # 从 %fs:0x28 读取线程/栈金丝雀值或 TLS 基址到 %rax(用于栈保护)
400e97: 00 00
400e99: 48 89 44 24 18 mov %rax,0x18(%rsp) # 将上面读取的 canary 保存到栈帧中的偏移 0x18 处,后续用于校验
400e9e: 31 c0 xor %eax,%eax # 将 %eax 清零(eax=0);常用于设置参数或初始化寄存器
400ea0: 48 89 e6 mov %rsp,%rsi # 将当前栈指针传给 %rsi,作为 read_six_numbers 的目标缓冲区地址(第2参数)
400ea3: e8 c1 05 00 00 call 401469 <read_six_numbers> # 调用 read_six_numbers,读取 6 个整数到 [%rsp]..[%rsp+20]
400ea8: 83 3c 24 00 cmpl $0x0,(%rsp) # 比较第一个读入的整数 a[0](位于 (%rsp))与 0
400eac: 78 07 js 400eb5 <phase_2+0x2a> # 如果 a[0] 带符号为负 (sign set),跳到 0x400eb5(调用 explode_bomb)
400eae: bb 01 00 00 00 mov $0x1,%ebx # 将 %ebx 置为 1,作为循环索引 i 的初始值(从 a[1] 开始检查)
400eb3: eb 11 jmp 400ec6 <phase_2+0x3b> # 跳转到循环检查的计算/比较入口
400eb5: e8 8d 05 00 00 call 401447 <explode_bomb> # 跳转目标:调用 explode_bomb(a[0] < 0 时爆炸)
400eba: eb f2 jmp 400eae <phase_2+0x23> # 跳回到 0x400eae(汇编布局用,实际爆炸后不会继续正常执行)
400ebc: 48 83 c3 01 add $0x1,%rbx # rbx = rbx + 1;循环索引 i++(准备检查下一个元素)
400ec0: 48 83 fb 06 cmp $0x6,%rbx # 比较 rbx 与 6(检查是否已完成 i = 1..5 的所有检查)
400ec4: 74 12 je 400ed8 <phase_2+0x4d> # 如果 rbx == 6,跳转到成功退出处(通过本阶段)
400ec6: 89 d8 mov %ebx,%eax # eax = ebx(把当前索引 i 复制到 eax,准备参与计算)
400ec8: 03 44 9c fc add -0x4(%rsp,%rbx,4),%eax # eax += *(int *)(%rsp + rbx*4 - 4)
# 地址解释:(%rsp + rbx*4) 指向 a[rbx],再 -4 即 a[rbx-1]
# 因此这里计算 eax = i + a[i-1]
400ecc: 39 04 9c cmp %eax,(%rsp,%rbx,4) # 比较上一步的结果(i + a[i-1])与 a[rbx](即 a[i])
400ecf: 74 eb je 400ebc <phase_2+0x31> # 如果相等,跳回 0x400ebc:i++ 并继续下一轮检查
400ed1: e8 71 05 00 00 call 401447 <explode_bomb> # 否则调用 explode_bomb(比较失败则爆炸)
400ed6: eb e4 jmp 400ebc <phase_2+0x31> # 跳回 i++ 的位置(汇编布局,爆炸后通常不会继续)
400ed8: 48 8b 44 24 18 mov 0x18(%rsp),%rax # 准备函数返回前的金丝雀校验:把栈上的 canary(之前保存的值)读回到 %rax
400edd: 64 48 33 04 25 28 00 xor %fs:0x28,%rax # 将当前线程的 %fs:0x28 与保存的 canary 异或,检查是否被修改
400ee4: 00 00
400ee6: 75 06 jne 400eee <phase_2+0x63> # 如果不为 0(说明金丝雀被篡改),跳到 __stack_chk_fail
400ee8: 48 83 c4 20 add $0x20,%rsp # 清理栈空间(恢复 %rsp)
400eec: 5b pop %rbx # 恢复保存的 %rbx
400eed: c3 ret # 返回,阶段通过
400eee: e8 0d fc ff ff call 400b00 <__stack_chk_fail@plt> # 金丝雀校验失败,调用 __stack_chk_fail(栈溢出保护)
phase 3
获得的汇编代码:
yaml
0000000000400ef3 <phase_3>:
400ef3: 48 83 ec 18 sub $0x18,%rsp
400ef7: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
400efe: 00 00
400f00: 48 89 44 24 08 mov %rax,0x8(%rsp)
400f05: 31 c0 xor %eax,%eax
400f07: 48 8d 4c 24 04 lea 0x4(%rsp),%rcx
400f0c: 48 89 e2 mov %rsp,%rdx
400f0f: be cf 25 40 00 mov $0x4025cf,%esi
400f14: e8 87 fc ff ff call 400ba0 <__isoc99_sscanf@plt>
400f19: 83 f8 01 cmp $0x1,%eax
400f1c: 7e 10 jle 400f2e <phase_3+0x3b>
400f1e: 83 3c 24 07 cmpl $0x7,(%rsp)
400f22: 77 42 ja 400f66 <phase_3+0x73>
400f24: 8b 04 24 mov (%rsp),%eax
400f27: ff 24 c5 40 24 40 00 jmp *0x402440(,%rax,8)
400f2e: e8 14 05 00 00 call 401447 <explode_bomb>
400f33: eb e9 jmp 400f1e <phase_3+0x2b>
400f35: b8 35 02 00 00 mov $0x235,%eax
400f3a: eb 3b jmp 400f77 <phase_3+0x84>
400f3c: b8 a7 01 00 00 mov $0x1a7,%eax
400f41: eb 34 jmp 400f77 <phase_3+0x84>
400f43: b8 2b 02 00 00 mov $0x22b,%eax
400f48: eb 2d jmp 400f77 <phase_3+0x84>
400f4a: b8 6c 00 00 00 mov $0x6c,%eax
400f4f: eb 26 jmp 400f77 <phase_3+0x84>
400f51: b8 f1 02 00 00 mov $0x2f1,%eax
400f56: eb 1f jmp 400f77 <phase_3+0x84>
400f58: b8 3e 00 00 00 mov $0x3e,%eax
400f5d: eb 18 jmp 400f77 <phase_3+0x84>
400f5f: b8 48 02 00 00 mov $0x248,%eax
400f64: eb 11 jmp 400f77 <phase_3+0x84>
400f66: e8 dc 04 00 00 call 401447 <explode_bomb>
400f6b: b8 00 00 00 00 mov $0x0,%eax
400f70: eb 05 jmp 400f77 <phase_3+0x84>
400f72: b8 21 01 00 00 mov $0x121,%eax
400f77: 39 44 24 04 cmp %eax,0x4(%rsp)
400f7b: 74 05 je 400f82 <phase_3+0x8f>
400f7d: e8 c5 04 00 00 call 401447 <explode_bomb>
400f82: 48 8b 44 24 08 mov 0x8(%rsp),%rax
400f87: 64 48 33 04 25 28 00 xor %fs:0x28,%rax
400f8e: 00 00
400f90: 75 05 jne 400f97 <phase_3+0xa4>
400f92: 48 83 c4 18 add $0x18,%rsp
400f96: c3 ret
400f97: e8 64 fb ff ff call 400b00 <__stack_chk_fail@plt>
观察汇编语句,注意到调用了sscanf函数,按 x86-64 System V 调用约定,函数前六个整数/指针参数依次放在
rdi, rsi, rdx, rcx, r8, r9。
看到的寄存器设置顺序是:
lea 0x4(%rsp), %rcx→ 第 4 个参数(rcx)指向rsp+4mov %rsp, %rdx→ 第 3 个参数(rdx)指向rspmov $0x4025cf, %esi→ 第 2 个参数(rsi)是地址0x4025cf- 还有第 1 个参数:函数被调用时
rdi已经含有传进来的字符串(由调用者设置)
这对应 sscanf(rdi, rsi, rdx, rcx) → sscanf(str, format, &first, &second)。所以 rsi 应该是格式串(比如 "%d %d"),而 rdx/rcx 是两个 int*(分别写入到 rsp 和 rsp+4)。
根据这个猜想把0x4025cf,%esi的地址在gdb中调试,看到
(gdb) x/s 0x4025cf
0x4025cf: "%d %d"
这证明了刚才的猜想,第三颗炸弹需要输入两个数字。 而这两个数字分别存在rsp 和 rsp+4中,为了方便起见,把这两个数叫做x和y.
cmpl $0x7,(%rsp) / ja explode:检查 x(在 (%rsp))是否 >7,若大于 7(unsigned >7)就爆炸。
mov (%rsp),%eax;jmp *0x402440(,%rax,8):将x的值放到 %eax,然后跳转到 0x402440 + eax * 8 的地址
而地址 0x402440 + eax * 8 的写法是数组的地址表示方法,eax 也就是x是索引.
那么再往后,多个形如 mov $imm,%eax; jmp 0x400f77:每个 case 把一个立即数 imm 装到 %eax(这是该索引对应的"正确值"),然后跳到统一的比较处。
到这里就可以确定下来了,我们需要输入的两个数,第一个数是索引,第二个数是表中对应索引的值,需要和表一致才不会让炸弹爆炸。那么接下来需要通过gdb调试来找到表中的值。
yaml
0x0000000000400f27 <+52>: jmp *0x402440(,%rax,8)
# 跳转表在反汇编里出现为 0x402440。先查看表里 8 个 8 字节条目(对应索引 0..7 的目标地址):
(gdb) x/8gx 0x402440
0x402440: 0x0000000000400f72 0x0000000000400f35
0x402450: 0x0000000000400f3c 0x0000000000400f43
0x402460: 0x0000000000400f4a 0x0000000000400f51
0x402470: 0x0000000000400f58 0x0000000000400f5f
# 这个表有八个项,但我们只需要一个,所以找第一个,也就是索引0的位置
(gdb) x/i 0x0000000000400f72
0x400f72 <phase_3+127>: mov $0x121,%eax
经过调试,我们就得到了表中的数据,这个炸弹有八种拆解方法,我们选择的是:0
但我们得到的是16进制数,需要再转换成10进制,也就是: 0 289
## phase 4
函数"phase_4"汇编代码:
```yaml
0000000000400fdb <phase_4>:
400fdb: 48 83 ec 18 sub $0x18,%rsp
400fdf: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
400fe6: 00 00
400fe8: 48 89 44 24 08 mov %rax,0x8(%rsp)
400fed: 31 c0 xor %eax,%eax
400fef: 48 8d 4c 24 04 lea 0x4(%rsp),%rcx # 4
400ff4: 48 89 e2 mov %rsp,%rdx # 3
400ff7: be cf 25 40 00 mov $0x4025cf,%esi # 2
400ffc: e8 9f fb ff ff call 400ba0 <__isoc99_sscanf@plt>
401001: 83 f8 02 cmp $0x2,%eax
401004: 75 06 jne 40100c <phase_4+0x31>
401006: 83 3c 24 0e cmpl $0xe,(%rsp)
40100a: 76 05 jbe 401011 <phase_4+0x36>
40100c: e8 36 04 00 00 call 401447 <explode_bomb>
401011: ba 0e 00 00 00 mov $0xe,%edx
401016: be 00 00 00 00 mov $0x0,%esi
40101b: 8b 3c 24 mov (%rsp),%edi
40101e: e8 79 ff ff ff call 400f9c <func4>
401023: 83 f8 03 cmp $0x3,%eax
401026: 75 07 jne 40102f <phase_4+0x54>
401028: 83 7c 24 04 03 cmpl $0x3,0x4(%rsp)
40102d: 74 05 je 401034 <phase_4+0x59>
40102f: e8 13 04 00 00 call 401447 <explode_bomb>
401034: 48 8b 44 24 08 mov 0x8(%rsp),%rax
401039: 64 48 33 04 25 28 00 xor %fs:0x28,%rax
401040: 00 00
401042: 75 05 jne 401049 <phase_4+0x6e>
401044: 48 83 c4 18 add $0x18,%rsp
401048: c3 ret
401049: e8 b2 fa ff ff call 400b00 <__stack_chk_fail@plt>
观察代码,注意到在调用了scanf,并且和炸弹3一样,都是传入两个数字
(gdb) x/s 0x4025cf
0x4025cf: "%d %d"
输入两个数后:
- 检查第一个整数
(%rsp)是否 ≤0xe(14)。 - 把
edi = (%rsp),esi = 0,edx = 14,然后调用func4(edi, esi, edx)(即func4(n,0,14))。 - 检查
func4的返回值是否等于3,并且要求第二个输入0x4(%rsp)等于3。
因此问题化为:找出n ∈ [0,14]使func4(n,0,14) == 3,并把第二个数字设为3。
至此,我们知道了我们输入的两个数的目标:输入的第一个数小于15并且要让func4输出eax=3;输入的第二个数只能是3
那么怎么能让函数"func4"输出eax=3呢?先来看func4的汇编代码:
yaml
0000000000400f9c <func4>:
400f9c: 48 83 ec 08 sub $0x8,%rsp
400fa0: 89 d0 mov %edx,%eax
400fa2: 29 f0 sub %esi,%eax
400fa4: 89 c1 mov %eax,%ecx
400fa6: c1 e9 1f shr $0x1f,%ecx
400fa9: 01 c1 add %eax,%ecx
400fab: d1 f9 sar $1,%ecx
400fad: 01 f1 add %esi,%ecx
400faf: 39 f9 cmp %edi,%ecx
400fb1: 7f 0e jg 400fc1 <func4+0x25>
400fb3: b8 00 00 00 00 mov $0x0,%eax # eax = 0
400fb8: 39 f9 cmp %edi,%ecx
400fba: 7c 11 jl 400fcd <func4+0x31>
400fbc: 48 83 c4 08 add $0x8,%rsp
400fc0: c3 ret
400fc1: 8d 51 ff lea -0x1(%rcx),%edx
400fc4: e8 d3 ff ff ff call 400f9c <func4>
400fc9: 01 c0 add %eax,%eax # eax = 2 * eax
400fcb: eb ef jmp 400fbc <func4+0x20>
400fcd: 8d 71 01 lea 0x1(%rcx),%esi
400fd0: e8 c7 ff ff ff call 400f9c <func4>
400fd5: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax # eax = 2 * eax + 1
400fd9: eb e1 jmp 400fbc <func4+0x20>
可以看到func4的函数体内部多次调用了func4函数,所以应该是一个递归函数。
直接看 func4 中会对 eax 的值造成影响的地方,发现它们分别发生在:
0x400fa0, 0x400fa2, 0x400fb3, 0x400fc9, 0x400fd5。
eax 的值在很多地方都会变化,一个一个跟踪比较复杂。但我们知道最后 eax 的输出结果是 3 。因此可以从函数返回点开始反推:看在什么情况下函数最终能返回 3。
因为 0x400fbc 是所有路径最终的返回点(retq 前),而会跳转到这一点的路径主要有三条:
- 来自
0x400fc9:执行add %eax,%eax,即eax = 2 * eax - 来自
0x400fd5:执行lea 0x1(%rax,%rax,1), %eax,即eax = 2 * eax + 1 - 直接来自
0x400fb3:mov $0x0,%eax,即eax = 0
如果函数最后输出为 3,那么只能来自第二条路径(0x400fd5),因为只有 2 * eax + 1 = 3 可以成立,此时 eax 在调用返回前为 1 。
因此可以推断最后一次递归前的输出是 eax = 1。
接下来,我们继续倒推:要在倒数第二层递归输出 eax = 1,再看这次返回时函数会走哪条路径。
若输出为 1,同样可以考虑两种可能:
- 由
eax = 2 * eax得到:不可能(1 不是 2 的倍数) - 由
eax = 2 * eax + 1得到:需要输入时eax = 0
因此,倒数第二次进入 func4 时的输出应为 eax = 0。
继续向前推:
当输出为 0 时,只能来自 mov $0x0,%eax 这一条路径(对应 cmp %edi, %ecx 相等时)。
说明这一次递归中,%edi == %ecx,程序在此处不再递归,直接返回 0。
从逻辑上看,这个函数是一个递归的二分查找函数 ,每次计算中点 ecx = (high + low)/2,并根据 edi(目标值)与中点比较决定往左或往右递归。返回值通过左、右子树递归结果计算,形成一种"路径编码"的结果。
而最终 eax = 3 表示递归路径走了两步(右→右),即 func4 的搜索区间和输入的 edi 使它递归了两次到右子区间。
而[0,14] 中可以通过两次右子区间递归到的数,只有12和13
- For
n = 13:- Level0 mid=7:
13 > 7→ right - Level1 mid=11:
13 > 11→ right - Level2 mid=13:
13 == 13→ base return 0 - 回溯: level1:
2*0 + 1 = 1; level0:2*1 + 1 = 3
- Level0 mid=7:
- For
n = 12:- Level0 mid=7:
12 > 7→ right - Level1 mid=11:
12 > 11→ right - Level2 mid=13:
12 < 13→ left → callfunc4(12,12,12) - Level3 mid=12:
12 == 12→ base return 0 - 回溯: level2 (left):
2*0 = 0; level1 (right):2*0 + 1 = 1; level0 (right):2*1 + 1 = 3
- Level0 mid=7:
所以答案应该是:
12 3
或者
13 3
phase 5
函数"phase_5"汇编代码:
yaml
000000000040104e <phase_5>:
40104e: 48 83 ec 18 sub $0x18,%rsp
401052: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
401059: 00 00
40105b: 48 89 44 24 08 mov %rax,0x8(%rsp)
401060: 31 c0 xor %eax,%eax
401062: 48 8d 4c 24 04 lea 0x4(%rsp),%rcx
401067: 48 89 e2 mov %rsp,%rdx
40106a: be cf 25 40 00 mov $0x4025cf,%esi
40106f: e8 2c fb ff ff call 400ba0 <__isoc99_sscanf@plt>
401074: 83 f8 01 cmp $0x1,%eax
401077: 7e 57 jle 4010d0 <phase_5+0x82>
401079: 8b 04 24 mov (%rsp),%eax
# 取低 4 位:eax = eax & 0xf (把索引限制为 0..15)
40107c: 83 e0 0f and $0xf,%eax
40107f: 89 04 24 mov %eax,(%rsp)
401082: 83 f8 0f cmp $0xf,%eax
401085: 74 2f je 4010b6 <phase_5+0x68>
401087: b9 00 00 00 00 mov $0x0,%ecx
40108c: ba 00 00 00 00 mov $0x0,%edx
# (循环开始)
401091: 83 c2 01 add $0x1,%edx
# edx++:进入一次跳表访问(计数)
401094: 48 98 cltq
# sign-extend %eax -> %rax(把索引装入 %rax 做 64 位地址计算)
# 此时 %eax 是当前索引(初始为 masked first input)
401096: 8b 04 85 80 24 40 00 mov 0x402480(,%rax,4),%eax
# %eax = TABLE[ %rax ] (从数据表 0x402480 以 4 字节步长索引)
# 注意:读取后 %eax 变为表中值(下一轮会用它作为索引)
40109d: 01 c1 add %eax,%ecx
# ecx += eax(累加表项到 ecx)
40109f: 83 f8 0f cmp $0xf,%eax
4010a2: 75 ed jne 401091 <phase_5+0x43>
# 如果读出的表项 != 15,继续循环(回到 edx++ 并再读 TABLE[ eax ])
# 若表项 == 15,则跳出循环(到下一条)
4010a4: c7 04 24 0f 00 00 00 movl $0xf,(%rsp)
# 将 0xf (15) 写入 (%rsp)(覆盖第一个输入),无实际必要,可能是清理或标记
4010ab: 83 fa 03 cmp $0x3,%edx
4010ae: 75 06 jne 4010b6 <phase_5+0x68>
# 如果循环迭代次数 edx != 3 则爆炸(需要恰好 3 次访问才到 15)
4010b0: 39 4c 24 04 cmp %ecx,0x4(%rsp)
4010b4: 74 05 je 4010bb <phase_5+0x6d>
# 比较累加和 ecx 与第二个输入(在 0x4(%rsp))
# 若相等通过,否则爆炸
4010b6: e8 8c 03 00 00 call 401447 <explode_bomb>
4010bb: 48 8b 44 24 08 mov 0x8(%rsp),%rax
4010c0: 64 48 33 04 25 28 00 00 xor %fs:0x28,%rax
4010c7: 00 00
4010c9: 75 0c jne 4010d7 <phase_5+0x89>
4010cb: 48 83 c4 18 add $0x18,%rsp
4010cf: c3 ret
4010d0: e8 72 03 00 00 call 401447 <explode_bomb>
4010d5: eb a2 jmp 401079 <phase_5+0x2b>
4010d7: e8 24 fa ff ff call 400b00 <__stack_chk_fail@plt>
还是scanf,直接读0x4025cf的内容,得到:
(gdb) x/s 0x4025cf
0x4025cf: "%d %d"
输入的值被存在%rsp 和 (%rsp)+4 中。
0x401079-0x40107f,将我们输入的第一个值对16取余,并将其存到eax寄存器
0x401082检查第一个输入mod16后的结果是否是15,若是15则直接爆炸
0x40108c-0x4010a2是一个循环函数,当eax=0xf时退出循环;edx是循环的计数器,循环几次edx就等于几;ecx是eax的一个累加器,刚进入循环时初值为0,eax每次循环的值都会加到ecx上。
大致逻辑梳理成伪代码是:
c
step = 0; sum = 0;
do {
step += 1;
val = TABLE[idx]; // TABLE 位于 0x402480,按 4 字节索引
sum += val;
idx = val; // 下一轮用 val 作为新的索引
} while (val != 15);
这道题关键的关键在于0x402480这个地址,读取它可以知道这个数组为:
(gdb) x/16dw 0x402480
0x402480 <array.3415>: 10 2 14 7
0x402490 <array.3415+16>: 8 12 15 11
0x4024a0 <array.3415+32>: 0 4 1 13
0x4024b0 <array.3415+48>: 3 9 6 5
| a[0] | a[1] | a[2] | a[3] | a[4] | a[5] | a[6] | a[7] | a[8] | a[9] | a[10] | a[11] | a[12] | a[13] | a[14] | a[15] |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| a | 2 | e | 7 | 8 | c | f | b | 0 | 4 | 1 | d | 3 | 9 | 6 | 5 |
0x4010ab判断edx是否等于3,若不等于3则直接爆炸,说明上面的循环要进行三次就退出。
唯一满足"恰好 3 步到达 15 且三次读出的值之和等于第二输入"的起点是 start = 2
start=2 -> table[2]=14 -> table[14]=6 -> table[6]=15
values seen: 14, 6, 15
steps = 3
sum = 14 + 6 + 15 = 35
所以答案是:
2 35
phase 6
yaml
00000000004010dc <phase_6>:
4010dc: 41 56 push %r14
4010de: 41 55 push %r13
4010e0: 41 54 push %r12
4010e2: 55 push %rbp
4010e3: 53 push %rbx
4010e4: 48 83 ec 60 sub $0x60,%rsp
4010e8: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
4010ef: 00 00
4010f1: 48 89 44 24 58 mov %rax,0x58(%rsp)
4010f6: 31 c0 xor %eax,%eax
4010f8: 48 89 e6 mov %rsp,%rsi
4010fb: e8 69 03 00 00 call 401469 <read_six_numbers>
401100: 49 89 e4 mov %rsp,%r12
401103: 49 89 e5 mov %rsp,%r13
401106: 41 be 00 00 00 00 mov $0x0,%r14d
40110c: eb 25 jmp 401133 <phase_6+0x57>
40110e: e8 34 03 00 00 call 401447 <explode_bomb>
401113: eb 2d jmp 401142 <phase_6+0x66>
401115: 83 c3 01 add $0x1,%ebx
401118: 83 fb 05 cmp $0x5,%ebx
40111b: 7f 12 jg 40112f <phase_6+0x53>
40111d: 48 63 c3 movslq %ebx,%rax
401120: 8b 04 84 mov (%rsp,%rax,4),%eax
401123: 39 45 00 cmp %eax,0x0(%rbp)
401126: 75 ed jne 401115 <phase_6+0x39>
401128: e8 1a 03 00 00 call 401447 <explode_bomb>
40112d: eb e6 jmp 401115 <phase_6+0x39>
40112f: 49 83 c5 04 add $0x4,%r13
401133: 4c 89 ed mov %r13,%rbp
401136: 41 8b 45 00 mov 0x0(%r13),%eax
40113a: 83 e8 01 sub $0x1,%eax
40113d: 83 f8 05 cmp $0x5,%eax
401140: 77 cc ja 40110e <phase_6+0x32>
401142: 41 83 c6 01 add $0x1,%r14d
401146: 41 83 fe 06 cmp $0x6,%r14d
40114a: 74 05 je 401151 <phase_6+0x75>
40114c: 44 89 f3 mov %r14d,%ebx
40114f: eb cc jmp 40111d <phase_6+0x41>
401151: 49 8d 4c 24 18 lea 0x18(%r12),%rcx
401156: ba 07 00 00 00 mov $0x7,%edx
40115b: 89 d0 mov %edx,%eax
40115d: 41 2b 04 24 sub (%r12),%eax
401161: 41 89 04 24 mov %eax,(%r12)
401165: 49 83 c4 04 add $0x4,%r12
401169: 4c 39 e1 cmp %r12,%rcx
40116c: 75 ed jne 40115b <phase_6+0x7f>
40116e: be 00 00 00 00 mov $0x0,%esi
401173: eb 1a jmp 40118f <phase_6+0xb3>
401175: 48 8b 52 08 mov 0x8(%rdx),%rdx
401179: 83 c0 01 add $0x1,%eax
40117c: 39 c8 cmp %ecx,%eax
40117e: 75 f5 jne 401175 <phase_6+0x99>
401180: 48 89 54 f4 20 mov %rdx,0x20(%rsp,%rsi,8)
401185: 48 83 c6 01 add $0x1,%rsi
401189: 48 83 fe 06 cmp $0x6,%rsi
40118d: 74 14 je 4011a3 <phase_6+0xc7>
40118f: 8b 0c b4 mov (%rsp,%rsi,4),%ecx
401192: b8 01 00 00 00 mov $0x1,%eax
401197: ba d0 32 60 00 mov $0x6032d0,%edx
40119c: 83 f9 01 cmp $0x1,%ecx
40119f: 7f d4 jg 401175 <phase_6+0x99>
4011a1: eb dd jmp 401180 <phase_6+0xa4>
4011a3: 48 8b 5c 24 20 mov 0x20(%rsp),%rbx
4011a8: 48 8b 44 24 28 mov 0x28(%rsp),%rax
4011ad: 48 89 43 08 mov %rax,0x8(%rbx)
4011b1: 48 8b 54 24 30 mov 0x30(%rsp),%rdx
4011b6: 48 89 50 08 mov %rdx,0x8(%rax)
4011ba: 48 8b 44 24 38 mov 0x38(%rsp),%rax
4011bf: 48 89 42 08 mov %rax,0x8(%rdx)
4011c3: 48 8b 54 24 40 mov 0x40(%rsp),%rdx
4011c8: 48 89 50 08 mov %rdx,0x8(%rax)
4011cc: 48 8b 44 24 48 mov 0x48(%rsp),%rax
4011d1: 48 89 42 08 mov %rax,0x8(%rdx)
4011d5: 48 c7 40 08 00 00 00 movq $0x0,0x8(%rax)
4011dc: 00
4011dd: bd 05 00 00 00 mov $0x5,%ebp
4011e2: eb 09 jmp 4011ed <phase_6+0x111>
4011e4: 48 8b 5b 08 mov 0x8(%rbx),%rbx
4011e8: 83 ed 01 sub $0x1,%ebp
4011eb: 74 11 je 4011fe <phase_6+0x122>
4011ed: 48 8b 43 08 mov 0x8(%rbx),%rax
4011f1: 8b 00 mov (%rax),%eax
4011f3: 39 03 cmp %eax,(%rbx)
4011f5: 7d ed jge 4011e4 <phase_6+0x108>
4011f7: e8 4b 02 00 00 call 401447 <explode_bomb>
4011fc: eb e6 jmp 4011e4 <phase_6+0x108>
4011fe: 48 8b 44 24 58 mov 0x58(%rsp),%rax
401203: 64 48 33 04 25 28 00 xor %fs:0x28,%rax
40120a: 00 00
40120c: 75 0d jne 40121b <phase_6+0x13f>
40120e: 48 83 c4 60 add $0x60,%rsp
401212: 5b pop %rbx
401213: 5d pop %rbp
401214: 41 5c pop %r12
401216: 41 5d pop %r13
401218: 41 5e pop %r14
40121a: c3 ret
40121b: e8 e0 f8 ff ff call 400b00 <__stack_chk_fail@plt>
在0x4010fb处调用的函数"read_six_numbers"就知道这个题要求的输入是6个整数,和第二颗炸弹一样。
在之后的汇编代码中有一个循环部分:
401136: mov 0x0(%r13),%eax ; 当前数
40113a: sub $0x1,%eax ; eax = x - 1
40113d: cmp $0x5,%eax ; 比较 x-1 与 5
401140: ja explode_bomb ; 如果 x < 1 或 x > 6,则爆炸
每个输入必须在 1~6 之间。
接下来
40111d~401128
cmp (%rsp,%rax,4),%eax
jne ...
explode_bomb
→ 遍历前面的数,如果有重复就爆炸。
6 个数字必须互不相同且在 [1,6] 之间。
再然后
40115b~40116c:
mov $7,%edx
sub (%r12),%eax
mov %eax,(%r12)
这段循环对每个数做:num = 7 - num
接下来:
401197: mov $0x6032d0,%edx
40119c: cmp $0x1,%ecx
40119f: jg 401175
401175~401180: 循环 rd = rd->next
401180: mov %rdx, 0x20(%rsp,%rsi,8)
说明 %rdx = 0x6032d0 是链表头节点地址。
链表中每个节点的结构类似:
c
struct node {
int value;
struct node *next;
};
那么大致的循环逻辑应该是:
c
for each i in [0..5]:
index = numbers[i] // 已经被 7-x 转换了
p = head
for k in range(1, index):
p = p->next
node_ptr[i] = p
即:根据每个数(1-6),从链表头开始走 (index-1) 步,
将对应的节点地址存到一个数组里。
这 6 个数决定了节点的重排顺序。
然后重连链表
4011a3~4011d5:
mov 0x20(%rsp),%rbx
mov 0x28(%rsp),%rax
mov %rax,0x8(%rbx)
...
最后把最后一个节点 next=0
# 确保链表递减排列
4011f3: cmp (%rbx),%eax
4011f5: jge 4011e4
4011f7: explode_bomb
那么到这里就很清楚了
- 6 个互异、1~6;
- 映射:每个输入 →
7-x; - 映射结果决定链表节点选择;
- 节点值需递减;
- 从节点值的降序索引反推回输入。
所以在gdb调试中查看链表的内容:
(gdb) x/24gw 0x6032d0
0x6032d0 <node1>: 634 1 0 0
0x6032e0 <node2>: 851 2 6304464 0
0x6032f0 <node3>: 921 3 6304480 0
0x603300 <node4>: 310 4 6304496 0
0x603310 <node5>: 585 5 6304512 0
0x603320 <node6>: 138 6 6304528 0
降序排列是:
node3: 921
node2: 851
node1: 634
node5: 585
node4: 310
node6: 138
对应的节点标号是:[3, 2, 1, 5, 4, 6]
反推回原输入为 7 - t:[7-3, 7-2, 7-1, 7-5, 7-4, 7-6] = [4, 5, 6, 2, 3, 1]
所以答案就是:4 5 6 2 3 1
phase secret
在汇编代码中找了一下,隐藏函数"secret_phase"的唯一入口在函数"phase_defused",那么就先观察这个函数的汇编代码:
yaml
00000000004015d6 <phase_defused>:
4015d6: 48 83 ec 78 sub $0x78,%rsp
4015da: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
4015e1: 00 00
4015e3: 48 89 44 24 68 mov %rax,0x68(%rsp)
4015e8: 31 c0 xor %eax,%eax
4015ea: 83 3d 7b 21 20 00 06 cmpl $0x6,0x20217b(%rip) # 60376c <num_input_strings>
4015f1: 74 15 je 401608 <phase_defused+0x32>
4015f3: 48 8b 44 24 68 mov 0x68(%rsp),%rax
4015f8: 64 48 33 04 25 28 00 xor %fs:0x28,%rax
4015ff: 00 00
401601: 75 67 jne 40166a <phase_defused+0x94>
401603: 48 83 c4 78 add $0x78,%rsp
401607: c3 ret
401608: 4c 8d 44 24 10 lea 0x10(%rsp),%r8
40160d: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx
401612: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
401617: be 19 26 40 00 mov $0x402619,%esi
40161c: bf 70 38 60 00 mov $0x603870,%edi
401621: e8 7a f5 ff ff call 400ba0 <__isoc99_sscanf@plt>
401626: 83 f8 03 cmp $0x3,%eax
401629: 74 0c je 401637 <phase_defused+0x61>
40162b: bf 58 25 40 00 mov $0x402558,%edi
401630: e8 ab f4 ff ff call 400ae0 <puts@plt>
401635: eb bc jmp 4015f3 <phase_defused+0x1d>
401637: be 22 26 40 00 mov $0x402622,%esi
40163c: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi
401641: e8 04 fd ff ff call 40134a <strings_not_equal>
401646: 85 c0 test %eax,%eax
401648: 75 e1 jne 40162b <phase_defused+0x55>
40164a: bf f8 24 40 00 mov $0x4024f8,%edi
40164f: e8 8c f4 ff ff call 400ae0 <puts@plt>
401654: bf 20 25 40 00 mov $0x402520,%edi
401659: e8 82 f4 ff ff call 400ae0 <puts@plt>
40165e: b8 00 00 00 00 mov $0x0,%eax
401663: e8 f7 fb ff ff call 40125f <secret_phase>
401668: eb c1 jmp 40162b <phase_defused+0x55>
40166a: e8 91 f4 ff ff call 400b00 <__stack_chk_fail@plt>
隐藏关被 phase_defused 触发的条件是:全局计数 num_input_strings 等于 6 (即程序已经记录到了 6 个输入字符串),并且 phase_defused 读入的一行满足 sscanf 的格式且第三个读入的字符串 等于 程序中存放在地址 0x402622 的那个秘密字符串。要找这个"字符串" ------ 直接在 gdb 里查看静态数据 0x402622 就能得到它;把它作为你在第四个炸弹后附加的那一行中的第三个字段传入即可触发隐藏关(前提是 num_input_strings==6)。
(gdb) x/s 0x402619
0x402619: "%d %d %s"
进入后,函数用 sscanf(格式字符串在地址 0x402619)把 一行 解析到三个缓冲区(位于栈:rsp+8, rsp+0xc, rsp+0x10)。然后它比较第三个缓冲区(rsp+0x10)与静态字符串在 0x402622,调试查看这个位置。
(gdb) x/s 0x402622
0x402622: "urxvt"
所以,我们进入secret phase的密语是"urxvt"
之后查看函数"secret_phase"的汇编代码:
yaml
000000000040125f <secret_phase>:
40125f: 53 push %rbx
401260: e8 43 02 00 00 call 4014a8 <read_line>
401265: ba 0a 00 00 00 mov $0xa,%edx
40126a: be 00 00 00 00 mov $0x0,%esi
40126f: 48 89 c7 mov %rax,%rdi
401272: e8 09 f9 ff ff call 400b80 <strtol@plt>
401277: 48 89 c3 mov %rax,%rbx
40127a: 8d 40 ff lea -0x1(%rax),%eax
40127d: 3d e8 03 00 00 cmp $0x3e8,%eax
401282: 77 27 ja 4012ab <secret_phase+0x4c>
401284: 89 de mov %ebx,%esi
401286: bf f0 30 60 00 mov $0x6030f0,%edi
40128b: e8 90 ff ff ff call 401220 <fun7>
401290: 83 f8 04 cmp $0x4,%eax
401293: 74 05 je 40129a <secret_phase+0x3b>
401295: e8 ad 01 00 00 call 401447 <explode_bomb>
40129a: bf 08 24 40 00 mov $0x402408,%edi
40129f: e8 3c f8 ff ff call 400ae0 <puts@plt>
4012a4: e8 2d 03 00 00 call 4015d6 <phase_defused>
4012a9: 5b pop %rbx
4012aa: c3 ret
4012ab: e8 97 01 00 00 call 401447 <explode_bomb>
4012b0: eb d2 jmp 401284 <secret_phase+0x25>
secret_phase的汇编还是比较简短的,也容易理解
secret_phase 做了以下事情:
-
调用
read_line→ 读入一行字符串。 -
调用
strtol(line, 0, 10)→ 把输入的字符串转换为一个整数(十进制)。 -
检查输入是否在
1 ≤ x ≤ 1001(因为比较eax-1与0x3e8=1000,超出就炸)。 -
调用
eax = fun7(0x6030f0, input)其中
0x6030f0是一棵二叉树的根节点地址。 -
如果
func7的返回值 == 4 → 成功(打印一句话,进入 phase_defused)。否则炸。
所以需要找到一个整数 x,使得 func7(0x6030f0, x) == 4。
那么接下来就看func7的函数体了:
yaml
0000000000401220 <fun7>:
401220: 48 85 ff test %rdi,%rdi
401223: 74 34 je 401259 <fun7+0x39>
401225: 48 83 ec 08 sub $0x8,%rsp
401229: 8b 17 mov (%rdi),%edx
40122b: 39 f2 cmp %esi,%edx
40122d: 7f 0e jg 40123d <fun7+0x1d>
40122f: b8 00 00 00 00 mov $0x0,%eax
401234: 39 f2 cmp %esi,%edx
401236: 75 12 jne 40124a <fun7+0x2a>
401238: 48 83 c4 08 add $0x8,%rsp
40123c: c3 ret
40123d: 48 8b 7f 08 mov 0x8(%rdi),%rdi
401241: e8 da ff ff ff call 401220 <fun7>
401246: 01 c0 add %eax,%eax
401248: eb ee jmp 401238 <fun7+0x18>
40124a: 48 8b 7f 10 mov 0x10(%rdi),%rdi
40124e: e8 cd ff ff ff call 401220 <fun7>
401253: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax
401257: eb df jmp 401238 <fun7+0x18>
401259: b8 ff ff ff ff mov $0xffffffff,%eax
40125e: c3 ret
又是递归,而且还很晦涩难懂,不过可以先分析一下:
func7 的开头如下:
401220: test %rdi,%rdi
401223: je 401259 <fun7+0x39>
这表示:如果 %rdi == 0,直接返回(最后 mov eax, -1)。
继续看:
401229: 8b 17 mov (%rdi),%edx
这行取出节点结构的第一个字段(偏移量 0),放进 %edx。
接着:
40123d: 48 8b 7f 08 mov 0x8(%rdi),%rdi
40124a: 48 8b 7f 10 mov 0x10(%rdi),%rdi
这说明:偏移 +0x8 和 +0x10 处是节点的另外两个数据;
而有这类似的数据结构的,就是二叉树的节点,不过现在还不是很确定
不过可以看一下在调用func7时传入的参数 edi是什么。
在gdb中调试多次后,发现通过x/24gw 0x6030f0命令可以显示出一个比较清晰的结构。
(gdb) x/24gw 0x6030f0
0x6030f0 <n1>: U"$"
0x6030f8 <n1+8>: U"\x603110"
0x603100 <n1+16>: U"\x603130"
0x603108: U""
0x60310c: U""
0x603110 <n21>: U"\b"
0x603118 <n21+8>: U"\x603190"
0x603120 <n21+16>: U"\x603150"
0x603128: U""
0x60312c: U""
0x603130 <n22>: U"2"
0x603138 <n22+8>: U"\x603170"
0x603140 <n22+16>: U"\x6031b0"
0x603148: U""
0x60314c: U""
--Type <RET> for more, q to quit, c to continue without paging--
0x603150 <n32>: U"\026"
0x603158 <n32+8>: U"\x603270"
0x603160 <n32+16>: U"\x603230"
0x603168: U""
0x60316c: U""
0x603170 <n33>: U"-"
0x603178 <n33+8>: U"\x6031d0"
0x603180 <n33+16>: U"\x603290"
0x603188: U""
看这个结构,就可以明显看出是二叉树了,
节点n1,存储一个值"$",有两个地址,分别指向:0x603110(n21)和0x603130(n22)
(这个储存的值有点不对劲,是因为输出的整数被打印成了Unicode编码的格式,查了一下ASCII码表,"$" 对应的是 36,所以n1实际存储的值应该是36)
整理了一下各节点,现在得到的树大概是这样,不过并不完整
[36]
/ \
[8] [50]
/ \ / \
[??] [22] [45] [??]
我们目前有了前两层,剩下的地址还指向:
- 0x603190(8的左)
- 0x603150(8的右 = 22)
- 0x603170(50的左 = 45)
- 0x6031b0(50的右)
需要在gdb中继续调试来找到完整的树:
(gdb) x/24gw 0x603190
(gdb) x/24gw 0x6031b0
(gdb) x/24gw 0x603230
(gdb) x/24gw 0x603270
(gdb) x/24gw 0x6031d0
(gdb) x/24gw 0x603290
返回结果有点长,就不列出了
最后整理完的结构是:
[36]
/ \
[8] [50]
/ \ / \
[6] [22] [45] [107]
/ \ / \ / \ / \
[1] [7][20][35][40][47][99][169]
很显然,这棵树很好看,是一棵二叉搜索树,那大胆推断func7函数应该就是用于在二叉搜索树中搜索的。
那么回过头来看func7,函数体内对于输出值 %eax都做了什么操作
cmpq $0x0, (%rdi)检查当前节点是否为空。如果为空,跳到 mov $-1, %eax。所以空节点时返回 -1。
mov (%rdi), %eax取出当前节点的值,放进 %eax。例如,这个值可能是 36。
cmp %esi, %eax比较目标值和当前节点的值:
- 如果
key < node->value:执行jg,走左子树。 - 如果
key > node->value:执行jl,走右子树。 - 如果相等:
mov $0, %eax,直接返回 0。
所以,eax = 0 表示找到了目标节点。
如果走左子树:
asm
mov 0x8(%rdi), %rdi
call func7
add %eax, %eax # eax = eax * 2
add $0, %eax # (其实就是保持 *2)
所以返回值 = 左子树返回值 × 2。
如果走右子树:
asm
mov 0x10(%rdi), %rdi
call func7
lea 0x1(%rax,%rax), %eax # eax = eax*2 + 1
所以返回值 = 右子树返回值 × 2 + 1。
总结一下 %eax 的意义,每次往下走:
- 左子树 →
*2 - 右子树 →
*2 + 1 - 找到目标 →
0 - 找不到 →
-1
也就是说,%eax 编码了一条从根节点到目标节点的"路径"。
而我们需要的值是4,也就是二进制100,但是返回值的二进制是"路径位"自下而上(从叶到根)排列的结果(因为输入40炸弹爆炸了(bushi))
相对应的不是 "右-左-左" ,而是"左-左-右"
对应二叉树中的节点值就是 7
所以隐藏炸弹的答案是 7
总结
这个bomblab整体难度好高,花费了好长时间,也整理了好久汇编的笔记,不过做完之后感觉对汇编和gdb的感悟深了很多。