Bomb Lab 属于《深入理解计算机系统》第三章 —— 程序的机器级表示,主要通过练习加深汇编知识的相关记忆。上一篇 中介绍了前三关的具体解答,本文继续解答后三关和隐藏关。
我们在通过前面的步骤成功获取了前三个字符串,此时我们可以开始获取第四个字符串。让我们重新运行程序 (gdb bomb),在 phase_4 入口处打上断点,然后运行程序使其进入函数 phase_4,并获取 phase_4 汇编代码:
...
Breakpoint 1, 0x000000000040100c in phase_4 ()
# 获取函数 phase_4 的汇编代码
(gdb) disas
Dump of assembler code for function phase_4:
=> 0x40100c <+0>: sub $0x18,%rsp
0x401010 <+4>: lea 0xc(%rsp),%rcx
0x401015 <+9>: lea 0x8(%rsp),%rdx
0x40101a <+14>: mov $0x4025cf,%esi
0x40101f <+19>: mov $0x0,%eax
0x401024 <+24>: callq 0x400bf0 <__isoc99_sscanf@plt>
0x401029 <+29>: cmp $0x2,%eax
0x40102c <+32>: jne 0x401035
0x40102e <+34>: cmpl $0xe,0x8(%rsp)
0x401033 <+39>: jbe 0x40103a
0x401035 <+41>: callq 0x40143a
0x40103a <+46>: mov $0xe,%edx
0x40103f <+51>: mov $0x0,%esi
0x401044 <+56>: mov 0x8(%rsp),%edi
0x401048 <+60>: callq 0x400fce
0x40104d <+65>: test %eax,%eax
0x40104f <+67>: jne 0x401058
0x401051 <+69>: cmpl $0x0,0xc(%rsp)
0x401056 <+74>: je 0x40105d
0x401058 <+76>: callq 0x40143a
0x40105d <+81>: add $0x18,%rsp
0x401061 <+85>: retq
End of assembler dump.
0x40100c <+0> ~ 0x40102c <+32>: 和前面类似,主要是通过 sscanf 获取输入字符串的两个 32 位有符号整数,假设分别存储在 a (0x8(%rsp)) 和 b (0xc(%rsp)) 中。如果没有成功获取两个整数,那么就会引爆炸弹,无法继续运行。
0x40102e <+34> ~ 0x401033 <+39>: 主要是判断 0 <= a <= 14 是否成立(注意跳转使用的是 jbe 指令,所以是把 a 当作无符号数时必须小于等于 14 ,即 a 必须非负且小于等于 14),成立则跳转至 0x40103a <+46> 继续运行;不成立则会引爆炸弹,无法继续运行。
0x40103a <+46> ~ 0x401048 <+60>: 主要是调用函数 func4 ,该函数签名大致为 int func4(int i, int j, int k) 。我们调用语句大致为 int c = func4(a, 0, 14); 。
0x40104d <+65> ~ 0x40104f <+67>: 判断 c == 0 是否成立 ,不成立则引爆炸弹,成立时继续运行。此时我们确定 func4 函数的调用结果必须为 0 ,从而反推出第一个数 (a) 的值。
0x401051 <+69> ~ 0x401056 <+74>: 判断 b == 0 是否成立,不成立则引爆炸弹,成立时继续运行并可顺利结束。此时我们已经确定了第二个数 (b) 为 0 。
此时我们已经确认了第二个数的值,还需确认第一个数的值。我们需要给 func4 入口处打上断点,并运行至 func4 入口处,查看 func4 的汇编代码:
# 在函数 func4 入口处打上断点
(gdb) break func4
Breakpoint 2 at 0x400fce
# 继续运行至 func4 处打上断点
(gdb) continue
Continuing.
Breakpoint 2, 0x0000000000400fce in func4 ()
# 获取函数 func4 的汇编代码
(gdb) disas
Dump of assembler code for function func4:
=> 0x400fce <+0>: sub $0x8,%rsp
0x400fd2 <+4>: mov %edx,%eax
0x400fd4 <+6>: sub %esi,%eax
0x400fd6 <+8>: mov %eax,%ecx
0x400fd8 <+10>: shr $0x1f,%ecx
0x400fdb <+13>: add %ecx,%eax
0x400fdd <+15>: sar %eax
0x400fdf <+17>: lea (%rax,%rsi,1),%ecx
0x400fe2 <+20>: cmp %edi,%ecx
0x400fe4 <+22>: jle 0x400ff2
0x400fe6 <+24>: lea -0x1(%rcx),%edx
0x400fe9 <+27>: callq 0x400fce
0x400fee <+32>: add %eax,%eax
0x400ff0 <+34>: jmp 0x401007
0x400ff2 <+36>: mov $0x0,%eax
0x400ff7 <+41>: cmp %edi,%ecx
0x400ff9 <+43>: jge 0x401007
0x400ffb <+45>: lea 0x1(%rcx),%esi
0x400ffe <+48>: callq 0x400fce
0x401003 <+53>: lea 0x1(%rax,%rax,1),%eax
0x401007 <+57>: add $0x8,%rsp
0x40100b <+61>: retq
End of assembler dump.
这一段代码比较繁琐,而且涉及到递归调用,直接根据每块指令看不太容易理解,所以直接可以先直译出相关的 C 代码:
inf func4(int i, int j, int k) {
// 0x400fd2 <+4>: mov %edx,%eax
// 0x400fd4 <+6>: sub %esi,%eax
int l = k - j;
// 0x400fd6 <+8>: mov %eax,%ecx
// 0x400fd8 <+10>: shr $0x1f,%ecx
// 由于 shr 是逻辑右移,而 C 语言是算术右移动,所以最后还要再与上 0x1
// 这一句相当于取符号位
int m = (l >> 31) & 0x1;
// 0x400fdb <+13>: add %ecx,%eax
// 0x400fdd <+15>: sar %eax
l = (l + m) >> 1;
// 0x400fdf <+17>: lea (%rax,%rsi,1),%ecx
m = l + j;
// 0x400fe2 <+20>: cmp %edi,%ecx
// 0x400fe4 <+22>: jle 0x400ff2
if (l <= i) {
// 0x400ff7 <+41>: cmp %edi,%ecx
// 0x400ff9 <+43>: jge 0x401007
if (l >= i) {
// 0x400ff2 <+36>: mov $0x0,%eax
return 0;
} else {
// 0x400ffb <+45>: lea 0x1(%rcx),%esi
// 0x400ffe <+48>: callq 0x400fce
// 0x401003 <+53>: lea 0x1(%rax,%rax,1),%eax
return 2 * func4(i, j + 1, k) + 1
}
} else {
// 0x400fe6 <+24>: lea -0x1(%rcx),%edx
// 0x400fe9 <+27>: callq 0x400fce
// 0x400fee <+32>: add %eax,%eax
return 2 * func4(i, j, k - 1)
}
}
直译后的代码还是有点难懂(难怪 main.c 里要问数学好不好),但是我们知道这个函数需要返回 0 才行,找到对应的分支语句,可以反推出 i == l , i 就是我们需要确定的值,而 l 只与 j, k 有关,并且第一次调用 func4 时 j = 0, k = 14 ,根据函数最开始的语句可以算出 l = 7 ,所以 i 的值也为 7 ,并且满足 0 <= i <= 14 ,所以 i = 7 是一个合法的解。(已经获取了 func4 的函数,所以可以直接枚举 i = 0 ~ 14 ,可得合法的解为: 0, 1, 3, 7)
至此我们已经推断出了字符串中两个数字的值分别为 7 和 0 ,那么第四个字符串为: 7 0 。
我们在通过前面的步骤成功获取了前四个字符串,此时我们可以开始获取第五个字符串。让我们重新运行程序 (gdb bomb),在 phase_5 入口处打上断点,然后运行程序使其进入函数 phase_5,并获取 phase_5 汇编代码:
...
Breakpoint 1, 0x0000000000401062 in phase_5 ()
# 获取函数 phase_5 的汇编代码
(gdb) disas
Dump of assembler code for function phase_5:
=> 0x401062 <+0>: push %rbx
0x401063 <+1>: sub $0x20,%rsp
0x401067 <+5>: mov %rdi,%rbx
0x40106a <+8>: mov %fs:0x28,%rax
0x401073 <+17>: mov %rax,0x18(%rsp)
0x401078 <+22>: xor %eax,%eax
0x40107a <+24>: callq 0x40131b
0x40107f <+29>: cmp $0x6,%eax
0x401082 <+32>: je 0x4010d2
0x401084 <+34>: callq 0x40143a
0x401089 <+39>: jmp 0x4010d2
0x40108b <+41>: movzbl (%rbx,%rax,1),%ecx
0x40108f <+45>: mov %cl,(%rsp)
0x401092 <+48>: mov (%rsp),%rdx
0x401096 <+52>: and $0xf,%edx
0x401099 <+55>: movzbl 0x4024b0(%rdx),%edx
0x4010a0 <+62>: mov %dl,0x10(%rsp,%rax,1)
0x4010a4 <+66>: add $0x1,%rax
0x4010a8 <+70>: cmp $0x6,%rax
0x4010ac <+74>: jne 0x40108b
0x4010ae <+76>: movb $0x0,0x16(%rsp)
0x4010b3 <+81>: mov $0x40245e,%esi
0x4010b8 <+86>: lea 0x10(%rsp),%rdi
0x4010bd <+91>: callq 0x401338
0x4010c2 <+96>: test %eax,%eax
0x4010c4 <+98>: je 0x4010d9
0x4010c6 <+100>: callq 0x40143a
0x4010cb <+105>: nopl 0x0(%rax,%rax,1)
0x4010d0 <+110>: jmp 0x4010d9
0x4010d2 <+112>: mov $0x0,%eax
0x4010d7 <+117>: jmp 0x40108b
0x4010d9 <+119>: mov 0x18(%rsp),%rax
0x4010de <+124>: xor %fs:0x28,%rax
0x4010e7 <+133>: je 0x4010ee
0x4010e9 <+135>: callq 0x400b30 <__stack_chk_fail@plt>
0x4010ee <+140>: add $0x20,%rsp
0x4010f2 <+144>: pop %rbx
0x4010f3 <+145>: retq
End of assembler dump.
0x40106a <+8> ~ 0x401073 <+17>: 看到 mov %fs:0x28,%rax 就想起了这是栈破坏检测的相关代码 (P199) ,主要是检测栈是否被破坏,也说明了我们刚刚定义了一个数组,并且这个数组在后面会作为参数传入一个函数中
0x401078 <+22> ~ 0x401082 <+32>: 主要判断我们输入的字符串的长度是否为 6 ,不为 6 则引爆炸弹,否则继续运行后续代码
0x4010d2 <+112> ~ 0x4010d7 <+117>: 将寄存器 %eax 赋值为 0 (书中 P123 提到 movl 指令以寄存器作为目的时,它会把该寄存器的高位 4 字节设置为 0 ,因此没必要使用 movq $0x0, %rax)
0x40108b <+41> ~ 0x4010ae <+76>: 这一段是循环,主要是遍历输入字符串的 6 个字符 ch ,并取该字符的低 4 位作为一个下标 index ,然后将相对地址 0x4024b0 偏移为 index 的一个字节赋值给我们定义的数组中的对应位置(假设输入字符串为 s ,栈中数组的定义语句为 char d[7]; ,地址 0x4024b0 对应的字符数组为 chs ,那么这一段循环的意思为 d[i] = chs[s[i] & 0xf];)
0x4010b3 <+81> ~ 0x4010c4 <+98>: 判断字符串 d 与以地址 0x40245e 开始的字符串是否相等,不相等则引爆炸弹,相等则继续运行校验栈是否被破坏,然后通过第五个字符串的验证