BOMB LAB
文件来源于《深入理解计算机系统》一书,
BOMB LAB 网址:http://csapp.cs.cmu.edu/3e/labs.html
运行坏境:Ubuntu 22.04.1
使用 GDB 调试 bomb,逐一破译 phrase
先使用反汇编命令
objdump -d bomb > bomb.s
对bomb
进行反汇编,通过输出重定向符号>
保存反汇编内容到bomb.s
文件中。
通过查看bomb.s
文件中的汇编指令,以及对应bomb.c
,可知,依次输入六个 phase,分别检验其准确性,只有输入都正确,才会成功拆解炸弹。
每个 phase 的读取、检验的汇编代码均为同一格式,例如:
-
phrase 的读取、检验 C 代码:
-
phrase 的读取、检验 汇编代码:
下面是函数传参时用到的寄存器
%rdi | %rsi | %rdx | %rcx | %r8 | %r9 | |
---|---|---|---|---|---|---|
作用 | 第一个参数 | 第二个参数 | 第三个参数 | 第四个参数 | 第五个参数 | 第六个参数 |
接下来开始分析汇编代码和破译 phase
1. 破译 phase1
通过分析 main 函数和 phase_1 函数的汇编代码,可以注意到两个寄存器
寄存器 | 作用 |
---|---|
%rdi | 保存用户输入的 phase |
%rsi | 保存 bomb 预先设置的 phase |
查看phase_1
汇编代码段
可以知道函数phase_1
输入参数(用户输入的 phase 字符串)的首地址保存在寄存器%rdi
中,预先设置的 phase1 字符串首地址保存在%rsi
中,通过调用函数strings_not_equal
判断两者是否相同,根据结果判断是否破译 phrase1 成功。
接下来我们使用GDB
对 bomb 进行调试,以获取 phase1 的字符串形式。
从 phase_1 的汇编代码可以知道其存储的地址为0x402400
,下面通过两种方式获取 phase1.
-
在 GDB 中运行 print,可知 phase1 为: Border relations with Canada have never been better.
-
在函数 phase_1 处设置断点,运行到断点处,通过 x/ls 命令 获取 phase1,结果与第一种方法一样
将结果输入到 bomb 运行程序中,phase1 破解成功。
2. 破译 phase2
phase2 的汇编代码偏长,接下来分片段进行分析
-
片段一(读取 6 位数字)
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>
该片段调用了函数
read_six_numbers
,并根据上下文推断出调用read_six_numbers
函数时传递了两个参数,其一为%rdi:用户输入的字符串首地址;其二为%rsi:phase_2
函数分配的数组首元素地址。接下来分析函数
read_six_numbers
000000000040145c <read_six_numbers>: 40145c: 48 83 ec 18 sub $0x18,%rsp 401460: 48 89 f2 mov %rsi,%rdx # y 401463: 48 8d 4e 04 lea 0x4(%rsi),%rcx # y+4 401467: 48 8d 46 14 lea 0x14(%rsi),%rax # y+20 40146b: 48 89 44 24 08 mov %rax,0x8(%rsp) # (%rsp+8)=y+20 401470: 48 8d 46 10 lea 0x10(%rsi),%rax # y+16 401474: 48 89 04 24 mov %rax,(%rsp) # (%rsp)=y+16 401478: 4c 8d 4e 0c lea 0xc(%rsi),%r9 # y+12 40147c: 4c 8d 46 08 lea 0x8(%rsi),%r8 # y+8 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
该函数调用了 sscanf,作用为:input 字符串按照 format 模式串的形式,将字符输出给指定的变量地址,返回成功输入的参数个数
int sscanf(char *input,char *format,arg1,..) { ... }
根据上下文我们可以得出,在
phase_2
中创建了一个长度为 6 的整型数组,暂命名为 y ,而在read_six_numbers
中使用sscanf
将输入中的 6 个整数存储到 y 中,sscanf 各参数所用寄存器或栈地址如下表参数 寄存器或栈地址 内容 input %rdi 用户输入字符串 format %rsi “%d %d %d %d %d %d” arg1 %rdx y arg2 %rcx y+4 arg3 %r8 y+8 arg4 %r9 y+12 arg5 (%rsp) y+16 arg6 (%rsp+8) y+20 对 format 格式内容可以通过 gdb 查看地址为 0x4025c3 的值,结果为"%d %d %d %d %d %d",说明输入应为 以单个空格相隔的六个整数
-
片段二(判别 6 位数字)
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
对数组 y 中的六个数字进行判断,先判断 y[0]是否为 1,之后判断是否
y[n]=y[n-1]*2 (0<n<6)所以 phase2 的正确破解结果为
"1 2 4 8 16 32"
输入后程序显示如下
3. 破译 phase3
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 前半段汇编代码,其也调用了sscanf
函数,接下来查看参数 format 的内容,使用 gdb 的 print 命令打印地址为 0x4025cf 的内容,结果为 "%d %d"
,说明需要用户输入两个整数。
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
...
400fad: e8 88 04 00 00 call 40143a <explode_bomb>
说明用户输入的第一个整数需要满足 0<=d[0]<=7
400f71: 8b 44 24 08 mov 0x8(%rsp),%eax
400f75: ff 24 c5 70 24 40 00 jmp *0x402470(,%rax,8)
使用 jmp 命令的间接跳转格式,说明 0x402470
是一个跳转表的首地址, %rax
是索引, 最后跳转地址为 *(0x402470 + %rax*8)
,接下来我们使用 gdb 执行命令 x/8gx 0x402470
查看跳转表的内容。
x/8gx:
第一个x表示查看
8表示查看指定地址开始的8个地址单元的内容(索引为0~7)
g表示一个地址单元为8个字节
x表示以十六进制显示
其执行结果为
通过跳转表可以看出,phase3 有八种输入情况,分别为
第一个整数(索引) | 第二个整数 |
---|---|
0 | 0xcf |
1 | 0x137 |
2 | 0x2c3 |
3 | 0x100 |
4 | 0x185 |
5 | 0xce |
6 | 0x2aa |
7 | 0x147 |
输入7 327
可以看出 phase3 破解成功。
4. 破译 phase4
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>
还是一样的开始,一样的调用 sscanf
, 而且 format 参数的地址也一样,所以 format 内容为 %d %d
,其用户输入也为两个整数,且第一个整数应小于等于 14。
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>
...
401058: e8 dd 03 00 00 call 40143a <explode_bomb>
该段汇编调用函数 fun4
,且应使返回值为 0, 传入参数分别为 a[0](用户输入的第一个整数)、0x0、0xe
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
由于函数 fun4
调用了自己,所以其为递归函数,对于递归函数,逐行分析较为困难,我们将其逆向为 c 代码,逆向结果如下:
//汇编代码fun4的逆向c代码
// a = ?, b = 0, c = 14
// a in %rdi, b in %rsi, c in %rdx
// 调用函数 fun4(a[0], 0, 14);
// a < 14
int func4(int a, int b, int c)
{
// temp1 in %rax
int temp1 = c - b;
// temp2 in %rcx
int temp2 = (unsigned)temp1 >> 31;
temp1 = (temp1 + temp2) >> 1;
temp2 = temp1 + b;
if (temp2 <= a)
{
temp1 = 0;
if (temp2 >= a)
return temp1;
else
{
b = temp2 + 1;
int r = func4(a, b, c);
return 2 * r + 1;
}
}
else
{
c = temp2 - 1;
int r = func4(a, b, c);
return 2 * r;
}
}
// 简化版的func4
int func4_simple(int a, int b, int c)
{
int temp = c - b;
temp = ((((unsigned)temp >> 31) + temp) >> 1) + b;
if (temp == a)
return 0;
else if (temp < a)
return 2 * func4_simple(a, temp + 1, c) + 1;
else if (temp > a)
return 2 * func4_simple(a, b, temp - 1);
}
由于已知 a[0]在[0,14]区间内,所以对 a[0]进行群举,如下:
int main()
{
int i = 0;
while (i <= 14)
{
if (!func4_simple(i, 0, 14))
printf("%d ", i);
++i;
}
}
得出结果为 0 1 3 7
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
对于 a[1]来说,其值应为 0。至此,可得 phase4 有 种情况,分别为:0 0
、1 0
、3 0
、7 0
。输入结果如下,phase4 破解成功。
5. 破译 phase5
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 //置零
...
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
这一段汇编代码起到了保护运行时栈的作用(对抗缓存区溢出攻击)。采用段寻址的方式从只读内存区读取一个值作为金丝雀值,并保存到当前函数运行时栈帧的0x18(%rsp)
处。在函数结束运行时,判断金丝雀值是否改变,从而判断函数是否正常运行。
40107a: e8 9c 02 00 00 call 40131b <string_length>
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>
调用了函数string_length
, 可以猜测 phase5 是一个字符串且长度为 6。
401082: 74 4e je 4010d2 <phase_5+0x70>
401084: e8 b1 03 00 00 call 40143a <explode_bomb>
...
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 //从该地址+offset处读值
4010a0: 88 54 04 10 mov %dl,0x10(%rsp,%rax,1) //保存到%rsp+0x10 开始的数组
4010a4: 48 83 c0 01 add $0x1,%rax
4010a8: 48 83 f8 06 cmp $0x6,%rax
4010ac: 75 dd jne 40108b <phase_5+0x29>
...
4010d2: b8 00 00 00 00 mov $0x0,%eax
4010d7: eb b2 jmp 40108b <phase_5+0x29>
显而易见,%rax 中存储的是一个对输入字符串的索引,其值为[0, 5],在这个范围内对省略号之间的汇编代码循坏执行,每循环以此%rax+1。每次循坏,都是以输入字符的低四位为索引,去首地址为0x4024b0
的数组中取值存到首地址为%rsp+0x10
的数组 a[6]中。
接下来查看首地址为0x4024b0
的数组中的内容:
内容为:maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?
,因为四位二进制表示的索引值范围为[0,15],所以有效信息为前十六位字符。
现在我们只知道以输入字符的低四位为索引,从maduiersnfotvbylSo
中读取了六位字符顺序存储到 a[6]中,还缺乏一些条件,我们继续看下一段汇编代码。
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>
...
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
注意到调用了函数strings_not_equal
来判断字符串是否相等。其输入的两个参数分别为:
- %rsp+0x10 处的字符串。
- 0x40245e 处的字符串。
接下来查看0x40245e
处的内容,为flyers
这样的话,结合前面的分析,我们可以得出下表
字符 | 索引 | 索引二进制 | 可配对的字符 | 输入顺序 |
---|---|---|---|---|
f | 9 | 1001 | )、9、I、Y、i、y | 1 |
l | 15 | 1111 | /、?、O、_、o | 2 |
y | 14 | 1110 | .、>、N、^、n、~ | 3 |
e | 5 | 0101 | %、5、E、U、e、u | 4 |
r | 6 | 0110 | &、6、F、V、f、v | 5 |
s | 7 | 0111 | '、7、G、W、g、w | 6 |
选取其中一种输入IONEFG
,phase5 破译成功。
6. 破译 phase6
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
,说明输入格式为"%d %d %d %d %d %d"
的六个整数,假定存储到数组 y 中(首地址位于函数phase_6
的%rsp 中)。
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> //y[i]>0
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>
通读一遍该段汇编代码,推测为双层嵌套的循坏,将其逆向为 c 代码如下:
// i in %r12d, j in %rbx, array(y) in %rsp
for(int i=0; i<6; ++i)
{
if(y[i]-1 > 5) explode_bomb();
for(int j=i+1; i<=5; ++j)
if(y[i] == y[j]) explode_bomb();
}
逆向为 c 代码可以很容易看出,输入得到六个数字必须在[1, 6]之间且各不相等。
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>
该段执行 y[i]=7-y[i],(0<=i<=5)
的操作。
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>
先通过 gdb x
命令查看地址$0x6032d0
的内容
(gdb) x 0x6032d0
0x6032d0 <node1>: 0x0000014c
node1
代表其可能有多个类似结构,下面查看更多的内容
(gdb) x/24wx 0x6032d0
0x6032d0 <node1>: 0x0000014c 0x00000001 0x006032e0 0x00000000
0x6032e0 <node2>: 0x000000a8 0x00000002 0x006032f0 0x00000000
0x6032f0 <node3>: 0x0000039c 0x00000003 0x00603300 0x00000000
0x603300 <node4>: 0x000002b3 0x00000004 0x00603310 0x00000000
0x603310 <node5>: 0x000001dd 0x00000005 0x00603320 0x00000000
0x603320 <node6>: 0x000001bb 0x00000006 0x00000000 0x00000000
可以观察到每个 node 的高 8 字节位存储着下一个 node 的地址,所以推测 node 类型为链式结构体数组。node 的低四字节位分别存储着 0~6,猜测其类型为 int 且为应输入的数字,同时考虑到字节对齐、机器为小端模式,所以假设该结构体定义如下
typedef struct node{
int something;
int input;
struct node *next;
}node;
回头再看上面的汇编代码,其实就是将每个 node 的地址按照 node[i].input 与 y[i]的对应关系存储到一个新的结构体数组中 z[i]中,其首地址为 %rsp+0x20;
假设 y[i]={1,2,3,4,5,6},那么 z[i]={0x6032d0,0x6032e0,0x6032f0,0x603300,0x603310,0x603320}。
继续看下面的汇编处理
4011ab: 48 8b 5c 24 20 mov 0x20(%rsp),%rbx //z[0]
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)
这段很绕,看了很久才弄个明白。
结合前面那一段的 node 地址数组 z 来对 node 链表进行重排。即按照用户输入的 1~6 数字,在进行7-y[i]
后的顺序,比对 node.input ,对 node 链表进行重排。
可能没说明白,接下来使用 gdb 断点功能查看函数运行完此段汇编代码的 node 链表内容。
Good work! On to the next...
3 2 1 6 5 4
Breakpoint 1, 0x00000000004011da in phase_6 ()
(gdb) x/24wx 0x6032d0
0x6032d0 <node1>: 0x0000014c 0x00000001 0x006032e0 0x00000000
0x6032e0 <node2>: 0x000000a8 0x00000002 0x006032f0 0x00000000
0x6032f0 <node3>: 0x0000039c 0x00000003 0x00000000 0x00000000
0x603300 <node4>: 0x000002b3 0x00000004 0x00603310 0x00000000
0x603310 <node5>: 0x000001dd 0x00000005 0x00603320 0x00000000
0x603320 <node6>: 0x000001bb 0x00000006 0x006032d0 0x00000000
当输入为 3 2 1 6 5 4
时,7-y[i]后为4 5 6 1 2 3
,如上结果,node 链表依照4 5 6 1 2 3
进行排序,即
node4->node5->node6->node1->node2->node3
再来看最后一段汇编代码。
4011d9: 00
4011da: bd 05 00 00 00 mov $0x5,%ebp
4011df: 48 8b 43 08 mov 0x8(%rbx),%rax //z[0].next
4011e3: 8b 00 mov (%rax),%eax //取z[1].something
4011e5: 39 03 cmp %eax,(%rbx) //z[1]与z[2] something比较
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
此段为以此比较 node 链表中相邻的两个 node.sometging,要求前者大于等于后者,即 y[i]>=y[i+1],i 在[0,4]中,因此比较原始链表中 node.something ,以降序排列可以得出顺序为3 4 5 6 1 2
,被 7 减去得出输入应为4 3 2 1 6 5
.
在程序中输入4 3 2 1 6 5
,成功破解 phase_6。
7. 破译隐藏关卡
在查看bomb.c
时,发现在 main 函数结尾有这样一句话:
/* Wow, they got it! But isn't something... missing? Perhaps
* something they overlooked? Mua ha ha ha ha! */
说明可能存在隐藏关卡,通览 main 函数,发现只有函数phase_defused()
中可能隐藏些东西,查看该函数,发现其确实执行了一些操作并且调用了函数secret_phase
,接下来以此分析。
先看函数 phase_defused
, 发现有很多处直接使用了地址传值,以此打印这些地址内容
4015f0: be 19 26 40 00 mov $0x402619,%esi
# 0x402619: "%d %d %s"
4015f5: bf 70 38 60 00 mov $0x603870,%edi
# 0x603870 <input_strings+240>: ""
401604: be 22 26 40 00 mov $0x402622,%esi
# 0x402622: "DrEvil"
401617: bf f8 24 40 00 mov $0x4024f8,%edi
# 0x4024f8: "Curses, you've found the secret phase!"
401621: bf 20 25 40 00 mov $0x402520,%edi
# 0x402520: "But finding it and solving it are quite different..."
401635: bf 58 25 40 00 mov $0x402558,%edi
# 0x402558: "Congratulations! You've defused the bomb!"
根据打印信息,函数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>
...
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
注意到0x202181(%rip)
,其地址应为(%rip=0x4015d9) + 0x202181 = 0x603760。 只有当该地址值为 6 时才会继续执行…中的函数。
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 //"%d %d %s"
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>
...
401635: bf 58 25 40 00 mov $0x402558,%edi //"Congratulations! You've defused the bomb!"
40163a: e8 d1 f4 ff ff call 400b10 <puts@plt>
...
这段说明当存储在地址0x603870
为%d %d %s
时才会继续执行下面的函数。
401604: be 22 26 40 00 mov $0x402622,%esi // "DrEvil"
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 // "Curses, you've found the secret phase!"
40161c: e8 ef f4 ff ff call 400b10 <puts@plt>
401621: bf 20 25 40 00 mov $0x402520,%edi //"But finding it and solving it are quite different..."
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>
由此可见,只有当地址处内容符合"%d %d %s"且第三个字符串为DrEvil
时会调用函数secret_phase
进入隐藏关卡。此外,由于时连续地址存储且两位数字,最符合的为 phase3 或 phase4
接下来,使用 gdb 打断点调试 bomb,查看何时符合进入隐藏关卡条件。
在该处打上断点
4015fa: e8 f1 f5 ff ff call 400bf0 <__isoc99_sscanf@plt>
当所有 phase 输入完之后,发现断点被执行,查看0x603870
处为输入的 phase4,说明在输入 phase4 时加上DrEvil
且所有 phase 输入正确后进入隐藏关卡。
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Border relations with Canada have never been better.
Phase 1 defused. How about the next one?
1 2 4 8 16 32
That's number 2. Keep going!
7 327
Halfway there!
0 0 DrEvil
So you got that one. Try this one.
IONEFG
Good work! On to the next...
4 3 2 1 6 5
Curses, you've found the secret phase!
But finding it and solving it are quite different...
当 phase4 输入为0 0 DrEvil
时,成功进入隐藏关卡。
继续看函数secret_phase
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 # %eax-1
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
调用了函数strtol
,调用格式为strtol(intput, 0, 10);
,查看其定义如下:
strtol是一个C语言函数,作用就是将一个字符串转换为长整型long
long int strtol (const char* str, char** endptr, int base);
其中:
str是要转换的字符
enptr是指向第一个不可转换的字符位置的指针
base的基数,表示转换成为几进制的数
且:
1.当 base 的值为 0 时,默认采用 10 进制转换,但如果遇到 '0x'/'0X' 前置字符则会使用 16 进制转换,遇到 '0' 前置字符则会使用 8 进制转换。
2.若endptr 不为NULL,则会将遇到的不符合条件而终止的字符指针由 endptr 传回;若 endptr 为 NULL,则表示该参数无效,或不使用该参数。
因此调用该函数后就将输入的有数字的字符串转为数字,且输入应满足 in - 1 <= 1000
.
注意到
40126e: bf f0 30 60 00 mov $0x6030f0,%edi
使用 gdb x
命令查看此处内容,可能为一片连续空间,查看更多的内容如下
(gdb) x/60gx 0x6030f0
0x6030f0 <n1>: 0x0000000000000024 0x0000000000603110
0x603100 <n1+16>: 0x0000000000603130 0x0000000000000000
0x603110 <n21>: 0x0000000000000008 0x0000000000603190
0x603120 <n21+16>: 0x0000000000603150 0x0000000000000000
0x603130 <n22>: 0x0000000000000032 0x0000000000603170
0x603140 <n22+16>: 0x00000000006031b0 0x0000000000000000
0x603150 <n32>: 0x0000000000000016 0x0000000000603270
0x603160 <n32+16>: 0x0000000000603230 0x0000000000000000
0x603170 <n33>: 0x000000000000002d 0x00000000006031d0
0x603180 <n33+16>: 0x0000000000603290 0x0000000000000000
0x603190 <n31>: 0x0000000000000006 0x00000000006031f0
0x6031a0 <n31+16>: 0x0000000000603250 0x0000000000000000
0x6031b0 <n34>: 0x000000000000006b 0x0000000000603210
0x6031c0 <n34+16>: 0x00000000006032b0 0x0000000000000000
0x6031d0 <n45>: 0x0000000000000028 0x0000000000000000
0x6031e0 <n45+16>: 0x0000000000000000 0x0000000000000000
0x6031f0 <n41>: 0x0000000000000001 0x0000000000000000
0x603200 <n41+16>: 0x0000000000000000 0x0000000000000000
0x603210 <n47>: 0x0000000000000063 0x0000000000000000
0x603220 <n47+16>: 0x0000000000000000 0x0000000000000000
0x603230 <n44>: 0x0000000000000023 0x0000000000000000
0x603240 <n44+16>: 0x0000000000000000 0x0000000000000000
0x603250 <n42>: 0x0000000000000007 0x0000000000000000
0x603260 <n42+16>: 0x0000000000000000 0x0000000000000000
0x603270 <n43>: 0x0000000000000014 0x0000000000000000
0x603280 <n43+16>: 0x0000000000000000 0x0000000000000000
0x603290 <n46>: 0x000000000000002f 0x0000000000000000
0x6032a0 <n46+16>: 0x0000000000000000 0x0000000000000000
0x6032b0 <n48>: 0x00000000000003e9 0x0000000000000000
0x6032c0 <n48+16>: 0x0000000000000000 0x0000000000000000
观察该地址空间的内容,推测此处为链表结构,且应为二叉树
typedef struct tree{
int val;
struct tree *left;
struct tree *right;
}tree;
此二叉树如下:
└─ 36
├─ 8
│ ├─ 6
│ │ ├─ left: 1
│ │ └─ right: 7
│ └─ 22
│ ├─ left: 20
│ └─ right: 35
└─ 50
├─ 45
│ ├─ left: 40
│ └─ right: 47
└─ 107
├─ left: 99
└─ right: 1001
接下来则是调用函数fun7
,调用格式为fun7(node, input_num);
再看函数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
递归掉用自己,为了方便查看,将其逆向为 c 代码如下:
int fun7(tree *node, int input_num)
{
if(!node) return -1;
int temp = node->val;
if(temp > input_num) return 2*fun7(node->left, input_num);
else if(temp < input_num) return 2*fun7(node->right, input_num) + 1;
else return 0;
}
寻找使 fun7 返回为 2 的 input_num 值。分析该递归函数,得出如下结论:
- 如果输入不为该二叉树上节点的值,最后返回值一定为负数。
当输入值为22
或20
的时候,fun4 返回结果为 2.