This Sherlock challenges you to analyze a fake WiFi driver update delivered via a social engineering FixIt site. Through static analysis of a packed .NET binary, you’ll decrypt RC4-encrypted shellcode, trace process hollowing into MSBuild.exe, and reverse engineer a second-stage payload performing sandbox evasion, credential theft from browsers and FTP clients, Steam JWT scraping, Discord token extraction, and Telegram-based exfiltration.
A fake WiFi driver update delivered through a social-engineering site shipped a packed .NET loader. The loader decrypts an embedded shellcode blob with RC4, and the shellcode hollows a legitimate .NET build tool to host the real payload. A second RC4 pass over an oversized high-entropy section yields a native second stage, a commodity-style information stealer. It refuses to run on a known analysis machine name, guards against double execution with a named event, and then harvests browser and FTP client credentials, Steam session tokens read from process memory, and Discord tokens. The loot is tagged with a hardcoded build identifier and exfiltrated through a messaging platform.
evidence/malware.zip → password-protected (AES) archive, extract with 7z. Holds the packed loader PEpefile, metadata with dnfilecapstone and read with stringsAddress note: Both address questions only accept the
0xprefix with uppercase hex (0x40F3B0style), even though the answer mask is all stars. Bare0040F3B0, lowercase and RVA forms (0000F3B0) are all rejected.
Extract the archive and read FILE_HEADER.TimeDateStamp with pefile. The value decodes to a date far in the future, so the compiler timestamp is forged rather than a real build time. The date is still what the question wants.
7z x -p<password> evidence/malware.zip -o work/s1
python3 -I -c "import pefile,sys,datetime;pe=pefile.PE(sys.argv[1]);print(datetime.datetime.fromtimestamp(pe.FILE_HEADER.TimeDateStamp,datetime.UTC))" work/s1/<sample>.exe
Tip: A 2089 timestamp is a deliberate fake. Don’t treat it as an error in your tooling.
Walk the section table in pefile and compare raw size and entropy. One non-standard section is about 140 KB at entropy 8.0, which is encrypted or compressed data and not code or resources.
python3 -I -c "import pefile,sys;pe=pefile.PE(sys.argv[1]);[print(s.Name,s.SizeOfRawData,s.get_entropy()) for s in pe.sections]" work/s1/<sample>.exe
The sample is .NET, so dump the metadata with dnfile and read the loader’s type and method names. A method named DeleteSentence runs a key-scheduling loop followed by a keystream loop, the shape of RC4’s KSA and PRGA. Proof came from decrypting: RC4 with a 36-byte key at RVA 14608 over the 1355-byte blob at RVA 14648 produces readable shellcode.
python3 -I -c "import dnfile,sys;pe=dnfile.dnPE(sys.argv[1]);[print(m.Name) for m in pe.net.mdtables.MethodDef]" work/s1/<sample>.exe
python3 -I work/rc4_dec.py work/s1/<sample>.exe > work/sc.bin
Gotcha: The shellcode’s own strings are not the key. The 36-byte key sits in the loader’s data, next to the blob.
No decompiler was available, so read the ImplMap table through dnfile to list the loader’s P/Invoke imports. Two stand out: VirtualProtect to make the buffer executable, and CallWindowProcA. Passing the shellcode address as the window procedure runs it as a callback, a known way to avoid CreateThread.
python3 -I -c "import dnfile,sys;pe=dnfile.dnPE(sys.argv[1]);[print(r.ImportName) for r in pe.net.mdtables.ImplMap]" work/s1/<sample>.exe
Run strings on the decrypted shellcode. It carries a full path to the .NET Framework v4.0.30319 directory ending in the host binary, which is the process it spawns and hollows.
strings -a work/sc.bin
Disassemble the shellcode with capstone and list its API calls: CreateProcessW (suspended host), GetThreadContext, WriteProcessMemory, SetThreadContext, ResumeThread. That sequence replaces the host’s image and redirects its entry point, which is process hollowing.
python3 -I work/dis_sc.py work/sc.bin
Second stage extraction: A second RC4 pass, with an 11-byte key at RVA 14592, over the
.CSSsection from Q2 produces a PE starting withMZ. Every remaining question is answered from this file.
python3 -I work/rc4_stage2.py work/s1/<sample>.exe > work/stage2.bin
Disassemble stage2.bin and find the GetComputerNameW call. At 0x4173dc through 0x417606 it compares the returned wide string one character at a time, a chain that spells out H, A, L, 9, T, H. A match exits the malware, and HAL9TH is a known sandbox machine name. This one was reasoned from the compare chain, not read as a single string, because the characters are split across instructions.
python3 -I work/dis.py work/stage2.bin | grep -n -E '4173d|4174|4175|4176'
Near 0x416080 the code builds a string on the stack: a 16-byte movaps from 0x41e3b0 followed by a single byte l. Combined, that gives the name passed to OpenEventA/CreateEventA, the run-once marker.
python3 -I work/dis.py work/stage2.bin | grep -n -E '416080|41e3b0'
Tip: The string is never contiguous in
.rdata. Rebuild it from the stack stores instead of searching for it withstrings.
While tracing the event-name code from Q8, the string object is first passed to a tiny function at 0x40f3b0 that zeroes [ecx], [ecx+4] and [ecx+8], the three fields of the stealer’s custom string type.
python3 -I work/dis.py work/stage2.bin | grep -n -A6 '40f3b0:'
Gotcha: The answer needs the
0xprefix and uppercase hex.0040F3B0,0040f3b0and0000F3B0(RVA) were all rejected.
strings over the second stage shows the registry path Software\Martin Prikryl\WinSCP 2, WinSCP’s stored-session key.
strings -a work/stage2.bin | grep -i -E 'winscp|filezilla|prikryl'
Find the steam.exe string at 0x41f453. It is referenced from a wrapper at 0x40f170, which calls 0x40ee70. That function holds OpenProcess (0x40eeb7) and ReadProcessMemory (0x40ef95), so it is the one that acquires the handle and starts reading. The wrapper and a later helper at 0x415DA0 are neighbours, not the answer.
python3 -I work/dis.py work/stage2.bin | grep -n -E '41f453|40f170|40ee70|40eeb7|40ef95'
Gotcha: The wrapper that references
steam.exeis not the handle-acquiring function. Follow its call to the one that containsOpenProcess.
The Steam routine scans the process memory it read in Q11 for a token prefix beginning eyJ, the base64 start of a JSON header. That prefix is how JWTs begin. This was concluded from the eyJ pattern and the Steam context, not from a string that names the artifact.
strings -a work/stage2.bin | grep -n 'eyJ'
In .rdata at 0x41f78f sits an 11-character string, dQw4w9WgXcQ, among the Discord strings. It looks like a YouTube video ID, an odd value to find in a token scraper, which marks it as a planted marker. This is inferred from its position beside the Discord strings rather than from a traced code path.
strings -a -t x work/stage2.bin | grep dQw4w9WgXcQ
A 32-character hex string at 0x41e020 in .rdata is the build identifier sent with the stolen data.
strings -a -t x work/stage2.bin | grep -E '[0-9a-f]{32}'
The .data section holds a t.me/l793oy link, a Telegram channel that serves as the dead drop and C2 pointer.
strings -a work/stage2.bin | grep -E 't\.me/'
| Phase | Action |
|---|---|
| Initial Access | Victim lured by a social-engineering site into running a fake WiFi driver update |
| Defense Evasion | Packed .NET loader with a forged 2089 compile timestamp and a high-entropy .CSS section |
| Defense Evasion | Loader RC4-decrypts shellcode, then RC4-decrypts .CSS into the second-stage PE |
| Execution | Shellcode run through CallWindowProcA after VirtualProtect |
| Execution | Process hollowing of MSBuild.exe |
| Defense Evasion | Second stage exits on a known sandbox computer name and sets a named event to block double runs |
| Credential Access | Browser and FTP client (FileZilla, WinSCP) credentials harvested |
| Credential Access | Steam process memory read for JWTs; Discord tokens crawled |
| Exfiltration | Data sent with a hardcoded build ID to a Telegram channel |
| Type | Value |
|---|---|
| Compile timestamp (forged) | 2089-10-01 03:41:58 |
| PE section | .CSS (entropy 8.0, about 140 KB) |
| Injection target | C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe |
| Sandbox computer name | hal9th |
| Run-once event name | approve_april |
| Build ID | ee4724586a69cda3db87344f07509f40 |
| Discord marker | dQw4w9WgXcQ |
| Exfil channel | t[.]me/l793oy |
| Registry key read | Software\Martin Prikryl\WinSCP 2 |
| Technique | ID | Description |
|---|---|---|
| User Execution: Malicious File | T1204.002 | Fake WiFi driver update run by the victim |
| Obfuscated Files or Information: Software Packing | T1027.002 | Encrypted .CSS section holding the second stage |
| Deobfuscate/Decode Files or Information | T1140 | Two RC4 passes in the loader |
| Process Injection: Process Hollowing | T1055.012 | Shellcode hollows MSBuild.exe |
| Virtualization/Sandbox Evasion: System Checks | T1497.001 | Computer name compared against a sandbox name |
| Execution Guardrails | T1480 | Named event prevents repeat execution |
| Credentials from Password Stores: Credentials from Web Browsers | T1555.003 | Browser credential theft |
| Unsecured Credentials: Credentials In Files | T1552.001 | FileZilla and WinSCP stored sessions |
| Steal Application Access Token | T1528 | Steam JWTs and Discord tokens |
| Exfiltration Over Web Service | T1567 | Stolen data posted to Telegram |
A future compile timestamp and a lone 8.0-entropy section are enough to triage a sample as packed. Both are visible in the PE header without running anything. A static rule on an oversized high-entropy non-standard section name catches the loader before detonation.
MSBuild.exe spawned by an unexpected parent, then making network connections, is a hollowing signal. MSBuild has no reason to talk to the internet outside a build server. Alert on it running from a user context and on suspended-process creation followed by WriteProcessMemory into it.
Callback-based shellcode execution skips the usual thread-creation telemetry. CallWindowProcA with a freshly VirtualProtected buffer produces no CreateThread event. Hunt for executable memory changes in .NET processes instead of relying on thread-start detections.
Stored FTP sessions, Steam and Discord tokens are all readable by the user’s own processes. Stop saving WinSCP and FileZilla passwords on workstations, and rotate Steam and Discord sessions after any stealer detection, since JWTs remain valid after a password change until they expire.
Traffic to Telegram from a workstation that never used it is a cheap egress detection. The exfil path rides a mainstream platform over HTTPS, so blocking or alerting on the domain for non-approved hosts removes the channel.
The attacker needed no exploit: a believable driver-update lure, two RC4 layers and a hollowed MSBuild.exe were enough to run a credential stealer that harvested FTP, Steam and Discord secrets and sent them out through Telegram. Static analysis alone, headers, metadata and a disassembler, unwound the full chain without detonating anything.