We are given x86 and arm virtual machine images. x86 is more familiar - let's start with it:
x86$ sudo apt install qemu-system
x86$ sudo ./run.sh
[ 0.000000] Linux version 5.1.16 (user@host) (gcc version 7.4.0 (Buildroot 2019.05.1-gb18f532-dirty)) #1 SMP Mon Sep 2 14:14:04 EEST 2019
Welcome
none login: root
#
Nice, we can login as root
without a password - no need to hack the image.
Ditto the arm one:
arm$ sudo ./run.sh
Linux version 5.1.16 (user@host) (gcc version 7.4.0 (Buildroot 2019.05.1-gaed32c5-dirty)) #1 Mon Sep 2 14:36:22 EEST 2019
Welcome
none login: root
#
That took a while compared to x86 (no KVM, TCG all the way), but the image seems to work as well. Now let's implement the module - quick googling gives us an inspiration.
Instead of manually messing with cross-compilers, let's just take buildroot:
FatModule$ git clone [email protected]:buildroot/buildroot.git
buildroot$ git worktree add ../buildroot-x86 2019.05.1
buildroot$ cp ../x86/linux_x86_config ../buildroot-x86
buildroot$ git worktree add ../buildroot-arm 2019.05.1
buildroot$ cp ../arm/linux_arm_config ../buildroot-arm
Let's build the x86 one:
buildroot-x86$ make menuconfig
BR2_x86_64=y
BR2_LINUX_KERNEL=y
BR2_LINUX_KERNEL_CUSTOM_VERSION=y
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="5.1.16"
BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG=y
BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="linux_x86_config"
buildroot-x86$ make -j$(getconf _NPROCESSORS_ONLN)
and the arm one:
buildroot-arm$ make menuconfig
BR2_arm=y
BR2_LINUX_KERNEL=y
BR2_LINUX_KERNEL_CUSTOM_VERSION=y
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="5.1.16"
BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG=y
BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="linux_arm_config"
buildroot-arm$ make -j$(getconf _NPROCESSORS_ONLN)
This is going to take a while. In the meantime, let's try reversing the kernel. First, extract and uncompress vmlinux (ELF image) from bzImage (bootable image). There is a script for this in the kernel source tree:
x86$ ./extract-vmlinux bzImage >vmlinux
vmlinux can be loaded into IDA. Module-related stuff lives in
kernel/module.c
- in particular, we are interested in load_module()
and
elf_header_check()
. Let's locate the latter by looking for comparisons with
the ELF signature \x7FELF
:
IDA$ Alt+B
Binary Search$ 7F 45 4C 46
There are about 10 entries, but only one is close to the beginning of a function. Let's reverse it a little bit:
.text:FFFFFFFF810DFF40 load_module proc near
...
.text:FFFFFFFF810DFF66 cmp [rdi+load_info.len], (size Elf_Ehdr) - 1
...
.text:FFFFFFFF810DFF7C jbe return_enoexec
This is supposed to be
if (info->len < sizeof(*(info->hdr)))
return -ENOEXEC;
but load_info.len
is at offset 0x20
, while it should be at 0x18
:
struct load_info {
const char *name;
/* pointer to module in temporary copy, freed at end of load_module() */
struct module *mod;
Elf_Ehdr *hdr;
unsigned long len;
Is there a new field? We'll see.
.text:FFFFFFFF810DFF82 cmp [r14+Elf_Ehdr.e_machine], EM_PWNTHYBYTES
.text:FFFFFFFF810DFF89 jz em_is_pwnthybytes
.text:FFFFFFFF810DFF8F again:
Hey, new machine type 0xE6
! It does not match any of the existing ones. How is it handled? Let's find out:
.text:FFFFFFFF810E0154 em_is_pwnthybytes:
.text:FFFFFFFF810E0154 lea rax, [r14+(size Elf_Ehdr)]
.text:FFFFFFFF810E0158 lea rdx, [r14+180h]
.text:FFFFFFFF810E015F jmp short em_is_pwnthybytes_cont
.text:FFFFFFFF810E0161
.text:FFFFFFFF810E0161 find_0x3e_loop:
.text:FFFFFFFF810E0161 add rax, size pwn_header
.text:FFFFFFFF810E0165 cmp rdx, rax
.text:FFFFFFFF810E0168 jz short return_enoexec
.text:FFFFFFFF810E016A
.text:FFFFFFFF810E016A em_is_pwnthybytes_cont:
.text:FFFFFFFF810E016A cmp [rax+pwn_header.magic], 3Eh ; '>'
.text:FFFFFFFF810E016D jnz short find_0x3e_loop
.text:FFFFFFFF810E016F mov [r15+load_info.pwn_header], r14
.text:FFFFFFFF810E0173 add r14, [rax+pwn_header.elf_offset]
.text:FFFFFFFF810E0177 mov [r15+load_info.hdr], r14
.text:FFFFFFFF810E017B mov rax, [rax+pwn_header.elf_size]
.text:FFFFFFFF810E017F mov [r15+load_info.len], rax
.text:FFFFFFFF810E0183 jmp again
So, between offsets 0x40
(right after the normal ELF header) and 0x180
we
now have 16 entries with the following format:
00000000 pwn_header struc
00000000 magic dd ?
00000004 elf_offset dq ?
0000000C elf_size dq ?
00000014 pwn_header ends
The loop tries to find the array entry with the magic value and saves the
pointer to it into the new load_info.pwn_header
field.
In x86 kernel, magic
appears to be 0x3E
, which corresponds to the existing
EM_X86_64
. No obfuscation - long live best coding practices! Keep Linus happy!
Also let's pray that ARM counterpart is simply EM_ARM
so that we wouldn't have
to reverse the ARM kernel.
The rest of the module loading flow appears to be unmodified. So we need to build and concatenate x86 and arm versions of the module, prepend ELF and PWN headers, nd the result should just work (TM).
The kernels have finished building, let's compile the stolen code:
FatModule$ cat >Makefile
obj-m += mod.o
x86:
make -C $(PWD)/buildroot-x86/output/build/linux-5.1.16 M=$(PWD)
arm:
PATH=$(PWD)/buildroot-arm/output/host/bin:$$PATH make -C $(PWD)/buildroot-arm/output/build/linux-5.1.16 ARCH=arm CROSS_COMPILE=arm-buildroot-linux-uclibcgnueabi- M=$(PWD)
^D
FatModule$ make x86
FatModule$ mv mod.ko mod-x86.ko
FatModule$ make arm
FatModule$ mv mod.ko mod-arm.ko
Squash 'em:
FatModule$ cat >script
#!/usr/bin/env python3
import struct
x86_blob = open('mod-x86.ko', 'rb').read()
arm_blob = open('mod-arm.ko', 'rb').read()
master = b'\x7FELF' + b'\x00' * 14 + b'\xE6\x00' + b'\x00' * 0x2c
x86 = struct.pack('<IQQ', 0x3e, 0x180, len(x86_blob))
arm = struct.pack('<IQQ', 0x28, 0x180 + len(x86_blob), len(arm_blob))
others = b'\0' * (0x14 * 14)
open('fat.ko', 'wb').write(master + x86 + arm + others + x86_blob + arm_blob)
^D
FatModule$ python3 script
Let's test whether this works... Oh, wait, how to copy this file into guests? We'll need to configure the networking and use py3tftp, which is great because it requires zero configuration:
FatModule$ sudo tunctl -u $USER -t tap0
FatModule$ sudo ifconfig tap0 192.168.100.1 up
FatModule$ py3tftp &
2019-10-02 22:37:52,677 [INFO] Starting TFTP server on 0.0.0.0:9069
Now test on x86:
FatModule$ vim x86/run.sh
Append
-netdev tap,id=mynet0,ifname=tap0,script=no,downscript=no -device e1000,netdev=mynet0,mac=52:55:00:d1:55:01
to qemu-system-x86_64 invocation.
FatModule$ cd x86
x86$ sudo ./run.sh
# ifconfig eth0 192.168.100.2 up
# tftp 192.168.100.1 9069 -gr fat.ko
# insmod fat.ko
# mknod foo c 400 0
# cat foo
Hello, World!
Cool! ARM works too, so our guess regarding EM_ARM
was right. Let's submit:
FatModule$ cat >submit
#!/usr/bin/env python3
import struct
import sys
f=open('fat.ko', 'rb').read()
sys.stdout.buffer.write(struct.pack('<I', len(f)))
sys.stdout.buffer.write(f)
^D
FatModule$ python3 submit | nc 137.117.216.128 13376
And the flag is ours!