bomblab 实验笔记 - Binary Bomb

技术

记录做 CSAPP 的 bomblab 解题思路,遇到好多坑,通宵了好几晚。我太菜了!🤣

用 gdb 查看炸弹:

linux> gdb -tui ./bomb

打开汇编视图:

(gdb) layout asm

如果你有足够大的显示区域,建议使用同时使用寄存器视图:

(gdb) layout regs

效果如下,上中下三层,上为寄存器,中为汇编,下为指令。

image-20201108112241249

琐碎

通过把答案存储在一个文本文件里,能避免每次解开新的 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"

输入是两个 int2 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_4func4 各自翻译成 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>

观察

  1. 能看出这是一个循环,一共四次,把输入的每一个字符都遍历一次。
  2. 第三行,有一个 & ,一开始没搞懂是用来干什么,后来想到之前 datalab 有个类似的,通过 & 操作符实现模运算 %a % b 功能上与 a & (b-1) 一样。因此 and $0x7,%edx 实际上是对 %edx 寄存器的内容进行模 8 操作,里面的内容是我们输入的四个字符。
  3. 第四行,加载地址到 %rsi ,里面内容很重要
  4. 第五行,是一个 imul ,猜测是自乘相关的操作。
  5. 倒数第二行,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:

  1. ASCII 码模 8: $17\bmod8=1$ 。
  2. 代入 array[ASCII 码模 8]array[1]
  3. 结果为 array[1] 元素,即 6。

例子二、T 在 ASCII 表里为 20,$20\bmod8=4$

  1. ASCII 码模 8: $20\bmod8=4$ 。
  2. 代入 array[ASCII 码模 8]array[4]
  3. 结果为 array[4] 元素,即 4。

把以上两个例子次序相反,能从数字变为 ASCII 字符。

例子三、数字 6 变 ASCII 字符:

  1. 数字 6,下标为 1,在 array[1]
  2. $1+8t$ ,则为 ASCII 码模 8 的逆运算,t 为自然数
  3. t = 1 为 9,t = 2 为 17……
  4. 对照 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。经测试,一样通过。

参考链接: