时间轴

2025-06-24

init


环境

win11 WSL2:

硬件环境

环境搭建

1
2
git clone https://github.com/SJTU-IPADS/OS-Course-Lab.git
cd OS-Course-Lab/Lab0

terminal1

1
qemu-aarch64-static -g 1234 ./bomb

terminal2

1
gdb-multiarch -ex "set architecture aarch64" -ex "target remote localhost:1234" -ex "file bomb"

main

main函数是有C代码的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include "phases.h"
#include "utils.h"

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

image-20250629225404452

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

image-20250629225341441

函数开头(栈帧设置):

1
2
400b10: a9bf7bfd    stp x29, x30, [sp, #-16]!    // 保存帧指针和返回地址到栈上,sp -= 16
400b14: 910003fd mov x29, sp // 设置新的帧指针 x29 = sp

获取 fgets 的目标缓冲区(x0),准备调用 _IO_fgets

该函数定义如下:

1
char *fgets(char *s, int size, FILE *stream);

可以看到第一个参数是char*(x0),第二个参数为int(w1),第三个参数为一个结构体指针(x2)

1
2
3
4
5
6
7
8
9
10
400b18: f00004e0    adrp x0, 49f000
400b1c: f946d000 ldr x0, [x0, #3488]
400b20: f9400002 ldr x2, [x0] // FILE *stream

400b24: 52800a21 mov w1, #0x51 // w1 = 81,最多读取 81 字节

400b28: d0000500 adrp x0, 4a2000
400b2c: 9104e000 add x0, x0, #0x138 // x0 = 0x4a2000 + 0x138,x0 是目标 buffer 指针

400b30: 94004a78 bl 413510 <_IO_fgets> // 调用 _IO_fgets(buffer, 81, stream)

到这里,函数已经从输入中读取了一行字符串到 0x4a2138 地址。

处理 fgets 结果:遍历字符串查找换行符:

1
2
3
4
5
6
7
8
9
10
11
12
13
400b34: d2800000    mov x0, #0                   // x0 = 0,作为字符串偏移索引

// 再次构造 x2 = buffer 基址
400b38: d0000502 adrp x2, 4a2000
400b3c: 9104e042 add x2, x2, #0x138 // x2 = buffer

400b40: 38626801 ldrb w1, [x0, x2] // w1 = buffer[x0]
400b44: 34000141 cbz w1, 400b6c // 如果 w1 == 0,字符串结束,跳到 return
400b48: 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(字符串结束):

1
2
3
400b60: d0000501    adrp x1, 4a2000
400b64: 9104e021 add x1, x1, #0x138 // x1 = buffer
400b68: 3820c83f strb wzr, [x1, w0, sxtw] // buffer[x0] = 0(wzr是0寄存器)

返回前恢复现场:

1
2
3
4
5
400b6c: d0000500    adrp x0, 4a2000
400b70: 9104e000 add x0, x0, #0x138 // x0 = buffer,作为返回值

400b74: a8c17bfd ldp x29, x30, [sp], #16 // 恢复帧指针和返回地址,sp += 16
400b78: d65f03c0 ret // 返回

image-20250629225645707

可以看到phase_defused函数读取一个全局变量值,然后减一

随后加载一个全局地址0x464000+0x7c0 = 0x4647c0作为参数调用printf打印

image-20250630125413000

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

image-20250630125539188

phase_0

image-20250624203514602

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命令查看内存这个位置的值:

image-20250624204122130

可以看到是2022

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

phase_1

phase_1和phase_0差不多

image-20250624205525968

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

image-20250624205829889

里需要注意的是0x4a0058地址存放的不是字符串,而是字符串的地址,所以要先读出这个地址,然后读字符串地址的字符串

答案为:”Fault Tolerance: Reliable Systems from Unreliable Components.”

phase_2

image-20250629223737226

第一行开辟了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这个函数

image-20250629224840291

可以看到,该函数首先开辟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 =x1 + 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返回后一直未变

image-20250630140224444

函数调用结束后比较返回值和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字节

image-20250630142117435

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

随后是一个循环

image-20250630142644664

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

phase_3