写在前面
贴一个下载链接CS:APP3e, Bryant and O’Hallaron (cmu.edu),点击Self-Study Handout下载lab。
/* Do all sorts of secret stuff that makes the bomb harder to defuse. */
initialize_bomb();
printf("Welcome to my fiendish little bomb. You have 6 phases with\n");
printf("which to blow yourself up. Have a nice day!\n");
/* Hmm... Six phases must be more secure than one phase! */
input = read_line(); /* Get input */
phase_1(input); /* Run the phase */
phase_defused(); /* Drat! They figured it out!
在进入到phase_1之前,对于input = read_line()函数,我们查看一下input变量的地址,使用gdb bomb命令进入gdb调试阶段,再通过查看反汇编代码确定input = read_line()函数return时的栈顶指针%rsp所指向的内容,先设置断点,为了方便,使用touch ~/.gdbinit命令创建一个默认配置,具体可以参考这个链接。


在x86-64架构中,返回值会通过特定的寄存器传递。在此架构中,返回值通常存放在 RAX 寄存器中。因此,当你的函数执行到 ret 指令时,返回值会放入 RAX 寄存器,建议在phase_1之前输入p $rax查看寄存器rax的值。
下图描述了进入explode_bomb()的gdb调试界面,代表这个炸弹即将爆炸:

gdb调试命令
gdb //命令
step //步进
layout regs //寄存器变量视图
p/d $rdi //十进制查看变量%rdi的值
x/s $esi //以c风格查看%esi的字符串
x/2d $rsp //十进制查看$rsp开始的两个单位,栈指针
info registers //打印所有寄存器的值
info breakpoints // 打印所有断点的信息
// %rip 储存下一条指令的地址
调试命令示例:

可以看到,在返回read_line 函数后,%rax寄存器的值被保存到了%rdi中,%rdi的值是6305664,%rsp是栈顶指针。
string_length复现

if (rdi != 0)
{
rdx = rdi;
rdx = rdx + 1;
eax = rdx;
rax = rdi
if (rdx != 0) 则跳到 rdx = rdx + 1;
}
接下来一起解答。
Phase_1
mov $0x402500, %edi ; 假设这是第二个字符串的地址,放入 %edi
call 401338 <strings_not_equal> ; 调用函数比较两个字符串
传入<strings_not_equal>的参数有两个%esi和%rdi
%esi:通常用来传递第一个指针或地址类型的参数给函数。在字符串操作函数中,它经常被用来传递源字符串的地址。%edi:通常用来传递第二个指针或地址类型的参数给函数。在字符串操作函数中,它经常被用来传递目标字符串的地址。- Linux遵循System V AMD64 ABI 调用约定,具体来说,函数传参的时候前六个整数或指针参数通常分别通过寄存器
RDI,RSI,RDX,RCX,R8,R9传递。 也就是说,我们只需要查看%esi的值就行了,在0x400ee9设置断点,输入x/s $esi得到:
于是我们返回到psol.txt中写下这句话,注意不要漏了标点。
Phase_2
第一步,通读代码了解跳转结构,<read_six_numbers >函数告诉我们应该在psol.txt文件下写入6个数字。
接着,我们可以在0x400f0e、0x400f1e、0x400f3a处设置断点(主要是前面两个),这三处都是跳转语句。对于(%rbx),使用x/d $rbx或者x/2d $rbx查看十进制的值,对于%eax,可以使用p/d查看十进制的值。<read_six_numbers >的返回结果先存在于%rsp中,随后第一位数字被“吃掉”,剩下的数字放在了(%rbx)中。
接着:

接下来,当gdb每一次在0x400f1e处停下时,我们可以通过p/d $eax查看%eax的值,修改对应的%rbx的值就可以了,最后答案是1 2 4 8 16 32。

phase_3
- 首先观察代码结构,凭借个人经验在
je、ja、jmp处打上breakpoints,很明显phase_3的代码有switch关键字。
在sscanf函数之前添加断点,使用(gdb) x/s $esi可以看到两个%d %d,这提醒我们输入十进制的两个数字。在400f6a处提醒我们输入的第一个数字应当小于7,我这里输入的是7。在400fb9和400fbe可以看到第二个数字应该等于0x137,也就是327。
解开答案,调试界面输出:

phase_4
我们注意到了这一句话,结合上一题的经验,很有可能在提醒我们应该有两个输入。
于是我们在psol.txt中写下:999 666。
continue!
phase_4 step1
我们在调试时发现:
这意味着输入的第一个数字应该小于等于14。
phase_4 step2
在调试时,遇到了难点<func4>它是一个递归函数,我们需要让其返回值%rax的值等于0,这样才不会触发0x40104f的跳转:

func4

这段代码类似于二分查找,发现其有极高的对称性。当输入的rdi(应该是rdi)为1时,func4函数返回值为0。
phase_4 step3
0x401051启示我们输入的第二个参数为0。
因此答案:1 0

phase_5
先标好跳转结构:
看到函数string_length和cmp $0x6 %eax输入应该是长为6的字符串,于是我们在psol.txt中写下:
'xjtu'
在0x4010bd处,我们发现:
'xjtu' -> snoies, snoies作为输入时,$rsp+0x10的值是就变成了snoies=>uylfeu。我们需要某六个数字,使得变换之后的结果是flyers。这究竟是什么样的映射呢?
这个过程不可谓不离奇曲折,但是当我们知道了具体的过程之后,就可以很容易地得到答案了。
首先,我们先去维基百科找来一张 ASCII 码表:

分析
在0x4010778处, xor %eax,%eax执行异或操作,将EAX寄存器与自身进行异或。由于任何数与自身进行异或的结果都是0,这条指令通常用于将EAX寄存器的值快速清零。
movzbl 指令是一个数据传送指令,从内存地址 %rdx + 0x4024b0 处读取一个字节(8位)的值,将这个字节值移动到EDX寄存器的最低位字节,之后将EDX寄存器的高位24位填充为零。
在0x40108b到0x4010ac,我们可以看到一个循环,直到%rax的值满6方能离开,但是这个循环中的代码十分难懂。
以输入'xjtu'为例,在循环中我们发现寄存器%rcx的变化,分别是115=》110=》111=》105=》101=》115,分别对应 snoies的acsii码,而读取'xjtu'的寄存器%rcx,值分别是39=>120=>106=>116=>117=>39,在下面语句设置断点发现%rdx的值就此变化:
401099: 0f b6 92 b0 24 40 00 movzbl 0x4024b0(%rdx),%edx
4010a0: 88 54 04 10 mov %dl,0x10(%rsp,%rax,1)
我们尝试破解它,在循环任意一处打上断点后,我们对movzbl 0x4024b0(%rdx),%edx进行探索,它将0x4024b0作为地址偏移量,读取一个字符,将其转换为无符号32位整数值放入$edx,接着%edx的最低八位寄存器%dl将字符入栈。现在调整地址偏移量看看:
以'xjtu'的输入中的'为例子,在0x401099处:
以'xjtu'的输入中的x为例子,在0x401099处(%rdx未变化):
以'xjtu'的输入中的j为例子,在0x401099处:
以'xjtu'的输入中的t为例子,在0x401099处:
以'xjtu'的输入中的u为例子,在0x401099处:
结合下面指令会发现取了%rcx的低位为%rdx寄存器的初始值。
401096: 83 e2 0f and $0xf,%edx
于是会发现一个很奇怪的字符串aduiersnfotvbyl,第七个是s,第八个是n,与%rdx初值刚好相等。至此,在psol.txt中输入:ionefw即可,答案不唯一。
这代表我们 :Success!
phase_6 (已烂尾)
看来又是读入六个数字,0x401121及其附近告诉我们第一个数应该小于等于5。

%rbx寄存着读入数字的个数。
未完待续……

