栈溢出 - ROP (一)
Last Update:
介绍
栈溢出是一种允许攻击者向栈中写入过长数据的漏洞。
ROP 是栈溢出的一种利于方式,攻击者对栈空间进行构造进而调用程序已有的指令片段进行攻击。
下面通过一个例题来进行讲解。
因为是入门第一篇文章,所以会讲的比较细。遇到不明白的继续往后看就清楚了。
环境搭建
1.Linux x64
2.IDA (Linux x64)
在官网下载后运行安装
1 |
|
调试分析
题目代码:
1 |
|
生成可执行文件:
1 |
|
检查保护机制
1 |
|
未开启 Canary 栈保护,说明不会对向栈写入的数据长度进行检查。
未开启 PIE 保护,说明加载到内存中的基址是固定的 (0x400000),可以对变量、函数等地址进行预知。
使用 IDA 进行静态调试
通过终端启动 IDA,不要双击快捷方式,否则无法与程序进行控制台交互。
1 |
|
shift + F7 进入 Segments 窗口。
进入可写的 .bss 段 (存放未初始化的全局变量),可以看到全局变量 buf 在内存中的绝对地址为 0x601060 (后面会用到)。
到这里你可能会有疑问,如果启动多个未开启 PIE 保护的程序它们的绝对地址不会冲突吗?
答案是系统会让它们运行在独立的虚拟空间。
使用 IDA 进行动态调试
在主函数 F12 设置一个软件断点,F9 运行。
在下方 .text 段内的 0x4005E2 处可以发现 pop r15 & retn (硬编码 41 5F C3)。
我们可以断章取义将 0x4005E3 处的 5F C3 作为以下汇编指令使用 (后面会用到)。
1 |
|
F8 单步到 call MyGets,下面开始分析该函数的漏洞。
进入 MyGets 前,可以发现 call MyGets 下一条指令的地址为 0x400579。
F7 步入 MyGets,再 F8 单步到 call gets
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。
F8 单步调用 gets,输入 9 个 1。再 F8 单步到函数结束。
可以看到我们输入的 111111111\0 从 0x00007FFFFFFFDDC6 填满了 v1。
恢复现场使栈区域回到了下面,这都要归功于栈底存储了之前栈底的地址。
retn 使栈顶下移回到了调用 call MyGets 前的原位,执行地址来到了 0x400579。
地址 | 栈 | 栈 | 栈 |
---|---|---|---|
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 |
|