记录做 CSAPP 的 bomblab 解题思路,遇到好多坑,通宵了好几晚。我太菜了!🤣
用 gdb 查看炸弹:
linux> gdb -tui ./bomb
打开汇编视图:
(gdb) layout asm
如果你有足够大的显示区域,建议使用同时使用寄存器视图:
(gdb) layout regs
效果如下,上中下三层,上为寄存器,中为汇编,下为指令。
琐碎
通过把答案存储在一个文本文件里,能避免每次解开新的 bomb ,都要输入一遍旧的,万一输错了就凉了。在拆开 phase_1
后,创建一个 txt,然后在 gdb 里:
(gdb) set args ./psol.txt
(gdb) show args
Argument list to give program being debugged when it is started is "./psol.txt ".
每次运行时都会把文本内容作为参数输入。
通过把断点放在 explode_bomb
前,能在爆炸前及时停止,这是我炸了七次之后才发现的。
(gdb) b explode_bomb
Phase 1
在汇编文件里 main
里,通过寻找 phase_1
能看到它的第一次出现在:
000000000000123a <main>:
133b: e8 31 09 00 00 callq 1c71 <read_line>
1340: 48 89 c7 mov %rax,%rdi
1343: e8 a5 00 00 00 callq 13ed <phase_1>
1348: e8 6b 0a 00 00 callq 1db8 <phase_defused>
callq
前,刚好有个 mov %rax,%rdi
的指令,合理怀疑是把输入的值或者地址放到 %rdi 中。
通过设置 breakpoint 在 phase_1
并运行,能看到寄存器中,%rdi
的值为:
(gdb) b phase_1
(gdb) run
test
这里我输入 test 作为测试 string。这时我们能看到 %rdi
的高八位存着一个十六进制地址。
通过 x/s 0x5619f2ae9540
,返回 0x5619f2ae9540 <input_strings>: "test"
。现在观察phase_1
的反汇编代码,能看到,
00000000000013ed <phase_1>:
13ed: 48 83 ec 08 sub $0x8,%rsp
13f1: 48 8d 35 f8 19 00 00 lea 0x19f8(%rip),%rsi # 加载 %rip+0x19f8 内存地址到 %rsi
13f8: e8 96 04 00 00 callq 1893 <strings_not_equal> # 校验输入的 string 和密码 string 是否一致
13fd: 85 c0 test %eax,%eax # %eax 存储函数的返回值
13ff: 75 05 jne 1406 <phase_1+0x19> # 返回不等于0 条转到爆炸函数
1401: 48 83 c4 08 add $0x8,%rsp
1405: c3 retq
1406: e8 a5 07 00 00 callq 1bb0 <explode_bomb>
140b: eb f4 jmp 1401 <phase_1+0x14>
第二行中,出现 lea 0x19f8(%rip),%rsi
,接着就是 <strings_not_equal>
,可能对比的就是 %rdi
(输入) 和 %rsi
是否一致,因此我们再放一个 breakpoint 在第三行,填写左侧指令地址:
(gdb) b *0x5619f28e53f8
Breakpoint 2 at 0x5619f28e53f8
这里参考了 GDB 指令的用法文档。
break *0x80483c3 - Set breakpoint at address 0x80483c3
输入 c
(代表 continue 继续)继续运行,看看 %rsi
的值是什么?
通过 x/s 0x55f28cdd9df0
,返回 0x5619f2ae9540 <input_strings>: "The moon unit will be divided into two divisions."
,获得密码,重启 bomb,输入字符串,拆单成功。
Phase 2
一开场我把 breakpoint 放在 <read_six_numbers>
后一行,因为函数名,猜测是六个数字。输入 「123456」 立刻就炸了。
000000000000140d <phase_2>:
140d: 53 push %rbx # 压入栈
140e: 48 83 ec 20 sub $0x20,%rsp
1412: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
1419: 00 00
141b: 48 89 44 24 18 mov %rax,0x18(%rsp)
1420: 31 c0 xor %eax,%eax
1422: 48 89 e6 mov %rsp,%rsi
1425: e8 06 08 00 00 callq 1c30 <read_six_numbers> # 读取六个数字
142a: 83 3c 24 01 cmpl $0x1,(%rsp)
142e: 75 07 jne 1437 <phase_2+0x2a>
1430: bb 01 00 00 00 mov $0x1,%ebx
1435: eb 0a jmp 1441 <phase_2+0x34>
1437: e8 74 07 00 00 callq 1bb0 <explode_bomb>
143c: eb f2 jmp 1430 <phase_2+0x23>
143e: 83 c3 01 add $0x1,%ebx
1441: 83 fb 05 cmp $0x5,%ebx
1444: 7f 19 jg 145f <phase_2+0x52>
1446: 48 63 d3 movslq %ebx,%rdx
1449: 8d 43 ff lea -0x1(%rbx),%eax
144c: 48 98 cltq
144e: 8b 04 84 mov (%rsp,%rax,4),%eax
1451: 01 c0 add %eax,%eax
1453: 39 04 94 cmp %eax,(%rsp,%rdx,4)
1456: 74 e6 je 143e <phase_2+0x31>
1458: e8 53 07 00 00 callq 1bb0 <explode_bomb>
145d: eb df jmp 143e <phase_2+0x31>
145f: 48 8b 44 24 18 mov 0x18(%rsp),%rax
1464: 64 48 33 04 25 28 00 xor %fs:0x28,%rax
146b: 00 00
146d: 75 06 jne 1475 <phase_2+0x68>
146f: 48 83 c4 20 add $0x20,%rsp
1473: 5b pop %rbx
1474: c3 retq
1475: e8 46 fb ff ff callq fc0 <__stack_chk_fail@plt>
研究下 <read_six_numbers>
的代码:
0000000000001c30 <read_six_numbers>:
1c30: 48 83 ec 08 sub $0x8,%rsp
1c34: 48 89 f2 mov %rsi,%rdx
1c37: 48 8d 76 14 lea 0x14(%rsi),%rsi
1c3b: 48 8d 42 10 lea 0x10(%rdx),%rax
1c3f: 48 8d 4a 04 lea 0x4(%rdx),%rcx
1c43: 56 push %rsi
1c44: 50 push %rax
1c45: 4c 8d 4a 0c lea 0xc(%rdx),%r9
1c49: 4c 8d 42 08 lea 0x8(%rdx),%r8
1c4d: 48 8d 35 08 15 00 00 lea 0x1508(%rip),%rsi # 315c <array.3419+0x2dc>
1c54: b8 00 00 00 00 mov $0x0,%eax
1c59: e8 12 f4 ff ff callq 1070 <__isoc99_sscanf@plt>
1c5e: 48 83 c4 10 add $0x10,%rsp
在 1c4d
行,能看到有东西填入 %rsi
里了。然后就是 __isoc99_sscanf
了,估计就是 scanf。弄了个 breakpoint 在 scanf 前,看看 %rsi
里是什么:
(gdb) x/s $rsi
0x55f5ecb0015c: "%d %d %d %d %d %d"
怪不得那么快就炸了,原来 <read_six_numbers>
读取格式为 「1 2 3 4 5 6」 ,而不是 「123456」。这次重新再跑一次,填入 「1 2 3 4 5 6」,没炸。
有理由相信,这个是用栈来实现存储,所以通过把 breakpoint 设置在 <read_six_numbers>
后,看看 %rsp
指向的内容:0x7ffe53e8d710
,是我们填入的 「1 2 3 4 5 6」:
(gdb) x 0x7ffe53e8d710
0x7ffe53e8d710: "\001"
(gdb) x 0x7ffe53e8d714
0x7ffe53e8d714: "\002"
(gdb) x 0x7ffe53e8d718
0x7ffe53e8d718: "\003"
(gdb) x 0x7ffe53e8d71C
0x7ffe53e8d71c: "\004"
(gdb) x 0x7ffe53e8d720
0x7ffe53e8d720: "\005"
(gdb) x 0x7ffe53e8d724
0x7ffe53e8d724: "\006"
Phase 3
0x55bd596fa4d2 <phase_3+88> mov $0x327,%eax
0x55bd596fa4d7 <phase_3+93> sub $0x36,%eax
0x55bd596fa4da <phase_3+96> add $0x225,%eax
0x55bd596fa4df <phase_3+101> sub $0x30c,%eax
0x55bd596fa4e4 <phase_3+106> add $0x30c,%eax
0x55bd596fa4e9 <phase_3+111> sub $0x30c,%eax
0x55bd596fa4ee <phase_3+116> add $0x30c,%eax
0x55bd596fa4f3 <phase_3+121> sub $0x30c,%eax
会看到当输入第一个数字是0后,会进行以上运算:$0x327-0x36+0x225-0x30c=0x20a$,也就是十进制的 522。然后有对比第二个输入的数字是不是 522,如果不是就爆炸。所以答案是:
That's number 2. Keep going!
0 522
Phase 4
(gdb) x $rsi
0x55c52bc6c168: "%d %d"
输入是两个 int
,2 6
试试。然后发现 cmpl $0x4,(%rsp)
,%rsp
指向第一个输入,如果不是 4,就爆炸。所以,第一个数字是 4 。再次输入:4 9
把 phase_4
逆向工程后,应该类似这样:
void phase_4(char * input){
int a,b;
int val = sscanf(input, "%d %d", &a, &b);
if (val!=2 || a!=4)
explode_bomb();
int x=0, y=0;
while(rsp>x){
y+=func4(x);
x++;
}
return;
}
在 while
循环里会发现调用了 func4
这个函数,传入参数 x
,因为太菜,逆向工程到一半放弃了。
不过通过追踪 phase_4
最后一个比较 cmp %ebp,0x4(%rsp)
,ebp
存着 0x8
,如果第二个输入不等于它,就爆炸。
试试用它作为密码的第二个输入。成功!
4 8
So you got that one. Try this one.
这题倒是取巧了,正确的做法应该是把 phase_4
和 func4
各自翻译成 C。
Phase 5
你将需要使用 ASCII 表。
在前几行出现了调用 string_length
函数,紧接着就是 cmp $0x4,%eax
,如果不等于 4 就跳转到爆炸函数,所以密码是长度为 4 的字符串。接着,看反汇编代码:
162e: 48 63 d0 movslq %eax,%rdx # 循环的开始, dx=val
1631: 0f b6 14 13 movzbl (%rbx,%rdx,1),%edx # dx=input[dx]
1635: 83 e2 07 and $0x7,%edx # edx = edx & 0x7,0~7 之间
1638: 48 8d 35 41 18 00 00 lea 0x1841(%rip),%rsi # 0x55fd8e3cbe80 <array.3419>
163f: 0f af 0c 96 imul (%rsi,%rdx,4),%ecx
1643: 83 c0 01 add $0x1,%eax
1646: 83 f8 03 cmp $0x3,%eax # Compute val:3,和3比,一共循环四次
1649: 7e e3 jle 162e <phase_5+0x21> # if <=, goto循环
164b: 81 f9 a0 02 00 00 cmp $0x2a0,%ecx
1651: 74 05 je 1658 <phase_5+0x4b>
观察
- 能看出这是一个循环,一共四次,把输入的每一个字符都遍历一次。
- 第三行,有一个
&
,一开始没搞懂是用来干什么,后来想到之前 datalab 有个类似的,通过&
操作符实现模运算%
,a % b
功能上与a & (b-1)
一样。因此and $0x7,%edx
实际上是对%edx
寄存器的内容进行模 8 操作,里面的内容是我们输入的四个字符。 - 第四行,加载地址到
%rsi
,里面内容很重要。 - 第五行,是一个
imul
,猜测是自乘相关的操作。 - 倒数第二行,
cmp $0x2a0,%ecx
,结果要等于 $0x2a0$
查看 %rsi
的内容,使用 print *address@20
,把数组的后二十个项输出。
这里参考另一份 GDB 指令的用法文档。
(gdb) print array[i]@count artificial array - print array range
(gdb) print *0x555b6f915e80@20
$19 = {2, 6, 1, 3, 4, 7, 5, 8, 2032168787, 1948284271, 1802398056, 1970239776, 1851876128, 1869902624, 1752440944,
1868701797, 1998611053, 543716457, 1819440227, 539779885}
前八个数字分别是 2、6、1、3、4、7、5、8 。
经过一连串的蛛丝马迹,稍加分析得知,输入 4 个字符,每个字符模 8 后得到 0 到 7 之间的数。代码中的数组地址,加上取模后得出来的数作为偏移,就能在内存中找到相应的值,应该一共有可选八个数,乘到一起,积应该为 $0x2a0$ 。
转换为十进制:$0x2a0 = 672_{10}$
换言之,我们要找四个字符,使得 $a \times b \times c \times d = 672$
a = array[ASCII 码模 8]
b = array[ASCII 码模 8]
c = array[ASCII 码模 8]
d = array[ASCII 码模 8]
而 array 内容正如上文里,为 [2, 6, 1, 3, 4, 7, 5, 8]
。
例子
例子一、Q 在 ASCII 表里为 17:
- ASCII 码模 8: $17\bmod8=1$ 。
- 代入
array[ASCII 码模 8]
有array[1]
- 结果为
array[1]
元素,即 6。
例子二、T 在 ASCII 表里为 20,$20\bmod8=4$
- ASCII 码模 8: $20\bmod8=4$ 。
- 代入
array[ASCII 码模 8]
有array[4]
- 结果为
array[4]
元素,即 4。
把以上两个例子次序相反,能从数字变为 ASCII 字符。
例子三、数字 6 变 ASCII 字符:
- 数字 6,下标为 1,在
array[1]
- $1+8t$ ,则为 ASCII 码模 8 的逆运算,t 为自然数
- t = 1 为 9,t = 2 为 17……
- 对照 ASCII 表找出 I、Q、Y,$(t=1,2,3,\dots)$,所以对应关系不唯一
解题
我们要找四个字符,使得 $a \times b \times c \times d = 672$
因为 $672=4\times4\times6\times7$ ,利用上面例子方式,分别是下标 4、4、1、5,同时 +16(8 的倍数,例子三第二步),得出 20、20、17、21,对照 ASCII 表,得到答案 TTQU
。
So you got that one. Try this one.
TTQU
Good work! On to the next...
显然,逆模运算解不唯一(可以+16、+32、+64…),因数组合也不唯一,所以这题有很多个答案。
这里写出另一个可行的方法,同时展示不同的逆模运算以及因数组合。
因为 $672=2\times6\times7\times8$ ,分别是下标 0、1、5、7,同时 +64(8 的倍数,例子三),得出 64、65、69、71,对照 ASCII 表,得到答案 @AEG
。经测试,一样通过。
参考链接: