ROP | Linux内核攻防
Task1:绕过stack canary和KASLR
- 首先根据实验指南,当我们
read时,读取的是全局变量prev_cmd,而当我们write的时候,会将buffer先赋值给cmd,再将cmd赋值给prev_cmd;在zjubof_write4中,看似对cmd.length作出了限制,但是并没有对len作出限制,因此存在overflow漏洞,可以通过溢出修改cmd.length,进而将canary/oldfp/ra读到prev_cmd中
| |
函数调用栈如下所示:

- 触发buffer overflow的核心在于:
| |
因此我们可以构造buffer如下:

buf[49] = "0123456789ABCDEF\x30",49 = 48 + 1,48为需要泄露的字节数,1为buf末尾的\0
- 进而我们可以泄露
canary/lr/oldfp,由于lr(ra)保存的是zjubof_write2中调用zjubof_write3之后的返回地址,反汇编zjubof_write2得到无偏移的RA,再与开启KASLR的lr(ra)相减即可得到偏移

关键代码如下:
| |
- 结果

至此,我们成功泄露了canary和offset,之后就可以通过偏移量绕过KASLR
Task2: 修改return address,获取 root 权限
- Task2要求我们修改
ra,跳转到first_level_gadget,由first_level_gadget实现提权;我们先反汇编一下first_level_gadget,发现第一行代码修改了sp,而修改ra后返回时,sp已经被设置好了,如果此时跳转到第一行,sp再次被修改,就会导致栈空间出错,进而导致程序出错,因此需要跳到第二行

- 分析函数调用时
sp的变化
zjubof_writecallzjubof_write2:
| |
zjubof_write2callzjubof_write3:
| |
zjubof_write3callzjubof_write4:
| |
当我们覆盖返回地址跳转到first_level_gadget时,sp+80和sp+32已经被执行,此时的sp[0]和sp[8]分别指向zjubof_write的oldfp(x29)和ra(x30),在first_level_gadget中无需再将x29和x30压栈,同时为了在first_level_gadget执行完后能够正确返回到zjubof_write,需要手动添加指令将sp+220
- 分析得到栈空间如下,并构造buffer

buffer的前24字节和Task1是一样的,但是需要将Task1中得到的canary/oldfp填入buffer的24-40字节,然后将新的返回地址填入40-48字节
- 关键代码及结果
| |

flag:sysde655sEc
Task3: ROP 获取 root 权限
- 这次我们需要使用ROP进行提权,函数调用栈为
| |
- 根据函数调用栈,得到栈布局

- 构造buffer如下
| |
各个ROP的跳转地址分别为:
RA of prepare_kernel_cred: 0xffff8000100a6214

RA of commit_creds: 0xffff8000100a5f6c

RA of second_level_gadget: 0xffff8000107abdb0(因为第一条指令不改变sp,因此可以直接跳 )

RA of zjubof_write: 0xffff8000107abe54

- 关键代码及结果
| |

flag:sysde655sEc
Task4: Linux内核对 ROP 攻击的防护
- 编译运行,之后执行exp
| |
发现出现段错误

- 查看汇编代码,发现相比之前多了
paciasp和autiasp这两个指令

这两个指令由ARM PA机制引入,这是一种指针认证的方式。其通过在存储指针之前向未使用的高位添加加密签名来工作,这也被称为指针认证码(PAC)。paciasp用于在存储指针值之前生成并插入指针认证代码(PAC),而autiasp则用于在从内存读回后验证并删除指针值中的PAC。这种机制可以保证在写入和读取指针之间对指针值的任何更改都会使签名无效,进而CPU会将身份验证失败解释为内存损坏,并设置指针中的高位,使该指针无效并导致应用程序崩溃。

总结来说,在进入函数的时候,栈上存储的不再是返回地址,而是附带着加密后的返回地址,在离开函数时会校验这个地址的合法性,如果合法即未被篡改函数才能正常返回;同时由于攻击者无法得知密钥以及加密算法,因此攻击者无法构造出加密后的返回地址进行buffer overflow,也就无法进行 ROP 攻击
思考题
- 为什么linux canary的最低位byte总是
\00?
保证内存对齐,可以提高内存访问效率;同时可以将其当做字符串的终止符,防止canary的泄露
- 在ARM64的ROP中,在
zjubof_write4中overflow覆盖到的返回地址,会在 什么时候/执行到哪个函数哪一行的时候被load到pc寄存器?
会在zjubof_write3的return 0;,也就是在zjubof_write3汇编的ldp x29, x30, [sp], #32行,x29和x30被更新,然后执行ret时load到pc寄存器
- 在Task2中,为什么在exp中直接覆盖返回地址为
first_level_gadget的汇编第一行地址,会造成kernel在运行到这一行的时候产生panic?并写出造成这个panic的触发链
当跳转到first_level_gadget且尚未执行其中的汇编时,栈空间如下所示

此时zjubof_write3和zjubof_write4部分的栈空间已经被释放,可以看到sp已经指向了栈底,同时x29和x30寄存器的值已经是被正确压栈了,此时我们如果跳到第一行,则又会将x29和x30压栈,而此时的x29和x30寄存器保存的是first_level_gadget的中的返回地址和旧栈帧,也就是下面这样

当first_level_gadget执行完成后,会首先更新x29和x30,正常情况下会跳回zjubof_write1,并将sp+220进而恢复sp,而现在则会再次跳到first_level_gadget,同时sp+220也会更新到错误的地方,之后就是重复的再次跳到first_level_gadget,同时sp+220错误的更新,直到stack overflow
- Linux 内核是如何利用 ARM PA 来防御 ROP 攻击的
见Task4中的分析
附exp.c完整代码:
| |
Written by Jiacheng Hu, at Zhejiang University, Hangzhou, China.
