HEV

Linux recovery移除签名校验

· hev

背景

某个设备配套的刷机程序是个Linux recovery kernel,刷机过程会先从U盘加载刷机脚本,仅在签名校验通过后才执行脚本。本文记录了分析和移除签名校验的方法。

分析

刷机程序是一个bzImage文件,从启动的输出来看,内部包含了一个initrd,在initrd中实现了读取U盘中的脚本和签名校验过程。

查看initrd内容

通过增加启动参数(cmdline)rdinit=/bin/sh,可以使Kernel启动后执行/bin/sh,而不是默认的/init程序,有了命令行接口后,就可以查看initrd的内容。

~ # busybox find /
/
/.ash_history
/init
/etc
/etc/shadow
/etc/passwd
/.gnupg
/.gnupg/trustdb.gpg
/.gnupg/secring.gpg
/.gnupg/pubring.gpg~
/.gnupg/pubring.gpg
/bin
/bin/kexec
/bin/gpg2
/bin/busybox
/bin/dd
/bin/umount
/bin/sleep
/bin/rmdir
/bin/rm
/bin/reboot
/bin/mount
/bin/mkdir
/bin/ls
/bin/cat
/bin/sh
/mnt
/sys
/proc
/dev
/dev/pts
/dev/loop0
/dev/tty0
/dev/console
# cat /init
...
gpg2 --ignore-time-conflict --ignore-valid-from --verify $FLASH_FILE_SIG $FLASH_FILE
if [ $? -eq 0 ]; then
    echo "PWR_LED 3" > /proc/BOARD_io
    /bin/busybox sh $FLASH_FILE
    if [ $? -eq 0 ]; then
        echo "PWR_LED 1" > /proc/BOARD_io
        echo "flash success..."
        echo "Please unplug USB drive and power cycle system"
    else
        echo "PWR_LED 4" > /proc/BOARD_io
        echo "flash failed..."
        echo "Please try again or try another board"
    fi
else
    echo "PWR_LED 4" > /proc/BOARD_io
    echo "flash failed..."
    echo "Script verify failed"
fi
...

从initrd的内容来看,由/init调用gpg2对U盘中的刷机脚本执行签名校验,只有公钥集成在initrd中,没有私钥。

到这一步,我们已经清楚了签名校验的实现方法,并且也能使启动过程进入受控的命令行交互状态,其实已经可以手工操作跳过签名过程来刷机。

修改

每次手工操作的确太麻烦,那就来移除initrd中的签名校验过程吧。

从bzImage的结构来看,要想修改initrd,先要从bzImage中提取出vmlinux,再从vmlinux中提取出initrd。

提取vmlinux

从bzImage中提取vmlinux比较简单,有现成的工具,位于Linux源代码中 scripts/extract-vmlinux

./scripts/extract-vmlinux bzImage > vmlinux

提取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压缩文档,提取出来尝试解压。

dd if=vmlinux of=t.xz bs=$((0x44E048)) skip=1
unxz t.xz
unxz: t.xz: Compressed data is corrupt

唯一的疑似压缩文档解压出错了,这个方法行不通,那就换另外一个方法吧。:)

  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 -S vmlinux
Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        c1000000 001000 376b28 00  AX  0   0 64
  [ 2] .notes            NOTE            c1376b28 377b28 000024 00  AX  0   0  4
  [ 3] __ex_table        PROGBITS        c1376b50 377b50 000c48 00   A  0   0  4
  [ 4] .rodata           PROGBITS        c1378000 379000 100858 00   A  0   0 64
  [ 5] __bug_table       PROGBITS        c1478858 479858 006588 00   A  0   0  1
  [ 6] .pci_fixup        PROGBITS        c147ede0 47fde0 000b38 00   A  0   0  4
  [ 7] __init_rodata     PROGBITS        c147f940 480940 004040 00   A  0   0 64
  [ 8] __param           PROGBITS        c1483980 484980 000960 00   A  0   0  4
  [ 9] __modver          PROGBITS        c14842e0 4852e0 000d20 00  WA  0   0  4
  [10] .data             PROGBITS        c1485000 486000 048d40 00  WA  0   0 4096
  [11] .init.text        PROGBITS        c14ce000 4cf000 025b13 00  AX  0   0  1
  [12] .init.data        PROGBITS        c14f3b40 4f4b40 3f3434 00  WA  0   0 64
  [13] .x86_trampoline   PROGBITS        c18e7000 8e8000 003328 00   A  0   0 4096
  [14] .x86_cpu_dev.init PROGBITS        c18ea328 8eb328 00001c 00   A  0   0  4
  [15] .altinstructions  PROGBITS        c18ea348 8eb348 002dcc 00   A  0   0  4
  [16] .altinstr_replace PROGBITS        c18ed114 8ee114 000bd9 00  AX  0   0  1
  [17] .exit.text        PROGBITS        c18edcf0 8eecf0 0011d0 00  AX  0   0  1
  [18] .data..percpu     PROGBITS        c18ef000 8f0000 00609c 00  WA  0   0 4096
  [19] .smp_locks        PROGBITS        c18f6000 8f7000 004000 00   A  0   0  4
  [20] .bss              NOBITS          c18fa000 8fb000 04a000 00  WA  0   0 4096
  [21] .brk              NOBITS          c1944000 8fb000 120000 00  WA  0   0  1
  [22] .shstrtab         STRTAB          00000000 8fb000 0000e8 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

程序地址0xc151596c和0xc18e6f70都隶属于.init.data section,文件偏移计算:

Offset = Addr - Section Base Addr + Section Base Offset

0xc151596c: 0xc151596c - 0xc14f3b40 + 0x4f4b40 = 0x51696c
0xc18e6f70: 0xc18e6f70 - 0xc14f3b40 + 0x4f4b40 = 0x8e7f70
  1. 提取initrd

提取initrd,首先需要知道__initramfs_size,该值位于vmlinux文件的0x8e7f70偏移处,类型是unsigned long。

08e7f70 1600 003d 0000 0000 0000 0000 0000 0000
dd if=vmlinux of=initrd bs=$((0x51696c)) skip=1
truncate -s $((0x3d1600)) initrd
  1. 分析initrd格式 虽然提取出了initrd,但不是已知的格式,内核支持的格式都有确定的magic number:
static const struct compress_format {
	unsigned char magic[2];
	const char *name;
	decompress_fn decompressor;
} compressed_formats[] = {
	{ {037, 0213}, "gzip", gunzip },
	{ {037, 0236}, "gzip", gunzip },
	{ {0x42, 0x5a}, "bzip2", bunzip2 },
	{ {0x5d, 0x00}, "lzma", unlzma },
	{ {0xfd, 0x37}, "xz", unxz },
	{ {0x89, 0x4c}, "lzo", unlzo },
	{ {0, 0}, NULL, NULL }
};

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 65 6D  1C 58 72 35  04 A4 0E DD  53 C5 CC D2  B2 4E 00 69  6E 69 74 2F  69 6E 69 74 em.Xr5....S....N.init/init

一个16字节的数据,与字符串混编在一起,极有可能是128位的密钥。

  1. 解密initrd
openssl enc -d -aes-128-ecb -in initrd -out initrd.img -K 656D1C58723504A40EDD53C5CCD2B24E

经常一些尝试,使用AES-128-ECB解密成功,还原出了initrd.img,实际为cpio格式。

  1. 修改initrd
mkdir rootfs && cd rootfs
cpio -id < ../initrd.img

rm -rf .gnupg bin/gpg2
vim init # Remove gpg2 verify

find . | cpio -H newc -o > ../initrd-noverify.img
  1. 写回initrd

从unpack_to_rootfs汇编代码可以看出,aes_decrypt将明文写到了对应密文的相同内存空间,我们可以修改代码来跳过解密过程,这样就可以直接把initrd-noverify.img写回到vmlinux中,而不需要再加密来找麻烦了。

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:	e9 14 00 00 00        	jmp    0xc14d016e
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

程序地址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+0x19>

现在可以写回initrd-noverify.img了

dd if=initrd-noverify.img of=vmlinux conv=notrunc bs=$((0x51696c)) seek=1

还需要编辑vmlinux文件0x8e7f70偏移处的__initramfs_size,改为initrd-noverify.img的长度。

  1. 写回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
...