//Nononono
>tl;dr
-
64-bit PIE that checks a 10×10×10×10 grid of digits (0–3) encoded as 1000 text lines of 10 chars (plus newlines) → 11,000-byte file.
-
Each line is validated by a run-length rule checker at 0x1c00 using per-axis clues pulled from .rodata.
-
Logged all 4,000 line constraints via GDB script, then solved the nonogram-like puzzle with a constraint propagating backtracker.
-
Produced the valid 1000-line grid, fed it to the binary, got the flag.
Flag: CyKor{31a0488743b8887ab460573b2d9f48ccee3185b48f727666708a95c71970205d}
>Recon and Input Format
-
file→ 64-bit PIE, stripped. Size check in main compares read length to 0x2af8 (10 * 1000 + 1000 newlines = 11,000 bytes). -
Parsing loop (0x14d8–0x1594) walks lines with counter
r15from 0 to 0x3e7 and expects 0x0a terminators at position 10. -
The index math (0x150e–0x1567) maps the current line number
r15to a base pointer offsetrdx = r13 + f(r15), wheref(r15) = r15 * 10. So line n corresponds to 10 cells starting atgrid[10*n]in a flattened 10⁴ array. -
Characters are validated as digits 0–3, then stored as bytes.
>The Validator (0x1c00)
-
Arguments:
edi = axis(0..3),rdx = pointer to 4 ints(axis coordinates),rsi = pointer to clue bytes (20 bytes: len/color pairs, zero-terminated). -
Algorithm: Treats the 10 cells along the chosen axis as a run-length encoded line. It enforces alternating blocks of the same color, with optional gaps when color repeats.
-
If runs match exactly and there are no leftover non-zero cells, it returns 1; otherwise 0.
-
Main iterates 1000 times (each axis has 250 lines of 10 cells → 4 axes × 1000 checks). Any failure aborts; all passes drop to success message.
>Extracting Constraints (GDB Script)
I disabled ASLR and logged every call to the validator while forcing it to return success so execution would continue and emit all clues.
Saved as log.gdb:
set pagination off
set logging file gdb_calls.log
set logging overwrite on
set logging enabled on
set disable-randomization on
file ./chall
set args dummy.txt
break *0x555555555c00
commands
silent
set $dptr = (unsigned int*)$rdx
printf "%u %u %u %u %u ", $edi, $dptr[0], $dptr[1], $dptr[2], $dptr[3]
set $cptr = (unsigned char*)$rsi
printf "%02x %02x %02x %02x %02x %02x %02x %02x %02x %02x ", $cptr[0],$cptr[1],$cptr[2],$cptr[3],$cptr[4],$cptr[5],$cptr[6],$cptr[7],$cptr[8],$cptr[9]
printf "%02x %02x %02x %02x %02x %02x %02x %02x %02x %02x\n", $cptr[10],$cptr[11],$cptr[12],$cptr[13],$cptr[14],$cptr[15],$cptr[16],$cptr[17],$cptr[18],$cptr[19]
set $eax = 1 # force success
set $pc = 0x555555555c8d # skip epilogue and continue
continue
end
run
quit
Run it with a dummy 1000-line file of zeros to trigger all calls:
gdb -q -x log.gdb
Output: gdb_calls.log containing 4000 lines of axis d0 d1 d2 d3 runs....
>Modeling the Puzzle
-
Grid indices:
idx = (((d0*10 + d1)*10 + d2)*10 + d3). -
Each log line defines one of the 1000 lines: fix the 3 coords, vary the axis coordinate 0..9.
-
Runs are (length, color) pairs until a zero length; colors are 0..3.
-
This is a 4D nonogram: find assignments in {0,1,2,3} satisfying all 1000 line constraints.
>Solver
I wrote a constraint propagating backtracker in solve.py. Key pieces:
-
generate_patterns(runs): enumerates all 10-cell patterns matching the run-length rule (with the required gap when the same color repeats back-to-back). -
propagate(): prunes cell domains using remaining-valid patterns per line until fixed point. -
solve(): choose a cell with minimal branching, recurse; returns first consistent solution. -
write_solution(): outputs the 1000 lines in the same order the binary expects (line n corresponds to (n//100, (n//10)%10, n%10) with varying d3).
Full solver code:
import sys
from collections import defaultdict
LOG_PATH = "gdb_calls.log"
GRID_SIZE = 10
VALUES = {0, 1, 2, 3}
def parse_log(path: str):
lines = []
with open(path, "r") as f:
for raw in f:
parts = raw.strip().split()
if len(parts) != 25:
continue
axis = int(parts[0])
coords = tuple(int(x) for x in parts[1:5])
bytes_list = [int(x, 16) for x in parts[5:]]
runs = []
for i in range(0, 20, 2):
length = bytes_list[i]
color = bytes_list[i + 1]
if length == 0:
break
runs.append((length, color))
lines.append((axis, coords, runs))
return lines
def encode_index(c0: int, c1: int, c2: int, c3: int) -> int:
return ((c0 * GRID_SIZE + c1) * GRID_SIZE + c2) * GRID_SIZE + c3
def min_remaining(runs, start_idx, prev_color):
total = 0
last_color = prev_color
for length, color in runs[start_idx:]:
gap = 1 if color == last_color else 0
total += gap + length
last_color = color
return total
def generate_patterns(runs):
if not runs:
return [[0] * GRID_SIZE]
patterns = []
def dfs(idx, acc, prev_color):
if idx == len(runs):
if len(acc) <= GRID_SIZE:
patterns.append(acc + [0] * (GRID_SIZE - len(acc)))
return
length, color = runs[idx]
min_gap = 1 if (prev_color == color) else 0
remaining_min = min_remaining(runs, idx + 1, color)
max_gap = GRID_SIZE - len(acc) - length - remaining_min
if max_gap < min_gap:
return
for gap in range(min_gap, max_gap + 1):
new_acc = acc + [0] * gap + [color] * length
dfs(idx + 1, new_acc, color)
dfs(0, [], -1)
return patterns
def build_line_data(log_lines):
line_cells = []
line_patterns = []
for axis, coords, runs in log_lines:
base = list(coords)
cells = []
for i in range(GRID_SIZE):
c = base.copy()
c[axis] = i
cells.append(encode_index(*c))
line_cells.append(tuple(cells))
line_patterns.append(generate_patterns(runs))
return line_cells, line_patterns
def propagate(domains, line_cells, line_patterns, cell_to_lines):
num_lines = len(line_cells)
to_check = set(range(num_lines))
while to_check:
line_idx = to_check.pop()
cells = line_cells[line_idx]
patterns = line_patterns[line_idx]
valid_patterns = []
for pat in patterns:
if all(pat[i] in domains[cells[i]] for i in range(GRID_SIZE)):
valid_patterns.append(pat)
if not valid_patterns:
return None
possible = [set() for _ in range(GRID_SIZE)]
for pat in valid_patterns:
for i, val in enumerate(pat):
possible[i].add(val)
for i, cell in enumerate(cells):
new_dom = domains[cell] & possible[i]
if not new_dom:
return None
if new_dom != domains[cell]:
domains[cell] = new_dom
to_check.update(cell_to_lines[cell])
return domains
def solved(domains):
return all(len(d) == 1 for d in domains)
def choose_cell(domains):
best_idx = None
best_size = None
for idx, dom in enumerate(domains):
size = len(dom)
if size > 1 and (best_size is None or size < best_size):
best_size = size
best_idx = idx
return best_idx
def solve(domains, line_cells, line_patterns, cell_to_lines):
domains = propagate(domains, line_cells, line_patterns, cell_to_lines)
if domains is None:
return None
if solved(domains):
return domains
cell = choose_cell(domains)
if cell is None:
return domains
for val in list(domains[cell]):
next_domains = [set(d) for d in domains]
next_domains[cell] = {val}
result = solve(next_domains, line_cells, line_patterns, cell_to_lines)
if result is not None:
return result
return None
def write_solution(domains, path):
with open(path, "w") as f:
for n in range(1000):
d0 = n // 100
d1 = (n // 10) % 10
d2 = n % 10
line_vals = []
for d3 in range(10):
idx = encode_index(d0, d1, d2, d3)
val_set = domains[idx]
if len(val_set) != 1:
raise RuntimeError("Unresolved cell in output")
val = next(iter(val_set))
line_vals.append(str(val))
f.write("".join(line_vals) + "\n")
def main():
log_lines = parse_log(LOG_PATH)
line_cells, line_patterns = build_line_data(log_lines)
cell_to_lines = defaultdict(set)
for i, cells in enumerate(line_cells):
for cell in cells:
cell_to_lines[cell].add(i)
domains = [set(VALUES) for _ in range(GRID_SIZE ** 4)]
solution = solve(domains, line_cells, line_patterns, cell_to_lines)
if solution is None:
print("No solution found", file=sys.stderr)
sys.exit(1)
print("Solved")
write_solution(solution, "solution.txt")
if __name__ == "__main__":
main()
>Solving
-
Generate log:
gdb -q -x log.gdb→gdb_calls.log. -
Run solver:
python3 solve.py→ printsSolved, writessolution.txt(1000 lines). -
Validate:
./chall solution.txt→ printsCorrect!and the flag.
>Notes & Pitfalls
-
Breakpoint address must be absolute with ASLR off; otherwise misses the validator.
-
The index math is just
line_index * 10; discovering that avoided off-by-one crashes when writing output. -
Patterns generation requires a gap of 1 when the same color repeats consecutively; missing this causes contradictions.
>Result
./chall solution.txt → Correct! and the flag above. Easy to reproduce: just rerun the steps with the included scripts.