(入门向)ROP原理初探

为便于讲述,以下内容除特殊说明外均以x86-64架构下的Linux系统为例。32位架构或Windows系统与下文所述略有差别。

ROP原理简介

函数调用过程

在C语言课上,我们学过:在进行函数调用时,实际发生了控制转移。那么被调用函数怎么知道自己的返回地址呢?调用函数(caller)是怎么保存自身信息的呢?

实际上,在cdecl调用约定下,调用函数时发生了如下几件事:

  1. 调用函数将前6个实参移至寄存器(rdi, rsi, rdx, rcx, r8, r9),其余参数压入栈
  2. 调用函数将被调用函数的返回地址(即call指令的下一个指令的地址)压入栈
  3. 指令指针移至被调用函数(即将被调用函数地址移至指令指针寄存器(rip))
  4. 被调用函数将调用函数的栈底地址(即rbp)压栈
  5. 被调用函数将栈顶指针地址移至栈底指针寄存器(rbp)
  6. 被调用函数按需开辟自己的栈空间
  7. 被调用函数将参数值存入栈空间内

返回时:

  1. 被调用函数将栈底指针地址移至栈顶指针寄存器(rsp)
  2. 被调用函数将栈中存储的原先栈底地址出栈至栈底指针寄存器(rbp)
  3. 指令指针移至栈中存储的返回地址对应位置
  4. 调用函数将实参所占据的栈空间清理

然而实际上,因为函数参数一般少于7个,所以压栈传参较少见。此处将展示foo函数调用bar(114,514)时的大致汇编代码:

C语言代码:

1
2
3
4
5
6
7
int bar(int a, int b){
int x=a,b=y;
return 0;
}
void foo(){
bar(114,514);
}

foo的部分汇编代码:

1
2
3
mov edi,114
mov esi,514
call bar

bar的部分汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
push rbp
mov rbp, rsp
sub rsp,0x20
mov dword ptr [rbp-0x14],edi
mov dword ptr [rbp-0x18],esi
mov eax, dword ptr [rbp-0x14]
mov dword ptr [rbp-0x4],eax
mov eax, dword ptr [rbp-0x18]
mov dword ptr [rbp-0x8],eax
mov eax,0x0
leave
ret

x86-64架构的栈帧布局

1
2
3
4
5
6
7
long myfunc(long a, long b, long c, long d, long e, long f, long g, long h)
{
long xx = a * b * c * d * e * f * g * h;
long yy = a + b + c + d + e + f + g + h;
long zz = utilfunc(xx, yy, xx % yy);
return zz + 20;
}

图片出处

特别地,”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的利用条件是:

  1. 程序栈上存在溢出,且可以控制返回地址
  2. 可以找到满足条件的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/