前言

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中,而在文件中输入是不行的。