---
title: 高地特供版CSAPP Bomb Lab全流程攻略
date: 2025-02-24 15:09:11
tags: [技术, 学习, 生活]
---

这篇文章记录高地CSAPP课程Bomblab实验操作流程，仅供参考交流（答案是随机生成的和学号相关）。

笔者实验环境为Archlinux/CachyOS，使用lldb作为调试器（和gdb操作差不多），其余用到的工具主要为objdump，strings，neovim/helix和zellij，全程开源环境不使用IDA。

## **Phase_1**

### **静态分析**

#### **`strings`扫描**
```bash
strings bomb_linux
```
先用strings寻找可能与`phase_1`相关的字符串或函数名，运气好说不定能直接找到密码毕竟是第一题。
![strings](/images/phase1_strings.png)
  - 结果没有明文密码无法直接秒掉第一问，可惜。
  - 但是找到`GenerateRandomString`函数可能与密码生成相关。

#### **用`objdump`反汇编**
```bash
objdump -d bomb_linux > bomb.asm
```
搜索`GenerateRandomString`和`phase_1`函数的汇编代码。
    ```assembly
    401b53 <phase_1>:
      401b53: endbr64
      401b57: push   %rbp
      401b58: mov    %rsp,%rbp
      401b5b: sub    $0x20,%rsp
      401b5f: mov    %rdi,-0x18(%rbp)
      401b63: lea    -0xb(%rbp),%rax
      401b67: mov    %rax,%rdi
      401b6a: callq  401ac1 <GenerateRandomString>  # 调用密码生成函数
      401b6f: lea    -0xb(%rbp),%rdx                # 生成的字符串地址%rbp-0xb存入%rdx，即密码存储位置
      401b73: mov    -0x18(%rbp),%rax
      401b77: mov    %rdx,%rsi
      401b7a: mov    %rax,%rdi
      401b7d: callq  401c0c <string_compare>        # 调用字符串比较函数
      401b82: test   %eax,%eax
      401b84: je     401b8d <phase_1+0x3a>
      401b86: callq  401d67 <explode_bomb>          # 比较失败则引爆炸弹
    ```
 - `phase_1`调用`GenerateRandomString`生成一个字符串。
 - 用户输入的字符串需要与此生成的字符串完全匹配。

---

### **动态调试**
![phase_1](/images/phase1.png)
下面是phase_1求解的完整流程：
```lldb
lldb bomb_linux <你的学号后六位>
(lldb) b phase_1                    # 在phase_1入口断点
(lldb) run                          # 从入口开始执行
请输入第1级的密码：114514           # 随便输入触发断点
(lldb) b 0x401b6f                   # 在GenerateRandomString返回后断点
(lldb) continue                     # 继续执行
(lldb) x/s $rbp - 0xb               # 计算字符串地址（-0xb偏移量）
0x7fffffffdaf5: "mJHurpQZtY"        # 轻松拿下，这里是根据学号伪随机生成的哦
```
将得到的密码保存入bomb_<学号后六位>.txt即可，避免后续重复输入。

---

## **Phase_2**

### **静态分析**

这道题目还是比较一目了然的，观察`phase_2`代码不难发现其实构建了一张跳转表：
```assembly
0000000000401b8e <phase_2>:
  401b8e:	f3 0f 1e fa          	endbr64
  401b92:	55                   	push   %rbp
  401b93:	48 89 e5             	mov    %rsp,%rbp
  401b96:	48 83 ec 10          	sub    $0x10,%rsp
  401b9a:	48 89 7d f8          	mov    %rdi,-0x8(%rbp)
  401b9e:	bf 10 00 00 00       	mov    $0x10,%edi
  401ba3:	e8 05 fb ff ff       	call   4016ad <GenerateRandomNumber>
  401ba8:	48 8b 05 71 6c 00 00 	mov    0x6c71(%rip),%rax        # 408820 <rand_div>
  401baf:	48 83 f8 0f          	cmp    $0xf,%rax
  401bb3:	0f 87 16 01 00 00    	ja     401ccf <phase_2+0x141>
  401bb9:	48 8d 14 85 00 00 00 	lea    0x0(,%rax,4),%rdx
  401bc0:	00 
  401bc1:	48 8d 05 4c 4a 00 00 	lea    0x4a4c(%rip),%rax        # 406614 <_IO_stdin_used+0x614>
  401bc8:	8b 04 02             	mov    (%rdx,%rax,1),%eax
  401bcb:	48 98                	cltq
  401bcd:	48 8d 15 40 4a 00 00 	lea    0x4a40(%rip),%rdx        # 406614 <_IO_stdin_used+0x614>
  401bd4:	48 01 d0             	add    %rdx,%rax
  401bd7:	3e ff e0             	notrack jmp *%rax
  401bda:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  401bde:	48 89 c7             	mov    %rax,%rdi
  401be1:	e8 f2 00 00 00       	call   401cd8 <phase_2_0>
  401be6:	e9 ea 00 00 00       	jmp    401cd5 <phase_2+0x147>
  401beb:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  401bef:	48 89 c7             	mov    %rax,%rdi
  401bf2:	e8 8b 01 00 00       	call   401d82 <phase_2_1>
  401bf7:	e9 d9 00 00 00       	jmp    401cd5 <phase_2+0x147>
  401bfc:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  401c00:	48 89 c7             	mov    %rax,%rdi
  ...
```
这里面需要注意的关键点是rand_div，它会决定你的跳转方向，而你的学号又决定了它的取值。然后是`GenerateRandomNumber`这个函数的原理需要了解一下，而这个函数将在跳转前后分别调用一次，第一次决定你的跳转方向，第二次则决定了你的密码线索。

---

### **动态调试**
理解原理就没什么难度了，自己找几个断点打好然后关注一下`rand_div`的值就好，观察自己的学号向哪个函数跳转并理解相应函数计算即可，比如我这里向`phase_2_14`跳转：
![phase_2_14](/images/phase_2_14.png)

而除了`phase_2_14`还有其他函数也是非常好理解的，第二题依旧可以轻松拿下。

---
## **Phase_3**

### **静态分析**

和Phase_2一样开局先跳转尽可能防止同学们答案雷同互相帮助（bushi

本体其实没有什么好说的，这里我跳转的方向是`Phase_3_5`简要解释一下可供参考：

```assembly
0000000000403001 <phase_3_5>:
  403001:	f3 0f 1e fa          	endbr64
  403005:	55                   	push   %rbp
  403006:	48 89 e5             	mov    %rsp,%rbp
  403009:	48 83 ec 20          	sub    $0x20,%rsp
  40300d:	48 89 7d e8          	mov    %rdi,-0x18(%rbp)
  403011:	c7 45 fc 00 00 00 00 	movl   $0x0,-0x4(%rbp)
  403018:	c7 45 f8 00 00 00 00 	movl   $0x0,-0x8(%rbp)
  40301f:	48 8d 4d f0          	lea    -0x10(%rbp),%rcx
  403023:	48 8d 55 f4          	lea    -0xc(%rbp),%rdx
  403027:	48 8b 45 e8          	mov    -0x18(%rbp),%rax
  40302b:	48 8d 35 5a 36 00 00 	lea    0x365a(%rip),%rsi        # 40668c <_IO_stdin_used+0x68c>
  403032:	48 89 c7             	mov    %rax,%rdi
  403035:	b8 00 00 00 00       	mov    $0x0,%eax
  40303a:	e8 51 e1 ff ff       	call   401190 <__isoc99_sscanf@plt>
  40303f:	89 45 f8             	mov    %eax,-0x8(%rbp)
  403042:	83 7d f8 01          	cmpl   $0x1,-0x8(%rbp)
  403046:	7f 05                	jg     40304d <phase_3_5+0x4c>
  403048:	e8 a9 2b 00 00       	call   405bf6 <explode_bomb>
  40304d:	bf 08 00 00 00       	mov    $0x8,%edi
  403052:	e8 56 e6 ff ff       	call   4016ad <GenerateRandomNumber>
  403057:	8b 45 f4             	mov    -0xc(%rbp),%eax
  40305a:	48 63 d0             	movslq %eax,%rdx
  40305d:	48 8b 05 bc 57 00 00 	mov    0x57bc(%rip),%rax        # 408820 <rand_div>
  403064:	48 39 c2             	cmp    %rax,%rdx
  403067:	74 05                	je     40306e <phase_3_5+0x6d>
  403069:	e8 88 2b 00 00       	call   405bf6 <explode_bomb>
  40306e:	bf c8 00 00 00       	mov    $0xc8,%edi
  403073:	e8 35 e6 ff ff       	call   4016ad <GenerateRandomNumber>
  403078:	8b 45 f4             	mov    -0xc(%rbp),%eax
  40307b:	83 f8 07             	cmp    $0x7,%eax
  40307e:	0f 87 eb 00 00 00    	ja     40316f <phase_3_5+0x16e>
  403084:	89 c0                	mov    %eax,%eax
  403086:	48 8d 14 85 00 00 00 	lea    0x0(,%rax,4),%rdx
  40308d:	00 
  40308e:	48 8d 05 9f 36 00 00 	lea    0x369f(%rip),%rax        # 406734 <_IO_stdin_used+0x734>
  403095:	8b 04 02             	mov    (%rdx,%rax,1),%eax
  403098:	48 98                	cltq
  40309a:	48 8d 15 93 36 00 00 	lea    0x3693(%rip),%rdx        # 406734 <_IO_stdin_used+0x734>
  4030a1:	48 01 d0             	add    %rdx,%rax
  4030a4:	3e ff e0             	notrack jmp *%rax
  4030a7:	48 8b 05 72 57 00 00 	mov    0x5772(%rip),%rax        # 408820 <rand_div>
  4030ae:	89 c2                	mov    %eax,%edx
  4030b0:	8b 45 fc             	mov    -0x4(%rbp),%eax
  4030b3:	01 d0                	add    %edx,%eax
  4030b5:	89 45 fc             	mov    %eax,-0x4(%rbp)
  4030b8:	bf c8 00 00 00       	mov    $0xc8,%edi
  4030bd:	e8 eb e5 ff ff       	call   4016ad <GenerateRandomNumber>
  ...
  403174:	8b 45 f0             	mov    -0x10(%rbp),%eax
  403177:	39 45 fc             	cmp    %eax,-0x4(%rbp)          # 注意这里
  40317a:	74 05                	je     403181 <phase_3_5+0x180>
  40317c:	e8 75 2a 00 00       	call   405bf6 <explode_bomb>
  403181:	90                   	nop
  403182:	c9                   	leave
  403183:	c3                   	ret

```
看起来一大堆很吓人对不对？实际上确实很吓人。

但是发现其中玄机后其实简单的没边，最终答案就藏在`0x403177`里面，前提是确保这一步前炸弹不爆炸（意识到要爆炸了直接`run`一下重开qwq）。

---

### **动态调试**

阅读`Phase_3_5`发现这一关其实需要两个输入，并且第一个输入必须是`rand_div`，这里建议通过`si`单步执行监控好`rand_div`值变化，确定正确结果后使用`run`重开正确输入第一个密码后才能进行下一步求解：
```lldb
(lldb) si
Process 13376 stopped
* thread #1, name = 'bomb_linux', stop reason = instruction step into
    frame #0: 0x000000000040317a bomb_linux`phase_3_5 + 377
bomb_linux`phase_3_5:
->  0x40317a <+377>: je     0x403181       ; <+384>
    0x40317c <+379>: callq  0x405bf6       ; explode_bomb
    0x403181 <+384>: nop
    0x403182 <+385>: leave
(lldb) x/wx $rbp-0x4
0x7fffffffdb0c: 0xffffffd7
```
例如这里我可以打印出第二个值结合第一个值得到第三关正确结果。

---

## **Phase_4**

### **静态分析**

本题依旧开局跳转，笔者的跳转方向是`phase_4_01`，如何跳转不再强调关注`rand_div`的值即可，下面请D指导解读一下`phase_4_01`的内容：
```assembly
0000000000404895 <phase_4_01>:
  ; 函数入口，初始化栈帧
  404895:	f3 0f 1e fa          	endbr64 
  404899:	55                   	push   %rbp
  40489a:	48 89 e5             	mov    %rsp,%rbp
  40489d:	48 83 ec 70          	sub    $0x70,%rsp    ; 分配栈空间

  ; 初始化斐波那契数组（F(10)~F(24)的十六进制值）
  4048a1:	48 89 7d 98          	mov    %rdi,-0x68(%rbp) ; 保存输入字符串指针
  4048a5:	c7 45 b0 37 00 00 00 	movl   $0x37,-0x50(%rbp)   ; F(10)=55
  4048ac:	c7 45 b4 59 00 00 00 	movl   $0x59,-0x4c(%rbp)   ; F(11)=89
  4048b3:	c7 45 b8 90 00 00 00 	movl   $0x90,-0x48(%rbp)   ; F(12)=144
  4048ba:	c7 45 bc e9 00 00 00 	movl   $0xe9,-0x44(%rbp)   ; F(13)=233
  4048c1:	c7 45 c0 79 01 00 00 	movl   $0x179,-0x40(%rbp)  ; F(14)=377
  4048c8:	c7 45 c4 62 02 00 00 	movl   $0x262,-0x3c(%rbp)  ; F(15)=610
  4048cf:	c7 45 c8 db 03 00 00 	movl   $0x3db,-0x38(%rbp)  ; F(16)=987
  4048d6:	c7 45 cc 3d 06 00 00 	movl   $0x63d,-0x34(%rbp)  ; F(17)=1597
  4048dd:	c7 45 d0 18 0a 00 00 	movl   $0xa18,-0x30(%rbp)  ; F(18)=2584
  4048e4:	c7 45 d4 55 10 00 00 	movl   $0x1055,-0x2c(%rbp) ; F(19)=4181
  4048eb:	c7 45 d8 6d 1a 00 00 	movl   $0x1a6d,-0x28(%rbp) ; F(20)=6765
  4048f2:	c7 45 dc c2 2a 00 00 	movl   $0x2ac2,-0x24(%rbp) ; F(21)=10946
  4048f9:	c7 45 e0 2f 45 00 00 	movl   $0x452f,-0x20(%rbp) ; F(22)=17711
  404900:	c7 45 e4 f1 6f 00 00 	movl   $0x6ff1,-0x1c(%rbp) ; F(23)=28657
  404907:	c7 45 e8 20 b5 00 00 	movl   $0xb520,-0x18(%rbp) ; F(24)=46368

  ; 读取输入到局部变量（格式为"%d"）
  40490e:	48 8d 55 ac          	lea    -0x54(%rbp),%rdx    ; 输入存储地址
  404912:	48 8b 45 98          	mov    -0x68(%rbp),%rax   ; 输入字符串
  404916:	48 8d 0d 93 1f 00 00 	lea    0x1f93(%rip),%rcx   ; 格式字符串"%d"
  40491d:	48 89 ce             	mov    %rcx,%rsi
  404920:	48 89 c7             	mov    %rax,%rdi
  404923:	b8 00 00 00 00       	mov    $0x0,%eax
  404928:	e8 63 c8 ff ff       	call   401190 <__isoc99_sscanf@plt>

  ; 验证输入有效性（必须为1个正数）
  40492d:	89 45 fc             	mov    %eax,-0x4(%rbp)    ; sscanf返回值
  404930:	83 7d fc 01          	cmpl   $0x1,-0x4(%rbp)   ; 检查是否读取1个参数
  404934:	75 07                	jne    40493d <phase_4_01+0xa8> ; 失败则爆炸
  404936:	8b 45 ac             	mov    -0x54(%rbp),%eax ; 获取输入值N
  404939:	85 c0                	test   %eax,%eax         ; 检查N > 0
  40493b:	7f 05                	jg     404942 <phase_4_01+0xad>
  40493d:	e8 b4 12 00 00       	call   405bf6 <explode_bomb>

  ; 检查输入值上限（必须 > 1999）
  404942:	8b 45 ac             	mov    -0x54(%rbp),%eax 
  404945:	3d cf 07 00 00       	cmp    $0x7cf,%eax       ; 1999的十六进制
  40494a:	7f 05                	jg     404951 <phase_4_01+0xbc> ; N > 1999?
  40494c:	e8 a5 12 00 00       	call   405bf6 <explode_bomb>

  ; 计算 N/2000（通过定点数乘法优化）
  404951:	8b 45 ac             	mov    -0x54(%rbp),%eax  ; 输入值N
  404954:	48 63 d0             	movslq %eax,%rdx        ; 符号扩展
  404957:	48 69 d2 d3 4d 62 10 	imul   $0x10624dd3,%rdx,%rdx ; 乘以274877907(≈2^32/2000)
  40495e:	48 c1 ea 20          	shr    $0x20,%rdx       ; 取高32位
  404962:	c1 fa 07             	sar    $0x7,%edx        ; 算术右移7位 → N/2000
  404965:	c1 f8 1f             	sar    $0x1f,%eax       ; 符号位扩展
  404968:	89 c1                	mov    %eax,%ecx        
  40496a:	89 d0                	mov    %edx,%eax        
  40496c:	29 c8                	sub    %ecx,%eax        ; 处理负数情况
  40496e:	89 45 ac             	mov    %eax,-0x54(%rbp) ; 保存k = N/2000

  ; 调用递归函数func4_0(k), 这个函数用于计算斐波那契数列
  404971:	8b 45 ac             	mov    -0x54(%rbp),%eax 
  404974:	89 c7                	mov    %eax,%edi        ; 参数k
  404976:	e8 ce fd ff ff       	call   404749 <func4_0>  ; 返回值eax=F(k+1)
  40497b:	89 45 f8             	mov    %eax,-0x8(%rbp)  ; 保存结果

  ; 生成随机索引并验证结果
  40497e:	bf 0f 00 00 00       	mov    $0xf,%edi        ; 参数15
  404983:	e8 25 cd ff ff       	call   4016ad <GenerateRandomNumber> ; 生成0~14随机数
  404988:	48 8b 05 91 3e 00 00 	mov    0x3e91(%rip),%rax        # 408820 <rand_div> ; 获取随机索引
  40498f:	8b 44 85 b0          	mov    -0x50(%rbp,%rax,4),%eax ; 取数组[rand_div]的值
  404993:	39 45 f8             	cmp    %eax,-0x8(%rbp)  ; 比较func4_0(k) == 数组值？
  404996:	74 05                	je     40499d <phase_4_01+0x108>
  404998:	e8 59 12 00 00       	call   405bf6 <explode_bomb>
```
所以相对还是很明了的，依旧是关注`rand_div`。

### **动态调试**
先找出`rand_div`在最后判断前的取值，比如我下面的0xa：

```lldb
(lldb) si
Process 27027 stopped
* thread #1, name = 'bomb_linux', stop reason = instruction step into
    frame #0: 0x0000000000401719 bomb_linux`GenerateRandomNumber + 108
bomb_linux`GenerateRandomNumber:
->  0x401719 <+108>: movq   %rax, 0x7100(%rip) ; rand_div
    0x401720 <+115>: jmp    0x401723       ; <+118>
    0x401722 <+117>: nop
    0x401723 <+118>: popq   %rbp
(lldb) si
Process 27027 stopped
* thread #1, name = 'bomb_linux', stop reason = instruction step into
    frame #0: 0x0000000000401720 bomb_linux`GenerateRandomNumber + 115
bomb_linux`GenerateRandomNumber:
->  0x401720 <+115>: jmp    0x401723       ; <+118>
    0x401722 <+117>: nop
    0x401723 <+118>: popq   %rbp
    0x401724 <+119>: retq
(lldb) x/gx &rand_div
0x00408820: 0x000000000000000a
```

而当 `rand_div = 0xa`（即十进制 **10**）时，输入值 `N` 的计算步骤如下：

- 数组索引 **10** 的值是 **斐波那契数列第 20 项**（`F(20) = 6765`）。

- `func4_0(k)` 实际计算的是 **标准斐波那契数列的第 `k+1` 项**（例如，`func4_0(0) = 1 = F(2)`） 需要满足：
  ```c
  func4_0(k) = F(k+1) = F(20)
  ```
  解得：
  k + 1 = 20 → k = 19
- `k = N / 2000` → `N = 2000 * k = 2000 * 19 = 38000`.
从而得解。
![phase_4](/images/phase_4.png)

---

## **Phase_Impossible**

Impossible？

从这道题开始偷懒了，掏出ghidra直接看c代码了解一下大概流程再去objdump看汇编：
```c
void phase_impossible(char *param_1)

{
  int iVar1;
  size_t sVar2;
  undefined local_118 [256];
  long local_18;
  long local_10;
  
  local_10 = GetTickCount();
  sVar2 = strlen(param_1);
  if ((sVar2 < 10) || (sVar2 = strlen(param_1), 0x300 < sVar2)) {
    explode_bomb();
  }
  memset(local_118,0,0x100);
  tohex(local_118,param_1);
  GenerateRandomNumber(0x400);
  iVar1 = check_buf_valid(local_118,rand_div & 0xffffffff);
  if (iVar1 == 0) {
    puts(&DAT_00406518);
    explode_bomb();
  }
  GenerateRandomNumber(3);
  if (rand_div != 2) {
    if (2 < rand_div) goto LAB_00401891;
    if (rand_div == 0) {
      goto_buf_0(local_118);
    }
    else if (rand_div != 1) goto LAB_00401891;
    goto_buf_1(local_118);
  }
  goto_buf_2(local_118);
LAB_00401891:
  explode_bomb();
  GenerateRandomNumber(0x400);
  if ((long)(int)result != rand_div) {
    printf(&DAT_00406560,rand_div,(ulong)result);
    explode_bomb();
  }
  local_18 = GetTickCount();
  if (1000 < (ulong)(local_18 - local_10)) {
    puts(&DAT_004065a8);
    explode_bomb();
  }
  return;
}
```
最终任务还是很明确的，需要写一段机器码修改`result`的数值，但是注意要能通过`check_buf_valid`检测，并且最后指令必须是跳转到`0x401896`不然就会触发`phase_impossible`中`0x401891`处的`explode_bomb`函数，唯一的难点是跟踪`rand_div`的数值变化，建议使用`register write`来修改`check_buf_valid`的返回值使其强制通过然后监控`rand_div`每一次的数值变化（`x/gx &rand_div`），记录好`rand_div`的结果后开始指令设计，需要满足：

 - 指令的异或和为`rand_div`第一次的数值末尾八位以通过检查;
 - 修改`result`使其数值等于`rand_div`第三次数值;
 - 跳转到`0x401896`避免炸弹;

 如果前几问都完成了到这里应该是没有问题的。

---

## **Phase_Secret**

隐藏彩蛋，并非隐藏。汇编里写的非常清楚：
```assembly
0000000000401a8b <phase_secret>:
  401a8b:	f3 0f 1e fa          	endbr64
  401a8f:	55                   	push   %rbp
  401a90:	48 89 e5             	mov    %rsp,%rbp
  401a93:	48 83 ec 10          	sub    $0x10,%rsp
  401a97:	48 89 7d f8          	mov    %rdi,-0x8(%rbp)
  401a9b:	48 8d 05 26 4b 00 00 	lea    0x4b26(%rip),%rax        # 4065c8 <_IO_stdin_used+0x5c8>
  401aa2:	48 89 c7             	mov    %rax,%rdi
  401aa5:	e8 76 f6 ff ff       	call   401120 <puts@plt>
  401aaa:	90                   	nop
  401aab:	c9                   	leave
  401aac:	c3                   	ret
```
注意到这段指令在原程序中完全没有执行说明是需要用户自己跳转的，也非常简单只需要在`phase_5`中设计指令时加一个要求跳转到`0x401a8b`即可。

完结
![Case Closed](/images/caseclosed.png)
