CSAPP实验:拆解二进制炸弹
序言
关于开放知识共享的声明:
开源和分享的目的是为了相互学习和借鉴,而不是为了抄袭。我们鼓励使用开源项目来了解优秀的编程实践、算法和解决方案。然而,我们严格禁止抄袭他人的代码或将其用于商业目的。请尊重他人的劳动成果,展示你自己的创造力和独立思考能力
学校作业的提交通常会受到查重要求。学校的数据库中往往包含了往届学生提交的内容。因此,严禁随意使用开源项目来完成作业。这种行为将被视为抄袭,可能导致严重的学术后果。我们鼓励学生们通过独立思考和努力来完成作业,从而加深对所学知识的理解和应用能力
本文在完成提交所有作业并结课得到评分后发出,参考本人实验报告原文并进行适当修改
请注意:CSAPP实验存在多种相似问题的不同组合。由此,每个人所得到的具体题目都将会不同。本人仅在此代表其中一种题目组合的解法及其他题目的相关思路。
X86-64架构
一.准备阶段
1.使用tar -xvf ./bomb163.tar
解压缩文件,得到README
、bomb
可执行文件与包含部分C语言源代码的文件
2.使用objdump -S -d bomb > bomb.c.txt
反汇编bomb可执行文件,得到bomb.c.txt
汇编代码,使用vim工具查看
3.使用gdb ./bomb
在GDB调试工具下运行炸弹程序
4.通过break explode_bomb
在函数<explode_bomb>
位置处设下断点,阻止错误时发生爆炸
5.在GDB初始模式下,run
可以运行程序
6.在GDB调试下,使用disassemble
可以反汇编当前函数并查看执行到了汇编的第几步骤
炸弹1
查看<phase_1>
对应汇编代码,发现将0x402690
放入%esi
寄存器中,进入<string_not_equal>
,若返回值为0(false)
则不执行<explode_bomb>
发生爆炸。即应当满足两字符串相等
字符串记录方式为数组地址。通过gdb下在<explode_bomb>
设置断点的方式,避免发生爆炸。在GDB模式下,通过x
指令,查看对应内存地址0x402690
所存的数据为字符串“Only you can give me that feeling.”
。因此答案即为该输入
炸弹2
%rsp
寄存器对应内存地址在<read_six_numbers>
中读入,且单个读入数据为4大小。
若%rsp
寄存器对应地址所存数据不为0,则跳转到<phase_2+0x2b>
行执行,即callq <explode_bomb>
发生爆炸
若%rsp
寄存器对应地址+4 所存数据为1,则跳转到<phase_2+0x30>
行执行,否则发生爆炸
将%rbp
寄存器存入%rsp
寄存器+0x10
作为进行循环的判断条件,即循环4次
每次循环中,%eax
寄存器累加%rbx
寄存器对应地址与%rbx
寄存器对应地址+4处数据,并与%rbx
寄存器对应地址+8处数据进行比较。若不相等则发生爆炸。即对于该数组,从第三位开始,每位数据应等于前两位相加。
因此,输入为0、1开头的共6个累加得到的数,即0 1 1 2 3 5
炸弹3
在调用scanf
之前,查看0x4029ad
地址所存内容为%d %d
,因此为读入两个整型%eax
寄存器的值大于1时会调用<explode_bomb>
函数,否则会继续执行后续指令< phase_3+0x30>
= <400fe5>:cmpl $0x7, (%rsp)
将%rsp
寄存器对应地址数据与7相比较,若大于7则跳转到<phase_3+0x71>
= <401026>
发生爆炸。mov (%rsp),%eax;
jmpq *0x4026e0(,%rax,8);
因此根据输入的第一个数值,将%rsp
寄存器对应地址数据移植%rax
,并跳转到(8 * rax + 0x4026e0)
处所存的地址
若输入1,则跳转到0x4026e8
地址所储存的地址
通过x/x 0x4026e8
查看,该地址为0x400ff5
该行执行:400ff5: mov $0x2d8, %eax
,随后jmp 401037 <phase_3+0x82>
开始比较
因此输入为0x2d8
= 728
因此,最终输入可以为1 728
.
该答案并不唯一。因为输入的首个数可以是0到7的任意整数,对应跳转到(8 * rax + 0x4026e0)
处所存的不同的地址
炸弹4
同理在调用scanf
函数之前,查看0x4029ad
地址所存内容为%d %d
,因此为读入两个整型
将%eax
与0x2
相比较,若%eax
等于数值2
则跳转至4010cd
调用爆炸函数mov (%rsp), %eax
,将读入第一个数据放入%eax
中。并执行%eax-2
,之后再次进行比较。若%eax
等于数值2,则跳转至4010de
,否则发生爆炸。之后,将输入值移入%esi
,$0x8
移入%edi
,调用fun4
函数。由此可以看出,第二个输入应当为4
在phase_4
当中,执行完func4
后,比较func4
函数返回值%eax
与(%rsp+0x4)
所存输入的第二个数进行比较,若相等跳转4010ea
避免发生爆炸
在运行完后,通过查看%eax
数值,确定为216
故输入答案为216 4
炸弹5
在执行输入前,可以注意到,在地址$0x4029ad中所存字符串为“%d %d”,推测为输入两个整型
调用scanf
函数后,通过比较指令检查,如果%eax
的值大于1,那么程序将跳转到phase_5
中的地址0x401134
,否则继续执行下一条指令(发生爆炸)
将栈顶的值(%rsp
指向的地址)(读入的第一个整型)加载到%eax
寄存器中,将%eax
寄存器中的值与15
(二进制四位为1)进行按位与操作,只保留最低的4位,将结果存储回栈顶。将%eax
与 15 进行比较,如果相等,跳转到401171
(发生爆炸)
将0存储到%ecx
寄存器,将0存储到%edx
寄存器,<40114c>
将%edx
加1
从一个数组中读取一个值,数组的起始地址为0x402720
,索引是%rax
的4倍。结果存储在%eax
。%ecx = %eax+%ecx
.将%eax
与15进行比较。如果不相等,跳转到40114c
将 15 存储到栈顶,将%edx
与15进行比较,如果不相等,跳转到401171
(发生爆炸)
将栈上偏移4位置的值(读入的第二个整型)与%ecx
比较,如果相等,跳转到401176
,否则继续执行(发生爆炸)。
由上分析,最终%edx
应该为15。故循环应该执行15次%edx
加1
通过对于0x402720
内存地址的分析,可以得知,以该内存为基址,存放了0-15共16个整型。因此,%eax
分别通过索引访问,并将指向的整型作为下一轮的索引。%eax
最终为15,索引为24(= 4 * 6)
%ecx
为逐次累加的%eax数值,等于第二个输入的整型115
%eax
最初的数值为第一个整型与15进行按位与操作,且操作完不为15
(%eax != 15)
%eax
按位与后为5,故输入为5
通过如上问题分析可得,正确答案应为5 115
炸弹6
将%eax
寄存器的值与立即数0x5
进行比较,如果比较结果满足小于等于条件,则跳转到地址 4011d4
处(避免爆炸)。%eax - 1 <= 5
,故输入的数据应当小于等于6
已知 %r14d
寄存器为0x0 + 0x1 != 0x6
,因此不执行跳转4011ff
%eax
寄存器为%rsp + 4 * %rax地址
处的数据,且 %rax
数值为1。%eax
, 0x0(%rbp)
应当不相等,跳转到4011f1
处(避免爆炸)。(此时%rbp = %rsp
)
此时 %ebx
寄存器数值为0x1 + 0x1 <= 0x5
。故跳转回4011e1
地址。不执行跳转需要执行共计6次使不小于等于5。此时在跳转中仍有比较 %eax
, 0x0(%rbp)
应当不相等。
在跳出循环之后,寄存器%r13
加上4,无条件跳转4011c0
地址。
初次执行时,寄存器 %r13
为寄存器 %rsp
内地址
在外部循环,需要比较$0x6
, %r14d
相等时4011ff
地址,其在各次执行中数值+1
,故需要执行共计6次
由此看出,通过双循环,外部保证输入的六个数要大于等于1,且小于等于6,内部保证互异。因此通过<read_six_numbers>
读入的六个数字为特定顺序的1 2 3 4 5 6
接下来通过循环,将读入的6个数,分别用0x7立即数减去这六个数,并放回原地址当中
通过进行两层循环,构造结构体,并在其中将其存放位置按照输入的数的大小计算得出
扫描内存地址0x6042f0
所得到的node
结构体内容
比较%eax
,(%rbx)
,大于或等于跳转到401293
避免爆炸。%ebp
初始为5,减至0时不再跳转401284
即结束循环。
故可得,通关条件是要求前面node
数据大于后面的节点。即989 > 970 > … > 289
根据node
结构体内的数据,通过7减去输入数据处理后结果为5 3 1 4 2 6
。
因此原来的数据为输入2 4 6 3 5 1
隐藏炸弹
完成“所有”炸弹后,我们注意到了还有一个<secret_phase>
函数没有使用。查找定位只有在<phase_defused>
函数中有对<secret_phase>
函数的调用
在该函数中,对于<strings_not_equal>
函数的返回值 %eax
,若不相等则会跳过调用secret_phase
。因此查看输入了什么字符串。
对于scanf
函数,返回值应为3。因此需要读入三个元素。已知调用该函数时读入了两整型。
在0x402a00
地址处,打印得知:该需要读入的字符串为DrEvil
在炸弹4中调用了scanf
函数。读入方式为“%d %d %s”
。因此在其输入后加上字符串“DrEvil”
。由此,在炸弹6之后进入了隐藏关卡
进入后,查看源代码为首先使用了<read_line>
及<strtol@plt>
函数,将返回值%rax–0x1
与立即数$0x3e8
比较,小于等于可以跳转避免爆炸。因此输入的数应为小于等于1001
。
将 %edi
置为$0x604110
,调用函数<fun7>
。调用该函数之后,要使返回值 %eax
与 $0x7
比较,相等可以跳转避免爆炸。因此探究如何使得函数<fun7>
返回值等于7。
查看函数<fun7>
,可以看出是一个递归调用。其中初始(%rdi = 0x604110)= 36
,%esi= 3
。
开头,若%rdi
等于0,则直接跳转返回0xffffffff
函数递归出口为地址 4012f7
,返回 %eax
存在三条路径,分别为:
1)add %eax, %eax
2)mov $0x0,%eax
3)lea 0x1(%rax,%rax,1), %eax
其分别为:%eax *= 2
; %eax = 0
; %eax = 2*%rax + 1
;
要使%eax
返回值为7,因此做法为:(2).0->(3).1->(3).3->(3).7
其中%rdi
有2种操作,分别为mov 0x8(%rdi),%rdi
; mov 0x10(%rdi),%rdi
其分别为:%rdi += 8
; %rdi += 16
;
对于rdi附近内存地址的考察有:
因此考察需要输入的数据:1001
ARM架构
一
查看<phase_1>
源代码,可以看出传入了x1
寄存器数据为基地址0x402000
以及加值0x608
。故使用x
查看当前内存数据,将该字符串传入后与输入数据进行比较。若<strings_not_equal>
返回为0
则字符串匹配,不爆炸结束阶段一
二
查看<phase_2
>源代码,可以看出其通过<read_six_numbers>
读入了6个数。
对于入栈区的元素,首个元素应为0,次位元素应为1,之后执行循环共计6次。
其中下一个元素等于前两个元素之和,因此输入应为0 1 1 2 3 5
可以满足要求。
三
对于<phase_3>
源代码,首先赋值x1
寄存器基地址0x402000
,然后增加数值0x368
。scanf
返回值为读入数据个数,应不小于等于1.
此处读入两个数。将首个数[x29, #28]
加载入x0
寄存器,可以看出,若读入首个数为1时,首先比较1与3,跳转到0x401134
,然后等于1,跳转到0x401150
。此处加载的第二个数据为0x4a = 74
。因此输入1 74
可以满足要求。
四
在函数<phase_4>
当中,首先读入2个数据,且读入首个数据应当小于0xe = 14
。之后调用<func4>
函数进行处理
首先查看其返回值。返回值应当与读入的第二个数值相等
通过输入不同的数据后,使用gdb查看寄存器w0
数据,可以得到8 35
满足要求
五
可以看出,读入了一串字符串,且字符串长度为6。查看最后进行比较的部分,其中x1寄存器为基地址0x402000 + 0x640
0x402640
所存字符串为“sabres”
。可以看出,其中对读入的字符串进行了处理,使用x2寄存器0x4025f8为首地址的字符数组进行构建。其中数组偏移量为读入字符串ASCII的二进制末4位,即十六进制末位0 - F.
偏移量分别为7 1 13 6 5 7
。故ASCII十六进制末位为7 1 D 6 5 7
可以存在的一种情况为GAMFEG
六
相似的思路,首先查看最后进行比较的地址数据
得到寄存器w4
基地址为0x420000
,加上了0x110
的偏移量。故查看0x420110
处的地址信息
成功读取到了结构体node1
的信息
继续向后查找可以得到其他结构体的信息
将结构体内360 – 211
进行比较,可以得到输入的六个数应为4 3 2 1 6 5