knote 复现
1 | !/bin/sh |
- kaslr,smep,smap
1 | !/bin/sh |
- kptr_restrict,dmesg_restrict
1 | / $ cat /proc/version |
- version 5.3.6(很难 ROP 到用户态)
模块分析
1 | int __cdecl note_init() |
- 在 Linux 系统中,存在一类字符设备,他们共享一个主设备号(10),但此设备号不同,我们称这类设备为混杂设备
misc_device
是特殊的字符设备,注册驱动程序时采用misc_register
函数注册
漏洞分析
1 | void __cdecl edit() |
- 没有加锁,
myarg.buf
为全局变量,有条件竞争漏洞 - 释放模块有 UAF
Double Fetch
Double Fetch
从漏洞原理上属于条件竞争漏洞,是一种内核态与用户态之间的数据访问竞争
- 通常情况下,用户空间向内核传递数据时,内核先通过通过
copy_from_user
等拷贝函数将用户数据拷贝至内核空间进行校验及相关处理 - 但在输入数据较为复杂时,内核可能只引用其指针,而将数据暂时保存在用户空间进行后续处理
- 此时,该数据存在被其他恶意线程篡改风险,造成内核验证通过数据与实际使用数据不一致,导致内核代码执行异常
- 一个用户态线程准备数据并通过系统调用进入内核,该数据在内核中有两次被取用:
- 内核第一次取用数据进行安全检查(如缓冲区大小、指针可用性等)
- 内核第二次取用数据进行实际处理
- 而在两次取用数据之间,另一个用户态线程可创造条件竞争,对已通过检查的用户态数据进行篡改,在真实使用时造成访问越界或缓冲区溢出,最终导致内核崩溃或权限提升
Double Fetch
需要使用 userfaultfd 机制:
- userfaultfd 并不是一种攻击的名字,它是 Linux 提供的一种让用户自己处理缺页异常的机制
- 初衷是为了提升开发灵活性,后来在 kernel pwn 中常被用于提高条件竞争的成功率
现在来看一个详细的例子:
1 | if (ptr) { |
- 如果,
user_buf
是一块 mmap 映射的未初始化区域,此时就会触发缺页错误copy_from_user
将暂停执行 - 在暂停的这段时间内,我们开另一个线程,将 ptr 释放掉,再把其他结构申请到这里(比如:
tty_struct
) - 然后当缺页处理结束后,
copy_from_user
恢复执行,然而 ptr 此时指向的是tty_struct
结构,那么就能对tty_struct
结构进行修改了(当然也可以是其他的结构体)
模板如下:
1 | void userfault(void *fault_page,void *handler) |
处理函数 handler
的模板如下:
1 | void* handler(void *arg) |
Modprobe_path Attack
modprobe_path
是用于在 Linux
内核中添加可加载的内核模块,当我们在 Linux
内核中安装或卸载新模块时,就会执行 modprobe_path
指向的程序
他的路径是一个内核全局变量,默认为 /sbin/modprobe
,源码如下:
1 | /* modprobe_path is set via /proc/sys */ |
- 也可以通过如下命令来查看该值:
1 | ➜ ~ cat /proc/sys/kernel/modprobe |
- 其就是 Linux
modprobe
命令(在sbin
目录中,说明该程序拥有 Root 权限) - 此外,
modprobe_path
在内核中且具有可写权限(普通权限即可修改该值)
而当内核运行一个错误格式的文件(或未知文件类型的文件)的时候,内核调用 call_modprobe
函数执行 modprobe_path
指向的文件:
- 由于
call_modprobe
函数拥有 Root 权限 - 我们只需要劫持
modprobe_path
,指向我们提权的脚本,然后system
一个非法文件,就能触发提权脚本的执行 - 其调用链如下: (内核版本 linux-4.20.1)
1 | do_execve() -> do_execveat_common() -> __do_execve_file() -> exec_binprm() -> search_binary_handler() -> request_module() -> call_modprobe() -> call_usermodehelper_exec() |
使用案例如下:
1 | system("echo '#!/bin/sh' > /tmp/shell.sh"); |
- 假设我们已经把
modprobe_path
修改为了/tmp/shell.sh
(提权脚本) - 当程序发现
/tmp/fake
不可执行时,就会通过call_modprobe
来调用modprobe_path
所指向的命令 - 然后执行
/tmp/shell.sh
完成提权
Slab Heap
Linux 内核使用的是 slab/slub 分配器,以内存池的形式分配内存(大小相同的堆靠在一起,8K的内存池专门管理8K的堆空间,16字节的内存池专门管理16字节的堆空间),使用如下命令可以查看 slab 内存池:
1 | ➜ ~ sudo cat /proc/slabinfo |
slab 为了提高效率实现了一个机制:
- 在
kfree
后,原用户数据区的前8字节会有指向下一个空闲块的指针 next - 如果用户
malloc
的大小在空闲的堆块里有满足要求的,则直接取出
有一个比较容易利用的就是,伪造空闲块的 next 指针,则可以很容易分配到我们想要读写的地方(不像 ptmalloc2 还需要伪造堆结构,这里只需要更改 next 指针即可)
入侵思路
利用 Double Fetch
可以在内核全局变量中写入一个 tty_struct
但是 5.0 版本以上的内核很难 ROP 到用户态(通过修改 tty_struct->tty_operations
为 gadget,ROP 到用户态的办法失效了)
此时需要另一个入侵技巧 modprobe_path attack
,通过伪造空闲堆的 next 指针,实现任意地址处分配,把 modprobe_path
分配到可控区域,然后进行修改
最后就是 modprobe_path attack
的攻击流程了
完整 exp:
1 |
|
- 这个 exp 很大程度上借鉴了 ha1vk 大佬的博客
- 之后我发现,只要
memcpy
复制了一个内核地址到page
,那么把该page
的内容打印出来就是0xffffffc0
(实际上是正确的数据),不知道这是不是内核的保护机制
小结:
学到了 Double Fetch
和 modprobe_path attack
:
Double Fetch
:如果copy_user
系列函数使用的是全局变量并且没有加锁,就可以使用这个方法modprobe_path attack
:对于 5.0 版本以上的内核很难使用 ROP 来绕过 smep,这种攻击算一种替代名,还可以利用mov cr4,xxx
使得 CR4 寄存器的第21/22位为“0”,即可关闭 smap/smep
现在对这个利用还不熟,只能用用模板,感觉 userfaultfd 机制有点难理解(还不清楚为什么模板要这么写),之后抽时间了解一下