Змейка (Snake)
Змейка (Snake)
Platform: duckerz | Category: Misc | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-01-09 | Status: Solved Techniques: ascii_table_parsing, expression_evaluation, graph_traversal, hash_cracking
Summary
Task: ASCII grid with snake path containing numbers and operators, solve 100 puzzles, decode SHA256 hashes. Solution: Parse ASCII table, traverse snake path using graph traversal, evaluate expression with proper operator precedence, brute-force 2-character SHA256 hashes.
Змейка (Snake) - duckerz CTF
Description
Я люблю математику. И играть в змейку. (I love math. And playing snake.)
Connection: nc tasks.duckerz.ru 30002
Analysis
When connecting to the service, an ASCII grid is displayed with a "snake" - a path of numbers and mathematical operators (+, -, *, /). The task requires solving 100 such puzzles.
Grid example:
+-----+---+-----+-----+-----+-----+-----+-----+-----+
| 839 | - | 868 | + | 782 | + | 710 | + | 803 |
+-----+---+-----+-----+-----+-----+-----+-----+-----+
| | | | | | | | | * |
+-----+---+-----+-----+-----+-----+-----+-----+-----+
| | | | | | | | | 358 |
+-----+---+-----+-----+-----+-----+-----+-----+-----+
Key observations:
- Snake - a connected path through grid cells
- The path contains alternating numbers and operators
- Need to find the start and end of the snake (cells with one neighbor)
- IMPORTANT: Calculation must use standard operator precedence (* and / before + and -)
Solution
Step 1: Parsing the ASCII table
python read-only • copy alloweddef parse_grid(data): """Parse ASCII table into 2D array of cell values""" lines = data.strip().split('\n') grid = [] for line in lines: if line.startswith('+') or not line.strip(): continue if line.startswith('|'): cells = line.split('|')[1:-1] row = [cell.strip() for cell in cells] grid.append(row) return grid
Step 2: Finding the snake path
python read-only • copy alloweddef find_snake_path(grid): """Find connected path through non-empty cells""" if not grid: return [] rows, cols = len(grid), len(grid[0]) if grid else 0 # Collect all non-empty cells non_empty = {} for r in range(rows): for c in range(len(grid[r])): if grid[r][c]: non_empty[(r, c)] = grid[r][c] def get_neighbors(r, c): """Get adjacent non-empty cells""" neighbors = [] for dr, dc in [(0, 1), (0, -1), (1, 0), (-1, 0)]: nr, nc = r + dr, c + dc if (nr, nc) in non_empty: neighbors.append((nr, nc)) return neighbors # Find start point (first non-empty cell in first row) start = None for c in range(cols): if (0, c) in non_empty: start = (0, c) break # Traverse the snake path path = [] visited = set() current = start while current: path.append(non_empty[current]) visited.add(current) # Find next unvisited neighbor next_cell = None for neighbor in get_neighbors(*current): if neighbor not in visited: next_cell = neighbor break current = next_cell return path
Step 3: Evaluating the expression
CRITICALLY IMPORTANT: Use standard operator precedence!
python read-only • copy alloweddef calculate_expression(path): """Calculate expression with proper operator precedence""" expr = ''.join(path) return eval(expr) # eval() respects operator precedence
Step 4: Decoding the flag
After solving 100 puzzles, the server outputs 16 SHA256 hashes:
5d544acfc7fb56cda14aedb9c796836947a7cd98070bc4248884425e498b0e0a
8d5579affb518db0185bafeb983ab99b73664d3d9d3a7c1defef61749474611a
7a475a8b0a074fa8bd3bf54dc2a172de8c70f8ad78980010b9d8e9bc5a5b978f
... (16 total)
Each hash is SHA256 of a 2-character string. Brute-force:
python read-only • copy allowedimport hashlib import string def crack_hash(target_hash): """Brute-force 2-character SHA256 hash""" charset = string.printable for c1 in charset: for c2 in charset: candidate = c1 + c2 if hashlib.sha256(candidate.encode()).hexdigest() == target_hash: return candidate return None # Results: # DU + CK + ER + Z{ + ea + s4 + _c + 4l + 3_ + 13 + _m + 1n + 3_ + b3 + s9 + !}
Full solution script
python read-only • copy allowed#!/usr/bin/env python3 """ Snake CTF Challenge Solver duckerz CTF - Task 25 """ import socket import re import hashlib import string def parse_grid(data): """Parse ASCII table into 2D array""" lines = data.strip().split('\n') grid = [] for line in lines: if line.startswith('+') or not line.strip(): continue if line.startswith('|'): cells = line.split('|')[1:-1] row = [cell.strip() for cell in cells] grid.append(row) return grid def find_snake_path(grid): """Find connected path through non-empty cells""" if not grid: return [] rows, cols = len(grid), len(grid[0]) if grid else 0 non_empty = {} for r in range(rows): for c in range(len(grid[r])): if grid[r][c]: non_empty[(r, c)] = grid[r][c] def get_neighbors(r, c): neighbors = [] for dr, dc in [(0, 1), (0, -1), (1, 0), (-1, 0)]: nr, nc = r + dr, c + dc if (nr, nc) in non_empty: neighbors.append((nr, nc)) return neighbors # Find start start = next((0, c) for c in range(cols) if (0, c) in non_empty) path, visited, current = [], set(), start while current: path.append(non_empty[current]) visited.add(current) current = next((n for n in get_neighbors(*current) if n not in visited), None) return path def calculate_expression(path): """Calculate with proper operator precedence""" expr = ''.join(path) return eval(expr) def crack_2char_hash(target_hash): """Brute-force 2-character SHA256""" charset = string.printable for c1 in charset: for c2 in charset: candidate = c1 + c2 if hashlib.sha256(candidate.encode()).hexdigest() == target_hash: return candidate return "??" def solve(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(("tasks.duckerz.ru", 30002)) sock.settimeout(10) for round_num in range(100): # Receive grid data = b"" while b"Answer:" not in data: data += sock.recv(4096) grid_text = data.decode('utf-8', errors='ignore') # Parse and solve grid = parse_grid(grid_text) path = find_snake_path(grid) answer = calculate_expression(path) print(f"Round {round_num + 1}: {answer}") # Send answer sock.send(f"{answer}\n".encode()) # Receive hashes final_data = sock.recv(8192).decode() hashes = re.findall(r'[a-f0-9]{64}', final_data) # Crack hashes flag = "" for h in hashes: flag += crack_2char_hash(h) print(f"Flag: {flag}") sock.close() if __name__ == "__main__": solve()
Lessons
- Operator precedence: Don't calculate left to right! Use
eval()or a proper parser - Graph traversal: The snake is a graph where you need to find a path between two endpoints
- Short string hashes: 2-character hashes are easily brute-forced (~10000 combinations)
- Automation: 100 rounds = mandatory script
References
- Python eval() documentation
- Operator precedence in Python