前言
“二进制炸弹”是作为目标代码文件提供给学生的程序。运行时,它提示用户键入6个不同的字符串。如果这些中的任何一个是不正确的,炸弹“爆炸”,打印一个错误消息。我们必须通过拆卸和反向工程来确定6个字符串应该是什么,来“消除”自己独特的炸弹。本实验的主要目的是熟悉汇编语言,并强制学习如何使用调试器。
本实验详细项目文件以及分析解决方案参见BombLab,下载实验代码,解压,进入工作目录,下面进入惊险刺激的的破译之旅。
基本知识
寄存器基本使用规律
1 | 12~15 被调用者保存寄存器 rbp rbx r |
汇编指令阅读
指令编写方式:指令名 源操作数 目的操作数
源操作数和目的操作数不能同时都为内存单元
对于源操作数和目的操作数中,除lea指令外,其他包含括号的指令均为访问对应的内存单元的值,在C语言中,可以将该变量视为指针变量。
cmp指令使用目的操作数-源操作数
,test指令使用目的操作数&源操作数
,接下来的条件指令根据命令规则进行跳转。
栈示意图
在一个函数栈帧中,首先将被调用者保存寄存器中的值入栈,因为在被调用函数中,会使用上述寄存器,因此需要保存,并在函数返回时,按照与入栈相反顺序重新出栈
;然后再保存函数中局部变量;最后,如果调用函数的参数多于6个,还需要按照从右至左顺序构造调用函数剩余的参数,栈示意图如下图所示。
phase_1
首先,在solutions.txt
文件中输入任意字符串;然后使用gdb命令gdb ./bomb
进入gdb调试,在gdb中使用set args ./solutions
设置破解密码保存文件名;最后,使用break phase_1
命令设置断点,运行程序。程序将在phase_1处进入断点,使用disas phase_1
反汇编phase_1获得对应的汇编代码如下:
1 | (gdb) disas phase_1 |
根据寄存器使用规则,可知%rdi
保存输入字符串,%esi
保存strings_not_equal
函数第二个参数内容。另外,从函数名称可以该函数比较两个字符串是否相等。
在GDB中使用x/s 0x402400查看0x402400内存单元中字符串的内容,从而获得密码
1 | (gdb) x/s 0x402400 |
phase_2
在phase_2函数处设置断点,然后反汇编phase_2
函数:
1 | (gdb) disas phase_2 |
在phase_2
函数中调用read_six_numbers
函数从输入中读入输入,所以继续反汇编出read_six_numbers
函数:
1 | (gdb) disas read_six_numbers |
在gdb中使用x/s 0x4025c3
查看输入格式化字符串为:
1 | (gdb) x/s 0x4025c3 |
因此,phase_2函数调用read_six_numbers
函数读入6个整型数据,。另外,从read_six_numbers
调用sscanf
函数前构造函数参数代码可知
1 | %rsi:保存phase_2栈帧的局部变量开始地址 |
在
phase_2
函数反汇编代码中,详细注释了每一个汇编语句的含义,很容易知道,该函数循环判断读入的数字中后一个数是否为前一个数的2倍,并且读入的第1个数必须为1。因此phase_2
函数的破解密码为1 2 4 8 16 32
phase_3
phase_3
函数的反汇编代码和详细注释如下:
1 | (gdb) disas phase_3 |
其中关键在于意识到0x0000000000400f81
地址的代码为switch
语句的跳转表,能否破解关卡的密码在于输入的两个参数中第一个参数作为switch
语句的参数,第2输入个参数是否和switch
语句的返回值相等。因此,使用gdb查看0x402470
开始的地址的内存单元的内容获得switch
跳转表如下所示:
1 | (gdb) x/1xg 0x402470 |
由跳转表从而获得switch
语句返回值如下:
1 | %rax(输入参数1) 跳转地址 0xc(%rsp)(输入参数2) |
所以,
phase_3
函数的破解密码存在上述多组。
phase_4
phase_4
函数的反汇编代码和详细注释如下:
1 | Dump of assembler code for function phase_4: |
在phase_4
函数中要求func4
函数的返回值等于0,并且func4
函数的参数为:第一个输入数,0,14。func4
函数反汇编代码如下所示:
1 | Dump of assembler code for function func4: |
其中,%rdi %rsi %rdx
依次保存第1,2,3个参数,分别对应于a b c;%eax
表示返回值。另外定义局部变量int result
, 保存在%rax
作为返回值,定义局部变量int tmp
,保存在%rcx
。按照上述定义,获得func4
函数对应的C语言代码:
1 | int func4(int a, int b, int c) |
使用如下的测试程序,获得所有满足phase_4
函数的破解密码:
1 | int main() |
因此phase_4破译可能结果为:
1 | 0 0 |
phase_5
phase_5
函数反汇编代码和详细注释如下所示:
1 | (gdb) disassemble phase_5 |
使用gdb查看0x4024b0
和0x40245e
开始的内存单元的内容:
1 | (gdb) x/32xb 0x4024b0 |
flyers
串对应的ascii值为0x66 0x6c 0x79 0x65 0x72 0x73
,与0x4024b0
内存地址开始的查找表比较获得偏移量为0x9 0xF 0xE 0x5 0x6 0x72
。因此输入长度为6的字符串中每个字符的低4bit的值分别为0x9 0xF 0xE 0x5 0x6 0x72
。所以,phase_5
函数的破解密码存在两种情形:若输入为大写字母,将低4bit的值加上0x40
,获得输入字符串IONEFG
,若输入为小写字母,将低4bit的值加上0x60
,获得输入字符串ionefg
。
phase_6
phase_6
函数反汇编代码有点长,需要一点耐心去解读。熟悉整个流程下来,其实phase_6
函数主要包含了4个循环过程。
1 | (gdb) disas phase_6 |
我们假设输入数据为4 3 2 1 6 5
,并且猜测0x6032d8
为链表首地址,链表中每个节点占用12个Byte,前8字节保存两个4字Byte的整型数,剩余的4Byte存放下个节点地址。
在第2个循环结束后,使用gdb查看使用7减去对应的输入后的数据:
1 | (gdb) p /x $rsp |
重新调整链表前的链表的结构:
1 | (gdb) x/24xw 0x006032d0 |
保存在栈中链表节点信息:
1 | (gdb) x/6xg 0x7fffffffe290 |
按照7减去对应的输入后重新调整链表后的链表结构索引顺序为3 4 5 6 1 2
,对应的链表结构为:
1 | (gdb) x/24xw 0x006032d0 |
因此,
phase_6
函数的破译过程为将链表中每个节点按照前4字节降序排序,降序的索引为3 4 5 6 1 2
,因为在前面使用7减去对应的值,所以破解密码为4 3 2 1 6 5
。
secret_phase
在整个实验中,还隐藏了一个秘密关卡,秘密关卡的入口位于phase_defused
函数中。因此,反汇编phase_defused
函数:
1 | Dump of assembler code for function phase_defused: |
使用gdb查看一系列的立即数对应的内容:
1 | (gdb) x/s 0x402619 |
猜测应该输入字符串DrEvil
时进入secret_phase
函数,并且同一行中前两个输入值为两个数字,满足条件的只有phase_3和phase_4
,直接穷举,获知在第4关末尾输入字符串DrEvil
进入秘密关卡。secret_phase
函数的反汇编代码如下:
1 | Dump of assembler code for function secret_phase: |
假设fun7的函数原型为int fun7(int *a, int b)
;并且变量a保存在%rdi,变量b保存在%rsi
中,另外定义局部变量int result,保存在%eax作为返回值以及变量int tmp,保存在%edx
。fun7
函数反汇编代码以及详细注释如下:
1 | Dump of assembler code for function fun7: |
fun7
对应的C程序:
1 | int fun7(int *a, int b) |
破解思路
1 | 即指针变量a的地址为0x6030f0,求参数b的值,使得函数的返回值等于2? |
结束语
历经两天的时间,仔细阅读理解每一行汇编语言,重新整理CSAPP第3章相关内容,并且在每一个过程调用时,动手去绘制对应的栈示意图,虽然前面几次感觉生疏,特别费时间,但随着更深入的理解,逐步进入状态,所以后面即使面对像phase_6
这样复杂的函数也得心应手。这也是第一次创作如此篇幅之长的博文,所以难免有诸多的纰漏错误之处,诸位可以留言指正。