浅学Kernelpwn
CISCN2017-babydriver
分析
把驱动拖进IDA查看
结构体babydevice_t
有两个变量buf和buf_len
1 | 00000000 babydevice_t struc ; (sizeof=0x10, align=0x8, copyof_429) |
babydriver_init
1 | int __cdecl babydriver_init() |
alloc_chrdev_region 动态分配设备编号cdev_init 初始化一个静态分配的cdev结构体变量cdev_add 向Linux内核添加一个新的cdev结构体变量所描述的字符设备,并使这个设备立即可用_class_create 动态创建设备的逻辑类,并完成部分字段的初始化,然后将其添加进Linux内核系统中device_create 动态地创建逻辑设备,并对新的逻辑设备类进行相应的初始化,将其与此函数的第一个参数所代表的逻辑类关联起来,然后将此逻辑设备加到Linux内核系统的设备驱动程序模型中
然后使用if嵌套一旦有某一步发生错误就进行回滚到初始状态
设置/dev/babydev作为设备文件
babydriver_exit
1 | void __cdecl babydriver_exit() |
卸载设备,删除class,移除cdev结构体变量所描述的字符设备,删除一个字符设备区
babyrelease
1 | int __fastcall babyrelease(inode *inode, file *filp) |
参数inode和filp
inode即索引节点,filp是文件指针
每一个设备都对应一个inode,且共享一个inode;而filp文件指针每次打开一个设备都会创建一个新的文件指针以供操作
发生在关闭设备时,释放缓冲区
babyopen
1 | int __fastcall babyopen(inode *inode, file *filp) |
申请空间,赋值buf_len=64
babyread1
2
3
4
5
6
7
8
9
10
11void __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
babywrite1
2
3
4
5
6
7
8
9
10
11void __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
babyioctl1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19void __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.sh1
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
39struct cred {
atomic_t usage;
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
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 */
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 */
void *security; /* subjective LSM security */
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_len1
2
3gef➤ x/4gx 0xffffffffc0000108+0x23d0-8
0xffffffffc00024d0: 0xffff880000a64d80 0x00000000000000a8
0xffffffffc00024e0: 0x0000000000000000 0x0000000000000000
cred结构体1
2
3
4
5
6
7
8
9
10
11
12gef➤ 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
12gef➤ 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 |
|
- 需要静态编译,因为kernel里没有共享库什么的
- 好像使用musl-gcc编译并去符号压缩一下体积会好一点
- exp中父进程需要
wait,让子进程操作 - gdb使用
gef插件,pwndbg非常的卡
漏洞利用 第二种方法
第一种方法非常的简单粗暴









