拆除csapp二进制炸弹超详细的解析之phase_5

概述

接着 拆除csapp二进制炸弹超详细的解析之phase_4 继续解析phase_5 。

cgdb5截图

phase_5解析

同样地首先执行si命令:

144│ bomb.c:
145│ 100         input = read_line();
146├──> 0x0000000000400ea2 <+258>:   callq  0x40149e <read_line>
147│
148│ 101         phase_5(input);
149│    0x0000000000400ea7 <+263>:   mov    %rax,%rdi
150│    0x0000000000400eaa <+266>:   callq  0x401062 <phase_5>
151│
152│ 102         phase_defused();
153│    0x0000000000400eaf <+271>:   callq  0x4015c4 <phase_defused>



                Figure 5.1

和前面的一样套路,Figure 5.1 中第146行读取我们的输入,第149行把输入的返回值rax复制给rdi,查看下我们的输入:

(gdb) x /s $rax
0x6038c0 <input_strings+320>:   "I'm in phase_5 for test"

我们输入的内容是 I’m in phase_5 for test 。

接下来si进入到第150行:

 1│ Dump of assembler code for function phase_5:
 2├──> 0x0000000000401062 <+0>:     push   %rbx
 3│    0x0000000000401063 <+1>:     sub    $0x20,%rsp
 4│    0x0000000000401067 <+5>:     mov    %rdi,%rbx
 5│    0x000000000040106a <+8>:     mov    %fs:0x28,%rax
 6│    0x0000000000401073 <+17>:    mov    %rax,0x18(%rsp)
 7│    0x0000000000401078 <+22>:    xor    %eax,%eax
 8│    0x000000000040107a <+24>:    callq  0x40131b <string_length>
 9│    0x000000000040107f <+29>:    cmp    $0x6,%eax
10│    0x0000000000401082 <+32>:    je     0x4010d2 <phase_5+112>
11│    0x0000000000401084 <+34>:    callq  0x40143a <explode_bomb>
12│    0x0000000000401089 <+39>:    jmp    0x4010d2 <phase_5+112>
13│    0x000000000040108b <+41>:    movzbl (%rbx,%rax,1),%ecx
14│    0x000000000040108f <+45>:    mov    %cl,(%rsp)
15│    0x0000000000401092 <+48>:    mov    (%rsp),%rdx
16│    0x0000000000401096 <+52>:    and    $0xf,%edx
17│    0x0000000000401099 <+55>:    movzbl 0x4024b0(%rdx),%edx
18│    0x00000000004010a0 <+62>:    mov    %dl,0x10(%rsp,%rax,1)
19│    0x00000000004010a4 <+66>:    add    $0x1,%rax
20│    0x00000000004010a8 <+70>:    cmp    $0x6,%rax
21│    0x00000000004010ac <+74>:    jne    0x40108b <phase_5+41>
22│    0x00000000004010ae <+76>:    movb   $0x0,0x16(%rsp)
23│    0x00000000004010b3 <+81>:    mov    $0x40245e,%esi
24│    0x00000000004010b8 <+86>:    lea    0x10(%rsp),%rdi
25│    0x00000000004010bd <+91>:    callq  0x401338 <strings_not_equal>
26│    0x00000000004010c2 <+96>:    test   %eax,%eax
27│    0x00000000004010c4 <+98>:    je     0x4010d9 <phase_5+119>
28│    0x00000000004010c6 <+100>:   callq  0x40143a <explode_bomb>
29│    0x00000000004010cb <+105>:   nopl   0x0(%rax,%rax,1)
30│    0x00000000004010d0 <+110>:   jmp    0x4010d9 <phase_5+119>
31│    0x00000000004010d2 <+112>:   mov    $0x0,%eax
32│    0x00000000004010d7 <+117>:   jmp    0x40108b <phase_5+41>
33│    0x00000000004010d9 <+119>:   mov    0x18(%rsp),%rax
34│    0x00000000004010de <+124>:   xor    %fs:0x28,%rax
35│    0x00000000004010e7 <+133>:   je     0x4010ee <phase_5+140>
36│    0x00000000004010e9 <+135>:   callq  0x400b30 <__stack_chk_fail@plt>
37│    0x00000000004010ee <+140>:   add    $0x20,%rsp
38│    0x00000000004010f2 <+144>:   pop    %rbx
39│    0x00000000004010f3 <+145>:   retq
40│ End of assembler dump.



                     Figure 5.2

在 Figure 5.2 中第1行把rbx压入栈中,第2行十六进制0x20的十进制值是32,开辟32字节的内存空间,第4行把我们的输入复制到rbx
中,第5行执行栈保护检查,第7行把eax设置为0,然后第8行调用 string_length 函数,第9行比较 string_length 的返回值eax和6的
大小,第10行当eax等于6时跳过炸弹爆炸函数,即eax返回字符串的长度。

如果我们输入的字符串的长度等于6时,跳转到第31行,把eax设置为0,执行第32行的jmp指令跳转到第13行 movzbl (%rbx,%rax,1),%ecx
其中rax等于0,movzbl以零扩展进行传送,即把一个字节的值前面加上24个0构成一个双字,第14行取ecx的低8位的一个字节复制到rsp
第15行再把rsp里面的内容复制到rdx,即现在rdx里面保存的是我们输入的第一个字符,第16行 and $0xf,%edx 让 edx低4位保持
不变,其它位设置0,即只取edx的低4位。第17行 movzbl 0x4024b0(%rdx),%edx 中又出现了常量地址,常量地址很重要,看看里面内容

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

综合前面的分析,很明显 movzbl 0x4024b0(%rdx),%edx 中,rdx作为以0x4024b0为起始地址的数组偏移量,以我们输入的字符串的
第一个字符对应的ascii码的低4位表示的值。第18行 mov %dl,0x10(%rsp,%rax,1) ,把输入的每个字符的低4位放入到地址为
0x10 + %rsp + %rax 中,第19行把rax加1,第20行比较rax和6的大小,如果rax不等于6时,回到第13行,一共重复执行6次,每次
把输入的字符的低4位放入ecx,一共输入6个字符,刚好循环6次。

当rax等于6时跳转到第22行,把rsp加16进制的0x16的结果设置为0,第23行又出现常量地址,看看里面的内容:

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

第23行把flyers复制到esi,第24行把rsp加上0x10的地址结果存放到rdi,然后第25行调用 strings_not_equal 判断以我们输入的每个
字符的低4位为偏移组成的字符串是否和flyers相等,如果不相等则 strings_not_equal 函数的返回值eax不相等,那么就会跳转到
第28行,触发炸弹爆炸。如果eax等于0,则会跳转到第33行,然后检查栈保护是否正确,如果正确则跳转到第37行,安全退出phase_5。

现在我们知道了怎么获取本关卡的答案了:

因为字符串flyers全部被包含在字符串maduiersnfotvbyl中,我们可以看下flyers每个字符分别在maduiersnfotvbyl中的位置,我们
可以得到对应的索引是 9 15 14 5 6 7 ,这6个数字分别表示我们输入的6个字符中,每个字符的二进制表示中的低4位,我们输入的
6个字符可以全部是小写字母,但是要发现哪一个字母的二进制表示中低4位是9,或者15,或者14,或者5,或者6,或者7用手工法
一个个去找太麻烦了,既费事费力还容易搞错,所以我写了一个程序,自动去找,源代码如下:

#include <stdio.h>
int main()       // 9 15 14 5 6 7
{
    int wantP, c;
    char a[10] = {'?', '?', '?', '?', '?', '?'};
    for(c = 'a'; c <= 'z'; c++){
        wantP = c & 0xF;
        switch(wantP){
            case(9):
                a[0] = c;
                break;
            case(15):
                a[1] = c;
                break;
            case(14):
                a[2] = c;
                break;
            case(5):
                a[3] = c;
                break;
            case(6):
                a[4] = c;
                break;
            case(7):
                a[5] = c;
                break;
        }
    }
    printf("%s\n",a);
    return 0;
}

编译并运行会得到输出 yonuvw ,拿这个字符串去执行,发现可以完美的破解本关卡。美滋滋!