CSAPP-bomblab总结
做 bomblab 的心路历程总结
工具
objdump
反汇编工具1
2
3objdump -t bomb # 显示符号表,函数名...
objdump -d bomb > bomb.s # 反汇编
objdump -d -M intel bomb > bomb.s # intel 风格(AT&T)gdb
调试工具1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21gdb bomb # 进入调试模式
b func # 在函数func处设置断点
b main.c:123 # 在main.c:123处设置断点
b *0x12345678 # 在地址0x12345678处设置断点
r # 运行程序
c # 继续运行
n # 单步执行
s # 进入函数
stepi <n> # 执行一条汇编指令, n可以执行n条
finish # 跳出当前函数
layout asm # 切换到汇编视图
layout src # 切换到源代码视图(快捷键ctrl + x + a)
p num # 打印变量num的值
p /<fmt> $rsp # 打印寄存器的值
p *(int*)(0xff + $rsp) # 拿到指针的值
p 0x11 # 打印0x11的十进制表示
p /x 555 # 打印555的十六进制表示
x/<n><fmt> <addr> # 打印内存的值(/后参数) b 1字节 w 4字节 g 8字节 d十进制 x十六进制
disas string_length # 查看函数的汇编
info registers # 查看寄存器
info stack # 查看调用栈和bt类似strings
提取 ELF 可执行文件中的所有可打印字符1
strings bomb > bomb.txt
配置.gdbinit 文件
1 | # 关闭分页 |
常见寄存器知识总结
let’s go
phase_1
看 phase_1 的汇编发现调用 strings_not_equal 来比较
1 | 400ee9: e8 4a 04 00 00 call 401338 <strings_not_equal> # 调用函数比较字符串是否相等 |
检查一下$rdi
和$rsi
寄存器查看存放了哪些函数调用参数
大致翻译为 c
1 | // 0x402400 => ans |
画了个图
1 | 40135c: 0f b6 03 movzbl (%rbx),%eax |
1 | //movzbl (%rbx)取一个字节b |
总结:
%rdi
是自己输入的,%rsi
是目标- x86_64 的函数调用约定规定:
参数位置 64 位寄存器 低 32 位寄存器 第一个 %rdi
%edi
第二个 %rsi
%esi
第三个 %rdx
%edx
第四个 %rcx
%ecx
- x86_64 的函数调用约定规定:
phase_2
在 phase_2 的汇编里面发现先调用read_six_numbers
的函数
1 | 400f05: e8 52 05 00 00 call 40145c <read_six_numbers> |
里面还调用了sscanf
函数, 读取成功的个数小于等于 5 就爆炸
补充一下:
scanf
函数读取用户输入sscanf
函数将某种格式的 str 读入变量中1
2
3
4
5int x, y;
const char* str = "10 23";
// x = 10 y = 23
// ans = 2 成功读取两个
int ans = sscanf(str, "%d %d", &x, &y);
返回值都是成功读取的变量个数
查看*$rsp
的值是不是 1(即读取六个数的第一个数是不是 1), 不是 1 就爆炸
1 | 400f0a: 83 3c 24 01 cmpl $0x1,(%rsp) |
把($rsp+0x4)
放到$rbx
里面
1 | 400f30: 48 8d 5c 24 04 lea 0x4(%rsp),%rbx |
把$rbx-0x4
放到$eax
(前一个值), 然后$eax
的值乘 2, 比较$eax
和*$rbx
的值(前一个值乘 2 和后一个值比较)
1 | 400f17: 8b 43 fc mov -0x4(%rbx),%eax |
然后就是循环, 大致翻译为 c
1 | for(int i=0;i<6;i++){ |
phase_3
根据汇编和寄存器分析出 x,y 在哪
1 | 400f47: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx |
1 | // 刚好说明rsp+0xc=>y(高地址) |
1 | 0x0000000000400f51 <+14>: mov $0x4025cf,%esi |
看出是读取两个值
1 | // sscanf后的结果会放在%eax |
1 | 0x0000000000400f6a <+39>: cmpl $0x7,0x8(%rsp) |
看汇编发现 x 的值要小于 7
1 | if(x > 7) { |
switch 语句跳转表, 0..7
8 个 case
1 | 400f75: ff 24 c5 70 24 40 00 jmp *0x402470(,%rax,8) # $rax * 8 + 0x402470 |
x/8gx 0x402470
查看跳转地址 8 个 g(8 字节)以 16 进制查看
对应的 val
val 表
x | hex(y) | y |
---|---|---|
0 | 0xcf | 207 |
1 | 0x137 | 311 |
2 | 0x2c3 | 707 |
3 | 0x100 | 256 |
4 | 0x185 | 389 |
5 | 0xce | 206 |
6 | 0x2aa | 682 |
7 | 0x147 | 327 |
1 | 0x0000000000400fbe <+123>: cmp 0xc(%rsp),%eax |
1 | // 那我让y==$eax这个值岂不是不爆炸了 |
把 x=5,看到$eax
是 206
输入就过了
phase_4
1 | 40101a: be cf 25 40 00 mov $0x4025cf,%esi |
翻译为 c
1 | if(sscanf(input,"%d %d",&x,&y)!=2){ |
1 | 40102e: 83 7c 24 08 0e cmpl $0xe,0x8(%rsp) |
翻译为 c
1 | if(x>14){ |
1 | 40103a: ba 0e 00 00 00 mov $0xe,%edx |
为调用 func4 准备参数
1 | int func4(int x, 0, 14); |
1 | 40104d: 85 c0 test %eax,%eax |
翻译成 c
1 | if(eax !=0){ |
1 | 401051: 83 7c 24 0c 00 cmpl $0x0,0xc(%rsp) |
发现 y 必须是 0
1 | if(y==0){ |
y 必须是 0, x<14
试了一下 x=3 过了(😂🤔)
func4()
把汇编完整翻译过来
1 | // 第一次调用func4(x, 0, 14); |
分析:
总目标: 期待func4
返回 0
x>mid
这条路不行,这里怎么都会带个+1x<mid
这条路有机会,再递归下去只要不走x>mid
的路就有解
总结:
x | 有解 |
---|---|
7 | ✔️ |
3 | ✔️ |
1 | ✔️ |
0 | ✔️ |
GPT 给的一个介绍
func4(x, 0, 14)
中,构造出来的是一棵“完全平衡”的 BST:
1 | 7 |
路径编码示意图:
1 | 编码值(路径): |
查 9,路径编码(从下往上看)为 0b01(结果是 2)
phase_5
1 | 40107a: e8 9c 02 00 00 call 40131b <string_length> |
找到爆炸条件
1 | if(string_length(str)!=6){ |
循环读取每个输入的字符并取低 4 位做索引转化,$rax
放着循环计数器
1 | 40108b: 0f b6 0c 03 movzbl (%rbx,%rax,1),%ecx |
按索引读取输入的一个字节,$rbx
放 input
1 | 40108b: 0f b6 0c 03 movzbl (%rbx,%rax,1),%ecx |
1 | char ch = input[idx]; |
1 | 401096: 83 e2 0f and $0xf,%edx # 取低4位 |
1 | char ch = str[idx]; // 0x4024b0 放着一个string |
1 | 4010ae: c6 44 24 16 00 movb $0x0,0x16(%rsp) |
6 个字符操作完后还加一个终结符’\0’
得到此映射表
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
m | a | d | u | i | e | r | s | n | f | o | t | v | b | y | l |
查看转化后的结果
查看期待的 str
大致流程,input(str)=>转换成 str’(按索引做转化),然后 strings_not_equal(str’, “flyers”)
就是要转换完的 str’和”flyers”相等
根据映射表的转换关系和期待字符串, 所以答案就是 ascii 码里面拿一个 6 个字符的组合,要求他们低 4 位分别是 9,15,14,5,6,7
输入ionefg
即可
phase_6
r13 放着第一个数字 x
x-1 > 5
就爆炸了,所以第一个数要<=6
1 | 401100: 49 89 e5 mov %rsp,%r13 |
r12d 一开始是 0, 有个循环 0..6
1 | 401128: 41 83 c4 01 add $0x1,%r12d |
4*$rax+$rsp
取后一个数
前一个和后一个相等爆炸
循环比较 6 个数,每个都必须 diff,有相等的就爆炸
p *(int*)($rsp+$rax*4)
1 | 401132: 44 89 e3 mov %r12d,%ebx |
大致翻译成 c
ans 必须是 1-6 六个数字的一个组合
1 |
|
1 | 4011da: bd 05 00 00 00 mov $0x5,%ebp |
x $rbx
拿到地址,再p *0x6032f0
查看值
发现$rbx
是个 node
要让*$rbx >= $eax
这关就过了
尝试了几个结果得到一个表
*$rbx |
$eax |
ans |
---|---|---|
332 | 477 | 6 2 4 3 1 5 (走了一趟) |
443 | 332 | 1 6 3 4 5 2 (走了两趟) |
332 | 691 | 1 6 3 4 5 2 |
168 | 332 | 5 6 4 3 1 2 (走了一趟) |
477 | 332 | 2 6 4 3 1 5 (走了两趟) |
332 | 924 | 2 6 4 3 1 5 |
443 | 168 | 1 5 2 3 6 4 (走了两趟) |
168 | 477 | 1 5 2 3 6 4 |
924 | 168 | 4 5 3 1 6 2 (走了两趟) |
168 | 691 | 4 5 3 1 6 2 |
发现一个规律, 几个数串联在一起的感觉
结合这段, 比完一趟*$rbx > $eax
, $rbx+8
了, 跳到后一个 node
1 | 4011ee: 48 8b 5b 08 mov 0x8(%rbx),%rbx |
又得到一个 kv 表
k | v |
---|---|
1 | 443 |
2 | 477 |
3 | 691 |
4 | 924 |
5 | 168 |
6 | 332 |
要让 v 从大往小排
输入4 3 2 1 6 5
过了
secret_phase
1 | // phase_defused |
读取三个参数, 在第四关加第三个参数进去(进入秘密关的钥匙)
1 | 401604: be 22 26 40 00 mov $0x402622,%esi |
发现第三个参数要传DrEvil
1 | 40125d: 8d 40 ff lea -0x1(%rax),%eax |
1 | // 无符号比较,大于1000炸 |
1 | 401273: e8 8c ff ff ff call 401204 <fun7> |
大致翻译成 c
1 | if(fun7(6303984, input)!=2){ |
好奇汇编里面的$edx = *rdi
1 | 40120d: 8b 17 mov (%rdi),%edx |
fun7 大致翻译成 c, 发现和 func4 的差不多, 也是二叉搜索树
1 | int fun7(x, input){ |
这样一个树的结构
1 | struct TreeNode{ |
1 | if(fun7(x,input)!=2){ |
需要 ans=2,就通过了
即路径要为 0b01(先左再右)或者 0b010(先左再右再左也行)
随便试了个 20(实际就是 0b010 这个路径),过了
成果
总结
做 bomblab 真的很有意思, 上下花了 5 天左右做完的, 花了半天整理笔记, 做下来又学会了更多的 gdb 高级用法, 在调试中发现代码运行的秘密, 有时不用完整把汇编翻译成 c, 只要摸清运行逻辑, 数据放在哪个寄存器也能成功拆弹 🤗(待提高: 有些地址的计算不是很熟练用命令去查看值, 分析数据结构)