Players are given a malicious Go binary for reverse engineering. Their objective is to uncover its purpose, analyze its internal logic, and determine how it communicates with its backend server. The challenge tests skills in static and dynamic analysis of Go malware.
The sample is a Windows executable written in Go, posing as a picture file. On first run it copies itself into a hidden-style folder on the system drive and registers that copy under the current user’s Run key so it survives logoff. It then enumerates drive letters to find attached storage and beacons on a timer to a hard-coded domain, sending a small POST body that identifies the infected host. Server responses are encoded and decoded before use. The whole analysis was static: Go packs its string literals together, so the work was recovering function boundaries from the runtime metadata and following each main.* function to the constants it uses.
strings, objdump and a small Python scriptTip: Go binaries carry no useful symbols in
objdump, andstringsreturns one giant blob because literals are not NUL-separated. Parse thepclntabto get function names and addresses, then resolve string pointers from each function’s disassembly.
Extract the zip with the password from the note, then hash the executable. file confirms a PE32+ before anything else is done with it.
md5sum secretPictures.exe
The function table in the binary lists main.* functions alongside the Go runtime and standard library packages (encoding/base64, time), which is the signature of a Go build. A custom parser over the pclntab pulls out the names, and the malware’s own logic sits under main..
strings -a secretPictures.exe | grep -E 'main\.|runtime\.|go1\.'
Gotcha: The format mask is six characters, so the short form
Gois rejected. The accepted answer is the full language name.
Disassemble the executable and follow the main.hide and copyExe routines. They load a global string pointer (0x90d2f0) that resolves to a path on the system drive. Reading the bytes at that address gives the destination folder name; the mask is ten characters.
objdump -d secretPictures.exe > work/dis.txt
main.lurk is the persistence routine. It references two strings: the key path at 0x6f22ef and the value name at 0x6e22be. Joined, they give the full per-user Run key path and value that launches the copied binary at logon.
python3 -I va.py secretPictures.exe 0x6f22ef 0x6e22be
The import table is the quick route: the executable imports the wide-character drive-type call from kernel32. main.outbreak is the caller, looping over drive letters from D: to Z: and checking the type of each, which skips the system drive.
strings -a secretPictures.exe | grep -i GetDrive
main.touchBase and main.heist are the network functions, and both take the same hard-coded host from the read-only data section. Searching the string dump around the networking constants for a dotted hostname turns it up.
strings -a secretPictures.exe | grep -E '[a-z0-9-]+\.[a-z]+\.com'
main.repeat calls into the time package to build a ticker and loops on its channel, which drives the recurring beacon. The symbol shows in the function table and in the call target inside main.repeat.
strings -a secretPictures.exe | grep -E 'time\.(New|After|Tick)'
The binary links encoding/base64, and the response handling in the network functions calls its decoder on the body before use. The linked package plus that call site point to Base64.
strings -a secretPictures.exe | grep -i 'encoding/'
main.formatData builds the POST body. Its map keys are the string constants it loads before assembling the request, and the format mask (****,*******) matches two fields in alphabetical order.
python3 -I va.py secretPictures.exe main.formatData
| Phase | Action |
|---|---|
| Execution | Go implant runs from its original location |
| Defense Evasion | Copies itself to a folder on the system drive under an innocuous name |
| Persistence | Writes a HealthCheck value under the per-user Run key |
| Discovery | Loops drive letters D: to Z: and checks each drive’s type |
| Command and Control | Beacons on a ticker to a hard-coded FQDN with a POST body, decodes Base64 responses |
| Type | Value |
|---|---|
| MD5 | fd46d178474f32f596641ff0f7bb337e |
| File | secretPictures.exe |
| Folder | C:\Systemlogs\ |
| Registry | HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run\HealthCheck |
| Domain | malware[.]invalid[.]com |
| Technique | ID | Description |
|---|---|---|
| Boot or Logon Autostart Execution: Registry Run Keys | T1547.001 | main.lurk writes the HealthCheck Run value |
| Masquerading | T1036 | Self-copy into a log-sounding folder, with a picture-themed filename |
| Peripheral Device Discovery | T1120 | GetDriveType loop over D: to Z: |
| Scheduled Transfer | T1029 | time.NewTicker drives the recurring beacon |
| Application Layer Protocol: Web Protocols | T1071.001 | HTTP POST to the backend |
| Data Encoding: Standard Encoding | T1132.001 | Base64 decoding of server responses |
Go implants hide behind packed strings, so triage the function table, not strings. Literals are concatenated without terminators, which makes plain string hunting noisy. Rebuilding function names from the pclntab gives named main.* entry points that make the logic readable without symbols.
Per-user Run keys are still the cheapest persistence. A new value under HKCU\...\CurrentVersion\Run pointing at a binary in a freshly created folder on the system drive is a high-signal event. Alert on Run key writes whose target path sits outside Program Files and Windows.
A self-copy into a log-sounding folder is a naming tell, not a safe name. Folders such as the one used here should not appear at the root of the system drive. Baseline root-level directories and flag new ones created by a process that then sets autostart.
Block or alert on the beacon domain at DNS. The implant resolves one fixed FQDN and repeats on a timer, so regular lookups of an unknown host from a single process are easy to spot in DNS logs.
A single Go binary gave the actor persistence, drive reconnaissance and a timed beacon with no dropped components. Its logic was fully recoverable statically once the runtime’s own function table replaced the missing symbols.