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.o
和 misc.o
,使用 dd
命令提取该部分进行分析,或者直接将整个固件拖入 IDA,选择 arm,并只反汇编固件头部部分。
运行上述 IDC 脚本,即可得到解压内核代码部分。可以对比内核源码,我们需要找到固件中,内核压缩映像文件的起始地址和结束地址。piggy.S
使用 incbin
关键字引入 piggy.gz
。其中全局变量 input_data
和 input_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.c
的 LC0
对象
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.S
和 misc.c
用于内核自解压,所以,如果我们需要直接通过修改内核二进制的方式打 patch,则需要了解内核压缩和解压的流程。从上图也可以看出来,piggy 就是压缩过的内核的一部分,其实也是内核的主体部分。
参考文献
Modifying Embedded Filesystems in ARM Linux zImages
Linux内核源码分析–内核启动之zImage自解压过程
Linux2.6 内核启动分析
initramfs 在内核中的作用与实现