两种方法调试 linux kernel

这篇文章的目的,是构造一个linux内核调试环境的壳,之后只需替换内核,就能完成对不同版本内核的调试。文中涉及编译linux内核、编译busybox生成文件系统、编译内核字符设备ko、以及两种内核调试方法。

本文中用到的环境如下:

  • x86_64架构ubuntu16.04虚拟机
  • gcc版本5.4.0

编译Linux Kernel

说明

编译内核,简单来说包含两个步骤:

(1)配置内核选项

(2)用配置的选项编内核

配置内核选项的命令 有若干个,我们应当选择哪个呢?

make configs:基于文本形式的配置,每次弹出一个选项,每个选项都需要手动确认。如果想更改前序配置,只能从头再来。

make menuconfig:基于图形界面的配置,可以在界面不同选项中来回切换,可以搜索某个选项,可以加载.config配置文件。它的GUI界面使用了ncurses库,在Ubuntu中可使用sudo apt install libncurses5-dev安装。

make defconfig:根据ARCH指定的架构,用默认选项生成.config配置文件。默认的配置存在arch/$(ARCH)/configs目录下。

make oldconfig:读取已存在的.config文件,并提示用户当前kernel的哪些选项在.config文件中找不到。

make savedefconfig:在当前目录下生成一个defconfig文件

例子

  1. 安装内核编译过程中可能要用到的包

    1
    sudo apt-get install git fakeroot build-essential ncurses-dev xz-utils libssl-dev bc flex libelf-dev bison
  2. 下载linux-4.4.72.tar.gz源码包到本地解压、设置、编译

    1
    2
    3
    4
    5
    tar -zxf linux-4.4.72.tar.gz
    cd linux-4.4.72
    make ARCH=x86 defconfig
    make menuconfig # 若无需更改默认配置,这条可省略。
    make -j4

    配置项参考Linux内核调试,设置如下几个关键选项:

    1
    2
    3
    4
    由于我们需要调试内核,注意下面这几项一定要配置好:
    KernelHacking --> Compile-time checks and compiler options
    选中Compile the kernel with debug info
    选中Compile the kernel with frame pointers
  3. 编译完成后,即可在当前目录下看到vmlinux,在arch/x86/boot目录下看到bzImage

编译busybox

通常利用busybox构建文件系统,有两种方式设置启动选项:

  1. /init文件:[内核pwn] 环境搭建
  2. /etc/init.d/rcS文件:从零开始的 kernel pwn 入门 - I:Linux kernel 简易食用指南

这里以第一种方式为例,通过以下几个步骤就能构造一个可以使用的文件系统:

  1. 下载 busybox-1.30.0.tar.bz2源码包。

  2. 以root用户,运行如下命令,解压、设置、编译。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    su root
    tar -jxf busybox-1.30.0.tar.bz2
    make menuconfig
    # 进Settings,勾上Build static binary (no shared libs),编译成静态文件
    # 关闭下面两个选项:
    # Linux System Utilities -> [] Support mounting NFS file system 网络文件系统
    # Networking Utilities -> [] inetd (Internet超级服务器)
    make install -j 4
    # 高版本gcc编译时报错:date.c:(.text.date_main+0x25f): undefined reference to `stime'
    # 解决:https://git.busybox.net/busybox/patch/?id=d3539be8f27b8cbfdfee460fe08299158f08bcd9
    # 或者下载新版本的busybox,就不会遇到这个问题
  3. 进入编译后生成的_install目录,里面存放了编译生成的文件,在该目录下创建init文件和需要的文件夹。

    1
    2
    3
    4
    5
    6
    cd _install
    mkdir proc
    mkdir sys
    touch flag
    touch init
    chmod +x init

    init中写入如下内容(或者参考其他ctf题目中的init文件)

    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
    #!/bin/sh

    mkdir /tmp
    mount -t proc none /proc
    mount -t sysfs none /sys
    mount -t devtmpfs devtmpfs /dev
    mount -t tmpfs none /tmp
    mdev -s
    echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
    echo 1 > /proc/sys/vm/unprivileged_userfaultfd

    insmod /xxx.ko
    chmod 666 /dev/xxx
    chmod 740 /flag
    echo 1 > /proc/sys/kernel/kptr_restrict
    echo 1 > /proc/sys/kernel/dmesg_restrict
    chmod 400 /proc/kallsyms

    #poweroff -d 120 -f &
    setsid /bin/cttyhack setuidgid 0 /bin/sh

    umount /proc
    umount /tmp

    poweroff -d 0 -f
  4. 打包生成roofs.cpio

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

编译漏洞ko

新建一个存放ko源码和Makefile的文件夹

1
2
3
4
mkdir testko
cd testko
touch Makefile
touch babydriver.c

Makefile内容如下:

1
2
3
4
5
6
7
KDIR := /home/bling/Documents/linux-4.4.72
obj-m += babydriver.o

all:
make -C $(KDIR) M=$(shell pwd) modules
clean:
make -C $(KDIR) M=$(shell pwd) clean

参考 CISCN2017-babydriver 这个题,有漏洞的babydrvier.c源码如下:

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
#include<linux/init.h>
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/fs.h>
#include<asm/uaccess.h>
#include<linux/io.h>
#include<linux/cdev.h>
#include<linux/slab.h>
#include<linux/device.h>


MODULE_LICENSE("Dual BSD/GPL");

static int test_major = 0;
static int test_minor = 0;

static struct cdev cdev_0;
dev_t babydev_no;
struct class *babydev_class;

struct babydevice{
char* device_buf;
uint64_t device_buf_len;
};

struct babydevice babydev_struct;


int babyrelease(struct inode *inode, struct file *filp)
{
kfree(babydev_struct.device_buf);
printk("device release\n");
return 0;
}

int babyopen(struct inode *inode, struct file *filp)
{
babydev_struct.device_buf = kmalloc(64,0x24000C0);
babydev_struct.device_buf_len = 64;
printk("device open\n");
return 0;
}


static ssize_t babyread(struct file *file, char *buf, size_t count, loff_t *ppos)
{
ssize_t v6;

if ( !babydev_struct.device_buf )
return -1;
if ( babydev_struct.device_buf_len > count )
{
v6 = count;
copy_to_user(buf,babydev_struct.device_buf,v6);
return v6;
}
return -2;
}


static ssize_t babywrite(struct file *file, const char *buf, size_t count, loff_t *ppos)
{
ssize_t result;
ssize_t v6;

if ( !babydev_struct.device_buf )
return -1;
result = -2;
if ( babydev_struct.device_buf_len > count )
{
v6 = count;
copy_from_user(babydev_struct.device_buf,buf,v6);
return v6;
}
return result;
}


long babyioctl(struct file *fd, unsigned int cmd, unsigned long arg)
{
size_t v4;

v4 = arg;
if ( cmd == 65537 )
{
kfree(babydev_struct.device_buf);
babydev_struct.device_buf = kmalloc(v4,0x24000C0);
babydev_struct.device_buf_len = v4;
printk("alloc done\n");
return 0LL;
}
else
{
printk("defalut:arg is %ld",arg);
return -22;
}
}

struct file_operations baby_fops = {
.owner = THIS_MODULE,
.write = babywrite,
.read = babyread,
.unlocked_ioctl = babyioctl,
.open = babyopen,
.release = babyrelease,
};

static int babydriver_init(void)
{
int v0;
int v1;
struct device *v2;

if( alloc_chrdev_region(&babydev_no, 0, 1, "babydev") >= 0 ){ // 申请一个主设备号
cdev_init(&cdev_0, &baby_fops); // 初始化字符设备cdev结构体,并建立其与操作方法集的关系
cdev_0.owner = THIS_MODULE;
test_major = MAJOR(babydev_no);
test_minor = MINOR(babydev_no);
printk("[+] test_major: %d ; test_minor: %d \n",test_major,test_minor);
v1 = cdev_add(&cdev_0, babydev_no, 1); // 将字符设备添加到系统中
if ( v1 >= 0 ){
babydev_class = class_create(THIS_MODULE, "babydev_class"); // 创建一个class结构体
if ( babydev_class ){
v2 = device_create(babydev_class, 0, MKDEV(test_major,0), 0, "babydev"); // 自动创建设备节点
if ( v2 ) return 0;
printk("create device failed");
class_destroy(babydev_class);
}else{
printk("create class failed");
}
cdev_del(&cdev_0);
} else{
printk("cdev init failed");
}
unregister_chrdev_region(babydev_no, 1);
return v1;
}
printk("alloc_chrdev_region failed");
return 1;
}

static void babydriver_exit(void)
{
device_destroy(babydev_class, babydev_no); // 删除设备
class_destroy(babydev_class); // 删除类
cdev_del(&cdev_0); // 删除字符设备
unregister_chrdev_region(babydev_no, 1); // 注销设备号
}

module_init(babydriver_init);
module_exit(babydriver_exit);

编译生成babydriver.ko

1
make

测试运行

_install目录下,准备好漏洞ko和对应的利用程序,exp参考第一道内核pwn-CISCN2017-babydriver。并在/init中添加对驱动ko的设置。最后,重打包生成cpio。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/sh
echo "{==DBG==} INIT SCRIPT"
mkdir /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t debugfs none /sys/kernel/debug
mount -t tmpfs none /tmp
insmod /babydriver.ko # load ko
mdev -s # We need this to find /dev/sda later
chmod 777 /dev/babydev
echo -e "{==DBG==} Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
setsid /bin/cttyhack setuidgid 1000 /bin/sh #normal user
#exec /bin/sh #root

umount /proc
umount /sys/kernel/debug
umount /sys
umount /tmp
poweroff -d 0 -f

启动命令:

1
qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -monitor /dev/null -m 128M --nographic

调试方法

经过上面的步骤,我们有了内核镜像bzImage、文件系统rootfs.cpio、漏洞模块babydriver.ko、以及带调试信息的vmlinux。因此,我们可以方便地对内核进行调试,这里汇总两个我目前了解到的调试方法。一种是kernel pwn常用的gdb调试,可以精确调试每一行汇编内容。另一种是源码调试,常用于开发场景,便于c语言级别的流程跟踪。

二进制调试 - gdb

安装好gdb或gdb插件(peda/gef/pwndbg)后,开启两个窗口,一个窗口运行qemu,另一个窗口运行gdb并加载符号文件。

  1. 窗口1

    1
    qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -monitor /dev/null -m 128M --nographic -S -s
  2. 窗口2

    1
    2
    3
    4
    5
    6
    gdb
    pwndbg> file ./vmlinux
    pwndbg> target remote :1234
    pwndbg> add-symbol-file ./xxx.ko 0xffffffffa0000000
    pwndbg> b start_kernel
    pwndbg> c

源码调试 - vscode

待调试内核文件及编译环境在一台ubuntu中,vscode运行在windows中。为了在windows侧调试ubuntu中的目标,需要先配置好两台电脑的环境。

参考文章推荐:

解决VScode配置远程调试Linux程序的问题

手把手教你利用VS Code+Qemu+GDB调试Linux内核

准备

  1. linux侧安装好gdb、gdbserver以及openssh-server

    1
    2
    3
    4
    sudo apt update
    sudo apt install gdb
    sudo apt install gdbserver
    sudo apt install openssh-server
  2. windows侧安装好vscode及插件

    1
    2
    3
    安装好vscode后安装如下两个插件:
    Remote Development插件
    C/C++插件
  3. 配置vscode使用密钥文件连接ubuntu

    1
    2
    3
    4
    5
    6
    linux侧放入公钥文件至~/.ssh/authorized_keys
    windows侧私钥文件在C:\Users\xxx\.ssh\id_rsa

    windows的vscode中ssh bling@192.168.133.155,设置好config后,刷新SSH,选中目标ip连接到远程

    给远程安装C/C++插件,然后就可以打开远程待调试的文件夹

配置

打开待调试的文件夹后,在文件夹下新建.vscode/launch.json,并填入如下内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"version": "0.2.0",
"configurations": [
{
"name": "linux4.4.72-debug",
"type": "cppdbg",
"request": "launch",
"miDebuggerServerAddress": "127.0.0.1:1234",
"program": "${workspaceFolder}/vmlinux",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"logging": {
"engineLogging": false
},
"MIMode": "gdb",
}
]
}

然后linux侧让qemu以调试模式运行并处于等待,windows侧vscode中设置断点后F5连接过去,即可开始源码调试。

1
2
3
4
# linux侧
qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -monitor /dev/null -m 128M --nographic -S -s

# windows侧,在代码中下好断点,F5开始调试

问题

出现错误:ubuntu16.04上的qemu版本太低,导致gdb调试出现Remote 'g' packet reply is too long: ...的问题。

解决方法

  1. 在ubuntu20.04中编译静态qemu

    1
    2
    3
    4
    5
    6
    7
    8
    sudo apt install libpixman-1-dev
    wget https://download.qemu.org/qemu-4.2.0.tar.xz
    tar xJf qemu-4.2.0.tar.xz
    cd qemu-4.2.0/
    mkdir build
    cd build
    ./configure --static # 指定静态编译
    make -j4 # 默认编译所有架构

    如果要编译某个特定架构的,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    ./configure --help | grep "target-list=LIST" -A20		# 查看支持哪些架构
    # --target-list=LIST set target list (default: build everything)
    # Available targets: aarch64-softmmu alpha-softmmu
    # arm-softmmu cris-softmmu hppa-softmmu i386-softmmu
    # lm32-softmmu m68k-softmmu microblaze-softmmu
    # microblazeel-softmmu mips-softmmu mips64-softmmu
    # mips64el-softmmu mipsel-softmmu moxie-softmmu
    # nios2-softmmu or1k-softmmu ppc-softmmu ppc64-softmmu
    # riscv32-softmmu riscv64-softmmu s390x-softmmu
    # sh4-softmmu sh4eb-softmmu sparc-softmmu
    # sparc64-softmmu tricore-softmmu unicore32-softmmu
    # x86_64-softmmu xtensa-softmmu xtensaeb-softmmu
    # aarch64-linux-user aarch64_be-linux-user
    # alpha-linux-user arm-linux-user armeb-linux-user
    # cris-linux-user hppa-linux-user i386-linux-user
    # m68k-linux-user microblaze-linux-user
    # microblazeel-linux-user mips-linux-user
    # mips64-linux-user mips64el-linux-user
    # mipsel-linux-user mipsn32-linux-user
    # mipsn32el-linux-user nios2-linux-user
    # or1k-linux-user ppc-linux-user ppc64-linux-user
    # ppc64abi32-linux-user ppc64le-linux-user
    ./configure --target-list=x86_64-softmmu --static # 假如仅编译x86_64架构的
    make -j4
  2. 拷贝对应的文件和文件夹到ubuntu16.04中

    1
    2
    qemu-4.2.0/build/x86_64-softmmu/qemu-system-x86_64	# 文件
    qemu-4.2.0/pc-bios # 整个文件夹
  3. 在ubuntu16.04中使用静态编译的4.2.0版本qemu运行kernel

    1
    2
    ./qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -monitor /dev/null -m 64M --nographic -L ./pc-bios/ -S -s
    # 通过-L指定pc-bios目录,使用其中的bios-256k.bin等文件启动系统