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中存在一个复杂的开栈记录数据的过程
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
- %fs是一个 段寄存器(segment register)在 Linux x86-64 中,%fs 通常指向当前线程的 Thread Local Storage(TLS) 区域它用于实现线程局部变量、栈保护等
- 0x28是一个偏移量(40 字节)%fs:0x28 表示:从 %fs 指向的基地址开始,偏移 0x28 字节处的内存
- 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>
简单执行发现:
0x4024b0
处存储着字符串maduiersnfotvbylSo you think you can stop the bomb with ctrl-c , do you?
0x40245e
处存储着字符串flyers
- 要求经过6次迭代处理的字符串(rsp+0x10)位置与
flyers
完全相同 - 现在的结果是
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
相等即可
随意构造一个
)/.%&'
phase_6 LAST_STAGE
Finally!!!
简单查看.s确定初始为6个数字。
1 2 3 4 5 6
简单运行
- r13存储的是rsp的地址
- 第一个值-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>
-
cmp %rsi,(%rax) # (rax) = 7 - (rsp) rax = rsp
这说明整个循环1就是为了将数组做一个互补映射 6 -> 1 5 -> 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次,遍历数组
- 此时 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’ ```
-
其在原数组后新构建了一个数组(每个数字长一个字节),在本次测试中的结果如下
(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
清晰可见整个链表的结构
最后得到答案就是(注意取补)
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),并存入目标寄存器。
这类“扩展命令”还有很多,基本是零扩展与符号扩展
指令 | 含义 | 源 → 目标 | 示例 |
---|---|---|---|
movzbl | Move Zero-extended Byte to Long | 8 → 32 | movzbl %al, %eax |
movzwl | Move Zero-extended Word to Long | 16 → 32 | movzwl %ax, %eax |
movzbq | Move Zero-extended Byte to Quad | 8 → 64 | movzbq %al, %rax |
movzwq | Move Zero-extended Word to Quad | 16 → 64 | movzwq %ax, %rax |
movzlq | Move Zero-extended Long to Quad | 32 → 64 | movzlq %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 以及相似的条件判断语句
指令 | 全称 | 比较类型 | 用途 |
---|---|---|---|
jg | jump if greater | 有符号(signed) | 比较 int、long 等带符号数 |
jl | jump if less | 有符号 | a < b |
je | jump if equal | 相等 | a == b |
ja | jump if above | 无符号(unsigned) | 比较地址、unsigned int |
jb | jump 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 键 |