CSAPP实验2:Bomb

介绍

(貌似有好多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+4
  • mov %rsp, %rdx → 第 3 个参数(rdx)指向 rsp
  • mov $0x4025cf, %esi → 第 2 个参数(rsi)是地址 0x4025cf
  • 还有第 1 个参数:函数被调用时 rdi 已经含有传进来的字符串(由调用者设置)

这对应 sscanf(rdi, rsi, rdx, rcx)sscanf(str, format, &first, &second)。所以 rsi 应该是格式串(比如 "%d %d"),而 rdx/rcx 是两个 int*(分别写入到 rsprsp+4)。

根据这个猜想把0x4025cf,%esi的地址在gdb中调试,看到

复制代码
(gdb) x/s 0x4025cf
0x4025cf:       "%d %d"

这证明了刚才的猜想,第三颗炸弹需要输入两个数字。 而这两个数字分别存在rsprsp+4中,为了方便起见,把这两个数叫做xy.

cmpl $0x7,(%rsp) / ja explode:检查 x(在 (%rsp))是否 >7,若大于 7(unsigned >7)就爆炸。

mov (%rsp),%eaxjmp *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 = 0edx = 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
  • 直接来自 0x400fb3mov $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
  • For n = 12:
    • Level0 mid=7: 12 > 7 → right
    • Level1 mid=11: 12 > 11 → right
    • Level2 mid=13: 12 < 13 → left → call func4(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

所以答案应该是:

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 做了以下事情:

  1. 调用 read_line → 读入一行字符串。

  2. 调用 strtol(line, 0, 10) → 把输入的字符串转换为一个整数(十进制)。

  3. 检查输入是否在 1 ≤ x ≤ 1001(因为比较 eax-10x3e8=1000,超出就炸)。

  4. 调用eax = fun7(0x6030f0, input)

    其中 0x6030f0 是一棵二叉树的根节点地址

  5. 如果 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的感悟深了很多。

相关推荐
我在人间贩卖青春5 天前
汇编之伪指令
汇编·伪指令
我在人间贩卖青春5 天前
汇编之伪操作
汇编·伪操作
济6175 天前
FreeRTOS基础--堆栈概念与汇编指令实战解析
汇编·嵌入式·freertos
myloveasuka5 天前
汇编TEST指令
汇编
我在人间贩卖青春5 天前
汇编编程驱动LED
汇编·点亮led
我在人间贩卖青春5 天前
汇编和C编程相互调用
汇编·混合编程
myloveasuka6 天前
寻址方式笔记
汇编·笔记·计算机组成原理
请输入蚊子6 天前
《操作系统真象还原》 第六章 完善内核
linux·汇编·操作系统·bochs·操作系统真像还原
myloveasuka6 天前
指令格式举例
汇编·笔记·计算机组成原理
我在人间贩卖青春7 天前
汇编之分支跳转指令
汇编·arm·分支跳转