CSAPP-Bomblab
前言
The nefarious Dr. Evil has planted a slew of “binary bombs” on our class machines. A binary bomb is a program that consists of a sequence of phases. Each phase expects you to type a particular string on stdin. If you type the correct string, then the phase is defused and the bomb proceeds to the next phase. Otherwise, the bomb explodes by printing “BOOM!!!” and then terminating. The bomb is defused when every phase has been defused. There are too many bombs for us to deal with, so we are giving each student a bomb to defuse. Your mission, which you have no choice but to accept, is to defuse your bomb before the due date. Good luck, and welcome to the bomb squad!
邪恶的Evil博士在我们的课堂机器上种植了一大堆“二进制炸弹”。二进制炸弹是一个由一系列阶段组成的程序。每个阶段都希望你在stdin上键入一个特定的字符串。如果你键入正确的字符串,那么阶段就被拆除了,炸弹就会进入下一个阶段。否则,炸弹会打印“BOOM!!!”,然后终止。当每个阶段都被拆除时,炸弹就被拆除了。
也就是说,我们需要在每个阶段输入一个字符串,如果输入正确,就会进入下一个阶段,如果输入错误,就会爆炸。
具体确定每个阶段的输入字符串,我们需要通过反汇编来确定。
可以使用gdb来进行反汇编。
gdb bomb
disas main
得到的main函数的核心代码如下:
main:
0x0000000000400e32 <+146>: call 0x40149e <read_line>
0x0000000000400e37 <+151>: mov %rax,%rdi
0x0000000000400e3a <+154>: call 0x400ee0 <phase_1>
0x0000000000400e3f <+159>: call 0x4015c4 <phase_defused>
0x0000000000400e44 <+164>: mov $0x4023a8,%edi
0x0000000000400e49 <+169>: call 0x400b10 <puts@plt>
0x0000000000400e4e <+174>: call 0x40149e <read_line>
0x0000000000400e53 <+179>: mov %rax,%rdi
0x0000000000400e56 <+182>: call 0x400efc <phase_2>
0x0000000000400e5b <+187>: call 0x4015c4 <phase_defused>
0x0000000000400e60 <+192>: mov $0x4022ed,%edi
0x0000000000400e65 <+197>: call 0x400b10 <puts@plt>
0x0000000000400e6a <+202>: call 0x40149e <read_line>
0x0000000000400e6f <+207>: mov %rax,%rdi
0x0000000000400e72 <+210>: call 0x400f43 <phase_3>
0x0000000000400e77 <+215>: call 0x4015c4 <phase_defused>
0x0000000000400e7c <+220>: mov $0x40230b,%edi
0x0000000000400e81 <+225>: call 0x400b10 <puts@plt>
0x0000000000400e86 <+230>: call 0x40149e <read_line>
0x0000000000400e8b <+235>: mov %rax,%rdi
0x0000000000400e8e <+238>: call 0x40100c <phase_4>
0x0000000000400e93 <+243>: call 0x4015c4 <phase_defused>
0x0000000000400e98 <+248>: mov $0x4023d8,%edi
0x0000000000400e9d <+253>: call 0x400b10 <puts@plt>
0x0000000000400ea2 <+258>: call 0x40149e <read_line>
0x0000000000400ea7 <+263>: mov %rax,%rdi
0x0000000000400eaa <+266>: call 0x401062 <phase_5>
0x0000000000400eaf <+271>: call 0x4015c4 <phase_defused>
0x0000000000400eb4 <+276>: mov $0x40231a,%edi
0x0000000000400eb9 <+281>: call 0x400b10 <puts@plt>
0x0000000000400ebe <+286>: call 0x40149e <read_line>
0x0000000000400ec3 <+291>: mov %rax,%rdi
0x0000000000400ec6 <+294>: call 0x4010f4 <phase_6>
0x0000000000400ecb <+299>: call 0x4015c4 <phase_defused>
0x0000000000400ed0 <+304>: mov $0x0,%eax
0x0000000000400ed5 <+309>: pop %rbx
0x0000000000400ed6 <+310>: ret
可以发现,每个阶段都是通过调用read_line函数来读取输入的字符串,然后调用对应的phase函数来判断输入的字符串是否正确。因此我们需要将注意力放在每个phase函数上。
Phase 1
read_line将读取到的字符串的首地址存放于%rax中,然后将%rax的值传递给%rdi,然后调用phase_1函数,就是说在phase_1函数中,我们可以通过%rdi来获取输入的字符串。%rdi一般用于存放第一个参数。
在gdb中输入
disas phase_1
得到的phase_1函数的核心代码如下:
0x0000000000400ee0 <+0>: sub $0x8,%rsp
0x0000000000400ee4 <+4>: mov $0x402400,%esi
0x0000000000400ee9 <+9>: call 0x401338 <strings_not_equal>
0x0000000000400eee <+14>: test %eax,%eax
0x0000000000400ef0 <+16>: je 0x400ef7 <phase_1+23>
0x0000000000400ef2 <+18>: call 0x40143a <explode_bomb>
0x0000000000400ef7 <+23>: add $0x8,%rsp
0x0000000000400efb <+27>: ret
函数将0x402400存放于%esi中,然后调用一个名叫strings_not_equal的函数。从名字我们猜测,这个函数是用来比较两个字符串是否不相等的。如果我们用gdb查看这个函数的代码,可以发现,这个函数的功能就是比较两个字符串是否相等,如果相等,返回0,否则返回1,这里不对这个函数进行分析了。
string_not_equal函数使用了两个寄存器来获取两个字符串的首地址,分别是%rdi和%rsi。%rsi即为0x402400。因此我们需要查看%rdi的值,即输入的字符串的首地址。
在gdb中输入
x/s 0x402400
可以得到0x402400处的字符串为
0x402400: "Border relations with Canada have never been better."
输入这个字符串,即可通过第一关。
Phase 2
在gdb中输入
disas phase_2
可以得到phase_2的反汇编代码。
下面我们逐步分析phase_2的代码。
phase_2:
0x0000000000400efc <+0>: push %rbp
0x0000000000400efd <+1>: push %rbx
0x0000000000400efe <+2>: sub $0x28,%rsp #分配栈空间,开辟了一个数组int a[6],数组的首地址为%rsp
0x0000000000400f02 <+6>: mov %rsp,%rsi #暂存数组首地址,作为read_six_numbers的参数,用于存放输入的六个数字
0x0000000000400f05 <+9>: call 0x40145c <read_six_numbers>
read_six_numbers函数,根据函数名可猜测是从输入字符串中读取六个数字,然后存放到%rsi指向的数组中。
read_six_numbers函数的反汇编代码如下:
read_six_numbers:
0x000000000040145c <+0>: sub $0x18,%rsp #开辟栈空间
0x0000000000401460 <+4>: mov %rsi,%rdx
0x0000000000401463 <+7>: lea 0x4(%rsi),%rcx
0x0000000000401467 <+11>: lea 0x14(%rsi),%rax
0x000000000040146b <+15>: mov %rax,0x8(%rsp)
0x0000000000401470 <+20>: lea 0x10(%rsi),%rax
0x0000000000401474 <+24>: mov %rax,(%rsp)
0x0000000000401478 <+28>: lea 0xc(%rsi),%r9
0x000000000040147c <+32>: lea 0x8(%rsi),%r8
# 至此:%rdx->%rsi %rcx->%rsi+4 %r8->%rsi+8 %r9->%rsi+12
# (%rsp)->%rsi+16 (%rsp+8)->%rsi+20
# 如果认为%rsi代表了数组a,则可以认为
# %rdx->&a[0] %rcx->&a[1] %r8->&a[2] %r9->&a[3]
# (%rsp)->&a[4] (%rsp+8)->&a[5] (寄存器不够用了,只能用栈了)
# a[0]的类型为int,数组a为phase_2中的局部变量
0x0000000000401480 <+36>: mov $0x4025c3,%esi #"%d %d %d %d %d %d"
0x0000000000401485 <+41>: mov $0x0,%eax #eax=0
0x000000000040148a <+46>: call 0x400bf0 <__isoc99_sscanf@plt> #sscanf($输入字符串$,"%d %d %d %d %d %d",&a[0],&a[1],&a[2],&a[3],&a[4],&a[5])
0x000000000040148f <+51>: cmp $0x5,%eax #检查输入的数字是否为大于等于6个
0x0000000000401492 <+54>: jg 0x401499 <read_six_numbers+61>
0x0000000000401494 <+56>: call 0x40143a <explode_bomb>
0x0000000000401499 <+61>: add $0x18,%rsp
0x000000000040149d <+65>: ret
%esi中存放模式字符串的地址0x4025c3,通过反汇编查看该处的字符串可以得知,该字符串为”%d %d %d %d %d %d”,即输入的字符串应该是由六个数字组成的,这帮助我们更好的理解了这个函数的行为。
该函数首先开辟了一个24字节栈空间,然后将%rsi(存放着phase_2开辟的数组a的首地址)的值赋给%rdx,将%rsi+4的值赋给%rcx,将%rsi+8的值赋给%r8,将%rsi+12的值赋给%r9,将%rsi+16的值赋给(%rsp),将%rsi+20的值赋给(%rsp+8),也就是说该函数创造了六个参数作为sscanf的输出。
所以在read_six_numbers函数执行完毕后,数组a中的值为输入字符串中的六个数字。
我们需要让这六个数字符合phase_2的要求,这需要继续分析phase_2的代码。
#现在%rsp到%rsp+24的空间中存放了六个数字,分别为a[0]~a[5],类型为int
0x0000000000400f0a <+14>: cmpl $0x1,(%rsp) #检查a[0]是否为1
0x0000000000400f0e <+18>: je 0x400f30 <phase_2+52>
0x0000000000400f10 <+20>: call 0x40143a <explode_bomb>#如果a[0]不为1,则炸弹爆炸
0x0000000000400f15 <+25>: jmp 0x400f30 <phase_2+52>
0x0000000000400f17 <+27>: mov -0x4(%rbx),%eax #eax=a[i-1]
0x0000000000400f1a <+30>: add %eax,%eax #eax=2*a[i-1]
0x0000000000400f1c <+32>: cmp %eax,(%rbx) #检查a[i]是否等于2*a[i-1]
0x0000000000400f1e <+34>: je 0x400f25 <phase_2+41>
0x0000000000400f20 <+36>: call 0x40143a <explode_bomb>
0x0000000000400f25 <+41>: add $0x4,%rbx #rbx指向a[i+1]
0x0000000000400f29 <+45>: cmp %rbp,%rbx #检查rbx是否指向%rsp+24
0x0000000000400f2c <+48>: jne 0x400f17 <phase_2+27>
0x0000000000400f2e <+50>: jmp 0x400f3c <phase_2+64>
0x0000000000400f30 <+52>: lea 0x4(%rsp),%rbx #rbx指向a[1]
0x0000000000400f35 <+57>: lea 0x18(%rsp),%rbp #rbp指向%rsp+24,这个地址是数组的终点,可以用来作为循环结束的标志
0x0000000000400f3a <+62>: jmp 0x400f17 <phase_2+27>
0x0000000000400f3c <+64>: add $0x28,%rsp
0x0000000000400f40 <+68>: pop %rbx
0x0000000000400f41 <+69>: pop %rbp
0x0000000000400f42 <+70>: ret
根据反汇编我们可以推测该部分对应的C语言代码为:
if(a[0] != 1)
explode_bomb();
for(int i = 1; i < 6; i++)
{
if(a[i] != 2 * a[i - 1] )
explode_bomb();
}
所以我们要输入的六个数就是“1 2 4 8 16 32”。
Phase 3
在gdb中输入
disas phase_3
可以得到phase_3的反汇编代码。
前半部分代码如下:
0x0000000000400f43 <+0>: sub $0x18,%rsp #开辟24个字节的空间
0x0000000000400f47 <+4>: lea 0xc(%rsp),%rcx #sscnf的第二个输出参数,设其为b
0x0000000000400f4c <+9>: lea 0x8(%rsp),%rdx #sscnf的第一个输出参数,设其为a
0x0000000000400f51 <+14>: mov $0x4025cf,%esi
0x0000000000400f56 <+19>: mov $0x0,%eax
0x0000000000400f5b <+24>: call 0x400bf0 <__isoc99_sscanf@plt> #sscanf($输入字符串$, "%d %d", &a, &b)
0x0000000000400f60 <+29>: cmp $0x1,%eax
0x0000000000400f63 <+32>: jg 0x400f6a <phase_3+39>
0x0000000000400f65 <+34>: call 0x40143a <explode_bomb>
0x0000000000400f6a <+39>: cmpl $0x7,0x8(%rsp) #比较a和7的大小
0x0000000000400f6f <+44>: ja 0x400fad <phase_3+106> #如果a>7,就爆炸
0x0000000000400f71 <+46>: mov 0x8(%rsp),%eax #将a的值赋给eax
0x0000000000400f75 <+50>: jmp *0x402470(,%rax,8) #跳转到*(0x402470+8*a)的地址
可以发现该题的核心在于跳转表,跳转表的地址为0x402470,跳转表的内容如下:
0x402470: 0x0000000000400f7c <phase_3 + 57>
0x402478: 0x0000000000400fb9 <phase_3 +118>
0x402480: 0x0000000000400f83 <phase_3 + 64>
0x402480: 0x0000000000400f8a <phase_3 + 71>
0x402490: 0x0000000000400f91 <phase_3 + 78>
0x402498: 0x0000000000400f98 <phase_3 + 85>
0x4024a0: 0x0000000000400f9f <phase_3 + 92>
0x4024a8: 0x0000000000400fa6 <phase_3 + 99>
当输入的第一个数字为0~7时,就会跳转到对应的地址,然后执行对应的代码。
下一半反汇编代码如下:
0x0000000000400f7c <+57>: mov $0xcf,%eax
0x0000000000400f81 <+62>: jmp 0x400fbe <phase_3+123>
0x0000000000400f83 <+64>: mov $0x2c3,%eax
0x0000000000400f88 <+69>: jmp 0x400fbe <phase_3+123>
0x0000000000400f8a <+71>: mov $0x100,%eax
0x0000000000400f8f <+76>: jmp 0x400fbe <phase_3+123>
0x0000000000400f91 <+78>: mov $0x185,%eax
0x0000000000400f96 <+83>: jmp 0x400fbe <phase_3+123>
0x0000000000400f98 <+85>: mov $0xce,%eax
0x0000000000400f9d <+90>: jmp 0x400fbe <phase_3+123>
0x0000000000400f9f <+92>: mov $0x2aa,%eax
0x0000000000400fa4 <+97>: jmp 0x400fbe <phase_3+123>
0x0000000000400fa6 <+99>: mov $0x147,%eax
0x0000000000400fab <+104>: jmp 0x400fbe <phase_3+123>
0x0000000000400fad <+106>: call 0x40143a <explode_bomb>
0x0000000000400fb2 <+111>: mov $0x0,%eax
0x0000000000400fb7 <+116>: jmp 0x400fbe <phase_3+123>
0x0000000000400fb9 <+118>: mov $0x137,%eax
0x0000000000400fbe <+123>: cmp 0xc(%rsp),%eax
0x0000000000400fc2 <+127>: je 0x400fc9 <phase_3+134>
0x0000000000400fc4 <+129>: call 0x40143a <explode_bomb>
0x0000000000400fc9 <+134>: add $0x18,%rsp
0x0000000000400fcd <+138>: ret
核心在于0x0000000000400fbe <+123>处的cmp指令,该指令比较0xc(%rsp)和%eax的值。即第二个输入的数字和%eax进行比较。如果相等,则通过,否则爆炸。
%eax根据跳转表的选择不同,对应的结果也不同,所以本题的答案有多个。
分别为
- “0 207”
- “1 311”
- “2 707”
- “3 256”
- “4 389”
- “5 206”
- “6 682”
- “7 327”
Phase 4
在gdb中输入
disas phase_4
可以得到phase_4的反汇编代码。
phase_4:
0x000000000040100c <+0>: sub $0x18,%rsp
0x0000000000401010 <+4>: lea 0xc(%rsp),%rcx #sscanf的第二个输出参数,设其为b
0x0000000000401015 <+9>: lea 0x8(%rsp),%rdx #sscanf的第一个输出参数,设其为a
0x000000000040101a <+14>: mov $0x4025cf,%esi
0x000000000040101f <+19>: mov $0x0,%eax
0x0000000000401024 <+24>: call 0x400bf0 <__isoc99_sscanf@plt> #sscanf($输入字符串$, "%d %d", &a, &b)
0x0000000000401029 <+29>: cmp $0x2,%eax
0x000000000040102c <+32>: jne 0x401035 <phase_4+41> #如果sscanf的返回值不为2,则爆炸
0x000000000040102e <+34>: cmpl $0xe,0x8(%rsp) #如果a不小于等于14,则爆炸
0x0000000000401033 <+39>: jbe 0x40103a <phase_4+46> #jump if below or equal
0x0000000000401035 <+41>: call 0x40143a <explode_bomb>
0x000000000040103a <+46>: mov $0xe,%edx # %edx的初始值为14
0x000000000040103f <+51>: mov $0x0,%esi # %esi的初始值为0
0x0000000000401044 <+56>: mov 0x8(%rsp),%edi # a的值存放于%rdi
0x0000000000401048 <+60>: call 0x400fce <func4>
0x000000000040104d <+65>: test %eax,%eax # 如果func4的返回值不为0,则爆炸,也就是说我们希望func4的返回值为0
0x000000000040104f <+67>: jne 0x401058 <phase_4+76>
0x0000000000401051 <+69>: cmpl $0x0,0xc(%rsp)
0x0000000000401056 <+74>: je 0x40105d <phase_4+81>
0x0000000000401058 <+76>: call 0x40143a <explode_bomb>
0x000000000040105d <+81>: add $0x18,%rsp
0x0000000000401061 <+85>: ret
查询0x4025cf可以看到内容为”%d %d”,所以我们要输入的字符串应该包含两个数字。
再往下可以看到phase4的核心在于调用的func4函数。
func4的反汇编如下;
#递归函数,输入为%edi,%esi,%edx,输出为%eax
#初始化时%esi为0,%edx为14,%edi为a
func4:
0x0000000000400fce <+0>: sub $0x8,%rsp #没有压栈的操作。
0x0000000000400fd2 <+4>: mov %edx,%eax
0x0000000000400fd4 <+6>: sub %esi,%eax # %eax = %edx - %esi
0x0000000000400fd6 <+8>: mov %eax,%ecx # %ecx = %eax
0x0000000000400fd8 <+10>: shr $0x1f,%ecx # %ecx = (%edx - %esi) >> 31, 这种操作一般用于获得符号位
0x0000000000400fdb <+13>: add %ecx,%eax # %eax = %eax + %ecx(因为上一步是逻辑右移,%ecx为0或1)
0x0000000000400fdd <+15>: sar %eax #当sar的操作数为1时,默认为算数右移一位,即除以2
0x0000000000400fdf <+17>: lea (%rax,%rsi,1),%ecx # %ecx = %rax + %rsi = %eax + %esi
# 如果设%esi为x,%edx为y,则以上操作可总结为
# %eax = ((y - x) + (y - x < 0)) / 2
# %ecx = (y + (y - x < 0)) / 2
0x0000000000400fe2 <+20>: cmp %edi,%ecx
0x0000000000400fe4 <+22>: jle 0x400ff2 <func4+36> #如果%ecx小于等于a,则跳转到0x400ff2
0x0000000000400fe6 <+24>: lea -0x1(%rcx),%edx # %edx = %ecx - 1
0x0000000000400fe9 <+27>: call 0x400fce <func4> #递归调用
0x0000000000400fee <+32>: add %eax,%eax # %eax = 2 * %eax
0x0000000000400ff0 <+34>: jmp 0x401007 <func4+57> #跳转到0x401007,返回值为2 * %eax
0x0000000000400ff2 <+36>: mov $0x0,%eax
0x0000000000400ff7 <+41>: cmp %edi,%ecx #可以发现只要%ecx等于a,就会返回0
0x0000000000400ff9 <+43>: jge 0x401007 <func4+57> # 如果%ecx大于等于a,则跳转到0x401007,返回值为0
0x0000000000400ffb <+45>: lea 0x1(%rcx),%esi # %esi = %ecx + 1
0x0000000000400ffe <+48>: call 0x400fce <func4> #递归调用
0x0000000000401003 <+53>: lea 0x1(%rax,%rax,1),%eax # %eax = 2 * %eax + 1
0x0000000000401007 <+57>: add $0x8,%rsp
0x000000000040100b <+61>: ret
观察到func4中存在call func4的递归调用,因此可以确定该函数为一个递归函数。
对于递归函数,最好的方式还是去将程序翻译成C语言,具体的解析可以看代码中的注释,一条条的进行解析。
一个猜测的C语言程序如下:
int func4(int x, int y, int a){
int ecx = (y + (y - x < 0)) / 2;
if (ecx > a){
return 2 * func4(x, ecx - 1, a);
}
int ret = 0;
if (ecx < a){
ret = func4(ecx + 1, y, a);
return 2 * ret + 1;
}
return 0;
}
经过测试,有以下答案能让该函数返回0的值有
- 0
- 1
- 3
- 7
这几个数都可以作为输入字符串中的第一个数字。
然后我们再去查看phase4 <+69>处检查第二个输入数字与0的关系,如果等于零则函数结束。
所以最终答案为以上4个数字之一与0组成的字符串
- 0 0
- 1 0
- 3 0
- 7 0
这题总感觉出的很奇怪,不知道想要考察什么,因为如果去阅读汇编,会发现func4在输入为7的时候在第一层就会返回0,根本连递归都不涉及。然后第二个数字的设计也太显然了。。。
Phase 5
该问的主题是活字印刷(bushi
在gdb中输入
disas phase_5
可以得到phase_5的反汇编代码。
部分如下
phase_5:
0x0000000000401062 <+0>: push %rbx
0x0000000000401063 <+1>: sub $0x20,%rsp
0x0000000000401067 <+5>: mov %rdi,%rbx #%rbx中暂存输入字符串的首地址
0x000000000040106a <+8>: mov %fs:0x28,%rax
0x0000000000401073 <+17>: mov %rax,0x18(%rsp) #%rsp+24处放置金丝雀值
0x0000000000401078 <+22>: xor %eax,%eax
0x000000000040107a <+24>: call 0x40131b <string_length>
0x000000000040107f <+29>: cmp $0x6,%eax #输入要求长度为6
0x0000000000401082 <+32>: je 0x4010d2 <phase_5+112>
0x0000000000401084 <+34>: call 0x40143a <explode_bomb>
0x0000000000401089 <+39>: jmp 0x4010d2 <phase_5+112>
0x000000000040108b <+41>: movzbl (%rbx,%rax,1),%ecx #设%rax中参数为i,%rbx中参数为输入字符串str,则%ecx = str[i]
0x000000000040108f <+45>: mov %cl,(%rsp) #%cl是%ecx的低字节,此处解析为char
0x0000000000401092 <+48>: mov (%rsp),%rdx #将str[i]放入栈顶(没有移动栈指针)与%rdx
0x0000000000401096 <+52>: and $0xf,%edx #%edx保留str[i]的低四位,如果按数字解析则为一个0~15的数
0x0000000000401099 <+55>: movzbl 0x4024b0(%rdx),%edx #获取0x4024b0+%edx处的字符,放置于%edx中
0x00000000004010a0 <+62>: mov %dl,0x10(%rsp,%rax,1) #将%edx中的字符存放于targetStr[i]中,targetStr即%rsp+16为开头的字符串
0x00000000004010a4 <+66>: add $0x1,%rax
0x00000000004010a8 <+70>: cmp $0x6,%rax #执行6次
0x00000000004010ac <+74>: jne 0x40108b <phase_5+41>
汇编显示该函数使用了金丝雀值进行栈保护,但在此问中与之无关。
string_lenth函数的调用告诉我们要求的输入字符串的长度应该为6。
随后开启一个次数为6的循环
循环中最有意思的地方在这里
0x0000000000401096 <+52>: and $0xf,%edx #%edx保留str[i]的低四位,如果按数字解析则为一个0~15的数
0x0000000000401099 <+55>: movzbl 0x4024b0(%rdx),%edx #获取0x4024b0+%edx处的字符,放置于%edx中
0x00000000004010a0 <+62>: mov %dl,0x10(%rsp,%rax,1) #将%edx中的字符存放于targetStr[i]中,targetStr即%rsp+16为开头的字符串
在第i次循环中(i从0开始),设输入字符串为str,则在这段代码开始前已将str[i]的值加载到%edx中。然后<+52>的and操作将str[i]的值按低位截断成一个四位的数字,即表示0~15的值。
随后从0x4024b0偏移这个值的位置获取一个字符放在一个输出字符串中。
使用
x/s 0x4024b0
可以看到此处存放字符串为:
0x4024b0 <array.3449>: "maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"
因为我们只关心0~15,所以为maduiersnfotvbyl。
这样我们会根据输入字符串的不同,获取到大概率不相同的长度为6的字符串,放置于输出字符串中,其首地址为%rsp + 16。
我们继续阅读phase5的反汇编:
0x00000000004010ae <+76>: movb $0x0,0x16(%rsp)
0x00000000004010b3 <+81>: mov $0x40245e,%esi
0x00000000004010b8 <+86>: lea 0x10(%rsp),%rdi
0x00000000004010bd <+91>: call 0x401338 <strings_not_equal>
0x00000000004010c2 <+96>: test %eax,%eax
0x00000000004010c4 <+98>: je 0x4010d9 <phase_5+119>
0x00000000004010c6 <+100>: call 0x40143a <explode_bomb>
0x00000000004010cb <+105>: nopl 0x0(%rax,%rax,1)
0x00000000004010d0 <+110>: jmp 0x4010d9 <phase_5+119>
0x00000000004010d2 <+112>: mov $0x0,%eax
0x00000000004010d7 <+117>: jmp 0x40108b <phase_5+41>
0x00000000004010d9 <+119>: mov 0x18(%rsp),%rax
0x00000000004010de <+124>: xor %fs:0x28,%rax
0x00000000004010e7 <+133>: je 0x4010ee <phase_5+140>
0x00000000004010e9 <+135>: call 0x400b30 <__stack_chk_fail@plt>
0x00000000004010ee <+140>: add $0x20,%rsp
0x00000000004010f2 <+144>: pop %rbx
0x00000000004010f3 <+145>: ret
看到strings_not_equal大概就能猜到了,原来是在要求我们通过活字印刷的方式获取和0x40245e存放的字符串一致的字符串。0x40245e存放字符串为”flyers”,只需查看ascii编码表,然后寻找可打印字符中后四位表示的十进制值能在”maduiersnfotvbyl”中对应’f’,’l’,’y’,’e’,’r’,’s’的即可。
可能的结果有很多,一个答案为9?>EFG
Phase 6
本题相当的复杂,需要慢慢地对反汇编进行分析
在gdb中输入
disas phase_6
可以得到phase_6的反汇编代码。
部分如下:
0x00000000004010f4 <+0>: push %r14
0x00000000004010f6 <+2>: push %r13
0x00000000004010f8 <+4>: push %r12
0x00000000004010fa <+6>: push %rbp
0x00000000004010fb <+7>: push %rbx
0x00000000004010fc <+8>: sub $0x50,%rsp #开辟80字节的栈空间,看来不怎么简单
0x0000000000401100 <+12>: mov %rsp,%r13 #可以认为%rsp开头存在一个int a[6]数组,这里暂存这个数组
0x0000000000401103 <+15>: mov %rsp,%rsi #将数组a的首地址放入%rsi,作为read_six_numbers的输出
0x0000000000401106 <+18>: call 0x40145c <read_six_numbers>
0x000000000040110b <+23>: mov %rsp,%r14 #暂存%rsp,现在%rsp为首地址的数组存放着我们输入的六个数字
0x000000000040110e <+26>: mov $0x0,%r12d #%r12d置0,设其保存变量i
0x0000000000401114 <+32>: mov %r13,%rbp #根据<+89>的指令,可以认为%r13指向的元素在变化,设其指向a[x],初始x为0
0x0000000000401117 <+35>: mov 0x0(%r13),%eax
0x000000000040111b <+39>: sub $0x1,%eax
0x000000000040111e <+42>: cmp $0x5,%eax
0x0000000000401121 <+45>: jbe 0x401128 <phase_6+52> #jump if below or equal
0x0000000000401123 <+47>: call 0x40143a <explode_bomb> # a[x]大于6则爆炸,同时小于等于0也会
0x0000000000401128 <+52>: add $0x1,%r12d # i += 1
0x000000000040112c <+56>: cmp $0x6,%r12d
0x0000000000401130 <+60>: je 0x401153 <phase_6+95> #如果i等于6则退出循环
0x0000000000401132 <+62>: mov %r12d,%ebx #设%ebx保存变量 j
0x0000000000401135 <+65>: movslq %ebx,%rax
0x0000000000401138 <+68>: mov (%rsp,%rax,4),%eax #4 * j + %rsp,即%eax = a[j]
0x000000000040113b <+71>: cmp %eax,0x0(%rbp) #比较a[j]与a[x]
0x000000000040113e <+74>: jne 0x401145 <phase_6+81>
0x0000000000401140 <+76>: call 0x40143a <explode_bomb> #如果a[j] == a[x]则爆炸
0x0000000000401145 <+81>: add $0x1,%ebx
0x0000000000401148 <+84>: cmp $0x5,%ebx
0x000000000040114b <+87>: jle 0x401135 <phase_6+65>
0x000000000040114d <+89>: add $0x4,%r13 #%r13,指向数组a的指针移动一个单位,从指向a[x]变为指向a[x + 1]
0x0000000000401151 <+93>: jmp 0x401114 <phase_6+32>
#只要满足输入序列每个数都小于等于6,并且两两不同就触发爆炸
该节代码很长,我们通过分析跳转指令可以跳转到的最后位置可以确定这样一个逻辑块。
现在我们需要解析这个逻辑块在做什么。
注意看加了缩进的块,该块表示一个循环语句,同时在该块外部,还有一个循环。所以我们能够确定这是一个双重循环的结构。
以上代码读取6个数字并进行存储,然后检查输入的六个数字是否满足以下条件:
- 小于等于6
- 两两不同
接着我们分析下一个反汇编代码块:
#初始化
0x0000000000401153 <+95>: lea 0x18(%rsp),%rsi #将%rsp + 24作为某个数组的首地址,不妨设为b
0x0000000000401158 <+100>: mov %r14,%rax #可以认为%rax指向为a[i],初始化i为0
0x000000000040115b <+103>: mov $0x7,%ecx #%ecx 置为7
0x0000000000401160 <+108>: mov %ecx,%edx
0x0000000000401162 <+110>: sub (%rax),%edx # %edx = 7 - a[i](因为a[i]小于等于0,则一定为正)
0x0000000000401164 <+112>: mov %edx,(%rax) # a[i] = 7 - a[i]
0x0000000000401166 <+114>: add $0x4,%rax # i += 1
0x000000000040116a <+118>: cmp %rsi,%rax #将%rsi(此时为%rsp + 24)作为遍历数组a的结束
0x000000000040116d <+121>: jne 0x401160 <phase_6+108>
#以上循环将导致a[i]变为7 - a[i]
正如注释所写的那样,这段代码将输入的六个数字改变为7 - a[i]
然后是下一段:
0x000000000040116f <+123>: mov $0x0,%esi #初始化%esi为0,不妨假设%rsi处存放变量为n
0x0000000000401174 <+128>: jmp 0x401197 <phase_6+163>
0x0000000000401176 <+130>: mov 0x8(%rdx),%rdx #8正好是一个链表节点的next指针的偏移量,读取到next指针后赋给%rdx,类似于操作curr = curr->next
0x000000000040117a <+134>: add $0x1,%eax
0x000000000040117d <+137>: cmp %ecx,%eax
0x000000000040117f <+139>: jne 0x401176 <phase_6+130> #算上开始的第一次,最后会在node(a[n])停下
0x0000000000401181 <+141>: jmp 0x401188 <phase_6+148> #当执行到这里时,%rdx将会是node(a[n])的值
0x0000000000401183 <+143>: mov $0x6032d0,%edx #将0x6032d0的值放入%edx,这是一堆节点的首地址
0x0000000000401188 <+148>: mov %rdx,0x20(%rsp,%rsi,2) #设%rsp + 32是集合b的开始地址,将node(a[n])放置于b[2n]
0x000000000040118d <+153>: add $0x4,%rsi #%rsi只在此处增加,也就是说想要退出循环,要让这个指令运行6次。
0x0000000000401191 <+157>: cmp $0x18,%rsi
0x0000000000401195 <+161>: je 0x4011ab <phase_6+183>
0x0000000000401197 <+163>: mov (%rsp,%rsi,1),%ecx #%ecx = a[n]
0x000000000040119a <+166>: cmp $0x1,%ecx
0x000000000040119d <+169>: jle 0x401183 <phase_6+143> #如果a[n] 小于等于1,则跳回<+143>
0x000000000040119f <+171>: mov $0x1,%eax
0x00000000004011a4 <+176>: mov $0x6032d0,%edx
0x00000000004011a9 <+181>: jmp 0x401176 <phase_6+130>
#至此,数组b中应该有6个有效的链表节点的地址
这段是该题的核心代码之一。
我们注意到存在一个固定的地址,查看该地址以及之后的数据有:
#地址 值(4字节,转为10进制) 序号(4字节) next(8字节)
0x6032d0 <node1>: 332 0x00000001 0x00000000006032e0
0x6032e0 <node2>: 168 0x00000002 0x00000000006032f0
0x6032f0 <node3>: 924 0x00000003 0x0000000000603300
0x603300 <node4>: 691 0x00000004 0x0000000000603310
0x603310 <node5>: 477 0x00000005 0x0000000000603320
0x603320 <node6>: 443 0x00000006 0x0000000000000000
最需要注意的就是node的命名,我们清楚在计算机科学中node一般用于表示节点,进一步研究我们能发现这些都是链表的节点。
节点中,前四个字节存放数值,再有四个字节存放序号,然后八个字节存放节点下一个元素的地址。
不难发现这是一个按顺序相连的链表。
这段代码的核心在于:
0x0000000000401176 <+130>: mov 0x8(%rdx),%rdx
正如上面说的,next指针开始于首地址+8。在这条指令之前,%rdx保证保存的是某一个节点的地址。所以这条指令将取出该节点中的next指针,然后将其赋给%rdx。这就相当于我们常做的curr = curr->next,是一个遍历链表的过程。
所以这段代码按顺序取出六个数字被7减去后的值,然后拿出相应的node值,存放于内存空间中。比如说第一个输入的数字为2,则7 - 2 = 5。那么在新的数组中,第一个存放的节点为node5。
注意地址以八字节存储,所以在存放到新数组时是使用 0x20(%rsp,%rsi,2)来寻址,其中%rsi每次加4。
运行完上述的代码后,我们的新数组b中应该根据我们的输入,存放着不同节点的值。
接下来是非常关键的一步:
0x00000000004011ab <+183>: mov 0x20(%rsp),%rbx #将b的第一个元素放置于%rbx,这就是我们第一个获取到的node的地址
0x00000000004011b0 <+188>: lea 0x28(%rsp),%rax #一个快指针,它始终在%rcx的后一个位置
0x00000000004011b5 <+193>: lea 0x50(%rsp),%rsi #遍历数组b的终点
0x00000000004011ba <+198>: mov %rbx,%rcx #将第一个获取到的node的地址放置于%rcx,用%rcx进行操作,称其为curr
0x00000000004011bd <+201>: mov (%rax),%rdx #将快指针所指向内存中的节点地址暂存于%rdx
0x00000000004011c0 <+204>: mov %rdx,0x8(%rcx) #curr->next = %rdx(即将链表按b节点中存储的顺序重新连接)
0x00000000004011c4 <+208>: add $0x8,%rax
0x00000000004011c8 <+212>: cmp %rsi,%rax
0x00000000004011cb <+215>: je 0x4011d2 <phase_6+222>
0x00000000004011cd <+217>: mov %rdx,%rcx #curr = curr->next
0x00000000004011d0 <+220>: jmp 0x4011bd <phase_6+201>
0x00000000004011d2 <+222>: movq $0x0,0x8(%rdx) #curr->next->next = null
在这段代码前,在数组b中存储着由我们决定的顺序存储的链表节点的地址。这段代码所做的就是将链表按照我们所存储的顺序进行重新连接。
比如输入为:
3 5 6 2 4 1
则变化后为:
4 2 1 5 3 6
数组b中所存储的链表节点为:
node4 node2 node1 node5 node3 node6
经过这段代码后,实际链表将变为和该顺序相同:
node4->node2->node1->node5->node3->node6
接下来这段代码总算让我们明白最后的目的是什么了
0x00000000004011da <+230>: mov $0x5,%ebp #循环5次的意思
0x00000000004011df <+235>: mov 0x8(%rbx),%rax
0x00000000004011e3 <+239>: mov (%rax),%eax
0x00000000004011e5 <+241>: cmp %eax,(%rbx)
0x00000000004011e7 <+243>: jge 0x4011ee <phase_6+250> #这里我们总算明白最后的目标是什么了,我们要构建一个降序链表
0x00000000004011e9 <+245>: call 0x40143a <explode_bomb>
0x00000000004011ee <+250>: mov 0x8(%rbx),%rbx
0x00000000004011f2 <+254>: sub $0x1,%ebp
0x00000000004011f5 <+257>: jne 0x4011df <phase_6+235>
原来是要求我们构建一个降序链表
按降序排列:
#地址 值(四字节,转为10进制) 序号(4字节) next(8字节)
0x6032f0 <node3>: 924 0x00000003 0x0000000000603300
0x603300 <node4>: 691 0x00000004 0x0000000000603310
0x603310 <node5>: 477 0x00000005 0x0000000000603320
0x603320 <node6>: 443 0x00000006 0x0000000000000000
0x6032d0 <node1>: 332 0x00000001 0x00000000006032e0
0x6032e0 <node2>: 168 0x00000002 0x00000000006032f0
所以我们希望变化后的序列为:
3 4 5 6 1 2
用7减去即可得到
4 3 2 1 6 5
即答案
Secret Phase
在解决phase5时,如果在查看0x4024b0处的字符串时让gdb多输出几个字符,会发现在0x4024b0处的字符串后面还有一些让人在意的字符串,这些字符串是:
0x4024f8: "Curses, you've found the secret phase!"
0x40251f: ""
0x402520: "But finding it and solving it are quite different..."
显然这是一个隐藏的phase,我们需要找到它的入口地址。
但问题在于main函数中并没有调用这个隐藏的phase。所以我们猜测应该是在某个函数中调用了这个隐藏的phase。
查看read_line时并没有发现特殊的函数调用。
然后我们查看phase_defused。
phase_defused:
0x00000000004015c4 <+0>: sub $0x78,%rsp
0x00000000004015c8 <+4>: mov %fs:0x28,%rax
0x00000000004015d1 <+13>: mov %rax,0x68(%rsp)
0x00000000004015d6 <+18>: xor %eax,%eax
0x00000000004015d8 <+20>: cmpl $0x6,0x202181(%rip) # 0x603760 <num_input_strings>
0x00000000004015df <+27>: jne 0x40163f <phase_defused+123>
0x00000000004015e1 <+29>: lea 0x10(%rsp),%r8 #sscnaf的第三个输出参数
0x00000000004015e6 <+34>: lea 0xc(%rsp),%rcx #第二个
0x00000000004015eb <+39>: lea 0x8(%rsp),%rdx #第一个
0x00000000004015f0 <+44>: mov $0x402619,%esi
0x00000000004015f5 <+49>: mov $0x603870,%edi #从名字判断这里应该是存放了输入字符串,在运行中查看此处内存,确定为phase4的输入字符串
0x00000000004015fa <+54>: call 0x400bf0 <__isoc99_sscanf@plt>
0x00000000004015ff <+59>: cmp $0x3,%eax
0x0000000000401602 <+62>: jne 0x401635 <phase_defused+113>
0x0000000000401604 <+64>: mov $0x402622,%esi
0x0000000000401609 <+69>: lea 0x10(%rsp),%rdi
0x000000000040160e <+74>: call 0x401338 <strings_not_equal>
0x0000000000401613 <+79>: test %eax,%eax
0x0000000000401615 <+81>: jne 0x401635 <phase_defused+113> #要避免跳转,则结果应该是0,也就是字符串相等
0x0000000000401617 <+83>: mov $0x4024f8,%edi
0x000000000040161c <+88>: call 0x400b10 <puts@plt>
0x0000000000401621 <+93>: mov $0x402520,%edi
0x0000000000401626 <+98>: call 0x400b10 <puts@plt>
0x000000000040162b <+103>: mov $0x0,%eax
0x0000000000401630 <+108>: call 0x401242 <secret_phase> #想要达到秘密函数,要避开所有的跳转,使程序运行至此
0x0000000000401635 <+113>: mov $0x402558,%edi
0x000000000040163a <+118>: call 0x400b10 <puts@plt>
0x000000000040163f <+123>: mov 0x68(%rsp),%rax
0x0000000000401644 <+128>: xor %fs:0x28,%rax
0x000000000040164d <+137>: je 0x401654 <phase_defused+144>
0x000000000040164f <+139>: call 0x400b30 <__stack_chk_fail@plt>
0x0000000000401654 <+144>: add $0x78,%rsp
0x0000000000401658 <+148>: ret
不难发现
0x0000000000401630 <+108>: call 0x401242 <secret_phase>
这就是调用secret_phase的地方。
我们需要研究在什么条件下才会调用secret_phase。
通过分析反汇编,phase_defused函数先按照0x402619处保存的格式解析0x603870处的字符串,将从0x603870开始的字符串解析为两个整数和一个字符串。然后比较字符串是否和0x402622处的字符串相等,如果相等则调用secret_phase。
如果我们用gdb监视0x603870处的字符串,会发现在phase4中,我们输入的字符串会被存放在这里。
而0x402622处存放的字符串为:
0x402622: "DrEvil"
所以我们将phase4的输入字符串后面再加上一个”DrEvil”就会开启secret_phase。
secret_phase的反汇编如下:
secret_phase:
0x0000000000401242 <+0>: push %rbx
0x0000000000401243 <+1>: call 0x40149e <read_line> #%rax中存放着返回值
0x0000000000401248 <+6>: mov $0xa,%edx
0x000000000040124d <+11>: mov $0x0,%esi
0x0000000000401252 <+16>: mov %rax,%rdi
0x0000000000401255 <+19>: call 0x400bd0 <strtol@plt> #该函数将输入字符串转化为一个长整型数(十进制)
0x000000000040125a <+24>: mov %rax,%rbx #%rbx中保存输入的长整型数n
0x000000000040125d <+27>: lea -0x1(%rax),%eax
0x0000000000401260 <+30>: cmp $0x3e8,%eax #将n - 1与1000比较
0x0000000000401265 <+35>: jbe 0x40126c <secret_phase+42> #如果n - 1 <= 1000。即n <= 1001,则跳转,避免爆炸
0x0000000000401267 <+37>: call 0x40143a <explode_bomb> #如果n > 1001,则爆炸
0x000000000040126c <+42>: mov %ebx,%esi #将n放置于%esi,作为func7的参数
0x000000000040126e <+44>: mov $0x6030f0,%edi
0x0000000000401273 <+49>: call 0x401204 <fun7>
0x0000000000401278 <+54>: cmp $0x2,%eax #最后要求返回2
0x000000000040127b <+57>: je 0x401282 <secret_phase+64>
0x000000000040127d <+59>: call 0x40143a <explode_bomb>
0x0000000000401282 <+64>: mov $0x402438,%edi
0x0000000000401287 <+69>: call 0x400b10 <puts@plt>
0x000000000040128c <+74>: call 0x4015c4 <phase_defused>
0x0000000000401291 <+79>: pop %rbx
0x0000000000401292 <+80>: ret
该函数将输入字符串解析为一个整数,要求输入的字符串不能超过1001。
然后与一个地址0x6030f0一同传入func7函数。
如果func7返回值为2,则认为结果正确。
func7的反汇编如下:
fun7:
0x0000000000401204 <+0>: sub $0x8,%rsp
0x0000000000401208 <+4>: test %rdi,%rdi #检查n是否为NULL,为NULL则为递归终点
0x000000000040120b <+7>: je 0x401238 <fun7+52>
#可以认为%rdi放置着curr节点
0x000000000040120d <+9>: mov (%rdi),%edx #%edx = curr->val
0x000000000040120f <+11>: cmp %esi,%edx
0x0000000000401211 <+13>: jle 0x401220 <fun7+28> #如果curr->val的值小于等于n,则
0x0000000000401213 <+15>: mov 0x8(%rdi),%rdi #当curr->val大于n,则递归地搜索左节点 curr = curr->left
0x0000000000401217 <+19>: call 0x401204 <fun7>
0x000000000040121c <+24>: add %eax,%eax
0x000000000040121e <+26>: jmp 0x40123d <fun7+57>
0x0000000000401220 <+28>: mov $0x0,%eax
0x0000000000401225 <+33>: cmp %esi,%edx
0x0000000000401227 <+35>: je 0x40123d <fun7+57> #如果curr->val 等于n,则结束递归
0x0000000000401229 <+37>: mov 0x10(%rdi),%rdi #如果curr->val小于n,则递归地搜索右节点 curr = curr->right
0x000000000040122d <+41>: call 0x401204 <fun7>
0x0000000000401232 <+46>: lea 0x1(%rax,%rax,1),%eax #%rax = 2 * %rax + 1
0x0000000000401236 <+50>: jmp 0x40123d <fun7+57>
0x0000000000401238 <+52>: mov $0xffffffff,%eax
0x000000000040123d <+57>: add $0x8,%rsp
0x0000000000401241 <+61>: ret
查看0x6030f0处以及之后的数据
0x6030f0 <n1>: 0x24
0x6030f8 <n1+8>: 0x603110 <n21>
0x603100 <n1+16>: 0x603130 <n22>
0x603110 <n21>: 0x8
0x603118 <n21+8>: 0x603190 <n31>
0x603120 <n21+16>: 0x603150 <n32>
0x603130 <n22>: 0x32
0x603138 <n22+8>: 0x603170 <n33>
0x603140 <n22+16>: 0x6031b0 <n34>
0x603150 <n32>: 0x16
0x603158 <n32+8>: 0x603270 <n43>
0x603160 <n32+16>: 0x603230 <n44>
0x603170 <n33>: 0x2d
0x603178 <n33+8>: 0x6031d0 <n45>
0x603180 <n33+16>: 0x603290 <n46>
0x603190 <n31>: 0x6
0x603198 <n31+8>: 0x6031f0 <n41>
0x6031a0 <n31+16>: 0x603250 <n42>
0x6031b0 <n34>: 0x6b
0x6031b8 <n34+8>: 0x603210 <n47>
0x6031c0 <n34+16>: 0x6032b0 <n48>
0x6031d0 <n45>: 0x28
0x6031d8 <n45+8>: 0x0
0x6031e0 <n45+16>: 0x0
0x6031f0 <n41>: 0x1
0x6031f8 <n41+8>: 0x0
0x603200 <n41+16>: 0x0
0x603210 <n47>: 0x63
0x603218 <n47+8>: 0x0
0x603220 <n47+16>: 0x0
0x603230 <n44>: 0x23
0x603238 <n44+8>: 0x0
0x603240 <n44+16>: 0x0
0x603250 <n42>: 0x7
0x603258 <n42+8>: 0x0
0x603260 <n42+16>: 0x0
0x603270 <n43>: 0x14
0x603278 <n43+8>: 0x0
0x603280 <n43+16>: 0x0
0x603290 <n46>: 0x2f
0x603298 <n46+8>: 0x0
0x6032a0 <n46+16>: 0x0
0x6032b0 <n48>: 0x3e9
0x6032b8 <n48+8>: 0x0
0x6032c0 <n48+16>: 0x0
很显然这是一个二叉树,结构如下
36
________|________
| |
8 50
____|____ ____|____
| | | |
6 22 45 107
__|__ __|__ __|__ __|__
| | | | | | | |
1 7 20 35 40 47 99 1001
然后按照func7的逻辑想办法找到返回值为2的输入即可。
显然要去解析一个递归函数是非常困难的,所以我们可以将这个反汇编代码翻译为C语言,然后直接通过运行这个函数来找到答案。
void func7(Node *node, int *eax, int n){
if (node == NULL){
*eax = 0xffffffff;
return;
}
if (node->val > n){
func7(node->left, eax, n);
*eax += *eax;
return;
}
*eax = 0;
if (node->val == n)
return;
if (node->val < n){
func7(node->right, eax, n);
*eax += *eax + 1;
return;
}
}
从0开始运行这个函数(注意限制递归层数),可以找到两个答案20和22。
注意secret_phase读取数据是直接从stdin中读取的,所以我们需要将答案输入到stdin中,而在文件中输入是不行的。