TP-Link Archer MR200(EU)v7 固件解密

2026-09-21 16:48:57

1、判断固件是否被加密

emba扫描

EMBA full-scan 自带熵分析(P02 模块)

P02_firmware_bin_file_check.sh

给出有关所提供的固件二进制文件的一些非常基本的信息。
主函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
P02_firmware_bin_file_check() {
module_log_init "${FUNCNAME[0]}"
module_title "Binary firmware file analyzer"

set_p02_default_exports

local FILE_BIN_OUT
# we set this var global to 1 if we find something UEFI related
export UEFI_DETECTED=0

write_csv_log "Entity""data""Notes"
write_csv_log "Firmware path""$FIRMWARE_PATH""NA"
if[[ -f "$FIRMWARE_PATH"]]; then

# 计算固件文件哈希
SHA512_CHECKSUM="$(sha512sum "$FIRMWARE_PATH" | awk '{print $1}')"
write_csv_log "SHA512""${SHA512_CHECKSUM:-}""NA"
SHA1_CHECKSUM="$(sha1sum "$FIRMWARE_PATH" | awk '{print $1}')"
write_csv_log "SHA1""${SHA1_CHECKSUM:-}""NA"
MD5_CHECKSUM="$(md5sum "$FIRMWARE_PATH" | awk '{print $1}')"
write_csv_log "MD5""${MD5_CHECKSUM:-}""NA"

# 使用 ent 计算文件的熵信息
ENTROPY="$(ent "$FIRMWARE_PATH" | grep Entropy | sed -e 's/^Entropy\ \=\ //')"
write_csv_log "Entropy""${ENTROPY:-}""NA"

print_output "[*] Entropy testing with binwalk ... "
# we have to change the working directory for binwalk, because everything except the log directory is read-only in
# Docker container and binwalk fails to save the entropy picture there
if[[ $IN_DOCKER -eq 1]] ; then
cd "$LOG_DIR"|| return

# 使用 binwalk 提取固件,并输出 json 的结果
print_output "$(binwalk -E -F -J "$FIRMWARE_PATH")"
mv "$(basename "$FIRMWARE_PATH".png)""$LOG_DIR"/firmware_entropy.png 2> /dev/null || true
cd /emba || return
else
print_output "$(binwalk -E -F -J "$FIRMWARE_PATH")"
mv "$(basename "$FIRMWARE_PATH".png)""$LOG_DIR"/firmware_entropy.png 2> /dev/null || true
fi
fi

local FILE_LS_OUT
FILE_LS_OUT=$(ls -lh "$FIRMWARE_PATH")

print_ln
print_output "[*] Details of the firmware file:"
print_ln
print_output "$(indent "$FILE_LS_OUT")"
print_ln
if[[ -f "$FIRMWARE_PATH"]]; then
print_ln

# 使用 file 命令提取基本信息
print_output "$(indent "$(file"$FIRMWARE_PATH")")"
print_ln

# 使用 hexdump 输出文件前面的十六进制和ASCII信息
hexdump -C "$FIRMWARE_PATH"| head | tee -a "$LOG_FILE"|| true
print_ln
print_output "[*] SHA512 checksum: $ORANGE$SHA512_CHECKSUM$NC"
print_ln
print_output "$(indent "$ENTROPY")"
print_ln
if[[ -x "$EXT_DIR"/pixde ]]; then

# 可视化二进制文件
print_output "[*] Visualized firmware file (first 2000 bytes):\n"
"$EXT_DIR"/pixde -r-0x2000"$FIRMWARE_PATH"| tee -a "$LOG_DIR"/p02_pixd.txt
print_ln

python3 "$EXT_DIR"/pixd_png.py -i "$LOG_DIR"/p02_pixd.txt -o "$LOG_DIR"/pixd.png -p 10> /dev/null
write_link "$LOG_DIR"/pixd.png
fi

# 对二进制文件进行检测(将在下方进行分析)
fw_bin_detector "$FIRMWARE_PATH"

# 备份所有的变量
backup_p02_vars
fi

module_end_log "${FUNCNAME[0]}"1
}

fw_bin_detector函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
fw_bin_detector() {

# 固件路径
local CHECK_FILE="${1:-}"
local FILE_BIN_OUT=""
local DLINK_ENC_CHECK=""
local QNAP_ENC_CHECK=""
local AVM_CHECK=0
local UEFI_CHECK=0

set_p02_default_exports

FILE_BIN_OUT=$(file"$CHECK_FILE")

# 输出第一行用于检查是否为DLINK公司产品
DLINK_ENC_CHECK=$(hexdump -C "$CHECK_FILE"| head -1|| true)

# 从固件中所有的字符串中检查AVM正则出现的行数。用于判断固件是否为AVM公司产品。
AVM_CHECK=$(strings "$CHECK_FILE"| grep -c "AVM GmbH .*. All rights reserved.\|(C) Copyright .* AVM"|| true)

# 搜索字符串并只显示包含指定字符串的结果,用于判断是否为QNAP加密文件系统
QNAP_ENC_CHECK=$(binwalk -y "qnap encrypted""$CHECK_FILE")
# we are running binwalk on the file to analyze the output afterwards:
binwalk "$CHECK_FILE"> "$TMP_DIR"/s02_binwalk_output.txt
UEFI_CHECK=$(grep -c "UEFI""$TMP_DIR"/s02_binwalk_output.txt || true)
UEFI_CHECK=$(( "$UEFI_CHECK"+"$(grep -c "UEFI" "$CHECK_FILE" || true)"))

if[[ -f "$KERNEL_CONFIG"]] && [[ "$KERNEL"-eq 1]]; then
# we set the FIRMWARE_PATH to the kernel config path if we have only -k parameter
if[[ "$(md5sum "$KERNEL_CONFIG" | awk '{print $1}')"=="$(md5sum "$FIRMWARE_PATH" | awk '{print $1}')"]]; then
print_output "[+] Identified Linux kernel configuration file"
write_csv_log "kernel config""yes""NA"
export SKIP_PRE_CHECKERS=1
return
fi
fi

if[[ "$UEFI_CHECK"-gt 0]]; then
print_output "[+] Identified possible UEFI firmware - using fwhunt-scan vulnerability scanning module"
export UEFI_DETECTED=1
UEFI_AMI_CAPSULE=$(grep -c "AMI.*EFI.*capsule""$TMP_DIR"/s02_binwalk_output.txt || true)
if[[ "$UEFI_AMI_CAPSULE"-gt 0]]; then
print_output "[+] Identified possible UEFI-AMI capsule firmware - using capsule extractors"
fi
write_csv_log "UEFI firmware detected""yes""NA"
fi
if[[ "$AVM_CHECK"-gt 0]] || [[ "$FW_VENDOR"==*"AVM"*]]; then
print_output "[+] Identified AVM firmware - using AVM extraction module"
export AVM_DETECTED=1
write_csv_log "AVM firmware detected""yes""NA"
fi
# if we have a zip, tgz, tar archive we are going to use the patools extractor
if[[ "$FILE_BIN_OUT"==*"gzip compressed data"*|| "$FILE_BIN_OUT"==*"Zip archive data"*|| \
"$FILE_BIN_OUT"==*"POSIX tar archive"*|| "$FILE_BIN_OUT"==*"ISO 9660 CD-ROM filesystem data"*|| \
"$FILE_BIN_OUT"==*"7-zip archive data"*|| "$FILE_BIN_OUT"==*"XZ compressed data"*|| \
"$FILE_BIN_OUT"==*"bzip2 compressed data"*]]; then
# as the AVM images are also zip files we need to bypass it here:
if[[ "$AVM_DETECTED"-ne 1]]; then
print_output "[+] Identified gzip/zip/tar/iso/xz/bzip2 archive file - using patools extraction module"
export PATOOLS_INIT=1
write_csv_log "basic compressed (patool)""yes""NA"
fi
fi
if[[ "$FILE_BIN_OUT"==*"QEMU QCOW2 Image"*]]; then
print_output "[+] Identified Qemu QCOW image - using QCOW extraction module"
export QCOW_DETECTED=1
write_csv_log "Qemu QCOW firmware detected""yes""NA"
fi
if[[ "$FILE_BIN_OUT"==*"VMware4 disk image"*]]; then
print_output "[+] Identified VMWware VMDK archive file - using VMDK extraction module"
export VMDK_DETECTED=1
write_csv_log "VMDK""yes""NA"
fi
if[[ "$FILE_BIN_OUT"==*"UBI image"*]]; then
print_output "[+] Identified UBI filesystem image - using UBI extraction module"
export UBI_IMAGE=1
write_csv_log "UBI filesystem""yes""NA"
fi

# 识别DLINK固件
if[[ "$DLINK_ENC_CHECK"==*"SHRS"*]]; then
print_output "[+] Identified D-Link SHRS encrpyted firmware - using D-Link extraction module"
export DLINK_ENC_DETECTED=1
write_csv_log "D-Link SHRS""yes""NA"
fi
if[[ "$DLINK_ENC_CHECK"=~ 00000000\ \ 00\ 00\ 00\ 00\ 00\ 00\ 0.\ ..\ \ 00\ 00\ 0.\ ..\ 31\ 32\ 33\ 00]]; then
print_output "[+] Identified EnGenius encrpyted firmware - using EnGenius extraction module"
export ENGENIUS_ENC_DETECTED=1
write_csv_log "EnGenius encrypted""yes""NA"
fi
if[[ "$DLINK_ENC_CHECK"=~ 00000000\ \ 00\ 00\ 00\ 00\ 00\ 00\ 01\ 01\ \ 00\ 00\ 0.\ ..\ 33\ 2e\ 3[89]\ 2e]]; then
print_output "[+] Identified EnGenius encrpyted firmware - using EnGenius extraction module"
export ENGENIUS_ENC_DETECTED=1
write_csv_log "EnGenius encrypted""yes""NA"
fi
if[[ "$DLINK_ENC_CHECK"==*"encrpted_img"*]]; then
print_output "[+] Identified D-Link encrpted_img encrpyted firmware - using D-Link extraction module"
export DLINK_ENC_DETECTED=2
write_csv_log "D-Link encrpted_img encrypted""yes""NA"
fi

# 识别u-boot固件
if[[ "$FILE_BIN_OUT"==*"u-boot legacy uImage"*]]; then
print_output "[+] Identified u-boot firmware - using u-boot module"
export UBOOT_IMAGE=1
write_csv_log "Uboot image""yes""NA"
fi
if[[ "$FILE_BIN_OUT"==*"Unix Fast File system [v2]"*]]; then
print_output "[+] Identified UFS filesytem - using UFS filesytem extraction module"
export BSD_UFS=1
write_csv_log "BSD UFS filesystem""yes""NA"
fi
if[[ "$FILE_BIN_OUT"==*"Linux rev 1.0 ext2 filesystem data"*]]; then
print_output "[+] Identified Linux ext2 filesytem - using EXT filesytem extraction module"
export EXT_IMAGE=1
write_csv_log "EXT2 filesystem""yes""NA"
fi
if[[ "$FILE_BIN_OUT"==*"Linux rev 1.0 ext3 filesystem data"*]]; then
print_output "[+] Identified Linux ext3 filesytem - using EXT filesytem extraction module"
export EXT_IMAGE=1
write_csv_log "EXT3 filesystem""yes""NA"
fi
if[[ "$FILE_BIN_OUT"==*"Linux rev 1.0 ext4 filesystem data"*]]; then
print_output "[+] Identified Linux ext4 filesytem - using EXT filesytem extraction module"
export EXT_IMAGE=1
write_csv_log "EXT4 filesystem""yes""NA"
fi
if[[ "$QNAP_ENC_CHECK"==*"QNAP encrypted firmware footer , model"*]]; then
print_output "[+] Identified QNAP encrpyted firmware - using QNAP extraction module"
export QNAP_ENC_DETECTED=1
write_csv_log "QNAP encrypted filesystem""yes""NA"
fi
# probably we need to take a deeper look to identify the gpg compressed firmware files better.
# Currently this detection mechanism works quite good on the known firmware images
if[[ "$DLINK_ENC_CHECK"=~ 00000000\ \ a3\ 01\ ]]; then
GPG_CHECK="$(gpg --list-packets "$FIRMWARE_PATH" | grep "compressed packet:")"
if[[ "$GPG_CHECK"==*"compressed packet: algo="*]]; then
print_output "[+] Identified GPG compressed firmware - using GPG extraction module"
export GPG_COMPRESS=1
write_csv_log "GPG compressed firmware""yes""NA"
fi
fi
if[[ "$DLINK_ENC_CHECK"==*"CrAU"*]]; then
print_output "[+] Identified Android OTA payload.bin update file - using Android extraction module"
export ANDROID_OTA=1
write_csv_log "Android OTA update""yes""NA"
fi
if[[ "$FILE_BIN_OUT"==*"openssl enc'd data with salted password"*]]; then
print_output "[+] Identified OpenSSL encrypted file - trying OpenSSL module for Foscam firmware"
export OPENSSL_ENC_DETECTED=1
write_csv_log "OpenSSL encrypted""yes""NA"
fi
# This check is currently only tested on one firmware - further tests needed:
if[[ "$DLINK_ENC_CHECK"=~ 00000000\ \ 62\ 67\ 6e\ 00\ 00\ 00\ 00\ 00\ \ 00\ 00\ 00\ b9\ 01\ ]]; then
print_output "[+] Identified Buffalo encrpyted firmware - using Buffalo extraction module"
export BUFFALO_ENC_DETECTED=1
write_csv_log "Buffalo encrypted""yes""NA"
fi
if[[ "$(basename "$CHECK_FILE")"=~ .*\.ri ]] && [[ "$FILE_BIN_OUT"==*"data"*]]; then
# ri files are usually used by zyxel
if[[ $(find "$LOG_DIR"/firmware -name "$(basename -s .ri "$CHECK_FILE")".bin| wc -l) -gt 0]]; then
# if we find a bin file with the same name then it is a Zyxel firmware image
print_output "[+] Identified ZyXel encrpyted ZIP firmware - using ZyXel extraction module"
export ZYXEL_ZIP=1
write_csv_log "ZyXel encrypted ZIP""yes"""
fi
fi
print_ln
}

binwalk

1
binwalk -E --log e.json 固件.bin

生成一个熵值图和一份 e.json 文件可以直接看到固件的熵值具体情况
image.png
image.png
我们可以看到"entropy"就是熵值的具体数值
0–1908 "entropy"= 6.948 混合区:0x200 明文 tag(0x0–0x200)+ RSA 签名 + 0xFF pad(0x200–0x330)+ 密文开头
1908 之后 "entropy"= 7.89–7.91 纯密文。
我们得出结论 固件整包加密

2、设备端升级链逆向 分析固件是如何被解密的

从 TP-Link GPL 下载 MR200V7_gpl_src.tar.gz ,解包后定位

1
2
tar tzf MR200V7_gpl_src.tar.gz | grep -E 'libcmm|libgdpr|libtpcrypto'
readelf -sW libcmm.so | grep -E 'writeFlash|updateFirmware|verifyFirmware'

image.png
image.png

使用ida 打开 libcmm.so 进行分析 定位到函数 writeFlash,用于将固件写入到flash芯片中
image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int writeFlash(uint addr, uchar *buf, uint len) {
if (access("/var/tmp/testMode", 0) == 0) { // 不碰 flash
cdbg_printf(..., "testMode, no flash opt, return");
return 0;
}
if (len > 0x10000000) { ... "Too many bytes" ... return -1; }
fd = open("/dev/flash0", ...); // flash 伪设备
if (fd < 0) { ... "Open flash pseudo device failed" ...; return -1; }
struct { void *buf; uint len; uint addr; } io = { buf, len, addr };
ret = ioctl(fd, 2, &io); // cmd=2 = FLASH_API 写命令
if (ret < 0) { ... "FLASH_API: ioctl error" ...; }
close(fd);
...
}

我们可以发现 writeFlash 中未有解密操作步骤

我们在ida中对函数 writeFlashx查看交叉引用情况
1788885963442923247.png
image.png
oal_sys_writeAppFlash 里的 util_findSystemProc("smbd"/"vsftpd")写 flash 前确认文件服务不在跑,然后 writeFlash(0x20000, buf, len)——0x20000 写死,印证固件在写入时候就已经进行了解密操作了

分析解密只能是 httpd,升级请求,上传固件是由 httpd 接收,但是httpd是被加密了我们无法得知加密的具体逻辑和解密密钥

3、解密固件

现在有两种方法:
1、手中有真实设备,直接读取真实设备的flash芯片,获取到 httpd 来分析加密逻辑
2、找到该设备最后未加密版本的固件,因为libcmm.so中并没有解密逻辑,那只能是在其中一个版本更新中在 httpd 中加入了解密逻辑,后续更新的固件都是加密的

我们选择第二种解决方案
历史版本固件下载

使用上述 binwalk 方法对各个版本固件进行分析

Archer_MR200(EU)v7_1.2.0_0.9.1
image.png

Archer_MR200(EU)v7_1.1.0_0.9.1
image.png

我们可以看到 Archer_MR200(EU)v7_1.1.0_0.9.1 版本时熵值是5.4左右,基本是压缩数据,而不是加密数据
提取httpd文件

1
2
3
4
5
6
7
sudo pacman -S squashfs-tools

python3 -c 'd=open("Archer_MR200(EU)v7_1.1.0_0.9.1_[250907-rel72442]_up_boot_All_Release_2025-09-08_09.12.59.bin","rb").read(); open("rootfs.sqfs","wb").write(d[0x160200:0x160200+0x864000])'

unsquashfs -d rootfs rootfs.sqfs

ls rootfs/usr/bin/httpd

image.png
使用ida打开httpd,shift+f12 从字符串中查找
image.png

1
BgIAAACkAABSU0ExAAgAAAEAAQA1HoEmJgj8kEpP2t94/Uhr77Gq4YngLjb6yYKBQMIqnv+15hYCIp+4fulmQ99ek/KZ8kCsSgRFrR2Ona9ICjeB+xuOKkMIXjkl6s1NhvAYVeszWfgVmxxxYG2r6k07KkPgv1GbWD2hC/xZ+ZbeB9XetEJuFfdDAqydGumX70oPQJPinFenq2CDZlQDMVHpQP/yICSh48tGlbqApg96yNXDmlh/XPBssAIzyPdgoxEAVwgEDulcxl7sWhNQ3r+7TGu0+0LRh4DfgBYKoKeKQcq557FPcufauj1y9i1H5XybPM9iYJxVAW+nnMnmkGA7HPYYKnO+Ni+4E7gShMl4OFnE

http_parser_illMultiObjForUpdate 函数是固件上传的处理函数
image.png
sub_408570 PSS验签 + AES密钥导出
image.png

image.png
固件解密操作

image.png
升级链

image.png
取出公钥 b64 字符串

image.png
base64 解码
image.png
解码表

1
2
3
4
5
6
7
8
ff,ff,ff,ff,ff,ff,ff,ff,ff,ff,ff,ff,ff,ff,ff,ff,
ff,ff,ff,ff,ff,ff,ff,ff,ff,ff,ff,ff,ff,ff,ff,ff,
ff,ff,ff,ff,ff,ff,ff,ff,ff,ff,ff,3e,ff,ff,ff,3f,
34,35,36,37,38,39,3a,3b,3c,3d,ff,ff,ff,ff,ff,ff,
ff,00,01,02,03,04,05,06,07,08,09,0a,0b,0c,0d,0e,
0f,10,11,12,13,14,15,16,17,18,19,ff,ff,ff,ff,ff,
ff,1a,1b,1c,1d,1e,1f,20,21,22,23,24,25,26,27,28,
29,2a,2b,2c,2d,2e,2f,30,31,32,33,ff,ff,ff,ff,ff

其实这个就是正常的base64表 我们将上面的字符串进行转码

image.png

1
06 02 00 00 00 a4 00 00 52 53 41 31 00 08 00 00 01 00 01 00 35 1e 81 26 26 08 fc 90 4a 4f da df 78 fd 48 6b ef b1 aa e1 89 e0 2e 36 fa c9 82 81 40 c2 2a 9e ff b5 e6 16 02 22 9f b8 7e e9 66 43 df 5e 93 f2 99 f2 40 ac 4a 04 45 ad 1d 8e 9d af 48 0a 37 81 fb 1b 8e 2a 43 08 5e 39 25 ea cd 4d 86 f0 18 55 eb 33 59 f8 15 9b 1c 71 60 6d ab ea 4d 3b 2a 43 e0 bf 51 9b 58 3d a1 0b fc 59 f9 96 de 07 d5 de b4 42 6e 15 f7 43 02 ac 9d 1a e9 97 ef 4a 0f 40 93 e2 9c 57 a7 ab 60 83 66 54 03 31 51 e9 40 ff f2 20 24 a1 e3 cb 46 95 ba 80 a6 0f 7a c8 d5 c3 9a 58 7f 5c f0 6c b0 02 33 c8 f7 60 a3 11 00 57 08 04 0e e9 5c c6 5e ec 5a 13 50 de bf bb 4c 6b b4 fb 42 d1 87 80 df 80 16 0a a0 a7 8a 41 ca b9 e7 b1 4f 72 e7 da ba 3d 72 f6 2d 47 e5 7c 9b 3c cf 62 60 9c 55 01 6f a7 9c c9 e6 90 60 3b 1c f6 18 2a 73 be 36 2f b8 13 b8 12 84 c9 78 38 59 c4

我们可以让大d老师来分析一下这串内容
image.png
我们解出来的是 MS PUBLICKEYBLOB 格式的 RSA-2048 公钥

TLV 遍历

1
2
3
4
5
6
7
8
004085ac  move $s1, $a0      ; $s1 = 固件buffer
00408584 move $s5, $a1 ; $s5 = IV 输出指针
004085c0 move $s4, $a2 ; $s4 = AES_KEY 输出指针
004085d4 move $s3, $a3 ; $s3 = saltLen 输出指针
004085c8 lw $s0, 0x6d0($sp) ; $s0 = em 缓冲
004085cc lw $s6, 0x6d4($sp) ; $s6 = H 输出
00408588 lui $a1, 0x42 ; addiu 0x4100 ; 0x424100 = 公钥 b64
004085c4 addiu $a2, $zero, 0x171 ; memcpy 369 字节(368+'\0')

image.png
$s1 = 固件buffer + 0x200 =TLV 表起点

image.png
循环

1
2
3
4
5
6
00408930  lw   $v0, ($s1)            ; v0 = TLV.type
00408938 bnez $v0, 0x40891c ; type != 0 → 继续处理
0040891c lw $a1, 4($s1) ; a1 = TLV.len
00408920 bne $v0, $a0, 0x40892c ; a0=1(0x408918);type != 1 → 跳过
00408928 addiu $v1, $s1, 8 ; type==1: $v1 = 条目数据指针
0040892c addu $s1, $s1, $a1 ; s1 += len → 下一条 TLV

签名反转 + modpow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
00408948  addiu $a2, $sp, 0x118     ; temp 缓冲
; ---- 反转循环 ----
0040894c addu $a1, $a2, $v0 ; a1 = temp + i
00408950 lbu $a2, 0x100($v1) ; 读 sig[0x100 - i] ($v1 每轮减 1)
00408958 sb $a2, ($a1) ; temp[i] = 上面读的
0040895c bne $v0, $a0, 0x408948 ; i != 0x100 继续
00408960 addiu $v1, $v1, -1 ; [延迟槽] v1--
; → temp = sig 的完整倒序,等价 bytes(reversed(sig))

00408964 jal RSA_bignum_bitcount(key->mod) ; → 2048
00408978 addiu $v0, $v0, 7 ; sra/srl/addu ; → emLen = (bits+7)/8 = 256
00408980 jal RSA_bignum_from_bytes(temp, 0x100) ; 签名(大端)→ bignum
004089a4 lw $t9, ... ; RSA_modpow
004089b0 jalr $t9 ; em = sig^e mod n

PSS 解包

尾字节检查

**image.png

1
2
3
4
5
6
7
8
9
.text:00408A14                 addu    $v0, $s0, $s1
em + 222

.text:00408A18 lbu $v1, -1($v0)
em[255] ← s1=222,-1 → 偏移 222+33=255

.text:00408A1C li $v0, 0xBC
.text:00408A20 lw $gp, 0x698+var_688($sp)
.text:00408A24 beq $v1, $v0, loc_408AA0
MGF1 + 异或
1
2
3
4
5
6
7
8
9
10
00408aa0  jal RSA_SHA256_MGF1(out=$sp+0x218, len=$s7=223, seed=$s0+$s7, 32)
seed = em[223:255] = H
00408b08-0x408b20 db[i] = em[i] ^ mask[i] (i < 223)
00408b24-0x408b30 db[0] &= 0x7F ; 只在 i==0 那轮(bnez $v1 跳过后续)
00408b3c lbu $a2, ($v0) ; db[i]
00408b44 bne $a2, $t0, 0x408b60 ; t0=1;不是 0x01 → 检查是不是 0x00
00408b4c subu $a2, $s1, $v1 ; saltLen = 222 - i ← saltLen 公式出处!
00408b5c sw $a2, ($s3) ; 写到参数4(saltLen_out)
00408b60 beqz $a2, 0x408be8 ; db[i]==0 → 继续扫(PS 区全 0)
00408b68 (否则) fprintf ... "RSA PSS Verify Error 2!" ← db 里出现非 0 非 0x01 字节
导出 key/IV
1
2
3
4
5
6
7
8
9
10
00408bfc  memset(em, 0, 8)                          ; M' 的前 8 字节 0x00
00408c14 lw $a2, ($s3) ; saltLen
00408c18 addiu $a0, $s0, 0x28 ; em + 40
00408c1c jal memcpy(em+40, $s2, saltLen) ; M'[40:] = salt
(M'[8:40] 留给调用方填 mHash)
00408c30 move $a0, $s5 ; IV_out(参数2)
00408c34 addiu $a1, $s2, 0x10 ; salt + 16!
00408c38 jal memcpy(..., 16) ; IV = salt[16:32] ← 0x408c38 出处
00408c4c move $a0, $s2 ; salt 基址
00408c54 jal AES_set_decrypt_key(salt, 0x80, $s4) ; key = salt[0:16],128位 ← 0x408c54 出处
AES-128-CBC
1
2
3
4
5
6
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

aligned = (len(data) - 0x330) & ~0xF
dec = Cipher(algorithms.AES(key), modes.CBC(iv)).decryptor()
plain = dec.update(data[0x330:0x330+aligned]) + dec.finalize()
assert plain[:4] == bytes.fromhex("ff000010")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import base64, hashlib
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

PUBKEY_B64 = "BgIAAACkAABSU0ExAAgAAAEAAQA1HoEmJgj8kEpP2t94/Uhr77Gq4YngLjb6yYKBQMIqnv+15hYCIp+4fulmQ99ek/KZ8kCsSgRFrR2Ona9ICjeB+xuOKkMIXjkl6s1NhvAYVeszWfgVmxxxYG2r6k07KkPgv1GbWD2hC/xZ+ZbeB9XetEJuFfdDAqydGumX70oPQJPinFenq2CDZlQDMVHpQP/yICSh48tGlbqApg96yNXDmlh/XPBssAIzyPdgoxEAVwgEDulcxl7sWhNQ3r+7TGu0+0LRh4DfgBYKoKeKQcq557FPcufauj1y9i1H5XybPM9iYJxVAW+nnMnmkGA7HPYYKnO+Ni+4E7gShMl4OFnE"

def mgf1(seed, ln):
o = b""
for c in range((ln+31)//32):
o += hashlib.sha256(seed + c.to_bytes(4,"big")).digest()
return o[:ln]

data = open("Archer_MR200(EU)v7_1.3.0_0.9.1_[260605-rel59443]_up_boot_Release_2026-06-07_20.06.16.bin","rb").read()
n = int.from_bytes(base64.b64decode(PUBKEY_B64)[20:276], "little")
em = pow(int.from_bytes(data[0x208:0x308][::-1],"big"), 65537, n).to_bytes(256,"big")
assert em[255] == 0xBC
H = em[223:255]
db = bytearray(a^b for a,b in zip(em[:223], mgf1(H,223))); db[0] &= 0x7F
salt = bytes(db[db.index(1)+1:])
key, iv = salt[:16], salt[16:32]
print("key:", key.hex(), "iv:", iv.hex())
aligned = (len(data)-0x330) & ~0xF
d = Cipher(algorithms.AES(key), modes.CBC(iv)).decryptor()
plain = d.update(data[0x330:0x330+aligned]) + d.finalize()
assert plain[:4] == bytes.fromhex("ff000010")
open("decrypted.bin","wb").write(data[:0x200] + plain + data[0x330+aligned:])
print("OK")

image.png

4、验证解密结果

xxd -s 0x200 -l 32 decrypted.bin
image.png

xxd -s 0x160200 -l 16 decrypted.bin
image.png

切出
dd if=decrypted.bin of=rootfs.sqfs bs=512 skip=2817 count=18432
image.png

unsquashfs -d rootfs rootfs.sqfs
image.png

附录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
RSA-2048 公钥 (b64, MS PUBLICKEYBLOB, e=65537, 提取自 1.1.0 httpd):
BgIAAACkAABSU0ExAAgAAAEAAQA1HoEmJgj8kEpP2t94/Uhr77Gq4YngLjb6yYKBQMIqnv+15hYCIp+4fulmQ99ek/KZ8kCsSgRFrR2Ona9ICjeB+xuOKkMIXjkl6s1NhvAYVeszWfgVmxxxYG2r6k07KkPgv1GbWD2hC/xZ+ZbeB9XetEJuFfdDAqydGumX70oPQJPinFenq2CDZlQDMVHpQP/yICSh48tGlbqApg96yNXDmlh/XPBssAIzyPdgoxEAVwgEDulcxl7sWhNQ3r+7TGu0+0LRh4DfgBYKoKeKQcq557FPcufauj1y9i1H5XybPM9iYJxVAW+nnMnmkGA7HPYYKnO+Ni+4E7gShMl4OFnE

v13 (1.3.0): key=c6ca6b09cbba7b1c2556354bfa5cfcfa iv=40288aac4026028be40bcae589b0477d
v12 (1.2.0): key=1f3ca1294491b26e81e815ece46252cb iv=eab622529b25d1b6e7bfa20a9a20f94f

固件下载 URL:
1.0.0: https://static.tp-link.com/upload/firmware/2024/202407/20240701/Archer_MR200(EU)v7_1.0.0%20Build%20240403.zip
1.1.0: https://static.tp-link.com/upload/firmware/2026/202603/20260316/Archer_MR200(EU)v7_1.1.0%20Build%20250907.zip
1.2.0: https://static.tp-link.com/upload/firmware/2026/202605/20260513/Archer_MR200(EU)v7_1.2.0_0.9.1_[250907-rel72360].zip
1.3.0: https://static.tp-link.com/upload/firmware/2026/202606/20260617/Archer_MR200(EU)v7_1.3.0%20Build%20260605.zip

GPL 源码(含 libcmm.so 符号/密钥,逆向用):
https://www.tp-link.com/en/support/gpl-code/ → MR200V7_gpl_src.tar.gz
参考实现(算法族佐证): https://github.com/watchfulip/tp-link-decrypt (基于 c310v2 GPL libsecurity 源码)
上一页
2026-09-21 16:48:57