CSAPP:Bomblab
逆向的传统艺能拆炸弹,👴的青春回来了。
文件结构:
1
2
3
4
|
bomb
├── README
├── bomb
└── bomb.c
|
只有一个程序,给的源码基本没用,我们要用逆向工程的方法理解程序,找到正确的字符串。
讲反汇编器的结果导出:objdump -d bomb > bomb.txt
可以看到有6关,每一关接受一个字符串,若跳转到explode_bomb函数,则答案错误。
第一关:字符串比较
1
2
3
4
|
0000000000400ee0 <phase_1>:
400ee0: 48 83 ec 08 sub $0x8,%rsp
400ee4: be 00 24 40 00 mov $0x402400,%esi
400ee9: e8 4a 04 00 00 callq 401338 <strings_not_equal>
|
逻辑是直接比较字符串是否相等,不过$0x402400
不是程序内地址,说明答案被藏在了我们看不到的内存位置。
于是上GDB,在<phase_1>下断点,stepi
单步执行到callq之前,查看寄存器:x\s $esi
,得到答案。(每台电脑的答案都不一样)
第二关:循环
1
2
3
4
5
6
|
0000000000400efc <phase_2>:
400efc: 55 push %rbp //压栈
400efd: 53 push %rbx
400efe: 48 83 ec 28 sub $0x28,%rsp //开辟栈帧
400f02: 48 89 e6 mov %rsp,%rsi//栈顶地址→rsi参数二
400f05: e8 52 05 00 00 callq 40145c <read_six_numbers>
|
如函数名所示,读6个数字,为什么是6呢,大概是因为存放参数的寄存器总共有6个吧。(然而并不)
可以看到调用前开辟了0x28的栈上空间,足够存放6个整数。栈顶地址被存入%rsi,以此传递该地址。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
000000000040145c <read_six_numbers>:
//%rsi:父进程<phase_2>的栈顶地址
40145c: 48 83 ec 18 sub $0x18,%rsp //栈帧长24
401460: 48 89 f2 mov %rsi,%rdx //rsi→参数三:num1
401463: 48 8d 4e 04 lea 0x4(%rsi),%rcx //rsi+4→参数四:num2
401467: 48 8d 46 14 lea 0x14(%rsi),%rax //rsi+20→rax
40146b: 48 89 44 24 08 mov %rax,0x8(%rsp) //rax→栈顶+8:num6
401470: 48 8d 46 10 lea 0x10(%rsi),%rax //rsi+16→rax
401474: 48 89 04 24 mov %rax,(%rsp) //rax→栈顶:num5
401478: 4c 8d 4e 0c lea 0xc(%rsi),%r9 //rsi+12→参数六:num4
40147c: 4c 8d 46 08 lea 0x8(%rsi),%r8 //rsi+8→参数五:num3
401480: be c3 25 40 00 mov $0x4025c3,%esi//0x4025c3:"%d %d %d %d %d %d"
401485: b8 00 00 00 00 mov $0x0,%eax //返回值赋0
40148a: e8 61 f7 ff ff callq 400bf0 <__isoc99_sscanf@plt> //sscanf()
40148f: 83 f8 05 cmp $0x5,%eax //返回值和5比较,即输入6个值才能通过
401492: 7f 05 jg 401499 <read_six_numbers+0x3d>
401494: e8 a1 ff ff ff callq 40143a <explode_bomb>
401499: 48 83 c4 18 add $0x18,%rsp //出栈
40149d: c3 retq
|
看<read_six_numbers>,%rsi中的地址以4为步长被分别储存。猜测sscanf函数的返回值中,第一个表示输入参数的个数;程序要求6个输入,加上rsi被占用,于是多的两个存入栈中。且sscanf函数的返回值按参数寄存器(多的地址在栈上)存放的地址传输,即输入值被按顺序存入phase_2的栈帧中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
400f0a: 83 3c 24 01 cmpl $0x1,(%rsp) //栈顶位置取双字和1比较
400f0e: 74 20 je 400f30 <phase_2+0x34>
400f10: e8 25 05 00 00 callq 40143a <explode_bomb>
400f17: 8b 43 fc mov -0x4(%rbx),%eax //循环头:num1→eax
400f1a: 01 c0 add %eax,%eax // eax*2
400f1c: 39 03 cmp %eax,(%rbx) //和num2比较
400f1e: 74 05 je 400f25 <phase_2+0x29> //相等才通过
400f20: e8 15 05 00 00 callq 40143a <explode_bomb>
400f25: 48 83 c3 04 add $0x4,%rbx //rbx增4
400f29: 48 39 eb cmp %rbp,%rbx //rbx和rsp+24比较,相等则跳出
400f2c: 75 e9 jne 400f17 <phase_2+0x1b> //循环尾,循环共6轮
400f2e: eb 0c jmp 400f3c <phase_2+0x40>
400f30: 48 8d 5c 24 04 lea 0x4(%rsp),%rbx //num2地址→rbx
400f35: 48 8d 6c 24 18 lea 0x18(%rsp),%rbp//rbx地址→rbp
400f3a: eb db jmp 400f17 <phase_2+0x1b> //开始循环
|
跳出<read_six_numbers>后,首先检查栈顶地址指向的值是否为1,即第一个数字是1。
之后进入循环,循环体每次都会把当前数字*2和下一个数字比较,即每个数字都是前一个的二倍;%rbx作计数变量,共循环6次。答案呼之欲出。
第三关:分支
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
0000000000400f43 <phase_3>:
400f43: 48 83 ec 18 sub $0x18,%rsp
400f47: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx //rsp+12→rcx: mun2
400f4c: 48 8d 54 24 08 lea 0x8(%rsp),%rdx //rsp+8→rdx: mun1
400f51: be cf 25 40 00 mov $0x4025cf,%esi //0x4025cf: "%d %d"
400f56: b8 00 00 00 00 mov $0x0,%eax
400f5b: e8 90 fc ff ff callq 400bf0 <__isoc99_sscanf@plt>
400f60: 83 f8 01 cmp $0x1,%eax //不少于一个输入
400f63: 7f 05 jg 400f6a <phase_3+0x27>
400f65: e8 d0 04 00 00 callq 40143a <explode_bomb>
400f6a: 83 7c 24 08 07 cmpl $0x7,0x8(%rsp) //
400f6f: 77 3c ja 400fad <phase_3+0x6a> //超过7则爆炸
400f71: 8b 44 24 08 mov 0x8(%rsp),%eax //取num1
400f75: ff 24 c5 70 24 40 00 jmpq *0x402470(,%rax,8)
|
此处*
相当于c中的取地址符&
,
1
2
3
|
400f7c: b8 cf 00 00 00 mov $0xcf,%eax
400f81: eb 3b jmp 400fbe <phase_3+0x7b>
······
|
这里有7段形式重复的代码,结合第一个数字不能大于7,猜测这里是switch型结构。
1
2
3
4
5
6
7
8
9
|
400fad: e8 88 04 00 00 callq 40143a <explode_bomb>
400fb2: b8 00 00 00 00 mov $0x0,%eax
400fb7: eb 05 jmp 400fbe <phase_3+0x7b>
400fb9: b8 37 01 00 00 mov $0x137,%eax
400fbe: 3b 44 24 0c cmp 0xc(%rsp),%eax //比较num2和eax
400fc2: 74 05 je 400fc9 <phase_3+0x86>
400fc4: e8 71 04 00 00 callq 40143a <explode_bomb>
400fc9: 48 83 c4 18 add $0x18,%rsp
400fcd: c3 retq
|
第二个数字是%eax的值,由第一个数决定。故答案有7个。
第四关:递归
1
2
3
4
5
6
7
8
9
10
11
|
000000000040100c <phase_4>:
......
401029: 83 f8 02 cmp $0x2,%eax //只能有2参数
40102c: 75 07 jne 401035 <phase_4+0x29>
40102e: 83 7c 24 08 0e cmpl $0xe,0x8(%rsp) //0 <= num1 <= 14
401033: 76 05 jbe 40103a <phase_4+0x2e>
401035: e8 00 04 00 00 callq 40143a <explode_bomb>
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 //num1→edi
401048: e8 81 ff ff ff callq 400fce <func4> //
|
输入规则和上一关一样,第一个数需在0到14之间(cmpl只能用于无符号数?)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
0000000000400fce <func4>:
//首次调用时:%eax:0x2 %ebx:0 %ecx:0 %edx:0xe %esi:0x0 %edi:num1
400fce: 48 83 ec 08 sub $0x8,%rsp
400fd2: 89 d0 mov %edx,%eax //eax:14
400fd4: 29 f0 sub %esi,%eax //eax:14-0
400fd6: 89 c1 mov %eax,%ecx //ecx:14
400fd8: c1 e9 1f shr $0x1f,%ecx //ecx:0 //逻辑右移31,即取符号位。
400fdb: 01 c8 add %ecx,%eax //eax:14+0
400fdd: d1 f8 sar %eax //算术右移1位?eax:14/2=7
400fdf: 8d 0c 30 lea (%rax,%rsi,1),%ecx //ecx:7+0
400fe2: 39 f9 cmp %edi,%ecx //比较num1和7
400fe4: 7e 0c jle 400ff2 <func4+0x24> //不大于→r17
400fe6: 8d 51 ff lea -0x1(%rcx),%edx //edx:ecx-1=6
400fe9: e8 e0 ff ff ff callq 400fce <func4> //递归→r3
400fee: 01 c0 add %eax,%eax
400ff0: eb 15 jmp 401007 <func4+0x39> //跳出
400ff2: b8 00 00 00 00 mov $0x0,%eax
400ff7: 39 f9 cmp %edi,%ecx //比较num1和7
400ff9: 7d 0c jge 401007 <func4+0x39> //不小于
400ffb: 8d 71 01 lea 0x1(%rcx),%esi //esi:ecx+1=8
400ffe: e8 cb ff ff ff callq 400fce <func4> //递归→r3
401003: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax //eax=2*eax+1
401007: 48 83 c4 08 add $0x8,%rsp
40100b: c3 retq
|
前面一通算术操作,后面设计了递归。
这里一步移位操作看起来像是取符号位,但是输入一定大于0,符号位是0,所以这个操作意义何在?
人肉IDA走起:
1
2
3
4
5
6
7
8
9
|
int fun4(int num1,int x,int y){
int s,a;
s=(x-y)/2+y;
if(num1>s) return 2*fun4(num1,s-1,y);
a=0;
if(num1<s) return 2*fun4(num1,x,s+1)+1;
return a;
}
fun4(num1,14,0);
|
第一个数设为7可避免递归调用,但返回值不是0,不符合。
1
2
3
4
5
6
7
|
40104d: 85 c0 test %eax,%eax //eax=0
40104f: 75 07 jne 401058 <phase_4+0x4c>//不等于0爆炸
401051: 83 7c 24 0c 00 cmpl $0x0,0xc(%rsp) //mun2和0比较?
401056: 74 05 je 40105d <phase_4+0x51>//不等于0爆炸
401058: e8 dd 03 00 00 callq 40143a <explode_bomb>
40105d: 48 83 c4 18 add $0x18,%rsp
401061: c3 retq
|
看到的返回值和num2皆需为0,则num2确定。
大不了爆破呗,索性试了1次就成了。emm
【后面三关施工中。。。】
第五关
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
|
0000000000401062 <phase_5>:
401062: 53 push %rbx
401063: 48 83 ec 20 sub $0x20,%rsp
401067: 48 89 fb mov %rdi,%rbx
40106a: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax //???
401073: 48 89 44 24 18 mov %rax,0x18(%rsp)
401078: 31 c0 xor %eax,%eax //eax:0
40107a: e8 9c 02 00 00 callq 40131b <string_length>
40107f: 83 f8 06 cmp $0x6,%eax //输入长度为6
401082: 74 4e je 4010d2 <phase_5+0x70>
401084: e8 b1 03 00 00 callq 40143a <explode_bomb>
401089: eb 47 jmp 4010d2 <phase_5+0x70>
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 //计数变量rax
4010a8: 48 83 f8 06 cmp $0x6,%rax //循环6轮
4010ac: 75 dd jne 40108b <phase_5+0x29> //循环尾
4010ae: c6 44 24 16 00 movb $0x0,0x16(%rsp)
4010b3: be 5e 24 40 00 mov $0x40245e,%esi //??
4010b8: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi
4010bd: e8 76 02 00 00 callq 401338 <strings_not_equal>
4010c2: 85 c0 test %eax,%eax
4010c4: 74 13 je 4010d9 <phase_5+0x77>
4010c6: e8 6f 03 00 00 callq 40143a <explode_bomb>
4010cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1) //???
4010d0: eb 07 jmp 4010d9 <phase_5+0x77>//跳出
4010d2: b8 00 00 00 00 mov $0x0,%eax
4010d7: eb b2 jmp 40108b <phase_5+0x29>
4010d9: 48 8b 44 24 18 mov 0x18(%rsp),%rax
4010de: 64 48 33 04 25 28 00 xor %fs:0x28,%rax //???
4010e7: 74 05 je 4010ee <phase_5+0x8c>
4010e9: e8 42 fa ff ff callq 400b30 <__stack_chk_fail@plt>
4010ee: 48 83 c4 20 add $0x20,%rsp
4010f2: 5b pop %rbx
4010f3: c3 retq
|
第六关
隐藏关
隐藏关藏在每一关的后面,
GDB使用:
基础:
1
2
3
4
5
|
q : quit
h : help
file prog//加载程序,也可作为gdb命令的参数
r : run
k : kill
|
断点:
1
2
3
4
5
6
7
8
9
10
|
b : breakpoints break
- func_name
- *0x400522
- &var
- main.c:100//源代码断点,运行前即可
- if con//条件断点
w : watch //观察对象变化时断点
d : delete
- b n
disable b n
|
执行:
1
2
3
4
5
6
|
c : continue
f : finish
stepi n
nexti
set args ./a.txt //从文件读取输入
|
检查代码:
1
2
3
4
5
6
|
disas //展示汇编
- funcname
- 0x400000
-
list
edit
|
检查数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
x : examine
p : print
格式控制:/[n][f][u]
- n:内存单元个数
- f:显示格式:
- x(hex) 按十六进制格式显示变量。
- d(decimal) 按十进制格式显示变量。
- u(unsigned decimal) 按十进制格式显示无符号整型。
- o(octal) 按八进制格式显示变量。
- t(binary) 按二进制格式显示变量。
- a(address) 按十六进制格式显示变量。
- c(char) 按字符格式显示变量。
- f(float) 按浮点数格式显示变量
- u:单元长度(按字节)
i : info
- r : registers
- b [n]
- $rsp
|
表达式:
堆栈: