0%

BombLab

Bomb Lab

操作系统:linux

调试工具:gdb

Link:CS:APP3e

文章分为实验前准备复习拆炸弹三个模块。读者可以自取所需。

实验前准备

实验由phase_1 ~ phase_6组成,共6个阶段,
我们需要分别输入6个字符串密码,以便解除Dr. Evil的炸弹,
但只要有1个字符串输错,炸弹就会爆炸

那么如何知道6个正确的字符串呢?
我们需要分别查看phase_1 ~ phase_6的汇编代码,
而炸弹就存放在其中,我们要根据汇编代码小心地避开炸弹,分析出正确的字符串即可。
而分析需要用到学习的汇编知识常用的gdb指令以及linux终端指令
需要用到的linux终端指令
别忘了先进入文件地址中

  • objdump -d bomb > assembly.s
    反编译出bomb.c的汇编代码,并存放在assembly.s中(assembly.s是自己命名的)

  • gdb 文件名
    用gdb调试工具打开某个文件
    eg. gdb bomb 用gdb调试工具打开bomb.c文件
    我们就是要用gdb调试工具打开bomb.c文件后做实验

常用gdb指令:
gdb指令必须用gdb调试工具打开文件后才能调用

  • run (可简写为:r
    运行文件
  • quit (可简写为:q
    停止运行,跳出gdb调试工具
  • break 函数名 (可简写为:b 函数名
    在某个函数处打断点,以便之后用disas展示这个函数的汇编代码。
    eg. b phase_1 在phase_1函数打断点
  • disas 函数名
    展示这个函数的汇编代码(但必须先打断点,因为bomb文件已经运行了)
    eg. disas phase_1 展示phase_1的汇编代码
  • x/NFU address or register
    用指定格式(FU)、指定数量(N)输出地址或者寄存器对应内容
    eg1. x/s 0x40139b 输出0x40139b对应内容
    eg2. x/s $rsi 输出%rsi对应内容
    eg3.x/4xw 0x6032d0 按24个十六进制4字节格式输出0x6032d0对应内容
  • p/F *address or *register
    用指定格式(F)打印地址或者寄存器对应内容(效果上,x/F = p/F
    eg1. p/x *(0x6032f0) 按十六进制打印0x6032f0对应内容
  • 格式F、单位U都与c语言类似,参考链接gdb:Format、Variables and Memory部分

复习

  • 测试和比较指令

    • ZF、OF、SF、CF条件码
      zero flag 零标志位;overflow flag 溢出标志位;sign flag 符号标志位;carry flag 进位标志位。
    • test S1,S2S1 & S2
      测试指令,进行位级与操作,若结果为0,返回ZF = 1;若结果非0,返回ZF = 0,但不会改变两个数的值。同时,需要特殊注意的是,可以对同一个寄存器使用test指令,例如test %eax,%eax
    • cmp S1,S2S2 - S1
      比较指令,进行减法操作,但不会改变两个数的值
  • 部分跳转指令

    • 不区分unsiged(无符号)、siged(有符号)跳转

      • jne
        根据ZF的值判断是否跳转,当ZF = 0才跳转。
      • je
        根据ZF的值判断是否跳转,当ZF = 1才跳转。
    • siged jump 指令

      • jlejump<=
        j:jump;l:less(小于);e:equal
      • jgejump<=
        j:jump;g:great(大于);e:equal
      • jgjump>
        j:jump;g:great(大于)
    • unsiged jump 指令

      • jbjump<
        j:jump;b:below(小于)
      • jbejump<=
        j:jump;b:below(小于);e:equal
      • jajump>
        j:jump;a:above(大于)
    • cmp与signed jump、unsigned jump的联系(原理与条件码有关,在此不解释了)

      • cmp S1,S2伴随signed jump指令时,S1置前,S2置后,例如:

        1
        2
        0x0000000000400f60 <+29>:	cmp    $0x1,%eax #若 1 > %eax,则不会爆炸
        0x0000000000400f63 <+32>: jg 0x400f6a <phase_3+39> #大于跳转
      • cmp S1,S2与伴随unsigned jump指令时,S2置前,S1置后,例如:

        1
        2
        0x0000000000400f6a <+39>:	cmpl   $0x7,0x8(%rsp) #若0x8(%rsp) > 7,则爆炸
        0x0000000000400f6f <+44>: ja 0x400fad <phase_3+106> #大于跳转
  • 部分二元操作

    • lea S,D (即D <-- S,等价于赋值算式D = S
      不是读取数据,是加载有效地址但和mov指令效果相同,常用于执行简洁的算术操作
    • sub S,D (即D <-- D - S,等价于赋值算式D -= S
      减法,D - S后赋值给D。
    • sar S,D (即D <-- D >> S,等价于赋值算式D >>= S
      算术右移,补符号位(sign),D >> S后赋值给D。注意:sar D,则默认是算术右移1
    • shr S,D (即D <-- D >> S,等价于赋值算式D >>= S
      **算术右移,补0**,D >> S后赋值给D。
  • 寄存器为空时,其值为0

  • %rdi
    系统往往要把一个值赋给%rdi后,才能当作入参(即输入的参数)

  • register
    注意:汇编代码为了优化常常使用同一个系列的register表示同一个变量
    例如:

    1
    2
    mov  $0x2,%rax
    sub $0x1,%eax #此时,%eax = %rax = 2,该行执行%eax -= 1即%eax = 2 - 1
    register(64bits) 作用
    %rax 存放返回值
    %rbx 被调用者保存(存放被调用者的地址,可以是函数地址,也可以是其它地址,callee save)
    %rcx 第4个参数(存储参数,可以是数字字符参数,也可以是地址参数
    %rdx 第3个参数
    %rsi 第2个参数
    %rdi 第1个参数(经常作入参使用)
    %rbp 被调用者保存
    %rsp 栈指针(用以开辟栈空间,注意入参的先进后出
    %r8 第5个参数
    %r9 第6个参数
    %10 调用者保存(存放调用者函数的地址,可以是函数地址,也可以是其它地址,caller save)
    %r11 调用者保存
    %r12 被调用者保存
    %r13 被调用者保存
    %r14 被调用者保存
    %r15 被调用者保存
  • 优化
    有些看似毫无作用的汇编代码,其实是在做优化

拆炸弹

正式拆炸弹之前需要注意到一点,我们输入的字符串都是放入到%rdi当中的。
(其实一般情况下,输入也是放到%rdi的)
也就是说,在每个phase调用时,默认%rdi这个寄存器存放着输入的字符串。
如何得到这点的呢?是通过分析main函数的汇编代码得出的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# main part 146 ~ 310
0x0000000000400e32 <+146>: callq 0x40149e <read_line>
0x0000000000400e37 <+151>: mov %rax,%rdi # input store %rdi
0x0000000000400e3a <+154>: callq 0x400ee0 <phase_1>
0x0000000000400e3f <+159>: callq 0x4015c4 <phase_defused>
0x0000000000400e44 <+164>: mov $0x4023a8,%edi
0x0000000000400e49 <+169>: callq 0x400b10 <puts@plt>
0x0000000000400e4e <+174>: callq 0x40149e <read_line>
0x0000000000400e53 <+179>: mov %rax,%rdi # input store %rdi
0x0000000000400e56 <+182>: callq 0x400efc <phase_2>
0x0000000000400e5b <+187>: callq 0x4015c4 <phase_defused>
0x0000000000400e60 <+192>: mov $0x4022ed,%edi
0x0000000000400e65 <+197>: callq 0x400b10 <puts@plt>
0x0000000000400e6a <+202>: callq 0x40149e <read_line>
0x0000000000400e6f <+207>: mov %rax,%rdi # input store %rdi
0x0000000000400e72 <+210>: callq 0x400f43 <phase_3>
0x0000000000400e77 <+215>: callq 0x4015c4 <phase_defused>
0x0000000000400e7c <+220>: mov $0x40230b,%edi
0x0000000000400e81 <+225>: callq 0x400b10 <puts@plt>
0x0000000000400e86 <+230>: callq 0x40149e <read_line>
0x0000000000400e8b <+235>: mov %rax,%rdi # input store %rdi
0x0000000000400e8e <+238>: callq 0x40100c <phase_4>
0x0000000000400e93 <+243>: callq 0x4015c4 <phase_defused>
0x0000000000400e98 <+248>: mov $0x4023d8,%edi
0x0000000000400e9d <+253>: callq 0x400b10 <puts@plt>
0x0000000000400ea2 <+258>: callq 0x40149e <read_line>
0x0000000000400ea7 <+263>: mov %rax,%rdi # input store %rdi
0x0000000000400eaa <+266>: callq 0x401062 <phase_5>
0x0000000000400eaf <+271>: callq 0x4015c4 <phase_defused>
0x0000000000400eb4 <+276>: mov $0x40231a,%edi
0x0000000000400eb9 <+281>: callq 0x400b10 <puts@plt>
0x0000000000400ebe <+286>: callq 0x40149e <read_line>
0x0000000000400ec3 <+291>: mov %rax,%rdi # input store %rdi
0x0000000000400ec6 <+294>: callq 0x4010f4 <phase_6>
0x0000000000400ecb <+299>: callq 0x4015c4 <phase_defused>
0x0000000000400ed0 <+304>: mov $0x0,%eax
0x0000000000400ed5 <+309>: pop %rbx
0x0000000000400ed6 <+310>: retq

分析:
我们可以看到,在每个phase调用前,都callq 0x40149e <read_line>,然后mov %rax,%rdi,这样子会将我们输入的字符串最终存到%rdi中。(我们是把字符串输入到read_line的,而read_line会返回到%rax中,mov操作将%rax存到%rdi注意%rax往往用来放返回值)

至于read_line的汇编代码我就不分析了,通过名字也可以看出:它是用来返回我们的输入值的

下面开始分析phase_1 ~ phase_6的汇编代码,
得出认为正确的字符串,并逐个解除炸弹

phase_1

查看phase_1的汇编代码:

1
2
3
4
5
6
7
8
0x0000000000400ee0 <+0>:  sub    $0x8,%rsp #开辟8个字节的栈空间
0x0000000000400ee4 <+4>: mov $0x402400,%esi #将立即数$0x402400赋值给%esi
0x0000000000400ee9 <+9>: callq 0x401338 <strings_not_equal> #调用函数strings_not_equal
0x0000000000400eee <+14>: test %eax,%eax #与操作(test),若为eax为0则跳转到phase_1+23;若为eax非0则顺序执行,调用explode_bomb,炸弹爆炸
0x0000000000400ef0 <+16>: je 0x400ef7 <phase_1+23>
0x0000000000400ef2 <+18>: callq 0x40143a <explode_bomb>
0x0000000000400ef7 <+23>: add $0x8,%rsp #释放8个字节的栈空间
0x0000000000400efb <+27>: retq #返回

分析:
重点就是<+14>的test指令,可以看出当%eax为0时才不会爆炸。也就是说函数strings_not_equal的返回值必须为0才行,根据名字我们可以猜测,字符串不相等时返回1,字符串相等时返回0。下面查看string_not_equal的汇编代码验证我们的猜测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#string_not_equal
0x0000000000401338 <+0>: push %r12 #将%r12入栈
0x000000000040133a <+2>: push %rbp #将%rbp入栈
0x000000000040133b <+3>: push %rbx #将%rbx入栈
0x000000000040133c <+4>: mov %rdi,%rbx #将%rdi赋给%rbx,不妨命名为第一参数指针,对应我们输入字符串的地址
0x000000000040133f <+7>: mov %rsi,%rbp #将%rsi赋给%rbp,不妨命名为第二参数指针,对应地址是$0x402400
0x0000000000401342 <+10>: callq 0x40131b <string_length> #调用函数string_length,系统默认%rdi为入参,那么,就会计算第一参数的字符串的长度
0x0000000000401347 <+15>: mov %eax,%r12d #将第一参数的字符串长度返回值保存到%r12d
0x000000000040134a <+18>: mov %rbp,%rdi #把第二参数指针赋给%rdi,作为string_length的入参
0x000000000040134d <+21>: callq 0x40131b <string_length> #计算第二参数的字符串的长度
0x0000000000401352 <+26>: mov $0x1,%edx #将1赋值给%edx,作为strings_not_equal的可能返回结果
0x0000000000401357 <+31>: cmp %eax,%r12d #比较第一、第二参数的字符串的长度
0x000000000040135a <+34>: jne 0x40139b <strings_not_equal+99> #长度不相等时,跳转到<+99>,最终返回1,炸弹爆炸
0x000000000040135c <+36>: movzbl (%rbx),%eax #长度相等时,将第一参数的第一个字符串赋给%eax
0x000000000040135f <+39>: test %al,%al #判断%al是否为空即0
0x0000000000401361 <+41>: je 0x401388 <strings_not_equal+80> #若%al为空,则跳转到<+80>,最终返回0,炸弹不会爆炸,这段我在优化,会顺序执行
0x0000000000401363 <+43>: cmp 0x0(%rbp),%al #取%rbp的第一个字符,和%al第一个字符做比较;此时(%rbp)对应$0x402400的第一个字符,%al是对应我们输入的字符串的第一个字符
0x0000000000401366 <+46>: je 0x401372 <strings_not_equal+58> #若相等跳转到<+58>
0x0000000000401368 <+48>: jmp 0x40138f <strings_not_equal+87> #若不相等跳转到<+87>,最终返回1,炸弹爆炸
0x000000000040136a <+50>: cmp 0x0(%rbp),%al #比较第一、第二参数的字符
0x000000000040136d <+53>: nopl (%rax) #空操作,为了扩展%eax
0x0000000000401370 <+56>: jne 0x401396 <strings_not_equal+94> #不相等跳转到<+94>,最终返回1,炸弹爆炸
0x0000000000401372 <+58>: add $0x1,%rbx #第一参数指针+1
0x0000000000401376 <+62>: add $0x1,%rbp #第二参数指针+1
0x000000000040137a <+66>: movzbl (%rbx),%eax #零扩展%eax,并将第一参数的字符赋给%eax
0x000000000040137d <+69>: test %al,%al #判断此时的%al是否为空,若空,则顺序执行,跳出循环,最终返回0,炸弹不会爆炸
0x000000000040137f <+71>: jne 0x40136a <strings_not_equal+50> #若不空,则继续比对字符
0x0000000000401381 <+73>: mov $0x0,%edx
0x0000000000401386 <+78>: jmp 0x40139b <strings_not_equal+99>
0x0000000000401388 <+80>: mov $0x0,%edx
0x000000000040138d <+85>: jmp 0x40139b <strings_not_equal+99>
0x000000000040138f <+87>: mov $0x1,%edx
0x0000000000401394 <+92>: jmp 0x40139b <strings_not_equal+99>
0x0000000000401396 <+94>: mov $0x1,%edx
0x000000000040139b <+99>: mov %edx,%eax #将%eax赋值为 01,准备返回
0x000000000040139d <+101>: pop %rbx #出栈
0x000000000040139e <+102>: pop %rbp #出栈
0x000000000040139f <+103>: pop %r12 #出栈
0x00000000004013a1 <+105>: retq #返回

分析:
通过查看string_not_equal函数的汇编代码,可以找到,string_not_equal函数先是比较了输入字符串和地址0x402400对应的字符长度,再逐个比较字符,最终,字符相同返回0,字符不同返回1。我们可以看到,汇编进行不少优化。例如,如果第一个字符已经是不同的,就不用循环进行更多的操作。

key:由此可以知道,只要我们输入的字符和地址0x402400对应的字符相同即可。那么,我们通过gdb指令x/s 0x402400即可访问0x402400对应的字符。代码如下:

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

那么,只要在运行界面输入Border relations with Canada have never been better.即可。(译为:与加拿大的边境关系从来没有这么好过。

1
2
3
//输入前
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
1
2
3
4
5
//after input
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_2

我们先查看phase_2的汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
0x0000000000400efc <+0>:	push   %rbp
0x0000000000400efd <+1>: push %rbx
0x0000000000400efe <+2>: sub $0x28,%rsp
0x0000000000400f02 <+6>: mov %rsp,%rsi #将%rsp赋给%rsi
0x0000000000400f05 <+9>: callq 0x40145c <read_six_numbers>
0x0000000000400f0a <+14>: cmpl $0x1,(%rsp) #(%rsp)必须是$0x1,否则爆炸
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

callq 0x40145c <read_six_numbers>,我们查看这个函数的汇编代码:
(根据名字,我们猜测其作用是读取6个数字)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
0x0000000000401480 <+36>: mov $0x4025c3,%esi
0x0000000000401485 <+41>: mov $0x0,%eax
0x000000000040148a <+46>: callq 0x400bf0 <__isoc99_sscanf@plt>
0x000000000040148f <+51>: cmp $0x5,%eax #即cmp $0x5,$0x0,用以跳过炸弹
0x0000000000401492 <+54>: jg 0x401499 <read_six_numbers+61>
0x0000000000401494 <+56>: callq 0x40143a <explode_bomb>
0x0000000000401499 <+61>: add $0x18,%rsp
0x000000000040149d <+65>: retq

分析:
注意到phase_2<+6>已经将%rsp赋给%rsi,那么,对%rsi进行操作也就是之后对phase_2中的%rsp操作。
以下表格是对%rsi操作的展示:

%rsi 0x4(%rsi) 0x8(%rsi) 0xc(%rsi) 0x10(%rsi) 0x14(%rsi)
%rdx %rcx %r8 %r9 (%rsp) 0x8(%rsp)

注意,0x10 = 16D0x14 = 20D。十进制后缀D,Decimal.

可以看到,这样操作传递了6个地址参数,其地址间隔为4,最后两个因为参数寄存器不够用了,只能用栈帧%rsp。(注意,此处的%rsp是read_six_numbers自己开辟的,原先phase_2的%rsp已经中断
<+36>处,mov $0x4025c3,%esi,访问0x4025c3内容:

1
2
(gdb) x/s 0x4025c3
0x4025c3: "%d %d %d %d %d %d"

可以知道%eax中存放着格式字符串
<+46>处,callq 0x400bf0 <__isoc99_sscanf@plt>,函数sscanf是c语言自带的函数,同scanf类似,作用是读取输入的字符串,并根据指定格式存储到指定地址。这告诉了我们的输入的字符串,应该是6个整型数字,而且之间用空格隔开。同时,它们存放在上述的6个参数地址中,也就是之后phase_2的%rsp按4递增地址的栈帧中。符合我们的猜测。

我们回到phase_2继续分析: 可以知道第一个参数数值(%rsp)必须是1,之后会进入一个循环。我们把这个循环单独拿出来看:

1
2
3
4
5
6
7
8
9
10
11
12
0x0000000000400f17 <+27>:	mov    -0x4(%rbx),%eax
0x0000000000400f1a <+30>: add %eax,%eax #此时的%eax是<+27>处的%eax的两倍
0x0000000000400f1c <+32>: cmp %eax,(%rbx) #%eax与(%rbx)须相等,否则爆炸
0x0000000000400f1e <+34>: je 0x400f25 <phase_2+41>
0x0000000000400f20 <+36>: callq 0x40143a <explode_bomb>
0x0000000000400f25 <+41>: add $0x4,%rbx #%rbx地址+4
0x0000000000400f29 <+45>: cmp %rbp,%rbx #%rbp与%rbx相等时,即都为空时,跳出循环
0x0000000000400f2c <+48>: jne 0x400f17 <phase_2+27>
0x0000000000400f2e <+50>: jmp 0x400f3c <phase_2+64>
0x0000000000400f30 <+52>: lea 0x4(%rsp),%rbx #用%rbx替代0x4(%rsp)
0x0000000000400f35 <+57>: lea 0x18(%rsp),%rbp #将%rbp置空
0x0000000000400f3a <+62>: jmp 0x400f17 <phase_2+27>

我们已经知道(%rsp) = 1,那么第一次循环中,<+32>处的%eax = 2,故只有(%rbx) = 0x4(%rsp) = 2,才不爆炸;第二次循环中,<+32>处的%eax = 4,故(%rbx) = 0x8(%rsp) = 4;第三次循环中,<+32>处的%eax = 8,故(%rbx) = 0xc(%rsp) = 8,可以看出是以1为首项,后一项是前一项两倍的递增数列,共6项,以此类推,有下表。

%rsi 0x4(%rsi) 0x8(%rsi) 0xc(%rsi) 0x10(%rsi) 0x14(%rsi)
%rdx %rcx %r8 %r9 (%rsp) 0x8(%rsp)
(%rsp) 0x4(%rsp) 0x8(%rsp) 0xc(%rsp) 0x10(%rsp) 0x14(%rsp)
1 2 4 8 16 32

key: 由此得知,phase_2该输入的字符串为1 2 4 8 16 32

1
2
//输入前
Phase 1 defused. How about the next one?
1
2
3
4
//after input
Phase 1 defused. How about the next one?
1 2 4 8 16 32
That's number 2. Keep going!

phase_3

phase_3的汇编代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
0x0000000000400f43 <+0>:	sub    $0x18,%rsp
0x0000000000400f47 <+4>: lea 0xc(%rsp),%rcx #第二个参数,栈帧先进后出
0x0000000000400f4c <+9>: lea 0x8(%rsp),%rdx #第一个参数
0x0000000000400f51 <+14>: mov $0x4025cf,%esi #字符串格式"%d %d"
0x0000000000400f56 <+19>: mov $0x0,%eax
0x0000000000400f5b <+24>: callq 0x400bf0 <__isoc99_sscanf@plt>
0x0000000000400f60 <+29>: cmp $0x1,%eax #若 1 > %eax,则不会爆炸
0x0000000000400f63 <+32>: jg 0x400f6a <phase_3+39> #大于跳转
0x0000000000400f65 <+34>: callq 0x40143a <explode_bomb>
0x0000000000400f6a <+39>: cmpl $0x7,0x8(%rsp) #若0x8(%rsp) > 7,则爆炸
0x0000000000400f6f <+44>: ja 0x400fad <phase_3+106> #大于跳转
0x0000000000400f71 <+46>: mov 0x8(%rsp),%eax
0x0000000000400f75 <+50>: jmpq *0x402470(,%rax,8) #*0x402470处地址 + 8 * %eax
0x0000000000400f7c <+57>: mov $0xcf,%eax #将$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 #第二个参数必须和%eax相等
0x0000000000400fc2 <+127>: je 0x400fc9 <phase_3+134>
0x0000000000400fc4 <+129>: callq 0x40143a <explode_bomb>
0x0000000000400fc9 <+134>: add $0x18,%rsp
0x0000000000400fcd <+138>: retq

分析:
访问0x4025cf,可知字符串格式为"%d %d",从而得知输入字符串是两个整数。根据<+39>: cmpl $0x7,0x8(%rsp)<+44>: ja 0x400fad <phase_3+106>可以知道0x8(%rsp) <= 7。那么,第一个参数%rdx取值要小于等于7
根据<+50>: jmpq *0x402470(,%rax,8)可以知道,跳转的地址是指针*0x402470的变换,增量为8,此时为0x8(%rsp)即为%eax。
下面我们用x/s指令逐个访问,指针地址是递增的,但对应存放的phase_3的地址并不是递增的。到这时,答案已经呼之欲出了。
(由此可以知道指针*0x402470指向一个存放phase_3部分地址的字符串。一开始我想的是对phase_3的部分地址直接进行增8操作,没有想到是对指针*0x402470进行增8操作,从而访问不同的phase_3中的地址,耽误了许久)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(gdb) x/s *0x402470
0x400f7c <phase_3+57>: "\270", <incomplete sequence \317>
(gdb) x/s *(0x402470+8)
0x400fb9 <phase_3+118>: "\270\067\001"
(gdb) x/s *(0x402470+16)
0x400f83 <phase_3+64>: "\270\303\002"
(gdb) x/s *(0x402470+24)
0x400f8a <phase_3+71>: "\270"
(gdb) x/s *(0x402470+32)
0x400f91 <phase_3+78>: "\270\205\001"
(gdb) x/s *(0x402470+40)
0x400f98 <phase_3+85>: "\270", <incomplete sequence \316>
(gdb) x/s *(0x402470+48)
0x400f9f <phase_3+92>: "\270\252\002"
(gdb) x/s *(0x402470+56)
0x400fa6 <phase_3+99>: "\270G\001"

phase_3继续分析:
根据<+123>: cmp 0xc(%rsp),%eax<+127>: je 0x400fc9 <phase_3+134>知道,第二个参数0xc(%rsp)必须和%eax相同,此时的%eax根据0x8(%rsp)不同而不同

key:
整理上述分析可以知道,共有8组答案如下:

0x8(%rsp) 0 1 2 3
x/s *0x402470 *(~ +8) *(~ +16) *(~ +24)
<phase_3+n> +57 +118 +64 +71
0xc(%rsp) H 0xcf 0x137 0x2c3 0x100
0xc(%rsp) D 207 311 707 256
0x8(%rsp) 4 5 6 7
x/s *(~ +32) *(~ +40) *(~ +48) *(~ +56)
<phase_3+n> +78 +85 +92 +99
0xc(%rsp) H 0x185 0xce 0x2aa 0x147
0xc(%rsp) D 389 206 682 327

(十六进制后缀,H,hexadecimal;十进制后缀,D,Decimal)

我们选择第8组答案7 327作为输入:

1
2
//输入前
That's number 2. Keep going!
1
2
3
4
//after input
That's number 2. Keep going!
7 327
Halfway there!

phase_4

phase_4的汇编代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0x000000000040100c <+0>:	sub    $0x18,%rsp
0x0000000000401010 <+4>: lea 0xc(%rsp),%rcx #第二个参数,先进后出
0x0000000000401015 <+9>: lea 0x8(%rsp),%rdx #第一个参数,参一
0x000000000040101a <+14>: mov $0x4025cf,%esi #格式字符串"%d %d"
0x000000000040101f <+19>: mov $0x0,%eax
0x0000000000401024 <+24>: callq 0x400bf0 <__isoc99_sscanf@plt>
0x0000000000401029 <+29>: cmp $0x2,%eax #返回值%eax必须为2,否则爆炸
0x000000000040102c <+32>: jne 0x401035 <phase_4+41>
0x000000000040102e <+34>: cmpl $0xe,0x8(%rsp) #满足0x8(%rsp) <= 0xe,否则爆炸
0x0000000000401033 <+39>: jbe 0x40103a <phase_4+46> #小于等于
0x0000000000401035 <+41>: callq 0x40143a <explode_bomb>
0x000000000040103a <+46>: mov $0xe,%edx #作为func4的入参
0x000000000040103f <+51>: mov $0x0,%esi #作为func4的入参
0x0000000000401044 <+56>: mov 0x8(%rsp),%edi #将0x8(%rsp)赋给%edi,作为func4的入参
0x0000000000401048 <+60>: callq 0x400fce <func4>
0x000000000040104d <+65>: test %eax,%eax #%eax必须为空即0,否则爆炸
0x000000000040104f <+67>: jne 0x401058 <phase_4+76>
0x0000000000401051 <+69>: cmpl $0x0,0xc(%rsp) #0xc(%rsp)必须为0,否则爆炸
0x0000000000401056 <+74>: je 0x40105d <phase_4+81>
0x0000000000401058 <+76>: callq 0x40143a <explode_bomb>
0x000000000040105d <+81>: add $0x18,%rsp
0x0000000000401061 <+85>: retq

分析:仍旧是输入两个int参数,根据<+34>处可知,第一个参数0x8(%rsp)满足0xe > 0x8(%rsp),第二个参数0xc(%rsp)必须为0。但是第一个参数需要根据<func4>函数来确定。

<func4>的汇编代码如下:

第一次进入func4函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0x0000000000400fce <+0>:	sub    $0x8,%rsp
0x0000000000400fd2 <+4>: mov %edx,%eax # %eax=14
0x0000000000400fd4 <+6>: sub %esi,%eax # %eax=14-0=14
0x0000000000400fd6 <+8>: mov %eax,%ecx # %ecx=14
0x0000000000400fd8 <+10>: shr $0x1f,%ecx #逻辑右移31位,%ecx=0
0x0000000000400fdb <+13>: add %ecx,%eax # %eax=14
0x0000000000400fdd <+15>: sar %eax #算术右移1位,补sign,%eax=7
0x0000000000400fdf <+17>: lea (%rax,%rsi,1),%ecx %ecx=7+0=7
0x0000000000400fe2 <+20>: cmp %edi,%ecx #第一个参数和%ecx=7比较
0x0000000000400fe4 <+22>: jle 0x400ff2 <func4+36> #第一个参数 <=7跳至<+36>
0x0000000000400fe6 <+24>: lea -0x1(%rcx),%edx #否则%edx=7-1=6
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 #第一个参数和%ecx=7比较
0x0000000000400ff9 <+43>: jge 0x401007 <func4+57> #若第一个参数 >=7跳出func4
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

分析:其实这个时候答案已经出来了,只要第一个参数为7%ecx=7相同即可。但显然func是一个递归函数,我们进入递归看看,还有什么答案。

第一次递归:(记为R1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0x0000000000400fce <+0>:	sub    $0x8,%rsp
0x0000000000400fd2 <+4>: mov %edx,%eax # %eax=6
0x0000000000400fd4 <+6>: sub %esi,%eax # %eax=6-0=6
0x0000000000400fd6 <+8>: mov %eax,%ecx # %ecx=6
0x0000000000400fd8 <+10>: shr $0x1f,%ecx #逻辑右移31位,%ecx=0
0x0000000000400fdb <+13>: add %ecx,%eax # %eax=6
0x0000000000400fdd <+15>: sar %eax #算术右移1位,%eax=3
0x0000000000400fdf <+17>: lea (%rax,%rsi,1),%ecx %ecx=3+0=3
0x0000000000400fe2 <+20>: cmp %edi,%ecx #第一个参数和%ecx=3比较
0x0000000000400fe4 <+22>: jle 0x400ff2 <func4+36> #若第一个参数 <=3跳至<+36>
0x0000000000400fe6 <+24>: lea -0x1(%rcx),%edx #否则,%edx=3-1=2,进入递归
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 #第一个参数和%ecx=3比较
0x0000000000400ff9 <+43>: jge 0x401007 <func4+57> #若第一个参数 >=3跳出递归
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

第一个参数为3%ecx=3相同即可。

第二次递归:(记为R2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0x0000000000400fce <+0>:	sub    $0x8,%rsp
0x0000000000400fd2 <+4>: mov %edx,%eax # %eax=2
0x0000000000400fd4 <+6>: sub %esi,%eax # %eax=2-0=2
0x0000000000400fd6 <+8>: mov %eax,%ecx # %ecx=2
0x0000000000400fd8 <+10>: shr $0x1f,%ecx #逻辑右移31位,%ecx=0
0x0000000000400fdb <+13>: add %ecx,%eax # %eax=2
0x0000000000400fdd <+15>: sar %eax #算术右移1位,%eax=1
0x0000000000400fdf <+17>: lea (%rax,%rsi,1),%ecx %ecx=1+0=1
0x0000000000400fe2 <+20>: cmp %edi,%ecx #第一个参数和%ecx=1比较
0x0000000000400fe4 <+22>: jle 0x400ff2 <func4+36> #若第一个参数 <=1跳至<+36>
0x0000000000400fe6 <+24>: lea -0x1(%rcx),%edx #否则,%edx=1-1=0,进入递归
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 #第一个参数和%ecx=1比较
0x0000000000400ff9 <+43>: jge 0x401007 <func4+57> #若第一个参数 >=1跳出递归
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

第一个参数为1%ecx=1相同即可。

第三次递归:(记为R3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0x0000000000400fce <+0>:	sub    $0x8,%rsp
0x0000000000400fd2 <+4>: mov %edx,%eax # %eax=0
0x0000000000400fd4 <+6>: sub %esi,%eax # %eax=0-0=0
0x0000000000400fd6 <+8>: mov %eax,%ecx # %ecx=0
0x0000000000400fd8 <+10>: shr $0x1f,%ecx #逻辑右移31位,%ecx=0
0x0000000000400fdb <+13>: add %ecx,%eax # %eax=0
0x0000000000400fdd <+15>: sar %eax #算术右移1位,%eax=0
0x0000000000400fdf <+17>: lea (%rax,%rsi,1),%ecx %ecx=0+0=0
0x0000000000400fe2 <+20>: cmp %edi,%ecx #第一个参数和%ecx=0比较
0x0000000000400fe4 <+22>: jle 0x400ff2 <func4+36> #若第一个参数 <=0跳至<+36>
0x0000000000400fe6 <+24>: lea -0x1(%rcx),%edx #否则,%edx=0-1=-1,进入递归
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 #第一个参数和%ecx=0比较
0x0000000000400ff9 <+43>: jge 0x401007 <func4+57> #若第一个参数 >=0跳出递归
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

第一个参数为0%ecx=0相同即可。

第四次递归:(记为R4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0x0000000000400fce <+0>:	sub    $0x8,%rsp
0x0000000000400fd2 <+4>: mov %edx,%eax # %eax=-1
0x0000000000400fd4 <+6>: sub %esi,%eax # %eax=-1-0=1
0x0000000000400fd6 <+8>: mov %eax,%ecx # %ecx=-1
0x0000000000400fd8 <+10>: shr $0x1f,%ecx #逻辑右移31位,%ecx=1
0x0000000000400fdb <+13>: add %ecx,%eax # %eax=2
0x0000000000400fdd <+15>: sar %eax #算术右移1位,%eax=1
0x0000000000400fdf <+17>: lea (%rax,%rsi,1),%ecx %ecx=1+0=1
0x0000000000400fe2 <+20>: cmp %edi,%ecx #第一个参数和%ecx=1比较
0x0000000000400fe4 <+22>: jle 0x400ff2 <func4+36> #若第一个参数 <=1跳至<+36>
0x0000000000400fe6 <+24>: lea -0x1(%rcx),%edx #否则,%edx=1-1=0,进入递归
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

第一个参数为1%ecx=1相同即可。

第五次递归:(记为R5,此时R5 = R3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0x0000000000400fce <+0>:	sub    $0x8,%rsp
0x0000000000400fd2 <+4>: mov %edx,%eax # %eax=0
0x0000000000400fd4 <+6>: sub %esi,%eax # %eax=0-0=0
0x0000000000400fd6 <+8>: mov %eax,%ecx # %ecx=0
0x0000000000400fd8 <+10>: shr $0x1f,%ecx #逻辑右移31位,%ecx=0
0x0000000000400fdb <+13>: add %ecx,%eax # %eax=0
0x0000000000400fdd <+15>: sar %eax #算术右移1位,%eax=0
0x0000000000400fdf <+17>: lea (%rax,%rsi,1),%ecx %ecx=0+0=0
0x0000000000400fe2 <+20>: cmp %edi,%ecx #第一个参数和%ecx=0比较
0x0000000000400fe4 <+22>: jle 0x400ff2 <func4+36> #若第一个参数 <=0跳至<+36>
0x0000000000400fe6 <+24>: lea -0x1(%rcx),%edx #否则,%edx=0-1=-1,进入递归
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 #第一个参数和%ecx=0比较
0x0000000000400ff9 <+43>: jge 0x401007 <func4+57> #若第一个参数 >=0跳出递归
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

第一个参数为0%ecx=0相同即可。

由此可以知道,R4之后会一直在R3、R4之间无限循环,直到程序崩溃。那么,第一个参数的取值只能为:0 1 3 7

key:

得出4组答案为:

0x8(%rsp) 第一个输入参数 7 3 1 0
0xc(%rsp) 第二个输入参数 0 0 0 0

我们取0 0这组输入。

1
2
//输入前
Halfway there!
1
2
3
4
//after input
Halfway there!
0 0
So you got that one. Try this one.

phase_5

phase_5的汇编代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
0x0000000000401062 <+0>:	push   %rbx
0x0000000000401063 <+1>: sub $0x20,%rsp
0x0000000000401067 <+5>: mov %rdi,%rbx #%rbx = 输入
0x000000000040106a <+8>: mov %fs:0x28,%rax #%rax = 0x28 = 40D
0x0000000000401073 <+17>: mov %rax,0x18(%rsp) #0x18(%rsp) = 0x28
0x0000000000401078 <+22>: xor %eax,%eax #%eax = 0x28 ^ 0x28 = 0
0x000000000040107a <+24>: callq 0x40131b <string_length>
0x000000000040107f <+29>: cmp $0x6,%eax #字符串长度是否为6
0x0000000000401082 <+32>: je 0x4010d2 <phase_5+112> 是,跳转至+112
0x0000000000401084 <+34>: callq 0x40143a <explode_bomb>否,爆炸
0x0000000000401089 <+39>: jmp 0x4010d2 <phase_5+112>
0x000000000040108b <+41>: movzbl (%rbx,%rax,1),%ecx #%ecx = %rbx + 0,第一次循环
0x000000000040108f <+45>: mov %cl,(%rsp)
0x0000000000401092 <+48>: mov (%rsp),%rdx #%rdx = (%rsp) = %cl
0x0000000000401096 <+52>: and $0xf,%edx #%edx &= $0xf,%edx不变
0x0000000000401099 <+55>: movzbl 0x4024b0(%rdx),%edx #%edx = 0x4024b0+%rdx
0x00000000004010a0 <+62>: mov %dl,0x10(%rsp,%rax,1) #%rsp+0x10(16D)+0 = %dl
0x00000000004010a4 <+66>: add $0x1,%rax #%rax++
0x00000000004010a8 <+70>: cmp $0x6,%rax #%rax = 6时,跳出循环
0x00000000004010ac <+74>: jne 0x40108b <phase_5+41> 否则,进入循环
0x00000000004010ae <+76>: movb $0x0,0x16(%rsp) #0x16(%rsp) = 0
0x00000000004010b3 <+81>: mov $0x40245e,%esi #%esi = 0x40245e
0x00000000004010b8 <+86>: lea 0x10(%rsp),%rdi #入参
0x00000000004010bd <+91>: callq 0x401338 <strings_not_equal>#字符相等返回值为0,否则为1
0x00000000004010c2 <+96>: test %eax,%eax
0x00000000004010c4 <+98>: je 0x4010d9 <phase_5+119> #返回值为0,跳转至<+119>
0x00000000004010c6 <+100>: callq 0x40143a <explode_bomb> #返回值为1,爆炸
0x00000000004010cb <+105>: nopl 0x0(%rax,%rax,1)
0x00000000004010d0 <+110>: jmp 0x4010d9 <phase_5+119>
0x00000000004010d2 <+112>: mov $0x0,%eax #%eax = 0
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> #0x18(%rsp)=0x28,结束pahse_5
0x00000000004010e9 <+135>: callq 0x400b30 <__stack_chk_fail@plt> #否则进入fail函数
0x00000000004010ee <+140>: add $0x20,%rsp
0x00000000004010f2 <+144>: pop %rbx
0x00000000004010f3 <+145>: retq

分析:
很显然,输入的字符串长度得为6。利用x/s指令,在<+55>处我们查看x/s 0x4024b0,给出一句嘲讽我们的话,"maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"很明显不是答案,也许是答案,也许不是答案,也许在暗示答案不能单单copy,我们无视他的嘲讽,对<+41> ~ <+74>处的循环进行专门分析。

第二次循环:

1
2
3
4
5
6
7
8
9
0x000000000040108b <+41>:	movzbl (%rbx,%rax,1),%ecx #%ecx = %rbx + 1
0x000000000040108f <+45>: mov %cl,(%rsp)
0x0000000000401092 <+48>: mov (%rsp),%rdx #%rdx = (%rsp) = %cl
0x0000000000401096 <+52>: and $0xf,%edx #%edx &= 0xf,位级与运算,取尾4位,范围为0 ~ 15
0x0000000000401099 <+55>: movzbl 0x4024b0(%rdx),%edx #%edx = 0x4024b0+%rdx
0x00000000004010a0 <+62>: mov %dl,0x10(%rsp,%rax,1) #%rsp+0x10(16D)+1 = %dl
0x00000000004010a4 <+66>: add $0x1,%rax #%rax++
0x00000000004010a8 <+70>: cmp $0x6,%rax #%rax = 6时,跳出循环
0x00000000004010ac <+74>: jne 0x40108b <phase_5+41> 否则,进入循环

以此类推至第六次循环后,应当有<+41>: %ecx = %rbx+5<+62>: %rsp+0x10+5 = %dl。此时,%rbx输入,会有: %rsp+0x10+5 = %dl = %edx = 0x4024b0+(%rdx & 0xf)
上式等价于:= 0x4024b0+(%cl & 0xf) = 0x4024b0+((%rbx+5) & 0xf)
所有情况见下表。

%rsp+0x10+0 0x4024b0+%rbx+0(0x4024b0 + (第一个输入参数 & 0xf))
%rsp+0x10+1 0x4024b0+%rbx+1(0x4024b0 + (第二个输入参数 & 0xf))
%rsp+0x10+2 0x4024b0+%rbx+2(0x4024b0 + (第三个输入参数 & 0xf))
%rsp+0x10+3 0x4024b0+%rbx+3(0x4024b0 + (第四个输入参数 & 0xf))
%rsp+0x10+4 0x4024b0+%rbx+4(0x4024b0 + (第五个输入参数 & 0xf))
%rsp+0x10+5 0x4024b0+%rbx+5(0x4024b0 + (第六个输入参数 & 0xf))

可以看出%rsp+0x10的赋值是0x4024b0的偏移,偏移量范围为0 ~ 15,也就是嘲讽句的前16个字符,即maduiersnfotvbyl

我们回到phase_5函数继续分析,利用x/s指令,在<+81>处我们查看x/s 0x40245e,给出一个单词字符串,"flyers"(译为飞翔者们或者考熟的鸭子飞走了)。也就是说,<+86>处的入参必须与flyers相同。

根据maduiersnfotvbyl,取得flyers对应0x4024b0的偏移量是9、15、14、5、6、7,但是只能输入6个字符,如果直接输入91514567,超过6个字符会爆炸。

要解决这个问题,可以先把偏移量转换为二进制1001、1111、1110、0101、0110、0111。因为有%rbx & 0xf的位级操作,那么我们只要保证尾4位相同即可。这时可以联想到用来定义字符ASCII表,那么答案有许多种,我比较懒,就只找尾4位为11111110的字符。分别为/.,从而得到输入9/.567

key:

1
2
//输入前
So you got that one. Try this one.
1
2
3
4
//after input
So you got that one. Try this one.
9/.567
Good work! On to the next...

phase_6

查看phase_6的汇编代码:
分析:
好家伙,一个电脑屏幕根本装不下全部代码。我们分成几个部分来看。

注意在循环开始前会有一些变量准备0x10 = 16D,0x14 = 20Dflag这个变量命名常用来当作循环是否结束的标志

Part1,读入了6个整数。

1
2
3
4
5
6
7
8
9
10
11
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> #读入6个数字
0x000000000040110b <+23>: mov %rsp,%r14
0x000000000040110e <+26>: mov $0x0,%r12d #%r12d = 0

根据phase_2<read_six_numbers>分析可以知道,这段读入了6个整数,对栈帧%rsp偏移(偏移量为4)即是输入input[i],i取0 ~ 5。如下表所示:

(%rsp) 0x4(%rsp) 0x8(%rsp) 0xc(%rsp) 0x10(%rsp) 0x14(%rsp)
input[0] input[1] input[2] input[3] input[4] input[5]

Part2,保证6个输入整数(input[i],i取0 ~ 5)都不相同且范围为1 ~ 6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#loop1,+32 ~ +93该循环说明:1<= input[i] <= 6,i取0 ~ 5
0x0000000000401114 <+32>: mov %r13,%rbp #%rbp = %r13
0x0000000000401117 <+35>: mov 0x0(%r13),%eax #%eax = %r13
0x000000000040111b <+39>: sub $0x1,%eax #%eax = %eax - 1
0x000000000040111e <+42>: cmp $0x5,%eax #比较5和%eax,
0x0000000000401121 <+45>: jbe 0x401128 <phase_6+52> #若%eax <= 5,跳转至<+52>
0x0000000000401123 <+47>: callq 0x40143a <explode_bomb> #否则,爆炸
0x0000000000401128 <+52>: add $0x1,%r12d #%r12d++,flag值
0x000000000040112c <+56>: cmp $0x6,%r12d #比较6和%r12d
0x0000000000401130 <+60>: je 0x401153 <phase_6+95> #若6 == %r12d,结束loop2
0x0000000000401132 <+62>: mov %r12d,%ebx #否则,%ebx = %r12d
#loop2,+65 ~ +87,该循环是loop1的子循环,用来是保证6个输入都不相等
0x0000000000401135 <+65>: movslq %ebx,%rax
0x0000000000401138 <+68>: mov (%rsp,%rax,4),%eax #%eax = %rsp+4*%rax
0x000000000040113b <+71>: cmp %eax,0x0(%rbp) #比较%rsp+4*%rax和%rsp
0x000000000040113e <+74>: jne 0x401145 <phase_6+81> #若不相等,跳转至<+81>
0x0000000000401140 <+76>: callq 0x40143a <explode_bomb> #否则,爆炸
0x0000000000401145 <+81>: add $0x1,%ebx #%ebx++,loop2取input[i]
0x0000000000401148 <+84>: cmp $0x5,%ebx #比较5和%ebx
0x000000000040114b <+87>: jle 0x401135 <phase_6+65> #若%ebx <= 5,进入loop2
0x000000000040114d <+89>: add $0x4,%r13 #%r13+4,loop1取input[i]
0x0000000000401151 <+93>: jmp 0x401114 <phase_6+32> #进入loop1

loop1确定了input[i]的取值范围为:1 ~ 6。loop2保证6个输入都不相等。
取值范围如何确定的?+39 ~ +45处得出,input[i] <= 6。同时,因为+45jbe 是对无符号数操作,若input[i]0 ,执行0 - 1 = -1在比较时会出错(溢出),所以不能取0。故只能取1 ~ 6

Part3,执行算式:input[i] = 7 - input[i]

1
2
3
4
5
6
7
8
9
10
0x0000000000401153 <+95>:	lea    0x18(%rsp),%rsi #%rsi = 0x18(%rsp)
0x0000000000401158 <+100>: mov %r14,%rax #%rax = %r14 = %rsp
0x000000000040115b <+103>: mov $0x7,%ecx #%ecx = 7
#loop3,+108 ~ +121,该循环执行算式:input[i] = 7 - input[i]
0x0000000000401160 <+108>: mov %ecx,%edx
0x0000000000401162 <+110>: sub (%rax),%edx #%edx = 7 - (%rax)
0x0000000000401164 <+112>: mov %edx,(%rax) #(%rax) = %edx
0x0000000000401166 <+114>: add $0x4,%rax #%rax+4,取input[i]
0x000000000040116a <+118>: cmp %rsi,%rax #比较%rsi=0x18(%rsp)和%rax+4
0x000000000040116d <+121>: jne 0x401160 <phase_6+108>

因此为了方便,我们重新命名:sub_input[i] = 7 - input[i]

Part4,引入链表改变链表顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
##注意,执行loop3后,sub_input[i] = 7 - input[i]
0x000000000040116f <+123>: mov $0x0,%esi #%esi = 0
#loop4,+128~+181,该循环执行赋值算式:%rsp + 8*i + 0x20 = $0x6032d0 + (8*(sub_input[i]-1))
0x0000000000401174 <+128>: jmp 0x401197 <phase_6+163>
#loop5,+130~+139,该循环是loo4的子循环,执行0x6032d自加8,自加次数为sub_input[i]-1
0x0000000000401176 <+130>: mov 0x8(%rdx),%rdx #%rdx += 8
0x000000000040117a <+134>: add $0x1,%eax #%eax++,flag值
0x000000000040117d <+137>: cmp %ecx,%eax #比较input[i]和%eax
0x000000000040117f <+139>: jne 0x401176 <phase_6+130> #不相等,则继续loop5
0x0000000000401181 <+141>: jmp 0x401188 <phase_6+148> #相等,->+148,进入loop4
0x0000000000401183 <+143>: mov $0x6032d0,%edx #%edx = 0x6032d0
0x0000000000401188 <+148>: mov %rdx,0x20(%rsp,%rsi,2) #0x20+%rsp+2*%rsi = %rdx
0x000000000040118d <+153>: add $0x4,%rsi #%rsi+4,flag值
0x0000000000401191 <+157>: cmp $0x18,%rsi #比较0x18和flag值
0x0000000000401195 <+161>: je 0x4011ab <phase_6+183> #若相等,结束loop4
0x0000000000401197 <+163>: mov (%rsp,%rsi,1),%ecx #取sub_input[i]
0x000000000040119a <+166>: cmp $0x1,%ecx #比较1和sub_input[i]
0x000000000040119d <+169>: jle 0x401183 <phase_6+143> #若sub_input[i] <= 1,跳转至+143
0x000000000040119f <+171>: mov $0x1,%eax #否则,%eax = 1
0x00000000004011a4 <+176>: mov $0x6032d0,%edx #&edx = 0x6032d0
0x00000000004011a9 <+181>: jmp 0x401176 <phase_6+130> #进入loop5

首先我们查看0x6032d0,一开始用x/s指令都是乱码,如图所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(gdb) x/s 0x6032d0
0x6032d0 <node1>: "L\001"
(gdb) x/100s 0x6032d0
0x6032d0 <node1>: "L\001"
0x6032d3 <node1+3>: ""
0x6032d4 <node1+4>: "\001"
0x6032d6 <node1+6>: ""
0x6032d7 <node1+7>: ""
0x6032d8 <node1+8>: "\340\062`"
0x6032dc <node1+12>: ""
0x6032dd <node1+13>: ""
0x6032de <node1+14>: ""
0x6032df <node1+15>: ""
0x6032e0 <node2>: "\250"
0x6032e2 <node2+2>: ""
...
--Type <RET> for more, q to quit, c to continue without paging--

但是注意到node1,说明其很有可能是链表,而不是字符串。
我们用x/24xw指令,按xw格式(十六进制4字节格式)输出24个数据。
如图所示,符合我们的猜测,就是链表,0x6032d0是head指针。
其中,node是节点地址,val是节点值,order是它的序列(一共6个),next是下个节点的地址(能够发现它刚好就是按照序列排列的,最后一个节点的next是0x0即NULL)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(gdb) x/24xw 0x6032d0
0x6032d0 <node1>: 0x0000014c 0x00000001 0x006032e0 0x00000000
0x6032e0 <node2>: 0x000000a8 0x00000002 0x006032f0 0x00000000
0x6032f0 <node3>: 0x0000039c 0x00000003 0x00603300 0x00000000
0x603300 <node4>: 0x000002b3 0x00000004 0x00603310 0x00000000
0x603310 <node5>: 0x000001dd 0x00000005 0x00603320 0x00000000
0x603320 <node6>: 0x000001bb 0x00000006 0x00000000 0x00000000

##注释一下
node val order next
0x6032d0 <node1>: 0x0000014c 0x00000001 0x006032e0 0x00000000
0x6032e0 <node2>: 0x000000a8 0x00000002 0x006032f0 0x00000000
0x6032f0 <node3>: 0x0000039c 0x00000003 0x00603300 0x00000000
0x603300 <node4>: 0x000002b3 0x00000004 0x00603310 0x00000000
0x603310 <node5>: 0x000001dd 0x00000005 0x00603320 0x00000000
0x603320 <node6>: 0x000001bb 0x00000006 0x00000000 0x00000000
##注意到,node的间隔为0x10 = 16D
##我们还可以用c语言描述node:
struct node{
int val;
int order;
node next;
}

那么,该段循环是怎么改变链表顺序的呢?首先引入栈帧%rsp + 8*i + 0x20,i取0 ~ 5,然后在+130 ~ +137 0x6032d0将随sub_input[i]的改变而改变,最后将改变后的链表逐个赋给栈帧。
即执行执行赋值算式:%rsp + 8*i + 0x20 = $0x6032d0 + (8*(sub_input[i]-1))
这样下来就能够重排链表了。但此处还没有给出改变链表顺序的标准是什么。
同时注意一个重要的细节问题:改变时0x6032d0的间隔为8D,但我们需要的间隔为0x10 = 16D,如何解决?
事实上,间隔为8也没关系。只要部分+8的地址所存数值恰好为+16的地址即可。(不是所有地址都需要这样,另一部分地址执行两次+8就行了)
上述表述等价于三个等式
*(0x6032d0+8) = 0x6032e0*(0x6032f0+8) = 0x603200*(0x603310+8)= 0x603320 成立。
我们用p/x 指令打印数值看三个等式是否成立。(这个成立是phase_6已经设置好的,但不查看之前完全不知道)

1
2
3
4
5
6
7
(gdb) p/x *(0x6032d0+8)
$1 = 0x6032e0
(gdb) p/x *(0x6032f0+8)
$2 = 0x603300
(gdb) p/x *(0x603310+8)
$3 = 0x603320
#证明等式成立。

执行part4后,可以得出如下表格:

rec_node0 rec_node1 rec_node2 rec_node3 rec_node4 rec_node5
%rsp + 0x20 %rsp + 0x28 %rsp + 0x30 %rsp + 0x38 %rsp + 0x40 %rsp + 0x48

说明rec_node1 ~ rec_node6将逐个放入栈帧%rsp + 0x20 ~ %rsp + 0x48中。
注意:因为node0 ~ node5已经随sub_input而重新排列了,但我们还不知道是如何排列的。
所以,不妨命名为:rec_node0 ~ rec_node5,表明链表重组,也就是所谓的改变链表顺序
(rec:recombination)

我们也可以得出sub_input对应rec_node的表格:

sub_input[0] sub_input[1] sub_input[2] sub_input[3] sub_input[4] sub_input[5]
1 2 3 4 5 6
0x6032d0 0x6032e0 0x6032f0 0x603300 0x603310 0x603320

part5重新连接next

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0x00000000004011ab <+183>:	mov    0x20(%rsp),%rbx 
0x00000000004011b0 <+188>: lea 0x28(%rsp),%rax
0x00000000004011b5 <+193>: lea 0x50(%rsp),%rsi #%rsi = 0x50(%rsp) = 0
0x00000000004011ba <+198>: mov %rbx,%rcx
#loop6,+201~+220,判断flag值的5次+初始1次,
#执行6次赋值算式:0x8+(0x20(%rsp)+(j-1)*8) = (0x28(%rsp)+j*8),j取0 ~ 5
#会将间隔为8的%0x28(%rsp) ~ 0x50(%rsp)的地址,逐个赋给间隔为8的%0x20(%rsp) ~ 0x48(%rsp)
#loop6结束时,%rdx = 0x48(%rsp)
0x00000000004011bd <+201>: mov (%rax),%rdx
0x00000000004011c0 <+204>: mov %rdx,0x8(%rcx) #0x8(%rcx+(j-1)*8) = %rax+j*8
0x00000000004011c4 <+208>: add $0x8,%rax #%rax += 8,记为j,每次增8,也是flag值
0x00000000004011c8 <+212>: cmp %rsi,%rax #比较%rsi和%rax,执行5
0x00000000004011cb <+215>: je 0x4011d2 <phase_6+222> #若相等,结束loop6
0x00000000004011cd <+217>: mov %rdx,%rcx #否则,%rcx = %rdx
0x00000000004011d0 <+220>: jmp 0x4011bd <phase_6+201> #-> +201,继续loop6
0x00000000004011d2 <+222>: movq $0x0,0x8(%rdx) #0x8(%rdx) = 0x50(%rsp) = 0

注意:

  • 0x50(%rsp) = 0 = NULL是next连接完才赋值的。
  • 这里利用栈帧%0x28(%rsp) ~ 0x50(%rsp)逐个赋给%0x20(%rsp) ~ 0x48(%rsp)
    改变的是rec_node ->next指向的地址,而不是rec_ndoe本身。也就是所谓的重新连接next
  • 之所以要重连next,也是因为node已经重排为rec_ndoe了。

part6,说明改变链表顺序的标准:val降序排列。(从大到小)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0x00000000004011da <+230>:	mov    $0x5,%ebp #%ebp = 5
#loop7,判断flag值的5次+初始1次,
#共执行6次判断算式:(0x8+0x20+%rsp+k*8) >= (0x20+%rsp+k*8) ? 继续执行直到k=5结束:爆炸
#即保证等式:node->val >= node->next->val成立。
0x00000000004011df <+235>: mov 0x8(%rbx),%rax
0x00000000004011e3 <+239>: mov (%rax),%eax
0x00000000004011e5 <+241>: cmp %eax,(%rbx) #比较(0x8+0x20+%rsp+k*8)和(0x20+%rsp+k*8)
0x00000000004011e7 <+243>: jge 0x4011ee <phase_6+250> #若前者 >= 后者,-> +250
0x00000000004011e9 <+245>: callq 0x40143a <explode_bomb> #否则,爆炸
0x00000000004011ee <+250>: mov 0x8(%rbx),%rbx #取0x20+%rsp+k*8
0x00000000004011f2 <+254>: sub $0x1,%ebp #%ebp--,flag值,执行5
0x00000000004011f5 <+257>: jne 0x4011df <phase_6+235> #%ebp不为0时,继续loop7
##
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

之所以按val降序排列,是因为必须满足(0x8+0x20+%rsp+k*8) >= (0x20+%rsp+k*8),k取0~5
node->val >= node->next->val成立,也就是链表中当前节点值大于下一个节点值

综上part1 ~ part6可以得出答案:(sub_input模7得到input

rec_node0 rec_node1 rec_node2 rec_node3 rec_node4 rec_node5
val 0x39c 0x2b3 0x1dd 0x1bb 0x14c 0x0a8
rec_node 0x6032f0 0x603300 0x603300 0x603300 0x6032d0 0x6032e0
sub_input 3 4 5 6 1 2
input 4 3 2 1 6 5

inuput为:4 3 2 1 6 5

ps:如果你仔细观察,你会发现,按val降序排列之后,order其实恰好对应着sub_input

key:

1
2
//before input
Good work! On to the next...
1
2
3
4
5
//after input
Good work! On to the next...
4 3 2 1 6 5
Congratulations! You've defused the bomb!
[Inferior 1 (process 46719) exited normally]

可恶啊!竟然还有个彩蛋关。什么?你说我怎么发现的,当然是“口口相传”啦。
(事实上可以在编译文件上查找ctrl+f到。)

secret_phase

用到了二叉树。

查看phase_defused的汇编代码:(可以知道在phase_4中多输入个DrEvil即0 0 DrEvil才能进入彩蛋关)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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 #输入格式%esi:"%d %d %s"
0x00000000004015f5 <+49>: mov $0x603870,%edi #input
0x00000000004015fa <+54>: callq 0x400bf0 <__isoc99_sscanf@plt>
0x00000000004015ff <+59>: cmp $0x3,%eax
0x0000000000401602 <+62>: jne 0x401635 <phase_defused+113>
0x0000000000401604 <+64>: mov $0x402622,%esi #%esi = DrEvil
0x0000000000401609 <+69>: lea 0x10(%rsp),%rdi
0x000000000040160e <+74>: callq 0x401338 <strings_not_equal>
0x0000000000401613 <+79>: test %eax,%eax #字符串相同返回值为0,进入secret_phase
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

分析:
我们对一些字符串进行访问,可以知道调用sscanf函数前得输入,其格式为"%d %d %s"但输入内容是空的,这说明我们还没输入。我们需要输入,但,是什么时候输入呢?

1
2
3
4
5
6
(gdb) x/s 0x402619
0x402619: "%d %d %s"
(gdb) x/s 0x603870
0x603870 <input_strings+240>: ""
(gdb) x/s 0x402622
0x402622: "DrEvil"

要确定输入是何时输入的,我们可以先在*0x4015f5处,即输入处打断点,然后run bomb文件,最后访问0x603870的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Reading symbols from bomb...
(gdb) b *0x4015f5
Breakpoint 1 at 0x4015f5
(gdb) run
Starting program: /home/duile/Desktop/csapp_lab/bomb-handout/bomb
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?
1 2 4 8 16 32
That's number 2. Keep going!
7 327
Halfway there!
0 0
So you got that one. Try this one.
9/.567
Good work! On to the next...
4 3 2 1 6 5

Breakpoint 1, 0x00000000004015f5 in phase_defused ()
(gdb) x/s 0x603870
0x603870 <input_strings+240>: "0 0 "

此时有0x603870 <input_strings+240>: "0 0 ",这说明输入是在phase_4处进行的。其输入格式为"%d %d %s",同时有*(0x402622) == DrEvil,之后要进行字符比对且相同才进入secret_phase。那么我们大胆猜测,应该在phase_4处输入0 0 DrEvil才能进入彩蛋关。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#果然如此
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?
1 2 4 8 16 32
That's number 2. Keep going!
7 327
Halfway there!
0 0 DrEvil #将0 0替换为0 0 DrEvil
So you got that one. Try this one.
9/.567
Good work! On to the next...
4 3 2 1 6 5
Curses, you've found the secret phase!
But finding it and solving it are quite different...

开始正式分析,首先查看secret_phase的汇编代码:(说明调用fun7之后,返回值必须为2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0x0000000000401242 <+0>:	push   %rbx
0x0000000000401243 <+1>: callq 0x40149e <read_line>
0x0000000000401248 <+6>: mov $0xa,%edx #base = 10D
0x000000000040124d <+11>: mov $0x0,%esi
0x0000000000401252 <+16>: mov %rax,%rdi #input
0x0000000000401255 <+19>: callq 0x400bd0 <strtol@plt>
0x000000000040125a <+24>: mov %rax,%rbx #返回值rax赋给%rbx
0x000000000040125d <+27>: lea -0x1(%rax),%eax
0x0000000000401260 <+30>: cmp $0x3e8,%eax #满足-0x1(%rax) <= 0x3e8,跳转至+42,否则爆炸
0x0000000000401265 <+35>: jbe 0x40126c <secret_phase+42>
0x0000000000401267 <+37>: callq 0x40143a <explode_bomb>
0x000000000040126c <+42>: mov %ebx,%esi #esi是输入
0x000000000040126e <+44>: mov $0x6030f0,%edi #rdi = 0x24
0x0000000000401273 <+49>: callq 0x401204 <fun7>
0x0000000000401278 <+54>: cmp $0x2,%eax #返回值必须等于2,否则爆炸
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

查看fun7的汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0x0000000000401204 <+0>:	sub    $0x8,%rsp
0x0000000000401208 <+4>: test %rdi,%rdi #rdi == 0时,返回-1
0x000000000040120b <+7>: je 0x401238 <fun7+52>
0x000000000040120d <+9>: mov (%rdi),%edx
0x000000000040120f <+11>: cmp %esi,%edx #(%rdi) <= %esi
0x0000000000401211 <+13>: jle 0x401220 <fun7+28>
0x0000000000401213 <+15>: mov 0x8(%rdi),%rdi #rdi += 0x8
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> #%esi == %edx
0x0000000000401229 <+37>: mov 0x10(%rdi),%rdi #rdi += 0x10
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

分析:

这是一棵二叉树,思路如图所示:

方法一:实现eax = 2。(ps:递归调用顺序与返回顺序相反

1
2
3
4
5
6
eax = 0 --> eax = 2*eax+1 --> eax = 2*eax
0 --> 1 --> 2
故返回顺序为:
D --> C --> B
因而递归调用顺序为:(与返回顺序相反)
B --> B --> D

下面描述调用过程:(ps:esi是input,每次调用完都会更新rid指向的地址

  • b:第一次调用fun7,(rdi) = (0x6030f0) = 0x24。同时输入要不满足(rdi) <= esi ,也就说输入小于0x24,才能走路径B。
    此次调用结束时更新rdi指向的地址,即:rdi+0x8 = 0x603110

  • c:第二次调用fun7,(rdi) = (0x603110) = 0x8。同时输入要满足(rdi) <= esi且不满足edx == esi,也就说输入大于0x8且不等于0x8,才能走路径C。
    此次调用结束时更新rdi指向的地址,即:rdi+0x10 = 0x603150

  • d:第三次调用fun7,(rdi) = (0x603150) = 0x16。同时输入要满足(rdi) <= esi且满足edx == esi,也就说输入等于0x16即20D,才能走路径D。
    三次调用结束,得出input为20

1
2
3
4
5
6
7
8
9
10
11
#这是gdb指令的访问过程:(使用的是打印指令,更清晰)
(gdb) p/a *(0x6030f0)
$1 = 0x24
(gdb) p/a *(0x6030f0+0x8)
$2 = 0x603110 <n21>
(gdb) p/a *(0x603110)
$3 = 0x8
(gdb) p/a *(0x603110+0x10)
$4 = 0x603150 <n32>
(gdb) p/a *(0x603150)
$5 = 0x16

方法二:实现eax = 2。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
eax = 0	--> eax = 2*eax--> eax = 2*eax+1 --> eax = 2*eax
0 --> 0 --> 1 --> 2
故返回顺序为:
D --> B --> C --> B
因而递归调用顺序为:(与返回顺序相反)
B --> C --> B --> D
我们按照递归调用访问(rdi)得出结果:0x14 = 20D(使用的是打印指令,更便捷)
(gdb) x/a 0x6030f0+0x8 #b
0x6030f8 <n1+8>: 0x603110 <n21>
(gdb) x/a 0x603110+0x10 #c
0x603120 <n21+16>: 0x603150 <n32>
(gdb) x/a 0x603150+0x8 #b
0x603158 <n32+8>: 0x603270 <n43>
(gdb) x/a 0x603270 #d
0x603270 <n43>: 0x14

key:我们取20这个答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//before input
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?
1 2 4 8 16 32
That's number 2. Keep going!
7 327
Halfway there!
0 0 DrEvil
So you got that one. Try this one.
9/.567
Good work! On to the next...
4 3 2 1 6 5
Curses, you've found the secret phase!
But finding it and solving it are quite different...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//after input
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?
1 2 4 8 16 32
That's number 2. Keep going!
7 327
Halfway there!
0 0 DrEvil
So you got that one. Try this one.
9/.567
Good work! On to the next...
4 3 2 1 6 5
Curses, you've found the secret phase!
But finding it and solving it are quite different...
20
Wow! You've defused the secret stage!
Congratulations! You've defused the bomb!
[Inferior 1 (process 48781) exited normally]

无聊地查看一下二叉树内容:(此处对解题不是很有必要)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
(gdb) x/116wx 0x6030f0
0x6030f0 <n1>: 0x00000024 0x00000000 0x00603110 0x00000000
0x603100 <n1+16>: 0x00603130 0x00000000 0x00000000 0x00000000
0x603110 <n21>: 0x00000008 0x00000000 0x00603190 0x00000000
0x603120 <n21+16>: 0x00603150 0x00000000 0x00000000 0x00000000
0x603130 <n22>: 0x00000032 0x00000000 0x00603170 0x00000000
0x603140 <n22+16>: 0x006031b0 0x00000000 0x00000000 0x00000000
0x603150 <n32>: 0x00000016 0x00000000 0x00603270 0x00000000
0x603160 <n32+16>: 0x00603230 0x00000000 0x00000000 0x00000000
0x603170 <n33>: 0x0000002d 0x00000000 0x006031d0 0x00000000
0x603180 <n33+16>: 0x00603290 0x00000000 0x00000000 0x00000000
0x603190 <n31>: 0x00000006 0x00000000 0x006031f0 0x00000000
0x6031a0 <n31+16>: 0x00603250 0x00000000 0x00000000 0x00000000
0x6031b0 <n34>: 0x0000006b 0x00000000 0x00603210 0x00000000
0x6031c0 <n34+16>: 0x006032b0 0x00000000 0x00000000 0x00000000
0x6031d0 <n45>: 0x00000028 0x00000000 0x00000000 0x00000000
0x6031e0 <n45+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x6031f0 <n41>: 0x00000001 0x00000000 0x00000000 0x00000000
0x603200 <n41+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x603210 <n47>: 0x00000063 0x00000000 0x00000000 0x00000000
0x603220 <n47+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x603230 <n44>: 0x00000023 0x00000000 0x00000000 0x00000000
0x603240 <n44+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x603250 <n42>: 0x00000007 0x00000000 0x00000000 0x00000000
0x603260 <n42+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x603270 <n43>: 0x00000014 0x00000000 0x00000000 0x00000000
0x603280 <n43+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x603290 <n46>: 0x0000002f 0x00000000 0x00000000 0x00000000
0x6032a0 <n46+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x6032b0 <n48>: 0x000003e9 0x00000000 0x00000000 0x00000000

#c语言描述二叉树
typedef struct BiTNode{
TElemType data;//数据
struct BiTNode *light, *right;//左右孩子指针
} BiTNode, *BiTree;

整理后的图状展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
└─ 36
├─ 8
│ ├─ 6
│ │ ├─ left: 1
│ │ └─ right: 7
│ └─ 22
│ ├─ left: 20
│ └─ right: 35
└─ 50
├─ 45
│ ├─ left: 40
│ └─ right: 47
└─ 107
├─ left: 99
└─ right: 1001

结语

  • But you know, Alan, sometimes it’s the very people who no one imagines anything of who do the things no one can imagine. —The Imitation Game
  • 从9.27到10.11,总共有一万两千多个字符,总算完结了。期间有几天是没有写的。总共应该是8天,最后两关都用了2天。当然不是一整天,5、6个小时应该是有的。完全靠自己写的是phase_4,奈何没啥基础。前几个都在适应gdb是怎么调试的,后几个难度比较大,phase_5是没联系到ASCII,后两个是链表、二叉树都不熟悉。也参考了不少其它大佬的解答。
  • 自己的汇编阅读能力是提升了不少,后面看都是比较自然,但是联系到数据结构就比较不了解,还是得继续提高姿势水平。
  • 如有谬误,敬请指正。
  • 成果截图