前面介绍了ret2libc和ret2plt,这两个攻击技术的相通点是函数参数是通过压栈来传递,这也是i386架构的调用约定。然而随着64位服务器的普及,以及后来越来越广泛,以致几乎所有服务器都升级到64位的硬件上。
根据X86_64 ABI的调用约定,函数间传递参数不再以压栈的方式,而是以寄存器方式传递参数,前面6个参数依次以rdi, rsi, rdx, rcx, r8和r9寄存来传递。
在Linux系统,64位架构只使用48位的虚拟地址空间,也即每个地址高16位全部为0,因此在64位系统上,地址已天然零化。
前面介绍的攻击方法似乎一夜之间没有用武之地,很快安全人员在ret2plt攻击方法基础上,做了一个升级片本的攻击方法,称为ROP(Return-oriented programming)方法。
顾名思义ROP,就是面向返回语句的编程方法,它借用libc代码段里面的多个retq前的一段指令拼凑成一段有效的逻辑,从而达到攻击的目标。
为什么是retq,因为retq指令返到哪里执行,由栈上的内容决定,而这是攻击者很容易控制的地址。
那参数如何控制,就是利用retq执行前的pop reg指令,将栈上的内容弹到指令的寄存器上,来达到预期
一段retq指令未必能完全到想攻击目标的前提条件,那可在栈上控制retq指令跳到另一段retq指令表,如果它还达不到目标,再跳到另一段retq,直到攻击目标实现。
在ret2plt攻击方法,我们使用PPR(pop, pop, ret)指令序列,实现顺序执行多个strcpy函数调用,其实这就是一种最简单的ROP用法。ROP更是ret2plt的升级版
ROP方法技巧性很强,那它能完全胜任所有攻击吗?返回语句前的指令是否会因为功能单一,而无法实施预期的攻击目标呢?业界大牛已经过充分研究并证明ROP方法是图灵完备的,换句话说, ROP可以借用libc的指令实现任何逻辑功能。
在这里省去对准EIP以及漏洞代码分析,直奔主题,如何构造ROP指令顺列来实现攻击逻辑。
简单起来,攻击目标为实现system(“echo success”) 这个函数调用。
首先,调用system的参数为”echo success”字符串的地址,而字符串是栈注入的内容,那它的地址应该是rsp + offset。而函数调用时,第一个参数是放到rdi寄存里面。 所以需要从libc里面,在retq或者call *reg指令前找到rdi = rsp + offset逻辑等价的指令序顺,发现有如下的两条指令:
0x7ffff7a610a3 lea 0x120(%rsp),%rdi
0x7ffff7a610ab call %rax
这样,可以将”echo success”字符串安排在rsp + 0x120的位置。但往下一条指令,要指令call %rax,因此需要在指令这个指令段前控制rax的值必须为system函数的地址。
然后,想将system地址放到rax相当容易,只需要在retq指令令,找到pop rax指令即可,从libc里面查找,发现如下:
0x7ffff7a3b076: pop %rax
0x7ffff7a3b077: pop %rbx
0x7ffff7a3b078: pop %rbp
0x7ffff7a3b0f9: retq
于是,构建如下的执行顺序:
pop %rax <— 这里弹出system函数地址
pop %rbx
pop %rbp
retq <— 这里从栈中跳到下段指令
lea 0x120(esp), %rdi <– 需要安排好”echo success”位置,使得此时的rsp + 0x120刚好是字符串地址
call *%rax <– 调用system,参数刚好。
通过gdb查看system函数的地址:
(gdb) p system
$4 = {<text variable, no debug info>} 0x7ffff7a61310 <system>
于是栈注入内容就很容易:
0x7fffff7a3b076 + address of system + dummy1 + dummy2 + 0x7ffff7a610a3 +dummy(0x120) + “echo success”
上面的攻击实例中,指令的执行过程和栈注入内存布局如下图所示。
上面提到已有研究员称ROP攻击借用的多个代码片段串起来的程序逻加是图灵完备的,也即这个程序包含顺序执行语句(这个当然是废话),还有分支语句,甚至有循环语句。
稍有反编译或者逆向工程经验,或者对C语言生成的汇编结构熟悉都知道,retq指令是函数的返回指令,在此之前的指令是弹栈指令(如pop rax, pop rbx等),怎么可以出现分支指仅(bne等),甚至循环指令呢?
是的,这个是事实,但不是事实的全部。如果将glibc进行逆向工程,会发现retq指令前向全是清一色的pop指令,但是事实上攻击者总是不按常规出牌。
X86指令集是CISR指令集,密集度很高,一条指令中的一部分,也可能是一条新指令。
攻击者就是利用这一点,不按常规出牌。retq指令就只有一个字节,是C3。通过编写工具,对glibc进行扫描,把C3的指令内容找到,然后向前解码各种可能的指令,形成一个指令表。这些指令表会是非常丰富,有运算指令,转跳指令,以及访存指令。利用它们可以形成图灵完备的计算逻辑。