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.
Setup
Open the challenge URL with DevTools, download the WASM file from the Network tab.
wasm2wat xSAR3.wasm -o xSAR3.watSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Decompile the WASM and locate the encoding logicObservationI noticed the challenge description mentioned a rotating XOR key and a WASM module, which suggested decompiling the binary with wasm2wat and wasm-decompile to read the copy_char function and identify both the key bytes and the XOR index formula embedded in the data segment.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.bashwasm2wat xSAR3.wasm -o xSAR3.watbashwasm-decompile xSAR3.wasm -o xSAR3.dcmpbashgrep -n 'xor\|key\|offset' xSAR3.wat | head -40What didn't work first
Tried: Use strings on the WASM binary to find the key and flag directly
strings extracts only printable ASCII sequences and will not reveal the XOR key bytes (which are non-ASCII values like 0xf1, 0xa7, 0xed) or the ciphertext (also non-printable). You need wasm2wat or wasm-decompile to read the binary data segment as raw hex values and to trace the encoding logic in the copy_char function.
Tried: Skip wasm-decompile and rely solely on the raw WAT output from wasm2wat to find the key index formula
Raw WAT represents the XOR and modulo as low-level stack operations (i32.xor, i32.rem_u, i32.sub) spread across many lines with no variable names, making the index formula 4 - (i % 5) very hard to recognize. wasm-decompile produces named pseudocode that makes the cyclic key pattern immediately visible; skipping it costs significant 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.Step 2
Extract the ciphertext and key, then XOR to recover the flagObservationI noticed the WAT disassembly showed the 43-byte ciphertext at memory offset 1024 and the 5-byte key at offset 1067, and that XOR is its own inverse, which meant running the same cyclic XOR operation on the ciphertext with the reversed key would directly recover 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.pythonpython3 - <<'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')) EOFExpected 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 accesses the stored key with the index formula key[4 - (i % 5)], which reverses the stored byte order. Using key[i % 5] against the raw stored bytes [0xf1, 0xa7, 0xf0, 0x07, 0xed] produces garbage output because the key schedule is wrong - every key byte is misaligned. You must either reverse the key array first or keep the 4 - (i % 5) index formula to match the WASM's access pattern.
Tried: Treat the ciphertext as a single-byte XOR and try all 256 possible keys with a brute-force loop
Single-byte XOR brute force assumes one key byte repeats across the entire ciphertext, but this cipher uses a 5-byte rotating key. With a 5-byte key, any single candidate byte only decrypts one out of five positions correctly, so the scored output looks like random noise for all 256 guesses and the flag never surfaces. You need to know the key length (5) and read 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 askey[i % 5]. Both formulations produce the same decryption.Why XOR works here: XOR is symmetric, meaning
plaintext XOR key = ciphertextandciphertext 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.