原创作者:王锐,多年 Linux 系统、龙芯平台移植与优化研发经验,Linux Contributor、Mozillian。
背景
某个设备配套的刷机程序是个 Linux recovery kernel,刷机过程会先从U盘加载刷机脚本,仅在签名校验通过后才执行脚本。本文记录了分析和移除签名校验的方法。
分析
刷机程序是一个 bzImage 文件,从启动的输出来看,内部包含了一个 initrd,在 initrd 中实现了读取U盘中的脚本和签名校验过程。
查看initrd内容
通过增加启动参数(cmdline)rdinit=/bin/sh,可以使 Kernel 启动后执行 /bin/sh
,而不是默认的 /init
程序,有了命令行接口后,就可以查看 initrd 的内容。
# busybox find /
# cat /init
从 initrd 的内容来看,由 /init
调用 gpg2 对 U 盘中的刷机脚本执行签名校验,只有公钥集成在 initrd 中,没有私钥。
到这一步,我们已经清楚了签名校验的实现方法,并且也能使启动过程进入受控的命令行交互状态,其实已经可以手工操作跳过签名过程来刷机。
修改
每次手工操作的确太麻烦,那就来移除 initrd 中的签名校验过程吧。
从 bzImage 的结构来看,要想修改 initrd,先要从 bzImage 中提取出 vmlinux,再从 vmlinux 中提取出 initrd。
1. 提取 vmlinux
从 bzImage 中提取 vmlinux 比较简单,有现成的工具,位于 Linux 源代码中 scripts/extract-vmlinux
./scripts/extract-vmlinux bzImage > vmlinux
2. 提取 initrd
initrd 的格式可以是 cpio archive,也可以是 gzip、bzip2、lzma、xz 或 lzo 压缩的,先用 binwalk 扫描一遍。
binwalk vmlinux
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 ELF, 32-bit LSB executable, Intel 80386, version 1 (SYSV)
3641536 0x3790C0 Linux kernel version "2.6.39 (ubuntu@ubuntu) (gcc version 4.9.3 20150311 (prerelease) (crosstool-NG 1.20.0) ) #24 SMP Fri Jun 7 14:32:37 CST 2019"
3922304 0x3BD980 CRC32 polynomial table, little endian
4318976 0x41E700 Unix path: /home/ubuntu/ce5300/barcelona_kernel/arch/x86/include/asm/desc.h
4321256 0x41EFE8 Unix path: /home/ubuntu/ce5300/barcelona_kernel/arch/x86/include/asm/i387.h
4322244 0x41F3C4 Unix path: /home/ubuntu/ce5300/barcelona_kernel/arch/x86/include/asm/processor.h
4323964 0x41FA7C Unix path: /x86/kernel/cpu/perf_event_intel.c
4324152 0x41FB38 Unix path: /x86/kernel/cpu/perf_event_intel_ds.c
4325960 0x420248 Unix path: /x86/kernel/cpu/mcheck/mce.c
4326820 0x4205A4 Unix path: /x86/kernel/cpu/mcheck/mce_intel.c
4327124 0x4206D4 Unix path: /x86/kernel/cpu/mcheck/therm_throt.c
4328480 0x420C20 Unix path: /x86/kernel/cpu/mtrr/generic.c
4329752 0x421118 Unix path: /x86/kernel/cpu/mtrr/cleanup.c
4329832 0x421168 Unix path: /x86/kernel/cpu/perfctr-watchdog.c
4336148 0x422A14 Unix path: /x86/kernel/apic/apic_noop.c
4336572 0x422BBC Unix path: /x86/kernel/apic/io_apic.c
4343276 0x4245EC Unix path: /home/ubuntu/ce5300/barcelona_kernel/arch/x86/include/asm/fixmap.h
4347540 0x425694 Unix path: /x86/kernel/cpu/common.c
4347663 0x42570F Unix path: /x86/kernel/cpu/vmware.c
4347911 0x425807 Unix path: /x86/kernel/cpu/intel.c
4350475 0x42620B Unix path: /x86/kernel/acpi/boot.c
4352464 0x4269D0 Unix path: /x86/kernel/apic/apic.c
4352799 0x426B1F Unix path: /x86/kernel/apic/ipi.c
4367224 0x42A378 Unix path: /home/ubuntu/ce5300/barcelona_kernel/arch/x86/include/asm/mmu_context.h
4374285 0x42BF0D Unix path: /sys/kernel/debug/tracing/trace_clock
4383716 0x42E3E4 Unix path: /home/ubuntu/ce5300/barcelona_kernel/arch/x86/include/asm/pgalloc.h
4384752 0x42E7F0 Unix path: /home/ubuntu/ce5300/barcelona_kernel/arch/x86/include/asm/dma-mapping.h
4513864 0x44E048 xz compressed data
4514016 0x44E0E0 Unix path: /home/ubuntu/ce5300/barcelona_kernel/arch/x86/include/asm/syscall.h
4533558 0x452D36 Unix path: /Buffer/String/Package/Ref/Ddb], found [%s] %p
4612622 0x46620E Unix path: /sys/kernel/debug/dri
4614914 0x466B02 Unix path: /sys/kernel/debug/dri.
4618302 0x46783E Unix path: /sys/kernel/debug/dri/%s/%s
4618366 0x46787E Unix path: /sys/kernel/debug/dri/%s
4618509 0x46790D Unix path: /sys/kernel/debug/dri.
4661219 0x471FE3 Unix path: /S70/S75/505V/F505/F707/F717/P8
4665828 0x4731E4 Unix path: /usr/include/asm/ioctls.h
4678778 0x47647A Copyright string: "Copyright(c) Pierre Ossman"
4690408 0x4791E8 Unix path: /x86/oprofile/../../../drivers/oprofile/event_buffer.c
5242204 0x4FFD5C ELF, 32-bit LSB shared object, Intel 80386, version 1 (SYSV)
5243884 0x5003EC ELF, 32-bit LSB shared object, Intel 80386, version 1 (SYSV)
在 vmlinux 文件偏移 0x44E048 处,有一个疑似 xz 压缩文档,提取出来尝试解压。
if=vmlinux
唯一的疑似压缩文档解压出错了,这个方法行不通,那就换另外一个方法吧。:)
2.1. 分析启动过程中的 initrd 加载
从 bzImage 中提取出的 vmlinux 是 strip 掉 symbols 的,不便于反汇编后定位函数,我们先提取该内核的 /proc/kallsyms
,直接在 rdinit=/bin/sh
启动的命令行中 cat /proc/kallsyms
就可以了。
有了 symbols 后,首先我们要找 populate_rootfs 函数,从汇编代码中获得__initramfs_start 和 __initramfs_size。
c14d03b1 t populate_rootfs
c14d0129 t unpack_to_rootfs
c14d03b1: 55 push %ebp
c14d03b2: b8 6c 59 51 c1 mov $0xc151596c,%eax
c14d03b7: 57 push %edi
c14d03b8: 56 push %esi
c14d03b9: 53 push %ebx
c14d03ba: 8d 64 24 b8 lea -0x48(%esp),%esp
c14d03be: 8b 15 70 6f 8e c1 mov 0xc18e6f70,%edx
c14d03c4: e8 60 fd ff ff call 0xc14d0129
在 0xc14d03c4 处调用了0xc14d0129 这个函数,也就是 unpack_to_rootfs,传递了两个参数,%eax 就是 __initramfs_start,值是 0xc151596c,%edx 就是 __initramfs_size,%edx 的值是存储在地址 0xc18e6f70 处的。
有了 __initramfs_start 的程序地址后,只需要转换为 vmlinux 文件的偏移地址后,就可以提取出 initrd 的内容了。映射关系可以通过 readelf 获得。
readelf
程序地址 0xc151596c 和 0xc18e6f70 都隶属于 .init.data section,文件偏移计算:
Offset = Addr - Section Base Addr + Section Base Offset
0xc151596c: 0xc151596c - 0xc14f3b40 + 0x4f4b40 = 0x51696c
0xc18e6f70: 0xc18e6f70 - 0xc14f3b40 + 0x4f4b40 = 0x8e7f70
2.2. 提取 initrd
提取 initrd,首先需要知道 __initramfs_size,该值位于 vmlinux 文件的 0x8e7f70 偏移处,类型是 unsigned long。
08e7f70
if=vmlinux
2.3. 分析 initrd 格式
虽然提取出了 initrd,但不是已知的格式,内核支持的格式都有确定的 magic number:
static
hexdump initrd
0000000 6fde 40fe 2ee2 5fbf 27e3 e8fe fb88 6a72
0000010 b649 904e 378a 49f4 057f 69b4 f9d9 4d43
0000020 7a8a fe5b 1ba5 2442 3ea5 365e 7945 fd49
0000030 9afb fca6 143c b30d eff8 a715 0982 424c
...
既然这个内核能执行,说明它有一种未知的加载方法,那就看看它是怎么做的吧,我们需要找到 unpack_to_rootfs 函数。
c14d0129 t unpack_to_rootfs
c1001410 T aes_key_schedule_128
c10017c0 T aes_decrypt_128
c14d0129: 55 push %ebp
c14d012a: b9 11 00 00 00 mov $0x11,%ecx
c14d012f: 89 d5 mov %edx,%ebp
c14d0131: 57 push %edi
c14d0132: 56 push %esi
c14d0133: be ce c6 41 c1 mov $0xc141c6ce,%esi
c14d0138: 53 push %ebx
c14d0139: 89 c3 mov %eax,%ebx
c14d013b: 8d a4 24 30 ff ff ff lea -0xd0(%esp),%esp
c14d0142: 8d 7c 24 0f lea 0xf(%esp),%edi
c14d0146: 8d 54 24 20 lea 0x20(%esp),%edx
c14d014a: 8d 44 24 0f lea 0xf(%esp),%eax
c14d014e: f3 a4 rep movsb %ds:(%esi),%es:(%edi)
c14d0150: e8 bb 12 b3 ff call 0xc1001410 // aes_key_schedule_128
c14d0155: 31 f6 xor %esi,%esi
c14d0157: 39 ee cmp %ebp,%esi
c14d0159: 73 13 jae 0xc14d016e
c14d015b: 8d 14 33 lea (%ebx,%esi,1),%edx
c14d015e: 8d 44 24 20 lea 0x20(%esp),%eax
c14d0162: 89 d1 mov %edx,%ecx
c14d0164: 83 c6 10 add $0x10,%esi
c14d0167: e8 54 16 b3 ff call 0xc10017c0 // aes_decrypt_128
c14d016c: eb e9 jmp 0xc14d0157
c14d016e: a1 7c 74 93 c1 mov 0xc193747c,%eax
果然是 unpack_to_rootfs 被修改了,里面调用了 aes_key_schedule_128 和 aes_decrypt_128 两个函数,加入了 AES128 解密过程,这说明我们提取出来的 initrd 是被加密的。
AES128 是对称加密,如果没有使用加硬件密钥不管,极有可能是硬编码在程序中的,试试提出它。从汇编代码中看,在栈上构建了一个 crypto 上下文,部分内容是从地址 0xc141c6ce 复制过来的,这会不会就是密钥呢?
041d6b2
一个 16 字节的数据,与字符串混编在一起,极有可能是 128 位的密钥。
2.4. 解密 initrd
-128-ecb -
经常一些尝试,使用 AES-128-ECB 解密成功,还原出了 initrd.img,实际为 cpio 格式。
2.5. 修改 initrd
cd rootfs
2.6. 写回 initrd
从 unpack_to_rootfs 汇编代码可以看出,aes_decrypt 将明文写到了对应密文的相同内存空间,我们可以修改代码来跳过解密过程,这样就可以直接把 initrd-noverify.img 写回到 vmlinux 中,而不需要再加密来找麻烦了。
55
程序地址 0xc14d0155 处的代码修改为 jmp 0xc14d016e
,这样就可以直接跳过 aes_decrypt 过程了。如果不懂 x86 的 instruction encoding 方法,有个简单的方法可以产生正确的 jmp 0xc14d016e
编码。
.text
.globl _start
_start:
jmp 0xc14d016e
gcc -m32 -o jmp -nostdlib -Wl,-Ttext=0xc14d0155 jmp.S
objdump -d jmp
c14d0155 <_start>:c14d0155: e9 14 00 00 00 jmp c14d016e <_start>
现在可以写回 initrd-noverify.img 了
if=initrd-noverify.img
还需要编辑 vmlinux 文件 0x8e7f70 偏移处的 __initramfs_size,改为 initrd-noverify.img 的长度。
2.7. 写回 vmlinux
extract-vmlinux 脚本也是通过搜索 magic number 和尝试解压来提取 vmlinux 的,以此就可以获得 vmlinux 在 bzImage 中的偏移,参考写回 initrd 类似的方法,可以将 vmlinux 写回至 bzImage 原来的偏移位置。
不幸的是,修改后的 bzImage 无法启动,启动参数加入 earlyprintk=ttyS0,115200
后,可以看到 LZMA data is corrupt
的错误。
bzImage 的解压缩过程是从 input addr 读取压缩数据,解压后写到 output addr,而 output addr 与 input addr 之间的空间是不够完整存放解压后的数据的,之所以没有问题是因为这个 input addr 与 output addr 之间的差值是经过精心计算的,能够保证覆盖发生时,被覆盖的数据已完成解压。
正是因为这个原因,我们修改后的 vmlinux 由于内容变化,压缩比也发现了变化,已不能适应之前的比例计算出的差值,有两种方法解决这个问题:1. 将 output addr 向低地址方向移动。2. 将 input addr 向高地址方向移动。
从反汇编 bzImage 的代码来看,output addr 在之后还有校验,相对修改 input addr更为复杂。
objdump -D -b binary -mi386 bzImage
...
5d91d8: 8d ab 00 10 bf ff lea -0x337000(%ebx),%ebp
5d91de: 55 push %ebp // output address
5d91df: 68 f5 d1 4f 00 push $0x5d5747 // input length
5d91e4: 8d 83 62 00 00 00 lea 0x62(%ebx),%eax // input address
5d91ea: 50 push %eax
5d91eb: 8d 83 40 7b 5d 00 lea 0x5d7b40(%ebx),%eax // heap
5d91f1: 50 push %eax
5d91f2: 56 push %esi // rmode
5d91f3: e8 14 04 00 00 call 0x5d960c
5d91f8: 83 c4 14 add $0x14,%esp
5d91fb: 31 db xor %ebx,%ebx
5d91fd: ff e5 jmp *%ebp
...
本文转载自作者的博客,欢迎移步本文文末左下方点击 “”。
扫 码 关 注 我 们
再 + 好 友 tinylab
进 泰 晓 技 术 群
泰 晓 科 技
关注“泰晓科技”!点“在看”