做 bomblab 的心路历程总结

工具

  1. objdump反汇编工具

    1
    2
    3
    objdump -t bomb # 显示符号表,函数名...
    objdump -d bomb > bomb.s # 反汇编
    objdump -d -M intel bomb > bomb.s # intel 风格(AT&T)
  2. gdb调试工具

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    gdb 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类似
  3. strings提取 ELF 可执行文件中的所有可打印字符

    1
    strings bomb > bomb.txt

配置.gdbinit 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 关闭分页
set pagination off

# 开启颜色
set style enabled on

# 禁止确认提示(如删除断点)
set confirm off

# 设置默认文件输入
# set args input.txt(我用这个不行,不知道为什么)
# 在启动时, 输入r input.txt读取就行

# 显示寄存器
display $eax
display $ebx
display $ecx
display $edx
display $esp
display $ebp
display $eip

# 设置断点在 main 和所有 phase 函数
break 73
# break phase_1
# break phase_2
# break phase_3
# break phase_4
# break phase_5
# break phase_6
# b phase_defused
# b secret_phase
# b fun7
# b strings_not_equal
# b string_length
# b read_six_numbers
# b func4

# 避免炸弹爆炸
break explode_bomb

常见寄存器知识总结

alt text

let’s go

phase_1

看 phase_1 的汇编发现调用 strings_not_equal 来比较

1
2
3
4
400ee9:	e8 4a 04 00 00       	call   401338 <strings_not_equal> # 调用函数比较字符串是否相等
400eee: 85 c0 test %eax,%eax # 检查返回值是否为0
400ef0: 74 05 je 400ef7 <phase_1+0x17>
400ef2: e8 43 05 00 00 call 40143a <explode_bomb> # 不等于0就爆炸

alt text

检查一下$rdi$rsi寄存器查看存放了哪些函数调用参数

alt text

大致翻译为 c

1
2
3
4
5
// 0x402400 => ans
if(strings_not_equal(input, ans)==0) {
return;
}
// strings_not_equal(input, ans)当两个字符串长度不相等时返回1, 相等且每个字符匹配返回0

画了个图

alt text

1
2
40135c:	0f b6 03             	movzbl (%rbx),%eax
40135f: 84 c0 test %al,%al
1
2
3
4
//movzbl (%rbx)取一个字节b
if(ch==0){
// 终止符'\0',结束
}

总结:

  1. %rdi是自己输入的,%rsi是目标

    • x86_64 的函数调用约定规定:
      参数位置 64 位寄存器 低 32 位寄存器
      第一个 %rdi %edi
      第二个 %rsi %esi
      第三个 %rdx %edx
      第四个 %rcx %ecx

phase_2

在 phase_2 的汇编里面发现先调用read_six_numbers的函数

1
400f05:	e8 52 05 00 00       	call   40145c <read_six_numbers>

里面还调用了sscanf函数, 读取成功的个数小于等于 5 就爆炸

alt text

补充一下:

  1. scanf函数读取用户输入

  2. sscanf函数将某种格式的 str 读入变量中

    1
    2
    3
    4
    5
    int 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
2
3
400f0a:	83 3c 24 01          	cmpl   $0x1,(%rsp)
400f0e: 74 20 je 400f30 <phase_2+0x34>
400f10: e8 25 05 00 00 call 40143a <explode_bomb>

($rsp+0x4)放到$rbx里面

1
400f30:	48 8d 5c 24 04       	lea    0x4(%rsp),%rbx

$rbx-0x4放到$eax(前一个值), 然后$eax的值乘 2, 比较$eax*$rbx的值(前一个值乘 2 和后一个值比较)

1
2
3
4
5
400f17:	8b 43 fc             	mov    -0x4(%rbx),%eax
400f1a: 01 c0 add %eax,%eax
400f1c: 39 03 cmp %eax,(%rbx)
400f1e: 74 05 je 400f25 <phase_2+0x29>
400f20: e8 15 05 00 00 call 40143a <explode_bomb>

alt text

然后就是循环, 大致翻译为 c

1
2
3
4
5
6
7
8
9
10
for(int i=0;i<6;i++){
if(i==0 && input[i]!=1){
explode_bomb();
}
if(2*input[i-1]==input[i]){
continue;
}else{
explode_bomb();
}
}

phase_3

根据汇编和寄存器分析出 x,y 在哪

1
2
400f47:	48 8d 4c 24 0c       	lea    0xc(%rsp),%rcx
400f4c: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
1
2
3
// 刚好说明rsp+0xc=>y(高地址)
// rsp+0x8=>x
sscanf(str, "%d %d", &x, &y);
1
2
3
4
5
6
0x0000000000400f51 <+14>:    mov    $0x4025cf,%esi
0x0000000000400f56 <+19>: mov $0x0,%eax
0x0000000000400f5b <+24>: call 0x400bf0 <__isoc99_sscanf@plt>
0x0000000000400f60 <+29>: cmp $0x1,%eax
0x0000000000400f63 <+32>: jg 0x400f6a <phase_3+39>
0x0000000000400f65 <+34>: call 0x40143a <explode_bomb>

看出是读取两个值

alt text

1
2
3
4
// sscanf后的结果会放在%eax
if(sscanf(input,"%d %d",&x,&y)<=1) {
explode_bomb();
}
1
2
0x0000000000400f6a <+39>:    cmpl   $0x7,0x8(%rsp)
0x0000000000400f6f <+44>: ja 0x400fad <phase_3+106>

看汇编发现 x 的值要小于 7

1
2
3
if(x > 7) {
explode_bomb();
}

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 进制查看

alt text

对应的 val

alt text

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
2
3
0x0000000000400fbe <+123>:   cmp    0xc(%rsp),%eax
0x0000000000400fc2 <+127>: je 0x400fc9 <phase_3+134>
0x0000000000400fc4 <+129>: call 0x40143a <explode_bomb>
1
2
3
4
// 那我让y==$eax这个值岂不是不爆炸了
if(y!=$eax){
explode_bomb();
}

alt text

把 x=5,看到$eax是 206

输入就过了

phase_4

1
2
3
4
5
40101a:	be cf 25 40 00       	mov    $0x4025cf,%esi
40101f: b8 00 00 00 00 mov $0x0,%eax
401024: e8 c7 fb ff ff call 400bf0 <__isoc99_sscanf@plt>
401029: 83 f8 02 cmp $0x2,%eax
40102c: 75 07 jne 401035 <phase_4+0x29>

alt text

翻译为 c

1
2
3
if(sscanf(input,"%d %d",&x,&y)!=2){
explode_bomb();
}
1
2
3
40102e:	83 7c 24 08 0e       	cmpl   $0xe,0x8(%rsp)
401033: 76 05 jbe 40103a <phase_4+0x2e>
401035: e8 00 04 00 00 call 40143a <explode_bomb>

翻译为 c

1
2
3
if(x>14){
explode_bomb();
}
1
2
3
4
40103a:	ba 0e 00 00 00       	mov    $0xe,%edx
40103f: be 00 00 00 00 mov $0x0,%esi
401044: 8b 7c 24 08 mov 0x8(%rsp),%edi
401048: e8 81 ff ff ff call 400fce <func4>

为调用 func4 准备参数

1
int func4(int x, 0, 14);
1
2
40104d:	85 c0                	test   %eax,%eax
40104f: 75 07 jne 401058 <phase_4+0x4c>

翻译成 c

1
2
3
if(eax !=0){
explode_bomb();
}
1
2
401051:	83 7c 24 0c 00       	cmpl   $0x0,0xc(%rsp)
401056: 74 05 je 40105d <phase_4+0x51>

发现 y 必须是 0

1
2
3
if(y==0){
return;
}

y 必须是 0, x<14

试了一下 x=3 过了(😂🤔)

func4()

把汇编完整翻译过来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 第一次调用func4(x, 0, 14);
int func4(int x, int low, int high) {
// 刷题时,更习惯的二分写法
// low+(high-low)/2
// 下面用位运算进行实现上述代码
int diff = high - low;
// flag保证无论low和high谁大都能计算出正确结果
int flag = diff >> 31;
diff = (diff + flag) >> 1;
int mid = low + diff;
if (mid > x) {
high = mid - 1;
return 2 * func4(x, low, high);
} else if (mid < x) {
low = mid + 1;
return 2 * func4(x, low, high) + 1;
} else {
return 0;
}
}

分析:

总目标: 期待func4返回 0

  1. x>mid这条路不行,这里怎么都会带个+1
  2. x<mid这条路有机会,再递归下去只要不走x>mid的路就有解

alt text

总结:

x 有解
7 ✔️
3 ✔️
1 ✔️
0 ✔️

GPT 给的一个介绍

func4(x, 0, 14) 中,构造出来的是一棵“完全平衡”的 BST:

1
2
3
4
5
6
7
            7
/ \
3 11
/ \ / \
1 5 9 13
/ \ / \ / \ / \
0 2 4 6 8 10 12 14

路径编码示意图:

1
2
3
4
5
6
7
8
编码值(路径):
root
/ \
0 1
/ \ / \
0 1 0 1
/ \ / \ / \ / \
0 1 0 1 0 1 0 1

查 9,路径编码(从下往上看)为 0b01(结果是 2)

phase_5

1
2
40107a:	e8 9c 02 00 00       	call   40131b <string_length>
40107f: 83 f8 06 cmp $0x6,%eax

找到爆炸条件

1
2
3
if(string_length(str)!=6){
explode_bomb();
}

循环读取每个输入的字符并取低 4 位做索引转化,$rax放着循环计数器

1
2
3
4
5
6
7
8
9
40108b:	0f b6 0c 03          	movzbl (%rbx,%rax,1),%ecx
40108f: 88 0c 24 mov %cl,(%rsp)
401092: 48 8b 14 24 mov (%rsp),%rdx
401096: 83 e2 0f and $0xf,%edx
401099: 0f b6 92 b0 24 40 00 movzbl 0x4024b0(%rdx),%edx
4010a0: 88 54 04 10 mov %dl,0x10(%rsp,%rax,1)
4010a4: 48 83 c0 01 add $0x1,%rax
4010a8: 48 83 f8 06 cmp $0x6,%rax
4010ac: 75 dd jne 40108b <phase_5+0x29>

按索引读取输入的一个字节,$rbx放 input

1
40108b:	0f b6 0c 03          	movzbl (%rbx,%rax,1),%ecx
1
char ch = input[idx];
1
2
401096:	83 e2 0f             	and    $0xf,%edx # 取低4位
401099: 0f b6 92 b0 24 40 00 movzbl 0x4024b0(%rdx),%edx

alt text

1
char ch = str[idx]; // 0x4024b0 放着一个string
1
4010ae:	c6 44 24 16 00       	movb   $0x0,0x16(%rsp)

6 个字符操作完后还加一个终结符’\0’

alt text

得到此映射表

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

查看转化后的结果

alt text

查看期待的 str

alt text

大致流程,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
2
3
4
5
6
7
8
9
10
11
401100:	49 89 e5             	mov    %rsp,%r13
401103: 48 89 e6 mov %rsp,%rsi
401106: e8 51 03 00 00 call 40145c <read_six_numbers>
40110b: 49 89 e6 mov %rsp,%r14
40110e: 41 bc 00 00 00 00 mov $0x0,%r12d
401114: 4c 89 ed mov %r13,%rbp
401117: 41 8b 45 00 mov 0x0(%r13),%eax
40111b: 83 e8 01 sub $0x1,%eax
40111e: 83 f8 05 cmp $0x5,%eax
401121: 76 05 jbe 401128 <phase_6+0x34>
401123: e8 12 03 00 00 call 40143a <explode_bomb>

r12d 一开始是 0, 有个循环 0..6

1
2
3
401128:	41 83 c4 01          	add    $0x1,%r12d
40112c: 41 83 fc 06 cmp $0x6,%r12d
401130: 74 21 je 401153 <phase_6+0x5f>

4*$rax+$rsp 取后一个数

前一个和后一个相等爆炸

循环比较 6 个数,每个都必须 diff,有相等的就爆炸

p *(int*)($rsp+$rax*4)

1
2
3
4
5
6
401132:	44 89 e3             	mov    %r12d,%ebx
401135: 48 63 c3 movslq %ebx,%rax
401138: 8b 04 84 mov (%rsp,%rax,4),%eax
40113b: 39 45 00 cmp %eax,0x0(%rbp)
40113e: 75 05 jne 401145 <phase_6+0x51>
401140: e8 f5 02 00 00 call 40143a <explode_bomb>

大致翻译成 c

ans 必须是 1-6 六个数字的一个组合

1
2
3
4
5
6
7
8
9
10
11

for(int i=0;i<6;i++){
if(s[i]-'0'-1>5){
explode_bomb();
}
for(int j=i+1;j<6;j++){
if(s[i]==s[j]){
explode_bomb();
}
}
}
1
2
3
4
5
6
4011da:	bd 05 00 00 00       	mov    $0x5,%ebp
4011df: 48 8b 43 08 mov 0x8(%rbx),%rax
4011e3: 8b 00 mov (%rax),%eax
4011e5: 39 03 cmp %eax,(%rbx)
4011e7: 7d 05 jge 4011ee <phase_6+0xfa>
4011e9: e8 4c 02 00 00 call 40143a <explode_bomb>

x $rbx拿到地址,再p *0x6032f0查看值

alt text

发现$rbx是个 node

alt text

要让*$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
2
3
4011ee:	48 8b 5b 08          	mov    0x8(%rbx),%rbx
4011f2: 83 ed 01 sub $0x1,%ebp
4011f5: 75 e8 jne 4011df <phase_6+0xeb>

又得到一个 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
2
3
4
5
// phase_defused
4015f0: be 19 26 40 00 mov $0x402619,%esi
4015f5: bf 70 38 60 00 mov $0x603870,%edi
4015fa: e8 f1 f5 ff ff call 400bf0 <__isoc99_sscanf@plt>
4015ff: 83 f8 03 cmp $0x3,%eax

读取三个参数, 在第四关加第三个参数进去(进入秘密关的钥匙)

alt text

1
2
3
401604:	be 22 26 40 00       	mov    $0x402622,%esi
401609: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi
40160e: e8 25 fd ff ff call 401338 <strings_not_equal>

发现第三个参数要传DrEvil

alt text

1
2
3
4
40125d:	8d 40 ff             	lea    -0x1(%rax),%eax
401260: 3d e8 03 00 00 cmp $0x3e8,%eax
401265: 76 05 jbe 40126c <secret_phase+0x2a>
401267: e8 ce 01 00 00 call 40143a <explode_bomb>
1
2
3
4
5
// 无符号比较,大于1000炸
// 1..1001
if(($eax-1)>1000){
explode_bomb();
}
1
2
3
4
401273:	e8 8c ff ff ff       	call   401204 <fun7>
401278: 83 f8 02 cmp $0x2,%eax
40127b: 74 05 je 401282 <secret_phase+0x40>
40127d: e8 b8 01 00 00 call 40143a <explode_bomb>

大致翻译成 c

1
2
3
if(fun7(6303984, input)!=2){
explode_bomb();
}

好奇汇编里面的$edx = *rdi

1
2
3
4
5
40120d:	8b 17                	mov    (%rdi),%edx
40120f: 39 f2 cmp %esi,%edx
401211: 7e 0d jle 401220 <fun7+0x1c>
401213: 48 8b 7f 08 mov 0x8(%rdi),%rdi
401217: e8 e8 ff ff ff call 401204 <fun7>

alt text

alt text

fun7 大致翻译成 c, 发现和 func4 的差不多, 也是二叉搜索树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int fun7(x, input){
if(x==null){
ans = -1;
return ans;
}
edx = *x; // 初始36 结构体的第一个字段
if(edx>input){
x = (x+8); // 准确说 x->left 结构体的第二个字段
ans = fun7(x, input);
return 2*ans;
} else{
if(edx==input){
ans = 0;
return ans;
} else{ // edx<input
x = (x+16); // 准确说 x->right 结构体的第三个字段
ans = fun7(x, input);
return 2*ans+1;
}
}
}

alt text

这样一个树的结构

1
2
3
4
5
struct TreeNode{
int val;
TreeNode* left;
TreeNode* right;
}

alt text

1
2
3
if(fun7(x,input)!=2){
explode_bomb();
}

需要 ans=2,就通过了

即路径要为 0b01(先左再右)或者 0b010(先左再右再左也行)

随便试了个 20(实际就是 0b010 这个路径),过了

成果

alt text

总结

做 bomblab 真的很有意思, 上下花了 5 天左右做完的, 花了半天整理笔记, 做下来又学会了更多的 gdb 高级用法, 在调试中发现代码运行的秘密, 有时不用完整把汇编翻译成 c, 只要摸清运行逻辑, 数据放在哪个寄存器也能成功拆弹 🤗(待提高: 有些地址的计算不是很熟练用命令去查看值, 分析数据结构)