TOC
Bomb Lab
在 CSAPP Lab 下载对应的 tar
包并解压。解压之后有三个文件,README
没啥用,bomb.c
中只给出了 main
函数,并且对我们进行了一番嘲讽 🤣:
/*************************************************************************** * 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. ***************************************************************************/
所以最重要的就是 bomb
这个可执行文件。
我们的工具有两个,一个是 objdump
,另一个是 gdb
。
首先使用 objdump
得到汇编代码:
$ touch assembly$ objdump -d bomb > assembly
为了阅读方便,我们将结果保存在文件中。
汇编代码非常长,我们在其中找到 main
函数的部分:
0000000000400da0 <main>: 400da0: 53 push %rbx 400da1: 83 ff 01 cmp $0x1,%edi 400da4: 75 10 jne 400db6 <main+0x16> 400da6: 48 8b 05 9b 29 20 00 mov 0x20299b(%rip),%rax # 603748 <stdin@@GLIBC_2.2.5> 400dad: 48 89 05 b4 29 20 00 mov %rax,0x2029b4(%rip) # 603768 <infile> 400db4: eb 63 jmp 400e19 <main+0x79> 400db6: 48 89 f3 mov %rsi,%rbx 400db9: 83 ff 02 cmp $0x2,%edi 400dbc: 75 3a jne 400df8 <main+0x58> 400dbe: 48 8b 7e 08 mov 0x8(%rsi),%rdi 400dc2: be b4 22 40 00 mov $0x4022b4,%esi 400dc7: e8 44 fe ff ff call 400c10 <fopen@plt> 400dcc: 48 89 05 95 29 20 00 mov %rax,0x202995(%rip) # 603768 <infile> 400dd3: 48 85 c0 test %rax,%rax 400dd6: 75 41 jne 400e19 <main+0x79> 400dd8: 48 8b 4b 08 mov 0x8(%rbx),%rcx 400ddc: 48 8b 13 mov (%rbx),%rdx 400ddf: be b6 22 40 00 mov $0x4022b6,%esi 400de4: bf 01 00 00 00 mov $0x1,%edi 400de9: e8 12 fe ff ff call 400c00 <__printf_chk@plt> 400dee: bf 08 00 00 00 mov $0x8,%edi 400df3: e8 28 fe ff ff call 400c20 <exit@plt> 400df8: 48 8b 16 mov (%rsi),%rdx 400dfb: be d3 22 40 00 mov $0x4022d3,%esi 400e00: bf 01 00 00 00 mov $0x1,%edi 400e05: b8 00 00 00 00 mov $0x0,%eax 400e0a: e8 f1 fd ff ff call 400c00 <__printf_chk@plt> 400e0f: bf 08 00 00 00 mov $0x8,%edi 400e14: e8 07 fe ff ff call 400c20 <exit@plt> 400e19: e8 84 05 00 00 call 4013a2 <initialize_bomb> 400e1e: bf 38 23 40 00 mov $0x402338,%edi 400e23: e8 e8 fc ff ff call 400b10 <puts@plt> 400e28: bf 78 23 40 00 mov $0x402378,%edi 400e2d: e8 de fc ff ff call 400b10 <puts@plt> 400e32: e8 67 06 00 00 call 40149e <read_line> 400e37: 48 89 c7 mov %rax,%rdi 400e3a: e8 a1 00 00 00 call 400ee0 <phase_1> 400e3f: e8 80 07 00 00 call 4015c4 <phase_defused> 400e44: bf a8 23 40 00 mov $0x4023a8,%edi 400e49: e8 c2 fc ff ff call 400b10 <puts@plt> 400e4e: e8 4b 06 00 00 call 40149e <read_line> 400e53: 48 89 c7 mov %rax,%rdi 400e56: e8 a1 00 00 00 call 400efc <phase_2> 400e5b: e8 64 07 00 00 call 4015c4 <phase_defused> 400e60: bf ed 22 40 00 mov $0x4022ed,%edi 400e65: e8 a6 fc ff ff call 400b10 <puts@plt> 400e6a: e8 2f 06 00 00 call 40149e <read_line> 400e6f: 48 89 c7 mov %rax,%rdi 400e72: e8 cc 00 00 00 call 400f43 <phase_3> 400e77: e8 48 07 00 00 call 4015c4 <phase_defused> 400e7c: bf 0b 23 40 00 mov $0x40230b,%edi 400e81: e8 8a fc ff ff call 400b10 <puts@plt> 400e86: e8 13 06 00 00 call 40149e <read_line> 400e8b: 48 89 c7 mov %rax,%rdi 400e8e: e8 79 01 00 00 call 40100c <phase_4> 400e93: e8 2c 07 00 00 call 4015c4 <phase_defused> 400e98: bf d8 23 40 00 mov $0x4023d8,%edi 400e9d: e8 6e fc ff ff call 400b10 <puts@plt> 400ea2: e8 f7 05 00 00 call 40149e <read_line> 400ea7: 48 89 c7 mov %rax,%rdi 400eaa: e8 b3 01 00 00 call 401062 <phase_5> 400eaf: e8 10 07 00 00 call 4015c4 <phase_defused> 400eb4: bf 1a 23 40 00 mov $0x40231a,%edi 400eb9: e8 52 fc ff ff call 400b10 <puts@plt> 400ebe: e8 db 05 00 00 call 40149e <read_line> 400ec3: 48 89 c7 mov %rax,%rdi 400ec6: e8 29 02 00 00 call 4010f4 <phase_6> 400ecb: e8 f4 06 00 00 call 4015c4 <phase_defused> 400ed0: b8 00 00 00 00 mov $0x0,%eax 400ed5: 5b pop %rbx 400ed6: c3 ret 400ed7: 90 nop 400ed8: 90 nop 400ed9: 90 nop 400eda: 90 nop 400edb: 90 nop 400edc: 90 nop 400edd: 90 nop 400ede: 90 nop 400edf: 90 nop
下面分阶段讲解。
phase_1
阶段一从地址 0x400e32
开始,这里调用了 read_line
函数从输入中(可以是命令行里的输入,也可以是文件里的输入,详见 bomb.c
里的注释)读取一行。
现在的问题是读取的内容存放在了哪里?
我起初认为读取的内容在寄存器中,在地址 0x400e37
设置断点(break * 0x400e37
)发现,无论输入是什么,在调用了 read_line
地址 0x400e37
函数后,相关的寄存器(info registers
)rax
的值总是 0x603780
,于是猜测读取的内容以字符串的形式存放在地址 0x603780
处(这很关键,不然会误入歧途 🤣)。
vgalaxy@vgalaxy-VirtualBox:~/Lab/bomb$ gdb bombGNU gdb (Ubuntu 10.1-2ubuntu2) 10.1.90.20210411-gitCopyright (C) 2021 Free Software Foundation, Inc.License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>This is free software: you are free to change and redistribute it.There is NO WARRANTY, to the extent permitted by law.Type "show copying" and "show warranty" for details.This GDB was configured as "x86_64-linux-gnu".Type "show configuration" for configuration details.For bug reporting instructions, please see:<https://www.gnu.org/software/gdb/bugs/>.Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".Type "apropos word" to search for commands related to "word"...Reading symbols from bomb...(gdb) break * 0x400e37Breakpoint 1 at 0x400e37: file bomb.c, line 74.(gdb) runStarting program: /home/vgalaxy/Lab/bomb/bombWelcome to my fiendish little bomb. You have 6 phases withwhich to blow yourself up. Have a nice day!test
Breakpoint 1, main (argc=<optimized out>, argv=<optimized out>) at bomb.c:7474 phase_1(input); /* Run the phase */(gdb) info registersrax 0x603780 6305664rbx 0x402210 4203024rcx 0x4 4rdx 0x1 1rsi 0x603780 6305664rdi 0x603786 6305670rbp 0x0 0x0rsp 0x7fffffffded0 0x7fffffffded0r8 0x603780 6305664r9 0x6046b0 6309552r10 0xfffffffffffffb7f -1153r11 0x7ffff7dfe8a0 140737352034464r12 0x400c90 4197520r13 0x0 0r14 0x0 0r15 0x0 0rip 0x400e37 0x400e37 <main+151>eflags 0x202 [ IF ]cs 0x33 51ss 0x2b 43ds 0x0 0es 0x0 0fs 0x0 0gs 0x0 0
地址 400e37
这一行将寄存器 rax
的值赋给 rdi
,并调用 phase_1
函数:
0000000000400ee0 <phase_1>: 400ee0: 48 83 ec 08 sub $0x8,%rsp 400ee4: be 00 24 40 00 mov $0x402400,%esi 400ee9: e8 4a 04 00 00 call 401338 <strings_not_equal> 400eee: 85 c0 test %eax,%eax 400ef0: 74 05 je 400ef7 <phase_1+0x17> 400ef2: e8 43 05 00 00 call 40143a <explode_bomb> 400ef7: 48 83 c4 08 add $0x8,%rsp 400efb: c3 ret
地址 0x400ee4
这一行将 0x402400
赋给寄存器 esi
,寄存器 rsi
的高四字节置 0
。接着便调用了 strings_not_equal
函数:
0000000000401338 <strings_not_equal>: 401338: 41 54 push %r12 40133a: 55 push %rbp 40133b: 53 push %rbx 40133c: 48 89 fb mov %rdi,%rbx 40133f: 48 89 f5 mov %rsi,%rbp 401342: e8 d4 ff ff ff call 40131b <string_length> 401347: 41 89 c4 mov %eax,%r12d 40134a: 48 89 ef mov %rbp,%rdi 40134d: e8 c9 ff ff ff call 40131b <string_length> 401352: ba 01 00 00 00 mov $0x1,%edx 401357: 41 39 c4 cmp %eax,%r12d 40135a: 75 3f jne 40139b <strings_not_equal+0x63> 40135c: 0f b6 03 movzbl (%rbx),%eax 40135f: 84 c0 test %al,%al 401361: 74 25 je 401388 <strings_not_equal+0x50> 401363: 3a 45 00 cmp 0x0(%rbp),%al 401366: 74 0a je 401372 <strings_not_equal+0x3a> 401368: eb 25 jmp 40138f <strings_not_equal+0x57> 40136a: 3a 45 00 cmp 0x0(%rbp),%al 40136d: 0f 1f 00 nopl (%rax) 401370: 75 24 jne 401396 <strings_not_equal+0x5e> 401372: 48 83 c3 01 add $0x1,%rbx 401376: 48 83 c5 01 add $0x1,%rbp 40137a: 0f b6 03 movzbl (%rbx),%eax 40137d: 84 c0 test %al,%al 40137f: 75 e9 jne 40136a <strings_not_equal+0x32> 401381: ba 00 00 00 00 mov $0x0,%edx 401386: eb 13 jmp 40139b <strings_not_equal+0x63> 401388: ba 00 00 00 00 mov $0x0,%edx 40138d: eb 0c jmp 40139b <strings_not_equal+0x63> 40138f: ba 01 00 00 00 mov $0x1,%edx 401394: eb 05 jmp 40139b <strings_not_equal+0x63> 401396: ba 01 00 00 00 mov $0x1,%edx 40139b: 89 d0 mov %edx,%eax 40139d: 5b pop %rbx 40139e: 5d pop %rbp 40139f: 41 5c pop %r12 4013a1: c3 ret
观察从地址 0x40133c
到地址 0x40135a
的部分,通过调用 string_length
函数依次计算寄存器 rdi
和 rsi
所指向地址的字符串的长度(观察 string_length: 0x40131b
的 (%rdi)
可知)。若长度不同,则返回 1
,注意 phase_1: 0x400eee
检测返回值是否为 0
,若不为 0
,则引爆炸弹。可以推断,若两个字符串相同,那么返回值就是 0
,炸弹就不会被引爆。
(╯‵□′)╯炸弹!•••*~●
于是,我们只要得到寄存器 rsi
中的 0x402400
所指向的字符串即可,我们使用下面的指令:
(gdb) x/s 0x4024000x402400: "Border relations with Canada have never been better."
将结果保存在 input
文件中即可:
$ touch input$ echo "Border relations with Canada have never been better." > input
来测试一下:
$ ./bomb inputWelcome to my fiendish little bomb. You have 6 phases withwhich to blow yourself up. Have a nice day!Phase 1 defused. How about the next one?
( ^_^ ) 不错嘛。
注:在走弯路的过程中 🤣,探索了
read_line
函数究竟是如何将字符串存放在地址0x603780
处的。在命令行输入的场景下,read_line
函数调用了skip
函数,skip
函数调用了**fgets
函数**和blank_line
函数。给几个观察寄存器的断点:
skip: 0x401421
,此时寄存器rsi
会有从字符串第二个字符开始的ASCII
码,若字符串为123456
,则编码为0x35343332
,这里只有四个字符,推测是按四个字符读取
blank_line: 0x4013e1
,此时寄存器rbp
会有从字符串第一个字符的ASCII
码,在上面的例子中,就是0x31
,这里推测是预读第一个字符,若遇到空行则继续读取
read_line: 0x40154b
,此时寄存器rcx
给出了读取的字符串的长度加2
的结果,在read_line
函数的后面,将寄存器rcx
的值减去了2
,并以此为依据,在read_line: 0x4015ab
处给字符串的最后加上了0
,此时寄存器rax
的值为0
,尚不清楚其作用 🤣
4015ab: c6 84 01 80 37 60 00 movb $0x0,0x603780(%rcx,%rax,1)
phase_2
阶段二从 main: 0x400e4e
开始,同样调用 read_line
函数从输入中读取一行,读取的内容以字符串的形式存放在地址 0x6037d0
处。
接下来 main
函数调用 phase_2
函数:
0000000000400efc <phase_2>: 400efc: 55 push %rbp 400efd: 53 push %rbx 400efe: 48 83 ec 28 sub $0x28,%rsp 400f02: 48 89 e6 mov %rsp,%rsi 400f05: e8 52 05 00 00 call 40145c <read_six_numbers>
注意 phase_2: 400f02
处,这里将寄存器 rsp
的值赋给寄存器 rsi
,通过断点调试可知此时寄存器 rsi
的值为 0x7fffffffde80
。接着调用 read_six_numbers
函数:
000000000040145c <read_six_numbers>: 40145c: 48 83 ec 18 sub $0x18,%rsp 401460: 48 89 f2 mov %rsi,%rdx 401463: 48 8d 4e 04 lea 0x4(%rsi),%rcx 401467: 48 8d 46 14 lea 0x14(%rsi),%rax 40146b: 48 89 44 24 08 mov %rax,0x8(%rsp) 401470: 48 8d 46 10 lea 0x10(%rsi),%rax 401474: 48 89 04 24 mov %rax,(%rsp) 401478: 4c 8d 4e 0c lea 0xc(%rsi),%r9 40147c: 4c 8d 46 08 lea 0x8(%rsi),%r8 401480: be c3 25 40 00 mov $0x4025c3,%esi 401485: b8 00 00 00 00 mov $0x0,%eax 40148a: e8 61 f7 ff ff call 400bf0 <__isoc99_sscanf@plt> 40148f: 83 f8 05 cmp $0x5,%eax 401492: 7f 05 jg 401499 <read_six_numbers+0x3d> 401494: e8 a1 ff ff ff call 40143a <explode_bomb> 401499: 48 83 c4 18 add $0x18,%rsp 40149d: c3 ret
先看前四行,刚进入 read_six_numbers
函数,寄存器 rsp
的值就从 0x7fffffffde80
变为了 0x7fffffffde78
(函数调用所致)。到 read_six_numbers: 0x40146b
时,相关寄存器的值如下:
rsp: 0x7fffffffde60
rdx: 0x7fffffffde80
rcx: 0x7fffffffde84
rax: 0x7fffffffde94
read_six_numbers: 0x40146b
将 0x7fffffffde94
赋给地址 0x7fffffffde68
处,接着寄存器 rax
的值变为 0x7fffffffde90
,并将 0x7fffffffde90
赋给地址 0x7fffffffde60
处。使用 gdb
可以观察:
(gdb) stepi0x0000000000401478 in read_six_numbers ()(gdb) x/2g 0x7fffffffde600x7fffffffde60: 0x00007fffffffde90 0x00007fffffffde94
其实看到这里是一头雾水 🤣
到了 read_six_numbers: 0x40148a
,read_six_numbers
函数调用了 __isoc99_sscanf
函数,然后对其返回值进行测试,若 ≤ 5
,则引爆炸弹。通过查阅sscanf可知,其返回值是成功赋值的接收参数的数量。根据函数名,我们可以推测输入的格式是 6
个以空格分割的数字。尝试输入 1 2 3 4 5 6
,并在 gdb
中一通乱搞🤣,发现从地址 0x7fffffffde80
开始的 6
个字节存储了我们输入的数字。
(gdb) stepi0x0000000000401499 in read_six_numbers ()(gdb) x/3g 0x7fffffffde800x7fffffffde80: 0x0000000200000001 0x00000004000000030x7fffffffde90: 0x0000000600000005
不太理解为什么是以这种形式存储 🤣
下面就容易了,从 read_six_numbers
函数返回到 phase_2
函数后:
400f0a: 83 3c 24 01 cmpl $0x1,(%rsp) 400f0e: 74 20 je 400f30 <phase_2+0x34> 400f10: e8 25 05 00 00 call 40143a <explode_bomb> 400f15: eb 19 jmp 400f30 <phase_2+0x34> 400f17: 8b 43 fc mov -0x4(%rbx),%eax 400f1a: 01 c0 add %eax,%eax 400f1c: 39 03 cmp %eax,(%rbx) 400f1e: 74 05 je 400f25 <phase_2+0x29> 400f20: e8 15 05 00 00 call 40143a <explode_bomb> 400f25: 48 83 c3 04 add $0x4,%rbx 400f29: 48 39 eb cmp %rbp,%rbx 400f2c: 75 e9 jne 400f17 <phase_2+0x1b> 400f2e: eb 0c jmp 400f3c <phase_2+0x40> 400f30: 48 8d 5c 24 04 lea 0x4(%rsp),%rbx 400f35: 48 8d 6c 24 18 lea 0x18(%rsp),%rbp 400f3a: eb db jmp 400f17 <phase_2+0x1b> 400f3c: 48 83 c4 28 add $0x28,%rsp 400f40: 5b pop %rbx 400f41: 5d pop %rbp 400f42: c3 ret
phase_2: 0x400f0a
测试寄存器 rsp
中对应的地址中存储的值是否为 1
,我们知道此时寄存器 rsp
中存储的值为 0x7fffffffde80
,也就是说这里测试输入的第一个数字是否为 1
。若为 1
,我们跳转到 phase_2: 0x400f30
,将寄存器 rbx
设置为 0x7fffffffde84
,寄存器 rbp
设置为 0x7fffffffde98
(终止条件)。接着我们跳转到 phase_2: 0x400f17
,将地址 0x7fffffffde80
中的值赋给寄存器 eax
,并乘 2
,与地址 0x7fffffffde84
中的值比较,若相等,则跳过炸弹引爆,并将 rbx
加 4
,判断是否到了 0x7fffffffde98
。若没有,则继续跳转到 phase_2: 0x400f17
。
也就是说,这里是一个循环。为了不引爆炸弹,我们输入的 6
个数字,后一个数字应该是前一个数字的 2
倍。
于是,我们将结果保存在文件中:
$ echo "1 2 4 8 16 32" >> input
来测试一下:
$ ./bomb inputWelcome to my fiendish little bomb. You have 6 phases withwhich to blow yourself up. Have a nice day!Phase 1 defused. How about the next one?That's number 2. Keep going!
没有问题。
phase_3
阶段三从 main: 0x400e6a
开始,同样调用 read_line
函数从输入中读取一行,读取的内容以字符串的形式存放在地址 0x603820
处。
接下来 main
函数调用 phase_3
函数:
0000000000400f43 <phase_3>: 400f43: 48 83 ec 18 sub $0x18,%rsp 400f47: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx 400f4c: 48 8d 54 24 08 lea 0x8(%rsp),%rdx 400f51: be cf 25 40 00 mov $0x4025cf,%esi 400f56: b8 00 00 00 00 mov $0x0,%eax 400f5b: e8 90 fc ff ff call 400bf0 <__isoc99_sscanf@plt>
断点调试可知,在 phase_3: 0x400f47
处,寄存器 rsp
的值为 0x7fffffffde90
,在调用 __isoc99_sscanf
函数前,寄存器 rcx
的值为 0x7fffffffde9c
,寄存器 rdx
的值为 0x7fffffffde98
。联系 phase_2
推测,这便是我们输入的 2
个数字(phase_3: 0x400f60
处检测参数数量是否 > 1
)存放的地址。
400f60: 83 f8 01 cmp $0x1,%eax 400f63: 7f 05 jg 400f6a <phase_3+0x27> 400f65: e8 d0 04 00 00 call 40143a <explode_bomb> 400f6a: 83 7c 24 08 07 cmpl $0x7,0x8(%rsp) 400f6f: 77 3c ja 400fad <phase_3+0x6a> 400f71: 8b 44 24 08 mov 0x8(%rsp),%eax 400f75: ff 24 c5 70 24 40 00 jmp *0x402470(,%rax,8) 400f7c: b8 cf 00 00 00 mov $0xcf,%eax 400f81: eb 3b jmp 400fbe <phase_3+0x7b> 400f83: b8 c3 02 00 00 mov $0x2c3,%eax 400f88: eb 34 jmp 400fbe <phase_3+0x7b> 400f8a: b8 00 01 00 00 mov $0x100,%eax 400f8f: eb 2d jmp 400fbe <phase_3+0x7b> 400f91: b8 85 01 00 00 mov $0x185,%eax 400f96: eb 26 jmp 400fbe <phase_3+0x7b> 400f98: b8 ce 00 00 00 mov $0xce,%eax 400f9d: eb 1f jmp 400fbe <phase_3+0x7b> 400f9f: b8 aa 02 00 00 mov $0x2aa,%eax 400fa4: eb 18 jmp 400fbe <phase_3+0x7b> 400fa6: b8 47 01 00 00 mov $0x147,%eax 400fab: eb 11 jmp 400fbe <phase_3+0x7b> 400fad: e8 88 04 00 00 call 40143a <explode_bomb> 400fb2: b8 00 00 00 00 mov $0x0,%eax 400fb7: eb 05 jmp 400fbe <phase_3+0x7b> 400fb9: b8 37 01 00 00 mov $0x137,%eax 400fbe: 3b 44 24 0c cmp 0xc(%rsp),%eax 400fc2: 74 05 je 400fc9 <phase_3+0x86> 400fc4: e8 71 04 00 00 call 40143a <explode_bomb> 400fc9: 48 83 c4 18 add $0x18,%rsp 400fcd: c3 ret
phase_3: 0x400f6a
处检测第一个数字是否 ≤ 7
,并将该数字赋给寄存器 eax
。
下面关键的地方是 phase_3: 0x400f75
:
400f75: ff 24 c5 70 24 40 00 jmp *0x402470(,%rax,8)
这里根据寄存器 rax
中的值进行无条件跳转,我们使用 gdb
观察地址 0x402470
周围的值:
(gdb) nexti0x0000000000400f75 in phase_3 ()(gdb) x/8g 0x4024700x402470: 0x0000000000400f7c 0x0000000000400fb90x402480: 0x0000000000400f83 0x0000000000400f8a0x402490: 0x0000000000400f91 0x0000000000400f980x4024a0: 0x0000000000400f9f 0x0000000000400fa6
注意到这里的每一条地址都恰好对应了一条 mov
语句和一条 jmp
语句,其跳转的目标便是 phase_3: 0x400fbe
:
400fbe: 3b 44 24 0c cmp 0xc(%rsp),%eax
也就是说,我们要让第二个数字和 mov
语句赋给寄存器 eax
的值相同,于是我们有如下 8
种可能的输入(注意将十六进制转换为十进制):
0 207
1 311
2 707
3 256
4 389
5 206
6 682
7 327
我们任选(其实是我第一次通过的那个 🤣)一个保存在文件中:
$ echo "1 311" >> input
测试一下:
$ ./bomb inputWelcome to my fiendish little bomb. You have 6 phases withwhich to blow yourself up. Have a nice day!Phase 1 defused. How about the next one?That's number 2. Keep going!Halfway there!
一半了 ~(≧▽≦)/~ 啦啦啦。
phase_4
阶段三从 main: 0x400e86
开始,同样调用 read_line
函数从输入中读取一行,读取的内容以字符串的形式存放在地址 0x603870
处。
接下来 main
函数调用 phase_4
函数:
000000000040100c <phase_4>: 40100c: 48 83 ec 18 sub $0x18,%rsp 401010: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx 401015: 48 8d 54 24 08 lea 0x8(%rsp),%rdx 40101a: be cf 25 40 00 mov $0x4025cf,%esi 40101f: b8 00 00 00 00 mov $0x0,%eax 401024: e8 c7 fb ff ff call 400bf0 <__isoc99_sscanf@plt> 401029: 83 f8 02 cmp $0x2,%eax 40102c: 75 07 jne 401035 <phase_4+0x29> 40102e: 83 7c 24 08 0e cmpl $0xe,0x8(%rsp) 401033: 76 05 jbe 40103a <phase_4+0x2e> 401035: e8 00 04 00 00 call 40143a <explode_bomb> 40103a: ba 0e 00 00 00 mov $0xe,%edx 40103f: be 00 00 00 00 mov $0x0,%esi 401044: 8b 7c 24 08 mov 0x8(%rsp),%edi 401048: e8 81 ff ff ff call 400fce <func4> 40104d: 85 c0 test %eax,%eax 40104f: 75 07 jne 401058 <phase_4+0x4c> 401051: 83 7c 24 0c 00 cmpl $0x0,0xc(%rsp) 401056: 74 05 je 40105d <phase_4+0x51> 401058: e8 dd 03 00 00 call 40143a <explode_bomb> 40105d: 48 83 c4 18 add $0x18,%rsp 401061: c3 ret
前面的部分我们已经很熟悉了,这里要求读取 2
个数字,存放在地址 0x7fffffffde98
处。
寄存器
rsp
的值为0x7fffffffde90
从 phase_4: 0x40103a
开始,将寄存器 edx
赋值为 e
,寄存器 esi
赋值为 0
,寄存器 edi
赋值为输入的第一个数字。然后调用 func4
函数,并检测其返回值是否为 0
,若不为 0
,则引爆炸弹。同时检测第二个数字是否为 0
。于是下面的重点便是 func4
函数。
0000000000400fce <func4>: 400fce: 48 83 ec 08 sub $0x8,%rsp 400fd2: 89 d0 mov %edx,%eax 400fd4: 29 f0 sub %esi,%eax 400fd6: 89 c1 mov %eax,%ecx 400fd8: c1 e9 1f shr $0x1f,%ecx 400fdb: 01 c8 add %ecx,%eax 400fdd: d1 f8 sar %eax 400fdf: 8d 0c 30 lea (%rax,%rsi,1),%ecx 400fe2: 39 f9 cmp %edi,%ecx 400fe4: 7e 0c jle 400ff2 <func4+0x24> 400fe6: 8d 51 ff lea -0x1(%rcx),%edx 400fe9: e8 e0 ff ff ff call 400fce <func4> 400fee: 01 c0 add %eax,%eax 400ff0: eb 15 jmp 401007 <func4+0x39> 400ff2: b8 00 00 00 00 mov $0x0,%eax 400ff7: 39 f9 cmp %edi,%ecx 400ff9: 7d 0c jge 401007 <func4+0x39> 400ffb: 8d 71 01 lea 0x1(%rcx),%esi 400ffe: e8 cb ff ff ff call 400fce <func4> 401003: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax 401007: 48 83 c4 08 add $0x8,%rsp 40100b: c3 ret
首先注意到这是一个递归函数,在 func4
函数内部有两次调用:
- 第一次发生在
func4: 0x400fe9
,不递归调用的条件是寄存器ecx
的值≤
寄存器edi
的值,在此之前将寄存器edx
赋值为rcx-1
- 第二次发生在
func4: 0x400ffe
,不递归调用的条件是寄存器ecx
的值≥
寄存器edi
的值,在此之前将寄存器esi
赋值为rcx+1
我们的目标是让 func4
函数返回 0
,为此我们不能走到 func4: 0x400ffe
,因为该调用语句的后面的 func4: 0x401003
让寄存器 eax
的值发生了变化(注意到 func4: 0x400ff2
将寄存器 eax
的值赋值为 0
,这里只是直觉,并不是严谨的解释 🤣)。
我们还发现这里跳转的条件中使用了寄存器 edi
的值,而寄存器 edi
的值是我们输入的第一个数字。若我们输入的第一个数字是 1
(还是直觉 🤣),从 func4: 0x400ffe
执行到 func4: 0x400fe4
,相关寄存器的值如下:
edx: e
eax: 从 e 变为 7 (e>>1)
ecx: 从 e 变为 0 (e>>31,相当于取符号位) 再变为 7
此时,不满足寄存器 ecx
的值 ≤
寄存器 edi
的值,我们将寄存器 edx
赋值为 rcx-1
,即 6
,递归调用 func4
函数:
从 func4: 0x400ffe
执行到 func4: 0x400fe4
,相关寄存器的值如下:
edx: 6
eax: 从 6 变为 3 (e>>1)
ecx: 从 6 变为 0 (e>>31,相当于取符号位) 再变为 3
此时,不满足寄存器 ecx
的值 ≤
寄存器 edi
的值,我们将寄存器 edx
赋值为 rcx-1
,即 2
,递归调用 func4
函数:
从 func4: 0x400ffe
执行到 func4: 0x400fe4
,相关寄存器的值如下:
edx: 2
eax: 从 2 变为 1 (e>>1)
ecx: 从 2 变为 0 (e>>31,相当于取符号位) 再变为 1
此时,满足寄存器 ecx
的值 ≤
寄存器 edi
的值,我们跳转到 func4: 0x400ff2
,将寄存器 eax
赋值为 0
。我们发现,此时也满足寄存器 ecx
的值 ≥
寄存器 edi
的值。于是我们成功的跳过了第二个递归调用,递归函数开始返回。
返回后的语句在 func4: 0x400fee
,由于这条语句只是让寄存器 eax
的值乘 2
,然而寄存器 eax
的值为 0
,所以最终的返回值还是 0
。
看样子我们碰运气猜出了答案 🤣,实际上可以考虑输入的第一个数字是
2
,我们会在两次递归调用func4: 0x400fe9(△)
后递归调用func4: 0x400ffe(☆)
,并继续调用func4: 0x400fe9(□)
。在(□)
返回后,寄存器eax
的值为0
,在(☆)
返回后寄存器eax
的值为1
,两次(△)
返回让寄存器eax
的值变为原来的4
倍,总之肯定不是0
了。对于更严谨的说明,可以写出等价的 C 语言程序(但是我不会 🤣)
于是我们将答案保存在文件中:
$ echo "1 0" >> input
测试一下:
$ ./bomb inputWelcome to my fiendish little bomb. You have 6 phases withwhich to blow yourself up. Have a nice day!Phase 1 defused. How about the next one?That's number 2. Keep going!Halfway there!So you got that one. Try this one.
注:
- 后来发现第一个数字还可以是
3
或者7
,规律也许是小于e
并且有效位均为1
?func4
函数感觉有点像二分?
phase_5
阶段五从 main: 0x400ea2
开始,同样调用 read_line
函数从输入中读取一行,读取的内容以字符串的形式存放在地址 0x6038c0
处。
接下来 main
函数调用 phase_5
函数:
0000000000401062 <phase_5>: 401062: 53 push %rbx 401063: 48 83 ec 20 sub $0x20,%rsp 401067: 48 89 fb mov %rdi,%rbx 40106a: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax 401071: 00 00 401073: 48 89 44 24 18 mov %rax,0x18(%rsp) 401078: 31 c0 xor %eax,%eax 40107a: e8 9c 02 00 00 call 40131b <string_length>
在 phase_5: 0x40106a
之后,寄存器 rax
为一个随机的值(联系下面的 phase_5: 0x4010e9
,猜测是为了进行栈破坏检测,这里的随机值便是哨兵值,亦或金丝雀值,详见 CSAPP §3.10.4
),并将其存入地址 0x7fffffffdea8
处(寄存器 rsp
的值为 0x7fffffffde90
)。接着 phase_5
调用 string_length
函数检测寄存器 rdi 中对应地址的字符串的长度(也就是我们输入的字符串长度)是否为 6
:
40107f: 83 f8 06 cmp $0x6,%eax 401082: 74 4e je 4010d2 <phase_5+0x70> 401084: e8 b1 03 00 00 call 40143a <explode_bomb> 401089: eb 47 jmp 4010d2 <phase_5+0x70> 40108b: 0f b6 0c 03 movzbl (%rbx,%rax,1),%ecx 40108f: 88 0c 24 mov %cl,(%rsp) 401092: 48 8b 14 24 mov (%rsp),%rdx 401096: 83 e2 0f and $0xf,%edx 401099: 0f b6 92 b0 24 40 00 movzbl 0x4024b0(%rdx),%edx 4010a0: 88 54 04 10 mov %dl,0x10(%rsp,%rax,1) 4010a4: 48 83 c0 01 add $0x1,%rax 4010a8: 48 83 f8 06 cmp $0x6,%rax 4010ac: 75 dd jne 40108b <phase_5+0x29> 4010ae: c6 44 24 16 00 movb $0x0,0x16(%rsp) 4010b3: be 5e 24 40 00 mov $0x40245e,%esi 4010b8: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi 4010bd: e8 76 02 00 00 call 401338 <strings_not_equal> 4010c2: 85 c0 test %eax,%eax 4010c4: 74 13 je 4010d9 <phase_5+0x77> 4010c6: e8 6f 03 00 00 call 40143a <explode_bomb> 4010cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1) 4010d0: eb 07 jmp 4010d9 <phase_5+0x77> 4010d2: b8 00 00 00 00 mov $0x0,%eax 4010d7: eb b2 jmp 40108b <phase_5+0x29> 4010d9: 48 8b 44 24 18 mov 0x18(%rsp),%rax 4010de: 64 48 33 04 25 28 00 xor %fs:0x28,%rax 4010e5: 00 00 4010e7: 74 05 je 4010ee <phase_5+0x8c> 4010e9: e8 42 fa ff ff call 400b30 <__stack_chk_fail@plt> 4010ee: 48 83 c4 20 add $0x20,%rsp 4010f2: 5b pop %rbx 4010f3: c3 ret
若为 6
,则跳转到 phase_5: 0x4010d2
,置寄存器 rax
为 0
,再跳转到 phase_5: 0x40108b
。
从 phase_5: 0x40108b
到 phase_5: 0x4010ac
是一个循环,**主要工作是依次取我们输入的字符的低 4
位,作为一个字节数组的索引,取出索引的字节,存入从地址 0x7fffffffdea0
开始的一块内存空间。**这个字节数组从 0x4024b0
开始,我们来看看:
(gdb) x/2g 0x4024b00x4024b0 <array.3449>: 0x737265697564616d 0x6c796276746f666e
换一种显示方式 🤣:
(gdb) x/16b 0x4024b00x4024b0 <array.3449>: 0x6d 0x61 0x64 0x75 0x69 0x65 0x72 0x730x4024b8 <array.3449+8>: 0x6e 0x66 0x6f 0x74 0x76 0x62 0x79 0x6c
举一个例子,若我们输入的第一个字符为 1
,对应的 ASCII
码为 0x31
。phase_5: 0x40108b
取得我们第一个字符(注意 phase_5: 0x4401067
将地址赋值给了寄存器 rbx
),并符号扩展为一个双字,存入寄存器 ecx
中。在 phase_5: 0x401099
之前,寄存器 edx
的值从 0x31
变为 1
(取输入字符的低 4
位,防止索引越界),同时地址 0x7fffffffde90
处的值为 0x31
(不知道有什么用 🤣)。
执行完 phase_5: 0x401099
后,有 array[%rdx]→ %edx
,对照字节数组可知,此时寄存器 edx
的值为 0x61
,我们将其存入地址 0x7fffffffdea0
处,此时附近内存空间的情况如下:
(gdb) x/3g 0x7fffffffde900x7fffffffde90: 0x0000000000000031 0x00000000004014310x7fffffffdea0: 0x00007fffffffdf61
这里地址
0x7fffffffdea0
原来的内容是0x00007fffffffdfb8
将我们输入的 6
个字符作为索引,得到 6
个字节以后。我们在这 6
个字节之后加上一个 0
(可见 phase_5: 0x4010ae
),形成一个字符串,并与地址 0x40245e
的字符串进行比较,我们来看看:
(gdb) x/s 0x40245e0x40245e: "flyers"(gdb) x/6bx 0x40245e0x40245e: 0x66 0x6c 0x79 0x65 0x72 0x73
注:
gdb
中x
的一些参数
- 数字:显示几个内存单元
- 输出格式
/s
字符串/x
十六进制/d
十进制- 以多少字节为一个内存单元
/b
一个字节/w
四个字节/g
八个字节若省略(或直接回车 🤣),则默认为上一次的配置(或命令)
与字节数组对应,可知索引依次为 9 15 14 5 6 7
,我们可以使用 i o n e f g
得到这组索引(不唯一)。
可对照
man ascii
编码
我们将答案保存在文件中,注意这里没有空格,上面加了空格是为了能看清楚:
$ echo "ionefg" >> input
测试一下:
$ ./bomb inputWelcome to my fiendish little bomb. You have 6 phases withwhich to blow yourself up. Have a nice day!Phase 1 defused. How about the next one?That's number 2. Keep going!Halfway there!So you got that one. Try this one.Good work! On to the next...
ヽ(✿゚▽゚)ノ
phase_6
阶段六从 main: 0x400ebe
开始,同样调用 read_line
函数从输入中读取一行,读取的内容以字符串的形式存放在地址 0x603910
处。接下来 main
函数调用 phase_6
函数:
00000000004010f4 <phase_6>: 4010f4: 41 56 push %r14 4010f6: 41 55 push %r13 4010f8: 41 54 push %r12 4010fa: 55 push %rbp 4010fb: 53 push %rbx 4010fc: 48 83 ec 50 sub $0x50,%rsp 401100: 49 89 e5 mov %rsp,%r13 401103: 48 89 e6 mov %rsp,%rsi 401106: e8 51 03 00 00 call 40145c <read_six_numbers>
这里类似阶段二,调用了 read_six_numbers
函数,可以分析得到这 6
个数字存储在以 0x7fffffffde30
开始的地址空间中(此时寄存器 rsp
的值便是 0x7fffffffde30
)。
phase_6
函数的主体由五个循环构成。下面我们来依次分析:
- 循环一:检测输入的
6
个数字是否各不相同,并且都≤ 6
40110b: 49 89 e6 mov %rsp,%r14 40110e: 41 bc 00 00 00 00 mov $0x0,%r12d 401114: 4c 89 ed mov %r13,%rbp 401117: 41 8b 45 00 mov 0x0(%r13),%eax 40111b: 83 e8 01 sub $0x1,%eax 40111e: 83 f8 05 cmp $0x5,%eax 401121: 76 05 jbe 401128 <phase_6+0x34> 401123: e8 12 03 00 00 call 40143a <explode_bomb> 401128: 41 83 c4 01 add $0x1,%r12d 40112c: 41 83 fc 06 cmp $0x6,%r12d 401130: 74 21 je 401153 <phase_6+0x5f> 401132: 44 89 e3 mov %r12d,%ebx 401135: 48 63 c3 movslq %ebx,%rax 401138: 8b 04 84 mov (%rsp,%rax,4),%eax 40113b: 39 45 00 cmp %eax,0x0(%rbp) 40113e: 75 05 jne 401145 <phase_6+0x51> 401140: e8 f5 02 00 00 call 40143a <explode_bomb> 401145: 83 c3 01 add $0x1,%ebx 401148: 83 fb 05 cmp $0x5,%ebx 40114b: 7e e8 jle 401135 <phase_6+0x41> 40114d: 49 83 c5 04 add $0x4,%r13 401151: eb c1 jmp 401114 <phase_6+0x20>
寄存器 r13
中存放着数字的地址,初始为 0x7fffffffde30
,即第 1 个数字的地址。寄存器 r12d
是计数器,初始为 1
,代表检测第 1
个数字,若等于 6
,说明所有的数字都通过了检测(第 6
个数字 ≤ 6
,不必再测试是否与其他数字相同,因为之前已经测试过了,详见下面的分析),那么就跳出循环来到 phase_6: 0x401153
。
在循环的内部还有一个循环,是检测当前的数字和之后的数字是否都不同,即从 phase_6: 0x401132
到 phase_6: 0x40114b
的部分,不再赘述。
- 循环二:用
7
减去输入的各个数字,并存储在原来的地方
401153: 48 8d 74 24 18 lea 0x18(%rsp),%rsi 401158: 4c 89 f0 mov %r14,%rax 40115b: b9 07 00 00 00 mov $0x7,%ecx 401160: 89 ca mov %ecx,%edx 401162: 2b 10 sub (%rax),%edx 401164: 89 10 mov %edx,(%rax) 401166: 48 83 c0 04 add $0x4,%rax 40116a: 48 39 f0 cmp %rsi,%rax 40116d: 75 f1 jne 401160 <phase_6+0x6c>
phase_6: 0x401153
将寄存器 rsi
的值设置为 0x7fffffffde48
,用于循环是否终止的测试,寄存器 r14
存放着数字的起始地址,即 0x7fffffffde30
,并赋值给了寄存器 rax
。其余部分不再赘述。
- 循环三:以各个数字为索引取得链表各个节点的地址
40116f: be 00 00 00 00 mov $0x0,%esi 401174: eb 21 jmp 401197 <phase_6+0xa3> 401176: 48 8b 52 08 mov 0x8(%rdx),%rdx 40117a: 83 c0 01 add $0x1,%eax 40117d: 39 c8 cmp %ecx,%eax 40117f: 75 f5 jne 401176 <phase_6+0x82> 401181: eb 05 jmp 401188 <phase_6+0x94> 401183: ba d0 32 60 00 mov $0x6032d0,%edx 401188: 48 89 54 74 20 mov %rdx,0x20(%rsp,%rsi,2) 40118d: 48 83 c6 04 add $0x4,%rsi 401191: 48 83 fe 18 cmp $0x18,%rsi 401195: 74 14 je 4011ab <phase_6+0xb7> 401197: 8b 0c 34 mov (%rsp,%rsi,1),%ecx 40119a: 83 f9 01 cmp $0x1,%ecx 40119d: 7e e4 jle 401183 <phase_6+0x8f> 40119f: b8 01 00 00 00 mov $0x1,%eax 4011a4: ba d0 32 60 00 mov $0x6032d0,%edx 4011a9: eb cb jmp 401176 <phase_6+0x82>
这个部分就有意思了,我们发现链表存储在以 0x6032d0
开始的一块内存空间中,来看一看:
(gdb) x/24w 0x6032d00x6032d0 <node1>: 0x0000014c 0x00000001 0x006032e0 0x000000000x6032e0 <node2>: 0x000000a8 0x00000002 0x006032f0 0x000000000x6032f0 <node3>: 0x0000039c 0x00000003 0x00603300 0x000000000x603300 <node4>: 0x000002b3 0x00000004 0x00603310 0x000000000x603310 <node5>: 0x000001dd 0x00000005 0x00603320 0x000000000x603320 <node6>: 0x000001bb 0x00000006 0x00000000 0x00000000
注意上面的内存空间内容的第三列,除了尾节点,其余节点的这个部分都存储了下一个节点的地址,所以我们判断这是一个链表。
我们回到循环本身,在 phase_6: 0x40116f
,寄存器 esi
被置为 0
,其作用相当于索引。刚开始,我们取出第一个数字(实际上是 7-第一个数字
)到寄存器 ecx
中,记为 x
。若 x!=1
,置寄存器 eax
为 1
,寄存器 edx
为 0x6032d0
,并通过一个内循环得到第 x
个节点的地址(在寄存器 rdx
中),并存放在 0x7fffffffde50
中;否则,直接置寄存器 rdx
为第一个节点的地址,并存放在 0x7fffffffde50
中。
对所有的数字进行相同操作后,我们可以从地址 0x7fffffffde50
观察到 6
个节点的地址:
(gdb) x/10g 0x7fffffffde300x7fffffffde30: 0x0000000400000003 0x00000006000000050x7fffffffde40: 0x0000000200000001 0x00000000000000000x7fffffffde50: 0x00000000006032f0 0x00000000006033000x7fffffffde60: 0x0000000000603310 0x00000000006033200x7fffffffde70: 0x00000000006032d0 0x00000000006032e0
这里假设输入的
6
个数字为4 3 2 1 6 5
,用7
减去后便是3 4 5 6 1 2
,可以观察到地址的对应
- 循环四:根据
0x7fffffffde50
处6
个节点的地址修改链表
4011ab: 48 8b 5c 24 20 mov 0x20(%rsp),%rbx 4011b0: 48 8d 44 24 28 lea 0x28(%rsp),%rax 4011b5: 48 8d 74 24 50 lea 0x50(%rsp),%rsi 4011ba: 48 89 d9 mov %rbx,%rcx 4011bd: 48 8b 10 mov (%rax),%rdx 4011c0: 48 89 51 08 mov %rdx,0x8(%rcx) 4011c4: 48 83 c0 08 add $0x8,%rax 4011c8: 48 39 f0 cmp %rsi,%rax 4011cb: 74 05 je 4011d2 <phase_6+0xde> 4011cd: 48 89 d1 mov %rdx,%rcx 4011d0: eb eb jmp 4011bd <phase_6+0xc9> 4011d2: 48 c7 42 08 00 00 00 movq $0x0,0x8(%rdx) 4011d9: 00
将存储的第一个节点的地址赋给寄存器 rbx
,寄存器 rbx
再赋给寄存器 rcx
,置寄存器 rax
为存储第二个节点的地址(这里少了一个**“的”**,你细品 🤣),置寄存器 rsi
为 0x7fffffffde80
,用于循环是否终止的测试。
在 phase_6: 0x4011c0
,我们将存储的第二个节点的地址赋给 0x8(%rcx)
,也就是存储的第一个节点的下一个节点。以上面为例,也就是节点 0x6032f0
的下一个节点改为 0x603300
(虽然原来就是如此 🤣)。接着将寄存器 rax
加上 8
(即存储的第三个节点的地址),判断循环是否终止,此时未终止,我们将存储的第二个节点的地址赋给寄存器 rcx
,然后跳转到 phase_6: 0x4011bd
,将存储的第二个节点的下一个节点改为存储的第三个节点的地址……以此类推,最后在 phase_6: 0x4011d2
将链表的尾节点的下一个节点设置为 0
。
综上可知,修改后链表的结构是 node3 → node4 → node5 → node6 → node1 → node2
:
(gdb) x/24w 0x6032d00x6032d0 <node1>: 0x0000014c 0x00000001 0x006032e0 0x000000000x6032e0 <node2>: 0x000000a8 0x00000002 0x00000000 0x000000000x6032f0 <node3>: 0x0000039c 0x00000003 0x00603300 0x000000000x603300 <node4>: 0x000002b3 0x00000004 0x00603310 0x000000000x603310 <node5>: 0x000001dd 0x00000005 0x00603320 0x000000000x603320 <node6>: 0x000001bb 0x00000006 0x006032d0 0x00000000
- 循环五:测试修改过的链表各节点的存储的数字是否为降序排列
4011da: bd 05 00 00 00 mov $0x5,%ebp 4011df: 48 8b 43 08 mov 0x8(%rbx),%rax 4011e3: 8b 00 mov (%rax),%eax 4011e5: 39 03 cmp %eax,(%rbx) 4011e7: 7d 05 jge 4011ee <phase_6+0xfa> 4011e9: e8 4c 02 00 00 call 40143a <explode_bomb> 4011ee: 48 8b 5b 08 mov 0x8(%rbx),%rbx 4011f2: 83 ed 01 sub $0x1,%ebp 4011f5: 75 e8 jne 4011df <phase_6+0xeb> 4011f7: 48 83 c4 50 add $0x50,%rsp 4011fb: 5b pop %rbx 4011fc: 5d pop %rbp 4011fd: 41 5c pop %r12 4011ff: 41 5d pop %r13 401201: 41 5e pop %r14 401203: c3 ret
置寄存器 ebp
为 5
,用于循环是否终止的测试。注意寄存器 rbx
中是存储的第一个节点的地址,也就是链表的头节点,我们将目前链表的第二个节点存储的数字(寄存器 eax
中)与链表的头节点存储的数字进行比较,若 头节点存储的数字≥第二个节点存储的数字
,则继续循环。由此我们推测,我们修改后的链表,元素应该是降序排列的。再次观察原链表:
(gdb) x/24w 0x6032d00x6032d0 <node1>: 0x0000014c 0x00000001 0x006032e0 0x000000000x6032e0 <node2>: 0x000000a8 0x00000002 0x006032f0 0x000000000x6032f0 <node3>: 0x0000039c 0x00000003 0x00603300 0x000000000x603300 <node4>: 0x000002b3 0x00000004 0x00603310 0x000000000x603310 <node5>: 0x000001dd 0x00000005 0x00603320 0x000000000x603320 <node6>: 0x000001bb 0x00000006 0x00000000 0x00000000
我们应该让链表的结构变为 node3 → node4 → node5 → node6 → node1 → node2
,也就是我们上面的例子 🤣。
我们将答案保存在文件中:
$ echo "4 3 2 1 6 5" >> input
测试一下:
$ ./bomb inputWelcome to my fiendish little bomb. You have 6 phases withwhich to blow yourself up. Have a nice day!Phase 1 defused. How about the next one?That's number 2. Keep going!Halfway there!So you got that one. Try this one.Good work! On to the next...Congratulations! You've defused the bomb!
O(∩_∩)O 哈!
secret_phase
等等,还没结束呢 🤣。在 bomb.c
的最后有这样一段注释:
/* Wow, they got it! But isn't something... missing? Perhaps * something they overlooked? Mua ha ha ha ha! */
return 0;}
我们回顾整个反汇编代码,发现在 phase_defused: 0x401630
处好像可以调用 secret_phase
函数:
00000000004015c4 <phase_defused>: 4015c4: 48 83 ec 78 sub $0x78,%rsp 4015c8: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax 4015cf: 00 00 4015d1: 48 89 44 24 68 mov %rax,0x68(%rsp) 4015d6: 31 c0 xor %eax,%eax 4015d8: 83 3d 81 21 20 00 06 cmpl $0x6,0x202181(%rip) # 603760 <num_input_strings> 4015df: 75 5e jne 40163f <phase_defused+0x7b> 4015e1: 4c 8d 44 24 10 lea 0x10(%rsp),%r8 4015e6: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx 4015eb: 48 8d 54 24 08 lea 0x8(%rsp),%rdx 4015f0: be 19 26 40 00 mov $0x402619,%esi 4015f5: bf 70 38 60 00 mov $0x603870,%edi 4015fa: e8 f1 f5 ff ff call 400bf0 <__isoc99_sscanf@plt> 4015ff: 83 f8 03 cmp $0x3,%eax 401602: 75 31 jne 401635 <phase_defused+0x71> 401604: be 22 26 40 00 mov $0x402622,%esi 401609: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi 40160e: e8 25 fd ff ff call 401338 <strings_not_equal> 401613: 85 c0 test %eax,%eax 401615: 75 1e jne 401635 <phase_defused+0x71> 401617: bf f8 24 40 00 mov $0x4024f8,%edi 40161c: e8 ef f4 ff ff call 400b10 <puts@plt> 401621: bf 20 25 40 00 mov $0x402520,%edi 401626: e8 e5 f4 ff ff call 400b10 <puts@plt> 40162b: b8 00 00 00 00 mov $0x0,%eax 401630: e8 0d fc ff ff call 401242 <secret_phase> 401635: bf 58 25 40 00 mov $0x402558,%edi 40163a: e8 d1 f4 ff ff call 400b10 <puts@plt> 40163f: 48 8b 44 24 68 mov 0x68(%rsp),%rax 401644: 64 48 33 04 25 28 00 xor %fs:0x28,%rax 40164b: 00 00 40164d: 74 05 je 401654 <phase_defused+0x90> 40164f: e8 dc f4 ff ff call 400b30 <__stack_chk_fail@plt> 401654: 48 83 c4 78 add $0x78,%rsp 401658: c3 ret 401659: 90 nop 40165a: 90 nop 40165b: 90 nop 40165c: 90 nop 40165d: 90 nop 40165e: 90 nop 40165f: 90 nop
现在我们研究如何走到 phase_defused: 0x401630
。首先在 phase_defused: 0x4015d8
对地址 0x603760
的值进行了检测,若不为 6
则跳过调用 secret_phase
。通过枚举法 🤣,我们很容易得到,在 main: 0x400ecb
处调用 phase_defused
时,地址 0x603760
的值为 6
(所以地址 0x603760
的值为阶段数),也就是说隐藏阶段在阶段六之后。
然后,在 phase_defused: 0x4015ff
处对 __isoc99_sscanf
函数的返回值进行了检测。通过前后对比,可知 sscanf
将地址 0x603870
处的 3
个以空格分割的字符传入了以地址 0x7fffffffde38
开头的内存空间中,前两个解释为数字,最后的一个解释为字符串:
(gdb) x/s 0x4026190x402619: "%d %d %s"
然而我们发现地址 0x402622
处存放的是我们在阶段四输入的内容(只有 2 个数字):
(gdb) x/w 0x6038700x603870 <input_strings+240>: 0x00302031
为此我们需要在阶段四的答案后面加上一个字符串,根据下面调用了 strings_not_equal
函数可知,这个字符串应该和地址 0x402520
存放的字符串相同:
(gdb) x/s 0x4026220x402622: "DrEvil"
点题了,点题了 🤣
为此我们修改 input
文件,使其变为:
$ cat inputBorder relations with Canada have never been better.1 2 4 8 16 321 3111 0 DrEvilionefg4 3 2 1 6 5
注意
phase_4: 0x401029
对sscanf
的返回值进行了检测,由于对应的模式中只有两个数字,所以再加上一个字符串并不影响检测:
(gdb) x/s 0x4025cf0x4025cf: "%d %d"
然后我们就能够进入隐藏阶段了:
Curses, you've found the secret phase!But finding it and solving it are quite different...
DrEvil
继续嘲讽 🤣
来看一看隐藏阶段的反汇编代码:
0000000000401242 <secret_phase>: 401242: 53 push %rbx 401243: e8 56 02 00 00 call 40149e <read_line> 401248: ba 0a 00 00 00 mov $0xa,%edx 40124d: be 00 00 00 00 mov $0x0,%esi 401252: 48 89 c7 mov %rax,%rdi 401255: e8 76 f9 ff ff call 400bd0 <strtol@plt> 40125a: 48 89 c3 mov %rax,%rbx 40125d: 8d 40 ff lea -0x1(%rax),%eax 401260: 3d e8 03 00 00 cmp $0x3e8,%eax 401265: 76 05 jbe 40126c <secret_phase+0x2a> 401267: e8 ce 01 00 00 call 40143a <explode_bomb> 40126c: 89 de mov %ebx,%esi 40126e: bf f0 30 60 00 mov $0x6030f0,%edi 401273: e8 8c ff ff ff call 401204 <fun7> 401278: 83 f8 02 cmp $0x2,%eax 40127b: 74 05 je 401282 <secret_phase+0x40> 40127d: e8 b8 01 00 00 call 40143a <explode_bomb> 401282: bf 38 24 40 00 mov $0x402438,%edi 401287: e8 84 f8 ff ff call 400b10 <puts@plt> 40128c: e8 33 03 00 00 call 4015c4 <phase_defused> 401291: 5b pop %rbx 401292: c3 ret 401293: 90 nop 401294: 90 nop 401295: 90 nop 401296: 90 nop 401297: 90 nop 401298: 90 nop 401299: 90 nop 40129a: 90 nop 40129b: 90 nop 40129c: 90 nop 40129d: 90 nop 40129e: 90 nop 40129f: 90 nop
secret_phase: 0x401243
从输入中读取一行,读取的内容存放在地址 0x603960
中(寄存器 rax
的值)。secret_phase: 0x401255
调用了 strtol
函数,将字符串转换为对应的数字,保存在寄存器 rax
中。rax → rbx
,eax-=1
,然后检测 eax
是否 ≤ 1000
(即 0x3e8
,后面二叉树中元素的最大值为 1001
)。
然后,ebx → esi
,0x6030f0 → edi
,并调用 fun7
函数,测试其返回值是否为 2
。
我们先看看地址 0x6030f0
里是什么:
(gdb) x/120w 0x6030f00x6030f0 <n1>: 0x00000024 0x00000000 0x00603110 0x000000000x603100 <n1+16>: 0x00603130 0x00000000 0x00000000 0x000000000x603110 <n21>: 0x00000008 0x00000000 0x00603190 0x000000000x603120 <n21+16>: 0x00603150 0x00000000 0x00000000 0x000000000x603130 <n22>: 0x00000032 0x00000000 0x00603170 0x000000000x603140 <n22+16>: 0x006031b0 0x00000000 0x00000000 0x000000000x603150 <n32>: 0x00000016 0x00000000 0x00603270 0x000000000x603160 <n32+16>: 0x00603230 0x00000000 0x00000000 0x000000000x603170 <n33>: 0x0000002d 0x00000000 0x006031d0 0x000000000x603180 <n33+16>: 0x00603290 0x00000000 0x00000000 0x000000000x603190 <n31>: 0x00000006 0x00000000 0x006031f0 0x000000000x6031a0 <n31+16>: 0x00603250 0x00000000 0x00000000 0x000000000x6031b0 <n34>: 0x0000006b 0x00000000 0x00603210 0x000000000x6031c0 <n34+16>: 0x006032b0 0x00000000 0x00000000 0x000000000x6031d0 <n45>: 0x00000028 0x00000000 0x00000000 0x000000000x6031e0 <n45+16>: 0x00000000 0x00000000 0x00000000 0x000000000x6031f0 <n41>: 0x00000001 0x00000000 0x00000000 0x000000000x603200 <n41+16>: 0x00000000 0x00000000 0x00000000 0x000000000x603210 <n47>: 0x00000063 0x00000000 0x00000000 0x000000000x603220 <n47+16>: 0x00000000 0x00000000 0x00000000 0x000000000x603230 <n44>: 0x00000023 0x00000000 0x00000000 0x000000000x603240 <n44+16>: 0x00000000 0x00000000 0x00000000 0x000000000x603250 <n42>: 0x00000007 0x00000000 0x00000000 0x000000000x603260 <n42+16>: 0x00000000 0x00000000 0x00000000 0x000000000x603270 <n43>: 0x00000014 0x00000000 0x00000000 0x000000000x603280 <n43+16>: 0x00000000 0x00000000 0x00000000 0x000000000x603290 <n46>: 0x0000002f 0x00000000 0x00000000 0x000000000x6032a0 <n46+16>: 0x00000000 0x00000000 0x00000000 0x000000000x6032b0 <n48>: 0x000003e9 0x00000000 0x00000000 0x000000000x6032c0 <n48+16>: 0x00000000 0x00000000 0x00000000 0x00000000
排版好丑 🤣
这是一个二叉树!大概的结构长这样:
注:观察节点对应的值,可知是一棵二叉查找树
再看看 fun7
函数长啥样:
0000000000401204 <fun7>: 401204: 48 83 ec 08 sub $0x8,%rsp 401208: 48 85 ff test %rdi,%rdi 40120b: 74 2b je 401238 <fun7+0x34> 40120d: 8b 17 mov (%rdi),%edx 40120f: 39 f2 cmp %esi,%edx 401211: 7e 0d jle 401220 <fun7+0x1c> 401213: 48 8b 7f 08 mov 0x8(%rdi),%rdi 401217: e8 e8 ff ff ff call 401204 <fun7> 40121c: 01 c0 add %eax,%eax 40121e: eb 1d jmp 40123d <fun7+0x39> 401220: b8 00 00 00 00 mov $0x0,%eax 401225: 39 f2 cmp %esi,%edx 401227: 74 14 je 40123d <fun7+0x39> 401229: 48 8b 7f 10 mov 0x10(%rdi),%rdi 40122d: e8 d2 ff ff ff call 401204 <fun7> 401232: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax 401236: eb 05 jmp 40123d <fun7+0x39> 401238: b8 ff ff ff ff mov $0xffffffff,%eax 40123d: 48 83 c4 08 add $0x8,%rsp 401241: c3 ret
fun7: 0x401208
测试当前节点是否为空。若为空,置寄存器 eax
为 -1
,那必然没救了 🤣,因为修改寄存器 eax
的两条语句:
fun7: 0x40121c
fun7: 0x401232
都没有办法让 -1
在几步之内变为 2
,所以我们不能走到空节点。
观察 fun7: 0x401211
可知,若当前节点存储的值 ≤
我们之前输入的值,则走到右子树(fun7: 0x401229
),反之走到左子树(fun7: 0x401213
)。另外,递归调用右子树前,还会置寄存器 eax
为 0
,并比较当前节点存储的值是否等于我们之前输入的值,若相等,则结束递归调用。
综合分析可知,我们输入的值应该等于二叉树中某一个节点的对应的值。并且,在递归返回的过程中,应该在第一次从右子树返回到父节点(1 → eax
)后,有且仅有一次从左子树返回到父节点 (eax+eax → eax
)。
于是我们输入的值可以是节点 n43
或者节点 n32
对应的值,即 20
或 22
(将十六进制转换为十进制)。
为此我们修改 input
文件,使其变为:
$ cat inputBorder relations with Canada have never been better.1 2 4 8 16 321 3111 0 DrEvilionefg4 3 2 1 6 520
测试一下,注意这里隐藏阶段的成功提示在阶段六之前:
vgalaxy@vgalaxy-VirtualBox:~/Lab/bomb$ ./bomb inputWelcome to my fiendish little bomb. You have 6 phases withwhich to blow yourself up. Have a nice day!Phase 1 defused. How about the next one?That's number 2. Keep going!Halfway there!So you got that one. Try this one.Good work! On to the next...Curses, you've found the secret phase!But finding it and solving it are quite different...Wow! You've defused the secret stage!Congratulations! You've defused the bomb!vgalaxy@vgalaxy-VirtualBox:~/Lab/bomb$ date2021 年 09 月 06 日 星期一 22:14:16 CST
完结撒花 🤗