Malware Busters CTF: Go Malware Reverse Engineering - Complete Technical Writeup
Challenge Overview
This writeup documents the complete analysis of the "Malware Busters" CTF challenge. The challenge required reverse engineering a garble-obfuscated Go malware binary, decrypting its XOR-encrypted configuration file, and decoding the C2 (command and control) protocol to extract the flag.
Duration: December 10-21, 2025 (~2 weeks, 15+ analysis sessions)
Challenge Description
You are presented with an unknown and odd binary in a compromised environment. Your job is to analyze the binary as best you can. Your analysis should include:
- Describe the actions performed by the malware
- Find the C2 server the malware communicates with
- Decrypt the malware's C2 protocol
By following these steps you will find the hidden flag to complete the challenge.
Hints Provided
- "Looking up UPX might nudge you in the right direction" - Pointed to corrupted UPX packing
- "Malware can be a curious beast. Sometimes, it refuses to function outside its native habitat" - Environment validation requirement
Phase 1: Initial Analysis & UPX Unpacking
Binary Overview
| Property | Value |
|---|---|
| Filename | buu |
| Size (packed) | ~2 MB |
| Size (unpacked) | ~4.7 MB |
| Format | ELF 64-bit executable |
| Language | Go (garble-obfuscated) |
| Packing | UPX with corrupted headers |
The UPX Anti-Unpacking Trick
Initial attempt to unpack with upx -d buu failed with NotPackedException: not packed by UPX. Investigation revealed the UPX magic bytes had been deliberately corrupted.
Using grep -oba "WTRT" buu revealed four locations where UPX! had been replaced with WTRT:
| Offset (decimal) | Offset (hex) | Original | Corrupted |
|---|---|---|---|
| 236 | 0xEC | UPX! | WTRT |
| 2014771 | 0x1EB633 | UPX! | WTRT |
| 2016022 | 0x1EBB16 | UPX! | WTRT |
| 2016032 | 0x1EBB20 | UPX! | WTRT |
Solution: Patching the Magic Bytes
cp buu buu_fix
printf '\x55\x50\x58\x21' | dd of=buu_fix bs=1 seek=236 conv=notrunc
printf '\x55\x50\x58\x21' | dd of=buu_fix bs=1 seek=2014771 conv=notrunc
printf '\x55\x50\x58\x21' | dd of=buu_fix bs=1 seek=2016022 conv=notrunc
printf '\x55\x50\x58\x21' | dd of=buu_fix bs=1 seek=2016032 conv=notrunc
upx -d buu_fix -o buu_unpacked
Result: Successfully unpacked to buu_unpacked (4.7 MB)
Phase 2: Static Analysis
Tools Used
- Ghidra - Primary disassembler for initial analysis
- Binary Ninja - Secondary analysis, better Go support
- strings/grep - Quick string extraction
- Python - Scripting for decryption attempts
Key Findings from String Analysis
The unpacked binary revealed several important strings:
Obfuscated Function Names (garble)
main.qvHPoPlV- Main config parsing/C2 communicationmain.ZZzgf2nH,main.yprG5We4,main.WG2BdUVbmain.dT6K8IGR,main.LVEnGayC
Crypto Libraries
crypto/aes- AES encryptioncrypto/cipher- Cipher modesvendor/golang.org/x/crypto/chacha20poly1305- Initially suspected but not used for C2encoding/hex- Hex encoding/decoding
Configuration Path
/tmp/.X11/cnf- Hidden configuration file location
Red Herring: The 96-Byte Hex String
Found a 96-byte hex string in the binary that appeared to be a cryptographic key.
Initial assumption: This was the XOR key for config decryption.
Reality: This is the NIST P-256 elliptic curve base point - a standard cryptographic constant, NOT a custom key. This consumed significant analysis time before being identified as a red herring.
Another Red Herring: ChaCha20-Poly1305
The binary imports chacha20poly1305, leading to extensive attempts to decrypt C2 responses using ChaCha20. This was a fruitless path - the actual encryption used was AES-128-CBC.
Phase 3: Dynamic Analysis & Environment Discovery
The "Native Habitat" Requirement
Running the binary outside the CTF environment produced:
I don't belong here...
The malware performs environment validation:
- Checks hostname equals
monthly-challenge - Reads config from
/tmp/.X11/cnf - Only proceeds if all checks pass
Setting Up the Environment
hostname # Returns: monthly-challenge
cat /tmp/.X11/cnf | xxd # 144 bytes of encrypted config
Using strace for Behavior Analysis
strace -f -s 2000 -e trace=read,write,connect ./buu 2>&1
Observations:
- Reads 144 bytes from
/tmp/.X11/cnf - Performs DNS lookup for AWS Lambda endpoint
- Establishes TLS connection on port 443
- Sends/receives encrypted data
Phase 4: Configuration Decryption
The Encrypted Config
The 144-byte encrypted configuration was extracted from /tmp/.X11/cnf. The data was XOR-encrypted with a 4-byte repeating key.
Finding the XOR Key
Through Ghidra analysis of main.kYgXL_QA, found a hardcoded value:
v18 = 0x[REDACTED]
The Byte-Swap Transformation
The XOR decryption required a specific byte transformation within 4-byte blocks:
config = bytes.fromhex("9cce2f39d99c7640...")
key = bytes.fromhex("[REDACTED]") # Little-endian representation
decrypted = bytes([config[i] ^ key[i % 4] for i in range(len(config))])
Decrypted Configuration
{
"myhostname": "monthly-challenge",
"url": "https://[REDACTED].lambda-url.us-east-2.on.aws",
"enc_key": "[REDACTED - 32 hex chars]"
}
Key Discovery: The enc_key field contains the AES key for C2 decryption!
Phase 5: C2 Protocol Analysis
Traffic Interception
Used a combination of:
stracefor syscall tracing- Custom Python MITM proxy
- Direct endpoint probing with
curl
C2 Protocol Structure
| Component | Value |
|---|---|
| Endpoint | https://[REDACTED].lambda-url.us-east-2.on.aws |
| Path | /command |
| Parameters | n=monthly-challenge&s=<sequence> |
| Method | GET (receive commands), POST (send results) |
Sequence Parameter
The s parameter increments with each request:
s=1,s=2,s=3, ...s=10
Each GET request returns an encrypted command.
Captured Encrypted Responses
| Sequence | Response Size |
|---|---|
| 1 | 32 bytes |
| 2 | 32 bytes |
| 3 | 64 bytes (anomaly!) |
| 4-10 | 32 bytes each |
Note: Sequence 3 returned 64 bytes instead of 32 - a hint that something important was there!
Phase 6: C2 Decryption Breakthrough
Failed Attempts
- AES-GCM with various nonce formats - Tag verification failed
- ChaCha20-Poly1305 - No valid output
- XOR with 96-byte key - Garbage output
- Various HKDF derivations - No success
The Solution: AES-128-CBC
After many failed attempts with AES-GCM, tried AES-CBC:
from Crypto.Cipher import AES
enc_key = bytes.fromhex("[REDACTED]")
def decrypt_command(hex_data):
data = bytes.fromhex(hex_data)
iv = data[:16]
ciphertext = data[16:]
cipher = AES.new(enc_key, AES.MODE_CBC, iv=iv)
plaintext = cipher.decrypt(ciphertext)
# Remove PKCS7 padding
pad = plaintext[-1]
if 1 <= pad <= 16:
plaintext = plaintext[:-pad]
return plaintext.decode('utf-8', errors='replace')
Decrypted C2 Commands
| Sequence | Decrypted Command |
|---|---|
| 1 | id |
| 2 | cat /etc/passwd |
| 3 | DONE WIZ_CTF{[REDACTED]} |
| 4 | whoami |
| 5 | hostname |
| 6 | uname -a |
| 7 | pwd |
| 8 | ls -la |
| 9 | env |
| 10 | exit |
FLAG FOUND in sequence 3!
Complete Attack Chain Summary
+------------------------------------------------------------------+
| ATTACK FLOW |
+------------------------------------------------------------------+
| |
| 1. UPX Magic Byte Corruption Detection |
| | |
| v |
| 2. Patch WTRT -> UPX! at four offsets |
| | |
| v |
| 3. Unpack binary with upx -d |
| | |
| v |
| 4. Static analysis reveals config path and crypto imports |
| | |
| v |
| 5. Run in CTF environment (hostname=monthly-challenge) |
| | |
| v |
| 6. Extract encrypted config from /tmp/.X11/cnf |
| | |
| v |
| 7. Find XOR key (0x3354EEBC) in Ghidra |
| | |
| v |
| 8. Decrypt config -> get C2 URL and AES key |
| | |
| v |
| 9. Probe C2 endpoint with sequence parameter |
| | |
| v |
| 10. Decrypt responses with AES-128-CBC |
| | |
| v |
| 11. Flag found in sequence 3 response |
| |
+------------------------------------------------------------------+
Red Herrings & Fruitless Paths
1. The 96-Byte NIST P-256 Constant
Time wasted: ~4 hours
Assumed this was the XOR key. Actually a standard elliptic curve parameter.
2. ChaCha20-Poly1305 Assumption
Time wasted: ~6 hours
Binary imports this library, but C2 uses AES-CBC instead.
3. AES-GCM Format Attempts
Time wasted: ~3 hours
Tried numerous nonce/tag arrangements before discovering CBC mode.
4. HKDF Key Derivation
Time wasted: ~2 hours
Saw HKDF references in binary, attempted various derivations.
5. The /tmp/b64.txt File
Time wasted: ~1 hour
Found a 2.7MB base64 file on the system - turned out to be unrelated.
6. Second 84-Byte Hex String
Time wasted: ~2 hours
Another hex blob in the binary that proved irrelevant to decryption.
Tools Summary
| Tool | Purpose |
|---|---|
| Ghidra | Static analysis, function identification |
| Binary Ninja | Go binary analysis, decompilation |
| UPX | Binary unpacking (after patching) |
| Python + pycryptodome | Cryptographic operations |
| strace | System call tracing |
| curl | C2 endpoint probing |
| xxd/od | Hex dumps and binary inspection |
| grep | Pattern searching |
Key Insights & Lessons Learned
- Anti-analysis techniques are layered - UPX corruption was just the first obstacle.
- Red herrings are intentional - The NIST P-256 constant was a deliberate distraction.
- Try simpler crypto first - AES-CBC worked when GCM kept failing.
- The "native habitat" hint was crucial - Running in the correct environment was necessary.
- Sequence anomalies matter - The 64-byte response at sequence 3 indicated something special.
- Known plaintext attacks work - Knowing the JSON structure helped derive the XOR key.
Timeline
| Date | Milestone |
|---|---|
| Dec 10-11 | UPX unpacking, initial static analysis |
| Dec 11-12 | Config file discovery, XOR key hunting |
| Dec 12-13 | C2 endpoint discovery, traffic capture |
| Dec 13-14 | Ghidra deep-dive, crypto function analysis |
| Dec 14-16 | Failed decryption attempts (GCM, ChaCha20) |
| Dec 16-17 | Binary Ninja analysis, HKDF investigation |
| Dec 17-21 | Config decryption breakthrough |
| Dec 21 | AES-CBC discovery, FLAG CAPTURED |
Security Implications
1. UPX Header Corruption
A simple but effective anti-analysis technique. Organizations should:
- Train analysts to recognize corrupted packer signatures
- Use tools that can identify packing regardless of magic bytes
- Maintain databases of known corruption patterns
2. Environment Validation
The hostname check prevents sandbox analysis:
- Configure analysis environments to match expected conditions
- Use dynamic instrumentation to bypass environment checks
- Patch binary to remove validation logic
3. Encrypted Configuration Files
Hiding C2 details in encrypted config files is common:
- Look for hardcoded keys in binary strings and constants
- Monitor file system access during dynamic analysis
- Use memory dumping to capture decrypted configs at runtime
Conclusion
The "Malware Busters" challenge was an excellent test of reverse engineering skills, combining:
- Binary unpacking with anti-analysis bypass
- Go binary analysis with obfuscation
- Cryptanalysis with multiple encryption layers
- Protocol reverse engineering
The flag was hiding in plain sight within the C2 command stream, waiting to be decrypted with the key extracted from the configuration file. The deliberate red herrings (NIST P-256 constants, ChaCha20 imports) added significant complexity and required methodical elimination of false paths.
Challenge created by Gili Tikochinski as part of The Ultimate Cloud Security Championship by Wiz. Writeup completed: December 21, 2025