Key takeaways
- A South Asian financial institution was targeted with two custom malware components: a modular backdoor (BRUSHWORM) and a keylogger (BRUSHLOGGER)
- BRUSHWORM features anti-analysis checks, AES-CBC encrypted configuration, scheduled task persistence, modular DLL payload downloading, USB worm propagation, and broad file theft targeting documents, spreadsheets, email archives, and source code
- The keylogger masquerades as libcurl via DLL side-loading, capturing system-wide keystrokes with window context tracking and XOR-encrypted log files
- Multiple earlier testing versions (
V1.exe,V2.exe, etc.) were discovered on VirusTotal, some using free dynamic DNS infrastructure, indicating iterative development
Introduction
During a recent investigation, Elastic Security Labs identified malware deployed on a South Asian financial institution’s infrastructure. The victim environment had only SIEM-level visibility enabled, which limited post-exploitation telemetry. The intrusion involved two custom binaries: a backdoor named paint.exe and a keylogger masquerading as libcurl.dll.
BRUSHWORM functions as the primary implant, responsible for installation, persistence, command-and-control communication, downloading additional modular payloads, spreading via removable media, and stealing files with targeted extensions. BRUSHLOGGER supplements this by capturing system-wide keystrokes via a simple Windows keyboard hook and logging the active window context for each keystroke session.
Neither binary employs meaningful code obfuscation, packing, or advanced anti-analysis techniques. The overall quality of the code is low — for example, the backdoor writes its decrypted configuration to disk in cleartext before encrypting and saving a second copy, then deletes the cleartext file. Given the absence of a kill switch, the use of free dynamic DNS servers in testing versions, and some coding mistakes, we assess with moderate confidence that the author is relatively inexperienced and may have leveraged AI code-generation tools during development without fully reviewing the output.
Through VirusTotal pivoting, we identified what appear to be earlier development and testing versions of the backdoor uploaded under filenames such as V1.exe, V2.exe, and V4.exe, with varying configurations.
BRUSHWORM code analysis
The backdoor is the primary implant responsible for establishing persistence, communicating with the C2 server, downloading modular payloads, spreading to removable media, and exfiltrating files.
Anti-analysis and sandbox detection
The malware begins execution with a series of environment checks designed to detect analysis environments, though the techniques are straightforward and lack sophistication.
Screen resolution check: If the display resolution is less than 1024×768 pixels, execution terminates immediately. This is a common sandbox detection technique.
Username and computer name check: The malware checks whether the machine's username or the computer name is “sandbox”. If either condition matches, it terminates. These checks target the default name commonly used in analysis sandboxes.
Hypervisor detection: Using the CPUID instruction, the malware queries the hypervisor vendor string and compares it against the following known virtualization platforms:
| Hypervisor Vendor String | Platform |
|---|---|
VMWAREVMWARE | VMware |
KVMKVMKVM | KVM |
XENVMMXENVMM | Xen |
PRL HYPERV | Parallels |
TCGTCGTCGTCG | QEMU (TCG) |
ACRNACRNACRN | ACRN |
MICROSOFT HV | Hyper-V |
If a hypervisor is detected, the malware does not terminate — it merely sleeps for one second before continuing execution.
Mouse activity check: After the initial setup, the malware monitors mouse movement for 5 minutes. If the cursor does not move during this period, execution terminates. This acts as an additional sandbox evasion measure.
Installation and directory setup
On first execution, the malware creates multiple hidden directories with hardcoded paths. These directories serve distinct roles throughout the malware's lifecycle:
| Directory | Purpose |
|---|---|
C:\ProgramData\Photoes\Pics\ | Main installation folder for the backdoor binary |
C:\Users\Public\Libraries\ | Storage for downloaded modules from the C2 server (e.g., Recorder.dll) |
C:\Users\Public\AppData\Roaming\Microsoft\Vault\ | Storage of the encrypted configuration file (keyE.dat) |
C:\Users\Public\Systeminfo\ | Staging directory for stolen files |
C:\Users\Public\AppData\Roaming\NuGet\ | Tracks exfiltrated file paths with their SHA-256 hashes (hashconfig) |
The misspelling "Photoes" (instead of "Photos") is consistent across both the backdoor and keylogger components and appears to be an authentic mistake by the author to blend with the other user’s media directories.
Configuration decryption
The backdoor's configuration is stored as a JSON structure with field values encrypted using AES-CBC. The AES key is hardcoded in the binary, while the initialization vector (IV) is prepended to each encrypted field's data blob.
The decrypted configuration follows this structure:
{
"internetCheckDomain": "<...>",
"downloadDomain": "<...>",
"retryCount": 0
}
This configuration is not referenced anywhere in the malware's operational logic. The C2 server address that the backdoor actually communicates with is a separate C++ global string variable stored in cleartext(resources.dawnnewsisl[.]com/updtdll), which is passed directly to the function responsible for C2 communication and payload downloads.
The decrypted configuration fields (internetCheckDomain, downloadDomain, retryCount) go entirely unused in this build. This configuration is likely intended for a future version, a separate payload component, or was simply disabled in the deployed build, reinforcing the impression of a codebase under active, disorganized development.
Persistence
The malware establishes persistence by creating a Windows scheduled task named MSGraphics through the COM Task Scheduler interface. The task is configured to execute the malware binary each time a user logs in, ensuring the backdoor survives system reboots.
Payload download and execution
The backdoor uses the WinHTTP library to issue a GET request to the C2 server at the URI /updtdll to download a DLL payload. The downloaded binary is written to C:\Users\Public\Libraries\ as Recorder.dll.
We were unable to recover the downloaded payload during our investigation, but the naming convention and execution method suggest it is a modular plugin — likely providing additional post-exploitation capabilities such as screen recording or data exfiltration.
The downloaded DLL is executed by creating a second scheduled task named MSRecorder that uses rundll32.exe to load and run it. This mirrors the same COM-based scheduled task creation method used for persistence.
The C2 server's SSL certificate is issued by Let's Encrypt.
USB spreading and file theft
Once BRUSHWORM detects that the host is already infected (by checking for the presence of the installation directory), its behavior diverges based on internet connectivity. The malware performs a connectivity check by attempting to reach www.google.com.
Scenario 1 — Internet access available:
The malware spawns two threads targeting external storage devices:
- Removable drive infection: The backdoor copies itself to any connected removable storage devices using socially-engineered filenames designed to entice victims in a corporate financial environment:
Salary Slips.exeNotes.exeDocuments.exeImportant.exeDont Delete.exePresentation.exeEmails.exeAttachments.exe
- File theft from removable and optical drives: Both threads enumerate files on the connected media and exfiltrate to a folder any file matching a broad set of targeted extensions:
| Category | Extensions |
|---|---|
| Documents & Word Processing | .doc, .docx, .dot, .dotx, .wps, .wpd, .wp, .rtf, .txt, .odt, .ott, .pages |
| Spreadsheets | .xls, .xlsx, .xlsm, .xlt, .xltx, .xlw, .ods, .ots, .csv, .tsv, .dbf, .wk1, .wk3, .wk4, .123 |
| Presentations | .ppt, .pptx, .pot, .potx, .pps, .ppsx, .odp, .otp, .key, .sxi |
| Portable & Layout | .pdf, .xps, .epub, .mobi, .ps, .prn, .tex, .latex, .pub, .p65, .fm |
| Archives & Disk Images | .zip, .rar, .7z, .tar, .gz, .bz2, .xz, .iso, .cab, .arj, .lzh, .lha, .tgz, .tbz, .txz |
| Email & Notes | .pst, .ost, .msg, .eml, .emlx, .mbox, .mbx, .maildir, .one |
| Code & Data | .py, .md, .xml, .json |
Stolen files are staged in the C:\Users\Public\Systeminfo\ directory. The malware also maintains a tracking file (hashconfig) at C:\Users\Public\AppData\Roaming\NuGet\ that records each exfiltrated file's path alongside its SHA-256 hash, likely to avoid re-exfiltrating the same files on subsequent runs.
Scenario 2 — No internet access:
If the internet connectivity check fails, the malware still infects removable drives with the same lure-named copies. However, in this scenario, it additionally copies stolen files and files from the user's profile directory (matching the same extension list) to the removable drives. This behavior serves as a data exfiltration bridge for environments with restricted or air-gapped network access — using USB drives to physically carry stolen data off the network.
BRUSHLOGGER code analysis
The second component is a 32-bit Windows DLL (4f1ea5ed6035e7c951e688bd9c2ec47a1e184a81e9ae783d4a0979501a1985cf) designed for DLL side-loading. It masquerades as libcurl.dll by exporting seven standard curl_easy_* API functions, all of which are empty stubs pointing to a single RET instruction. The malicious functionality executes entirely from the DllMain entry point on DLL_PROCESS_ATTACH.
Initialization
At startup, the keylogger decodes a mutex name from a Base64-encoded string: “Windows-Updates-KB852654856”. The mutex name mimics a Windows Update knowledge base identifier. If CreateMutexA returns ERROR_ALREADY_EXISTS, the process terminates immediately to enforce single-instance execution.
BRUSHLOGGER retrieves the current Windows username via GetUserNameA, computes its MD5 hash using the Windows CryptoAPI, and constructs the final log file path:
C:\programdata\Photoes\<username>_<MD5(username)>.trn
The log file is initially created with CreateFileA using CREATE_NEW, then reopened with FILE_APPEND_DATA access for append-mode writing throughout the session.
Hook installation and message pump
After file setup, the keylogger installs a low-level keyboard hook:
SetWindowsHookExA(WH_KEYBOARD_LL, keyboard_hook_callback, NULL, 0);
The WH_KEYBOARD_LL hook type captures keyboard input system-wide across all threads and processes. The hook procedure runs in the context of the installing thread, requiring a standard Windows message pump (GetMessageA / TranslateMessage / DispatchMessageA) to keep the hook alive.
Keystroke capture logic
The core capture logic resides in the hook callback, which processes every keyboard event in the system.
For each keystroke event, the callback:
- Retrieves the foreground window handle via
GetForegroundWindow - Allocates memory via
VirtualAllocfor the window title - Captures the current timestamp via
GetLocalTime, formatted asDD-MM-YYYY HH:MM - Retrieves the window title bar text via
GetWindowTextA
When the foreground window changes, a context marker is appended to the keystroke buffer:
\n<timestamp> <window title>\n
Keystroke encoding: The callback processes WM_KEYDOWN, WM_SYSKEYDOWN, WM_KEYUP, and WM_SYSKEYUP messages. Each keystroke is logged as a two-digit hexadecimal virtual key.
XOR-encrypted log files
The flush routine copies the keystroke buffer to a local stack buffer, XOR-encrypts each byte with a hardcoded single-byte key 0x43, and writes the result to the log file via WriteFile. After a successful write, the global buffer is cleared.
The XOR key 0x43 is trivially reversible — the encryption serves only as basic obfuscation rather than meaningful cryptographic protection.
Conclusion
Despite their low sophistication and multiple implementation flaws, these two binaries deliver a functional collection platform that combines modular payload loading, USB worm propagation, broad file theft with air-gap bridging, and persistent keystroke capture via DLL side-loading. The iterative testing versions and active C2 infrastructure suggest an actor still refining their toolset. Elastic Security Labs will continue monitoring this activity cluster.
BRUSHLOGGER, BRUSHWORM, and MITRE ATT&CK
Elastic uses the MITRE ATT&CK framework to document common tactics, techniques, and procedures that advanced persistent threats use against enterprise networks.
Tactics
Tactics represent the why of a technique or sub-technique. It is the adversary’s tactical goal: the reason for performing an action.
- Execution
- Persistence
- Defense Evasion
- Credential Access
- Discovery
- Lateral Movement
- Collection
- Exfiltration
- Command and Control
Techniques
Techniques represent how an adversary achieves a tactical goal by performing an action.
- Scheduled Task/Job: Scheduled Task
- Hijack Execution Flow: DLL Side-Loading
- Input Capture: Keylogging
- Obfuscated Files or Information
- Deobfuscate/Decode Files or Information
- Virtualization/Sandbox Evasion: System Checks
- Data Staged: Local Data Staging
- Replication Through Removable Media
- Automated Collection
- Data from Removable Media
- Application Window Discovery
- Ingress Tool Transfer
- Masquerading: Match Legitimate Name or Location
Detecting BRUSHLOGGER and BRUSHWORM
YARA
Elastic Security has created YARA rules to identify this activity. Below are YARA rules to identify the BRUSHWORM and BRUSHLOGGER:
rule Windows_Trojan_BrushLogger_304ee146 {
meta:
author = "Elastic Security"
os = "Windows"
arch = "x86"
category_type = "Trojan"
family = "BrushLogger"
threat_name = "Windows.Trojan.BrushLogger"
reference_sample = "4f1ea5ed6035e7c951e688bd9c2ec47a1e184a81e9ae783d4a0979501a1985cf"
strings:
$a = "%02d-%02d-%d %02d:%02d " fullword
$b = { 81 ?? ?? A1 00 00 00 74 09 81 ?? ?? A0 00 00 00 75 09 6A 00 6A 10 E8 }
condition:
all of them
}
rule Windows_Trojan_BrushWorm_7c2098ef {
meta:
author = "Elastic Security"
os = "Windows"
arch = "x86"
category_type = "Trojan"
family = "BrushWorm"
threat_name = "Windows.Trojan.BrushWorm"
reference_sample = "89891aa3867c1a57512d77e8e248d4a35dd32e99dcda0344a633be402df4a9a7"
strings:
$a = "internetCheckDomain" wide fullword
$b = { B8 00 00 00 40 33 C9 0F A2 48 8D ?? ?? ?? 89 07 89 5F 04 89 4F 08 89 57 0C 45 33 C0 }
condition:
all of them
}
Observations
The following observables were discussed in this research.
| Observable | Type | Name | Reference |
|---|---|---|---|
89891aa3867c1a57512d77e8e248d4a35dd32e99dcda0344a633be402df4a9a7 | SHA-256 | paint.exe | BRUSHWORM |
4f1ea5ed6035e7c951e688bd9c2ec47a1e184a81e9ae783d4a0979501a1985cf | SHA-256 | libcurl.dll | BRUSHLOGGER |
resources.dawnnewsisl[.]com/updtdll | domain-name | C2 server |
