only (71 solves)
1 | void __fastcall __noreturn main(const char *a1, char **a2, char **a3) |
There are two menus in the binary: one to manage notes and one to trigger a stack buffer overflow.
1 | void bof(const char *a1) |
The bof function reads up to 36 double values from stdin and stores them on the stack.
When the value, reinterpreted as a QWORD, equals 0xD0E0A0D0B0E0E0F, it calls the gift function.
1 | void gift() |
In gift, we can either:
- Print the stack canary to stdout, or
- Execute user-supplied shellcode after a small stub that clobbers several registers.
1 | * thread #1, name = 'chal', stop reason = breakpoint 1.1 |
The address of the shellcode region is on the stack.
We can pop it into rsi and set a large value in rdx.
Since the stub has already set rax to 0, we can turn this into a read syscall
and load a longer second-stage shellcode.
1 | from pwn import * |
only_rev (27 solves)
1 | unsigned __int64 gift() |
only_rev is very similar to only challenge,
But it comes with a small modification in the gift function.
In this challenge, the shellcode length is restricted to 9 bytes (2 bytes shorter),
and the stub zeroes more registers. With only 9 bytes, what you can
do is very limited, so the previous approach no longer works.
1 | unsigned __int64 __fastcall note_menu(const char *a1) |
So I switched to the note menu to try to leak a libc address.
1 | void create_note() |
Normally, if you can allocate a large chunk and free it, and later reclaim it without overwriting its contents, leaking a libc address is easy.
However, in this challenge you’re not allowed to hold references to more than one note at the same time. So you can’t do the classic pattern of:
- Allocate a big chunk
- Allocate a guard chunk
- Free the first one
If you only allocate a big chunk and free it, no libc address is stored in it because it’s just merged into the top chunk.
1 | unsigned __int64 save_note() |
You can work around this using save_note.
If you call save_note with an invalid file path, it allocates a 0x1e0 sized chunk and frees it.
As with this size, the freed chunks goes into tcache,
it doesn’t get merged into the top chunk.
We can use that as a guard chunk.
1 | alloc(0x1000) # allocate 0x1000 sized chunk |
So the idea to leak the libc address is like above.
1 | unsigned __int64 save_note() |
If you give a valid path like "0" to save_note, it will call snprintf
with the chunk pointer and then printf with the chunk pointer.
That prints the chunk contents until the null byte, which allows you leak a libc address.
1 | from pwn import * |
Once we have a libc leak, we can use the gift function again. By running add rsp, 0x58 instruction,
we pivot rsp to just before the first element of
the double array we filled earlier. That lets us treat the double array as a
rop chain and call mprotect + read to run arbitrary shellcode.
mstr (7 solves)
1 | import ctypes |
mstr is a Python ctypes challenge.
It uses ctypes.cast to manipulate internal Python string structures directly,
accessing the character buffer and length fields via low-level api functions.
1 | 0x7bff9a84a1c0: 0x0000000000000002 0x0000000000a472c0 |
The field offsets used in the challenge are basically correct.
For 8-bit compact strings in cpython 3.12, the relevant fields are indeed at offsets 0x10 and 0x28.
It isn’t for 16 bits and 32 bits strings but it’s not the point of this challenge.
1 | Python 3.12.3 (main, Aug 14 2025, 17:47:21) [GCC 13.3.0] on linux |
The main issue of this challenge is that the one chararacter strings like 0
are meant to be a read-only global value in cpython.
1 | class MutableStr: |
If you set max_size_str of a MutableStr to a one-character string like'9' and then append more characters so that the resulting string (when passed
to int()) becomes a larger number, you get an out-of-bounds write:
max_sizeis computed asint(self.max_size_str)after you’ve already grown that Python string.- The underlying allocation for the character buffer is still based on the original size.
MutableStr._add_strtrustsmax_sizeand happily writes past the end of the actual buffer.
With this, you can corrupt the length field of an adjacent string in python.
1 | class MutableStr: |
Once you’ve created a string whose length field is huge,
you can use modify command with arbitrary indices.
That gives you an arbitrary write primitive.
With this, you can crate a fake vtable in a writable section
And make an accessible string to have that vtable pointer.
Then when you prints that string, rip becomes a value in the fake vtable and
rdi becomes that string used.
1 | from pwn import * |
In the final exploit, I overwrite the vtable so that a call ends up jumping to system, and I place "sh\0" at the beginning of the string, giving me a shell.
no_check_WASM (5 solves)
1 | diff --git a/src/wasm/function-body-decoder-impl.h b/src/wasm/function-body-decoder-impl.h |
This challenge is about v8 wasm with a missing validation for the return value type.
1 | ;; fakeobj |
Although the bug directly gives you a fakeobj/addrof primitives,
That was not enough because of v8 heap sandbox.
As I believed that this challenge is not meant to find v8 heap sandbox escape 0 day exploit,
I started to look for other way to utilize the given bug.
1 | (func $stack_leak (export "stack_leak") (result i64 i64)) |
After playing with different function signatures,
I was able to find ways to leak pie and stack addresses.
1 | (func $array_set (export "array_set") |
Then by fasking a stack pointer into a wasm array which has a big length.
I was able to overwrite the values in the stack with a rop chain.
Eventually I called mprotect and read function then jump to the address,
so I can run an arbitrary shellcode.
1 | BigInt.prototype.hex = function() { |
This is the full solution.
rd (5 solves)
1 | from pwn import * |
In this challenge, if you allocate more than 16 users,
it stops assigning the task chunks with the value from the malloc function call,
and this leads to the use of an uninitialized value.
So you can spray chunks with a pointer at offset 0x20, where the tasks pointer is located.
Then, if you allocate a user, it writes a pointer allocated with strdup to that address + 0x20.
When pthread_create is called, it allocates a heap chunk for the TLS of that thread.
You can leak this address by writing zero to the LSB of the token pointer in the heap,
so it can be printed when you log in.
Then you can overwrite the RBP pointer of a child thread which is at the sleep function call.
With this, you can stack pivot to a heap address.
In addition, to prevent a crash caused by the use of [RBP-0x10],
I wrote another heap pointer at [RBP-0x10] so it can have 0 as the RDX value given to memcpy.
Otherwise it crashes, because it would use the chunk size, which is at [RBP-0x8], as a pointer as the destination of memcpy.
This challenge was painful, especially because there was a big distance between where I live and where the challenge server is.
I wrote the solution quite soon, but it never worked initially.
Then I realized that the single inputs I sent were getting split unintentionally, for whatever reason.
So I modified the way I spray to not use a big input at a single time.
Then there were some differences between the stack offsets when it’s remote and when it’s local.
At the end, I have bought a VPS server far away from me and I could figure out what’s happening.