← 返回首页

CMU-15213-BombLab

CMU-15213-BombLab

BombLab

要求通过反汇编与debuger,借助主函数源文件实现对6个密码的findout。

记录

环境与工具

objdump: 反汇编工具 cgdb: debuger工具 gdb+vim ubuntu 22.04

bomb 解密

prepare stage

生成 bomb .t .s 符号表与反汇编

objdump -s bomb > bomb.s
objdump -t bomb > bomb.t

.t 搜符号表 bomb可以发现 explode_bomb,cgdb调试,并b explode_bomb设置断点,避免误bomb。。。随后打断点到input=read_line()中。

详细注释在.s 反汇编中。

phase_1 stage

首先给phase_1打断点,然后单步调试,通过dump of assmebler code的内容直接可以看到一个地址0x402400,根据底下的strings_not_equal猜测与字符串有关,通过x/s 地址查看字符串格式的内容,可见

 1│ Dump of assembler code for function phase_1:
 2│    0x0000000000400ee0 <+0>:     sub    $0x8,%rsp
 3├──> 0x0000000000400ee4 <+4>:     mov    $0x402400,%esi
 4│    0x0000000000400ee9 <+9>:     call   0x401338 <strings_not_equal>
 5│    0x0000000000400eee <+14>:    test   %eax,%eax
 6│    0x0000000000400ef0 <+16>:    je     0x400ef7 <phase_1+23>
 7│    0x0000000000400ef2 <+18>:    call   0x40143a <explode_bomb>
 8│    0x0000000000400ef7 <+23>:    add    $0x8,%rsp
 9│    0x0000000000400efb <+27>:    ret
10│ End of assembler dump.
(gdb) x/s 0x402400 
0x402400:       "Border relations with Canada have never been better."

初步可猜测其是第一步的答案

sub $0x8 %rsp 这样对栈顶减去一个直接值得情况,通常可认为这是开辟了一个局部变量,在传参过程中使用第一个寄存器大概率表示第一个参数已经被使用了,根据源代码可以猜测,第一个参数大概是phase_1的传参

Dump of assembler code for function strings_not_equal:
 2│    0x0000000000401338 <+0>:     push   %r12
 3│    0x000000000040133a <+2>:     push   %rbp
 4│    0x000000000040133b <+3>:     push   %rbx
 5│    0x000000000040133c <+4>:     mov    %rdi,%rbx
 6│    0x000000000040133f <+7>:     mov    %rsi,%rbp
 7├──> 0x0000000000401342 <+10>:    call   0x40131b <string_length>
 8│    0x0000000000401347 <+15>:    mov    %eax,%r12d

这里使用到了rbx基址寄存器,虽然该寄存器有这个名字,但是其调用者使用原则使其可以稳定的长期记录数据(不会被随意更改)

很显然,这里是在获取我们输入的参数的长度,并将数据保存到r12d中。

随后的一个结构中

14│ 	 0x000000000040135c <+36>:    movzbl (%rbx),%eax
15│    0x000000000040135f <+39>:    test   %al,%al
16│    0x0000000000401361 <+41>:    je     0x401388 <strings_not_equal+80>
17│    0x0000000000401363 <+43>:    cmp    0x0(%rbp),%al
18│    0x0000000000401366 <+46>:    je     0x401372 <strings_not_equal+58>
19│    0x0000000000401368 <+48>:    jmp    0x40138f <strings_not_equal+87>

这里将rbx指向的字符(8位,一个字节)传入到ax中扩展为32位,并判断是否为空,随后对比一个字节大小的bp与al,即写死的字符与最初传参的第一个字符

20├──> 0x000000000040136a <+50>:    cmp    0x0(%rbp),%al
21│    0x000000000040136d <+53>:    nopl   (%rax)
22│    0x0000000000401370 <+56>:    jne    0x401396 <strings_not_equal+94>
23│    0x0000000000401372 <+58>:    add    $0x1,%rbx
24│    0x0000000000401376 <+62>:    add    $0x1,%rbp
25│    0x000000000040137a <+66>:    movzbl (%rbx),%eax
26│    0x000000000040137d <+69>:    test   %al,%al
27│    0x000000000040137f <+71>:    jne    0x40136a <strings_not_equal+50>

这是一个循环体,其中26行为判断退出的条件,即传入的rbx字符串在循环迭代中非空。

综上两个接口,可以看出这是一个while...do,或者说for循环的结构体,用于判断两个字符串是否相同,至此可以判断phase_1的答案是

Border relations with Canada have never been better. 

完成对剩余代码的分析

b 0x401381
c

跳过循环

可以看到一堆jmp以及mov .. to %edx

这里直接给出定义,这里是不同分支的归一并给标志参数赋值!

28│    0x0000000000401381 <+73>:    mov    $0x0,%edx
29│    0x0000000000401386 <+78>:    jmp    0x40139b <strings_not_equal+99>
30│    0x0000000000401388 <+80>:    mov    $0x0,%edx
31│    0x000000000040138d <+85>:    jmp    0x40139b <strings_not_equal+99>
32│    0x000000000040138f <+87>:    mov    $0x1,%edx
33│    0x0000000000401394 <+92>:    jmp    0x40139b <strings_not_equal+99>
34│    0x0000000000401396 <+94>:    mov    $0x1,%edx
35├──> 0x000000000040139b <+99>:    mov    %edx,%eax
36│    0x000000000040139d <+101>:   pop    %rbx
37│    0x000000000040139e <+102>:   pop    %rbp
38│    0x000000000040139f <+103>:   pop    %r12
39│    0x00000000004013a1 <+105>:   ret    

最后复原寄存器的值,并将标志参数rdx返回到ax中(标准返回寄存器)

phase_2 stage

phase2中存在一个复杂的开栈记录数据的过程

img

phase2中由于超出了6个参数(包括格式),因此出现了栈上传参的方法,常见传参六个寄存器为

参数号对应寄存器
第 1 个RDI
第 2 个RSI
第 3 个RDX
第 4 个RCX
第 5 个R8
第 6 个R9

注意上图,这里传入的参数全部保存在phase_2开辟的空间当中,而read_six_number开辟的空间,完全是为了保存局部变量,也就是读入的参数。

在调用read_six_number函数前,程序保存了rsi参数的值,这是第二个参数,为什么不是第一个参数rdi呢?这是因为rdi的设置发生在更早的指令中。例如传入phase_2的参数

这里就不细说read_six_numers这个函数了,其中涉及的lea命令,jg命令可以在command ref位置查询。

随后可见又出现了一个循环体

11│    0x0000000000400f17 <+27>:    mov    -0x4(%rbx),%eax
12│    0x0000000000400f1a <+30>:    add    %eax,%eax
13│    0x0000000000400f1c <+32>:    cmp    %eax,(%rbx)
14│    0x0000000000400f1e <+34>:    je     0x400f25 <phase_2+41>
15│    0x0000000000400f20 <+36>:    call   0x40143a <explode_bomb>
16│    0x0000000000400f25 <+41>:    add    $0x4,%rbx
17│    0x0000000000400f29 <+45>:    cmp    %rbp,%rbx
18│    0x0000000000400f2c <+48>:    jne    0x400f17 <phase_2+27>
19│    0x0000000000400f2e <+50>:    jmp    0x400f3c <phase_2+64>
20│    0x0000000000400f30 <+52>:    lea    0x4(%rsp),%rbx
21│    0x0000000000400f35 <+57>:    lea    0x18(%rsp),%rbp
22│    0x0000000000400f3a <+62>:    jmp    0x400f17 <phase_2+27>

前面几个与循环体无关的我省略了,基本就是比较栈顶与1,然后进入+52的循环段。这里初始化了两个指针bx,bp,分别是栈顶的前一位和最后一位。

然后然后可以看看哪里是判断结束的,可见+50处有一个jump到+60的,认为这里就是跳出循环的,可见,需要bp,bx相等。

这也就是双指针的逻辑了,然后+30处很容易想到这是一个倍增的数组。于是答案就是

1 2 4 8 16 32

因为不是第一个phase了,对汇编也熟悉一些了,这里略过剩下的汇编代码

phase_3 stage

这里直接字符串查看看见的两个地址0x4025cf,0x400bf0。可见前一个是输入两个整数的规范字符串,后者是-1。随后读入了两个字符

__isoc99_sscanf@plt 是 反汇编工具(如 objdump 或 GDB)添加的**“符号注释” **。

__isoc99_sscanf 这是 sscanf 函数的实际符号名(symbol name)

为什么不是 sscanf?因为: 编译器为了兼容 C99 标准,使用 __isoc99_sscanf作为内部符号它是标准库函数 int sscanf(const char *str, const char *format, …); 的实现

随后+32,jg该命令告诉我们,此时输入的字符数量必须大于1

这里我们先填入

1 2

顺利逃过了第一个explode,继续运行,又遇到了一个分支,如果我们填入的第一个参数比7大,则会跳转到+106,是一个explode,因此第一个参数必须比7小,这里也顺利逃过。

随后看到了一个奇怪的jump

14├──> 0x0000000000400f75 <+50>:    jmp    *0x402470(,%rax,8)

这是一个switch-case 跳转表(jump table)。

语法结构(AT&T 格式):

基地址 + 索引 * 尺寸 + 偏移
 disp(base, index, scale)
# 0x402470(,%rax,8) = 0x402470 + %rax * 8

查询这个跳转表

(gdb)  x/8gx 0x402470
0x402470:       0x0000000000400f7c      0x0000000000400fb9
0x402480:       0x0000000000400f83      0x0000000000400f8a
0x402490:       0x0000000000400f91      0x0000000000400f98
0x4024a0:       0x0000000000400f9f      0x0000000000400fa6

可见我们会跳转到0x0000000000400fb9

随后直接跳转到

25│    0x0000000000400f9f <+92>:    mov    $0x2aa,%eax
26│    0x0000000000400fa4 <+97>:    jmp    0x400fbe <phase_3+123>
27│    0x0000000000400fa6 <+99>:    mov    $0x147,%eax
28│    0x0000000000400fab <+104>:   jmp    0x400fbe <phase_3+123>
29│    0x0000000000400fad <+106>:   call   0x40143a <explode_bomb>
30│    0x0000000000400fb2 <+111>:   mov    $0x0,%eax
31│    0x0000000000400fb7 <+116>:   jmp    0x400fbe <phase_3+123>
32├──> 0x0000000000400fb9 <+118>:   mov    $0x137,%eax
33│    0x0000000000400fbe <+123>:   cmp    0xc(%rsp),%eax
34│    0x0000000000400fc2 <+127>:   je     0x400fc9 <phase_3+134>
35│    0x0000000000400fc4 <+129>:   call   0x40143a <explode_bomb>
36│    0x0000000000400fc9 <+134>:   add    $0x18,%rsp
37│    0x0000000000400fcd <+138>:   ret

然后比较0x137(d311)与第二个值得大小,相等则跳到 + 134,似乎直接返回了

于是尝试

1 311

phase_4 stage

照例查看一下.s文件,先尝试输入两个数字

1 2 

查看0x4025cf确认是输入两个数字。确认是的。随后jbe判断第一个数小于等于14

jbe:Jump if Below or Equal <=

随后调用了func4,调用参数如下

esi = 0
edx = 14
edi = 第一个参数(1)

首先用edx得值减了一下esi,保存值在eax、ecx。然后ecx逻辑右移了16+15=31位,保留了最高位的值为1或0,随后又加上eax,随后对eax进行算数右移/2。

ecx = rax + rsi * 1 = 7 然后比较edi与ecx即edi是否小于等于7

如果小于则rcx减一赋值给rdx,递归调用func4

esi = 0
edx = 6
edi = 第一个参数

最后一次递归的处理返回值是

16│    0x0000000000400ff2 <+36>:    mov    $0x0,%eax
17│    0x0000000000400ff7 <+41>:    cmp    %edi,%ecx
18│    0x0000000000400ff9 <+43>:    jge    0x401007 <func4+57>
19│    0x0000000000400ffb <+45>:    lea    0x1(%rcx),%esi
20│    0x0000000000400ffe <+48>:    call   0x400fce <func4>
21│    0x0000000000401003 <+53>:    lea    0x1(%rax,%rax,1),%eax
22│    0x0000000000401007 <+57>:    add    $0x8,%rsp
23│    0x000000000040100b <+61>:    ret

比较di与cx的值(前面保证cx小于等于di),即,这里如果等于则返回0,如果不等于则赋值si,递归,返回一个其他值。

按照目前输入,最终递归结果为

esi = 0
edx = 2
edi = 1
eax = 0

函数停留在+32,继续执行返回0。

退回phase_4

17│--> 0x000000000040104d <+65>:    test   %eax,%eax
18│    0x000000000040104f <+67>:    jne    0x401058 <phase_4+76>
19│    0x0000000000401051 <+69>:    cmpl   $0x0,0xc(%rsp)
20│    0x0000000000401056 <+74>:    je     0x40105d <phase_4+81>
21│    0x0000000000401058 <+76>:    call   0x40143a <explode_bomb>
22│    0x000000000040105d <+81>:    add    $0x18,%rsp
23│    0x0000000000401061 <+85>:    ret    

可见返回值不能是非0,然后比较第二个参数是否为0。

那么改为

1 0

即可通过phase_4

phase_5 stage

观察一下phase_5,输入一个长度等于6的字符串

abcdef

先解释一下这一个奇怪的地址

 5│    0x000000000040106a <+8>:     mov    %fs:0x28,%rax
  1. %fs是一个 段寄存器(segment register)在 Linux x86-64 中,%fs 通常指向当前线程的 Thread Local Storage(TLS) 区域它用于实现线程局部变量、栈保护等
  2. 0x28是一个偏移量(40 字节)%fs:0x28 表示:从 %fs 指向的基地址开始,偏移 0x28 字节处的内存
  3. mov …, %rax将该内存地址中的值读取到 %rax 寄存器中

简单查询该处的值,为0x28(d40),但执行到该处发现,寄存器的值与这样查询出的值完全不同, 查看fs的地址,为0,显然有错误在其中。

查询ai,发现该条mov是程序在访问 x86-64 架构中的线程局部存储(TLS),并且这行代码通常是 栈保护机制(Stack Canary) 的一部分。

无论如何,rax的值为

(gdb) p/d $eax
$37 = 1120943104
(gdb) x/d $eax
0x42d03c00:     Cannot access memory at address 0x42d03c00
(gdb) p/d $rax
$38 = -4452265605897110528
(gdb) x/d $rax
0xc236600542d03c00:     Cannot access memory at address 0xc236600542d03c00

运行 7│ 0x0000000000401078 <+22>: xor %eax,%eax后,发现,eax,rax均变为了0。

随后,发现跳转到一个循环内了

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>:    call   0x401338 <strings_not_equal>
26│    0x00000000004010c2 <+96>:    test   %eax,%eax
27│    0x00000000004010c4 <+98>:    je     0x4010d9 <phase_5+119>
28│    0x00000000004010c6 <+100>:   call   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>

简单执行发现:

  1. 0x4024b0处存储着字符串maduiersnfotvbylSo you think you can stop the bomb with ctrl-c , do you?
  2. 0x40245e处存储着字符串flyers
  3. 要求经过6次迭代处理的字符串(rsp+0x10)位置与flyers完全相同
  4. 现在的结果是aduier

尝试是否是简单顺序替换

ffcdef

发现似乎变成了

rruier

显然是不对的

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>

仔细分析这样的循环并倒推一下代码可见只有 18│ 0x00000000004010a0 <+62>: mov %dl,0x10(%rsp,%rax,1) 对我们输入的结果产生了影响

又发现

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

利用输入字符的ascii码赋值了edx,下面贴出ascii码表,发现0x4024b0处存储的字符完全满足flyers母集的特征,因此。需满足输入字符为低4位满足与9,15,14,5,6,7相等即可

什么是ASCII码?-CSDN博客

随意构造一个

)/.%&'

phase_6 LAST_STAGE

Finally!!!

简单查看.s确定初始为6个数字。

1 2 3 4 5 6

简单运行

  1. r13存储的是rsp的地址
  2. 第一个值-1 <= 5

修改为

6 2 3 4 5 6

r12d会自动将r12的高32位清零

随后遇到一个分支比较r12d的数值,为6。估计是一个循环,找一下

13│    0x0000000000401114 <+32>:    mov    %r13,%rbp
14│    0x0000000000401117 <+35>:    mov    0x0(%r13),%eax
15│    0x000000000040111b <+39>:    sub    $0x1,%eax
16│    0x000000000040111e <+42>:    cmp    $0x5,%eax
17│    0x0000000000401121 <+45>:    jbe    0x401128 <phase_6+52>
18│    0x0000000000401123 <+47>:    call   0x40143a <explode_bomb>
19│    0x0000000000401128 <+52>:    add    $0x1,%r12d
20├──> 0x000000000040112c <+56>:    cmp    $0x6,%r12d
21│    0x0000000000401130 <+60>:    je     0x401153 <phase_6+95>
22│    0x0000000000401132 <+62>:    mov    %r12d,%ebx
23│    0x0000000000401135 <+65>:    movslq %ebx,%rax
24│    0x0000000000401138 <+68>:    mov    (%rsp,%rax,4),%eax
25│    0x000000000040113b <+71>:    cmp    %eax,0x0(%rbp)
26│    0x000000000040113e <+74>:    jne    0x401145 <phase_6+81>
27│    0x0000000000401140 <+76>:    call   0x40143a <explode_bomb>
28│    0x0000000000401145 <+81>:    add    $0x1,%ebx
29│    0x0000000000401148 <+84>:    cmp    $0x5,%ebx
30│    0x000000000040114b <+87>:    jle    0x401135 <phase_6+65>
31│    0x000000000040114d <+89>:    add    $0x4,%r13
32│    0x0000000000401151 <+93>:    jmp    0x401114 <phase_6+32>

可见这是个嵌套循环,二级循环如下

23│    0x0000000000401135 <+65>:    movslq %ebx,%rax
24│    0x0000000000401138 <+68>:    mov    (%rsp,%rax,4),%eax
25│    0x000000000040113b <+71>:    cmp    %eax,0x0(%rbp)
26│    0x000000000040113e <+74>:    jne    0x401145 <phase_6+81>
27│    0x0000000000401140 <+76>:    call   0x40143a <explode_bomb>
28│    0x0000000000401145 <+81>:    add    $0x1,%ebx
29│    0x0000000000401148 <+84>:    cmp    $0x5,%ebx
30│    0x000000000040114b <+87>:    jle    0x401135 <phase_6+65>

第一个循环的退出为%r12d 1-6

20│    0x000000000040112c <+56>:    cmp    $0x6,%r12d
21│    0x0000000000401130 <+60>:    je     0x401153 <phase_6+95>

第二个循环的退出为%ebx %r12d-5

22│    0x0000000000401132 <+62>:    mov    %r12d,%ebx
...
29│    0x0000000000401148 <+84>:    cmp    $0x5,%ebx

首先注意到二级循环的eax不能等于(rbp)(由r13赋值,即第一个参数),查看ax的赋值过程,由栈赋值,循环1-5中的所有值。后面的每个数不能与bp指向的数出现重复。

再来看外层循环,这个循环很简单,就是不断迭代栈中的每个元素,并为二级循环做初始化

13│    0x0000000000401114 <+32>:    mov    %r13,%rbp
14│    0x0000000000401117 <+35>:    mov    0x0(%r13),%eax
15│    0x000000000040111b <+39>:    sub    $0x1,%eax
16│    0x000000000040111e <+42>:    cmp    $0x5,%eax
17│    0x0000000000401121 <+45>:    jbe    0x401128 <phase_6+52>
18│    0x0000000000401123 <+47>:    call   0x40143a <explode_bomb>
19│    0x0000000000401128 <+52>:    add    $0x1,%r12d
20├──> 0x000000000040112c <+56>:    cmp    $0x6,%r12d
21│    0x0000000000401130 <+60>:    je     0x401153 <phase_6+95>
22│    0x0000000000401132 <+62>:    mov    %r12d,%ebx
...
31│    0x000000000040114d <+89>:    add    $0x4,%r13
32│    0x0000000000401151 <+93>:    jmp    0x401114 <phase_6+32>

首先,他会用r13更新bp为目前迭代的元素,其次,初始化eax,这说明所有元素都必须小于等于6

也就是是一个1-6的排列,修改答案为

6 4 5 3 1 2

***好好好!!!***成功跑出了二层循环。

简单看看下面的

33├──> 0x0000000000401153 <+95>:    lea    0x18(%rsp),%rsi # rsi 0
34│    0x0000000000401158 <+100>:   mov    %r14,%rax # r14也指向栈顶
35│    0x000000000040115b <+103>:   mov    $0x7,%ecx
36│    0x0000000000401160 <+108>:   mov    %ecx,%edx
37│    0x0000000000401162 <+110>:   sub    (%rax),%edx
38│    0x0000000000401164 <+112>:   mov    %edx,(%rax)
39│    0x0000000000401166 <+114>:   add    $0x4,%rax
40│    0x000000000040116a <+118>:   cmp    %rsi,%rax
41│    0x000000000040116d <+121>:   jne    0x401160 <phase_6+108>
42│    0x000000000040116f <+123>:   mov    $0x0,%esi
43│    0x0000000000401174 <+128>:   jmp    0x401197 <phase_6+163>
44│    0x0000000000401176 <+130>:   mov    0x8(%rdx),%rdx
45│    0x000000000040117a <+134>:   add    $0x1,%eax
46│    0x000000000040117d <+137>:   cmp    %ecx,%eax
47│    0x000000000040117f <+139>:   jne    0x401176 <phase_6+130>
48│    0x0000000000401181 <+141>:   jmp    0x401188 <phase_6+148>
49│    0x0000000000401183 <+143>:   mov    $0x6032d0,%edx
50│    0x0000000000401188 <+148>:   mov    %rdx,0x20(%rsp,%rsi,2)
51│    0x000000000040118d <+153>:   add    $0x4,%rsi

顶真出里面有两个循环

36│    0x0000000000401160 <+108>:   mov    %ecx,%edx
37│    0x0000000000401162 <+110>:   sub    (%rax),%edx
38│    0x0000000000401164 <+112>:   mov    %edx,(%rax)
39│    0x0000000000401166 <+114>:   add    $0x4,%rax
40│    0x000000000040116a <+118>:   cmp    %rsi,%rax
41│    0x000000000040116d <+121>:   jne    0x401160 <phase_6+108>
44│    0x0000000000401176 <+130>:   mov    0x8(%rdx),%rdx
45│    0x000000000040117a <+134>:   add    $0x1,%eax
46│    0x000000000040117d <+137>:   cmp    %ecx,%eax
47│    0x000000000040117f <+139>:   jne    0x401176 <phase_6+130>
  1. cmp %rsi,(%rax) # (rax) = 7 - (rsp) rax = rsp

    这说明整个循环1就是为了将数组做一个互补映射 6 -> 1 5 -> 2 …

  2. 循环2是一个嵌套的二级循环

44│    0x0000000000401176 <+130>:   mov    0x8(%rdx),%rdx
45│    0x000000000040117a <+134>:   add    $0x1,%eax
46│    0x000000000040117d <+137>:   cmp    %ecx,%eax
47│    0x000000000040117f <+139>:   jne    0x401176 <phase_6+130>
48│    0x0000000000401181 <+141>:   jmp    0x401188 <phase_6+148>
49│    0x0000000000401183 <+143>:   mov    $0x6032d0,%edx
50│    0x0000000000401188 <+148>:   mov    %rdx,0x20(%rsp,%rsi,2)
51│    0x000000000040118d <+153>:   add    $0x4,%rsi
52│    0x0000000000401191 <+157>:   cmp    $0x18,%rsi
53│    0x0000000000401195 <+161>:   je     0x4011ab <phase_6+183>
54│    0x0000000000401197 <+163>:   mov    (%rsp,%rsi,1),%ecx
55│    0x000000000040119a <+166>:   cmp    $0x1,%ecx
56│    0x000000000040119d <+169>:   jle    0x401183 <phase_6+143>
57│    0x000000000040119f <+171>:   mov    $0x1,%eax
58│    0x00000000004011a4 <+176>:   mov    $0x6032d0,%edx
59│    0x00000000004011a9 <+181>:   jmp    0x401176 <phase_6+130>

查看一下循环推出的条件

51│    0x000000000040118d <+153>:   add    $0x4,%rsi
52│    0x0000000000401191 <+157>:   cmp    $0x18,%rsi
53│    0x0000000000401195 <+161>:   je     0x4011ab <phase_6+183>

要求rsi == 0x18,rsi被初始化为0,即 rsi 0-0x18 0x4,迭代6次,遍历数组

  1. 此时 0x6030d0的值为-122,加载进rdx时的值为76,rsp+20的值为-48

(gdb) x/20wc $rsp 0x7fffffffdb90: 1 ‘\001’ 3 ‘\003’ 2 ‘\002’ 4 ‘\004’ 0x7fffffffdba0: 6 ‘\006’ 5 ‘\005’ 0 ‘\000’ 0 ‘\000’ 0x7fffffffdbb0: -48 ‘\320’ 0 ‘\000’ -16 ‘\360’ 0 ‘\000’ 0x7fffffffdbc0: -32 ‘\340’ 0 ‘\000’ 0 ‘\000’ 0 ‘\000’ 0x7fffffffdbd0: 32 ’ ’ 0 ‘\000’ 16 ‘\020’ 0 ‘\000’ ```

  1. 其在原数组后新构建了一个数组(每个数字长一个字节),在本次测试中的结果如下

    (gdb) x/6gx $rsp + 0x20
    0x7fffffffdbb0: 0x00000000006032d0      0x00000000006032f0
    0x7fffffffdbc0: 0x00000000006032e0      0x0000000000603300
    0x7fffffffdbd0: 0x0000000000603320      0x0000000000603310

FINNALY!!!

60│    0x00000000004011ab <+183>:   mov    0x20(%rsp),%rbx
61│    0x00000000004011b0 <+188>:   lea    0x28(%rsp),%rax
62│    0x00000000004011b5 <+193>:   lea    0x50(%rsp),%rsi
63│    0x00000000004011ba <+198>:   mov    %rbx,%rcx
64│    0x00000000004011bd <+201>:   mov    (%rax),%rdx
65│    0x00000000004011c0 <+204>:   mov    %rdx,0x8(%rcx)
66│    0x00000000004011c4 <+208>:   add    $0x8,%rax
67│    0x00000000004011c8 <+212>:   cmp    %rsi,%rax
68│    0x00000000004011cb <+215>:   je     0x4011d2 <phase_6+222>
69│    0x00000000004011cd <+217>:   mov    %rdx,%rcx
70│    0x00000000004011d0 <+220>:   jmp    0x4011bd <phase_6+201>
71│    0x00000000004011d2 <+222>:   movq   $0x0,0x8(%rdx)
72│    0x00000000004011da <+230>:   mov    $0x5,%ebp
73│    0x00000000004011df <+235>:   mov    0x8(%rbx),%rax
74│    0x00000000004011e3 <+239>:   mov    (%rax),%eax
75│    0x00000000004011e5 <+241>:   cmp    %eax,(%rbx)
76│    0x00000000004011e7 <+243>:   jge    0x4011ee <phase_6+250>
77│    0x00000000004011e9 <+245>:   call   0x40143a <explode_bomb>
78│    0x00000000004011ee <+250>:   mov    0x8(%rbx),%rbx
79│    0x00000000004011f2 <+254>:   sub    $0x1,%ebp
80│    0x00000000004011f5 <+257>:   jne    0x4011df <phase_6+235>

注意到循环

64│    0x00000000004011bd <+201>:   mov    (%rax),%rdx
65│    0x00000000004011c0 <+204>:   mov    %rdx,0x8(%rcx)
66│    0x00000000004011c4 <+208>:   add    $0x8,%rax
67│    0x00000000004011c8 <+212>:   cmp    %rsi,%rax
68│    0x00000000004011cb <+215>:   je     0x4011d2 <phase_6+222>
69│    0x00000000004011cd <+217>:   mov    %rdx,%rcx
70│    0x00000000004011d0 <+220>:   jmp    0x4011bd <phase_6+201>

这是遍历后面数组的前五位,并处理0x6032e0 - 0x663320位置的值

这里实际上是在链接节点

且最后一个explode_bomb是

73│    0x00000000004011df <+235>:   mov    0x8(%rbx),%rax
74├──> 0x00000000004011e3 <+239>:   mov    (%rax),%eax
75│    0x00000000004011e5 <+241>:   cmp    %eax,(%rbx)
76│    0x00000000004011e7 <+243>:   jge    0x4011ee <phase_6+250>
77│    0x00000000004011e9 <+245>:   call   0x40143a <explode_bomb>
78│    0x00000000004011ee <+250>:   mov    0x8(%rbx),%rbx
79│    0x00000000004011f2 <+254>:   sub    $0x1,%ebp
80│    0x00000000004011f5 <+257>:   jne    0x4011df <phase_6+235>

执行下去,发现对于0x6032d0位置与0x6032f0位置的值的比较爆炸了,即cmp %eax,(%rbx)的比较重rbx的值大于rax,具体的是

(gdb) x/x $rbx
0x6032d0 <node1>:       0x000000010000014c
(gdb) p/x $rax
$106 = 0x39c
(gdb) x/x $rbx + 8
0x6032d8 <node1+8>:     0x00000000006032f0
(gdb) p/x $rax
$105 = 0x39c
(gdb) x/x 0x6032f0
0x6032f0 <node3>:       0x000000030000039c

这里从eax向rax转移时不仅出现了*解释的情况,而且出现了32->64的0填补扩充解释。

这时让我们回到上面的根据第二个数组简历0x60系列数据的循环。根据其中写入的值找到一个非递增的序列即可。

注:这里很难理解,但注意前面的循环,实际上整个过程是在创建连边0x603空间中的值实际上是malloc出来的空间,整个函数实际上就是要找出链表中的非递增节点排序。逆天的难度是由于需要对齐!!!,前面两个数据后面一个padding,一个节点地址。

(gdb) x/12x 0x6032d0
0x6032d0 <node1>:       0x000000010000014c      0x00000000006032f0
0x6032e0 <node2>:       0x00000002000000a8      0x0000000000603300
0x6032f0 <node3>:       0x000000030000039c      0x00000000006032e0
0x603300 <node4>:       0x00000004000002b3      0x0000000000603320
0x603310 <node5>:       0x00000005000001dd      0x0000000000000000
0x603320 <node6>:       0x00000006000001bb      0x0000000000603310

清晰可见整个链表的结构

img

最后得到答案就是(注意取补)

4 3 2 1 6 5

Congratulations! You’ve defused the bomb!

regs ref

rbp base point基址指针寄存器 rbx base 基址寄存器 rsp 栈顶寄存器

rbx,rbp,r12,r13,14,15 用作数据存储,遵循被调用者使用规则,简单说就是随便用,调用子函数之前要备份它,以防他被修改

al寄存器是rax八位寄存器的一部分(地位),还有相似的其他寄存器,例如bl等

参数号对应寄存器
第 1 个RDI
第 2 个RSI
第 3 个RDX
第 4 个RCX
第 5 个R8
第 6 个R9

command ref

lea

lea Load Effective Address

lea  源地址,  目的寄存器
lea  0x10(%rax), %rbx

将 %rax + 0x10 这个地址的值,存入 %rbx <==>rbx = rax + 0x10;

tip:

lea  (%rax, %rax, 2), %rdx

计算:%rax + %rax * 2 = 3 * %rax 所以 %rdx = 3 * %rax

cmp

比较两个数据是否相等

eg:

cmp %eax %r12d	# 后者在前,前者在后的比较大小

就是比较寄存器eax与r12d的值,该命令会更改标志寄存器,常与jne,je搭配使用跳转函数

movzbl 与 扩展命令

MovZero-extended Byte to Long

将一个 8 位的字节(byte) 零扩展为 32 位的整数(long),并存入目标寄存器。

这类“扩展命令”还有很多,基本是零扩展与符号扩展

指令含义源 → 目标示例
movzblMove Zero-extended Byte to Long8 → 32movzbl %al, %eax
movzwlMove Zero-extended Word to Long16 → 32movzwl %ax, %eax
movzbqMove Zero-extended Byte to Quad8 → 64movzbq %al, %rax
movzwqMove Zero-extended Word to Quad16 → 64movzwq %ax, %rax
movzlqMove Zero-extended Long to Quad32 → 64movzlq %ax, %rax

test

对两个操作数进行 按位与(bitwise AND) 运算,但不保存结果,只根据结果 更新标志寄存器(EFLAGS)

equal

// test %eax, %eax 相当于:
if (eax & eax) {
    // 非零 ZF = 0
} else {
    // 为零 ZF = 1
}

nopl

No Operation Long

不执行任何有意义的操作(does nothing),只是占用一个时钟周期(极短),用于填充(padding),主要用于 对齐指令边界 或 填充空间

jg 以及相似的条件判断语句

指令全称比较类型用途
jgjump if greater有符号(signed)比较 int、long 等带符号数
jljump if less有符号a < b
jejump if equal相等a == b
jajump if above无符号(unsigned)比较地址、unsigned int
jbjump if below无符号a < b(无符号)

shr

Shift Right

shr $n, %reg        # 将寄存器右移 n 位

⚠️ 注意:AT&T 语法是 shr $移动位数, 源操作数

add 、sub

add\sub 源操作数, 目标操作数

目标 = 目标 +\- 源
标志位说明
ZF (Zero Flag)结果为 0 时置 1
SF (Sign Flag)结果为负时置 1
CF (Carry Flag)无符号溢出(加法有进位,减法有借位)
OF (Overflow Flag)有符号溢出(正+正=负,负+负=正)

mov

mov 源操作数, 目标操作数
目标 = 源

sar

Shift Arithmetic Right

sar $n, %reg        # 将寄存器右移 n 位
sar %reg            # /2

⚠️ 注意:AT&T 语法是 shr $移动位数, 源操作数

gdb ref

break main          # 在 main 函数设断点
break phase_1       # 在 phase_1 设断点
break *0x401234     # 在指定地址设断点
break file.c:10     # 在文件某行设断点
delete              # 删除所有断点
delete 1            # 删除编号为 1 的断点
disable 1           # 禁用断点
enable 1            # 启用断点
run                 # 开始运行程序
run < input.txt     # 带输入文件运行
continue (或 c)     # 继续执行(跳出断点)
stepi (或 si)       # 单步执行一条汇编指令
nexti (或 ni)       # 单步跳过函数调用(汇编级)
step                # 单步进入函数(源码级)
next                # 单步跳过函数(源码级)
finish              # 运行到当前函数返回
disassemble         # 反汇编当前函数
disas phase_1       # 反汇编 phase_1 函数
info registers      # 查看所有寄存器值
print $rax          # 打印寄存器 rax 的值
x/s $rdi            # 以字符串格式查看 $rdi 指向的内容
x/d $rsp            # 以十进制查看栈顶内容
x/10gx $rsp         # 查看栈顶 10 个 8 字节十六进制值
info frame          # 查看当前栈帧信息
backtrace (或 bt)   # 查看调用栈
print variable      # 打印变量值(如果有符号信息)
print &variable     # 打印变量地址
x/20c array         # 以字符形式查看数组前 20 项
x/6dw input_array   # 查看 6 个整数(如 read_six_numbers 的输入)
x/d $rsp + 0x8      # 查看rsp寄存器地址偏移一个字节的位置内容
x/d 0x8($rsp)       # 上一个的等价写法(部分版本不能这么用)
p/x $rsp + 0x8    	# 以十六进制显示的地址值
break phase_2 if $rdi == 0x402450
break *0x401234 if $rax > 100
delete														# 删除所有断点
run
r
启动程序(可带参数,如
run < input.txt

continue
c
继续执行,直到下一个断点或程序结束
step
s
单步执行(进入函数内部)
next
n
单步执行(跳过函数调用)
stepi
si
单步执行一条汇编指令
(关键!用于汇编级调试)
nexti
ni
单步执行一条汇编指令,但不进入函数
finish
fin
运行到当前函数返回(跳出函数)
kill
-
终止当前运行的程序
quit
q
退出 CGDB

特殊操作

# 回溯操作,需程序支持
record	#在run前调用
reverse——stepi
reverse-nexti
reverse-continue	# 反向continue
操作快捷键
进入命令行窗口(输入 GDB 命令)按 i 或 Insert 键
返回源码/汇编窗口按 Esc 键

references

  1. [zhihu CSAPPLab]https://zhuanlan.zhihu.com/p/449879729
  2. (99+ 封私信 / 80 条消息) 手把手教你拆解 CSAPP 的 炸弹实验室 BombLab - 知乎