Lab 2 Bomb Lab

本文介绍了一项逆向工程实验——二进制炸弹,通过使用gdb调试器逐步解析炸弹程序,揭示了六个阶段及隐藏阶段的破解方法。文章详细记录了每个阶段的调试过程、关键代码分析及解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Lab 2 逆向工程实验(二进制炸弹)

写在前言:这个实验的来源是CSAPP官网:CSAPP Labs ,如果感兴趣的话,可以点击这个链接🔗去下载,Boom Lab这个实验可以锻炼我们的逆向工程能力,还可以检验对CSAPP书上第三章:程序的机器级表示 的掌握程度,实验给出的是一个二进制文件bomb,包含6个基本阶段和一个隐藏阶段,需要使用 gdb 调试器进行调试分析,得出每一关的答案,本文对所有阶段的拆弹工作具有详细的解释,每一关都会有等价的C语言代码,后面会详细介绍,仅供参考。

Phase 1

首先,我们在终端输入指令:

$ gdb bomb

在 gdb 环境下调试bomb。

给phase 1添加断点,接着运行bomb程序:

(gdb) break phase_1
Breakpoint 1 at 0x400ee0
(gdb) run
Starting program: /home/strggle/CSAPP/bomb/bomb

程序运行phase 1过程中输出提示:

Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!

因为我们对phase 1一无所知,我们可以随便输入一些内容,再进行观察,这对于每个阶段都是适用的,假设输入的内容如下:

Hello CSAPP!

按下Enter键后到达断点位置,使用gdb的反汇编指令将phase 1的二进制机器代码翻译为汇编语言代码:

# Breakpoint 1, 0x0000000000400ee0 in phase_1()
(gdb) disas phase_1
Dump of assembler code for function phase_1:
=> 0x0000000000400ee0 <+0>:		sub    $0x8,%rsp
   0x0000000000400ee4 <+4>:		mov    $0x402400,%esi
   0x0000000000400ee9 <+9>:		callq  0x401338 <strings_not_equal>
   0x0000000000400eee <+14>:	test   %eax,%eax
   0x0000000000400ef0 <+16>:	je     0x400ef7 <phase_1+23>
   0x0000000000400ef2 <+18>:	callq  0x40143a <explode_bomb>
   0x0000000000400ef7 <+23>:	add    $0x8,%rsp
   0x0000000000400efb <+27>:	retq   
End of assembler dump.

从phase 1的反汇编结果可以看到,它调用了一个函数名为 strings_not_equal,推测这个函数的功能是检查两个字符串是否不相同,它的返回值位于寄存器 %eax,接下来的两条指令,testje 指令说明如果 函数 strings_not_equal 的返回值不为0,那么就会执行 explode_bomb 函数,所以,这道题的关键就在于调用函数strings_not_equal前设置参数的代码,可以看到,寄存器 %esi 的值为 0x402400,为第2个参数,第1个参数是我们输入的字符串内容,因此,我们只要查看位于地址 0x402400 的字符串内容即可:

(gdb) x/s 0x402400
0x402400:		"Border relations with Canada have never been better."

所以,得到phase 1的答案:”Border relations with Canada have never been better.“

重新运行bomb程序,输入上述答案:

(gdb) run
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Border relations with Canada have never been better.
Phase 1 defused. How about the next one?

答案正确。

phase 1的汇编语言代码对应下面的 C 语言代码:

char* secret_string = "Border relations with Canada have never been better."
void phase_1(char* input_string) {
    if (strings_not_equal(input_string, secret_string)) {
        explode_bomb();
        exit(-1);
    }
    printf("Phase 1 defused. How about the next one?\n");
}

Phase 2

为Phase 2添加断点,并运行程序,反汇编phase 2:

(gdb) break phase_2
Breakpoint 2 at 0x400efc
(gdb) run
......

Phase 1 defused. How about the next one?
Hello Phase2
Breakpoint 2, 0x0000000000400efc in phase_2 ()
(gdb) disas phase_2
Dump of assembler code for function phase_2:
=> 0x0000000000400efc <+0>:		push   %rbp
   0x0000000000400efd <+1>:		push   %rbx
   0x0000000000400efe <+2>:		sub    $0x28,%rsp
   0x0000000000400f02 <+6>:		mov    %rsp,%rsi
   0x0000000000400f05 <+9>:		callq  0x40145c <read_six_numbers>
   0x0000000000400f0a <+14>:	cmpl   $0x1,(%rsp)
   0x0000000000400f0e <+18>:	je     0x400f30 <phase_2+52>
   0x0000000000400f10 <+20>:	callq  0x40143a <explode_bomb>
   0x0000000000400f15 <+25>:	jmp    0x400f30 <phase_2+52>
   0x0000000000400f17 <+27>:	mov    -0x4(%rbx),%eax
   0x0000000000400f1a <+30>:	add    %eax,%eax
   0x0000000000400f1c <+32>:	cmp    %eax,(%rbx)
   0x0000000000400f1e <+34>:	je     0x400f25 <phase_2+41>
   0x0000000000400f20 <+36>:	callq  0x40143a <explode_bomb>
   0x0000000000400f25 <+41>:	add    $0x4,%rbx
   0x0000000000400f29 <+45>:	cmp    %rbp,%rbx
   0x0000000000400f2c <+48>:	jne    0x400f17 <phase_2+27>
   0x0000000000400f2e <+50>:	jmp    0x400f3c <phase_2+64>
   0x0000000000400f30 <+52>:	lea    0x4(%rsp),%rbx
   0x0000000000400f35 <+57>:	lea    0x18(%rsp),%rbp
   0x0000000000400f3a <+62>:	jmp    0x400f17 <phase_2+27>
   0x0000000000400f3c <+64>:	add    $0x28,%rsp
   0x0000000000400f40 <+68>:	pop    %rbx
   0x0000000000400f41 <+69>:	pop    %rbp
   0x0000000000400f42 <+70>:	retq   
End of assembler dump.

这一关的汇编代码相对第一关多了很多,比较好的一种做法是将这一段汇编代码根据它们的逻辑划分,比如 if-else语句,for循环,switch分支等等。

对于每一个函数的汇编代码,开头和结尾都具有相似的结构,开头无非就是申请栈空间,调用者保存的寄存器入栈,结尾无非就是回收栈空间,并且将原来入栈的寄存器值pop回原寄存器。

这一部分是函数read_six_number相关部分代码:

   0x0000000000400f02 <+6>:	mov    %rsp,%rsi
   0x0000000000400f05 <+9>:	callq  0x40145c <read_six_numbers>
   0x0000000000400f0a <+14>:	cmpl   $0x1,(%rsp)
   0x0000000000400f0e <+18>:	je     0x400f30 <phase_2+52>
   0x0000000000400f10 <+20>:	callq  0x40143a <explode_bomb>

可以知道在调用函数 read_six_number 前,位于地址 0x400f02 的指令设置了 read_six_number 的第二个参数 %rsi 为栈指针,根据函数的名字推测,这一关需要输入6个数字,6个数字存放的在位置 (%rsp),连续6个数字。

接下来的 cmplje 指令,如果输入的第一个数字不为1,那么炸弹将会引爆,因此得到一个比较重要的信息,输入的6个数字中,第1个数字一定为1。

接下来这一部分是一个循环结构:

   0x0000000000400f17 <+27>:	mov    -0x4(%rbx),%eax
   0x0000000000400f1a <+30>:	add    %eax,%eax
   0x0000000000400f1c <+32>:	cmp    %eax,(%rbx)
   0x0000000000400f1e <+34>:	je     0x400f25 <phase_2+41>
   0x0000000000400f20 <+36>:	callq  0x40143a <explode_bomb>
   0x0000000000400f25 <+41>:	add    $0x4,%rbx
   0x0000000000400f29 <+45>:	cmp    %rbp,%rbx
   0x0000000000400f2c <+48>:	jne    0x400f17 <phase_2+27>
   0x0000000000400f2e <+50>:	jmp    0x400f3c <phase_2+64>
   0x0000000000400f30 <+52>:	lea    0x4(%rsp),%rbx
   0x0000000000400f35 <+57>:	lea    0x18(%rsp),%rbp
   0x0000000000400f3a <+62>:	jmp    0x400f17 <phase_2+27>

在前一部分分析中,第1个数字输入一定为1,那么就会 jump 到 地址 0x400f30 的代码。

我们可以观察最后三行代码,它实际上是循环的初始化阶段,

lea    0x4(%rsp),%rbx			# Load address of $rsp+4 to $rbx
lea    0x18(%rsp),%rbp			# Load address of $rsp+24 to $rbp
jmp    0x400f17 <phase_2+27>	# Goto loop

给这三行加入一个注释,可以推测出,phase 2一开始分配的栈空间,包含了一个int类型数组,$rbp 存储的可能是这个数组的末地址,$rbx 存储的是数组的第2个元素的地址。

接下来分析这个循环:

   0x0000000000400f17 <+27>:	mov    -0x4(%rbx),%eax
   0x0000000000400f1a <+30>:	add    %eax,%eax
   0x0000000000400f1c <+32>:	cmp    %eax,(%rbx)
   0x0000000000400f1e <+34>:	je     0x400f25 <phase_2+41>
   0x0000000000400f20 <+36>:	callq  0x40143a <explode_bomb>
   0x0000000000400f25 <+41>:	add    $0x4,%rbx
   0x0000000000400f29 <+45>:	cmp    %rbp,%rbx
   0x0000000000400f2c <+48>:	jne    0x400f17 <phase_2+27>
   0x0000000000400f2e <+50>:	jmp    0x400f3c <phase_2+64>

给前五行指令添上注释:

mov    -0x4(%rbx),%eax			# Copy ($rbx-4) to $eax
add    %eax,%eax				# Compute $eax = $eax * 2
cmp    %eax,(%rbx)				# Compare ($rbx) and $eax
je     0x400f25 <phase_2+41>	# if equal, jump to phase_2+41
callq  0x40143a <explode_bomb>	# else call explode_bomb

这几行代码的效果是检查数组元素 a[k] 是否等于 2 * a[k-1],如果不相等,炸弹被引爆,

分析这个循环接下来的代码:

   0x0000000000400f25 <+41>:	add    $0x4,%rbx
   0x0000000000400f29 <+45>:	cmp    %rbp,%rbx
   0x0000000000400f2c <+48>:	jne    0x400f17 <phase_2+27>
   0x0000000000400f2e <+50>:	jmp    0x400f3c <phase_2+64>

第1行就是循环的更新操作,第2,3行就是判断 $rbx 是否等于 $rbp,也就是循环的遍历是否到达末尾,如果到达了,就退出循环,退出循环之后,就是执行退出phase 2函数的操作了,回收栈空间等操作。

所以,phase 2的答案为:1 2 4 8 16 32。一个以1为首项,2为公比的等比数列。

验证答案:

Phase 1 defused. How about the next one?
1 2 4 8 16 32 
That's number 2.  Keep going!

答案正确。

将phase 2的汇编语言代码等价翻译为C语言:

// arr is our input numbers and n = 6
void phase_2(int nums[], int n) {
    for (int i = 1; i < n; i++) {
        int temp = nums[i - 1] * 2;
        if (nums[i] != temp) {
            explode_bomb();
            exit(-1);
        }
    }
    printf("That's number 2. Keep going!");
}

Phase 3

给phase 3添加断点,反汇编phase 3,结果如下:

(gdb) break phase_3
Breakpoint 3 at 0x400f43
(gdb) run
......

Breakpoint 3, 0x0000000000400f43 in phase_3 ()
(gdb) disas phase_3
Dump of assembler code for function phase_3:
=> 0x0000000000400f43 <+0>:		sub    $0x18,%rsp
   0x0000000000400f47 <+4>:		lea    0xc(%rsp),%rcx
   0x0000000000400f4c <+9>:		lea    0x8(%rsp),%rdx
   0x0000000000400f51 <+14>:	mov    $0x4025cf,%esi
   0x0000000000400f56 <+19>:	mov    $0x0,%eax
   0x0000000000400f5b <+24>:	callq  0x400bf0 <__isoc99_sscanf@plt>
   0x0000000000400f60 <+29>:	cmp    $0x1,%eax
   0x0000000000400f63 <+32>:	jg     0x400f6a <phase_3+39>
   0x0000000000400f65 <+34>:	callq  0x40143a <explode_bomb>
   0x0000000000400f6a <+39>:	cmpl   $0x7,0x8(%rsp)
   0x0000000000400f6f <+44>:	ja     0x400fad <phase_3+106>
   0x0000000000400f71 <+46>:	mov    0x8(%rsp),%eax
   0x0000000000400f75 <+50>:	jmpq   *0x402470(,%rax,8)
   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>:	callq  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>:	callq  0x40143a <explode_bomb>
   0x0000000000400fc9 <+134>:	add    $0x18,%rsp
   0x0000000000400fcd <+138>:	retq   
End of assembler dump.

可以看到这个函数很长,还是老样子,根据它们的逻辑,划分不同块来分析,

这是第一部分:

   0x0000000000400f47 <+4>:		lea    0xc(%rsp),%rcx
   0x0000000000400f4c <+9>:		lea    0x8(%rsp),%rdx
   0x0000000000400f51 <+14>:	mov    $0x4025cf,%esi
   0x0000000000400f56 <+19>:	mov    $0x0,%eax
   0x0000000000400f5b <+24>:	callq  0x400bf0 <__isoc99_sscanf@plt>
   0x0000000000400f60 <+29>:	cmp    $0x1,%eax
   0x0000000000400f63 <+32>:	jg     0x400f6a <phase_3+39>
   0x0000000000400f65 <+34>:	callq  0x40143a <explode_bomb>

注意到第五行调用了一个函数名为__isoc99_sscanf@plt,实际上,他是一个库函数,在cplusplus官网查阅相关文档得知,sscanf的声明如下:

int sscanf ( const char * s, const char * format, ...);

Read formatted data from string

Reads data from s and stores them according to parameter format into the locations given by the additional arguments, as if scanf was used, but reading from s instead of the standard input (stdin)

The additional arguments should point to already allocated objects of the type specified by their corresponding format specifier within the format string.

意思就是根据指定的格式(format)提取字符串s中的内容,返回读取成功的内容个数,其中...就是提取内容存储的地址。

比如:

/* sscanf example */
#include <stdio.h>

int main ()
{
  char sentence []="Rudolph is 12 years old";
  char str [20];
  int i;

  sscanf (sentence,"%s %*s %d",str,&i);
  printf ("%s -> %d\n",str,i);
  
  return 0;
}
// Output: Rodolph -> 12

分析在调用sscanf函数前代码准备的参数语句:

lea    0xc(%rsp),%rcx	# Load address of $rsp+12 to $rcx
lea    0x8(%rsp),%rdx	# Load address of $rsp+8 to $rdx
mov    $0x4025cf,%esi	# Copy 0x4025cf to $esi
mov    $0x0,%eax		# Clear $eax

# callq  0x400bf0 <__isoc99_sscanf@plt>

在调用函数sscanf之前准备了3个寄存器:$rsi, $rdx, $rcx,它们分别是函数调用第2,3,4位置的参数寄存器,第1个寄存器为:$rdi,就是我们输入的字符串,因此我们可以确定:

  • $rdi 存放了需要提取内容的字符串地址;

  • $rsi 存放了输入格式字符串;

  • $rdx$rcx 分别存放着两个地址,用来存放提取内容,显然是4字节的integer,因为它们的地址差为4。

为了验证上述推测,我们可以在gdb测试,输入下列指令:

(gdb) x/s 0x425cf
0x4025cf:	"%d %d"							# format string
(gdb) x/s $rdi
0x603820 <input_strings+160>:	"6 6 6 6"	# input string

因此我们实际上只需要输入两个整数。

接下来分析这一部分的剩下三行代码:

cmp    $0x1,%eax
jg     0x400f6a <phase_3+39>
callq  0x40143a <explode_bomb>

调用了sscanf函数后,返回值就是从input string根据format string提取内容的数量,而这三行代码就是判断输入的整数是否大于1个,否则引爆炸弹。

第二部分内容分析:

   0x0000000000400f6a <+39>:	cmpl   $0x7,0x8(%rsp)
   0x0000000000400f6f <+44>:	ja     0x400fad <phase_3+106>
   0x0000000000400f71 <+46>:	mov    0x8(%rsp),%eax
   0x0000000000400f75 <+50>:	jmpq   *0x402470(,%rax,8)
   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>:	callq  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>:	callq  0x40143a <explode_bomb>

这一部分的代码很长,但是都很有规律,实际上他是一个很典型的 switch 语句结构,第一行 cmp 指令将我们输入的第1个数字和7比较,如果大于7,将会引爆炸弹,因此我们又得到一条重要信息:

输入的第1个参数不能大于7。

接着的两行代码:

mov    0x8(%rsp),%eax		# Load input value1 to $eax
jmpq   *0x402470(,%rax,8)	# Goto (0x402470 + $rax * 8)

注意第二行代码是一个间接跳转指令:

jump *operand

间接跳转就是从取出地址为 operand 的内容作为跳转地址。

因此第二行指令跳转的地址就是:M[0x402470+$rax*8]

它是跟$rax有关的,也就是跟我们输入的第1个数字有关,所以我们可以通过计算得出对应的正确跳转地址。

首先查看内存 0x402470 的内容

x 0x402470
0x402470:	0x0000000000400f7c

实际上switch语句需要有一个跳转表 jump table,而这个地址 0x402470实际就是跳转表的的起始地址。

因此它的基址为:0x400f7c,我们再看swicth语句条目:

   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>:	callq  0x40143a <explode_bomb>
   0x0000000000400fb2 <+111>:	mov    $0x0,%eax
   0x0000000000400fb7 <+116>:	jmp    0x400fbe <phase_3+123>
   0x0000000000400fb9 <+118>:	mov    $0x137,%eax

上面的代码是 mov, jmp 成对出现的,一共有8个case,所以跳转表应该有8个表项,

输入指令:

(gdb) x/8g 0x402470
0x402470:	0x0000000000400f7c	0x0000000000400fb9
0x402480:	0x0000000000400f83	0x0000000000400f8a
0x402490:	0x0000000000400f91	0x0000000000400f98
0x4024a0:	0x0000000000400f9f	0x0000000000400fa6

可以看到上述的每一个跳转表表项都对应着swith语句代码的一个case入口,而每一个case就是一个mov指令,将不同的值放入寄存器 $eax,因此可以得到输入和跳转的一个对应的关系:

value1jump address$eax
00x400f7c0xcf
10x400fb90x137
20x400f830x2c3
30x400f8a0x100
40x400f910x185
50x400f980xce
60x400f9f0x2aa
70x400fa60x147

最后分析最后三行重要的代码:

   0x0000000000400fbe <+123>:	cmp    0xc(%rsp),%eax
   0x0000000000400fc2 <+127>:	je     0x400fc9 <phase_3+134>
   0x0000000000400fc4 <+129>:	callq  0x40143a <explode_bomb>

这里就是简单匹配我们输入的第二参数和 $eax 的结果是否匹配,所以我们这一关是有8个答案的:

value1value2
0207
1311
2707
3256
4389
5206
6682
7327

将phase 3转换为C语言代码:

void phase_3(int value1, int value2) {
	int temp;
    switch(value1) {
        case 0: temp = 207; break;
        case 1: temp = 311; break;
        case 2: temp = 707; break;
        case 3: temp = 256; break;
        case 4: temp = 389; break;
        case 5: temp = 206; break;
        case 6: temp = 682; break;
        case 7: temp = 327; break;
    }
    if (value2 != temp) {
        explode_bomb();
        exit(-1);
    }
    printf("Halfway there!");
}

Phase 4

还是老样子,先添加断点,再反汇编phase 4,查看大致结构:

(gdb) break phase_4
Breakpoint 2, 0x000000000040100c in phase_4 ()
(gdb) disas phase_4
Dump of assembler code for function phase_4:
=> 0x000000000040100c <+0>:		sub    $0x18,%rsp
   0x0000000000401010 <+4>:		lea    0xc(%rsp),%rcx
   0x0000000000401015 <+9>:		lea    0x8(%rsp),%rdx
   0x000000000040101a <+14>:	mov    $0x4025cf,%esi
   0x000000000040101f <+19>:	mov    $0x0,%eax
   0x0000000000401024 <+24>:	callq  0x400bf0 <__isoc99_sscanf@plt>
   0x0000000000401029 <+29>:	cmp    $0x2,%eax
   0x000000000040102c <+32>:	jne    0x401035 <phase_4+41>
   0x000000000040102e <+34>:	cmpl   $0xe,0x8(%rsp)
   0x0000000000401033 <+39>:	jbe    0x40103a <phase_4+46>
   0x0000000000401035 <+41>:	callq  0x40143a <explode_bomb>
   0x000000000040103a <+46>:	mov    $0xe,%edx
   0x000000000040103f <+51>:	mov    $0x0,%esi
   0x0000000000401044 <+56>:	mov    0x8(%rsp),%edi
   0x0000000000401048 <+60>:	callq  0x400fce <func4>
   0x000000000040104d <+65>:	test   %eax,%eax
   0x000000000040104f <+67>:	jne    0x401058 <phase_4+76>
   0x0000000000401051 <+69>:	cmpl   $0x0,0xc(%rsp)
   0x0000000000401056 <+74>:	je     0x40105d <phase_4+81>
   0x0000000000401058 <+76>:	callq  0x40143a <explode_bomb>
   0x000000000040105d <+81>:	add    $0x18,%rsp
   0x0000000000401061 <+85>:	retq   
End of assembler dump.

将其划分为下列几个部分:

第一部分:

   0x0000000000401010 <+4>:		lea    0xc(%rsp),%rcx
   0x0000000000401015 <+9>:		lea    0x8(%rsp),%rdx
   0x000000000040101a <+14>:	mov    $0x4025cf,%esi
   0x000000000040101f <+19>:	mov    $0x0,%eax
   0x0000000000401024 <+24>:	callq  0x400bf0 <__isoc99_sscanf@plt>
   0x0000000000401029 <+29>:	cmp    $0x2,%eax
   0x000000000040102c <+32>:	jne    0x401035 <phase_4+41>
   0x000000000040102e <+34>:	cmpl   $0xe,0x8(%rsp)
   0x0000000000401033 <+39>:	jbe    0x40103a <phase_4+46>
   0x0000000000401035 <+41>:	callq  0x40143a <explode_bomb>

这一段代码前半部分很熟悉,和 phase 3的开始是相同的,就不再赘述了,我们看后面的部分:

   0x0000000000401029 <+29>:	cmp    $0x2,%eax
   0x000000000040102c <+32>:	jne    0x401035 <phase_4+41>
   0x000000000040102e <+34>:	cmpl   $0xe,0x8(%rsp)
   0x0000000000401033 <+39>:	jbe    0x40103a <phase_4+46>
   0x0000000000401035 <+41>:	callq  0x40143a <explode_bomb>

第1,2行代码实际上限制了输入的数字一定要包含2个整数,第3,4行代码限制输入的第1个参数必须小于等于14。

第二部分:

   0x000000000040103a <+46>:	mov    $0xe,%edx
   0x000000000040103f <+51>:	mov    $0x0,%esi
   0x0000000000401044 <+56>:	mov    0x8(%rsp),%edi
   0x0000000000401048 <+60>:	callq  0x400fce <func4>
   0x000000000040104d <+65>:	test   %eax,%eax
   0x000000000040104f <+67>:	jne    0x401058 <phase_4+76>
   0x0000000000401051 <+69>:	cmpl   $0x0,0xc(%rsp)
   0x0000000000401056 <+74>:	je     0x40105d <phase_4+81>
   0x0000000000401058 <+76>:	callq  0x40143a <explode_bomb>

注意到这一部分调用一个函数func4,在此之前就是准备这个函数的参数:

  • $edi 为我们输入的第一个整数;
  • $esi 为0;
  • $edx 为14。

暂且略过func4的细节部分,我们可以看到,这一关要求调用了func4函数后,其返回值为0,否则引爆炸弹,并且我们输入的第2个参数一定要是0,否则引爆炸弹。

到这里,我们确定了输入的第二个参数一定是0。

接下来分析函数 func4 的细节:

(gdb) disas func4
Dump of assembler code for function func4:
   0x0000000000400fce <+0>:		sub    $0x8,%rsp
   0x0000000000400fd2 <+4>:		mov    %edx,%eax
   0x0000000000400fd4 <+6>:		sub    %esi,%eax
   0x0000000000400fd6 <+8>:		mov    %eax,%ecx
   0x0000000000400fd8 <+10>:	shr    $0x1f,%ecx
   0x0000000000400fdb <+13>:	add    %ecx,%eax
   0x0000000000400fdd <+15>:	sar    %eax
   0x0000000000400fdf <+17>:	lea    (%rax,%rsi,1),%ecx
   0x0000000000400fe2 <+20>:	cmp    %edi,%ecx
   0x0000000000400fe4 <+22>:	jle    0x400ff2 <func4+36>
   0x0000000000400fe6 <+24>:	lea    -0x1(%rcx),%edx
   0x0000000000400fe9 <+27>:	callq  0x400fce <func4>
   0x0000000000400fee <+32>:	add    %eax,%eax
   0x0000000000400ff0 <+34>:	jmp    0x401007 <func4+57>
   0x0000000000400ff2 <+36>:	mov    $0x0,%eax
   0x0000000000400ff7 <+41>:	cmp    %edi,%ecx
   0x0000000000400ff9 <+43>:	jge    0x401007 <func4+57>
   0x0000000000400ffb <+45>:	lea    0x1(%rcx),%esi
   0x0000000000400ffe <+48>:	callq  0x400fce <func4>
   0x0000000000401003 <+53>:	lea    0x1(%rax,%rax,1),%eax
   0x0000000000401007 <+57>:	add    $0x8,%rsp
   0x000000000040100b <+61>:	retq   
End of assembler dump.

我们从phase 4 中调用func4 用了3个参数:func4(value1, 0, 14)

先看第一部分:

mov    %edx,%eax			# Copy $edx to $eax 
sub    %esi,%eax			# Compute $eax = $eax - $esi
mov    %eax,%ecx			# Copy $eax to $ecx
shr    $0x1f,%ecx			# Shift $eax right by 31
add    %ecx,%eax			# Compute $eax = $eax + $ecx
sar    %eax					# Shift $eax right by 1
lea    (%rax,%rsi,1),%ecx	# Compute $ecx = $rax + $rsi

换成 C 语言的形式为:

# Assume func4(int x, int y, int z)

int temp = z - y;
temp = ((unsigned)temp >> 31) + temp;
temp = temp / 2;
temp = temp + y;

到这一步,其实我们可以假设(z - y)的值始终为正数,那么计算结果就是:temp = y + (z - y)/2

接下来看第二部分:

   0x0000000000400fe2 <+20>:	cmp    %edi,%ecx
   0x0000000000400fe4 <+22>:	jle    0x400ff2 <func4+36>
   0x0000000000400fe6 <+24>:	lea    -0x1(%rcx),%edx
   0x0000000000400fe9 <+27>:	callq  0x400fce <func4>
   0x0000000000400fee <+32>:	add    %eax,%eax
   0x0000000000400ff0 <+34>:	jmp    0x401007 <func4+57>
   0x0000000000400ff2 <+36>:	mov    $0x0,%eax
   0x0000000000400ff7 <+41>:	cmp    %edi,%ecx
   0x0000000000400ff9 <+43>:	jge    0x401007 <func4+57>
   0x0000000000400ffb <+45>:	lea    0x1(%rcx),%esi
   0x0000000000400ffe <+48>:	callq  0x400fce <func4>
   0x0000000000401003 <+53>:	lea    0x1(%rax,%rax,1),%eax

第一行指令会比较我们从第1部分计算的结果和第1个参数(我们输入的第1个整数val)进行比较。

  • 如果val>temp ,那么就调用函数 func4($edi, $esi, temp - 1)
  • 如果val<temp,那么调用函数func4($edi, temp + 1, $edx)
  • 如果val=temp,那么返回0

看到这里,其实可以大概知道函数func4的功能了,不就是一个二分查找嘛。

实际上就是二分查找我们输入的第1个参数,范围在:[0, 14]。

注意到两条指令:

   0x0000000000400fee <+32>:	add    %eax,%eax
   .......
   0x0000000000401003 <+53>:	lea    0x1(%rax,%rax,1),%eax

这两条分别是不同分支的func4返回函数所计算的 $eax,计算方式不同,

  • 一个是:2 * $eax
  • 一个是:2 * $eax + 1

把函数func4写成C语言代码:

int func4(int val, int lo, int hi) {
    int mid = lo + (hi - lo) / 2;
    if (key < mid) {
        return 2 * func4(val, lo, mid - 1);
    } else if (key > mid) {
        return 2 * func4(val, mid + 1, hi) + 1;
    } else if (key == mid) {
        return 0;
    }
}

结合前面的分析,func4的结果必须为0,那么它递归调用只能够总是走 key < mid 的分支。

下面这个图是[0, 14]区间内的二叉搜索树:

7
3
11
1
5
9
13
0
2
4
6
8
10
12
14

我们必须走左子树,因此答案有4个:0, 1, 3, 7

综合前面第2个数字为0,所以总共有4个答案:

value1value2
00
10
30
70

将phase 4转化为C语言代码:

int func4(int val, int lo, int hi) {
    int mid = lo + (hi - lo) / 2;
    if (key < mid) {
        return 2 * func4(val, lo, mid - 1);
    } else if (key > mid) {
        return 2 * func4(val, mid + 1, hi) + 1;
    } else if (key == mid) {
        return 0;
    }
}

// n 为nums的长度
void phase_4(int nums[], int n) {
    // 输入长度限制为2
    if (n != 2) {
        explode_bomb();
        exit(-1);
    }
    // nums[0] 的合法性判断
    if (nums[0] > 14) {
        explode_bomb();
        exit(-1);
    }
    
    if (func4(nums[0], 0, 14) != 0) {
        explode_bomb();
        exit(-1);
    }
    
    // nums[1]的合法性判断
    if (nums[1] != 0) {
        explode_bomb();
        exit(-1);
    }
    printf("So you got that one.  Try this one.");
}

Phase 5

添加断点,反汇编 phase 5:

(gdb) break phase_5
Breakpoint 3, 0x0000000000401062 in phase_5 ()
(gdb) run
......


(gdb) disas phase_5
Dump of assembler code for function phase_5:
=> 0x0000000000401062 <+0>:		push   %rbx
   0x0000000000401063 <+1>:		sub    $0x20,%rsp
   0x0000000000401067 <+5>:		mov    %rdi,%rbx
   0x000000000040106a <+8>:		mov    %fs:0x28,%rax
   0x0000000000401073 <+17>:	mov    %rax,0x18(%rsp)
   0x0000000000401078 <+22>:	xor    %eax,%eax
   0x000000000040107a <+24>:	callq  0x40131b <string_length>
   0x000000000040107f <+29>:	cmp    $0x6,%eax
   0x0000000000401082 <+32>:	je     0x4010d2 <phase_5+112>
   0x0000000000401084 <+34>:	callq  0x40143a <explode_bomb>
   0x0000000000401089 <+39>:	jmp    0x4010d2 <phase_5+112>
   0x000000000040108b <+41>:	movzbl (%rbx,%rax,1),%ecx
   0x000000000040108f <+45>:	mov    %cl,(%rsp)
   0x0000000000401092 <+48>:	mov    (%rsp),%rdx
   0x0000000000401096 <+52>:	and    $0xf,%edx
   0x0000000000401099 <+55>:	movzbl 0x4024b0(%rdx),%edx
   0x00000000004010a0 <+62>:	mov    %dl,0x10(%rsp,%rax,1)
   0x00000000004010a4 <+66>:	add    $0x1,%rax
   0x00000000004010a8 <+70>:	cmp    $0x6,%rax
   0x00000000004010ac <+74>:	jne    0x40108b <phase_5+41>
   0x00000000004010ae <+76>:	movb   $0x0,0x16(%rsp)
   0x00000000004010b3 <+81>:	mov    $0x40245e,%esi
   0x00000000004010b8 <+86>:	lea    0x10(%rsp),%rdi
   0x00000000004010bd <+91>:	callq  0x401338 <strings_not_equal>
   0x00000000004010c2 <+96>:	test   %eax,%eax
   0x00000000004010c4 <+98>:	je     0x4010d9 <phase_5+119>
   0x00000000004010c6 <+100>:	callq  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>:	callq  0x400b30 <__stack_chk_fail@plt>
   0x00000000004010ee <+140>:	add    $0x20,%rsp
   0x00000000004010f2 <+144>:	pop    %rbx
   0x00000000004010f3 <+145>:	retq   
End of assembler dump.

一眼看去这个函数的汇编语言代码很长,我们分成几部分进行分析。

第一部分:

mov    %rdi,%rbx
mov    %fs:0x28,%rax
mov    %rax,0x18(%rsp)
xor    %eax,%eax				# Clear $eax
callq  0x40131b <string_length>	# Call function string_length
cmp    $0x6,%eax				# if string_length != 6, explode_bomb
je     0x4010d2 <phase_5+112>
callq  0x40143a <explode_bomb>

显然,phase 5要求我们输入一个字符串并且长度为6。

第二部分(循环):

   0x000000000040108b <+41>:	movzbl (%rbx,%rax,1),%ecx
   0x000000000040108f <+45>:	mov    %cl,(%rsp)
   0x0000000000401092 <+48>:	mov    (%rsp),%rdx
   0x0000000000401096 <+52>:	and    $0xf,%edx
   0x0000000000401099 <+55>:	movzbl 0x4024b0(%rdx),%edx
   0x00000000004010a0 <+62>:	mov    %dl,0x10(%rsp,%rax,1)
   0x00000000004010a4 <+66>:	add    $0x1,%rax
   0x00000000004010a8 <+70>:	cmp    $0x6,%rax
   0x00000000004010ac <+74>:	jne    0x40108b <phase_5+41>
   0x00000000004010ae <+76>:	movb   $0x0,0x16(%rsp)
   0x00000000004010b3 <+81>:	mov    $0x40245e,%esi
   0x00000000004010b8 <+86>:	lea    0x10(%rsp),%rdi
   0x00000000004010bd <+91>:	callq  0x401338 <strings_not_equal>
   0x00000000004010c2 <+96>:	test   %eax,%eax
   0x00000000004010c4 <+98>:	je     0x4010d9 <phase_5+119>
   0x00000000004010c6 <+100>:	callq  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>

这段代码比较复杂,但是可以看出这段代码最后两行是这个循环的初始化部分。

这里可以细分成两个部分

第2.1部分:

movzbl (%rbx,%rax,1),%ecx		# Copy $rbx[$rax] to $ecx
mov    %cl,(%rsp)				# Store $cl to M[$rsp]
mov    (%rsp),%rdx				# Load M[$rsp]	to $rdx
and    $0xf,%edx				# Compute $edx = $edx & 0xf
movzbl 0x4024b0(%rdx),%edx		# Copy M[0x4024b0 + $edx] to $edx
mov    %dl,0x10(%rsp,%rax,1)	# Store $dl to M[$rsp + $rax + 16]
add    $0x1,%rax
cmp    $0x6,%rax
jne    0x40108b <phase_5+41>

根据循环的初始化部分以及该部分的后三行代码可知,这个循环代码将会执行6次,

$rbx 实际上保存着我们输入的字符串的首地址,这个循环的工作就是遍历这个长度为6的字符串,每个字符的值作为一个偏移offset将内存 M[0x4024b0+offset]的字符存入内存 M[$rsp+i+16](i为循环计数器值),这里出现了一个基地址,我们查看它的值:

(gdb) x/s 0x4024b0
0x4024b0 <array.3449>:	"maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"

这个地址前16个字符是乱序的,后面跟着一句英文 “So you think you can stop the bomb with ctrl-c, do you?”,意思是“你认为你可以使用CTRL+C停止炸弹吗?”,可恶,居然敢嘲讽我,我一定要把你拆掉。

第2.2部分:

movb   $0x0,0x16(%rsp)
mov    $0x40245e,%esi
lea    0x10(%rsp),%rdi
callq  0x401338 <strings_not_equal>
test   %eax,%eax
je     0x4010d9 <phase_5+119>
callq  0x40143a <explode_bomb>
nopl   0x0(%rax,%rax,1)
jmp    0x4010d9 <phase_5+119>

这里调用了函数strings_not_equal,这是phase_1中所调用的函数,如果它的返回值为1,就是说两个字符串不相等,那么炸弹将会被引爆,因此,位于地址 0x40245e 和 地址 $rsp+16 的字符串必须相等,这里出现了一个地址,我们可以去查看它的值:

(gdb) x/s 0x40245e
0x40245e:	"flyers"

结合2.1,我们可以知道,实际上2.1就是构造一个长度为6的字符串,根据我们输入的字符串的偏移,要得到字符串“flyers”,偏移序列应为:9->15->14->5->6->7,注意到这些偏移都是在0-15之间的,因为第2.1部分有一个指令 and $0xf,%edx,实际上把我们输入的字符串只是截取低 4bit,因此我们查阅ASCII码表,知道A的ascii码值为:0x65=100 0001,最后得到输入的字符串应为:IONEFG,也可以是:ionefg。

所以这个关卡有两个答案:IONEFG、ionefg。

Phase 6

添加断点,运行程序,反汇编phase 6:

(gdb) break phase_6
Breakpoint 4 at 0x4010f4
(gdb) run
......

(gdb) disas phase_6
Dump of assembler code for function 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
   0x0000000000401100 <+12>:	mov    %rsp,%r13
   0x0000000000401103 <+15>:	mov    %rsp,%rsi
   0x0000000000401106 <+18>:	callq  0x40145c <read_six_numbers>
   0x000000000040110b <+23>:	mov    %rsp,%r14
   0x000000000040110e <+26>:	mov    $0x0,%r12d
   0x0000000000401114 <+32>:	mov    %r13,%rbp
   0x0000000000401117 <+35>:	mov    0x0(%r13),%eax
   0x000000000040111b <+39>:	sub    $0x1,%eax
   0x000000000040111e <+42>:	cmp    $0x5,%eax
   0x0000000000401121 <+45>:	jbe    0x401128 <phase_6+52>
   0x0000000000401123 <+47>:	callq  0x40143a <explode_bomb>
   0x0000000000401128 <+52>:	add    $0x1,%r12d
   0x000000000040112c <+56>:	cmp    $0x6,%r12d
   0x0000000000401130 <+60>:	je     0x401153 <phase_6+95>
   0x0000000000401132 <+62>:	mov    %r12d,%ebx
   0x0000000000401135 <+65>:	movslq %ebx,%rax
   0x0000000000401138 <+68>:	mov    (%rsp,%rax,4),%eax
   0x000000000040113b <+71>:	cmp    %eax,0x0(%rbp)
   0x000000000040113e <+74>:	jne    0x401145 <phase_6+81>
   0x0000000000401140 <+76>:	callq  0x40143a <explode_bomb>
   0x0000000000401145 <+81>:	add    $0x1,%ebx
   0x0000000000401148 <+84>:	cmp    $0x5,%ebx
   0x000000000040114b <+87>:	jle    0x401135 <phase_6+65>
   0x000000000040114d <+89>:	add    $0x4,%r13
   0x0000000000401151 <+93>:	jmp    0x401114 <phase_6+32>
   0x0000000000401153 <+95>:	lea    0x18(%rsp),%rsi
   0x0000000000401158 <+100>:	mov    %r14,%rax
   0x000000000040115b <+103>:	mov    $0x7,%ecx
   0x0000000000401160 <+108>:	mov    %ecx,%edx
   0x0000000000401162 <+110>:	sub    (%rax),%edx
   0x0000000000401164 <+112>:	mov    %edx,(%rax)
   0x0000000000401166 <+114>:	add    $0x4,%rax
   0x000000000040116a <+118>:	cmp    %rsi,%rax
   0x000000000040116d <+121>:	jne    0x401160 <phase_6+108>
   0x000000000040116f <+123>:	mov    $0x0,%esi
   0x0000000000401174 <+128>:	jmp    0x401197 <phase_6+163>
   0x0000000000401176 <+130>:	mov    0x8(%rdx),%rdx
   0x000000000040117a <+134>:	add    $0x1,%eax
   0x000000000040117d <+137>:	cmp    %ecx,%eax
   0x000000000040117f <+139>:	jne    0x401176 <phase_6+130>
   0x0000000000401181 <+141>:	jmp    0x401188 <phase_6+148>
   0x0000000000401183 <+143>:	mov    $0x6032d0,%edx
   0x0000000000401188 <+148>:	mov    %rdx,0x20(%rsp,%rsi,2)
   0x000000000040118d <+153>:	add    $0x4,%rsi
   0x0000000000401191 <+157>:	cmp    $0x18,%rsi
   0x0000000000401195 <+161>:	je     0x4011ab <phase_6+183>
   0x0000000000401197 <+163>:	mov    (%rsp,%rsi,1),%ecx
   0x000000000040119a <+166>:	cmp    $0x1,%ecx
   0x000000000040119d <+169>:	jle    0x401183 <phase_6+143>
   0x000000000040119f <+171>:	mov    $0x1,%eax
   0x00000000004011a4 <+176>:	mov    $0x6032d0,%edx
   0x00000000004011a9 <+181>:	jmp    0x401176 <phase_6+130>
   0x00000000004011ab <+183>:	mov    0x20(%rsp),%rbx
   0x00000000004011b0 <+188>:	lea    0x28(%rsp),%rax
   0x00000000004011b5 <+193>:	lea    0x50(%rsp),%rsi
   0x00000000004011ba <+198>:	mov    %rbx,%rcx
   0x00000000004011bd <+201>:	mov    (%rax),%rdx
   0x00000000004011c0 <+204>:	mov    %rdx,0x8(%rcx)
   0x00000000004011c4 <+208>:	add    $0x8,%rax
   0x00000000004011c8 <+212>:	cmp    %rsi,%rax
   0x00000000004011cb <+215>:	je     0x4011d2 <phase_6+222>
   0x00000000004011cd <+217>:	mov    %rdx,%rcx
   0x00000000004011d0 <+220>:	jmp    0x4011bd <phase_6+201>
   0x00000000004011d2 <+222>:	movq   $0x0,0x8(%rdx)
   0x00000000004011da <+230>:	mov    $0x5,%ebp
   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>:	callq  0x40143a <explode_bomb>
   0x00000000004011ee <+250>:	mov    0x8(%rbx),%rbx
   0x00000000004011f2 <+254>:	sub    $0x1,%ebp
   0x00000000004011f5 <+257>:	jne    0x4011df <phase_6+235>
   0x00000000004011f7 <+259>:	add    $0x50,%rsp
   0x00000000004011fb <+263>:	pop    %rbx
   0x00000000004011fc <+264>:	pop    %rbp
   0x00000000004011fd <+265>:	pop    %r12
   0x00000000004011ff <+267>:	pop    %r13
   0x0000000000401201 <+269>:	pop    %r14
   0x0000000000401203 <+271>:	retq   
End of assembler dump.

Oh my god!,好家伙,终于放大招了,直接给我整一个这么长的汇编代码,蚌埠住了。

还是老样子,将程序大卸八块,根据对应功能划分,开头和结尾申请和释放栈空间等操作先不用看,

下面分析第一部分:

   0x0000000000401100 <+12>:	mov    %rsp,%r13
   0x0000000000401103 <+15>:	mov    %rsp,%rsi
   0x0000000000401106 <+18>:	callq  0x40145c <read_six_numbers>

注意到这里调用了函数 read_six_numbers,这个函数在phase 2也出现过,就是要求我们输入6个整数,这6个整数将会存储在当前栈空间里地址为 R[$rsp] 开头的24个bytes。

第二部分(二重循环):

   0x000000000040110b <+23>:	mov    %rsp,%r14
   0x000000000040110e <+26>:	mov    $0x0,%r12d
   0x0000000000401114 <+32>:	mov    %r13,%rbp
   0x0000000000401117 <+35>:	mov    0x0(%r13),%eax
   0x000000000040111b <+39>:	sub    $0x1,%eax
   0x000000000040111e <+42>:	cmp    $0x5,%eax
   0x0000000000401121 <+45>:	jbe    0x401128 <phase_6+52>
   0x0000000000401123 <+47>:	callq  0x40143a <explode_bomb>
   0x0000000000401128 <+52>:	add    $0x1,%r12d
   0x000000000040112c <+56>:	cmp    $0x6,%r12d
   0x0000000000401130 <+60>:	je     0x401153 <phase_6+95>
   0x0000000000401132 <+62>:	mov    %r12d,%ebx
   0x0000000000401135 <+65>:	movslq %ebx,%rax
   0x0000000000401138 <+68>:	mov    (%rsp,%rax,4),%eax
   0x000000000040113b <+71>:	cmp    %eax,0x0(%rbp)
   0x000000000040113e <+74>:	jne    0x401145 <phase_6+81>
   0x0000000000401140 <+76>:	callq  0x40143a <explode_bomb>
   0x0000000000401145 <+81>:	add    $0x1,%ebx
   0x0000000000401148 <+84>:	cmp    $0x5,%ebx
   0x000000000040114b <+87>:	jle    0x401135 <phase_6+65>
   0x000000000040114d <+89>:	add    $0x4,%r13
   0x0000000000401151 <+93>:	jmp    0x401114 <phase_6+32>

这一部分很长,实际上是一个二重循环,功能是判断输入的6个整数的合法性,这里的合法性指的是:

  • 每一个输入的整数都必须小于等于6;
  • 并且每一个整数互不相同。

第三部分(循环):

   0x0000000000401153 <+95>:	lea    0x18(%rsp),%rsi
   0x0000000000401158 <+100>:	mov    %r14,%rax
   0x000000000040115b <+103>:	mov    $0x7,%ecx
   0x0000000000401160 <+108>:	mov    %ecx,%edx
   0x0000000000401162 <+110>:	sub    (%rax),%edx
   0x0000000000401164 <+112>:	mov    %edx,(%rax)
   0x0000000000401166 <+114>:	add    $0x4,%rax
   0x000000000040116a <+118>:	cmp    %rsi,%rax
   0x000000000040116d <+121>:	jne    0x401160 <phase_6+108>

这一部分是一个简单的循环,它的作用就是:

for (int i = 0; i < 6; i++) 
    arr[i] = 7 - arr[i]

第四部分(循环):

   0x000000000040116f <+123>:	mov    $0x0,%esi
   0x0000000000401174 <+128>:	jmp    0x401197 <phase_6+163>
   0x0000000000401176 <+130>:	mov    0x8(%rdx),%rdx
   0x000000000040117a <+134>:	add    $0x1,%eax
   0x000000000040117d <+137>:	cmp    %ecx,%eax
   0x000000000040117f <+139>:	jne    0x401176 <phase_6+130>
   0x0000000000401181 <+141>:	jmp    0x401188 <phase_6+148>
   0x0000000000401183 <+143>:	mov    $0x6032d0,%edx
   0x0000000000401188 <+148>:	mov    %rdx,0x20(%rsp,%rsi,2)
   0x000000000040118d <+153>:	add    $0x4,%rsi
   0x0000000000401191 <+157>:	cmp    $0x18,%rsi
   0x0000000000401195 <+161>:	je     0x4011ab <phase_6+183>
   0x0000000000401197 <+163>:	mov    (%rsp,%rsi,1),%ecx
   0x000000000040119a <+166>:	cmp    $0x1,%ecx
   0x000000000040119d <+169>:	jle    0x401183 <phase_6+143>
   0x000000000040119f <+171>:	mov    $0x1,%eax
   0x00000000004011a4 <+176>:	mov    $0x6032d0,%edx
   0x00000000004011a9 <+181>:	jmp    0x401176 <phase_6+130>

最后三行是循环的初始化部分,注意到倒数第二行的内存地址 0x6032d0,我们可以查看一下它的内容,实际上是一个链表结构:

(gdb) x/12xg 0x6032d0
0x6032d0 <node1>:	0x000000010000014c	0x00000000006032e0
0x6032e0 <node2>:	0x00000002000000a8	0x00000000006032f0
0x6032f0 <node3>:	0x000000030000039c	0x0000000000603300
0x603300 <node4>:	0x00000004000002b3	0x0000000000603310
0x603310 <node5>:	0x00000005000001dd	0x0000000000603320
0x603320 <node6>:	0x00000006000001bb	0x0000000000000000

这个地址的内容是一个节点数为6的一个链表,它的结构为:

typdef struct {
    int val;
    struct node* next;
}node;

实际上这段代码等同于下列C语言代码:

for (int i = 0; i < 6; i++) {
    node* p = head;
    for (int count = 1; count < 6; count++, p = p->next) {
        if (count == arr[i]) {
            b[i] = p;
        }
    }
}

作用就是根据输入的6个整数存储链表节点的6个地址值在数组b中。

第四部分(循环):

   0x00000000004011ab <+183>:	mov    0x20(%rsp),%rbx
   0x00000000004011b0 <+188>:	lea    0x28(%rsp),%rax
   0x00000000004011b5 <+193>:	lea    0x50(%rsp),%rsi
   0x00000000004011ba <+198>:	mov    %rbx,%rcx
   0x00000000004011bd <+201>:	mov    (%rax),%rdx
   0x00000000004011c0 <+204>:	mov    %rdx,0x8(%rcx)
   0x00000000004011c4 <+208>:	add    $0x8,%rax
   0x00000000004011c8 <+212>:	cmp    %rsi,%rax
   0x00000000004011cb <+215>:	je     0x4011d2 <phase_6+222>
   0x00000000004011cd <+217>:	mov    %rdx,%rcx
   0x00000000004011d0 <+220>:	jmp    0x4011bd <phase_6+201>
   0x00000000004011d2 <+222>:	movq   $0x0,0x8(%rdx)

等同与下面C语言代码:

// 数组b用来存放链表6个指针值
node* head = b[0];
node* p = head;
for (int i = 1; i < 6; i++) {
    p->next = b[i];
    p = p->next;
}

这一部分的作用是,根据数组b的指针序列,重组链表

第五部分(循环):

   0x00000000004011da <+230>:	mov    $0x5,%ebp
   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>:	callq  0x40143a <explode_bomb>
   0x00000000004011ee <+250>:	mov    0x8(%rbx),%rbx
   0x00000000004011f2 <+254>:	sub    $0x1,%ebp
   0x00000000004011f5 <+257>:	jne    0x4011df <phase_6+235>

这里是一个简单的循环结构,等同与下面的C语言代码:

node* p = head->next;
node* q = head;
while (p != nullptr) {
    if (p->val < q->val) {
        explode_bomb();
        exit(-1);
    }
}

我们发现,第四部分重组后链表的值必须是递减的,否则炸弹被引爆。

再看看链表原来的结构:

(gdb) x/12xg 0x6032d0
0x6032d0 <node1>:	0x000000010000014c	0x00000000006032e0
0x6032e0 <node2>:	0x00000002000000a8	0x00000000006032f0
0x6032f0 <node3>:	0x000000030000039c	0x0000000000603300
0x603300 <node4>:	0x00000004000002b3	0x0000000000603310
0x603310 <node5>:	0x00000005000001dd	0x0000000000603320
0x603320 <node6>:	0x00000006000001bb	0x0000000000000000

递减的序列为:3->4->5->6->1->2,由于开头进行了转化arr[i]=7-arr[i],所以我们得出答案,输入的6个整数应为:4 3 2 1 6 5

将phase 6转换为C语言如下:

typdef struct {
    long val;
    struct node* next;
}node;

// n = 6
void phase_6(int arr[], int n) {
    if(n != 6) {
        explode_bomb();
        exit(-1);
    }
    // 合法性判断
    for (int i = 0; i < n; i++) {
        if (arr[i] > 6) {
            explode_bomb();
            exit(-1);
        }
        for (int j = i + 1; j < n; j++) {
            if (arr[j] == arr[i]) {
                explode_bomb();
                exit(-1);
            }
        }
    }
    // 输入变换
    for (int i = 0; i < n; i++) {
        arr[i] = 7 - arr[i];
    }
    // 根据arr存储不同节点的指针值
    node* b[n];
    for (int i = 0; i < n; i++) {
        node* p = head;
        for (int count = 1; count < 6; count++, p = p->next) {
            if (count == arr[i]) {
                b[i] = p;
            }
   		 }
	}
    // 重组链表
    node* head = b[0];
    node* p = head;
    for (int i = 1; i < n; i++) {
        p->next = b[i];
        p = p->next;
    }
    
    // 检验重组链表是否为递减链表
    node* p = head->next;
    node* q = head;
    while (p != nullptr) {
        if (p->val < q->val) {
            explode_bomb();
            exit(-1);
        }
    }
    printf("Congratulations, you've defused the bomb!")
}

Secret Phase

到此为止,炸弹实验的6个基本阶段已经通过了,但是细心的小伙伴发现了,这个实验附带的文件还有一个bomb.c文件,打开后发现里面的一些注释:

/***************************************************************************
 * Dr. Evil's Insidious Bomb, Version 1.1
 * Copyright 2011, Dr. Evil Incorporated. All rights reserved.
 *
 * LICENSE:
 *
 * Dr. Evil Incorporated (the PERPETRATOR) hereby grants you (the
 * VICTIM) explicit permission to use this bomb (the BOMB).  This is a
 * time limited license, which expires on the death of the VICTIM.
 * The PERPETRATOR takes no responsibility for damage, frustration,
 * insanity, bug-eyes, carpal-tunnel syndrome, loss of sleep, or other
 * harm to the VICTIM.  Unless the PERPETRATOR wants to take credit,
 * that is.  The VICTIM may not distribute this bomb source code to
 * any enemies of the PERPETRATOR.  No VICTIM may debug,
 * reverse-engineer, run "strings" on, decompile, decrypt, or use any
 * other technique to gain knowledge of and defuse the BOMB.  BOMB
 * proof clothing may not be worn when handling this program.  The
 * PERPETRATOR will not apologize for the PERPETRATOR's poor sense of
 * humor.  This license is null and void where the BOMB is prohibited
 * by law.
 ***************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include "support.h"
#include "phases.h"

/* 
 * Note to self: Remember to erase this file so my victims will have no
 * idea what is going on, and so they will all blow up in a
 * spectaculary fiendish explosion. -- Dr. Evil 
 */

FILE *infile;

int main(int argc, char *argv[])
{
    char *input;

    /* Note to self: remember to port this bomb to Windows and put a 
     * fantastic GUI on it. */

    /* When run with no arguments, the bomb reads its input lines 
     * from standard input. */
    if (argc == 1) {  
	infile = stdin;
    } 

    /* When run with one argument <file>, the bomb reads from <file> 
     * until EOF, and then switches to standard input. Thus, as you 
     * defuse each phase, you can add its defusing string to <file> and
     * avoid having to retype it. */
    else if (argc == 2) {
	if (!(infile = fopen(argv[1], "r"))) {
	    printf("%s: Error: Couldn't open %s\n", argv[0], argv[1]);
	    exit(8);
	}
    }

    /* You can't call the bomb with more than 1 command line argument. */
    else {
	printf("Usage: %s [<input_file>]\n", argv[0]);
	exit(8);
    }

    /* 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!
				      * Let me know how they did it. */
    printf("Phase 1 defused. How about the next one?\n");

    /* The second phase is harder.  No one will ever figure out
     * how to defuse this... */
    input = read_line();
    phase_2(input);
    phase_defused();
    printf("That's number 2.  Keep going!\n");

    /* I guess this is too easy so far.  Some more complex code will
     * confuse people. */
    input = read_line();
    phase_3(input);
    phase_defused();
    printf("Halfway there!\n");

    /* Oh yeah?  Well, how good is your math?  Try on this saucy problem! */
    input = read_line();
    phase_4(input);
    phase_defused();
    printf("So you got that one.  Try this one.\n");
    
    /* Round and 'round in memory we go, where we stop, the bomb blows! */
    input = read_line();
    phase_5(input);
    phase_defused();
    printf("Good work!  On to the next...\n");

    /* This phase will never be used, since no one will get past the
     * earlier ones.  But just in case, make this one extra hard. */
    input = read_line();
    phase_6(input);
    phase_defused();

    /* Wow, they got it!  But isn't something... missing?  Perhaps
     * something they overlooked?  Mua ha ha ha ha! */
    
    return 0;
}

注意到最后一个注释:"Wow, they got it! But isn’t something… missing? Perhaps something they overlooked? Mua ha ha ha ha! "。

实际上,隐藏关卡的激活在于一个我们没有注意过的函数里,phase_defused

(gdb) disas phase_defused
Dump of assembler code for function 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
   0x00000000004015e6 <+34>:	lea    0xc(%rsp),%rcx
   0x00000000004015eb <+39>:	lea    0x8(%rsp),%rdx
   0x00000000004015f0 <+44>:	mov    $0x402619,%esi
   0x00000000004015f5 <+49>:	mov    $0x603870,%edi
   0x00000000004015fa <+54>:	callq  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>:	callq  0x401338 <strings_not_equal>
   0x0000000000401613 <+79>:	test   %eax,%eax
   0x0000000000401615 <+81>:	jne    0x401635 <phase_defused+113>
   0x0000000000401617 <+83>:	mov    $0x4024f8,%edi
   0x000000000040161c <+88>:	callq  0x400b10 <puts@plt>
   0x0000000000401621 <+93>:	mov    $0x402520,%edi
   0x0000000000401626 <+98>:	callq  0x400b10 <puts@plt>
   0x000000000040162b <+103>:	mov    $0x0,%eax
   0x0000000000401630 <+108>:	callq  0x401242 <secret_phase>
   0x0000000000401635 <+113>:	mov    $0x402558,%edi
   0x000000000040163a <+118>:	callq  0x400b10 <puts@plt>
   0x000000000040163f <+123>:	mov    0x68(%rsp),%rax
   0x0000000000401644 <+128>:	xor    %fs:0x28,%rax
   0x000000000040164d <+137>:	je     0x401654 <phase_defused+144>
   0x000000000040164f <+139>:	callq  0x400b30 <__stack_chk_fail@plt>
   0x0000000000401654 <+144>:	add    $0x78,%rsp
   0x0000000000401658 <+148>:	retq   
End of assembler dump.

这一段代码也是比较长的,重点在于中间的一段代码:

   0x00000000004015e1 <+29>:	lea    0x10(%rsp),%r8
   0x00000000004015e6 <+34>:	lea    0xc(%rsp),%rcx
   0x00000000004015eb <+39>:	lea    0x8(%rsp),%rdx
   0x00000000004015f0 <+44>:	mov    $0x402619,%esi
   0x00000000004015f5 <+49>:	mov    $0x603870,%edi
   0x00000000004015fa <+54>:	callq  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>:	callq  0x401338 <strings_not_equal>
   0x0000000000401613 <+79>:	test   %eax,%eax
   0x0000000000401615 <+81>:	jne    0x401635 <phase_defused+113>
   0x0000000000401617 <+83>:	mov    $0x4024f8,%edi
   0x000000000040161c <+88>:	callq  0x400b10 <puts@plt>
   0x0000000000401621 <+93>:	mov    $0x402520,%edi
   0x0000000000401626 <+98>:	callq  0x400b10 <puts@plt>
   0x000000000040162b <+103>:	mov    $0x0,%eax
   0x0000000000401630 <+108>:	callq  0x401242 <secret_phase>

这段代码的上下文,只有在第6关卡通过后,才会执行这段代码段,先调用了 sscanf 函数,提取的内容是 0x603870 的字符串,模式串为 0x402619 位置的字符串,输入可得:

(gdb) x/s 0x402619
0x402619:	"%d %d %s"

说明输入的模式为两个整数和一个字符串。

接着调用函数strings_not_equal比较输入的字符串是否和地址0x402622的字符串是相等的。

输出其内容:

(gdb) x/s 0x402622
0x402622:	"DrEvil"

那么问题来了,我们应该在哪一个关卡输入这个附带信息呢?

显然只有phase 4和这里调用了sscanf函数,它们存放的地址实际上是一样的。

所以我们在第4关卡多输入一个字符串 "DrEvil"即可进入隐藏关卡。

进入隐藏关

(gdb) disas secret_phase
Dump of assembler code for function secret_phase:
   0x0000000000401242 <+0>:		push   %rbx
   0x0000000000401243 <+1>:		callq  0x40149e <read_line>
   0x0000000000401248 <+6>:		mov    $0xa,%edx
   0x000000000040124d <+11>:	mov    $0x0,%esi
   0x0000000000401252 <+16>:	mov    %rax,%rdi
   0x0000000000401255 <+19>:	callq  0x400bd0 <strtol@plt>
   0x000000000040125a <+24>:	mov    %rax,%rbx
   0x000000000040125d <+27>:	lea    -0x1(%rax),%eax
   0x0000000000401260 <+30>:	cmp    $0x3e8,%eax
   0x0000000000401265 <+35>:	jbe    0x40126c <secret_phase+42>
   0x0000000000401267 <+37>:	callq  0x40143a <explode_bomb>
   0x000000000040126c <+42>:	mov    %ebx,%esi
   0x000000000040126e <+44>:	mov    $0x6030f0,%edi
   0x0000000000401273 <+49>:	callq  0x401204 <fun7>
   0x0000000000401278 <+54>:	cmp    $0x2,%eax
   0x000000000040127b <+57>:	je     0x401282 <secret_phase+64>
   0x000000000040127d <+59>:	callq  0x40143a <explode_bomb>
   0x0000000000401282 <+64>:	mov    $0x402438,%edi
   0x0000000000401287 <+69>:	callq  0x400b10 <puts@plt>
   0x000000000040128c <+74>:	callq  0x4015c4 <phase_defused>
   0x0000000000401291 <+79>:	pop    %rbx
   0x0000000000401292 <+80>:	retq   
End of assembler dump.

第一部分:

   0x0000000000401243 <+1>:		callq  0x40149e <read_line>
   0x0000000000401248 <+6>:		mov    $0xa,%edx
   0x000000000040124d <+11>:	mov    $0x0,%esi
   0x0000000000401252 <+16>:	mov    %rax,%rdi
   0x0000000000401255 <+19>:	callq  0x400bd0 <strtol@plt>
   0x000000000040125a <+24>:	mov    %rax,%rbx
   0x000000000040125d <+27>:	lea    -0x1(%rax),%eax
   0x0000000000401260 <+30>:	cmp    $0x3e8,%eax
   0x0000000000401265 <+35>:	jbe    0x40126c <secret_phase+42>
   0x0000000000401267 <+37>:	callq  0x40143a <explode_bomb>

第1行就是读取我们输入的字符串内容,接下来就是调用一个string to l的一个转换函数,将我们输入的内容转换为int型,这意味着输入的内容为一个整数。

接着判断这个返回值是否大于0x3e9,如果是,炸弹被引爆,因此,这里限定了一个输入的范围不得大于 0x3e9

第二部分:

   0x000000000040126c <+42>:	mov    %ebx,%esi
   0x000000000040126e <+44>:	mov    $0x6030f0,%edi
   0x0000000000401273 <+49>:	callq  0x401204 <fun7>
   0x0000000000401278 <+54>:	cmp    $0x2,%eax
   0x000000000040127b <+57>:	je     0x401282 <secret_phase+64>
   0x000000000040127d <+59>:	callq  0x40143a <explode_bomb>
   0x0000000000401282 <+64>:	mov    $0x402438,%edi

这里调用了一个fun7函数,在此之前,准备了两个参数,一个是0x6030f0一个地址,以及我们输入的整数。

这里我们可以查看一下地址 0x6030f0 的内容:

0x6030f0 <n1>:	0x0000000000000024	0x0000000000603110
0x603100 <n1+16>:	0x0000000000603130	0x0000000000000000
0x603110 <n21>:	0x0000000000000008	0x0000000000603190
0x603120 <n21+16>:	0x0000000000603150	0x0000000000000000
0x603130 <n22>:	0x0000000000000032	0x0000000000603170
0x603140 <n22+16>:	0x00000000006031b0	0x0000000000000000
0x603150 <n32>:	0x0000000000000016	0x0000000000603270
0x603160 <n32+16>:	0x0000000000603230	0x0000000000000000
0x603170 <n33>:	0x000000000000002d	0x00000000006031d0
0x603180 <n33+16>:	0x0000000000603290	0x0000000000000000
0x603190 <n31>:	0x0000000000000006	0x00000000006031f0
0x6031a0 <n31+16>:	0x0000000000603250	0x0000000000000000
0x6031b0 <n34>:	0x000000000000006b	0x0000000000603210
0x6031c0 <n34+16>:	0x00000000006032b0	0x0000000000000000
0x6031d0 <n45>:	0x0000000000000028	0x0000000000000000
0x6031e0 <n45+16>:	0x0000000000000000	0x0000000000000000
0x6031f0 <n41>:	0x0000000000000001	0x0000000000000000
0x603200 <n41+16>:	0x0000000000000000	0x0000000000000000
0x603210 <n47>:	0x0000000000000063	0x0000000000000000
0x603220 <n47+16>:	0x0000000000000000	0x0000000000000000
0x603230 <n44>:	0x0000000000000023	0x0000000000000000
0x603240 <n44+16>:	0x0000000000000000	0x0000000000000000
0x603250 <n42>:	0x0000000000000007	0x0000000000000000
0x603260 <n42+16>:	0x0000000000000000	0x0000000000000000
0x603270 <n43>:	0x0000000000000014	0x0000000000000000
0x603280 <n43+16>:	0x0000000000000000	0x0000000000000000
0x603290 <n46>:	0x000000000000002f	0x0000000000000000
0x6032a0 <n46+16>:	0x0000000000000000	0x0000000000000000
0x6032b0 <n48>:	0x00000000000003e9	0x0000000000000000
0x6032c0 <n48+16>:	0x0000000000000000	0x0000000000000000

仔细观察可知,这个地址对应了一棵二叉树,树的节点定义为:

typedef struct {
    int val;
    struct node* left;
    struct node* right;
}node;

下图为这棵二叉树:

n1 0x24
n21 0x8
n22 0x32
n31 0x6
n32 0x16
n33 0x2d
n34 0x6b
n41 0x1
n42 0x7
n43 0x14
n44 0x23
n45 0x28
n46 0x2f
n47 0x63
n48 0x3e9

其实这颗树是一个二叉搜索树。

接下来看函数fun7的细节:

(gdb) disas fun7
Dump of assembler code for function fun7:
   0x0000000000401204 <+0>:		sub    $0x8,%rsp
   0x0000000000401208 <+4>:		test   %rdi,%rdi
   0x000000000040120b <+7>:		je     0x401238 <fun7+52>
   0x000000000040120d <+9>:		mov    (%rdi),%edx
   0x000000000040120f <+11>:	cmp    %esi,%edx
   0x0000000000401211 <+13>:	jle    0x401220 <fun7+28>
   0x0000000000401213 <+15>:	mov    0x8(%rdi),%rdi
   0x0000000000401217 <+19>:	callq  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>
   0x0000000000401229 <+37>:	mov    0x10(%rdi),%rdi
   0x000000000040122d <+41>:	callq  0x401204 <fun7>
   0x0000000000401232 <+46>:	lea    0x1(%rax,%rax,1),%eax
   0x0000000000401236 <+50>:	jmp    0x40123d <fun7+57>
   0x0000000000401238 <+52>:	mov    $0xffffffff,%eax
   0x000000000040123d <+57>:	add    $0x8,%rsp
   0x0000000000401241 <+61>:	retq   
End of assembler dump.

类似于第4关,用到了递归,实际上,它也是一个二分查找,在一棵二叉搜索树上查找目标值。

对应的C语言代码为:

int fun7(node* root, int val) {
    if (root == nullptr) { return -1; }
    if (root->val > val) {
        return 2 * fun7(root->left, val);
    } else if (root->val < val) {
        return 2 * fun7(root->right, val) + 1;
    } else if (root->val == val) {
        return 0;
    }
}

结合secret phase函数要求,fun7的返回值应该是2,所以我们在二叉搜索树寻找一个节点值,让其作为目标值调用结果为2,只有一个节点满足就是:n32,对应的值为:0x16=22,这就是隐藏关卡的答案。

将secret phase翻译为C语言:

void secret_phase(char* input_string) {
    // 转换为整数
    int val = strol(input_string);
    if (fun7(val) != 2) {
        exlode_bomb();
        exit(-1);
    }
    printf("Wow! You've defused the secret stage!");
}

总结

  • phase 1只有简单if分支语句
  • phase 2只有简单的循环语句
  • phase 3考察了switch语句
  • phase 4有一个递归函数的调用
  • phase 5通过输入的字符串作为偏移,在给定的字符串中构造出”flyers“字符串
  • phase 6最复杂,包含多个循环语句,有简单循环,还有二重循环,还有了链表结构,链表重组等等操作,包含的内容很多,逆向工程最为困难
  • secret phase是phase 4的一个变体,同样是一个二分查找,只不过这里是在一棵以二叉链表为结构的二叉搜索树上进行二分搜索

写在最后:以上的内容都是自己手敲的,所有的分析思路也是自己想的,仅供参考,看到这里的小伙伴不妨点个赞再走吧,希望能够帮到你。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值