记录对netgear XR300路由器固件分析以及重新打包过程
文章亮点就是 全方面的讲解了固件重打包的流程,从怎么分析到实际操作都进行了讲解,并且根据固件的分层结构,详细介绍了每层数据的具体处理方法
一台 netgear XR300路由器
网线
固件下载地址
官方打包工具源码地址
Note:
This package has been built successfully on 32-bit i386 Fedora 6 Linux
host machine. Compiling this package on platforms other than Fedora Core 6
may have unexpected results.
根据官方源码中的说明,配置好32位 i386 Fedora 6虚拟机
[root@localhost]# uname -a
Linux localhost.localdomain 2.6.22.14-72.fc6 #1 SMP Wed Nov 21 13:44:07 EST 2007 i686 i686 i386 GNU/Linux
下载交叉编译工具 arm-uclibc 工具链
$ binwalk XR300-V1.0.2.24_10.3.21.chk DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
58 0x3A TRX firmware header, little endian, image size: 35921920 bytes, CRC32: 0x3DB7DE14, flags: 0x0, version: 1, header size: 28 bytes, loader offset: 0x1C, linux kernel offset: 0x21159C, rootfs offset: 0x0
86 0x56 LZMA compressed data, properties: 0x5D, dictionary size: 65536 bytes, uncompressed size: 5325664 bytes
2168278 0x2115D6 Squashfs filesystem, little endian, version 4.0, compression:xz, size: 33750586 bytes, 2792 inodes, blocksize: 131072 bytes, created: 2019-04-12 06:24:01
通过分析binwalk解析结果,可以看到固件由netgear header(0x3A字节) +TRX header(0x1c字节)+linux kernel+squashfs文件系统构成。接下来由从内到外的顺序对每一个部分进行详细分析
首先固件最内层是一个squash文件系统,从binwalk解析结果可以看到对文件系统版本以及大小等属性的详细描述
2168278 0x2115D6 Squashfs filesystem, little endian, version 4.0, compression:xz, size: 33750586 bytes, 2792 inodes, blocksize: 131072 bytes, created: 2019-04-12 06:24:01
目前网上针对squashfs的打包工作都是使用开源的mksquash这个程序进行打包,使用binwalk解析出来的压缩参数进行压缩后与原本的固件进行对比
原本文件系统:
文件开头:
文件结尾:
重打包文件系统:
文件开头:
文件结尾:
可以看到不管是开头还是结尾都有明显差异,原本文件系统在结尾处有明显英文的信息描述,所以使用网上方法直接对文件系统进行打包是不行的(事实证明,这种方法的确会让路由器变砖)
因为已经有了官方打包的源码,所以我选择直接运行官方的构建固件代码,但是在编译运行的时候出现了很多报错,可能是我选择的Linux版本的问题,在修复了几个报错的文件后仍然会出现新的报错,所以我暂时放弃了这个做法。
因为官方代码编译运行是依据 src/router/Makefile 这个文件,所以我选择直接分析这个文件,文件比较大,我从头到尾把这个MakeFile读了一遍,基本就了解了netgear固件打包的全流程,这个部分只分析与squash文件系统相关的地方。
在MakeFile中和构建squash文件系统相关的语句如下:
ifeq ($(CONFIG_SQUASHFS), y)
ifeq (2_6_36,$(LINUX_VERSION))
$(MAKE) -C squashfs-4.2 mksquashfs
find $(TARGETDIR) -name ".svn" | xargs rm -rf
squashfs-4.2/mksquashfs $(TARGETDIR) $(PLATFORMDIR)/$(ROOT_IMG) -noappend -all-root
首先是在squashfs-4.2 目录下执行Make命令,然后删除所有.svn文件,最后执行mksquashfs命令,其中
PLATFORM := $(PLT)-uclibc
export PLATFORMDIR := $(TOP)/$(PLATFORM)
export TARGETDIR := $(PLATFORMDIR)/target
最终我们要选择我们构建好后门的文件系统文件夹
./mksquashfs squashfs-root/ target.squashfs -noappend -all-root
这里需要注意的是,(TARGETDIR)参数要使用原来固件解析出的文件系统文件夹,src文件夹中的文件系统缺少文件会导致web等服务无法启动
新生成的文件系统和binwalk解出来的不管是开头还是结尾都很相似了,是可以使用的。
86 0x56 LZMA compressed data, properties: 0x5D, dictionary size: 65536 bytes, uncompressed size: 5325664 bytes
从binwalk解析结果来看,文件系统上面是Linux内核,接下来介绍几种常见的Linux内核文件
initrd.img是一个小的映象,包含一个最小的linux系统。通常的步骤是先启动内核,然后内核挂载initrd.img,并执行里面的脚本来进一步挂载各种各样的模块,然后发现真正的root分区,挂载并执行/sbin/init…
initrd.img当然是可选的了,如果没有initrd.img,内核就试图直接挂载root分区。
说 initrd.img文件还会提到另外一个名角---vmlinuz。vmlinuz是可引导的、压缩的内核。“vm”代表 “Virtual Memory”。Linux 支持虚拟内存,不像老的操作系统比如DOS有640KB内存的限制。Linux能够使用硬盘空间作为虚拟内存,因此得名“vm”。另外:vmlinux是未压缩的内核,vmlinuz是vmlinux的压缩文件。
系统内核vmlinuz被加载到内存后开始提供底层支持,在内核的支持下各种模块,服务等被加载运行。这样当然是大家最容易接受的方式,曾经的linux就是这样的运行的。假设你的硬盘是scsi 接口而你的内核又不支持这种接口时,你的内核就没有办法访问硬盘,当然也没法加载硬盘上的文件系统,怎么办?把内核加入scsi驱动源码然后重新编译出一个新的内核文件替换原来vmlinuz。
vmlinuz是可引导的、压缩的内核。“vm”代表“Virtual Memory”。Linux 支持虚拟内存,不像老的操作系统比如DOS有640KB内存的限制。Linux能够使用硬盘空间作为虚拟内存,因此得名“vm”。vmlinuz是可执行的Linux内核,它位于/boot/vmlinuz,它一般是一个软链接,比如图中是vmlinuz-2.4.7-10的软链接。
vmlinuz的建立有两种方式。一是编译内核时通过“make zImage”创建,然后通过:
“cp /usr/src/linux-2.4/arch/i386/linux/boot/zImage /boot/vmlinuz”产生。zImage适用于小内核的情况,它的存在是为了向后的兼容性。二是内核编译时通过命令make bzImage创建,然后通过:“cp /usr/src/linux-2.4/arch/i386/linux/boot/bzImage /boot/vmlinuz”产生。bzImage是压缩的内核映像,需要注意,bzImage不是用bzip2压缩的,bzImage中的bz容易引起 误解,bz表示“big zImage”。bzImage中的b是“big”意思。
zImage(vmlinuz)和bzImage(vmlinuz)都是用gzip压缩的。它们不仅是一个压缩文件,而且在这两个文件的开头部分内嵌有gzip解压缩代码。所以你不能用gunzip 或 gzip –dc解包vmlinuz。
内核文件中包含一个微型的gzip用于解压缩内核并引导它。两者的不同之处在于,老的zImage解压缩内核到低端内存(第一个640K), bzImage解压缩内核到高端内存(1M以上)。如果内核比较小,那么可以采用zImage 或bzImage之一,两种方式引导的系统运行时是相同的。大的内核采用bzImage,不能采用zImage。
在makefile中针对linux kernel的命令主要有以下几条:
linux_kernel:
ifeq ($(LINUXDIR), $(BASEDIR)/components/opensource/linux/linux-2.6.36)
$(MAKE) -C $(LINUXDIR) zImage
$(MAKE) CONFIG_SQUASHFS=$(CONFIG_SQUASHFS) -C $(SRCBASE)/router/compressed
可以发现源码中已经有了官方编译好的内核可以直接使用,或者也可以直接将固件中这一部分提取出来直接使用
接下来是对TRX头进行分析
58 0x3A TRX firmware header, little endian, image size: 35921920 bytes, CRC32: 0x3DB7DE14, flags: 0x0, version: 1, header size: 28 bytes, loader offset: 0x1C, linux kernel offset: 0x21159C, rootfs offset: 0x0
从binwalk解析结果来看,TRX头是对linux kernel和squashfs的拼接和校验
在我使用的固件中TRX内容如下:
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F00000030 48 44 52 30 00 F0 HDR0 ?
00000040 E2 01 C3 75 71 F9 00 00 01 00 1C 00 00 00 60 E5 ?胾q? `?
00000050 21 00 00 00 00 00 !
TRX是某些路由器(如linksys)和开源固件(如OpenWRT和DD-WRT)中使用的内核映像文件的格式。
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 */
};
0x3A-0x3D magic魔数
0x3E-0x41 image size
0x42-0x45 CRC value
0x46-0x47 TRX_flag
0x48-0x49 TRX_version
0x4A-0x55 分区偏移量:loader 偏移: 0x1C, linux kernel 偏移: 0x21E560, rootfs 偏移: 0x0
官方打包代码中已经有trx工具可以直接使用,在MakeFile中针对trx头使用的命令如下,可以看到其实就是对之前生成的vmlinuz文件和squash文件做一个组装
trx -o $(PLATFORMDIR)/linux.trx $(PLATFORMDIR)/vmlinuz $(PLATFORMDIR)/$(ROOT_IMG) ;
在虚拟机中运行以下的命令,可得到相应的加了trx头的文件
$ ./trx -o ./linux.trx vmlinuz target.squashfs
append nvram file target.squashfs
append nvram 33751040 bytes
$ binwalk linux.trx DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 TRX firmware header, little endian, image size: 35921920 bytes, CRC32: 0x42E58DB3, flags: 0x0, version: 1, header size: 28 bytes, loader offset: 0x1C, linux kernel offset: 0x0, rootfs offset: 0x2030000
28 0x1C LZMA compressed data, properties: 0x5D, dictionary size: 65536 bytes, uncompressed size: 5325664 bytes
2168220 0x21159C Squashfs filesystem, little endian, version 4.0, compression:xz, size: 33750709 bytes, 2792 inodes, blocksize: 131072 bytes, created: 2021-03-24 06:51:29
$ binwalk XR300-V1.0.2.24_10.3.21.chk
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
58 0x3A TRX firmware header, little endian, image size: 35921920 bytes, CRC32: 0x3DB7DE14, flags: 0x0, version: 1, header size: 28 bytes, loader offset: 0x1C, linux kernel offset: 0x21159C, rootfs offset: 0x0
86 0x56 LZMA compressed data, properties: 0x5D, dictionary size: 65536 bytes, uncompressed size: 5325664 bytes
2168278 0x2115D6 Squashfs filesystem, little endian, version 4.0, compression:xz, size: 33750586 bytes, 2792 inodes, blocksize: 131072 bytes, created: 2019-04-12 06:24:01
使用binwalk对比原本的固件以及生成的trx文件可以发现在TRX头部分除了CRC32不一样外,偏移部分也存在较大差异,CRC32不一样是正常的,偏移部分差异过大肯定存在问题。
通过将偏移数值与binwalk解析结果对比分析,可以发现使用trx工具生成的TRX文件将linux kernel和squashfs的偏移搞反了,为了印证我的猜想,我对TRX工具进行了逆向
可以看到这里反汇编出来的逻辑与原始固件中TRX头所表现出来的肯定是不一样的, 为了与原固件保持一致需要在TRX生成的文件基础上做一些调整,具体调整方式如下:
将trx文件中0x18 - 0x1b也就是rootfs对应的偏移改为0
将trx文件中0x14 - 0x17也就是linux kernel对应的偏移改为0x21159C,这是因为使用的就是原始固件中的linux kernel所以这里的偏移与原固件保持一致,如果修改了内核文件导致与原始内核大小不一致,这里需要变成修改后的偏移
重新计算CRC32
以XR300-V1.0.2.24_10.3.21.chk文件为例,CRC32校验值为0x3DB7DE14,根据TRX定义,这里的CRC32校验是计算从flag部分到文件结尾,使用winhex计算该部分校验值
发现与固件中的值不符合,这里其实是做了一个取反操作
>>> bin(0x3DB7DE14)
'0b111101101101111101111000010100'
>>> bin(0xC24821EB)
'0b11000010010010000010000111101011'
所以在修改完TRX偏移部分后,计算出CRC32校验,取反之后修改CRC32部分即可
在线取反工具
$ binwalk linux.trx DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 TRX firmware header, little endian, image size: 35921920 bytes, CRC32: 0x16**65F4, flags: 0x0, version: 1, header size: 28 bytes, loader offset: 0x1C, linux kernel offset: 0x21159C, rootfs offset: 0x0
28 0x1C LZMA compressed data, properties: 0x5D, dictionary size: 65536 bytes, uncompressed size: 5325664 bytes
2168220 0x21159C Squashfs filesystem, little endian, version 4.0, compression:xz, size: 33750709 bytes, 2792 inodes, blocksize: 131072 bytes, created: 2021-03-24 06:51:29
最后一个部分就是针对netgear header的构建,前0x3A字节是netgear自带的header,
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F00000000 2A 23 24 5E 00 00 00 3A 01 01 00 03 1A 0A 03 16 *#$^ :
00000010 C2 70 61 44 00 00 00 00 02 24 20 00 00 00 00 00 聀aD $
00000020 C2 70 61 44 F1 AC 09 FF 55 31 32 48 33 33 32 54 聀aD瘳 U12H332T
00000030 37 38 5F 4E 45 54 47 45 41 52 78_NETGEAR
通过查看R7000,XR300的多个版本chk文件,0-8字节是不变的,从9字节开始是固件的版本号。
0x9-0x10字节是固件的版本号,对应着文件名。
0x10-0x17,0x20-0x27 是netgear的chencksum信息,具体介绍在后面说明
0x18-0x1F是固件大小,每一个chk文件大小都以0x3A结尾,所以文件大小信息的0x3A存放在0x7的位置。
0x28-0x39字节是固件的种类信息,比如同一系列R7000,这12字节就是相同的。
在Makefile中针对这一部分的命令如下:
###########################################
### Create .chk files for Web UI upgrade ##
cd $(PLATFORMDIR) && touch rootfs &&
../../../tools/packet -k linux.trx -f rootfs -b $(BOARDID_FILE)
-ok kernel_image -oall kernel_rootfs_image -or rootfs_image
-i $(fw_cfg_file) &&
rm -f rootfs &&
cp kernel_rootfs_image.chk $(FW_NAME)_`date +%m%d%H%M`.chk
这里的操作逻辑是先创建一个空的rootfs文件然后使用packet程序对上面生成的linux.trx文件添加header,完整命令如下:
$ ./packet -k linux.trx -b compatible_xr300.txt -ok kernel -oall image -or rootfs -i ambitCfg.h
-b -i 的参数都可以在源码文件夹中找到
生成的image.chk就是最终的完整固件
重打包工作到这里其实就已经结束了,但是我对packet工具进行了进一步分析
因为不是很了解packet参数对应的具体含义,所以我使用IDA对packet进行了逆向
程序的逻辑是先通过程序参数输入对要使用的文件名变量进行赋值,然后通过 -i [configure file path/name] 获得的cfg文件提取出固件的版本信息,这个文件对应的就是 ambitCfg.h 查看文件内容,可以看到文件中定于了固件版本
/*formal version control*/#define AMBIT_HARDWARE_VERSION "U12H240T00"
#define AMBIT_SOFTWARE_VERSION "V1.0.3.2"
#define AMBIT_UI_VERSION "1.0.57"
#define STRING_TBL_VERSION "1.0.3.2_2.1.33.8"
如果要打包别的版本固件需要对这里的字段修改成对应固件版本的信息。
接下来就是对三个输出文件添加header,这三个输出都是采用fwrite函数,将malloc_chunk的内容输入到文件中
查看addheader函数中对malloc_chunk的引用,可以发现,修改malloc_chunk的地方只要下面的代码
memcpy(malloc_chunk, v20, v24);
memcpy((char *)malloc_chunk + v24, &s, v26);
memcpy((char *)malloc_chunk + v25, dest, v27);
v20数组中先是存储了4个字节的字符串然后拷贝进了固件的版本信息
然后拷贝了 kernel_checksum 和 rootfs_checksum 接着是对kernel文件长度和rootfs文件长度的信息,然后是rootfs_kernel_checksum,填充4字节0,加上compatible.txt里的内容,最后对整个头部再求一个checksum,将结果填充进刚才4字节0的位置
接下来分析这里的计算checksum的函数
第一次调用时a1 = 0,所以c1 = 0,c0 = 0
第二次调用时a1 = 1
这里的逻辑很简单,就是从file中每次读取一个字节的数据,然后
c0 += *(unsigned char *)(kernel_file+i);
c1 += c0;
为了验证这里的计算结果,我使用GDB调试packet
查看程序的保护措施
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
只开启了NX,没有地址随机化,所以调试起来很方便
使用gdb加载packet,设置 args 后输入
set args -k linux.bin -f _R6300v2-V1.0.3.2_1.0.57.chk.extracted/20E2BA.squashfs -b compatible_r6300v2.txt -ok kernel -oall kernel_rootfs -or rootfs -i ambitCfg.h
我根据IDA反汇编的逻辑,自己写了一个C的计算checksum代码,如下:
#include<stdio.h>
#include<stdlib.h>
#include<stdint.h>int c1,c0;
int main()
{
//FILE *kernel_file_fd = fopen("R6300v2-V1.0.3.2_1.0.57.chk","rb");
FILE *kernel_file_fd = fopen("1","rb");
void *kernel_file = malloc(0x2000000);
int file_len = fread(kernel_file,1,0x2000000,kernel_file_fd);
int i;
for(i=0;i<file_len;i++){
c0 += *(unsigned char *)(kernel_file+i);
c1 += c0;
}
c0 = (c0 & 0x0ffff) + ((unsigned int)c0 >> 16);
c0 = ((c0 >> 16) + c0) & 0xffff;
c1 = (c1 & 0x0ffff) + ((unsigned int)c1 >> 16);
c1 = ((c1 >> 16) + c1) & 0xffff;
int checksum;
checksum = (c1 << 16) | c0;
printf("0x%x",checksum);
}
首先在对rootfs计算checksum的位置下断点,通过查看计算的checksum
与C计算的一致
接下来计算对linux kernel的checksum
发现与C计算的也是一致的
接下来是计算了rootfs+linux kernel的checksum
第一部分校验和是对TRX header + linux kernel + squashfs文件系统的校验
56.7z 对应着固件中linux kernel+squashfs文件系统的部分
cd rootfs/usr/sbin
mv dlnad dlnadd
touch dlnad
vim dlnad
#!/bin/sh/usr/sbin/telnetd -F -l /bin/sh -p 1234 &
/usr/sbin/dlnadd &
sudo chown 777 dlnad
将修改后的文件系统重新打包上传
在netgear某些版本中,无法直接开启telnet需要先创建设备文件
完整命令如下:
mknod /dev/ptyp0 c 2 0; mknod /dev/ttyp0 c 3 0; mknod /dev/ptyp1 c 2 1; mknod /dev/ttyp1 c 3 1; telnetd -p6666 -l/bin/sh
完整流程图如下:
下载nmrpflash.exe工具
链接:https://pan.baidu.com/s/140aE74ZUsRMcW1sbdLYqYQ,
提取码:opw4
配置静态IP
用管理员权限打开cmd
使用网线插上netgear lan口,在命令行中进入刚下载工具的目录
输入命令nmrpflash.exe -L查询网卡编号
这里可以看到对应的编号是net1
上传固件
查询好编号后,执行命令
nmrpflash.exe -i net1 -f R6220-V1.1.0.86.img
开启救砖之路,值得注意的是,此命令执行的时机为:在命令提示符中输入好指令后,此时路由在处于关机状态,开启路由器电源,等待5秒,执行命令,大概等待1-5分钟左右的时间,即可恢复成功。命令执行时机决定救砖是否成功,可以不断尝试,直到成功。