Files
blog.hifuu.ink/source/_posts/nudtbomblab.md
2025-02-26 09:13:52 +08:00

421 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: 高地特供版CSAPP Bomb Lab全流程攻略
date: 2025-02-24 15:09:11
tags: [技术, 学习, 生活]
---
这篇文章记录高地CSAPP课程Bomblab实验操作流程仅供参考交流答案是随机生成的和学号相关
笔者实验环境为Archlinux/CachyOS使用lldb作为调试器和gdb操作差不多其余用到的工具主要为objdumpstringsneovim/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)