时间轴
时间轴
2025-06-24
init
本文介绍了在Win11 WSL2环境下,针对CSAPP BombLab的AArch64版本进行逆向分析与解题的过程。文章详细描述了环境搭建方法,以及通过GDB调试工具分析main函数调用流程,逐步破解phase_0至phase_3的步骤。其中,phase_0要求输入整数2022;phase_1要求输入字符串"Fault Tolerance: Reliable Systems from Unreliable Components.";phase_2要求输入8个满足特定递推关系的数字(1,1,6,11,21,36,61,101);phase_3则涉及更复杂的逻辑判断。文章总结了各阶段的栈帧结构、寄存器使用和关键比较指令,为读者提供了完整的解题思路和答案。
环境
Win11 WSL2:

环境搭建
123 | git clone https://github.com/SJTU-IPADS/OS-Course-Lab.gitcd OS-Course-Lab/Lab0sudo apt-get install qemu-user gdb-multiarch |
terminal1
可以把答案写在 ans.txt 上,这样已经解过的就不用再敲了
1 | qemu-aarch64-static -g 1234 ./bomb < ans.txt |
terminal2
1 | gdb-multiarch -ex "set architecture aarch64" -ex "target remote localhost:1234" -ex "file bomb" |
main
main 函数是有 C 代码的
1234567891011121314151617181920212223242526272829303132333435 | int main() { char* input; printf("Type in your defuse password!\n"); input = read_line(); phase_0(input); phase_defused(); input = read_line(); phase_1(input); phase_defused(); input = read_line(); phase_2(input); phase_defused(); input = read_line(); phase_3(input); phase_defused(); input = read_line(); phase_4(input); phase_defused(); input = read_line(); phase_5(input); phase_defused(); printf("Congrats! You have defused all phases!\n"); return 0;} |
可以看到 read_line 的返回值作为参数传递给 phase_x

可以看到,main 函数每次调用 read_line 读取输入,随后调用 phase_x 函数,随后调用 phase_defused 打印信息

函数开头(栈帧设置):
12 | 400b10: a9bf7bfd stp x29, x30, [sp, #-16]! // 保存帧指针和返回地址到栈上,sp -= 16400b14: 910003fd mov x29, sp // 设置新的帧指针 x29 = sp |
获取 fgets 的目标缓冲区(x0),准备调用_IO_fgets:
该函数定义如下:
1 | char *fgets(char *s, int size, FILE *stream); |
可以看到第一个参数是 char*(x0),第二个参数为 int(w1),第三个参数为一个结构体指针(x2)
12345678910 | 400b18: f00004e0 adrp x0, 49f000400b1c: f946d000 ldr x0, [x0, #3488]400b20: f9400002 ldr x2, [x0] // FILE *stream400b24: 52800a21 mov w1, #0x51 // w1 = 81,最多读取 81 字节400b28: d0000500 adrp x0, 4a2000400b2c: 9104e000 add x0, x0, #0x138 // x0 = 0x4a2000 + 0x138,x0 是目标 buffer 指针400b30: 94004a78 bl 413510 <_IO_fgets> // 调用 _IO_fgets(buffer, 81, stream) |
到这里,函数已经从输入中读取了一行字符串到
0x4a2138地址。
处理 fgets 结果:遍历字符串查找换行符:
12345678910111213 | 400b34: d2800000 mov x0, #0 // x0 = 0,作为字符串偏移索引// 再次构造 x2 = buffer 基址400b38: d0000502 adrp x2, 4a2000400b3c: 9104e042 add x2, x2, #0x138 // x2 = buffer400b40: 38626801 ldrb w1, [x0, x2] // w1 = buffer[x0]400b44: 34000141 cbz w1, 400b6c // 如果 w1 == 0,字符串结束,跳到 return400b48: 7100283f cmp w1, #0xa // 检查是否是换行符 '\n'400b4c: 540000a0 b.eq 400b60 // 是换行符,跳转到去掉换行符400b50: 91000400 add x0, x0, #1 // x0++,继续检查下一个字符400b54: f101401f cmp x0, #0x50 // 最多检查 0x50 字节(最多 80 字节)400b58: 54ffff41 b.ne 400b40 // 没到头就继续循环 |
如果没有在前 80 字节找到 ‘\n’,调用explode:
1 | 400b5c: 97ffffe6 bl 400af4 <explode> // 没有 '\n',爆炸 |
如果找到了换行符\n,把它替换为\0(字符串结束):
123 | 400b60: d0000501 adrp x1, 4a2000400b64: 9104e021 add x1, x1, #0x138 // x1 = buffer400b68: 3820c83f strb wzr, [x1, w0, sxtw] // buffer[x0] = 0(wzr 是零寄存器) |
返回前恢复现场:
12345 | 400b6c: d0000500 adrp x0, 4a2000400b70: 9104e000 add x0, x0, #0x138 // x0 = buffer,作为返回值400b74: a8c17bfd ldp x29, x30, [sp], #16 // 恢复帧指针和返回地址,sp += 16400b78: d65f03c0 ret // 返回 |

可以看到 phase_defused 函数读取一个全局变量值,然后减一
随后加载一个全局地址 0x464000+0x7c0 = 0x4647c0 作为参数调用 printf 打印

而 w1 存的是最开始减一的全局变量的值,地址是 0x4a0000+ 80 = 0x4a0000 + 0x50 = 0x4a0050。

phase_0

1 | stp x29, x30, [sp, #-16]! |
- 作用:将 x29(frame pointer,帧指针)和 x30(link register,返回地址)压入栈中。这一步保存了调用者的帧指针和返回地址,便于后续恢复。
1 | mov x29, sp |
- 作用:将当前的栈指针 sp 赋值给帧指针 x29,即建立当前函数的新栈帧。
从现在开始,x29 就指向这个函数调用的栈底,方便以后访问局部变量或传递参数。
随后,函数先是调用 read_int 函数读取一个 int 值,将返回值存入 w0,然后对比 0x4a0000 + 0x54(84) = 0x4a0054 的值看是否相等。可通过 gdb examine 命令查看内存这个位置的值:

可以看到是 2022
随后是 cmp 指令对比输入的值(存储在 x0)和 2022(存储在 x1),如果不相等则跳到调用 explode 函数的那一行,所以必须要相等,即输入值必须是 2022
phase_1
phase_1 和 phase_0 差不多

可以看到函数把 0x4a0000+0x58(88) = 0x4a0058 地址的值装入 x1,然后调用 strcmp,其参数应该是 x0,x1,而 x0 是我们输入的值,也就是说让我们输入的字符串和这个 0x4a0058 地址的字符串进行比较,看返回值 w0 是否为 0,不为 0 就跳转到调用 explode 的那一行

这里需要注意的是 0x4a0058 地址存放的不是字符串,而是字符串的地址,所以要先读出这个地址,然后读字符串地址的字符串
答案为:“Fault Tolerance: Reliable Systems from Unreliable Components.”
phase_2

函数开辟了 64 字节的栈空间,共可存放 64/8=8 个 64 位值,首先将 x29 放在 sp,将 x30 放在 sp+8
随后保存 sp 到 x29 寄存器
随后将 x19, x20 寄存器的值放在 sp+16, sp+24 的位置,即
- sp —> x29
- sp+8 —> x30
- sp+16 —> x19
- sp+24 —> x20
随后 x1 = sp+ 0x20 = sp+32,作为 read_8_numbers 的第二个参数,随后调用 read_8_numbers 这个函数

可以看到,该函数首先开辟 0x20 即 32 字节的栈空间 sp= sp-32,随后将 x29 存入 sp+16,x30 存入 sp+24
随后设置栈帧指针 x29 为 sp+0x10 即 sp+16
随后将 x1 放入 x2,x1 是 read_8_numbers 的第二个参数
随后,x1 = x1+0x1c = x1+ 28
将 x1 存入 sp+8
x1 = x2 + 0x18 = x2 + 24
将 x1 存入 sp
随后
x7 = x2+ 0x14
x6 = x2 + 0x10
x5 = x2+0xc
x4 = x2+ 0x8
x3 = x2+0x4
x1 是地址 0x464000+0x858 的值,是一个指针,这里可以明显看出是格式化字符串的起始地址,作为 __isoc99_scanf 的第二个参数
1 | int sscanf(const char *str, const char *format, ...); |
第一个参数是 x0,即从 read_line 返回后一直未变

函数调用结束后比较返回值和 0x7 的大小,如果小于等于就爆炸,因此必须要输入 8 个数字。
函数返回后恢复原来的栈结构
- sp —> x29
- sp+8 —> x30
- sp+16 —> x19
- sp+24 —> x20
- sp+32 —> array 的首地址,共 8 个数字,也是第一个数字的地址 array[0]
- sp+36 —> array[1]
- sp+40 —> array[2]
- sp+44 —> array[3]
- sp+48 —> array[4]
- sp+52 —> array[5]
- sp+56 —> array[6]
- sp+60 —> array[7]
刚好占满 phase_2 最初开辟的 64 字节

然后是对输入的 8 个数字的判断了,这里很容易看出第一个数字和第二个数字都必须为 1
随后是一个循环

x19 = sp+0x20 = sp+32 即 array[0]
x20 = sp+0x38 = sp+56 即终止条件为 (56-32)/4=6,即到第六个就停止
首先 b 0x4007d0 跳过一次 x19 的自增与和 x20 的比较
如果 x19 == x20 则跳到 phase_2+108 结束
否则,首先将 x19 作为地址加载到 w0(array[i]),将它下一个加载到 w1(array[i+1])
令 w0 = array[i]+array[i+1] +4
w1 = array[i+2]
比较 w0 是否等于 w1,如果相等则跳到 phase_2+60,将 i++
如果不相等则爆炸
第一个数和第二个数必须为 1
那么第三个数字为 1+1+4=6
第四个数字为 1+6+4=11
第五个数字为 6+11+4=21
第六个数字为 11+21+4=36
第七个数字为 21+36+4=61
第八个数字为 61+36+4=101
故答案为
1 | 1 1 6 11 21 36 61 101 |