Gamer - Level 2

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:

  1. Limited Resources: You start with 30 Tokens (Health). Every move consumes tokens equal to the distance moved.
  2. Win Condition: You must land exactly on tile 100 with exactly 0 tokens left.
  3. 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:

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:

  1. The sequence of moves is converted to a byte array.
  2. SHA-256 hash is calculated.
  3. Constraint Check:
    • hash[25] == 0x7c
    • hash[26] == 0x03
    • hash[27] == 0x92
  4. 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):

  1. State: (Current Position, Remaining Tokens, Path Taken).
  2. Recursion: From the current state, try moves 10 down to 1.
  3. Simulation: For each move, calculate the landing position, resolving any chained Snakes/Ladders.
  4. Validation: If we hit 100 with 0 tokens, 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.)