《CSAPP》Bomb Lab解析

Last Updated: 2023-10-09 02:27:46 Monday

-- TOC --

《CSAPP》的Bomb Lab很经典,是学习x86-64汇编的很好的练手项目。这个Lab需要你完全通过分析汇编代码,输入6个字符串,让程序顺序执行到最后,此时算解除Bomb,否则Bomb爆炸,任务失败。本文将自己的解题过程完整的记录下来,供同学们参考。

前置技能:

开始

首先通过gdb打开bomb程序,执行start指令,停在main处,设置你喜欢的汇编风格,开始查看汇编源码。

$ gdb -q bomb
Reading symbols from bomb...
(gdb) start

This GDB supports auto-downloading debuginfo from the following URLs:
https://debuginfod.fedoraproject.org/
Enable debuginfod for this session? (y or [n])
Debuginfod has been disabled.
To make this setting permanent, add 'set debuginfod enabled off' to .gdbinit.
Temporary breakpoint 1 at 0x400da0: file bomb.c, line 37.
Starting program: /home/xinlin/test2/bomb/bomb
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

Temporary breakpoint 1, main (argc=1, argv=0x7fffffffe3b8) at bomb.c:37
37      bomb.c: No such file or directory.
Missing separate debuginfos, use: dnf debuginfo-install glibc-2.34-49.fc35.x86_64
(gdb) set disassembly-flavor intel
(gdb) disass
Dump of assembler code for function main:
=> 0x0000000000400da0 <+0>:     push   rbx
   0x0000000000400da1 <+1>:     cmp    edi,0x1
   0x0000000000400da4 <+4>:     jne    0x400db6 <main+22>
   0x0000000000400da6 <+6>:     mov    rax,QWORD PTR [rip+0x20299b]        # 0x603748 <stdin@@GLIBC_2.2.5>
   0x0000000000400dad <+13>:    mov    QWORD PTR [rip+0x2029b4],rax        # 0x603768 <infile>
   0x0000000000400db4 <+20>:    jmp    0x400e19 <main+121>
   0x0000000000400db6 <+22>:    mov    rbx,rsi
   0x0000000000400db9 <+25>:    cmp    edi,0x2
   0x0000000000400dbc <+28>:    jne    0x400df8 <main+88>
   0x0000000000400dbe <+30>:    mov    rdi,QWORD PTR [rsi+0x8]
   0x0000000000400dc2 <+34>:    mov    esi,0x4022b4
   0x0000000000400dc7 <+39>:    call   0x400c10 <fopen@plt>
   0x0000000000400dcc <+44>:    mov    QWORD PTR [rip+0x202995],rax        # 0x603768 <infile>
   0x0000000000400dd3 <+51>:    test   rax,rax
   0x0000000000400dd6 <+54>:    jne    0x400e19 <main+121>
   0x0000000000400dd8 <+56>:    mov    rcx,QWORD PTR [rbx+0x8]
   0x0000000000400ddc <+60>:    mov    rdx,QWORD PTR [rbx]
   0x0000000000400ddf <+63>:    mov    esi,0x4022b6
   0x0000000000400de4 <+68>:    mov    edi,0x1
   0x0000000000400de9 <+73>:    call   0x400c00 <__printf_chk@plt>
   0x0000000000400dee <+78>:    mov    edi,0x8
   0x0000000000400df3 <+83>:    call   0x400c20 <exit@plt>
   0x0000000000400df8 <+88>:    mov    rdx,QWORD PTR [rsi]
   0x0000000000400dfb <+91>:    mov    esi,0x4022d3
   0x0000000000400e00 <+96>:    mov    edi,0x1
   0x0000000000400e05 <+101>:   mov    eax,0x0
   0x0000000000400e0a <+106>:   call   0x400c00 <__printf_chk@plt>
   0x0000000000400e0f <+111>:   mov    edi,0x8
   0x0000000000400e14 <+116>:   call   0x400c20 <exit@plt>
   0x0000000000400e19 <+121>:   call   0x4013a2 <initialize_bomb>
   0x0000000000400e1e <+126>:   mov    edi,0x402338
   0x0000000000400e23 <+131>:   call   0x400b10 <puts@plt>
   0x0000000000400e28 <+136>:   mov    edi,0x402378
   0x0000000000400e2d <+141>:   call   0x400b10 <puts@plt>
   0x0000000000400e32 <+146>:   call   0x40149e <read_line>
   0x0000000000400e37 <+151>:   mov    rdi,rax
   0x0000000000400e3a <+154>:   call   0x400ee0 <phase_1>
   0x0000000000400e3f <+159>:   call   0x4015c4 <phase_defused>
   0x0000000000400e44 <+164>:   mov    edi,0x4023a8
   0x0000000000400e49 <+169>:   call   0x400b10 <puts@plt>
   0x0000000000400e4e <+174>:   call   0x40149e <read_line>
   0x0000000000400e53 <+179>:   mov    rdi,rax
   0x0000000000400e56 <+182>:   call   0x400efc <phase_2>
   0x0000000000400e5b <+187>:   call   0x4015c4 <phase_defused>
   0x0000000000400e60 <+192>:   mov    edi,0x4022ed
   0x0000000000400e65 <+197>:   call   0x400b10 <puts@plt>
   0x0000000000400e6a <+202>:   call   0x40149e <read_line>
   0x0000000000400e6f <+207>:   mov    rdi,rax
   0x0000000000400e72 <+210>:   call   0x400f43 <phase_3>
   0x0000000000400e77 <+215>:   call   0x4015c4 <phase_defused>
   0x0000000000400e7c <+220>:   mov    edi,0x40230b
   0x0000000000400e81 <+225>:   call   0x400b10 <puts@plt>
   0x0000000000400e86 <+230>:   call   0x40149e <read_line>
   0x0000000000400e8b <+235>:   mov    rdi,rax
   0x0000000000400e8e <+238>:   call   0x40100c <phase_4>
   0x0000000000400e93 <+243>:   call   0x4015c4 <phase_defused>
   0x0000000000400e98 <+248>:   mov    edi,0x4023d8
   0x0000000000400e9d <+253>:   call   0x400b10 <puts@plt>
   0x0000000000400ea2 <+258>:   call   0x40149e <read_line>
   0x0000000000400ea7 <+263>:   mov    rdi,rax
   0x0000000000400eaa <+266>:   call   0x401062 <phase_5>
   0x0000000000400eaf <+271>:   call   0x4015c4 <phase_defused>
   0x0000000000400eb4 <+276>:   mov    edi,0x40231a
   0x0000000000400eb9 <+281>:   call   0x400b10 <puts@plt>
   0x0000000000400ebe <+286>:   call   0x40149e <read_line>
   0x0000000000400ec3 <+291>:   mov    rdi,rax
   0x0000000000400ec6 <+294>:   call   0x4010f4 <phase_6>
   0x0000000000400ecb <+299>:   call   0x4015c4 <phase_defused>
   0x0000000000400ed0 <+304>:   mov    eax,0x0
   0x0000000000400ed5 <+309>:   pop    rbx
   0x0000000000400ed6 <+310>:   ret
End of assembler dump.

由于bomb程序没有strip,有符号表,因此反汇编出来的代码带有符号名称,这降低了解除bomb的难度。大致浏览一下代码结构,一共6个phase,每个phase的入口,都只有rdi一个参数,这个参数指向用户输入的字符串的开始地址。

call   0x40149e <read_line>
# rax是read_line的返回,付给rdi,传入phase接口
mov    rdi,rax
call   ...

phase_1

先看看phase_1的代码:

(gdb) disass 0x400ee0
Dump of assembler code for function phase_1:
   0x0000000000400ee0 <+0>:     sub    rsp,0x8
   0x0000000000400ee4 <+4>:     mov    esi,0x402400
   0x0000000000400ee9 <+9>:     call   0x401338 <strings_not_equal>
   0x0000000000400eee <+14>:    test   eax,eax
   0x0000000000400ef0 <+16>:    je     0x400ef7 <phase_1+23>
   0x0000000000400ef2 <+18>:    call   0x40143a <explode_bomb>
   0x0000000000400ef7 <+23>:    add    rsp,0x8
   0x0000000000400efb <+27>:    ret
End of assembler dump.

esi得到了一个地址,然后与rdi一起,传给了strings_not_equal接口,显然,这个接口用来判断两个字符串是否相同,eax是返回值,如果不等于0,explode bomb。

解除phase_1,我们只需要看一下esi这个地址指向的字符串是啥,输入一个完全一样的,就可以了:

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

这就是phase_1要输入的字符串:

(gdb) b *0x400efc
Breakpoint 2 at 0x400efc
(gdb) c
Continuing.
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的代码如下:

(gdb) disass 0x400efc
Dump of assembler code for function phase_2:
   0x0000000000400efc <+0>:     push   rbp
   0x0000000000400efd <+1>:     push   rbx
   0x0000000000400efe <+2>:     sub    rsp,0x28
   0x0000000000400f02 <+6>:     mov    rsi,rsp
   0x0000000000400f05 <+9>:     call   0x40145c <read_six_numbers>
   0x0000000000400f0a <+14>:    cmp    DWORD PTR [rsp],0x1
   0x0000000000400f0e <+18>:    je     0x400f30 <phase_2+52>
   0x0000000000400f10 <+20>:    call   0x40143a <explode_bomb>
   0x0000000000400f15 <+25>:    jmp    0x400f30 <phase_2+52>
   0x0000000000400f17 <+27>:    mov    eax,DWORD PTR [rbx-0x4]
   0x0000000000400f1a <+30>:    add    eax,eax
   0x0000000000400f1c <+32>:    cmp    DWORD PTR [rbx],eax
   0x0000000000400f1e <+34>:    je     0x400f25 <phase_2+41>
   0x0000000000400f20 <+36>:    call   0x40143a <explode_bomb>
   0x0000000000400f25 <+41>:    add    rbx,0x4
   0x0000000000400f29 <+45>:    cmp    rbx,rbp
   0x0000000000400f2c <+48>:    jne    0x400f17 <phase_2+27>
   0x0000000000400f2e <+50>:    jmp    0x400f3c <phase_2+64>
   0x0000000000400f30 <+52>:    lea    rbx,[rsp+0x4]
   0x0000000000400f35 <+57>:    lea    rbp,[rsp+0x18]
   0x0000000000400f3a <+62>:    jmp    0x400f17 <phase_2+27>
   0x0000000000400f3c <+64>:    add    rsp,0x28
   0x0000000000400f40 <+68>:    pop    rbx
   0x0000000000400f41 <+69>:    pop    rbp
   0x0000000000400f42 <+70>:    ret
End of assembler dump.

从符号名称上看,需要输入6个数字,rdi指向用户输入的字符串,rsi指向了栈顶。我们需要看一看read_six_numbers的代码:

(gdb) disass 0x40145c
Dump of assembler code for function read_six_numbers:
   0x000000000040145c <+0>:     sub    rsp,0x18
   0x0000000000401460 <+4>:     mov    rdx,rsi
   0x0000000000401463 <+7>:     lea    rcx,[rsi+0x4]
   0x0000000000401467 <+11>:    lea    rax,[rsi+0x14]
   0x000000000040146b <+15>:    mov    QWORD PTR [rsp+0x8],rax
   0x0000000000401470 <+20>:    lea    rax,[rsi+0x10]
   0x0000000000401474 <+24>:    mov    QWORD PTR [rsp],rax
   0x0000000000401478 <+28>:    lea    r9,[rsi+0xc]
   0x000000000040147c <+32>:    lea    r8,[rsi+0x8]
   0x0000000000401480 <+36>:    mov    esi,0x4025c3
   0x0000000000401485 <+41>:    mov    eax,0x0
   0x000000000040148a <+46>:    call   0x400bf0 <__isoc99_sscanf@plt>
   0x000000000040148f <+51>:    cmp    eax,0x5
   0x0000000000401492 <+54>:    jg     0x401499 <read_six_numbers+61>
   0x0000000000401494 <+56>:    call   0x40143a <explode_bomb>
   0x0000000000401499 <+61>:    add    rsp,0x18
   0x000000000040149d <+65>:    ret
End of assembler dump.

这个接口内部调用了sscanf,第2个参数esi指向了一个地址,这个地址指向格式解析字符串:

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

这就很明显了,需要用户输入6个数字。这6个数字解析之后的存放位置如下:

最后两个参数,只能压栈了。这几行代码,将rsi+0x10和rsi+0x14这两个地址,进行了压栈:

0x0000000000401467 <+11>:    lea    rax,[rsi+0x14]
0x000000000040146b <+15>:    mov    QWORD PTR [rsp+0x8],rax
0x0000000000401470 <+20>:    lea    rax,[rsi+0x10]
0x0000000000401474 <+24>:    mov    QWORD PTR [rsp],rax

压栈的规则是:必须8字节对齐,从右到左。所以,rsi+0x14这个地址,在当前stack frame的rsp+0x8位置,栈顶是最左边的参数。轻轻地将这两个参数压栈之后,最后调用sscanf,得到6个int,全部都在上一层的stack frame中。

现在就要来研究一下,这6个数字到底是什么,才不会bomb!

   0x0000000000400f05 <+9>:     call   0x40145c <read_six_numbers>
   0x0000000000400f0a <+14>:    cmp    DWORD PTR [rsp],0x1
   0x0000000000400f0e <+18>:    je     0x400f30 <phase_2+52>
   0x0000000000400f10 <+20>:    call   0x40143a <explode_bomb>

当read_six_numbers返回后,栈顶保存的是用户输入的第1个int,与0x1进行比较,不相等就bomb,显然第1个数字只能是1。

   0x0000000000400f15 <+25>:    jmp    0x400f30 <phase_2+52>
   0x0000000000400f17 <+27>:    mov    eax,DWORD PTR [rbx-0x4]
   0x0000000000400f1a <+30>:    add    eax,eax
   0x0000000000400f1c <+32>:    cmp    DWORD PTR [rbx],eax
   0x0000000000400f1e <+34>:    je     0x400f25 <phase_2+41>
   0x0000000000400f20 <+36>:    call   0x40143a <explode_bomb>
   0x0000000000400f25 <+41>:    add    rbx,0x4
   0x0000000000400f29 <+45>:    cmp    rbx,rbp
   0x0000000000400f2c <+48>:    jne    0x400f17 <phase_2+27>
   0x0000000000400f2e <+50>:    jmp    0x400f3c <phase_2+64>
   0x0000000000400f30 <+52>:    lea    rbx,[rsp+0x4]
   0x0000000000400f35 <+57>:    lea    rbp,[rsp+0x18]
   0x0000000000400f3a <+62>:    jmp    0x400f17 <phase_2+27>

仔细推敲这段代码的逻辑,为了不bomb,后面的数字,都是前面数字的2倍,关键就是add eax eax这一行指令。rbp指向的地址,刚好是6个int占用空间的上界,cmp指令进行的是地址的比较,到了这个地址,这段循环就结束。因此,这6个数字就是:1 2 4 8 16 32

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

phase_3

phase_3接口的代码如下:

(gdb) disass 0x400f43
Dump of assembler code for function phase_3:
   0x0000000000400f43 <+0>:     sub    rsp,0x18
   0x0000000000400f47 <+4>:     lea    rcx,[rsp+0xc]
   0x0000000000400f4c <+9>:     lea    rdx,[rsp+0x8]
   0x0000000000400f51 <+14>:    mov    esi,0x4025cf
   0x0000000000400f56 <+19>:    mov    eax,0x0
   0x0000000000400f5b <+24>:    call   0x400bf0 <__isoc99_sscanf@plt>
   0x0000000000400f60 <+29>:    cmp    eax,0x1
   0x0000000000400f63 <+32>:    jg     0x400f6a <phase_3+39>
   0x0000000000400f65 <+34>:    call   0x40143a <explode_bomb>
   0x0000000000400f6a <+39>:    cmp    DWORD PTR [rsp+0x8],0x7
   0x0000000000400f6f <+44>:    ja     0x400fad <phase_3+106>
   0x0000000000400f71 <+46>:    mov    eax,DWORD PTR [rsp+0x8]
   0x0000000000400f75 <+50>:    jmp    QWORD PTR [rax*8+0x402470]
   0x0000000000400f7c <+57>:    mov    eax,0xcf
   0x0000000000400f81 <+62>:    jmp    0x400fbe <phase_3+123>
   0x0000000000400f83 <+64>:    mov    eax,0x2c3
   0x0000000000400f88 <+69>:    jmp    0x400fbe <phase_3+123>
   0x0000000000400f8a <+71>:    mov    eax,0x100
   0x0000000000400f8f <+76>:    jmp    0x400fbe <phase_3+123>
   0x0000000000400f91 <+78>:    mov    eax,0x185
   0x0000000000400f96 <+83>:    jmp    0x400fbe <phase_3+123>
   0x0000000000400f98 <+85>:    mov    eax,0xce
   0x0000000000400f9d <+90>:    jmp    0x400fbe <phase_3+123>
   0x0000000000400f9f <+92>:    mov    eax,0x2aa
   0x0000000000400fa4 <+97>:    jmp    0x400fbe <phase_3+123>
   0x0000000000400fa6 <+99>:    mov    eax,0x147
   0x0000000000400fab <+104>:   jmp    0x400fbe <phase_3+123>
   0x0000000000400fad <+106>:   call   0x40143a <explode_bomb>
   0x0000000000400fb2 <+111>:   mov    eax,0x0
   0x0000000000400fb7 <+116>:   jmp    0x400fbe <phase_3+123>
   0x0000000000400fb9 <+118>:   mov    eax,0x137
   0x0000000000400fbe <+123>:   cmp    eax,DWORD PTR [rsp+0xc]
   0x0000000000400fc2 <+127>:   je     0x400fc9 <phase_3+134>
   0x0000000000400fc4 <+129>:   call   0x40143a <explode_bomb>
   0x0000000000400fc9 <+134>:   add    rsp,0x18
   0x0000000000400fcd <+138>:   ret
End of assembler dump.

开始依然是调用sscanf接口,rdi保持没变,指向用户输入,rsi指向格式字符串,地址为0x4025cf,看看这里面是啥:

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

如此变明了了,phase 3需要我们输入两个数字。紧跟后面的cmp指令也说明了这一点。跳过了第一处引爆点,来到下面这段代码:

   0x0000000000400f6a <+39>:    cmp    DWORD PTR [rsp+0x8],0x7
   0x0000000000400f6f <+44>:    ja     0x400fad <phase_3+106>
   0x0000000000400f71 <+46>:    mov    eax,DWORD PTR [rsp+0x8]
   0x0000000000400f75 <+50>:    jmp    QWORD PTR [rax*8+0x402470]

cmp结果如果above,即unsigned大于,就调到0x400fad地址,看看这个地址,又是个引爆点。那我们就不能让cmp满足ja条件。参与cmp的第1个operand是rsp+0x8这个地址的内容,这个地址在前面的代码中,被赋给了rdx,属于sscanf的第3个参数,因此,cmp是在用我们输入的第1个int数字,与立即数7进行比较。不能ja,那么这个数就只能小于等于7了。紧跟着的mov,将输入的第1个int传给rax,然后一个间接跳转,地址为rax*8+0x402470,就是在0x402470的位置偏移rax个8字节。此时,我们就要看看0x402470开始位置,都有些啥了:

(gdb) x/zg 0x402470
0x402470:       0x0000000000400f7c
(gdb) x/zg 0x402470+8
0x402478:       0x0000000000400fb9
(gdb) x/zg 0x402470+8*2
0x402480:       0x0000000000400f83
(gdb) x/zg 0x402470+8*3
0x402488:       0x0000000000400f8a
(gdb) x/zg 0x402470+8*4
0x402490:       0x0000000000400f91
(gdb) x/zg 0x402470+8*5
0x402498:       0x0000000000400f98
(gdb) x/zg 0x402470+8*6
0x4024a0:       0x0000000000400f9f
(gdb) x/zg 0x402470+8*7
0x4024a8:       0x0000000000400fa6

rax中取值不能大于7,以上就是可能计算出的全部地址。

仔细观察这些地址,它们就是后面指令的地址,而且它们基本都一样,给rax一个值,然后再调到一个固定的位置:

   0x0000000000400fbe <+123>:   cmp    eax,DWORD PTR [rsp+0xc]
   0x0000000000400fc2 <+127>:   je     0x400fc9 <phase_3+134>
   0x0000000000400fc4 <+129>:   call   0x40143a <explode_bomb>
   0x0000000000400fc9 <+134>:   add    rsp,0x18
   0x0000000000400fcd <+138>:   ret

这个位置通过rax与rsp+0xc地址的内容进行比较,必须相等,才能绕过后面的引爆点。

因此,phase 3的输入有多个可能,第1个int决定了跳转地址,这里有决定了第2个int的值。我的选择是1和311。第1个int为1,给rax的值为311,然后就紧跟着cmp,没有跳转,代码执行最快。

1 311
Halfway there!

phase_4

还是先要看看phase_4的代码,找找线索:

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

前面几行代码,与phase 3完全一样,给esi的地址也一样,这说明,phase 4也要求我们输入两个int。调用sscanf后面的cmp也说明了,如果不是输入两个int,bomb。然后,第1个int与0xe比较,jbe是signed小于等于,必须要满足,否则bomb。下面开始准备调用func4:

后面的代码说明,必须要让func4返回0,否则bomb。下面是func4的代码:

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

func4里面没有引爆点,我们需要仔细跟踪代码逻辑,看看rdi在什么值的时候,能够让func4返回0。咋一看func4,还是个递归接口,好恐怖!只能从rdx=0xe开始,一行行代码跟踪计算。最后会发现,ecx的值与edi比较,edi是我们输入的第1个int,ecx=7,edi要先小于等于7,然后要大于等于7,才能顺利带着0离开func4。因此,我们输入的第1个int就是7

func4顺利返回0后,才出现使用到输入的第2个int的代码,第2个int等于0

7 0
So you got that one.  Try this one.

phase_5

(gdb) disass 0x401062
Dump of assembler code for function phase_5:
   0x0000000000401062 <+0>:     push   rbx
   0x0000000000401063 <+1>:     sub    rsp,0x20
   0x0000000000401067 <+5>:     mov    rbx,rdi
   0x000000000040106a <+8>:     mov    rax,QWORD PTR fs:0x28
   0x0000000000401073 <+17>:    mov    QWORD PTR [rsp+0x18],rax
   0x0000000000401078 <+22>:    xor    eax,eax
   0x000000000040107a <+24>:    call   0x40131b <string_length>
   0x000000000040107f <+29>:    cmp    eax,0x6
   0x0000000000401082 <+32>:    je     0x4010d2 <phase_5+112>
   0x0000000000401084 <+34>:    call   0x40143a <explode_bomb>
   0x0000000000401089 <+39>:    jmp    0x4010d2 <phase_5+112>
   0x000000000040108b <+41>:    movzx  ecx,BYTE PTR [rbx+rax*1]
   0x000000000040108f <+45>:    mov    BYTE PTR [rsp],cl
   0x0000000000401092 <+48>:    mov    rdx,QWORD PTR [rsp]
   0x0000000000401096 <+52>:    and    edx,0xf
   0x0000000000401099 <+55>:    movzx  edx,BYTE PTR [rdx+0x4024b0]
   0x00000000004010a0 <+62>:    mov    BYTE PTR [rsp+rax*1+0x10],dl
   0x00000000004010a4 <+66>:    add    rax,0x1
   0x00000000004010a8 <+70>:    cmp    rax,0x6
   0x00000000004010ac <+74>:    jne    0x40108b <phase_5+41>
   0x00000000004010ae <+76>:    mov    BYTE PTR [rsp+0x16],0x0
   0x00000000004010b3 <+81>:    mov    esi,0x40245e
   0x00000000004010b8 <+86>:    lea    rdi,[rsp+0x10]
   0x00000000004010bd <+91>:    call   0x401338 <strings_not_equal>
   0x00000000004010c2 <+96>:    test   eax,eax
   0x00000000004010c4 <+98>:    je     0x4010d9 <phase_5+119>
   0x00000000004010c6 <+100>:   call   0x40143a <explode_bomb>
   0x00000000004010cb <+105>:   nop    DWORD PTR [rax+rax*1+0x0]
   0x00000000004010d0 <+110>:   jmp    0x4010d9 <phase_5+119>
   0x00000000004010d2 <+112>:   mov    eax,0x0
   0x00000000004010d7 <+117>:   jmp    0x40108b <phase_5+41>
   0x00000000004010d9 <+119>:   mov    rax,QWORD PTR [rsp+0x18]
   0x00000000004010de <+124>:   xor    rax,QWORD PTR fs:0x28
   0x00000000004010e7 <+133>:   je     0x4010ee <phase_5+140>
   0x00000000004010e9 <+135>:   call   0x400b30 <__stack_chk_fail@plt>
   0x00000000004010ee <+140>:   add    rsp,0x20
   0x00000000004010f2 <+144>:   pop    rbx
   0x00000000004010f3 <+145>:   ret
End of assembler dump.

开头有几行代码是编译器插入的对抗buffer overflow的代码:

   0x000000000040106a <+8>:     mov    rax,QWORD PTR fs:0x28
   0x0000000000401073 <+17>:    mov    QWORD PTR [rsp+0x18],rax
   0x0000000000401078 <+22>:    xor    eax,eax

可以忽略这几行代码。

紧接着call,从符号名称上看,这个调用返回用户输入字符串的长度,而且长度必须要等于6。两次很随性的跳转之后,来到下面这段关键的代码:

   0x000000000040108b <+41>:    movzx  ecx,BYTE PTR [rbx+rax*1]
   0x000000000040108f <+45>:    mov    BYTE PTR [rsp],cl
   0x0000000000401092 <+48>:    mov    rdx,QWORD PTR [rsp]
   0x0000000000401096 <+52>:    and    edx,0xf
   0x0000000000401099 <+55>:    movzx  edx,BYTE PTR [rdx+0x4024b0]
   0x00000000004010a0 <+62>:    mov    BYTE PTR [rsp+rax*1+0x10],dl
   0x00000000004010a4 <+66>:    add    rax,0x1
   0x00000000004010a8 <+70>:    cmp    rax,0x6
   0x00000000004010ac <+74>:    jne    0x40108b <phase_5+41>
   0x00000000004010ae <+76>:    mov    BYTE PTR [rsp+0x16],0x0
   0x00000000004010b3 <+81>:    mov    esi,0x40245e
   0x00000000004010b8 <+86>:    lea    rdi,[rsp+0x10]
   0x00000000004010bd <+91>:    call   0x401338 <strings_not_equal>
   0x00000000004010c2 <+96>:    test   eax,eax
   0x00000000004010c4 <+98>:    je     0x4010d9 <phase_5+119>
   0x00000000004010c6 <+100>:   call   0x40143a <explode_bomb>

第1次进来这段代码时,rax=0,rbx一直指向用户输入。这段代码通过用户输入的每个字符计算了一个偏移,0-15,加到0x4024b0地址,从这个地址取字节,存入rsp+0x10开始的位置。这个过程重复6次后,对两个字符串进行比较,必须要相等。

(gdb) x/s 0x40245e
0x40245e:       "flyers"
(gdb) x/15c 0x4024b0
0x4024b0 <array.3449>:  109 'm' 97 'a'  100 'd' 117 'u' 105 'i' 101 'e' 114 'r' 115 's'
0x4024b8 <array.3449+8>:        110 'n' 102 'f' 111 'o' 116 't' 118 'v' 98 'b'  121 'y'

所以,输入的每个字符的ASCII码的低4bit作为基于0x4024b0地址的偏移,6个偏移对应6个字符,这6个字符是flyers。我们需要用flyers和0x4024b0地址开始的16个字节,反推输入。由于只取了输入字符的低4bit,这一关的解也有多个,我使用ionefg过关:

ionefg
Good work!  On to the next...

phase_6

面对眼花缭乱的汇编代码,我觉得语言好无力,完全不知道应该如何描述这一关的内容...代码就不贴了,大致说一下这段代码都在干什么吧。

首先是读取6个数字,然后对这6个数字进行一个合法性判断,这里是一个烧脑的双重循环,即判断输入数字的范围,也判断相互关系。这6个数字,只能在1-6之间,而且每个都不相同。然后,用一个循环,用7原地减去每个数字。然后,从神奇的0x6032d0这个地址(属于.data)开始的一段范围内,以被7减去后的值为index,读取6个指针到stack的前半段。然后还有一段指向指针的指针的操作....晕死了,指针的值一会儿作为值,一会儿作为地址....最后,按照某种顺序,对这一组指针指向的地址的值做比较,比较如果不成功,Bomb!

有一个感觉,虽然x86属于传统意义上的CISC,但实际上在直接阅读汇编的时候,x86的可读性可能更好。

通关截屏

$ ./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!
1 311
Halfway there!
7 0
So you got that one.  Try this one.
ionefg
Good work!  On to the next...
4 3 2 1 6 5
Congratulations! You've defused the bomb!

secret phase

debug的过程,看到代码中,有一个secret phase,在最后才可能开启。但仔细研究了代码后发现,网上下载的自学版本,无法进入secret phase!没有输入的调用,内部流程直接给了一个错误的输入,然后就走到了最后的Congratulations...

《CSAPP》Data Lab

我将《CSAPP》Data Lab分成了两个部分:

本文链接:https://cs.pynote.net/hd/asm/202310061/

-- EOF --

-- MORE --