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.
Setup
Disassemble the Go binary, recover both answers, then connect and submit them in order.
nc mercury.picoctf.net <PORT_FROM_INSTANCE>Solution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Locate the two checks in the Go binaryObservationI 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.bashghidra gogobash# 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:
- Password (32 chars): recovered by XORing two hardcoded arrays. This is the value typed at the
Enter Password:prompt. - Unhashed key (short word): the preimage of the stored MD5 hash. This is the value typed at the
What is the unhashed key?prompt.
- Password (32 chars): recovered by XORing two hardcoded arrays. This is the value typed at the
Step 2
Recover the 32-character password by XORObservationI 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.)pythonpython3 - <<'PY' from pwn import xor a = bytes.fromhex("3836313833366631336533643632376466613337356264623833383932313465") b = bytes.fromhex("4a53475d414503545d025a0a5357450d05005d555410010e4155574b45504601") print(xor(a, b).decode()) # -> reverseengineericanbarelyforward PYWhat 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 string861836f13e3d627dfa375bdb8389214e, which is the MD5 hash you deal with next.Step 3
Crack the MD5 hash to get the unhashed keyObservationI 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.netbash# or:bashhashcat -m 0 -a 0 861836f13e3d627dfa375bdb8389214e rockyou.txtbash# -> goldfishWhat 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.
Step 4
Submit both answersObservationI 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.bashnc mercury.picoctf.net <PORT_FROM_INSTANCE>bash# Enter Password: reverseengineericanbarelyforwardbash# 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.