CSAPP BombLab
时间轴
2025-06-24
init
环境
win11 WSL2:
环境搭建1
2git 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 |
|
可以看到read_line的返回值作为参数传递给phase_x
可以看到,main函数每次调用read_line读取输入,随后调用phase_x函数,随后调用phase_defused打印信息
函数开头(栈帧设置):
1 | 400b10: a9bf7bfd stp x29, x30, [sp, #-16]! // 保存帧指针和返回地址到栈上,sp -= 16 |
获取 fgets 的目标缓冲区(x0),准备调用 _IO_fgets
:
该函数定义如下:
1 | char *fgets(char *s, int size, FILE *stream); |
可以看到第一个参数是char*(x0),第二个参数为int(w1),第三个参数为一个结构体指针(x2)
1 | 400b18: f00004e0 adrp x0, 49f000 |
到这里,函数已经从输入中读取了一行字符串到
0x4a2138
地址。
处理 fgets 结果:遍历字符串查找换行符:
1 | 400b34: d2800000 mov x0, #0 // x0 = 0,作为字符串偏移索引 |
如果没有在前 80 字节找到 ‘\n’,调用 explode
:
1 | 400b5c: 97ffffe6 bl 400af4 <explode> // 没有 '\n',爆炸 |
如果找到了换行符 \n
,把它替换为 \0
(字符串结束):
1 | 400b60: d0000501 adrp x1, 4a2000 |
返回前恢复现场:
1 | 400b6c: d0000500 adrp x0, 4a2000 |
可以看到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 =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返回后一直未变
函数调用结束后比较返回值和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 |