从用户态传入了0x20的数据,其数据放在了pool里面
在IDA里面看的话就是
就是他传入的四个QWORD 。
我们可以看得出来30001的时候就是通过v17来找到相关地址,然后kfree,再清零
所以其实就看得出来v17是index。
功能30002
v8是slab的地址
v20是从哪开始写
v9[1]里面就是size
所以就是往里面写
功能30003
上面是写进去
所以这个自然就是读出来
功能30000
读入一段
根据这一段申请slab
漏洞有两个
首先我们显而易见的在读写slab的时候对offset没有检查
如果我们的offset是负数
就可以任意地址读写。
第二个是条件竞争
是双核的,对pool的时候缺少锁。
如果我们在一个线程释放slab的时候另一个线程去对他进行读写,就可以触发uaf。
首先我们要充分熟悉一下slab机制
在Linux中,伙伴系统(buddy system)是以页为单位管理和分配内存。但是现实的需求却以字节为单位,假如我们需要申请20Bytes,总不能分配一页吧!那岂不是严重浪费内存。那么该如何分配呢?slab分配器就应运而生了,专为小内存分配而生。slab分配器分配内存以Byte为单位。但是slab分配器并没有脱离伙伴系统,而是基于伙伴系统分配的大内存进一步细分成小内存分配。
https://blog.youkuaiyun.com/xiaoqiaoq0/article/details/122051942
再熟悉一下userfaultfd机制
内存页的分配是创建时先分配页表但并不会实际分配物理页面。在读写发生时,由于物理页面不存在,触发缺页异常进入内核处理该缺页中断,再实际分配物理页面进行相应的读写。这种延迟分配的机制使得系统性能在一定程序上得到了提升。
userfaultfd机制允许在用户态处理缺页异常,这个特性使得它在内核漏洞利用中可以发挥较大的作用。一个典型的场景时mmap出来一块内存,使用userfaultfd监视该地址,如果发生缺页异常先进入内核,再从内核到用户态定义的缺页处理程序,此时可以在用户态暂停从而间接的实现暂停内核态代码的运行。这种特性使得该机制在竞争以及double fetch类的漏洞利用中能够发挥较大作用。
思路一:任意地址读写。
因为开了kaslr,所以首先当然是需要泄露地址
泄露地址又包括泄露堆地址,内核地址,模块地址。
首先我们知道slub的分配很类似于glibc 的fastbin,里面会有指针fd指向下一块空闲的块
所以我们还是通过已经释放掉的slab来拿到堆地址,就通过任意读写。
slub机制不会像glibc一样加个头,是多少就是多少。
size = 0x100;
ko_malloc(fd, 0, data_ptr, size);
ko_malloc(fd, 1, data_ptr, size);
ko_malloc(fd, 2, data_ptr, size);
ko_malloc(fd, 3, data_ptr, size);
ko_malloc(fd, 4, data_ptr, size);
ko_free(fd, 1);
ko_free(fd, 3);
idx = 4;
offset = -0x100;
size = 0x100;
ko_read(fd, idx, data_ptr, size, offset);
uint64_t kheap_ptr = *(uint64_t *)data_ptr;
printf("[+] leaking kheap address: 0x%lx\n", kheap_ptr);
然后考虑泄露内核基地址。
我们可以用任意读,去泄露0号slab之前的数据。然后找个基地址相关的地址,通过偏移获得内核地址。
那我咋知道0号slab在哪?
推荐两种方法:
1、可以在slab里写一个类似0xdeadbeef的字符串然后在gdb中search搜索。
2、可以在kmalloc下断点看他的返回值
我们实际调试一下
首先我们可能比较需要一个有符号的内核
当然没有也勉强可以但是不好调不好看
编译linux内核
内核不要只顾着用最新的
因为驱动必须用编译它的内核模块跑
自己整个内核的话驱动挂载补上的。
这个题的内核模块是4.20.13
上面说的直接划线
有点问题
根据实验
就算我们用了跟它本来版本一模一样的bzimage
这是因为linux内部有模块检测机制,导致驱动挂不上去,就不赘述了
那我们会想能不能bzimage用它给的 vmlinux用我们自己的 反正版本一样
也哒咩
用不了 里面的很多函数偏移啥的都不一样。
亲测亲测。
可以考虑编译内核的时候关闭CONFIG_MODVERSIONS选项来关闭模块检测。
但是我们一般似乎更通用的是
只能用一些现有的工具从他给的bzimage中尝试恢复一些符号
能恢复的不多
但是也能凑或用吧
网上找了个教程
看得到一个是我自己编的有符号的
一个是它恢复的 差八九十倍…
但是对于一些简单的驱动题的话也够用
调试方法不再赘述,补充一个小trick
qemu启动脚本中中-s是-gdb tcp::1234缩写的意思;-S是freeze CPU at startup,等待调试。
如果多次启动可能碰到端口占用
kill -9 `lsof -ti:2222`
用这个命令杀死就好。
gdb远程链接
我当然用的是自己编译的内核 有符号表。
方便调试
有符号表之后虽然有些地方看着还是没符号的样子比较丑
但是下断点啥的也还是方便了很多
那么我们就先kmalloc了五个0x100的slab
这是第五次kmalloc的返回值
我们看一下周围
可以很清晰看到堆的结构
以及我们可以看到
确实内核里的slab机制就是的slab就是不会加很多头。
然后释放掉slab1 3
看地址下图是slab1
看得出来里面第一个地址是9900 这显然是800后面空闲的slab
释放掉3之后
我们显然看到slab指向被释放的slab1
然后我们整上向上越界就能独处heap的地址。
然后我们试图读kernal base
我们试图向上找
看看上面有什么好的地址
这个228的地方找到一个地址
经过测试之后是kernal中的一个地址
那为啥不用上面那个呢?
那一堆链表看起来就像slab中的。
减偏移的话我们需要知道kernal的基地址
也可以通过在/proc/kallsyms里面查看
然后我们要泄露mod的地址。
mod的地址在mod_tree中有写。
所以我们需要先劫持slab那个链条
申请到mod_tree
然后泄露一下mod的地址
怎么知道mod_tree的地址呢?
泄露kernal_base之后通过偏移就能找的到。
然后要申请到mod_tree
这个地址改成mod_tree + 0x40
两次申请申请到
申请0x40是因为0x40这里的地址会循环起来
我们申请这里的话这个地址又会被写进链条里
方便下次利用。
然后泄露这个地址出来
这个就是hackme模块的基地址
那这个地址哪来的?
我们可以
cat kallsyms | grep hackme
当然也可以
是的没错
这个模块地址就是IDA里面驱动text基地址
memset(mem,'A',0x100);
*((size_t *)mem) = (0x811000 + kernel_addr + 0x40); // mod_tree +0x40
write_to_kernel(fd,4,mem,0x100,-0x100);
alloc(fd,5,mem,0x100);
alloc(fd,6,mem,0x100);
read_from_kernel(fd,6,mem,0x40