Gamer - Level 2
- Category: Reversing
Description
Since you like games so much, I made a harder challenge just for you
Challenge by Eli Kaski
Sources were attached.
Solution
We are presented with a binary implementing a text-based “Snakes and Ladders” game. The objective seems simple: navigate from position 1 to position 100.
However, the game introduces a twist:
- Limited Resources: You start with 30 Tokens (Health). Every move consumes tokens equal to the distance moved.
- Win Condition: You must land exactly on tile
100with exactly0tokens left. - Hidden Validation: Winning isn’t enough. The game hashes your sequence of moves to decrypt the flag.
1. The Game Loop (main / sub_401a00)
The decompiled code reveals the core mechanics:
- Start: Position 1, Tokens 30.
- Input: Accepts integers between
1and10. - Update:
Position += Input,Tokens -= Input.
Code Snapshot:
v6 = 1; // Starting Position
v4 = 30; // Starting Health (Tokens)
while ( v4 > 0 )
{
sub_401050(a1, (int)aYourHealthIsDA, v6, v4);
// ... input parsing ...
if ( v7 <= 0 || v7 > 10 || v7 > v4 || v7 + v6 > 100 ) {
sub_401050(a1, (int)aInvalid_0);
return 0;
}
v6 += v7; // Move Position
v4 -= v7; // Deduct Tokens
*((_BYTE *)&v9 + v3++) = v7; // Store move in history
2. The Board Logic (Chained Jumps)
The board logic is interesting. It checks two arrays: triggers (Snakes/Ladders start) and targets (destinations).
Crucially, the assembly shows a reset mechanism in the check loop:
Code Snapshot:
for ( i = 0; i < 0x11; ++i )
{
if ( byte_421900[2 * i] == v6 ) // Check if current tile is a Snake/Ladder
{
v6 = byte_421901[2 * i]; // Jump to destination
i = 0; // THE SCAN RESETS!
}
}
Implication: Jumps are recursive. If a Snake drops you onto a Ladder, you immediately take the Ladder. This chain reaction must be simulated correctly.
3. The Validation (sub_401890)
If the win condition is met (Pos == 100 and Tokens == 0), the binary verifies the path:
- The sequence of moves is converted to a byte array.
- SHA-256 hash is calculated.
- Constraint Check:
hash[25] == 0x7chash[26] == 0x03hash[27] == 0x92
- Decryption: If the constraints match, the hash is XORed with a hardcoded stack string to reveal the flag.
Code Snapshot:
// Identifying SHA-256 IVs in sub_4014E0
a1[20] = 1779033703; // 0x6A09E667
a1[21] = -1150833019; // 0xBB67AE85
a1[22] = 1013904242; // 0x3C6EF372
// Flag Decryption in sub_401890
if ( v31 == 124 || v32 == 3 || v33 == 146 ) // 0x7c, 0x03, 0x92
{
for ( i = 0; i < 24; ++i )
v29[i] ^= *(&v4 + i); // XOR SHA-256 hash with stack data
sub_401050(a1, (int)aBsidestlv2025S, v29);
}
Solution Strategy
This challenge was particularly annoying because of a misleading error message.
If you find a valid winning path that doesn’t match the specific hash, the game mocks you with:
“You did it but there is a better solution! Try combining steps and find a shorter input.”
This sent us down a rabbit hole trying to minimize the number of steps. We manually found a valid winning path of only 5 moves: [4, 7, 10, 2, 7].
However, the game still rejected it with the same “find a shorter input” message.
The Realization: The hint is a red herring (or at least very poorly phrased). The game doesn’t actually care about the length of the input; it only cares if the SHA-256 hash of the input matches the hardcoded bytes. The “shorter input” message is just a generic failure string for “Hash Mismatch.”
To solve this properly, we must ignore the length hint and write a script to find the exact path that satisfies the hash.
The Algorithm (DFS):
- State:
(Current Position, Remaining Tokens, Path Taken). - Recursion: From the current state, try moves
10down to1. - Simulation: For each move, calculate the landing position, resolving any chained Snakes/Ladders.
- Validation: If we hit
100with0tokens, immediately check if the path satisfies the SHA-256 constraints.
Solution Script
import hashlib
# --- Game Logic Setup ---
# Snakes and Ladders Mapping (Trigger -> Destination)
JUMPS = {
3: 65, 4: 93, 11: 21, 12: 2, 14: 52,
27: 9, 31: 21, 39: 28, 40: 97, 49: 33,
53: 43, 55: 36, 56: 38, 67: 76, 78: 69,
98: 27, 99: 74
}
# The XOR Key for flag decryption
# Constructed from stack variables in sub_401890
KEY = (
b"j/&L" +
b"\xc4\xc0\x76\x70\x90\xd1\x87\x9b\x2c\x4b\x91\x20\x8a\x2a\x7f\x2e\xcb\x1d" +
b"\x43\x39"
)
# Target Hash constraints (from sub_401890)
TARGET_BYTE_25 = 0x7c
TARGET_BYTE_26 = 0x03
TARGET_BYTE_27 = 0x92
def resolve_position(pos):
"""Handles recursive/chained jump logic."""
while True:
moved = False
if pos in JUMPS:
pos = JUMPS[pos]
moved = True
if not moved:
break
return pos
def check_path_hash(path):
"""Hashes the path and checks constraints."""
path_bytes = bytes(path)
sha256 = hashlib.sha256(path_bytes).digest()
if (sha256[25] == TARGET_BYTE_25 and
sha256[26] == TARGET_BYTE_26 and
sha256[27] == TARGET_BYTE_27):
# Decrypt Flag
flag_chars = []
for i in range(24):
flag_chars.append(chr(sha256[i] ^ KEY[i]))
return "".join(flag_chars)
return None
def solve_dfs(current_pos, health, path):
"""Recursive DFS prioritizing larger moves (10 down to 1)."""
# Base Case: Winning State
if health == 0:
if current_pos == 100:
# Crucial Step: Check the hash, not just the win condition
flag = check_path_hash(path)
if flag:
print(f"\n[+] WINNING PATH FOUND: {path}")
print(f"[+] FLAG: BSidesTLV2025{{{flag}}}")
return True
return False
# Pruning
if current_pos > 100:
return False
# Try largest moves first
for move in range(10, 0, -1):
if move <= health:
next_raw = current_pos + move
if next_raw > 100:
continue
next_pos = resolve_position(next_raw)
if solve_dfs(next_pos, health - move, path + [move]):
return True
return False
if __name__ == "__main__":
print("[*] Starting DFS search...")
solve_dfs(1, 30, [])
[*] Starting DFS search...
[+] WINNING PATH FOUND: [3, 5, 5, 1, 6, 7, 3]
[+] FLAG: BSidesTLV2025{YoU_aRe_A_Tru3_G4m3r!!!!}
The script bypasses the misleading hint and finds the one true path that satisfies the hash.
Winning Sequence: [3, 5, 5, 1, 6, 7, 3]
(Note: This is 7 moves long, which is ironically longer than our manual 5-move solution that was rejected.)