RTOS系统的路由器固件分析

学习文章来源于[RTOS] 基于VxWorks的TP-Link路由器固件的通用解压与修复思路 – 赤道企鹅的博客 | Eqqie Blog

固件来源

传送门:TL-WDR7660千兆版 V1.0升级软件20190830_2.0.30

固件下载链接:https://service.tp-link.com.cn/download/20207/TL-WDR7660%E5%8D%83%E5%85%86%E7%89%88%20V1.0%E5%8D%87%E7%BA%A7%E8%BD%AF%E4%BB%B620190830_2.0.30.zip?forcedownload=TL-WDR7660%E5%8D%83%E5%85%86%E7%89%88%20V1.0%E5%8D%87%E7%BA%A7%E8%BD%AF%E4%BB%B620190830_2.0.30.zip

固件提取

binwalk看一下

有很多LZMA格式的数据,所谓LZMA,简单理解就是压缩后的数据块,像这种没有识别到文件系统,而是一堆LZMA格式的数据块,就是RTOS架构

同时我们可以看到,这个固件是ARM架构的固件,还有一个Entry Point:0x41C00000,这个Entry Point通常来说是uBoot程序的入口,而不是我们要分析的主程序的入口

提取uBoot

uBoot通常由uImage header和紧随其后的一块LZMA compressed data组成,先要将他们提取出来:

dd if=wdr7660gv1-cn-up_2019-08-30_10.37.02.bin of=uboot.raw bs=1 skip=512 count=66048

skip为起始位置,count为总大小(uImage header+LZMA compressed data = 66560 – 512)

提取主程序

0x10400偏移也就是uImage header之后的第二块LZMA compressed data的位置存放了1.3M左右特别大的数据,一般来说这也是主程序所在,将其用同样的方法提取出来…

lzma -d ./data_0x10400.lzma

提取出来解压时有一些问题

010打开固件程序,定位到0x15B440附近

一般来说,文件末尾后面会有一堆0xff,但是很显然这个地方是一些有用的数据,而前面刚好有一堆0xff,往上回溯一下,可以发现有一堆0xff,找到0xff的起始

所以猜测真正的结束地址是0x15a477,于是将count修改为0x15a477 – 66560大小后再次提取

dd if=wdr7660gv1-cn-up_2019-08-30_10.37.02.bin of=data_0x10400.lzma bs=1 skip=66560 count=1351799
lzma -d ./data_0x10400.lzma

来自AI的分析:

你正在对 TP-LINK TL-WDR7660 路由器固件的一个片段 data_0x10400 进行分析,使用了 binwalk -Me(递归提取嵌入的文件系统/数据)。从 binwalk 的输出可以看出以下关键信息:


🔍 关键发现摘要

1. 操作系统识别

  • VxWorks 5.5.1text编辑VxWorks operating system version "5.5.1", compiled: "Aug 30 2019, 10:21:15"
    • 这是 Wind River 公司开发的实时操作系统(RTOS),常见于嵌入式设备如路由器、工业控制器等。
    • 同时检测到 WIND kernel version “2.6”

2. 证书与密钥

  • 多个 X.509 DER 格式证书(共 11 处)
  • 一处 PEM 格式的 RSA 私钥:text编辑3688452 0x384804 PEM RSA private key⚠️ 安全风险提示:如果这是设备出厂固件中包含的私钥,可能意味着所有同型号设备共享同一密钥,存在严重的安全漏洞(例如中间人攻击、伪造固件等)。

3. 硬件与驱动线索

  • 路径 /etc/wireless/mediatek/MT7626_EEPROM.bin 表明使用 联发科(MediaTek)MT7626 芯片组。
  • 另一个路径 /etc/Wireless/RT2860/RT2860_2G.dat 指向 Ralink RT2860 驱动配置(Ralink 已被 MediaTek 收购,两者技术融合)。

4. 网络功能相关字符串

  • 大量 "NeighborReq""Neighbor report" 等文本,表明支持 IEEE 802.11k(无线邻居报告,用于漫游优化)。
  • 出现 IEEE80211_RRM_NUM_CHANRPT_MAX,进一步确认 Radio Resource Management (RRM) 功能启用。

5. 其他技术细节

  • 存在 AES S-Box / Inverse S-BoxSHA256 常量表CRC32 表 → 设备支持加密与校验。
  • HTML 片段(可能为 Web 管理界面的一部分)。
  • XML 文档(可能是配置文件模板)。
  • StuffIt 数据段(可能是压缩资源或误报,但较罕见于嵌入式 Linux/VxWorks)。

6. 版权信息

  • TP-LINK 版权:Copyright(C) 2001-2011 by TP-LINK TECHNOLOGIES CO., LTD.
  • Wind River 版权:Copyright 1984-2002 Wind River Systems, Inc.

确定程序入口

这一步直接搬来师傅的结论

入口地址存放的大致规律:

首先从主程序偏移往前找,在这个例子里面就是0x10400往前
在这个范围内搜索如下字符串:MyFirmware
从字符串的偏移往上一点会发现两个重复的地址,猜测这就是主程序的加载地址和入口

所以在这里是0x40205000

另一种方法是,首先确定程序架构是ARM32位little

至于到底是32位还是64位,试一下便知,64位反汇编不出正常指令

VxWorks系统的加载基址与栈初始化地址相同,根据VxWorks官方给出的说明,其采用usrInit进行栈初始化,而usrInit是VxWorks系统引导后运行的第一个函数,因此可以直接把VxWorks系统文件丢到ida中,先用0作为加载基址,然后找sp寄存器首次出现的位置,就是VxWorks系统的加载基址。

发现”LDR R0, =0x40205000”,后面接着”MOV SP, R0”,可以确定加载基址为0x40205000。

一开始没有直接显示0x40205000是因为我把这个地址按了下“c”,解析成代码了

来自AI:

1. LDR 指令(Load Register)

功能

从内存中加载一个 32 位字(word)到寄存器。

基本语法

armasm编辑LDR Rt, [Rn, #offset] ; 从地址 (Rn + offset) 处读取 4 字节到 Rt

常见形式

形式示例说明
直接寻址LDR R0, [R1]从 R1 指向的地址读取数据到 R0
带偏移LDR R0, [R1, #4]从 R1+4 地址读取
前索引(写回)LDR R0, [R1, #4]!先加偏移,再读取,并将 R1 更新为 R1+4
后索引LDR R0, [R1], #4先读取 R1 地址的数据,再将 R1 += 4
PC 相对寻址LDR R0, [PC, #8]从当前指令 + 8 字节处读取(常用于文字池)
伪指令(加载立即数)LDR R0, =0x40205000汇编器自动处理,把常量放入文字池并生成 LDR

注意:LDR 加载的是 32 位字(little-endian 默认),地址必须 4 字节对齐(否则可能触发异常或未定义行为)。

示例

armasm编辑LDR R0, =0x12345678 ; 伪指令:R0 ← 0x12345678 LDR R1, [R2] ; R1 ← *(uint32_t*)R2 LDR R3, [R4, #8] ; R3 ← *(uint32_t*)(R4 + 8)

2. BIC 指令(Bit Clear)

功能

对寄存器进行 按位与非(AND NOT) 操作,用于清除某些特定位

基本语法

armasm编辑BIC Rd, Rn, Operand2

等价于:

c编辑Rd = Rn & (~Operand2);

操作数(Operand2)

可以是:

  • 立即数(需符合 ARM 立即数规则,如 #0xFF#3
  • 寄存器(如 R1
  • 带移位的寄存器(如 R1, LSL #2

经典用途:地址对齐

armasm编辑BIC R0, R0, #3

  • #3 的二进制是 0b11
  • ~3 是 ...11111100
  • 所以 R0 & ~3 会清零最低两位,使地址 4 字节对齐

符号表恢复

TP-LINK WDR7660的符号表和VxWorks系统文件是分离的,所以需要从binwalk解压升级文件后得到的文件中寻找符号文件。Bzero是VxWorks中一个函数,系统启动过程中会使用bzero函数对bss区的数据进行清零,因此可以利用”

”查找bzero函数,找到一个文件,很明显就是符号表所在文件了。

找到符号文件后,可以看出符号表的位置是从0x08到0x1a728,符号字符串的起始位置是0x1a728。

经过分析,得出符号文件中符号的存储规则如下:每8字节为一组,以54 00 00 00 40 37 36 84为例,54表示符号的类型(54表示函数名),00 00 00 表示符号在字符串表中的偏移,40 37 36 84表示符号对象在内存中的绝对地址。

常见的 flag 值(来自 ELF 或 VxWorks 风格):

  • 0x54 ('T'):Text(代码,函数)
  • 0x44 ('D'):Data(已初始化全局变量)
  • 0x42 ('B'):BSS(未初始化全局变量)
  • 0x64 ('d'):调试符号

随便找一个函数验证一下

直觉告诉我们这个函数是printf函数

经过搜索可以得知他的字符串偏移为0x2B12C

计算0x1a728+0x2B12C=0x45854,在对应位置果然发现了printf

利用如下脚本可以恢复符号表

import idaapi
import idc
import ida_bytes
import ida_funcs
import struct

# ==================== 配置区 ====================
SYM_FILE_PATH = './sym_table'
SYMBOLS_TABLE_START = 8
STRINGS_TABLE_START = 0x1a728

# 架构字节序:
#   '>' → Big-Endian (MIPS, PowerPC)
#   '<' → Little-Endian (ARM, x86)
ENDIAN = '>'  # MT7621 是 MIPS 大端
# ==============================================

def get_string_from_table(strings_data, offset):
    """从字符串表中安全读取以 \\x00 结尾的字符串"""
    if offset >= len(strings_data):
        return "<invalid_offset>"
    end = strings_data.find(b'\x00', offset)
    raw = strings_data[offset:end] if end != -1 else strings_data[offset:]
    try:
        return raw.decode('utf-8', errors='replace')
    except Exception:
        return repr(raw)

def parse_symbol_entry(entry, strings_data):
    """
    解析 8 字节符号项:
        [1B flag][3B str_off][4B addr]
    """
    flag = entry[0]

    # 手动解析 3 字节 string offset(大端或小端)
    if ENDIAN == '>':
        str_off = (entry[1] << 16) | (entry[2] << 8) | entry[3]
    else:
        str_off = entry[1] | (entry[2] << 8) | (entry[3] << 16)

    # 解析 4 字节地址
    addr = struct.unpack(ENDIAN + 'I', entry[4:8])[0]

    name = get_string_from_table(strings_data, str_off)
    return flag, name, addr

def make_function_at(ea):
    """在地址 ea 强制创建函数"""
    if ida_funcs.get_func(ea):
        return True  # 已存在

    # 清除可能的数据定义
    ida_bytes.del_items(ea, ida_bytes.DELIT_SIMPLE)

    # 创建第一条指令
    if not idc.create_insn(ea):
        print(f"[-] create_insn failed at {ea:#x}")
        return False

    # 创建函数
    if not ida_funcs.add_func(ea):
        print(f"[-] add_func failed at {ea:#x}")
        return False

    return True

def main():
    try:
        with open(SYM_FILE_PATH, 'rb') as f:
            data = f.read()
    except Exception as e:
        print(f"[!] Failed to open symbol file: {e}")
        return

    if SYMBOLS_TABLE_START >= len(data) or STRINGS_TABLE_START >= len(data):
        print("[!] Invalid symbol table offsets!")
        return

    symbols_raw = data[SYMBOLS_TABLE_START:STRINGS_TABLE_START]
    strings_raw = data[STRINGS_TABLE_START:]

    symbols = []
    entry_size = 8
    for i in range(0, len(symbols_raw), entry_size):
        if i + entry_size > len(symbols_raw):
            break
        entry = symbols_raw[i:i+entry_size]
        try:
            flag, name, addr = parse_symbol_entry(entry, strings_raw)
            symbols.append((flag, name, addr))
        except Exception as e:
            print(f"[!] Parse error at offset {i}: {e}")
            continue

    print(f"[+] Parsed {len(symbols)} symbol entries.")

    applied_names = 0
    created_funcs = 0

    for flag, name, addr in symbols:
        if addr == 0 or not idaapi.is_loaded(addr):
            continue

        # 设置名称(即使不是函数也设名)
        if idaapi.set_name(addr, name, idaapi.SN_NOWARN | idaapi.SN_NOCHECK):
            applied_names += 1

        # 如果是代码符号(0x54 = 'T')
        if flag == 0x54:
            if make_function_at(addr):
                created_funcs += 1

    print(f"[+] Applied {applied_names} names, created {created_funcs} functions.")
    print("[+] Symbol loading completed.")

# ==================== 入口 ====================
if __name__ == "__main__":
    main()

适配于ida9.0,我还将其做成了插件,但是还在测试适配性和通用性,等测试没问题了在发出来吧~

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇