Skip to main content

Some Assembly Required 3 picoCTF 2021 Solution

A WebAssembly crackme that encrypts the flag using a key hidden inside the compiled module. Analyze and reverse the cipher.

Published: April 2, 2026Updated: August 13, 2026

Description

This challenge extends Some Assembly Required 2 by adding a rotating XOR key. The WASM module encodes the flag with a 5-byte XOR key applied cyclically, then stores the ciphertext in the data segment. Find the key and the ciphertext, then XOR them together to recover the flag.

Remote

Open the challenge URL with DevTools, download the WASM file from the Network tab.

bash
wasm2wat xSAR3.wasm -o xSAR3.wat

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Decompile the WASM and locate the encoding logic
    Observation
    The description mentions a rotating XOR key and a WASM module. Decompile with wasm2wat and wasm-decompile to read the copy_char function, which holds both the key bytes and the XOR index formula.
    Convert the WASM to WAT (or use wasm-decompile for readable pseudocode), then inspect the copy_char function. Compared to Some Assembly Required 2, you will see a new variable that tracks a key index and an XOR operation applied to each character before it is stored. The key index cycles using the expression 4 - (position % 5), which walks the 5-byte key in reverse order.
    bash
    wasm2wat xSAR3.wasm -o xSAR3.wat
    bash
    wasm-decompile xSAR3.wasm -o xSAR3.dcmp
    bash
    grep -n 'xor\|key\|offset' xSAR3.wat | head -40
    What didn't work first

    Tried: Use strings on the WASM binary to find the key and flag directly

    strings extracts only printable ASCII, so it shows neither the key bytes, which are values like 0xf1 and 0xa7, nor the ciphertext, which is equally non-printable. wasm2wat or wasm-decompile reads the data segment as raw hex and lets you trace the encoding logic in copy_char.

    Tried: Skip wasm-decompile and rely solely on the raw WAT output from wasm2wat to find the key index formula

    Raw WAT expresses the XOR and modulo as low-level stack operations spread over many lines with no variable names, which makes the index formula very hard to spot. wasm-decompile produces named pseudocode where the cyclic key pattern is obvious; skipping it costs real analysis time.

    Learn more

    The WASM data segment holds two important regions. The ciphertext (the XOR-encoded flag) starts at memory offset 1024 and is 43 bytes long. The key is stored as 5 bytes beginning at offset 1067 in the order 0xf1, 0xa7, 0xf0, 0x07, 0xed.

    The encoding function XORs each input byte at position i with key[4 - (i % 5)] before comparing. That index formula just walks the key bytes backwards (4, 3, 2, 1, 0, 4, 3, ...), which is equivalent to using the reversed key [0xed, 0x07, 0xf0, 0xa7, 0xf1] in the normal forward direction. Because XOR is its own inverse, decryption is identical to encryption.

  2. Step 2Extract the ciphertext and key, then XOR to recover the flag
    Observation
    The WAT puts the 43-byte ciphertext at memory offset 1024 and the 5-byte key at 1067. XOR is its own inverse, so running the same cyclic XOR over the ciphertext with the reversed key recovers the flag.
    Read the 43 ciphertext bytes from the data segment starting at offset 1024, and the 5 key bytes from offset 1067. Apply the same cyclic XOR the WASM uses: for each byte at position i, XOR with key[4 - (i % 5)]. The result is the flag.
    python
    python3 - <<'EOF'
    # Ciphertext bytes from data segment at offset 1024 (43 bytes)
    ciphertext = bytes([
        0x9d, 0x6e, 0x93, 0xc8, 0xb2,
        0xb9, 0x41, 0x8b, 0x9f, 0x90,
        0x8c, 0x62, 0xc5, 0xc3, 0x95,
        0x88, 0x34, 0xc8, 0x93, 0x92,
        0x88, 0x3f, 0xc1, 0x92, 0xc7,
        0xdb, 0x3f, 0xc8, 0x9e, 0xc7,
        0x89, 0x31, 0xc6, 0xc5, 0xc9,
        0x8b, 0x36, 0xc6, 0xc6, 0xc0,
        0x90, 0x00, 0x00,
    ])
    
    # Key bytes from data segment at offset 1067 (5 bytes, stored reversed)
    key_raw = [0xf1, 0xa7, 0xf0, 0x07, 0xed]
    
    # The WASM indexes the key as key[4 - (i % 5)], which is the reversed key forward.
    key = [key_raw[4 - (i % 5)] for i in range(len(ciphertext))]
    
    flag = bytes(c ^ k for c, k in zip(ciphertext, key))
    print(flag.decode('ascii', errors='replace'))
    EOF

    Expected output

    picoCTF{8aae5dde...}
    What didn't work first

    Tried: XOR the ciphertext with the raw key in forward order (key[i % 5]) using the bytes as stored at offset 1067

    The WASM indexes the stored key as 4 minus i mod 5, which reverses the stored byte order. Index it forward against the raw stored bytes and every key byte is misaligned, so the output is garbage. Either reverse the key array first, or keep the same index formula the WASM uses.

    Tried: Treat the ciphertext as a single-byte XOR and try all 256 possible keys with a brute-force loop

    A single-byte brute force assumes one key byte repeats across the whole ciphertext, but this cipher rotates a 5-byte key. Any single candidate decrypts only one position in five, so all 256 guesses score as noise and the flag never appears. You need the key length and the actual key bytes from the data segment.

    Learn more

    Why the reversed key? The WASM stores the key bytes in reverse order in memory and then accesses them via key[4 - (i % 5)]. When i = 0, the index is 4, which picks the last stored byte (0xed). When i = 1, the index is 3, picking 0x07, and so on. The net effect is identical to storing the key as [0xed, 0x07, 0xf0, 0xa7, 0xf1] and accessing it as key[i % 5]. Both formulations produce the same decryption.

    Why XOR works here: XOR is symmetric, meaning plaintext XOR key = ciphertext and ciphertext XOR key = plaintext. The decryption script is literally the same as the encryption loop in the WASM - you just run it on the ciphertext instead of the user input.

Interactive tools
  • XOR CipherXOR-decrypt hex or text ciphertext with a known key, or brute-force the single-byte key automatically.

Flag

Reveal flag

picoCTF{8aae5dde...}

The WASM encodes the flag with a rotating 5-byte XOR key (cyclic index 4 - (pos % 5)). XOR is its own inverse, so running the same operation on the ciphertext recovers the plaintext.

Key takeaway

Repeating-key XOR, essentially a Vigenere cipher over bytes, breaks the moment the key length is known, because it becomes N independent single-byte problems, one per key position. When the key is stored alongside the ciphertext in the same binary, static analysis yields both the length and the bytes, and the cipher inverts trivially. The same attack works on firmware images, CTF binaries, and obfuscated scripts that rotate XOR to dodge plaintext string detection.

Related reading

Useful tools for Web Exploitation

Where to go next