The challenge
The prompt is short. The author says they made the challenge because they were upset about how a threat group named a payload, and introduces it as "Wonder Wall". The archive password is the usual infected.
| answer_id | Question | Format |
|---|---|---|
flag |
(no question text in the start reply) | No answer_ given; the event's flag format is 32 lowercase hex characters (MD5-style) |
One submission only. Standard challenges keep the single-answer rule (event announcement of 2026-10-29), so a wrong guess would have cost the 5 points for good.
Reading the prompt: what it said we'd need
| Clue | What it pointed to | What I did about it |
|---|---|---|
| "SHA1-Hulud" (title) | A pun on the "Shai-Hulud" name used for a real supply-chain worm, with SHA-1 in it | SHA-1 is central: the binary hashes its own code with SHA-1 and three modified SHA-1 compressions, and those digests key the rest |
| The payload-name complaint | A nod to real-world malware naming | Not followed up; no part of the solve depended on it. What it refers to wasn't recorded |
| "Wonder Wall" | Unclear. Possibly the wall of anti-analysis checks | Not resolved; recorded here only as a guess |
Password infected |
Treat the contents as live malware | Static only. The sample was never executed |
| No question text, flag format only | Find the input the program accepts | The program prints a success line that echoes the input, so the flag is whatever input passes its check |
Safety
Static analysis only. The sample was never executed, loaded, or debugged. The archive was unpacked on our Kali VM and read as bytes, with disassemblers and a decompiler. The self-decrypting code was recovered by re-implementing its decryption in Python on the file's bytes, not by letting it run. Behaviour below is described at a high level only. The sample and the analysis files stayed on the VM.
What was in the files
sha1_hulud.zip(passwordinfected) holdingsha1-hulud.exe: a 64-bit Windows console program written in Rust, 632,320 bytes, with an extra section of encrypted strings.- What it does, at a high level: it asks for the flag, reads one line, trims and lowercases it, runs a long key-derivation and comparison, and on success prints a "correct, the flag is …" line followed by the input. So the flag is the input that passes the check, not a value stored in the file.
- Encrypted strings. Two small decoders were re-implemented in Python. The strings are the prompt and success messages, a few DLL names, and anti-analysis material: registry paths and a blacklist of debugger and analysis-tool process names.
- Heavy anti-analysis: a vectored exception handler, breakpoint traps, several timing checks, debugger queries, debug-register checks, a hypervisor and firmware-table VM check, and screen, memory, CPU-count, and install-path checks. Every check feeds a value into the key chain instead of simply exiting, so a debugger or VM changes the keys rather than stopping the program.
- Self-decrypting code. About 230 KB of the code section is encrypted at rest and decrypts itself block by block just ahead of execution, then goes back to its file bytes. It holds four hash compressions: standard SHA-1 and three deliberately altered variants.
- The check, in outline: hash a normalised copy of the program's own code; mix in an environment fingerprint built from the anti-analysis results; run a long anti-emulation loop; decode two embedded data blobs with keystreams from those digests into lookup tables; push the input through a block mixer and compare the result with what the tables expect.
Single Solve (not solved)
Why the automated solver left it
Our automated morning solver (a scheduled script on our Kali VM) has a standard-challenge guard (live since 2026-10-28): it saw "no Time Trial or Mastery listed; one submission" and started nothing and sent nothing, leaving it for Claude. That guard worked as intended. The untimed start at 09:02:31 ET marks the start of the helper's work.
What the helper got done (static only)
- Strings decrypted (by about 09:04 ET).
- The protected code unpacked by re-implementing the per-block decryption. All four protected functions came out clean. The four compressions were identified: one checked against a reference SHA-1 on random inputs, three found to be altered variants. Each was lifted into a Python function.
- First report, about 09:25 ET (about 22 minutes in): not solved, no candidate, submission unused. We decided to keep working, and the helper carried on.
- Phase 1 rebuilt: the self-hash over the normalised code section and the derived parameters.
- The environment fingerprint modelled for a clean run. The helper found that each anti-analysis check enters through a term that cancels when the check sees a clean machine, and computed the fingerprint on that basis. This is a candidate, not a confirmed value: it assumes every such term cancels. One good sign: an embedded compressed blob decompressed to exactly its expected length.
- Phase 2 rebuilt, with the long loop in a small C helper.
- The blob stream decoder rebuilt and checked against the first blob's header: the decoded header gave a sensible length with zero high bytes, which suggests that phases 1–2, the fingerprint, and the lifted compressions are right so far.
Where it stopped
At about 09:38 ET, Claude's safety filter stopped one step of the analysis, and we didn't work around it. The challenge stayed parked, and nothing was sent before the event closed.
What was left: the later stages of the key chain, the final comparison, and confirming the fingerprint candidate.
Even without the stop, this wasn't close. Our estimate was several more hours of static work, with real risk of a wrong key somewhere in the chain. The one built-in sanity check was a good one: a wrong recovery would most likely produce non-hex bytes, so a clean 32-character hex plaintext would itself have been strong evidence. No candidate was ever produced, so the single submission was never at risk.
Time to solve
Not solved. Static work ran for about 36 minutes on 2026-10-30 (from the 09:02:31 ET start to about 09:38 ET), and the Game Master log holds only the start: no submission was made, and the attempt was still open when the event closed on 2026-11-01.
Takeaways
- The guard that protects a single submission worked. The automated solver didn't spend it, and the helper never had a candidate it could verify, so nothing was guessed. Skipping 5 points beat losing them to a wrong answer.
- Environment-keyed checks are expensive to solve statically. When every anti-debug and VM check feeds the key instead of exiting, each one has to be modelled for a clean run, and one wrong assumption breaks every later phase.
- Validate as you go. Checking the stream decoder against the first blob's header gave real evidence that the first two phases were right, long before any flag candidate.
- Re-implement, don't run. The self-decrypting code was recovered by re-implementing its decryption on the file's bytes; nothing in the sample ever ran.