实验简介
二进制炸弹是一个作为目标代码提供的程序。运行时提示用户输入6个不同的字符串,如其中一个字符串不正确,炸弹会引爆并打印一条错误信息。需要通过反汇编确定输入的6个字符串,从而拆除炸弹。
知识点
- 汇编语言基础
- GDB和OBJDUMP工具的使用
实验环境
Centos7 x86_64
获取二进制炸弹
首先从CSAPP官网获取二进制炸弹bomb.tar: http://csapp.cs.cmu.edu/3e/labs.html
在linux下执行tar xvf bomb.tar,得到二进制炸弹的文件,文件列表如下:
|-- bomb # 二进制炸弹,x86-64位
|-- bomb.c # 主程序,逻辑是接受用户输入的6个字符串,并判断每个字符串是否正确。如果正确,调用phase_defused进入下一关,否则调用explode_bomb引爆炸弹
第一关
1. 首先理解main函数执行过程
反汇编炸弹,使用objdump -d bomb > bomb.txt命令,内容如下:
0000000000400da0 <main>:
400da0: 53 push %rbx
......
400e19: e8 84 05 00 00 callq 4013a2 <initialize_bomb>
400e1e: bf 38 23 40 00 mov $0x402338,%edi
400e23: e8 e8 fc ff ff callq 400b10 <puts@plt>
400e28: bf 78 23 40 00 mov $0x402378,%edi
400e2d: e8 de fc ff ff callq 400b10 <puts@plt>
400e32: e8 67 06 00 00 callq 40149e <read_line> # 接受用户输入的字符串,保存到%rax寄存器
400e37: 48 89 c7 mov %rax,%rdi # 用户输入的字符串保存在$rdi寄存器,并作为phase_1函数的第一个入参传递
400e3a: e8 a1 00 00 00 callq 400ee0 <phase_1> # 调用phase_1(), 执行第一关代码
400e3f: e8 80 07 00 00 callq 4015c4 <phase_defused>
查看0x400e32处的代码: 主程序调用read_line接收用户输入的字符串,保存到$rax,并且将该字符串作为函数入参传递给phase_1,执行第一阶段的代码。
这里需要理解x86-64的过程调用规则:(关于过程调用,可参考《CSAPP原书第三版》3.7小节 —— 过程)
- 函数调用中,利用
%rax寄存器保存返回值。 - 关于参数传递 , 如果函数参数不超过6个,会依次通过
%rdi,%rsi,%rdx,%rcx,%r8,%r9传递; 如超过6个参数,超出的参数利用栈传递。 %rbx, %rbp, %r12~%15被划分为被调用者保存寄存器;其余的寄存器,除了栈指针%rsp,都被划分为调用者保存寄存器。
2. 理解phase_1执行过程
查看phase_1的代码,如下:
0000000000400ee0 <phase_1>:
400ee0: 48 83 ec 08 sub $0x8,%rsp
400ee4: be 00 24 40 00 mov $0x402400,%esi # $esi作为strings_not_equal函数的第二个参数传递
400ee9: e8 4a 04 00 00 callq 401338 <strings_not_equal> # 调用strings_not_equal函数,比较用户输入的字符串($rdi)和$esi处的字符串是否相等
400eee: 85 c0 test %eax,%eax # 判断%eax的值是否为0
400ef0: 74 05 je 400ef7 <phase_1+0x17> # 如%eax等于0,跳转到400ef7,正常退出
400ef2: e8 43 05 00 00 callq 40143a <explode_bomb> # 如%eax不等于0,炸弹爆炸
400ef7: 48 83 c4 08 add $0x8,%rsp
400efb: c3 retq
phase_1执行过程如下:
- 首先通过
callq指令调用strings_not_equal函数。根据过程调用的规则,可以确定这个函数接受2个参数。第一个参数是%rdi, 上面分析过,是我们拆弹时输入的字符串;第二个参数是$esi,值为0x402400。顾名思义,strings_not_equal函数用来判断两个字符串是否相等。 - 接着用
test指令判断函数的返回值是否为0。如果为0进行跳转并返回,调用phase_defused拆除炸弹,否则通过callq指令调用explode_bomb,引爆炸弹。
因此,拆弹的关键在于,确认0x402400地址处的字符串是什么。只需要在0x400ee9处打个gdb断点就可以了。
3. gdb调试炸弹
执行gdb bomb, 设置断点b *0x400ee9, 执行run。 此时程序会要求用户输入字符串,先随便输一个字符串使程序运行到我们设置的断点 0x400ee9处,如下:
# gdb bomb
(gdb) b *0x400ee9 # 设置断点, b相当于break
Breakpoint 1 at 0x400ee9
(gdb) r # 运行程序, r相当于run
Starting program: /home/pc/CSAPP/bomb/bomb
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
123 # 随便输入一个字符串,让程序运行到断点0x400ee9
Breakpoint 1, 0x0000000000400ee9 in phase_1 ()
(gdb) p (char *)$rdi # $rdi保存用户输入字符串,为123,与x/s $rdi指令等价
$2 = 0x603780 <input_strings> "123"
(gdb) x/s $esi # 查看esi处字符串,这就是第一关的答案
$1 = 0x402400 "Border relations with Canada have never been better."
查看%esi处字符串, 即第一关的答案,如下:
Border relations with Canada have never been better.
用quit指令退出gdb, 新建一个文件answer,将答案写入该文件。重新执行bomb程序,指定answer文件名作为参数,如下:
# ./bomb answer
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Phase 1 defused. How about the next one?
显示Phase 1 defused, 表示第一关已成功通过。
几个调试汇编代码相关的GDB指令
使用disassemble指令查看汇编代码,箭头表示下一步即将执行的汇编指令。
si指令用于单步执行汇编代码,相当于stepi
ni指令以函数调用为单位进行单步执行, 相当于nexti
(gdb) disassemble
Dump of assembler code for function phase_1:
0x0000000000400ee0 <+0>: sub $0x8,%rsp
0x0000000000400ee4 <+4>: mov $0x402400,%esi
=> 0x0000000000400ee9 <+9>: callq 0x401338 <strings_not_equal>
0x0000000000400eee <+14>: test %eax,%eax
0x0000000000400ef0 <+16>: je 0x400ef7 <phase_1+23>
0x0000000000400ef2 <+18>: callq 0x40143a <explode_bomb>
0x0000000000400ef7 <+23>: add $0x8,%rsp
0x0000000000400efb <+27>: retq
End of assembler dump.
更多用法参考gdb手册或《CSAPP 原书第3版》3.10.2小节 —— 使用GDB调试器
拆弹小技巧
-
查看
bomb符号表或者直接查看汇编代码,会发现有个explode_bomb符号,该函数用来引爆炸弹。 -
调试时可以对该函数设置断点,在拆弹失败时暂停运行,不让其爆炸,便于调试。可使用
b explode_bomb指令设置断点
第二关
先贴出第二关phase_2的部分汇编代码:
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作为read_six_numbers的第二个入参传递, $rsp既是入参也是出参;第一个入参为$rdi, 即用户输入的字符串
400f05: e8 52 05 00 00 callq 40145c <read_six_numbers> # 读六个数字
......
phase_2程序先调用read_six_numbers。该函数接受两个参数,第一个参数为$rdi, 即我们输入的字符串;第二个参数为$rsp, $rsp既是入参也是出参,用于保存read_six_numbers函数解析$rdi后得到的6个整数。想得到这个结论,需要分析read_six_numbers代码,如下:
(gdb) disassemble read_six_numbers
Dump of assembler code for function read_six_numbers:
0x000000000040145c <+0>: sub $0x18,%rsp
0x0000000000401460 <+4>: mov %rsi,%rdx # sscanf函数的第1个可变参数,第3个参数,通过%rdx传递
0x0000000000401463 <+7>: lea 0x4(%rsi),%rcx # sscanf函数的第2个可变参数,第4个参数, 通过%rcx传递
0x0000000000401467 <+11>: lea 0x14(%rsi),%rax # sscanf函数的第6个可变参数,第8个参数,通过栈传递
0x000000000040146b <+15>: mov %rax,0x8(%rsp)
0x0000000000401470 <+20>: lea 0x10(%rsi),%rax # sscanf函数的第5个可变参数,第7个参数,通过栈传递
0x0000000000401474 <+24>: mov %rax,(%rsp)
0x0000000000401478 <+28>: lea 0xc(%rsi),%r9 # sscanf函数的第4个可变参数,第6个参数, 通过%r9传递
0x000000000040147c <+32>: lea 0x8(%rsi),%r8 # sscanf函数的第3个可变参数,第5个参数, 通过%r8传递
0x0000000000401480 <+36>: mov $0x4025c3,%esi # sscanf函数的第2个参数,通过%esi传递,0x4025c3地址的格式化字符串为"%d %d %d %d %d %d",表示输入字符串应该为6个整数
0x0000000000401485 <+41>: mov $0x0,%eax
0x000000000040148a <+46>: callq 0x400bf0 <__isoc99_sscanf@plt>
0x000000000040148f <+51>: cmp $0x5,%eax # sscanf读入的可变参数需大于5个,否则爆炸
0x0000000000401492 <+54>: jg 0x401499 <read_six_numbers+61>
0x0000000000401494 <+56>: callq 0x40143a <explode_bomb>
0x0000000000401499 <+61>: add $0x18,%rsp
0x000000000040149d <+65>: retq
查看0x400f02, 0x401463 ~ 0x40147c处的代码,我们可以把调用者phase_2中的$rsp看作一个一维数组的首地址,该数组的长度为6,内容依次为$rsp, $rsp + 4, $rsp + 8, $rsp + 12, $rsp + 16, $rsp + 20,用于 保存sscanf函数执行后生成的6个整数。
炸弹用sscanf读取并解析用户输入字符串
注意到0x40148a处调用sscanf函数,作用是从用户输入的字符串%rdi中解析出6个整数,保存到调用者phase_2中的$rsp
C语言中sscanf函数原型如下:
int sscanf(const char *str, const char *format, ...);
对照汇编代码, $rdi相当于sscanf的参数str;查看0x401480代码,$esi相当于sscanf的参数format, 用gdb查看0x4025c3处的字符串,为"%d %d %d %d %d %d", 说明sscanf中的可变参数个数为6,且都是指向int类型的地址。
(gdb) x/s 0x4025c3
0x4025c3: "%d %d %d %d %d %d"
</

本文详细介绍了二进制炸弹的拆解过程,通过分析炸弹的各阶段代码,使用GDB和OBJDUMP工具,逐步揭示了每关的破解技巧和原理。从汇编语言基础到GDB调试,再到具体关卡的策略,全面覆盖了二进制炸弹的拆解流程。
992

被折叠的 条评论
为什么被折叠?



