algoatson.github.io ~/posts
segfault@algoatson.github.io
back to index

the first five minutes with an unknown binary

2026-04-223 min readre tooling linux

Opening Ghidra first is a beginner tell. Five minutes of cheap static and dynamic triage tells you what kind of problem you have, and half the time it tells you the answer outright. Here's the order I run things, and why.

1 · identify#

What is this file, and what was it built to assume?

console
$ file ./chal
./chal: ELF 64-bit LSB executable, x86-64, dynamically linked,
        interpreter /lib64/ld-linux-x86-64.so.2, BuildID[...], not stripped

$ checksec --file=./chal          # pwntools ships this; so does the checksec project
[*] '/.../chal'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

file tells you bitness, dynamic vs static, stripped vs not, and the requested interpreter. checksec tells you the mitigations — and therefore the route. No PIE + no canary in that output means a fixed-address stack overflow; you basically have the genre already.

2 · enumerate the ELF#

Read the binary's own table of contents before reading its code.

console
1
2
3
4
5
6
$ readelf -h ./chal      # header: entrypoint, type (EXEC vs DYN=PIE)
$ readelf -d ./chal      # dynamic section: NEEDED libs, RELRO, RUNPATH
$ readelf -l ./chal      # program headers / segments + which sections map where
$ nm -C ./chal           # symbols, C++ names demangled (empty if stripped)
$ objdump -d -M intel ./chal | less   # full disassembly, Intel syntax
$ objdump -R ./chal      # relocations — the live GOT entries

readelf over ldd

ldd ./chal actually runs the loader against the binary; on a hostile target that's an execution primitive you didn't mean to hand over. readelf -d reads the NEEDED entries statically and tells you the same thing safely.

Then the strings — fast, and shockingly often decisive:

console
$ strings -n 8 ./chal | less        # printable runs >= 8 chars
$ strings -t x ./chal | grep -i flag # with file offsets, hunting a keyword

A format string, a /bin/sh, a hard-coded password, a system call — strings finds all of them before you've read a single instruction.

tool one-line job
file bitness · linkage · stripped? · interpreter
checksec NX · PIE · RELRO · canary → the route
readelf -d shared-lib deps, RELRO, RUNPATH
nm -C / nm -D static / dynamic symbols
strings -n 8 hard-coded secrets, format strings, command strings
rabin2 -zI ./chal rizin's combined info + strings, if you prefer it

3 · watch it run#

Static lies by omission; the live process doesn't. Trace the calls it makes:

console
$ ltrace ./chal          # library calls: strcmp(input, "s3cret"), system(...)
$ strace ./chal          # raw syscalls: read/write/open/execve, and the fds

ltrace is the cheat code on CrackMe-style binaries — it shows the strcmp against the password in plaintext. strace shows the I/O shape: which fds it reads, whether it opens a flag file, whether it execves anything.

And if it sandboxes itself, find out before you write shellcode:

console
$ seccomp-tools dump ./chal   # prints the BPF filter: which syscalls survive

If execve is blocked but open/read/write aren't, you already know the exploit ends in an open-read-write ROP chain, not a shell.

4 · set up the libc, then open the disassembler#

For a remote pwn you want the exact libc locally so offsets match. pwninit automates the patchelf dance:

console
$ pwninit                       # finds ./chal + ./libc.so.6, patches the interp + rpath
$ one_gadget ./libc.so.6        # magic execve("/bin/sh") gadgets + their constraints

Now — and only now — Ghidra or rizin, because you walk in already knowing the bitness, the mitigations, the imported functions, the strings, the syscall surface, and the libc. The decompiler is for the one function that matters, not for orientation.

the whole thing as a copy-paste block

bash
1
2
3
4
5
f=./chal
file "$f"; echo; checksec --file="$f"; echo
readelf -d "$f"; nm -C "$f" 2>/dev/null | head
strings -n 8 "$f" | grep -iE 'flag|/bin/sh|%s|%n|passwd|system' 
seccomp-tools dump "$f" 2>/dev/null
Five minutes, no GUI, and you know the shape of the challenge.