Linux Kernel Building & Exploit Preparation
TyeYeah Lv4

Linux Kernel Build

To learn kernel, we should know how to compile it and how it works.

Source code can be found online and all history versions are on there to be downloaded. You can also view source code online on bootlin

Just Compile

First install some essential tools:

1
2
$ sudo apt-get update
$ sudo apt-get install git gcc fakeroot build-essential ncurses-dev xz-utils libssl-dev bc libncurses-dev flex bison

Then you can build the kernel. If the kernel is too old, some dependencies will not be satisfied.

1
2
3
4
5
6
7
8
9
10
$ make menuconfig   
# if you want to build one available to debug,
# choose 'KernelHacking --> Compile-time checks and compiler options ---> Compile the kernel with debug info'
# disbale KASLR by unselecting 'Processor type and features -> Randomize the address of the kernel image (KASLR)'
# actually different version has different path to select the items
# and some other config items
$ make # 'make -j4' to be faster
$ make all # build all
$ make bzImage # build bzImage
$ make modules_prepare #

After that we can get bzImage form linux-x.x.x/arch/x86/boot/bzImage, and get vmlinux from linux-x.x.x root directory.

As for differences between vmLinux, vmlinuz, vmlinux.bin, zImage & bzImage

  • vmlinux
    This is the Linux kernel in an statically linked executable file format. Generally, you don’t have to worry about this file, it’s just a intermediate step in the boot procedure.
    The raw vmlinux file may be useful for debugging purposes.
  • vmlinux.bin
    The same as vmlinux, but in a bootable raw binary file format. All symbols and relocation information is discarded. Generated from vmlinux by objcopy -O binary vmlinux vmlinux.bin.
  • vmlinuz
    The vmlinux file usually gets compressed with zlib. Since 2.6.30 LZMA and bzip2 are also available. By adding further boot and decompression capabilities to vmlinuz, the image can be used to boot a system with the vmlinux kernel. The compression of vmlinux can occur with zImage or bzImage.
    The function decompress_kernel() handles the decompression of vmlinuz at bootup, a message indicates this:
    1
    2
    Decompressing Linux... done
    Booting the kernel.
  • zImage (make zImage)
    This is the old format for small kernels (compressed, below 512KB). At boot, this image gets loaded low in memory (the first 640KB of the RAM).
  • bzImage (make bzImage)
    The big zImage (this has nothing to do with bzip2), was created while the kernel grew and handles bigger images (compressed, over 512KB). The image gets loaded high in memory (above 1MB RAM). As today’s kernels are way over 512KB, this is usually the preferred way.

More in Here

Busybox and QEMU

Actually to run the customized (built) kernel, we need QEMU, while QEMU needs a filesystem image to run, so we first build one image using busybox.

Download source on the official site, and comiple it with setting Build static binary (no shared libs) (also check (./_install) Destination path for 'make install' for not installing to /usr)

1
2
$ make menuconfig
$ make install -j4

If you want to cross compile like using arm-linux-gcc, edit Makefile

1
2
3
4
5
ARCH ?= $(SUBARCH)
CROSS_COMPILE ?=
# to be
ARM ?= arm
CROSS_COMPILE ?= arm-linux-

After that things are generated in _install directory, do some extra operations

1
2
3
4
$ cd _install
$ mkdir proc sys dev etc etc/init.d
$ vim etc/init.d/rcS
$ chmod +x etc/init.d/rcS

And in etc/init.d/rcS

1
2
3
4
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
/sbin/mdev -s

Finally we build the file system

1
$ find . | cpio -o --format=newc > ../rootfs.img

Then run it by QEMU

1
2
3
4
5
6
7
$ qemu-system-x86_64 \
-kernel /path/to/linux-x.x.x/arch/x86/boot/bzImage \
-initrd /path/to/busybox-x.x.x/rootfs.img \
-append "console=ttyS0 root=/dev/ram rdinit=/sbin/init" \
-cpu kvm64,+smep,+smap \
-nographic \
-gdb tcp::1234

So now you run your os on your own kernel, but all scripts and commands above are just samples, there are more config modifications later.

Add Customized Functions

For example, if we want to implement a personal syscall, First we need to put things in a directory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# in root directory of linux kernel
$ cd helloworld/
$ tree
.
├── helloworld.c
└── Makefile

0 directories, 2 files

$ cat helloworld.c
#include <linux/kernel.h>
asmlinkage long sys_helloworld(void){
printk("hello world\n");
return 0;
}

$ cat Makefile
obj-y=helloworld.o

Then edit the Makefile (1 line) to include target directory

1
core-y      += x/ xx/ xxx/ ... helloworld/

Edit include/linux/syscalls.h to add function prototype to the tail

1
asmlinkage long sys_helloworld(void);

Add customized syscall number

1
2
3
4
// arch/x86/entry/syscalls/syscall_32.tbl, for i386
1000 i386 helloworld sys_helloworld
// arch/x86/entry/syscalls/syscall_64.tbl, for amd64
1000 common helloworld sys_helloworld

Then re-compile the kernel, and write a demo to test:

1
2
3
4
5
6
7
8
9
10
11
// compiled: gcc helloworld.c -o helloworld
#include <stdio.h>
#include <unistd.h>

int main()
{
puts("start");
syscall(1000);
puts("end");
return 0;
}

Test:

1
2
3
4
5
6
7
$ id
uid=0 gid=0
$ ./helloworld
start
[ 12.345678] hello world
end
$

Compile Driver

As for driver module, it is apart from linux kernel compilation.
First we also need a driver program written in C like hello_lkm.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<linux/init.h>
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/cred.h>

struct cred c;
static int hello_lkm_init(void)
{
printk("hello lkm!\n");
printk("size of cred : %d \n",sizeof(c));
return 0;
}
static void hello_lkm_exit(void)
{
printk("Bye, lkm\n");
}
module_init(hello_lkm_init);
module_exit(hello_lkm_exit);

And its Makefile specifies module name, linux kernel path, and intermediate object name:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
obj-m := hello_lkm.o

KERNELDR := ../

PWD := $(shell pwd)

modules:
$(MAKE) -C $(KERNELDR) M=$(PWD) modules

moduels_install:
$(MAKE) -C $(KERNELDR) M=$(PWD) modules_install

clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions

We put them in one directory, and make to get one hello_lkm.ko under the current dir.

To install this module, we can pack it into filesystem image, and add commands in init to install it:

1
2
3
4
insmod ./hello_lkm.ko
# if the driver is for 'character device' or sth, also add
# `mknod /dev/kmod c 100 0`
# 100 is the primary device number

Actually this driver reacts when you just when you initialize and exit it, so we can see the results when:

1
2
3
4
5
6
7
8
9
10
$ insmod hello_lkm.ko
[ 20.247241] hello_lkm: loading out-of-tree module taints kernel.
[ 20.247512] hello_lkm: module license 'unspecified' taints kernel.
[ 20.247894] Disabling lock debugging due to kernel taint
[ 20.252195] hello lkm!
[ 20.252373] size of cred : 168
$ lsmod
hello_lkm 16384 0 - Live 0xffffffffc030b000 (PO)
$ rmmod hello_lkm.ko
[ 59.195751] Bye, lkm

In gdb we can load it and set breakpoints to debug step by step:

1
2
3
4
5
6
7
pwndbg> add-symbol-file /path/to/linux.x.x.x/hello_lkm/hello_lkm.ko 0xffffffffc030b000
add symbol table from file "/path/to/linux.x.x.x/hello_lkm/hello_lkm.ko" at
.text_addr = 0xffffffffc030b000
Reading symbols from /path/to/linux.x.x.x/hello_lkm/hello_lkm.ko...done.

pwndbg> b hello_lkm_exit
Breakpoint1 at 0xffffffffc030b020: file /path/to/linux.x.x.x/hello_lkm/hello_lkm.c, line 25.

Linux Kernel Exploit

Normally the pwn challenges give us xxx.ko, bzImage, initramfs.img, and start.sh

  • xxx.ko is the module with bugs, can be analyzed by IDA
  • bzImage is the packed kernel, and we can use extract-vmlinux to extract vmlinux from it. vmlinux can be loaded into gdb to debug.
    After that we use objdump -d vmlinux > gadget to save gadgets and Ctrl+f to search, or we can use ropper and ROPgadget.
  • initramfs.img is the filesystem image.
    When building image using busybox, we
    1
    2
    3
    4
    # produce `rootfs.img` 
    $ find . | cpio -o --format=newc > ../rootfs.img
    # extract image
    $ cpio -idmv <rootfs.img
    however initramfs.img is always compressed, so we
    1
    2
    3
    4
    5
    6
    7
    8
    # make initramfs.img 
    $ find . | cpio -o -H newc | gzip > /boot/initramfs.img.gz
    $ mv initramfs.img.gz initramfs.img
    # and extract
    $ file initramfs.img
    $ mv initramfs.img initramfs.img.gz # or 'initramfs.img.xz' according to file type
    $ gunzip initramfs.img.gz # or 'xz -d initramfs.img.xz'
    $ cpio -idmv < ./initramfs.img
  • start.sh is the script to run QEMU system mode.

Security Mechanism

Some challenges run on environment with security mechanism (reference), so we normally disable them to obtain more debug info.

Like in init in extracted initramfs.img, we can add commands to remove kptr_restrict, dmesg_restrict and MMAP_MIN_ADDR

1
2
3
4
5
6
7
8
9
echo 0 > /proc/sys/kernel/dmesg_restrict
# 0: no restrictions
# 1: users must have CAP_SYSLOG to use dmesg and see 'printk()' results
echo 0 > /proc/sys/kernel/kptr_restrict
# 0: print using '%p' in 'seq_printf()' of 'kallsyms.c'
# 1: print 0 unless having CAP_SYSLOG
# 2: kernel pointers printed using '%pK' will be replaced with 0's regardless of privileges
setsid /bin/cttyhack setuidgid 0000 /bin/sh
# give root privilege to normal user

And when stopping KASLR, SMEP and SMAP, we modify QEMU running parameters (add nokaslr)

1
2
-append "console=ttyS0 nokaslr ..." # replace kaslr with nokaslr
-cpu kvm64,... # remove '+smep,+smap'

As for NX and Canary(stack protector), these can be done in compilation.

Debug

Using GDBStub (start gdbserver port locally) of QEMU to debug, we add parameter

1
2
3
-s: open port 1234
-S: waiting for debugger attachment
-gdb tcp::2345

And a script to quickly connect

1
2
3
4
#!/bin/sh
gdb \
-ex "target remote localhost:1234" \
-ex "add-symbol-file ./xxx.ko 0xdeadbeef" \

And some key files & commands to get info:

  • cat /proc/modules: see loaded kernel modules
  • cat /proc/kallsyms: see all kernel symbols
  • cat /sys/module/xxx/sections: list sections of module xxx
  • dmesg: see kernel logs, where printk() prints
  • cat /proc/kallsyms |grep commit_creds & cat /proc/kallsyms |grep prepare_kernel_cred: get the two funcs addrs

Script

Here are scripts for packing and unpacking:
unpack.py

1
2
3
4
5
6
7
#!/bin/sh
mkdir unzip
cp rootfs.cpio.gz ./unzip/
cd unzip
gunzip rootfs.cpio.gz
cpio -idmv <rootfs.cpio
rm -rf rootfs.cpio

repack.py

1
2
3
#!/bin/sh
cd unzip
find . | cpio -ov -H newc | gzip -9 > ../rootfs2.cpio.gz

If we do experiments locally, we can pack the exploit binary into filesystem image, and see it in the system when booting, while when interacting with remote, we cannot alternate remote filesystem.

Therefore we copy exploit binary to remote via interaction shell, and here is a script to facilitate it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# -*- coding: utf-8 -*-
from pwn import *
import os
# context.log_level = 'debug'
cmd = '$ '

def exploit(r):
r.sendlineafter(cmd, 'stty -echo')
os.system('musl-gcc -static -O2 ./poc/exp.c -o ./poc/exp')
os.system('gzip -c ./poc/exp > ./poc/exp.gz')
r.sendlineafter(cmd, 'cat <<EOF > exp.gz.b64')
r.sendline((read('./poc/exp.gz')).encode('base64'))
r.sendline('EOF')
r.sendlineafter(cmd, 'base64 -d exp.gz.b64 > exp.gz')
r.sendlineafter(cmd, 'gunzip ./exp.gz')
r.sendlineafter(cmd, 'chmod +x ./exp')
#r.sendlineafter(cmd, './exp')
r.interactive()

p = process('./run.sh', shell=True)
# p = remote('', )

exploit(p)
Powered by Hexo & Theme Keep
Total words 135.7k