core_solid 复现
1 | qemu-system-x86_64 \ |
- kaslr,smep,smap
1 | !/bin/sh |
- kptr_restrict,dmesg_restrict
漏洞分析
驱动程序的逆向有点麻烦,主要是 csaw_ioctl
不同功能传入的结构体不同
- 3个8字节,有时是指针,有时是 size,甚至有时只传入4字节
而函数 csaw_ioctl
中只定义了一个结构体 channel_args_from
,于是我就默认每个位置的功能固定,走了不少弯路,最后是通过函数名和一些特殊函数分析出了 channel_args_from
各个位置的含义:
1 | alloc_new_ipc_channel /* 因为里面有kmalloc,可以判断传入的参数为size */ |
- 顺带一提,以下两个结构体在驱动模块中经常出现(部分条目可能不一样)
1 | 00000000 list struc ; (sizeof=0x28, mappedto_4) |
1 | 00000000 item struc ; (sizeof=0x20, mappedto_5) |
程序的漏洞点就在 realloc_ipc_channel
中:
1 | if ( key_cannel ) |
- 如果
user_size
大于channel->size
就会导致负数溢出 - 但这里我们想要的不是
krealloc
申请的大空间,而是channel->buf
空间不变,但是item->size
超大,可以绕过后面的检查:
1 | item_write = using_list->item; |
- 程序将在 CSAW_WRITE_CHANNEL 完成局部任意写
任意读写
程序利用 CSAW_SEEK_CHANNEL 和 CSAW_READ_CHANNEL 可以完成局部任意读:
1 | item_seek->data = channel_from.user_ptr; |
1 | item_read = using_list->item; |
item_seek
和item_read
都指向using_list->item
(是由alloc_new_ipc_channel
进行分配的)- 因此
data_read == channel_from.user_ptr
模板如下:
1 | void RAA(int fd, int channel_id, void *read_buff, uint64_t addr, uint32_t len) |
程序利用 CSAW_SEEK_CHANNEL 和 CSAW_WRITE_CHANNEL 可以完成局部任意写:
1 | item_seek->data = channel_from.user_ptr; |
1 | data_write = (unsigned __int64)item_write->data; |
- 首先
data_write == channel_from.user_ptr
- 而
item_write->buf
是由_kmalloc
申请出来的,理论上来说我们是不好泄露堆地址的,但是realloc_ipc_channel
中有解决的办法:
1 | buf = (char *)krealloc(channel->buf, size + 1, 0x14000C0LL); |
- 当
size+1 == 0
时,krealloc
会返回 NULL,同时被赋值给using_list->item->buf
(这就不需要考虑堆地址了)
模板如下:
1 | void WAA(int fd, int channel_id, void* write_buff, uint64_t addr, uint32_t len) |
入侵思路
可以用 HijackPrctl 在不提权的情况下获取 flag
HijackPrctl 的核心就是利用 prctl
系统调用:
1 | SYSCALL_DEFINE5(prctl, int, option, unsigned long, arg2, unsigned long, arg3, |
- 然后跟进
security_task_prctl
1 | int security_task_prctl(int option, unsigned long arg2, unsigned long arg3, |
- 在
security_task_prctl
中会定位到一个虚表里面去,并且第一个参数可控 - 劫持这里,然后调用
prctl
,就可以实现任意代码执行
利用程序漏洞实现的 WAA 可以轻松覆盖这里,但是有一个问题:
1 | int security_task_prctl(int option, unsigned long arg2, unsigned long arg3, |
security_task_prctl
的第一个参数是int
类型- 为了执行
commit_creds(prepare_kernel_cred(0))
,我们需要传入prepare_kernel_cred(0)
的指针,但是在64位的系统中该指针会被int
类型截断(32位就没有这个困扰)
取而代之的是函数 __orderly_poweroff
:
1 | static int __orderly_poweroff(bool force) |
- 该函数会调用
run_cmd
,进而调用call_usermoderhelper
(内核运行用户程序的一个api
,并且拥有Root
的权限,如果我们能够控制性的调用它,就能以Root
权限执行我们想要执行的程序) - 参数
poweroff_cmd
是全局变量,可以修改
因此 HijackPrctl 的大体步骤为:
- 篡改
poweroff_cmd
使其等于我们预期执行的命令 - 篡改
prctl_hook
为orderly_poweroff
- 调用
prctl
为此我们需要先泄露 kernel_base
:
- 当我们有 RAA 任意读后,可以用爆破的形式泄露 VDSO 的 ELF 头文件
- 然后利用 VDSO和
kernel_base
相差不远的特性,泄露出内核基址
把网上的模板拿来改一改就好了:
1 | for(addr=0xffffffff80000000; addr<0xffffffffffffefff; addr+=0x1000) { |
- 接下来就是一些套路化的东西,但是这个消息获取的过程需要注意
信息获取
先关闭 kaslr,开始调试内核:
1 | / |
kernel_base == 0xffffffffa3a00000
1 | / |
poweroff_work_func == 0xffffffffa3a9c4c0
poweroff_work_func_offset = 0xffffffffa3a9c4c0 - 0xffffffffa3a00000 = 0x9c4c0
然后连接 GDB,打印 security_task_prctl-ffffffffa3cbd410
1 | pwndbg> x /30iw 0xffffffffa3cbd410 |
在 0xffffffffa3cbd454
打断点,调试至此:
1 | *RBX 0xffffffffa4c4fce8 —▸ 0xffffffffa5162100 ◂— 0xffffffffa4c4fce8 |
prctl_hook == 0xffffffffa4c4fd00
prctl_hook_offset = 0xffffffffa4c4fd00 - 0xffffffffa3a00000 = 0x124fd00
然后连接 GDB,打印 poweroff_work_func-ffffffffa3a9c4c0
1 | pwndbg> x /30iw 0xffffffffa3a9c4c0 |
- 第一个 call 就会调用
run_cmd
,所以第一个参数poweroff_cmd == rdi
poweroff_cmd_offset = 0xffffffffa4c3d1e0 - 0xffffffffa3a00000 = 0x123d1e0
完整 exp:
1 |
|
小结:
第一次遇到 HijackPrctl,最后的 exp 参考了下 raycp 大佬的思路