(入门向)ROP原理初探
为便于讲述,以下内容除特殊说明外均以x86-64架构下的Linux系统为例。32位架构或Windows系统与下文所述略有差别。
ROP原理简介
函数调用过程
在C语言课上,我们学过:在进行函数调用时,实际发生了控制转移。那么被调用函数怎么知道自己的返回地址呢?调用函数(caller)是怎么保存自身信息的呢?
实际上,在cdecl调用约定下,调用函数时发生了如下几件事:
- 调用函数将前6个实参移至寄存器(rdi, rsi, rdx, rcx, r8, r9),其余参数压入栈
- 调用函数将被调用函数的返回地址(即call指令的下一个指令的地址)压入栈
- 指令指针移至被调用函数(即将被调用函数地址移至指令指针寄存器(rip))
- 被调用函数将调用函数的栈底地址(即rbp)压栈
- 被调用函数将栈顶指针地址移至栈底指针寄存器(rbp)
- 被调用函数按需开辟自己的栈空间
- 被调用函数将参数值存入栈空间内
返回时:
- 被调用函数将栈底指针地址移至栈顶指针寄存器(rsp)
- 被调用函数将栈中存储的原先栈底地址出栈至栈底指针寄存器(rbp)
- 指令指针移至栈中存储的返回地址对应位置
- 调用函数将实参所占据的栈空间清理
然而实际上,因为函数参数一般少于7个,所以压栈传参较少见。此处将展示foo函数调用bar(114,514)时的大致汇编代码:
C语言代码:
1 | int bar(int a, int b){ |
foo的部分汇编代码:
1 | mov edi,114 |
bar的部分汇编代码:
1 | push rbp |
x86-64架构的栈帧布局
1 | long myfunc(long a, long b, long c, long d, long e, long f, long g, long h) |
特别地,”red zone”是x86_64 linux系统下特有的部分,编译器保证这128字节不会被外界(signals和interrupt handlers)修改,因此当函数内部不再调用其它函数时(因为被调用函数会覆盖调用函数的”red zone”),编译器可能不移动rsp,直接使用”red zone”存储。
ROP
由图可见,只要控制了栈上的return address
,就可以控制函数返回的位置,从而劫持控制流,使程序执行我们想要的代码。那么如何控制栈上的return address
呢?一种朴素的方法是,当栈上存在溢出时(比如在栈上定义了数组,然而发生了数组越界),就可以向栈底方向覆盖我们想输入的内容。我们可以找到类似pop rdi; ret
的多个gadget,一次性覆盖后几次的返回地址,构造ROP链,从而使程序顺序执行链上的指令,完成利用。
因此显然,ROP的利用条件是:
- 程序栈上存在溢出,且可以控制返回地址
- 可以找到满足条件的gadgets及其地址
缓解措施及绕过方式
为了防止程序员错误写出栈溢出的代码造成严重的风险,编译器提供了几种缓解措施。下文将介绍其中两种——Stack Canary和PIE,以及常见的绕过方式。
Stack Canary
原理
Canary 的意思是金丝雀,来源于英国矿井工人用来探查井下气体是否有毒的金丝雀笼子。工人们每次下井都会带上一只金丝雀。如果井下的气体有毒,金丝雀由于对毒性敏感就会停止鸣叫甚至死亡,从而使工人们得到预警。
在程序中,Canary是一串随程序启动便确定的随机8字节,一般存储在函数栈帧的[rbp-8]位置,在函数返回前会检查这个值有没有被改变,如果改变了说明存在栈溢出,程序将终止。
绕过
Leak
我们可以想办法泄露出Canary的值,并在覆盖时将对应位置填充Canary即可。因为C语言中,字符串为以\0
结束的字符数组。如果临时变量区一个字符数组存在溢出,我们可以将它覆盖至Canary前。此时输出字符串,Canary内容也会被当做字符串的一部分被输出,从而使攻击者得到Canary值进行下一次攻击。另外,也可以利用格式化字符串漏洞泄露。
PIE
原理
PIE即Position-Independent Executable,位置无关可执行文件。PIE可以随机化程序的代码段和数据段地址,让返回地址难以猜测,大幅增加攻击难度。
绕过
Leak
与Canary类似,可以利用相似的手法泄露PIE基址(比如泄露函数的原返回地址),从而实现利用。
Partial Overwrite
地址随机化是以0x1000大小为单位的,即程序的低12位地址不会改变。我们可以在溢出时只覆盖返回地址的后两个字节,从而有1/16的概率实现利用。
Step Further
在学习完本文内容后,可以尝试了解ret2libc、got表劫持、ret2csu、栈迁移、SROP等攻击技术,也可以学习格式化字符串漏洞。这些都是pwn入门的必备知识。
后记
本文是pwn入门系列的第一篇文章(至于是不是最后一篇就不好说了)。本文诞生的契机是作为学校专业知识探索活动的分享内容,但在写作中,我也感受到了“温故而知新”的过程,对函数栈帧排布有了更细节的理解。pwn的学习过程漫长,且常常艰难。“路漫漫其修远兮,吾将上下而求索”,愿你我共勉。
参考资料
https://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64
https://ctf-wiki.org/pwn/linux/user-mode/mitigation/canary/
https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/basic-rop/