修改嵌入式 ARM Linux 内核映像中的文件系统

zImage 是编译内核后在 arch/arm/boot 目录下生成的一个已经压缩过的内核映像。通常我们不会使用编译生成的原始内核映像 vmlinux,因其体积很大。因此,zImage 是我们最常见的内核二进制,可以直接嵌入到固件,也可以直接使用 qemu 进行调试。当然,在 32 位嵌入式领域还能见到 uImage,这是在 zImage 首位增加 64B 的头,描述映像文件类型、加载位置、内核大小等信息。

有些嵌入式设备的文件系统直接嵌入到内核中,这种内置文件系统的机制被称为 ramdisk/initramfs,如果只是使用 extract-vmlinux/binwalk 解压固件,释放大量 shell 脚本和配置文件,是很容易做到的,但是如果想要修改这些文件,并进行重新打包,生成实际设备可以运行的 zImage 内核映像可能不是那么简单。

本文将演示如何在 32位 ARM zImage 中替换 piggy 中的文件系统,我们以 openWRT 的某个版本固件为例进行讲解。

初始设置

下载 OpenWRT ARM zImage-initramfs 映像,这是一个基于 ramdisk 的典型内核映像,不需要额外的文件系统,实际上也无法使用 binwalk 直接提取我们想要修改的操作系统启动提示信息。

$ wget https://downloads.openwrt.org/releases/17.01.0/targets/armvirt/generic/lede-17.01.0-r3205-59508e3-armvirt-zImage-initramfs -O zImage-initramfs
$ openssl dgst zImage-initramfs
SHA256(zImage-initramfs)= 5ad269e95b2db16aea3794dd0e97dabb6f9712184d79b0764bb10a810f8d7639

使用 qemu 启动

$ qemu-system-arm -M virt -m 1024 -kernel zImage-initramfs -append "console=ttyAMA0" -nographic

最小 shell 控制台

BusyBox v1.25.1 () built-in shell (ash)

     _________
    /        /\      _    ___ ___  ___
   /  LE    /  \    | |  | __|   \| __|
  /    DE  /    \   | |__| _|| |) | _|
 /________/  LE  \  |____|___|___/|___|                      lede-project.org
 \        \   DE /
  \    LE  \    /  -----------------------------------------------------------
   \  DE    \  /    Reboot (17.01.0, r3205-59508e3)
    \________\/    -----------------------------------------------------------

=== WARNING! =====================================
There is no root password defined on this device!
Use the "passwd" command to set up a new password
in order to prevent unauthorized SSH logins.
--------------------------------------------------
root@LEDE:/#

查看内核版本,找到对应的源码,因为我们有可能会根据内核解压缩的源码,调整重打包方式。

root@LEDE:/# uname -a
Linux LEDE 4.4.50 #0 SMP Mon Feb 20 17:13:44 2017 armv7l GNU/Linux

找到相应版本的内核,推荐在线浏览 https://elixir.bootlin.com/linux/v4.4.50/source/,版本匹配也没有那么重要,因为内核解压缩的核心代码其实一直以来变化不大,位于源码目录 arch/arm/boot/compressed

提取 Piggy

使用 binwalk 分析固件,就像我们在开始说的,binwalk 可能可以提取其中的配置文件,也有可能无法提取,即使提取,也都是归在一个文件夹下,并没有常见的 squashfs 文件系统

$ binwalk zImage-initramfs         

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             Linux kernel ARM boot executable zImage (little-endian)
15400         0x3C28          xz compressed data
15632         0x3D10          xz compressed data

毫无疑问,固件开始部分是可以直接运行的未经压缩的用于解压内核的 head.omisc.o,使用 dd 命令提取该部分进行分析,或者直接将整个固件拖入 IDA,选择 arm,并只反汇编固件头部部分。
在这里插入图片描述
运行上述 IDC 脚本,即可得到解压内核代码部分。可以对比内核源码,我们需要找到固件中,内核压缩映像文件的起始地址和结束地址。piggy.S 使用 incbin 关键字引入 piggy.gz。其中全局变量 input_datainput_data_end 分别是 piggy 的起始地址和结束地址。

	.section .piggydata,#alloc
	.globl	input_data
input_data:
	.incbin	"arch/arm/boot/compressed/piggy.gz"
	.globl	input_data_end
input_data_end:

毫无疑问,内核解压代码需要这些全局变量,这样才能够解压真正压缩的内核。

putstr("Uncompressing Linux...");
ret = do_decompress(input_data, input_data_end - input_data,
    output_data, error);
if (ret)
    error("decompressor returned an error");
else
    putstr(" done, booting the kernel.\n");

IDA 反编译的固件头部,寻找 Uncompressing Linux...,对比源码很容易知道 piggy 的实际偏移。
在这里插入图片描述
继续分析汇编,找到全局变量存放的位置
在这里插入图片描述
对比原始固件二进制时,发现压缩结束 magic YZ 后面多出了 4B 数据,这 4B 其实是原始未经压缩的xz大小。实际上 YZ 才是压缩文件的结尾。因此使用 xz 解压时,估计会出现 Unexpected end of input 错误,只需要添加参数即可。
在这里插入图片描述
dd 截取 piggy

$ dd if=zImage-initramfs of=vmlinux.xz bs=1 skip=$[0x3d10] count=$[0x2bb404]                     
2864132+0 records in
2864132+0 records out
2864132 bytes (2.9 MB, 2.7 MiB) copied, 13.595 s, 211 kB/s

解压 piggy

$ unxz --verbose --single-stream < vmlinux.xz > /tmp/vmlinux
  100 %   2,797.0 KiB / 8,883.5 KiB = 0.315 

我们发现解压后的 vmlinux 内核映像大小果然是 28 c3 8a 00

$ ls -l /tmp/vmlinux                
-rw-r--r-- 1 kali kali 9096744 Dec 20 04:04 /tmp/vmlinux

$ python -c "print(0x8ace28)"
9096744

重打包

修改 vmlinux,例如修改启动界面字符串,找到需要修改信息的地址。这些信息显示 initramfs 嵌入在解压后的 vmlinux 中,该部分由一个没有校验和的未经压缩的 CPIO 文档组成(binwalk 可以识别)。

$ strings -t x /tmp/vmlinux | grep "WARNING\!" 
 76ac3a === WARNING! =====================================

使用 hexedit 编辑,回车键可快速定位此地址,tab 可切换 16 进制 / ASCII 码,ctrl+x 保存并退出。

0076AC3C   3D 20 57 41  52 4E 49 4E  47 21 20 4D  6F 64 69 66  69 63 61 74  = WARNING! Modificat
0076AC50   69 6F 6E 20  73 75 63 63  65 65 64 65  64 21 21 21  3D 3D 3D 3D  ion succeeded!!!====

如果直接使用 xz 压缩,我们会发现压缩后大小大于原始压缩文件 0x2bb404(2864132),通过 Linux 源码可以找到压缩命令位于 xz_wrap.sh

xz --check=crc32 --arm --lzma2=$LZMA2OPTS,dict=32MiB

仅仅使用上述命令压缩还是不够的,压缩后的文件仍然较大,nice 可以达到最大压缩比。最终压缩命令如下

$ xz --check=crc32 --arm --lzma2=,dict=32MiB,nice=128 < /tmp/vmlinux > /tmp/vmlinux.xz             

$ ls -l /tmp/vmlinux.xz 
-rw-r--r-- 1 kali kali 2863832 Dec 20 04:26 /tmp/vmlinux.xz

显然小于原始压缩文件,符合要求。要记住,piggy 末尾 4 字节存放原始文件大小,而我们只是修改启动信息,并没有改变原始 vmlinux 大小

$ echo -en "\x28\xce\x8a\x00" >> /tmp/vmlinux.xz # piggy.gz

替换 piggy

$ cp zImage-initramfs zImage-initramfs-warnmod
$ dd if=/tmp/vmlinux.xz of=zImage-initramfs-warnmod bs=1 seek=$[0x3d10] conv=notrunc
2863836+0 records in
2863836+0 records out
2863836 bytes (2.9 MB, 2.7 MiB) copied, 11.1713 s, 256 kB/s

修改内核解压代码中的 piggy 结束地址,input_data_end = hex(0x3d10+2863836) = 0x2befec,原始大小为 0x2bf114

002BF124   EC EF 2B 00  68 F5 2B 00  10 3D 00 00  64 F5 2B 00  64 F1 2B 00  ..+.h.+..=..d.+.d.+.

尝试启动内核,修改成功!

BusyBox v1.25.1 () built-in shell (ash)

     _________
    /        /\      _    ___ ___  ___
   /  LE    /  \    | |  | __|   \| __|
  /    DE  /    \   | |__| _|| |) | _|
 /________/  LE  \  |____|___|___/|___|                      lede-project.org
 \        \   DE /
  \    LE  \    /  -----------------------------------------------------------
   \  DE    \  /    Reboot (17.01.0, r3205-59508e3)
    \________\/    -----------------------------------------------------------

=== WARNING! Modification succeeded!!!============
There is no root password defined on this device!
Use the "passwd" command to set up a new password
in order to prevent unauthorized SSH logins.
--------------------------------------------------
root@LEDE:/# 

小结

如果需要增加而不是修改 initramfs 的内容,可能就没那么简单了。因为你需要准确掌握固件的每一个部分,而且需要注意的是 piggy 的 inflated size 也就是 xz 实际大小其实是 input_data_end - 4,这一部分代码位于 misc.cLC0 对象

LC0:	.word	LC0			@ r1
		.word	__bss_start		@ r2
		.word	_end			@ r3
		.word	_edata			@ r6
		.word	input_data_end - 4	@ r10 (inflated size location)
		.word	_got_start		@ r11
		.word	_got_end		@ ip
		.word	.L_user_stack_end	@ sp
		.word	_end - restart + 16384 + 1024*1024
		.size	LC0, . - LC0

以本文中的固件为例,piggy 实际大小 0x2bf110,位于固件偏移 0x258,因此如果修改了 piggy 的大小,还需要修改此处地址对应的数据。

在这里插入图片描述
当然,实际还需要考虑各个部分的偏移,可参考 https://gist.github.com/jamchamb/243e6973aeb5c9a2e302a4d4f57f16e1

如果你需要增加内核内容并且改变了原有内核大小,而不只是简单修改,则需要掌握内核解压缩的详细流程,在这里,我们只将内核压缩映像生成流程简单呈现如下,详细流程可参见

vmlinux
   │
   │ -R.note-R.comment
   │
   └─arch/arm/boot/Image
       │
       │ gzip -f -9 < Image > piggy.gz
       │
       └─arch/arm/boot/compressed/piggy.gz
           │
           │ piggy.S 直接引入piggy.gz
           │
           └─arch/arm/boot/compressed/piggy.o
               │
               │ +head.o
               │ +misc.o
               │
               └─arch/arm/boot/compressed/vmlinux
                   │
                   │ -debuginfo
                   │
                   └─arch/arm/boot/compressed/zImage

内核代码中的 head.Smisc.c 用于内核自解压,所以,如果我们需要直接通过修改内核二进制的方式打 patch,则需要了解内核压缩和解压的流程。从上图也可以看出来,piggy 就是压缩过的内核的一部分,其实也是内核的主体部分。

参考文献

Modifying Embedded Filesystems in ARM Linux zImages
Linux内核源码分析–内核启动之zImage自解压过程
Linux2.6 内核启动分析
initramfs 在内核中的作用与实现

猜你喜欢

转载自blog.csdn.net/song_lee/article/details/128345166
今日推荐