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).
# recover_path.py
from collections import deque
from 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))# recover_path_runtime.py
from collections import deque
from 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 deque
from 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
# solve.py
from collections import deque
from pathlib import Path
BINARY = "pathfinder"
OFFSET = 0x2020
SIZE = 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}