在今天的玄武实验室的安全推送中,看到了Removing ROP Gadgets from OpenBSD这个议题的PPT,一开始看了下标题,感觉有点疑惑,但是没马上看,后来下午抽实践看了看,感觉这个操作还是可以的。
注意下面针对的是系统是OpenBSD,而且是kernel,思路值得借鉴
这个我之前没看到过,所以把它叫做新思路
ROP简介
说到ROP就得说说ROP Gadgets
ROP Gadgets就是汇编代码中的片段,一般是ret或者jmp结尾的
他们可能完成下面的一些功能:
1、寄存器赋值
2、给寄存器加上一个数
3、寄存器置0
4、调用函数
5、改变esp的指向
6、。。。。。。
ROP Gadgets还可以分为对齐的,还有不对齐的,不对其就是地址偏移了
比如下面的
1 | 8a 5d c3 movb -61(%rbp), %bl |
但是假如你将汇编解析的起始地址指向5d的位置,那么汇编的意思就变了
1 | 5d popq %rbp |
比如你想执行
1 | execve(“/bin//sh”, NULL, NULL) |
你可能需要布置下面的ROP链
目的就是将寄存器赋值为相应的值,进行系统调用
ROP Gadgets查找工具有ROPGadget、ropper等
作者使用ROPGadget去生成直接可利用的ROP链
如何减少ROP Gadgets
作者讲了两个思路:
1、编译出非预期的returns(就是不是我们经常看到的pop pop ret)
2、使正常的returns难以构成ROP链
并不需要使ROP Gadgets的数量变为0,只需要减少ROP Gadgets的数量使得构建一个可用的ROP链变得困难或者不可能(我们可以用上面的ROP Gadgets查找工具来衡量效果)
Polymorphic Gadget的减少
Polymorphic Gadget 中文直接翻译叫多态Gadget
看了下作者的例子就是通过地址偏移来获得Gadget
在x86/amd64有四种ret类型
抓主要矛盾:C3 ret是最常见的,也是最容易用在Gadget上的
从两方面减少polymorphic gadgets
1、寄存器的选择
2、代码的生成
寄存器的选择
常见的带c3结尾的gadgets,ret前面的汇编指令的ModR/M字节(汇编指令中,Opcode之后就是ModR/M)经常使用的寄存器如下:(这里说的比如常见的汇编:mov ebx,eax)
- 源寄存器使用RAX/EAX/AX/AL
- 目的寄存器使用RBX/EBX/BX/BL
此外下面的指令也经常操作RBX / EBX / BX / BL,比如inc, dec, test
而带B系列的寄存器代理很多c3字节
所以一个idea就是避免使用RBX/EBX/BX/BL
Clang按此顺序分配寄存器:RAX, RCX, RDX, RSI, RDI, R8, R9, R10, R11, RBX, R14,
R15, R12, R13, RBP
可以将RBX寄存器几乎挪到最后:RAX, RCX, RDX, RSI, RDI, R8, R9, R10, R11, R14, R15,
R12, R13, RBX, RBP
当然ebx的顺序也是要改变的
这样,性能的损耗为0,代码的字节可以忽略不计(因为有一些REX prefix字节)
最终减少了kernel的大概4500个唯一gadgets(约为6%),效果还是有的
代码的生成
我们知道有哪些指令会有return的字节(比如c3)
1、ModR/M, SIB或者特殊的指令
2、还有就是常量包含了return字节
我们可以实现相同的功能,但是不使用rerun字节或者要求强制对齐
对于ModR/M, SIB会出现return字节的如下
减少的方法就是
1、先交换寄存器
2、用寄存器进行操作
3、再交换回来
例子如下:
如果上面的方法不能使用,我们就要使用强制对齐,比如我们可以在指令前插入一个陷阱来减少gadget
- 正常的程序会跳过我们的陷阱
- return字节前面的int3会使得gadget受限
例子如下:
损耗总结:
1、效率损耗约为1%,因为xchg指令很快
2、代码方面,影响较小,多了6个字节,一对xchg指令
3、用来强制对齐的字节在4-11个
4、总的来说增大了kernel的大小约为2.5%
最终减少了kernel约60%的gadgets
但是我们还有一些可做
1、清理一些汇编函数
2、一些常量可能需要转换
3、重定向地址
对齐的gadget的减少(Aligned Gadget Reduction)
就是没有进行地址偏移的gadget
首先介绍下RETGUARD(小写好看一点retguard,这个其实跟windows和linux的GS/CANARY的是一样的)
实现如下:
1、给每个函数分配一个随机的cookie(用openbsd.randomdata section来分配)
2、函数开始处:计算cookie^return address,放在栈帧上,记为saved value
3、在函数返回时,计算saved value^return address,再跟cookie比较,不相等就终止程序运行
值得注意的是在返回前加了je还有int 3指令,这才是减少gadgets的功臣
因为你要把这当做gadget,你必须跳过int 3,再往前就是必须满足cookie的比较,而cookie无法预测,那就没法用了啊
方法小结:
1、损耗:运行时间多了约2%,还有就是初始化cookie的时间是可变的(跟函数的数量有关)
2、代码方面:每个函数多了31个byte,而kernel大约大了7%
最终减少了50%的gadget,15-25%的唯一gadget
针对于Arm64
arm64是有固定的指令长度,所以没有不对齐的gadget,只有对齐的gadget
对于对齐gadget的减少同样也可以是上面int 3的思路,只不过arm64是brk #0x1
这样就几乎删除绝大多数gadget了
在6.3-release arm64 kernel中ROP gadgets的数量:69935
在6.4-release arm64 kernel中ROP gadgets的数量:46
而剩余的gadget是在引导代码中的汇编中,具体如下:
- create_pagetables
- link_l0_pagetable
- link_l1_pagetable
- build_l1_block_pagetable
- build_l2_block_pagetable
引导后OpenBSD可以unlink或粉碎引导代码,那么这些功能在系统运行时就不可用
那么在用户层,那么gadget也几乎为0了,可能存在于crt0,ld.so
最后看看效果图,先看统计的
看看常用的库还有sshd服务
最后看看ROPGadget查找效果,已经不能自动化构成利用链了
这是6.4的libc
6.5的更加惨不忍睹
结尾
还有更多东西可做
比如重定向地址,剩余的可用于gadget的汇编,JOP还没有动
参考
AsiaBSDCon 2019 —— Removing ROP Gadgets from OpenBSD
https://www.openbsd.org/papers/asiabsdcon2019-rop-slides.pdf