ICC 2025
Hi all, it’s been a while. I barely played CTFs this year, but I can finally tick another dream item: participating in the International Cybersecurity Challenge. The 2025 edition took place in Tokyo. This was my first ICC (and probably the last unless they change the age limit :p), and I joined a new squad called Team ASEAN, made up of players from across Southeast Asia. Huge thanks to everyone who helped create and manage Team ASEAN; you helped me achieve one of my dreams.
I also want to thank the ICC 2025 Tokyo organizers and problem setters. I truly enjoyed both the Jeopardy and Attack-Defense challenges (especially the pwn tasks). The quality was excellent, the preparation was obvious, and the event ran smoothly. It is easily one of the best CTFs I have joined so far. I only had time to look at the pwn challenges, and since I am short on free time right now, this post covers just one of them: baby orqpy by hugeh0ge, which me and my teammate (Lord_Idiot) solved during the Jeopardy sssion. We enjoyed it a lot, so here is the writeup :D
Pwn
baby orqpy
I love tiny codes! Note: you have to do PoW for connection. Please see this doc.
nc 34.104.158.128 5000
authored by hugeh0ge
Initial Analysis
In this challenge, we were given a really small source code named chal.c.
|
|
First, let’s start by checking the binary protection with checksec.
|
|
So the binary is No PIE, which means its address is fixed, and it uses Partial RELRO, which keeps the GOT writable. The challenge code is tiny: we get a single chance to OR one bit at any known address, and we have to turn that into code execution.
Solution
Before diving into the exploit, let’s write a helper script that makes experimentation easier.
|
|
Now it’s time to think about how to solve the challenge. I’ll walk through our reasoning step by step.
Convert one bit flip to infinite bit flip
For challenges like this, usually the first objective is to turn the single bit OR opportunity into an infinite number of flips. Because exit has not been called yet, its GOT entry still points into the binary rather than to its libc address, so we can treat it as a controllable pointer that can be replaced with gadget located in binary.
Furthermore, the exit GOT entry is a great target because the program always ends by calling exit, and if we redirect that entry to an instruction that jumps back to main, we effectively gain unlimited chances to flip bits.
We do not even need to pick a precise address ahead of time. Brute forcing the bits that we want to OR on the current exit value quickly shows that flipping the fourth bit (zero indexed) routes execution back to main. GDB confirms that this change drops us into _start+6, which then calls __libc_start_main_impl again, so the binary restarts.
So, extend our script to do this:
|
|
After running the snippet above, the binary loops inside main, so we can move to the next step.
Create a stronger primitive
This is where creativity starts to matter. We now have infinite opportunities to apply a bitwise OR to any known address, but we still lack a leak, so our writable region is limited to the binary. Worse, the OR flip can only change bits from zero to one, which constrains what we can build.
Our next goal is to find a stronger gadget to drop into the GOT so we can perform richer actions later. To do that we have to understand which registers we control before the gadget runs. Looking at the GOT, only exit and puts still resolve inside the binary. puts is invoked only when we request a flip with an index of eight or more.
One nice thing is that puts actually serves as a decent oracle. If we try to flip the exit GOT again, we have to do it in a single shot, because our infinite flip bit depends on the exit GOT value. However, we can control when puts is called, so we can safely flip the bit in the puts GOT entry until it matches what we want. Once it does, we can redirect the binary to call puts by setting the bit-flip index to a value greater than 8.
The plan, therefore, is to turn the puts GOT entry into a stronger gadget. Before that, we must know which registers we control when puts runs. Setting a breakpoint at main+152 (just before the call) and inspecting the state answers that question.
Let’s say I want to flip the bits at address 0x4141414141414141 with value 0x4242424242424242. The registers look like this.
From this we control rsi, [rbp-8], and [rbp-0x10], so we started hunting for a gadget that would give us a stronger primitive.
After a good amount of searching (this took most of our time during solving it) we found a gadget in the binary that we could reach by flipping bits of the current puts value.
This gadget gives us an arbitrary one-byte write because we control both esi and [rbp-8]. Even better, it calls exit afterward, which will make us back to main, so we gain infinite one-byte writes. Luckily, the current puts GOT value (0x401036) can be converted to 0x401237 using our bitwise OR flips.
So, let’s extend our script to update the puts value:
|
|
After redirecting the puts GOT entry, we can write one arbitrary byte to any address.
Getting a libc leak
Up to this point we still lack a libc leak, so we needed another idea. After plenty of debugging we noticed that when we call exit (for example with target 0x404028), the registers look like this.
Observed that we actually control rax, and the disassembly of main reveals this another good gadget.
If we overwrite the setbuf GOT entry, that gadget effectively calls any_function(any_param) because the rdi can be controlled. That is enough to obtain both a leak and RCE. Observed that the .bss area contains a libc pointer (stdout). If we rewrite the setbuf GOT entry with the printf PLT address (which we know) and pass the address that holds that libc pointer (also known because the binary is No PIE), we get an easy leak.
However, our current primitive only writes one byte and exit still returns to the start of main, where setbuf runs. We cannot partially overwrite the setbuf GOT entry because the function would run mid-write and crash the program.
The fix is straightforward: change the exit GOT value so it returns to the instructions after setbuf. Then we can patch the setbuf GOT entry without triggering the function.
Overwriting exit is risky, though, because it gives us unlimited flips. Right now the exit GOT entry is 0x401096, and skipping setbuf would normally require redirecting it to 0x40119c based on the disassembly (instruction located just after _setbuf is called), which takes two byte writes.
To make that safe we looked for addresses where each partial write keeps the binary alive. After looking through the disassembly for a while, we noticed that returning to 0x40119d works well. Observed that after the first byte overwrite, the GOT entry becomes 0x40109d, which still loops back to main.
And then, when we overwrite the second byte, the exit GOT will change to 0x40119d, which is basically the below instruction:
If we redirect it to 0x40119c it executes lea rax,[rip+0xe65], but sending it to 0x40119d uses eax instead of rax. Because the binary is No PIE, using eax is actually fine. This lets us redirect exit to the instruction of main after setbuf, so setbuf will never be called.
So, let’s extend our script to do this:
|
|
After doing this we can overwrite the setbuf GOT entry with the printf PLT address, and once that is set up we can point exit to the gadget we found earlier at 0x401194.
Let’s add our script to do this:
|
|
After overwriting exit, every call to setbuf prints the value of rax, which equals the address we passed when the binary asked for the target to flip. We can therefore pass the .bss address of stdout as that target and MUST set the bit index to match the stdout least significant byte, because remember that an index below eight performs a simple bit flip, while eight or above triggers our arbitrary write because of our previous replacement that we did in the puts GOT entry.
Once again, let’s extend the script to do this:
|
|
That leak lets us move to the next stage.
Getting Remote Code Execution
With the libc base known, the remaining job is to spawn a shell. The steps are actually the same with what we did during the leak stage, except we overwrite setbuf with system instead of printf :)
Before that, don’t forget to place a /bin/sh string in .bss so we have a stable pointer.
So, what we need to do is point the exit to return just after setbuf again, write /bin/sh, update setbuf to call system, and finally restore exit so it uses our any_func(any_arg) gadget.
|
|
Running the script now spawns a shell as soon as the binary calls setbuf. After plenty of back and forth with the exit GOT gadgets, it was satisfying to see the shell pop up :)
Below is the full script of the solver:
|
|
I had a lot of fun working on this challenge at ICC, so it definitely deserved a writeup here. Big applause to hugeh0ge for coming up with such a creative task.
Social Media
Follow me on twitter