gogo picoCTF 2021 Solution

Published: April 2, 2026

Description

A weird binary compiled from Go. It asks for a password and then for an 'unhashed key'. The Go runtime makes the disassembly large, but the check reduces to recovering one 32-character password by XOR and one short key by cracking an MD5 hash.

Remote + binary

Disassemble the Go binary, recover both answers, then connect and submit them in order.

bash
nc mercury.picoctf.net <PORT_FROM_INSTANCE>

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Locate the two checks in the Go binary
    Observation
    I noticed the binary prompts for two separate inputs ('Enter Password:' and 'What is the unhashed key?'), which suggested there were two independent validation stages in the disassembly that needed to be located and analyzed separately.
    Load the ELF in Ghidra. Despite the Go noise, the validation is two stages. Stage one ('Enter Password:') XORs your 32-character input against a hardcoded byte array and compares it to a second hardcoded byte array. Stage two ('What is the unhashed key?') takes your input, MD5-hashes it, and compares the digest to a stored hash.
    bash
    ghidra gogo
    bash
    # Find the two prompt strings, then the XOR loop and the MD5 comparison.

    Expected output

    reverseengineericanbarelyforward
    What didn't work first

    Tried: Run 'strings gogo' to find the password in the binary before opening Ghidra.

    strings will surface plenty of Go runtime symbols and even the MD5 hex digest, but the 32-character password is not stored as a printable string - it is reconstructed at runtime by XORing two byte arrays. strings only shows data that is already ASCII in the binary, so the obfuscated array bytes appear as garbage or are skipped entirely. You need to trace the actual XOR loop in the disassembly to obtain the plaintext value.

    Tried: Look for a single 'password check' function and assume both answers come from the same place.

    The binary has two independent validation stages at separate call sites. Stage one XORs your input and compares arrays; stage two hashes your input with MD5 and compares digests. Treating them as one check causes you to submit the XOR password at the hash prompt or vice versa, which fails. Each prompt requires a different derived value found in a different code path.

    Learn more

    The binary gates the flag behind two independent answers, and it is easy to conflate them. They are different values for different prompts:

    1. Password (32 chars): recovered by XORing two hardcoded arrays. This is the value typed at the Enter Password: prompt.
    2. Unhashed key (short word): the preimage of the stored MD5 hash. This is the value typed at the What is the unhashed key? prompt.
  2. Step 2
    Recover the 32-character password by XOR
    Observation
    I noticed that Ghidra showed the first validation stage comparing XOR'd input against a second hardcoded byte array, which suggested the plaintext password was simply the XOR of those two stored arrays.
    Pull both hardcoded byte arrays out of the disassembly and XOR them together. The result is the 32-character password. (Note that one of the two arrays is the ASCII text of an MD5 hex digest, which is the same hash you crack in the next step.)
    python
    python3 - <<'PY'
    from pwn import xor
    
    a = bytes.fromhex("3836313833366631336533643632376466613337356264623833383932313465")
    b = bytes.fromhex("4a53475d414503545d025a0a5357450d05005d555410010e4155574b45504601")
    print(xor(a, b).decode())   # -> reverseengineericanbarelyforward
    PY
    What didn't work first

    Tried: XOR only one of the two byte arrays against 0xFF or a guessed key instead of XORing both arrays together.

    The obfuscation pairs two arrays so that A XOR B equals the plaintext. Applying a constant like 0xFF to just one array produces 32 bytes of garbage because the key is the other hardcoded array, not a simple single-byte value. Both arrays must be extracted from the disassembly and XORed with each other to recover the printable ASCII password.

    Tried: Decode the first byte array directly as ASCII, since it contains hex-like characters.

    The first array when decoded as ASCII spells out the MD5 hex digest 861836f13e3d627dfa375bdb8389214e, which looks like a password but is not. It is the hash for the second prompt. Using that hex string at the 'Enter Password:' prompt fails because the binary compares the XOR result, not the raw array value. You still need to XOR both arrays to get the actual 32-character password.

    Learn more

    XOR of the two arrays yields reverseengineericanbarelyforward, which is exactly 32 characters. This is the password for the first prompt. The first array, 3836..., is ASCII for the hex string 861836f13e3d627dfa375bdb8389214e, which is the MD5 hash you deal with next.

  3. Step 3
    Crack the MD5 hash to get the unhashed key
    Observation
    I noticed the second validation stage passed the user's input through Go's crypto/md5 and compared the resulting 32-hex-character digest to the hardcoded hash 861836f13e3d627dfa375bdb8389214e, which suggested cracking that MD5 via a rainbow table to recover the plaintext preimage.
    The stored hash is 861836f13e3d627dfa375bdb8389214e. Look it up in a rainbow table (CrackStation) or crack it with hashcat. It resolves to 'goldfish'. That word is the answer to the 'What is the unhashed key?' prompt.
    bash
    # Paste 861836f13e3d627dfa375bdb8389214e into crackstation.net
    bash
    # or:
    bash
    hashcat -m 0 -a 0 861836f13e3d627dfa375bdb8389214e rockyou.txt
    bash
    # -> goldfish
    What didn't work first

    Tried: Run hashcat with mode -m 1800 (sha512crypt) instead of -m 0 (raw MD5) because the binary is a 'hashed key' comparison.

    Mode -m 1800 targets salted Unix password hashes, not bare MD5 digests. hashcat will reject the 32-character hex string as an invalid hash format for that mode. The binary calls Go's standard crypto/md5 on the raw input and stores the resulting 32-hex-character digest with no salt or stretching, so -m 0 (raw MD5) is the correct mode.

    Tried: Submit the MD5 hex digest 861836f13e3d627dfa375bdb8389214e directly as the answer to the 'What is the unhashed key?' prompt.

    The prompt name is the hint: it asks for the preimage, not the hash itself. The binary hashes your input and then compares that hash to the stored digest, so submitting the digest causes a comparison of MD5(digest) vs digest, which fails. Only the original plaintext word 'goldfish', when hashed, will match 861836f13e3d627dfa375bdb8389214e.

    Learn more

    MD5 of a common word is in every public rainbow table, so this is an instant lookup. The binary stores the digest, not the word, which is why the prompt literally asks you to supply the unhashed key.

  4. Step 4
    Submit both answers
    Observation
    I noticed both values were now recovered (the XOR password 'reverseengineericanbarelyforward' and the MD5 preimage 'goldfish'), which meant the only remaining step was to supply them to the live service in the order the prompts appear.
    Connect and answer the two prompts in order: the 32-character XOR password first, then 'goldfish'. The service prints the flag.
    bash
    nc mercury.picoctf.net <PORT_FROM_INSTANCE>
    bash
    # Enter Password: reverseengineericanbarelyforward
    bash
    # What is the unhashed key? goldfish
Interactive tools
  • Hex ViewerView text or raw hex bytes as a xxd-style hex dump with byte offset, hex columns, and ASCII sidebar. Highlights printable characters and null bytes.
  • Strings ExtractorPull printable text from any binary, library, or image. ASCII and UTF-16 detection, configurable minimum length, flag-like highlight, no command line needed.
  • File Magic IdentifierIdentify file types from magic numbers. Paste hex bytes or drop a file to detect PNG, JPEG, ZIP, PDF, ELF, PCAP, SQLite, and dozens of other formats.

Flag

Reveal flag

picoCTF{p1kap1ka_p1c0...}

Two prompts, two answers. The 32-character password reverseengineericanbarelyforward is the XOR of two hardcoded arrays; the unhashed key goldfish is the MD5 preimage of the stored hash 861836f13e3d627dfa375bdb8389214e. Submit them in order to get the flag.

Key takeaway

Compiled binaries often layer multiple independent checks, each using a different primitive. XOR obfuscation hides a value by pairing it with a second array, and the original is trivially recovered by XORing those same bytes again, since XOR is its own inverse. MD5 is a fast, keyless hash and has no preimage resistance against dictionary attacks because public rainbow tables already map billions of common words to their digests. Both weaknesses appear regularly in license-key and crackme challenges across all architectures and languages.

Related reading

Want more picoCTF 2021 writeups?

Useful tools for Reverse Engineering

What to try next