【漏洞挖掘】使用Syzkaller&QEMU捕捉内核堆溢出Demo

参考:

        使用Syzkaller&QEMU捕捉内核堆溢出Demo   

        内核漏洞挖掘技术系列(4)——syzkaller(1) 

        实验所需源码

本文主要参考第一篇文章,但是文章中很多步骤不全面,而且存在错误,故记录本实验过程。本实验前提是参照配置syzkaller&QEMU环境配置好syzkaller环境。

本文将演示使用Syzkaller联合QEMU触发一个内核溢出的bug。 包括三个步骤:

        (1)在syzkaller中添加规则触发堆溢出

        (2)编译一个带有堆溢出模块的kernel

        (3)运行syzkaller&QEMU捕捉内核堆溢出

1.在syzkaller中添加规则

本例新增调用模板见proc_operation.txt,/syzkaller/sys/linux目录下的sys.txt中有通用的调用形式可以参考。

解释:syzkaller使用它自己的声明式语言来描述系统调用模板,docs目录下的syscall_descriptions.md中可以找到相关的说明。这些系统调用模板被翻译成syzkaller使用的代码需要经过两个步骤。第一步是使用syz-extract从linux源代码中提取符号常量的值,结果被存储在.const文件中,例如/sys/linux/tty.txt被转换为sys/linux/tty_amd64.const。第二步是根据系统调用模板和第一步中生成的const文件使用syz-sysgen生成syzkaller用的go代码。可以在/sys/linux/gen/amd64.go和/executor/syscalls.h中看到结果。最后,重新编译生成带有相应规则的syzkaller二进制可执行文件。

调用规则:$号前的syscallname是系统调用名,$号后的type是指特定类型的系统调用。如上文的 open$proc 指的就是open这个类调用中proc这个具体的调用,这个名字是由规则编写者确定的,具体行为靠的是后面的参数去确定。 参数的格式如下: ArgumentName ArgumentType[Limit] ArgumentName是指参数名,ArgumentType指的是参数类型,例如上述例子有string、flags等类型。[ ]号中的内容就是具体的类型的值,不指定的时候由syzkaller自动生成,若要指定须在后文指定,以上文为例:

            mode flags[proc_open_mode]

            proc_open_mode = ...

        因为我们给的例子是通过/proc/test这个内核接口的写操作来触发堆溢出,因此我们需要控制的参数是open函数中的file参数为“/proc/test”即可,其他操作参考sys.txt即可。

步骤

    (1)编译生成syz-extract

         ~/Desktop/ctf/kernel_fuzz/gopath/src/github.com/google/syzkaller$ make bin/syz-extract

    (2)编译生成syz-sysgen

        ~/Desktop/ctf/kernel_fuzz/gopath/src/github.com/google/syzkaller$ make bin/syz-sysgen

    (3)用syz-extract生成.const文件(本例proc_operation.txt有错误,read和write不需要返回值)可选择只生成你新增的.txt

        ~/Desktop/ctf/kernel_fuzz/gopath/src/github.com/google/syzkaller$ bin/syz-extract -os linux -sourcedir "/home/john/Desktop/ctf/kernel_

fuzz/linux/linux" -arch amd64 sys/linux/proc_operation.txt 

        同目录下生成了proc_operation_amd64.const。

    (4)然后运行syz-sysgen

        ~/Desktop/ctf/kernel_fuzz/gopath/src/github.com/google/syzkaller$ bin/syz-sysgen

    (5)重编译syzkaller

        进入syzkaller源码目录,运行:

            $ make clean

            $ make all

    (6)拷贝编译好的syz-fuzzer到目标系统:

            scp -P 10021 -i ./stretch.id_rsa -r ../gopath/bin root@127.0.0.1:/root/bin

2.编译带有堆溢出的内核模块

漏洞:代码见test.c,主要是proc_write()函数有堆溢出。

编译:参考https://www.cnblogs.com/zhangjy6/p/5462644.html

            $ make

            $ scp -P 10021 -i ./stretch.id_rsa -r ./test/test.ko root@127.0.0.1:/root/bin

            目标机器上$ sudo insmod test.ko

问题:无法insmod

        我本机linux-headers版本是4.4.0-103-generic,目标内核版本是5.2.0-rc1+(查看版本命令$ uname -r),编译出来的driver在目标机上无法ismod,需安装linux-headers-5.2.0-rc1+。

解决:安装linux-headers-5.2.0-rc1+方法,先在网页上找到对应版本的headers。

        $ wget https://kernel.ubuntu.com/~kernel-ppa/mainline/v5.2-rc1/linux-headers-5.2.0-050200rc1_5.2.0-050200rc1.201905191930_all.deb

        $ wget https://kernel.ubuntu.com/~kernel-ppa/mainline/v5.2-rc1/linux-headers-5.2.0-050200rc1-generic_5.2.0-050200rc1.201905191930_amd64.deb

        $ sudo dpkg -i *.deb

        修改KDIR =/usr/src/linux-headers-5.2.0-050200rc1-generic

结果:还是不匹配,很无奈,要么重新编译内核吧,我选择linux-4.4.0.184版本,大不了把本机内核也更新为这个版本。(参考https://www.itsmearunchandel.co.in/linux/ubuntu/upgrade-kernel-version-in-ubuntu.html; 内核源码https://kernel.ubuntu.com/~kernel-ppa/mainline/v4.4.184/)

问题:应该更新了也还是不行,linux-headers-5.2.0-050200rc1-generic跟linux-headers-5.2.0-rc1+还是不匹配。

解决:将驱动模块编译进内核。所以要么把模块编译到目标内核,要么魔改许多module里的数据结构进行错版本加载。

        (参考https://blog.csdn.net/qq_33487044/article/details/81949703https://kouriba.iteye.com/blog/1632767

         make menuconfig->Enable loadable module support->Forced module loading(同时关闭Module versioning support,只开启前3个)。

        修改drvier目录下的MAKEFILE文件,在最后一行添加: obj-y += testmod/

        然后在testmode目录下,添加一个MAKEFILE 文件,文件的内容为:obj-m := hello.o

步骤如下

        (1)test.c拷贝到/linux/drivers/char/目录下

        (2)/linux/drivers/char/Kconfig下添加

            menu "Character devices"            #已有

            config TEST_MODULE

            tristate "Heap Overflow Test"        #在menuconfig中显示的名字

            default y                                        #默认选项

            help

                This file is to test a buffer overflow.

            endmenu              #已有

        (3)/linux/drivers/char/Makefile下添加

                obj-$(CONFIG_TEST_MODULE) += test.o

        (4)如果/linux/drivers/char/是新目录,需修改/linux/drivers/Kconfig(加上source "drivers/char/Kconfig");修改/linux/drivers/Makefile(加上obj-$(CONFIG_TEST_MODULE) += char/)。

        (5)配置文件选择该模块$ make menuconfig -> Device Drivers -> Character devices -> Heap Overflow Test  (*表示直接编如内核,M表示模块形式,)

        (6)运行内核,查看模块是否加载

                $ls /proc/test  或    $dmesg|grep test

3.运行syzkaller捕捉溢出

(1)修改my.cfg(只允许某些调用,速度更快):

    "enable_syscalls": [

                "open$proc",

                "read$proc",

                "write$proc",

                "close$proc"

        ],

(2)运行虚机测试: 

        $ bin/syz-manager -config ./my.cfg  -v 10

        用浏览器打开“127.0.0.1:56741”,运行一段时间后出现以下日志:

```

PROC_DEV:into open!

==================================================================

BUG: KASAN: slab-out-of-bounds in copy_from_user arch/x86/include/asm/uaccess.h:698 [inline] at addr ffff88003c1f5e20

BUG: KASAN: slab-out-of-bounds in proc_write+0x64/0x90 drivers/mod_test/test.c:45 at addr ffff88003c1f5e20

Write of size 4096 by task syz-executor0/2569

CPU: 0 PID: 2569 Comm: syz-executor0 Not tainted 4.11.0-rc8+ #23

Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.10.2-1 04/01/2014

Call Trace:

__dump_stack lib/dump_stack.c:16 [inline]

dump_stack+0x95/0xe8 lib/dump_stack.c:52

kasan_object_err+0x1c/0x70 mm/kasan/report.c:164

print_address_description mm/kasan/report.c:202 [inline]

kasan_report_error mm/kasan/report.c:291 [inline]

kasan_report+0x252/0x510 mm/kasan/report.c:347

check_memory_region_inline mm/kasan/kasan.c:326 [inline]

check_memory_region+0x13c/0x1a0 mm/kasan/kasan.c:333

kasan_check_write+0x14/0x20 mm/kasan/kasan.c:344

copy_from_user arch/x86/include/asm/uaccess.h:698 [inline]

proc_write+0x64/0x90 drivers/mod_test/test.c:45

proc_reg_write+0xf6/0x180 fs/proc/inode.c:230

__vfs_write+0x10b/0x560 fs/read_write.c:508

vfs_write+0x187/0x520 fs/read_write.c:558

SYSC_write fs/read_write.c:605 [inline]

SyS_write+0xd4/0x1a0 fs/read_write.c:597

entry_SYSCALL_64_fastpath+0x18/0xad

RIP: 0033:0x450a09

RSP: 002b:00007ff6efd15b68 EFLAGS: 00000216 ORIG_RAX: 0000000000000001

RAX: ffffffffffffffda RBX: 00000000006f8000 RCX: 0000000000450a09

RDX: 0000000000000090 RSI: 0000000020d09000 RDI: 0000000000000005

RBP: 0000000000000046 R08: 0000000000000000 R09: 0000000000000000

R10: 0000000000000000 R11: 0000000000000216 R12: 0000000000000000

R13: 00007ffc210fd8ff R14: 00007ff6efd16700 R15: 0000000000000000

Object at ffff88003c1f5e20, in cache kmalloc-512 size: 512

...

Dumping ftrace buffer:

  (ftrace buffer empty)

Kernel Offset: disabled

```

        这就是内核被crash时打印出来的调用信息,我们可以看到溢出发生在proc_write+0x64/0x90 drivers/mod_test/test.c:45处,也就是我们添加进去的带有堆溢出的模块。说明我们用Syzkaller添加规则捕捉到了堆溢出的代码。

        结果我啥都没跑出来。

syzkaller网页显示
syzkaller所跑的syscall

4.漏洞复现

        workdir/crashes目录下包含crash之前执行的程序。/tools目录下几个工具值得关注:syz-execprog以各种模式执行单个或一组程序,首先在循环中运行log中所有的程序来确认确实它们之一引发了crash。

        $./syz-execprog -executor=./syz-executor -repeat=0 -procs=16 -cover=0 crash-log

        然后尝试识别是哪个程序导致的crash。

        $./syz-execprog -executor=./syz-executor -repeat=0 -procs=16 -cover=0 single-program

        syz-execprog在本地执行程序,所以需要将syz-execprog和syz-executor复制到带有测试内核的VM中并在那里运行它。一旦确认了引发崩溃的单个程序,尝试通过注释掉单个系统调用并删除无关的数据来缩小范围。还可以尝试将所有mmap调用合并为一个。给syz-execprog加上-threaded=0 -collide=0标志确认这个程序仍然能够导致crash。

        如果不能复现的话,尝试把每个系统调用移到单独的线程中[4]。然后通过syz-prog2c得到C程序,C程序应该也可以导致crash。这个过程在某种程度上也可以通过syz-repro自动化,需要提供config文件和crash报告。

$./syz-repro -config my.cfg crash-qemu-1-1455745459265726910