Category: Reverse Engineering
Flag: EHAX{2E3S2W6S8E2NE2S}
Challenge Description
You can go funky ways
Analysis
file "pathfinder"pathfinder: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=49d4c26d0aea83a8776dafd321a309b57fe2a66b, for GNU/Linux 4.4.0, strippedThe binary being a stripped PIE ELF told me I should expect minimal symbol help and runtime-resolved addresses, so I started by hunting high-signal constants and prompts before deep control-flow work.
strings -a "pathfinder" | rg -i "EHAX\{|pathfinder|best path|Flag"EHAX{Are you a pathfinder?Ok, tell me the best path:You have what it takes. Flag: %sThat immediately confirmed two important things: the challenge was definitely expecting a path string as input, and the final formatter already hardcoded the event prefix.
r2 -e scr.color=0 -A -q -c 's 0x11ee;pdg' "pathfinder" | rg "for \(var_ch|0x2020|0x40a0|fcn\.000011c9|\*\(var_ch"for (var_ch = 0; var_ch < 100; var_ch = var_ch + 1) { uVar1 = *(var_ch + 0x2020); uVar2 = fcn.000011c9(var_ch); *(var_ch + 0x40a0) = uVar1 ^ uVar2;r2 -e scr.color=0 -A -q -c 's 0x11c9;pdg' "pathfinder"uint32_t fcn.000011c9(int64_t arg1){ return arg1 << 3 ^ arg1 * 0x1f + 0x11U ^ 0xffffffa5;}This was the first real click: a 100-byte blob from .rodata gets XOR-decoded with an index-dependent keystream into a 10x10 table at 0x40a0. So the “pathfinder” prompt is really a maze/graph walk checker backed by decoded cell metadata.
r2 -e scr.color=0 -A -q -c 's 0x1444;pdg' "pathfinder"bool fcn.00001444(char *arg1){ ... if (*var_18h == '\0') { if ((var_28h == 9) && (var_24h == 9)) { iVar8 = fcn.0000126b(arg1); bVar9 = iVar8 == -0x7945adf4; } ... } uVar3 = *(uVar1 * 0xc + 0x4120); uVar2 = *(uVar1 * 0xc + 0x4128); ... uVar4 = fcn.0000123f(var_28h,var_24h); uVar5 = fcn.0000123f(uVar6,uVar7); if ((uVar5 & (uVar1 * 'k' ^ var_4h._1_1_ ^ 0x3c)) == 0 && (uVar4 & (uVar1 * 'k' ^ var_4h ^ 0x3c)) == 0) { return false; } ...}r2 -e scr.color=0 -A -q -c 's 0x1602;pdg' "pathfinder" | rg "EHAX\{|%d%c|var_14h < 2|\*s = cVar1|\*s = '\\}'|sprintf"iVar2 = sym.imp.sprintf(arg2,"EHAX{");if (var_14h < 2) { *s = cVar1; iVar2 = sym.imp.sprintf(s,"%d%c",var_14h,cVar1);*s = '}';From the validator, each character (N/S/E/W) selects a 12-byte movement descriptor from 0x4120, updates (x,y) accumulators, and checks movement legality with per-cell bitmasks. The end condition is exact: land on (9,9) and satisfy the hash gate. The formatter at 0x1602 run-length-encodes the successful raw path between EHAX{ and } (single chars stay literal, repeated runs become count+char).
from collections import dequefrom pathlib import Path
blob = Path("pathfinder").read_bytes()[0x2020:0x2020 + 100]
def key(i: int) -> int: return ((((i << 3) & 0xFFFFFFFF) ^ ((i * 0x1F + 0x11) & 0xFFFFFFFF) ^ 0xFFFFFFA5) & 0xFF)
grid = [b ^ key(i) for i, b in enumerate(blob)]
step = { "N": (-1, 0, 0x04, 0x01), "S": ( 1, 0, 0x01, 0x04), "E": ( 0, 1, 0x02, 0x08), "W": ( 0,-1, 0x08, 0x02),}
def cell(r, c): return grid[r * 10 + c]
def can_move(r, c, ch): dr, dc, m_cur, m_next = step[ch] nr, nc = r + dr, c + dc if not (0 <= nr < 10 and 0 <= nc < 10): return False return not ((cell(nr, nc) & m_next) == 0 and (cell(r, c) & m_cur) == 0)
q = deque([(0, 0, "")])seen = {(0, 0)}while q: r, c, p = q.popleft() if (r, c) == (9, 9): print("path", p) break for ch in "NSEW": if can_move(r, c, ch): dr, dc, _, _ = step[ch] nr, nc = r + dr, c + dc if (nr, nc) not in seen: seen.add((nr, nc)) q.append((nr, nc, p + ch))from collections import dequefrom pathlib import Path
b = Path("pathfinder").read_bytes()enc = b[0x2020:0x2020 + 100]
def key(i: int) -> int: return ((((i << 3) & 0xFFFFFFFF) ^ ((i * 0x1F + 0x11) & 0xFFFFFFFF) ^ 0xFFFFFFA5) & 0xFF)
grid = [c ^ key(i) for i, c in enumerate(enc)]step = { "N": (-1, 0, 0x04, 0x01), "S": (1, 0, 0x01, 0x04), "E": (0, 1, 0x02, 0x08), "W": (0, -1, 0x08, 0x02),}
def cell(r: int, c: int) -> int: return grid[r * 10 + c]
def ok(r: int, c: int, ch: str) -> bool: dr, dc, m0, m1 = step[ch] nr, nc = r + dr, c + dc if not (0 <= nr < 10 and 0 <= nc < 10): return False return not (((cell(nr, nc) & m1) == 0) and ((cell(r, c) & m0) == 0))
q = deque([(0, 0, "")])seen = {(0, 0)}
while q: r, c, p = q.popleft() if (r, c) == (9, 9): print("path", p) break for ch in "NSEW": if ok(r, c, ch): dr, dc, _, _ = step[ch] nr, nc = r + dr, c + dc if (nr, nc) not in seen: seen.add((nr, nc)) q.append((nr, nc, p + ch))from collections import dequefrom pathlib import Path
b = Path("pathfinder").read_bytes()enc = b[0x2020:0x2020 + 100]def key(i): return ((((i << 3) & 0xffffffff) ^ ((i * 0x1f + 0x11) & 0xffffffff) ^ 0xffffffa5) & 0xff)grid = [c ^ key(i) for i, c in enumerate(enc)]step = { "N": (-1, 0, 0x04, 0x01), "S": (1, 0, 0x01, 0x04), "E": (0, 1, 0x02, 0x08), "W": (0, -1, 0x08, 0x02),}def cell(r, c): return grid[r * 10 + c]def ok(r, c, ch): dr, dc, m0, m1 = step[ch]; nr, nc = r + dr, c + dc if not (0 <= nr < 10 and 0 <= nc < 10): return False return not (((cell(nr, nc) & m1) == 0) and ((cell(r, c) & m0) == 0))q = deque([(0, 0, "")]); seen={(0,0)}while q: r,c,p=q.popleft() if (r,c)==(9,9): print("path",p) break for ch in "NSEW": if ok(r,c,ch): dr,dc,_,_=step[ch] nr,nc=r+dr,c+dc if (nr,nc) not in seen: seen.add((nr,nc)); q.append((nr,nc,p+ch))path EESSSWWSSSSSSEEEEEEEENNESSOnce the BFS gave a single clean route to (9,9), that path was exactly what the checker wanted.

printf "y\nEESSSWWSSSSSSEEEEEEEENNESS\n" | ./pathfinderAre you a pathfinder?[y/n]: Ok, tell me the best path: You have what it takes. Flag: EHAX{2E3S2W6S8E2NE2S}Bye.The binary accepted the route and printed the final run-length encoded flag directly.
Solution
from collections import dequefrom pathlib import Path
BINARY = "pathfinder"OFFSET = 0x2020SIZE = 100
def key(i: int) -> int: return ((((i << 3) & 0xFFFFFFFF) ^ ((i * 0x1F + 0x11) & 0xFFFFFFFF) ^ 0xFFFFFFA5) & 0xFF)
def decode_grid() -> list[int]: data = Path(BINARY).read_bytes()[OFFSET:OFFSET + SIZE] return [b ^ key(i) for i, b in enumerate(data)]
def can_move(grid: list[int], r: int, c: int, d: str) -> tuple[bool, int, int]: step = { "N": (-1, 0, 0x04, 0x01), "S": ( 1, 0, 0x01, 0x04), "E": ( 0, 1, 0x02, 0x08), "W": ( 0,-1, 0x08, 0x02), } dr, dc, m_cur, m_next = step[d] nr, nc = r + dr, c + dc if not (0 <= nr < 10 and 0 <= nc < 10): return False, nr, nc
def cell(x: int, y: int) -> int: return grid[x * 10 + y]
blocked = (cell(nr, nc) & m_next) == 0 and (cell(r, c) & m_cur) == 0 return (not blocked), nr, nc
def shortest_path(grid: list[int]) -> str: q = deque([(0, 0, "")]) seen = {(0, 0)} while q: r, c, p = q.popleft() if (r, c) == (9, 9): return p for d in "NSEW": ok, nr, nc = can_move(grid, r, c, d) if ok and (nr, nc) not in seen: seen.add((nr, nc)) q.append((nr, nc, p + d)) raise RuntimeError("No valid path found")
def rle_path(path: str) -> str: out = [] i = 0 while i < len(path): j = i while j < len(path) and path[j] == path[i]: j += 1 run = j - i if run == 1: out.append(path[i]) else: out.append(f"{run}{path[i]}") i = j return "".join(out)
def main() -> None: grid = decode_grid() path = shortest_path(grid) print(f"EHAX{{{rle_path(path)}}}")
if __name__ == "__main__": main()python3.12 solve.pyEHAX{2E3S2W6S8E2NE2S}