在上一篇文章中我们介绍了 return-oriented 这种攻击手段,它的强大之处在于攻击者不需要插入恶意代码,通过构造特殊的函数返回栈利用程序中原有的代码即可达到攻击者的目地。

北卡州立大学的学者们提出了一种防止 return-oriented 攻击的思路,思路很简单,一句话概括,就是去掉代码里所有的 ret 指令!

思路很简单,真正做起来还是很复杂的。x86 中的 ret 指令只有一个字节,即 0xc3。要去掉所有的 0xc3,不仅要修改原来代码中的 ret 指令,还要移除其他指令片段中的 0xc3(例如 movl $0xc3, %rax)。接下来我们来看看 EuroSys 10 上的这篇文章是怎么解决这些问题的。

首先是原来就作为 ret 指令用的 0xc3 代码。注意 return-oriented 之所以成功一大原因就是 ret 指令在返回时不会检查栈上的返回地址是否正确。要保证这一点,需要引入一个间接跳转层。传统的调用过程是调用者把返回地址压入栈上,然后被调用函数返回时从栈上得到返回地址并返回。现在我们加入一个新的跳转表,这张表里记录了所有的返回地址,而且它不在栈上,因此不能被攻击者修改。当调用者调用函数时,把返回地址在表中的序号压入栈上;函数返回时,从栈上读出地址序号,再查表得到实际地址,然后返回。通过引入这样一层额外的地址转换机制,攻击者就不能通过修改栈上返回地址让函数返回到任意地址了。

接下来我们要解决其他指令引入的 0xc3,这里面也分几类情况。首先是由于寄存器分配引起的。例如 movl %rax, %rbx 对应的机器码是 48 89 c3,这边就有个 0xc3。对于这一类代码,只需要在编译器做寄存器分配时把有可能产生 0xc3 的情况排除掉即可。

另一类是代码中直接使用了 0xc3 作为直接数。这种情况需要对代码进行适当的修补,以 cmp $0xc3, %ecx 为例,0xc3 这个直接数可以通过 0xc4 - 1 得到,于是这条指令可以被修改为:

mov $0xc4, %reg
dec %reg
cmp %reg, %ecx

到这里所有包含 0xc3 的代码都已经被修改成具有同等功能的不包含 0xc3 的版本了,也就彻底杜绝了 ret 指令被用来做 return-oriented 攻击的可能。对具体实现细节有兴趣的同学可以读一下这篇论文,作者借助 LLVM 生成了一个没有 0xc3 的 FreeBSD 内核。

如果一个程序没有 0xc3,是不是意味着 return-oriented 攻击也从根本上被阻止了呢?