The summary and the C2
The sample is a PE32 .NET binary, not native code: everything that follows happens in IL and CLR metadata, not in x86 instructions. It hides an AgentTesla across five stages, and each hop uses a different technique: ConfuserEx LZMA constants, a hand-rolled reversed hexadecimal encoding, steganography in a PNG’s pixels, .NET Reactor with control-flow flattening, QuickLZ and a positional XOR.
The final payload exfiltrates over SMTP, with the credentials sitting inside the binary. It harvests passwords from 26 Chromium browsers and 39 further applications, logs keystrokes, watches the clipboard and captures the screen.
Command and control configuration
- SMTP server
- mail.scottbyscott.com
- Port
- 587 EnableSsl = false
- Username
- ideshow@eflownutrition.com
- Password
- ngo•••••• masked
- Recipient
- harrisonjeff401@gmail.com
The password is masked deliberately. eflownutrition.com appears to be a legitimate
company whose mailbox is being used as a relay, so publishing the full credential would expose that
account to anyone reading this. The rest are ordinary indicators.
That choice of channel is not incidental: SMTP dominates in this family precisely because the operator only needs one compromised mailbox. No infrastructure of their own, no domain, no server to maintain.
The five-stage chain
Dropper — CorrectionAlgorit.exe
VB.NET protected with ConfuserEx. It poses as a school project called “SpellingBee”, with 13 working WinForms forms. Ten classes whose names are made of invisible Unicode characters implement the LZMA range decoder.
Internal paths give away the origin of the reused project:
\\file-home1\TomOye161$\Computing\Year 2\Spelling Bee\. Someone stole a
student’s coursework to use as a wrapper.
It fires from Admin_page::InitializeComponent, when the form is constructed.
Loader — TimeSpan.dll
Buried among classes from the open-source WinForms theme “Pico2012”. It sleeps for 39 to 61 seconds before acting, to outlast sandbox analysis windows.
Signature evasion by string splitting:
"Pico2012".Replace("Pico","Inv").Replace("2012","oke") yields
"Invoke" at runtime, so the name never appears in the binary.
Stub — SinkProvider.dll
Protected with .NET Reactor. It carries anti-VM, anti-Wine, Defender evasion, persistence and the injector. It is not the stealer: it is the crypter’s wrapper.
Camouflaged with the FastColoredTextBox library, whose T-SQL and HTML regexes
make up most of its 237 encrypted strings.
Resource container
A .NET assembly with 524 bytes of metadata and not a single method: it exists purely to hold resources. The assembly name is a GUID.
The trick: the previous stage’s resources are moved into a separate assembly loaded in
memory with Assembly.Load, so a static look at the stub never finds them. One of
them is called SinkProvider.g.resources, which gives the relationship away.
AgentTesla payload
An executable with an entry point, 84 types and 1,347 methods. It references
System.Management, System.Security and System.Xml.
Everything lives here: credentials, keylogger, clipboard, screenshots and exfiltration.
The obfuscation layers
ConfuserEx constants
The dropper keeps its strings in an embedded array of 11,040 32-bit integers. An xorshift PRNG
seeded with -794857484 produces 16 keys applied in blocks, each evolving after use. The
result is LZMA-decompressed.
x ^= x >>> 12; x ^= x << 25; x ^= x >>> 27 // 16 keys
for (i = 0; i < 11040; i += 16) {
for (j = 0; j < 16; j++) {
v = data[i+j] ^ key[j];
write_le32(out, v);
key[j] ^= v; // the key evolves
}
}
out = LZMA_decompress(out) // 215,920 B of strings
An invented hexadecimal encoding
Inside those constants sits a 212,461-character string drawn from an alphabet of just 19 symbols:
the 16 hexadecimal digits plus space, hyphen and <. It is hexadecimal read backwards,
space-separated, where the two extra symbols compress runs of null bytes.
| Element | Meaning | Example |
|---|---|---|
| The whole string | Reversed, with StrReverse | …15 17 65 45 → 54 56 71 51 |
| Space | Separator between hex pairs | 54 56 71 51 = "TVqQ" |
- | One base64 A, i.e. a null byte | --4D → AAM |
< | Three base64 As | <<<51 → AAAAAAAAAQ |
The mapping checks out against the canonical DOS header of a .NET binary, which is fixed and known: the resulting 92,844 base64 characters decode to stage 2.
Steganography in the pixels
Stage 3 travels inside a 354×354 PNG stored as a Bitmap resource under the name
TaskSchedulerAwaitTaskContinuati.
"pHw" in UTF-16, and the seed comes from the last byte of the block itself: without that byte nothing decrypts.The keystream has a period of 6: 9a ea 9a d2 9a ed. Against the known DOS header the
key falls out immediately.
.NET Reactor
Stage 3 encrypts its 237 strings with AES, indexed by offset, through a decryptor of
3,517 IL instructions flattened into a state machine with some 370 targets and
opaque predicates. Two helper methods turn out to be constants: one always returns 1 and
the other always 0, which accounts for 61 of the branches.
The native NecroBit stubs come to about 70 bytes in total: two x86 and x64 trampolines of the
mov rax, imm64; jmp rax kind. They do not protect the string decryptor.
QuickLZ, and two hand-rolled XORs
The hop to stage 4 uses a bespoke decompressor with the magic QCLZ, written with
pointer arithmetic. The last hop compresses nothing: it is two chained XOR layers, verified against
the real binary.
// iNi: XOR with a seed derived from the last byte + a repeating key
key = Encoding.BigEndianUnicode("OpWegUbUUQ") // cycles over 10 characters
k1 = d[last] ^ 0x70
out[i] = d[i] ^ k1 ^ key[i % 10] // the last byte is dropped
// ONb: self-keyed XOR -- the header is its own key
k = d[0:16]
out[i] = d[16+i] ^ k[i % 16]
// 221,713 B -> iNi -> 221,712 B -> ONb -> 221,696 B = AgentTesla
And here is the detail that says most about the author: iNi is the same
primitive that decrypts the PNG steganography two stages earlier. Identical structure — a
^ 0x70 seed over the last byte, a key repeating over the string length — differing only
in the encoding used to turn the key into bytes. It is one routine reused, and it explains why the
constant 0x70 reappears across unrelated stages. As a family signature it is excellent.
What the stub does
Before dropping the payload, stage 3 checks where it is.
| Category | What it looks for |
|---|---|
| Hypervisors | Registry keys for VirtualBox Guest Additions and VMware Tools, DEVICEMAP\Scsi, QEMU, and via WMI SELECT * FROM Win32_VideoController against VirtualBox Graphics Adapter, VMware SVGA II and S3 Trio32/64 |
| Sandboxes | SbieDll.dll from Sandboxie, plus decoy usernames: SANDBOX, VIRUS, MALWARE, SCHMIDTI, SAMPLE, C:\file.exe |
| Wine | GetProcAddress(GetModuleHandle("kernel32.dll"), "wine_get_unix_file_name") — if that export exists, you are not on Windows |
| Defender | powershell Add-MpPreference -ExclusionPath on its own install path |
| Injection | ZwUnmapViewOfSection, VirtualAllocEx, WriteProcessMemory, CreateProcessA, Get/SetThreadContext and their Wow64 variants, into MSBuild.exe, vbc.exe or RegSvcs.exe — signed .NET Framework binaries |
Persistence
With the real configuration values in place, the sequence it executes is:
GetFolderPath(ApplicationData) -> %APPDATA%
File.Exists('%APPDATA%\GIxVTvvWqR.exe') -> if absent:
DirectorySecurity.SetAccessRuleProtection(...) breaks ACL inheritance
FileSystemSecurity.AddAccessRule(...) x9 hardened ACL
FileSystemInfo.set_Attributes(...) x2 hidden / system
File.Copy(sample.exe -> %APPDATA%\GIxVTvvWqR.exe) self-copy
WindowsIdentity.GetCurrent().Name user for the XML
Path.GetTempFileName() + String.Replace x2 substitutes [LOCATION] and [USERID]
File.WriteAllText(temp) writes the task XML
Process.Start('schtasks.exe', '/Create /TN "Updates\..." /XML "temp"')
File.Delete(temp) deletes the XML
The XML template is not among the strings: it lives in a resource inside the stage 4 container. Its parameters say a fair amount:
| Element | Value | Implication |
|---|---|---|
| LogonTrigger | [USERID] | Starts at every logon |
| RunLevel | LeastPrivilege | No UAC prompt; works as a standard user |
| ExecutionTimeLimit | PT0S | No execution limit |
| AllowHardTerminate | false | Resists termination by the scheduler |
| Hidden | false | Visible in Task Scheduler — detection for free |
| Date | 2014-10-25 | Fake date, a decoy |
What it steals, and how
The entry point pins TLS 1.0–1.3, builds a victim identifier from the username and machine name, and arms a 30-second timer. The static constructor installs the keyboard hook before anything else.
HttpWebRequest does not speak SOCKS — but its destination URL was left as an unsubstituted placeholder. That the live channel is SMTP matches what is documented for the family: the operator only needs one compromised mailbox.The machinery it carries
What stands out about the payload is not what it steals, but that it depends on nothing from the system to do it.
| Component | What it is |
|---|---|
| SQLite reader | Walks B-tree pages by hand, 1,301 instructions. No System.Data.SQLite: it needs no native DLL and sidesteps the most obvious signature of a browser credential stealer |
| DER parser | 895 instructions of recursive ASN.1 TLV decoding, without a single external API |
| Berkeley DB | For key3.db, the format Firefox used before key4.db |
| AES-GCM | Via BCrypt, for modern Chromium’s v10/v11 values |
| 3DES via reflection | Sets Key, Mode, IV and Padding through LateSet, by name. The IL never statically references the cryptography APIs: they do not appear in the binary’s reference table |
| keylogger | WH_KEYBOARD_LL: captures Alt and system keys too, along with the foreground window title |
| clipboard | The classic viewer chain, with SetClipboardViewer |
| HTTP→SOCKS proxy | Because HttpWebRequest does not speak SOCKS and the Tor channel needs it |
That reflection-based 3DES explains some otherwise stray strings — Key,
Mode, IV, Padding — sitting alone in the string table. They are
not leftovers: they are the names through which it calls cryptography without declaring it.
The two decryption paths
nss3.dll or System.Data.SQLite appears — the two most obvious signatures of a browser credential stealer.The targets
Chromium browsers, 26 of them enumerated: Opera, Yandex, Iridium, Chromium, 7Star, Torch, Cool Novo, Kometa, Amigo, Brave, CentBrowser, Chedot, Orbitum, Sputnik, Comodo Dragon, Vivaldi, Citrio, 360, Uran, Liebao, Elements, Epic Privacy, Coccoc, Sleipnir 6, QIP Surf and Coowon. UC Browser, Edge and QQ get their own module.
Safari for Windows deserves a separate mention: rather than implementing a binary
plist parser, it invokes Apple’s own plutil.exe — a signed third-party binary — to
convert the keychain to XML before parsing it. Living off someone else’s land.
Cookies have their own, wider list, with 12 Gecko browsers via
cookies.sqlite. K-Meleon, WaterFox, BlackHawk and CyberFox are touched for cookies only,
never for credentials.
And 39 application modules, each isolated in a Visual Basic On Error
so that one failure does not break the chain:
| Category | Applications | Predominant technique |
|---|---|---|
| Outlook, Foxmail, Opera Mail, eM Client, Mailbird, Becky!, The Bat!, IncrediMail, PocoMail, Eudora, Claws Mail, Thunderbird, Postbox, SeaMonkey | Registry and DPAPI; 3DES with bespoke substitution alphabets in The Bat! and Opera Mail; PBKDF2 with a hardcoded key in eM Client | |
| FTP and SFTP | FileZilla, WinSCP, SmartFTP, FlashFXP, WS_FTP, FTP Navigator, FTP Commander, FTPGetter, CoreFTP | XML, INI and a bespoke hex deobfuscation in WinSCP |
| VPN | NordVPN, OpenVPN GUI, Private Internet Access | DPAPI, with extra entropy for OpenVPN |
| Messaging | Pidgin, Trillian, Psi/Psi+, Paltalk | Plaintext XML and DPAPI |
| Remote desktop and DB | Windows RDP, RealVNC 3.x and 4.x, TightVNC, MySQL Workbench | Registry and DPAPI over credential blobs |
| Other | JDownloader, DynDNS Updater, NO-IP, Windows Credential Manager | Embedded fixed keys and vaultcli.dll |
What shipped switched off
Several capabilities are compiled but inert. What makes this interesting is that it can be proven: in both cases the switch has a single writer — the static constructor, which sets it to zero — and the readers use it as a guard.
| Feature | State |
|---|---|
Second persistence, via %startupfolder%\%insfolder%\%insname% | dead The entry point skips the whole block. Those placeholders are never reached, so the only live persistence is the stub’s |
The IP Address: line in the report | dead It blocks both the lookup and the line itself. And even enabled, the fetcher calls WebRequest.Create('%'): the unsubstituted placeholder |
| Tor exfiltration over HTTP POST | inactive Fully implemented, proxy included, but its destination URL was left as a template |
All three share a cause: this build left the builder with several options unconfigured. A useful reminder that “the malware has capability X” and “the malware does X” are different claims, and the difference is sometimes two bytes in a static constructor.
Detection
Six YARA rules, from the most generic to the most specific, anchored on bytes verified against the binaries rather than on guesswork:
| Rule | Anchor | Stages |
|---|---|---|
| Xor70_Primitive | IL 1F 70 61, the shared primitive. Exactly one occurrence in each stage that uses it | 2, 3 |
| Dropper_ConfuserEx_Stego | The PNG resource name, more than 50 occurrences of U+200B in UTF-8 and the decoy | 0 |
| Loader_IExpando | The namespace plus the pieces of the Pico2012 → Invoke trick | 2 |
| Stub_NETReactor | .NET Reactor sentinels, the native delegate and the decoy library | 3 |
| Payload_XorStringTable | IL of the table[i] ^= i ^ K loop, with field tokens and the constant wildcarded | 5 |
| Campaign | Random resource names from this specific build | 3, 4 |
All five stages are covered, and the negative controls — the PNG, the encrypted blob, the XML, native binaries — trigger none of them.
An honest limitation: these were not tested against a corpus of benign .NET binaries, so the false-positive risk is estimated by construction rather than measured. The shared primitive rule is three bytes of IL: use it as a supporting signal, never alone. And stage 5 has not one plaintext string, so its detection rests entirely on the IL pattern.
What I could not close
Two things, and neither is a detail.
None of this was observed on real Windows. I extracted the bytes of all five stages and each is a structurally valid PE, and the SMTP configuration comes from the very code that consumes it: that part is strong evidence. But the injection, the scheduled task, the Defender exclusion and the SMTP traffic were read in code, not watched happening. The distinction matters.
Pivoting was not done; it did not come back negative. Without access to a threat intelligence API I was limited to web search, which does not index that kind of database. There were no hits for the hash or the domain, but that does not mean “no known activity”: it means “not checked”. Real pivoting would need VirusTotal or MalwareBazaar, passive DNS on the domain, and correlation by resource names against a sample repository.
There is also an opaque block of some 900 instructions without a single string, and eight stage 3 strings whose offset is computed at runtime.