BAVACAI Ransomware — Full Analysis
1. Sample Identification
| Field | Value |
|---|---|
| Family | BAVACAI (self-identified via .BAVACAI extension; internal project name locker_win_x64_encrypter) |
| SHA-256 | 86b4d075d5bd0c49cbb21fd43935789b6612a2165273cc158dd0607b68941d04 |
| MD5 | 7ae9c8b779ce3114199f4fd6335f681d |
| Type | PE32+ x86-64 console subsystem, Windows |
| Size | 752 640 bytes (735 KB) |
| Language | C++17 (MSVC) — STL streams, <filesystem>, nlohmann::json v3.11.3, MFC fragments (afxGlobalData), Microsoft osyncstream |
| Compile timestamp | not relied upon for attribution |
| PDB path | J:\edu\niggerProject\bin\Publish\locker_win_x64_encrypter.pdb |
| Image base | 0x140000000 |
| Sections | 5 — .text (0x70000), .idata (0x680), .rdata (0x3E980), .data (0x8000), .pdata (0x6000) |
| Functions | 2 253 total (284 named after Lumina pull + analysis renames; 931 library detected) |
| Entry point | 0x1400426B8 (start — MSVC CRT init) → main at 0x14001F890 |
The binary is a single-stage ransomware encryptor built with verbose debug logging left in. Strings are not obfuscated — the only encrypted artefact is the embedded JSON configuration (PE resource id 101). The PDB path includes the developer's project tree, an internal slur as folder name, and the build subfolder — strong amateur-development signal. There is no anti-debug, no anti-VM, no anti-sandbox, no string obfuscation, no API hashing, no code injection, no packer; the whole pipeline is plainly visible to a casual reverser. The binary nonetheless ships with curated kill lists, multi-vector recovery inhibition, network-share enumeration, multi-threaded scanning, and a per-victim PKI hierarchy.
Imports (10 DLLs)
| DLL | Count | Purpose |
|---|---|---|
| KERNEL32 | 139 | Threading (CreateThread, SRW locks), file I/O (CreateFileW, FindFirstFileW, FindFirstVolumeW, WriteFile), drives (GetLogicalDrives, GetDriveTypeW, SetVolumeMountPointW, QueryDosDeviceW, GetVolumePathNamesForVolumeNameW), processes (CreateProcessW, OpenProcess, TerminateProcess, CreatePipe, WaitForSingleObject), resources (FindResourceW, LoadResource, LockResource, SizeofResource), CRT support |
| ADVAPI32 | 17 | Legacy CryptoAPI (CryptAcquireContextW, CryptGenKey, CryptImportKey, CryptExportKey, CryptDuplicateKey, CryptDestroyKey, CryptEncrypt, CryptGetKeyParam, CryptGenRandom, CryptReleaseContext), token query (OpenProcessToken, GetTokenInformation), registry (RegCreateKeyExW, RegSetValueExW, RegGetValueA/W, RegCloseKey) |
| GDI32 | 13 | Bitmap composition for the wallpaper option (CreateCompatibleDC, CreateCompatibleBitmap, CreateFontW, TextOutA, GetBitmapBits, SelectObject, SetTextColor, SetBkMode, CreateSolidBrush, FillRect, GetTextExtentPoint32A, DeleteDC, DeleteObject, GetObjectW) |
| USER32 | 10 | Console window control (ShowWindow, GetConsoleWindow, GetForegroundWindow, IsWindowVisible, EnumWindows, GetWindowThreadProcessId, GetShellWindow), wallpaper application (SystemParametersInfoW, FillRect, GetDC), hotkey thread (GetAsyncKeyState) |
| SHLWAPI | 6 | Path predicates (PathFileExistsW, PathFindExtensionW, PathIsDirectoryW, PathIsNetworkPathW, PathIsUNCW, StrStrIW) — last one is the skip-path matcher |
| RstrtMgr | 5 | Restart Manager (RmStartSession, RmRegisterResources, RmGetList, RmShutdown, RmEndSession) — release handles on locked files |
| CRYPT32 | 4 | Base64 codec (CryptBinaryToStringA/W, CryptStringToBinaryA/W) for blob encoding in note + registry |
| SHELL32 | 2 | SHEmptyRecycleBinW, SHGetFolderPathW |
| MPR | 1 | WNetGetConnectionW — enumerate mapped network drives (mode -network) |
| urlmon | 1 | URLDownloadToFileW — fetch external IP from api.ipify.org |
Notably absent:
- No WinHTTP/WinINet/Ws2_32/socket APIs → no exfiltration in this binary, no C2 callback (the only outbound HTTP is api.ipify.org for victim IP fingerprinting)
- No OpenSCManager/EnumServicesStatus → service stops are shell-spawned via net stop, not in-process
- No CreateToolhelp32Snapshot → process kills are shell-spawned via taskkill, not in-process
- No IsDebuggerPresent/NtQueryInformationProcess/CheckRemoteDebuggerPresent reachable from non-CRT code paths
- No BCryptOpenAlgorithmProvider / CNG — relies entirely on legacy CryptoAPI (Microsoft Base Cryptographic Provider, MS_DEF_PROV)
- No mutex API call (CreateMutexA/W/OpenMutexW) — no single-instance enforcement
2. Infrastructure
| Field | Value |
|---|---|
| Onion (file server) | http://t33zoj4qwv455fog7qnb2azi5xcdxkixughmmduzbw2rtdgryqfbh6id.onion |
nhuvgh@outlook.com |
|
| TOX ID (qtox) | 7C564920870C0D33535D2012ECDDE389FE25BAF7AF427DD584EE39C04AF8CF024F8BFA93D8DB |
| Note filename | WHATS_HAPPEND.txt (typo — "HAPPEND" instead of "HAPPENED") |
| Encrypted file extension | .BAVACAI (uppercase, appended after original extension) |
| Mutex | None |
| Payment | Free-text Tor / email negotiation (no wallet, no portal URL) |
| Master public key | RSA-2048, e=65537 — embedded in JSON config as base64 MS PUBLICKEYBLOB |
| Victim ID | base64(RSA(masterPubkey, PCInfo_blob)) substituted for [IDENTIFIER] placeholder in note |
Infrastructure surface is thin and operator-grade: a single Tor file server (claimed to host victim data), a clearnet outlook.com address, and a Tox ID. No leak site URL is embedded; no chat-portal UUID; no per-build campaign identifier. The note text is entirely static — there is no per-victim cohort key or affiliate flag.
3. Ransom Note
Filename and deployment
WHATS_HAPPEND.txt (note the misspelling) — deployed by CreateNote after encryption finishes. Function CreateNote writes the note in:
- The root of every encrypted drive (per drive iterator
Drive_EnumLogicalat0x140030A80andDrive_EnumCipherat0x140039F30) - Every directory the scanner enters (per
DirWalkerat0x14001AA20) - Path-skip exclusions apply: directories matching any
skipPathessubstring receive no note
If openRequirementsOnFinish=true (default), the note is ShellExecute-opened on the operator's console at the end of the run.
Content (decrypted JSON field requirementsFileDataUTF8, 723 bytes)
DON'T PANIC!!! YOUR FILES ARE PERFECT AND SAFE!
We've found flaws in your security system and gained access to your internal corporate network. Your files were encrypted, and we can help you decrypt them and fix any existing security flaws.
We've also retrieved files from your servers, which will be published in 72 hours if you don't contact us.
Our contact information: qtox - 7C564920870C0D33535D2012ECDDE389FE25BAF7AF427DD584EE39C04AF8CF024F8BFA93D8DB
e-mail: nhuvgh@outlook.com
our tor fileserver with your files - http://t33zoj4qwv455fog7qnb2azi5xcdxkixughmmduzbw2rtdgryqfbh6id.onion
Your ID: [IDENTIFIER]
Please do not use file recovery services. They are either scammers or middlemen. In both cases, you will simply pay more.
The [IDENTIFIER] placeholder is replaced at runtime by Context__InitializePCInfo at 0x1400070E0:
-
Build a plaintext UTF-8 PCInfo blob via
Util_GetPCInfoat0x140033040. The exact line-by-line format is:IP: <api.ipify.org response> PC-Name: <GetComputerNameA> Domain: <GetComputerNameExA(ComputerNameDnsDomain)> CPU: <RegGetValueA HKLM\HARDWARE\DESCRIPTION\System\CentralProcessor\0\ProcessorNameString> RAM: <GlobalMemoryStatusEx.ullTotalPhys / 1048576> MB Disks: \\.\PhysicalDrive0 \\.\PhysicalDrive1 ...Each section is omitted when the underlying API call fails. Disks are enumerated by opening
\\.\PhysicalDrive<N>for increasing N untilCreateFileAfails. -
RSA-encrypt with master public key (multi-block PKCS#1 v1.5 → 5 × 256 = 1280 B output for the ~1 KB plaintext)
- Base64-encode via
CryptBinaryToStringA(CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF) - Single string-replace of
[IDENTIFIER]substring with the resulting base64 ID
Because the plaintext is a deterministic function of (hostname, DNS domain, CPU model, RAM, physical-disk count, external IP), two runs from the same machine produce different ciphertexts (RSA-PKCS#1 padding randomises) but the underlying plaintext digest is reproducible — useful for attacker-side de-duplication of victims contacting from different IPs over time.
The 72-hour exfil claim is not backed by code in this build — see §10 (Exfiltration). The ransom note hints at exfiltration the binary cannot perform.
4. Execution Flow
main at 0x14001F890
1. setmode(stdout, _O_U16TEXT) — wide console output
2. Argv parsing loop (i=1..argc):
-help → print usage, exit
-network → set flag: only network/UNC drives encrypted
-skip_misc → set flag: skip preRunCommands, recycle, wallpaper
<unknown> → "Unknown argument: <arg>" warning
3. App_Run (0x1400081D0):
a. SHGetFolderPathW for CSIDL 26 (APPDATA), 28 (LOCAL_APPDATA),
46 (COMMON_APPDATA), 35 (WINDOWS), 36 (SYSTEM) — all appended
to runtime skipPaths
b. Context::LoadSettings → decrypt resource SETTINGS, parse JSON,
hydrate global settings (extension, public key, etc.)
c. Context::InitializeKeys → load HKLM\SOFTWARE\PAIDMEMES or
generate fresh RSA-2048 victim keypair
d. Crypto::RSA::ctor (master pubkey context)
e. Context::InitializePCInfo → fetch external IP, build victim ID,
substitute [IDENTIFIER] in note template
f. Crypto::RSA::Cleanup
4. hideConsole=true → ShowWindow(GetConsoleWindow(), SW_HIDE)
5. Spawn Thread_HotkeyMonitor (0x14001EB90) — VK_INSERT/VK_HOME watcher
6. Run preRunCommands (unless -skip_misc): 30 shell commands via
Util_RunProcessElevated (no PPID spoof; sysnative WoW64 hop)
7. FindFirstVolumeW / FindNextVolumeW iteration over all volumes
including non-lettered \\?\Volume{GUID}\ entries; for each:
- QueryDosDeviceW validates the NT mapping
- Drive_EnumLogical (GetVolumePathNamesForVolumeNameW +
SetVolumeMountPointW) mounts unmounted volumes at temp letters
8. Drive_EnumCipher (GetLogicalDrives + GetDriveTypeW filter to
REMOVABLE+FIXED+RAMDISK) — populates the local-drives vector
9. Drive_EnumNetworkConnections (WNetGetConnectionW) — populates the
mapped-network-drives vector
10. Privilege check: OpenProcessToken(TOKEN_QUERY) +
GetTokenInformation(TokenElevation) → r15 = TokenIsElevated
11. Branch on elevation (master gate at 0x140020e8e):
- elevated → encrypt local drives only (the result of step 8)
+ Util_RunProcess(<own_path> -network -skip_misc)
with PPID spoofed to explorer.exe to fork a
sibling process that handles the network drives
in parallel (see §10 PPID Spoofing)
- not elev. → encrypt mapped network drives only (step 9)
12. For each drive selected:
a. CreateNote at root
b. DirWalker recursion
- StrStrIW skip-path check at every directory
- For each file: extension skip check, then EncryptFile
- threadPool=true → enqueue file path on thread pool
(threadPoolMaxThreads=32, priority queue pulls
threadPoolPriorityExtensions first)
13. Wait for all encryptor threads to drain
14. byte_1400B7B3C = 1 → signal Thread_HotkeyMonitor to exit
15. removeRecycle=true → SHEmptyRecycleBinW(NULL) per drive
16. backgroundImage=true → CreateAndSetBackgroundImage (renders the
note text into a 1280×1024 BMP and sets it as wallpaper via
SystemParametersInfoW(SPI_SETDESKWALLPAPER)) — disabled by this
build's config (backgroundImage=false)
17. postRunCommands: 0 entries in this build — no-op
18. openRequirementsOnFinish=true → open the note in default handler
Privilege-asymmetric encryption strategy
Steps 8–11 implement a deliberate privilege-aware target split:
| Token state at runtime | Target | Drive enumerator | Driven by |
|---|---|---|---|
| Elevated (admin) | Local drives (REMOVABLE + FIXED + RAMDISK) | Drive_EnumCipher (0x140039F30) |
direct in-process loop |
| Not elevated | Already-mapped network drives | Drive_EnumNetworkConnections (0x14003A160) |
direct in-process loop, OR self-relaunched sibling |
When admin, the binary additionally spawns a child copy of itself with the command line <own_path> -network -skip_misc, using the PPID-spoofing primitive described in §10 to make the child appear as a descendant of explorer.exe. The child then runs as the "non-elevated" branch and processes network drives in parallel with the admin parent's local-drive pass. The -skip_misc flag suppresses re-execution of preRunCommands, recycle-bin emptying, and wallpaper setting in the child — those side effects only run once, in the parent.
When not admin, the binary processes only what it has access to via existing user mappings, and does not attempt elevation, escalation, or new SMB authentication.
Volume coverage beyond drive letters
Step 7 (FindFirstVolumeW loop) is independent of drive letters. It runs before the elevation check and reaches every volume the OS exposes to the active user, including hidden recovery partitions, EFI System Partitions, and BitLocker-suspended volumes that have no letter assignment. For each such volume it calls SetVolumeMountPointW to mount it at a temporary letter, after which the standard drive-letter pipeline picks it up. The hard-coded skipPathes entries B:\Boot, A:\Boot, B:\EFI, A:\EFI, and :\Boot are expressly there to guard against re-encryption of these mount targets.
Thread architecture
When threadPool=true (default):
- A worker pool of up to
threadPoolMaxThreads(default 32) consumes a producer-consumer queue fed byDirWalker - The queue prioritises extensions in
threadPoolPriorityExtensions = [".sql", ".bak", ".VHDX"]— ensures high-value VM/database files encrypt early so kills happen before volumes are unmounted by service stops - One auxiliary thread (
Thread_HotkeyMonitor) runs in parallel watchingGetAsyncKeyStatepolled at ticks (no blocking hook)
If threadPool=false, EncryptFile is called inline from DirWalker for each file.
5. Encryption System
Key Hierarchy (3 layers)
[Master RSA-2048 keypair]
(private kept by operator,
public embedded in JSON config)
│
│ wraps
▼
[Victim RSA-2048 keypair]
(generated/persisted in HKLM\SOFTWARE\PAIDMEMES;
PRIVATEKEYBLOB stored under PRIVATE,
wrapped with master pub via RSA-PKCS1)
│
│ wraps
▼
[Per-file ChaCha20 32-byte key + 8-byte nonce]
(32+8=40 bytes random per file via CryptGenRandom,
wrapped with victim pub key, footer-appended)
This is a textbook operator-master / victim / per-file three-tier key hierarchy. Recovery requires only the master private key — every encrypted file carries its own self-sufficient key envelope.
Asymmetric: RSA-2048 (legacy CryptoAPI)
| Parameter | Value |
|---|---|
| Algorithm | RSA (Microsoft Base Cryptographic Provider, PROV_RSA_FULL) |
| Key size | 2048 bits |
| AlgID | CALG_RSA_KEYX (0x0000A400) |
| Padding | PKCS#1 v1.5 (default for CryptEncrypt with CALG_RSA_KEYX, no CRYPT_OAEP flag set) |
| Public exponent | 65537 (0x10001) |
| Master public key (config) | base64 MS PUBLICKEYBLOB, see appendix |
| Master modulus (BE, hex first 64 B) | d1db59… (full PEM extracted) |
Crypto::RSA::Encrypt at 0x14003D210 chunks input into ≤245-byte blocks (RSA-2048 PKCS#1 max plaintext) and concatenates the 256-byte ciphertexts. It is used for:
- Per-file 40-byte key+nonce wrap (1 block × 256 B)
- Victim PRIVATEKEYBLOB wrap during key generation (5 blocks × 256 B = 1280 B)
- PCInfo wrap for victim ID generation (5 blocks × 256 B = 1280 B, base64-encoded)
Symmetric: ChaCha20 (DJB original variant, no MAC)
| Parameter | Value |
|---|---|
| Algorithm | ChaCha20 (custom implementation, not BCrypt/CryptoAPI) |
| Sigma constant | expand 32-byte k — DJB classic 16-byte constant |
| State layout | 64 B init state + 8 B keystream byte counter at offset 0x80 |
| Key | 32 bytes (per-file from CryptGenRandom) |
| Nonce | 8 bytes (per-file from CryptGenRandom) — DJB original, not IETF 12-byte |
| Block counter | 8 bytes (initial = 0), at state[12..13] |
| Authentication | None — no Poly1305, no HMAC, no MAC of any kind |
| Per-file rekey | Yes (32+8=40 fresh bytes per file) |
ChaCha20__InitState at 0x140002E90 performs the canonical state setup:
state[0..15] = "expand 32-byte k" // sigma
state[16..47] = key[0..31] // 32-byte key
state[48..55] = 0 // block counter (low+high = 0)
state[56..63] = nonce[0..7] // 8-byte nonce
state[128] = 64 // keystream byte index, set in EncryptFile
// ("flush" forces ChaCha block on first XOR)
The block function at sub_140003390 performs the standard 20-round ChaCha20 block (10 double-rounds: column + diagonal). Output is XORed against plaintext byte-by-byte from state[64..127] (the keystream block).
File Encryption Process — EncryptFile at 0x14001A2A0
1. CreateFileW(GENERIC_RW, share=0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL)
2. If failed with ERROR_SHARING_VIOLATION (32) or ERROR_LOCK_VIOLATION (33),
AND path is not network → Util_RestartManagerUnlock:
RmStartSession → RmRegisterResources → RmShutdown(RmForceShutdown)
to force-close handles held by other processes (Office, SQL, …)
→ reopen
3. Crypto::GenerateBytes(40) → buf[0..31]=key, buf[32..39]=nonce
4. ChaCha20::InitState(state, key, nonce); state[128]=64; state[12..13]=0
5. Crypto::RSA::Encrypt(victim_pubkey, buf40) → wrap = 256 B
6. if (sizeof(wrap) != 256) throw "Bad password size: …"
7. Crypto::CryptFile(hFile, state, ?, fileSize) ── see §5 below
8. Build footer (1544 B): victim_priv_blob_wrapped (1280) ‖ wrap (256) ‖
qword(0x6) (8 bytes constant — sizeof(L".crypt") in chars)
9. WriteFile(hFile, footer, 1544, offset=fileSize)
10. CloseHandle, MoveFileW(orig → orig + ".BAVACAI")
Intermittent Encryption — Crypto::CryptFile at 0x14003DDE0
Two parameters from the JSON config drive the pattern (bytesForEncrypt is loaded into global ::Size via LODWORD so its 32-bit value is what is consulted in the file-size comparison):
bytesForEncrypt(BFE) = 630 057 793 bytes (≈ 601 MiB) — Range A maximum buffer sizebytesCryptAndSkip(BCS) = 352 055 393 bytes (≈ 336 MiB) — chunk size of the inner encrypt-N / skip-N pattern
Algorithm:
size_A = min(fileSize, BFE)
read [0, size_A] into buffer of size_A bytes
v12 = 0
while v12 < size_A:
encrypt min(BCS, size_A - v12) bytes at offset v12 (XOR ChaCha20 keystream)
v12 += 2 * BCS # skip BCS bytes
write buffer back at file offset 0
if fileSize > 2 * BFE:
offset_B = fileSize - BFE
read [offset_B, fileSize] into buffer of BFE bytes
apply the same encrypt-BCS / skip-BCS inner loop on this buffer
write buffer back at offset_B
Effective coverage (with the in-build constants BFE = 601 MiB, BCS = 336 MiB):
| File size | Range A encrypts | Range B encrypts | Total encrypted | Coverage |
|---|---|---|---|---|
| ≤ 336 MiB | Whole file (one pass, no skip triggered) | — | full file | 100 % |
| 336 MiB – 601 MiB | [0, 336 MiB] |
— | 336 MiB | 56 % – 100 % |
| 601 MiB – 1.2 GiB | [0, 336 MiB] (within the 601 MiB Range A buffer) |
— | 336 MiB | 28 % – 56 % |
| > 1.2 GiB | [0, 336 MiB] |
[fileSize − 601 MiB, fileSize − 265 MiB] (the first 336 MiB inside the last-601 MiB window) |
672 MiB | 6 % – 56 % (decreases as file grows) |
Two consequences of this exact wording of the inner loop:
- Files larger than 1.2 GiB always have an unencrypted 265 MiB tail at the very end. Range B encrypts only the first 336 MiB of the last-601 MiB window, not the last 336 MiB of the file. The bytes in
[fileSize − 265 MiB, fileSize]are never touched. - Range A coverage stops at offset 336 MiB even for files between 600 MiB and 1.2 GiB. The Range A buffer is 601 MiB but the inner loop encrypts only the first 336 MiB inside it, then the
v12 += 2*BCSjump goes past the buffer end and the loop exits. The bytes[336 MiB, 601 MiB]of the Range A buffer are read into memory and immediately written back unmodified.
Because BCS itself is 336 MiB (larger than virtually any user document), the "skip" is never observed within a single encrypted span — the encrypt N, skip N pattern degenerates into "encrypt one contiguous chunk per Range, then exit". The intermittent-encryption knob is effectively dormant in this build; what acts is the per-Range cap. For typical files (≤ 336 MiB), the binary performs full file encryption.
The configured constants make the encryption strategy practically equivalent to: "encrypt up to 336 MiB at the start of every file, plus 336 MiB ~600 MiB before the end of files larger than 1.2 GiB". The strategy is tuned to corrupt VHDX/database/backup files large enough to make full-encryption time-prohibitive while still touching enough of the file structure to render it unusable.
Encrypted File Format
[ original file content, partially XORed with ChaCha20 keystream ]
[ size: original size (unchanged) — encryption is in-place ]
----- Footer: 1544 bytes appended at end -----
[ 0 .. 1279 ] : RSA(masterPubkey, victim_PRIVATEKEYBLOB)
— 5 × 256-byte PKCS#1 blocks
— recoverable only with master private key
[ 1280 .. 1535 ] : RSA(victim_pubkey, ChaCha20_key32 ‖ ChaCha20_nonce8)
— 1 × 256-byte PKCS#1 block
— recoverable with victim private key
[ 1536 .. 1543 ] : 0x06 0x00 0x00 0x00 0x00 0x00 0x00 0x00
— fixed 8-byte trailer = sizeof(L".crypt") = 6
— usable as YARA tail magic
Recovery flow (operator-side):
- Operator has master private RSA-2048 key
- Decrypt footer[0..1279] → victim PRIVATEKEYBLOB
- Import victim private key into CryptoAPI
- Decrypt footer[1280..1535] → 32 B key + 8 B nonce
- Re-init ChaCha20 state, XOR over Range A (and Range B if applicable) → plaintext
- Truncate file to (size – 1544), strip
.BAVACAIextension
Recovery feasibility for the victim: zero. Without the master private RSA key, neither inline footer nor registry blob is reachable. Both bavacai_rsa_pubkey.pem (2048-bit n+e) and the registry-resident victim public key are public information; the corresponding private keys are not in the binary.
6. File Targeting
Targeted extensions
All files except those matched by skipExtensions — there is no allow-list. Extension matching uses PathFindExtensionW followed by case-insensitive comparison against the skip list.
Excluded directories — substring match via StrStrIW on each directory path
JSON config skipPathes (typo) — 13 entries:
| Path | Matches |
|---|---|
C:\perflogs |
OEM diag |
C:\Intel |
Intel drivers |
C:\HP |
HP drivers |
C:\AMD |
AMD drivers |
C:\Dell |
Dell drivers |
C:\Drivers |
generic drivers |
C:\inetpub |
IIS web root |
B:\Boot, A:\Boot |
floppy boot |
B:\EFI, A:\EFI |
EFI partitions on letter B/A |
C:\ProgramData\Anydesk |
RMM (probably the operator's own remote-access tool) |
:\Boot |
any drive's boot dir |
Runtime additions (appended in App_Run):
| CSIDL | Resolves to | Purpose |
|---|---|---|
| 26 (APPDATA) | %AppData%\… |
Roaming user app data |
| 28 (LOCAL_APPDATA) | %LocalAppData%\… |
Local user app data |
| 35 (WINDOWS) | %windir% |
Windows itself (avoid bricking) |
| 36 (SYSTEM) | %windir%\System32 |
System binaries |
| 46 (COMMON_APPDATA) | %ProgramData%\… |
Machine-wide app data |
The hard-coded path list also contains hard-coded JSON entry \AppData\ (substring), which means any directory path containing the substring "\AppData\" is skipped — a wider exclusion than just %AppData%. Likewise \Google\Chrome\ excludes Chrome user data.
Excluded extensions
| Ext | Notes |
|---|---|
.dll |
exe stability |
.sys |
kernel drivers |
.readtext* |
non-existent extension — unclear intent (possibly placeholder for .readme.* family the dev forgot to finalise) |
.readtext95 |
same as above |
The .readtext* glob is never used — comparison is exact-string, not glob. So in practice only .dll and .sys are skipped.
Excluded files
None hard-coded. The ransom note WHATS_HAPPEND.txt is not in any excluded list — it would be re-encrypted by a second pass if the binary were re-run, but the encrypted file would have extension .BAVACAI, breaking the substring check on note name. The 4 bytes of effort it would have taken to skip the note are absent.
Linux/NAS leftovers — startPathes (sic)
"/volume/", "/vmfs/", "/share/", "/volumeusb/",
"/volume0/", "/volume1", "/volume2", "/volumeUSB1",
"/volume3", "/volume4", "/volume5", "/volume6", "/shares"
These 13 entries are completely dead in this Windows binary. The string "startPathes" is absent from .rdata — neither IDA's strings analysis nor strings(1) over the raw image returns a match. The Windows binary therefore cannot read this JSON field; the entries are pure config bloat inherited from a Linux/ESXi/Synology variant (notable /vmfs/ for ESXi, /volumeusb/ and /volume[0-6]/ for Synology DSM, /shares for QNAP). The presence of these paths in the JSON template strongly suggests an existing or planned non-Windows counterpart in the same family.
7. Recovery Inhibition
All recovery-inhibition is shell-spawned via cmd.exe /c <cmd> (not in-process). The 7 commands embedded in preRunCommands (config-defined, not hard-coded in .text):
| Command | Purpose | Notes |
|---|---|---|
vssadmin.exe Delete Shadows /All /Quiet |
Wipe all shadow copies | Standard vector |
wbadmin delete backup -keepVersion:0 -quiet |
Delete Windows Backup catalog | |
wbadmin DELETE SYSTEMSTATEBACKUP |
Delete system state backup | |
wbadmin DELETE SYSTEMSTABACKUP -deleteOldest |
Typo (SYSTEMSTABACKUP ≠ SYSTEMSTATEBACKUP) — silent failure |
Inert command |
wmic.exe SHADOWCOPY /nointeractive |
Implicit (with delete action absent) → does nothing destructive in this exact form |
Likely intended wmic.exe shadowcopy delete /nointeractive — typo, inert |
bcdedit.exe /set {default} recoverynabled No |
Typo (recoverynabled instead of recoveryenabled) — bcdedit error, no change applied |
Inert command |
bcdedit.exe /set {default} bootstatuspolicy ignoreallfailures |
Disable Windows Recovery Environment auto-launch | Effective |
Two of the seven recovery-inhibition commands silently fail due to typos (SYSTEMSTABACKUP, recoverynabled); a third (wmic.exe SHADOWCOPY /nointeractive without delete) is essentially a no-op listing. Net effect: VSS deletion and bcdedit boot-status policy do take effect; system-state backup deletion and bcdedit recovery-enable do not.
8. Targeted Services (10 explicit net stop)
MSSQLServerADHelper100 MSSQL$ISARS MSSQL$MSFW
SQLAgent$ISARS SQLAgent$MSFW SQLBrowser
REportServer$ISARS SQLWriter
Plus taskkill -f -im of equivalent service binaries. Service stops cover the MSSQL Server product family (Forefront/ISA-Reporting "ISARS" and "MSFW" are the historical instance names of Microsoft Forefront Threat Management Gateway / ISA Server) — i.e. the malware targets very specific legacy MSSQL deployments.
There is no OpenSCManager/EnumServicesStatus/StopService in-process — all stops go via cmd.exe /c net stop <svc>. There is no service-name pattern matching; the list is fixed.
9. Targeted Processes (13 taskkill -f -im)
sqlbrowser.exe sql writer.exe (typo - should be sqlwriter.exe)
sqlserv.exe msmdsrv.exe
MsDtsSrvr.exe sqlceip.exe
fdlauncher.exe Ssms.exe
SQLAGENT.EXE fdhost.exe
ReportingServicesService.exe
msftesql.exe pg_ctl.exe
"taskkill -f -impostgres.exe" (missing space — never executes)
Two entries are broken:
sql writer.exe— process name with embedded space; without quotestaskkillparses this as two arguments, failstaskkill -f -impostgres.exe—-imjoined withpostgres.exe;taskkillrejects the malformed switch
So PostgreSQL (postgres.exe) and SQLWriter (sqlwriter.exe) effectively survive preRunCommands. The intent was clearly to cover both Microsoft SQL Server, SSIS/SSRS/SSMS, MS Search, and PostgreSQL — but the kill list is buggy in this build.
10. Persistence & Evasion
Operator-side persistence (registry — victim side)
| Hive | Subkey | Value | Type | Content |
|---|---|---|---|---|
| HKLM | SOFTWARE\PAIDMEMES |
PUBLIC |
REG_BINARY | base64(victim PUBLICKEYBLOB) |
| HKLM | SOFTWARE\PAIDMEMES |
PRIVATE |
REG_BINARY | base64(RSA(masterPubkey, victim PRIVATEKEYBLOB)) — 1280 B |
Only these two values are written. There is no SETTINGS registry value — the wide string "SETTINGS" at 0x14009C6E0 is referenced exactly once, at 0x1400050CF, where it is passed as the lpType parameter to FindResourceW(101, "SETTINGS") to load the ChaCha20-encrypted JSON config from the PE resource directory. Runtime configuration comes exclusively from the PE resource; no registry-based override path exists.
Util_LoadKeyEntry_PUBLIC_PRIVATE (0x140004D30) reads PUBLIC and PRIVATE on every run; if both decode and RSA-import successfully, fresh keys are not generated. With regenerateKeysAlways=false (default), this means a single victim machine is encrypted with the same victim keypair across multiple runs of the binary.
The PAIDMEMES registry key name is a deliberate edgy/troll handle — not an MSSQL vendor or product key.
Parent PID Spoofing (T1134.004)
Util_RunProcess at 0x14003AD60 is the malware's only PPID-spoofing primitive. The sequence:
GetShellWindow()→ handle to the desktop's shell window (typically owned byexplorer.exe)GetWindowThreadProcessId(shellWnd, &pid)→ resolve the explorer PIDOpenProcess(PROCESS_CREATE_PROCESS = 0x80, FALSE, pid)→ handle onexplorer.exe(no read/query rights — only the right needed forUpdateProcThreadAttribute)InitializeProcThreadAttributeList(NULL, 1, 0, &size)to size the attribute list, then a real-allocated init passUpdateProcThreadAttribute(attrList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS = 0x20000, &hExplorer, sizeof(HANDLE), NULL, NULL)— declaresexplorer.exeas the parent of the to-be-spawned processCreateProcessW(NULL, lpCommandLine, ..., dwCreationFlags = 0x80010 (EXTENDED_STARTUPINFO_PRESENT | CREATE_DEFAULT_ERROR_MODE | CREATE_NO_WINDOW), ..., &startupInfoEx, &pi)with the extendedSTARTUPINFOEX.lpAttributeList
The single call site in main is at 0x140020D5D, and the command line built immediately before is <own_path> -network -skip_misc. The admin process therefore self-relaunches with explorer.exe recorded as parent so that the network-drive child is not visibly a descendant of the admin malware in EDR/Sysmon process trees.
WoW64 redirection hop in preRunCommands execution
Util_RunProcessElevated at 0x140030530 (which runs every entry of preRunCommands and postRunCommands and also the hotkey-thread's explorer restart) wraps each command line as:
<windir>\SysWOW64\cmd.exe /c %windir%\sysnative\cmd.exe /c <command>
This is a 64 → 32 → 64 process hop via the WoW64 redirection alias sysnative. The 32-bit cmd.exe (in SysWOW64\) starts as a WoW64 process; from a WoW64 process, the path %windir%\sysnative\ resolves to %windir%\System32\ (i.e. the 64-bit binaries) and re-spawns the native 64-bit cmd.exe to execute the actual command. Net effect for monitoring: the parent-child chain becomes bavacai.exe → SysWOW64\cmd.exe (WoW64) → System32\cmd.exe → <recovery_command>, breaking detection rules that match <malware> → cmd.exe → vssadmin/taskkill directly. This is not PPID spoofing — it adds extra real cmd parents into the chain rather than rewriting the recorded parent.
Console hide
hideConsole=true (default in this config) → after App_Run returns, ShowWindow(GetConsoleWindow(), SW_HIDE) removes the window from the user. This is not an analysis-evasion technique — Console-Mode hide does not prevent process or kernel-mode introspection — but it serves the operator UX (silent execution).
Hotkey monitor — Thread_HotkeyMonitor at 0x14001EB90
A background thread polls (no hook) every iteration:
- VK_INSERT (0x2D) pressed → toggle visibility of every window owned by the current PID via
EnumWindows+ShowWindow(SW_HIDE/SW_SHOWNORMAL). Used by the operator to peek at the encryption progress on a hidden console. - VK_HOME (0x24) pressed AND console window is foreground → spawn
taskkill /f /im explorer.exethenstart explorer.exe(viaUtil_RunProcessElevated, no PPID spoof). Forces a desktop refresh — used to make a wallpaper change visible immediately. Only triggers when the operator is sitting at the console.
These are operator UX features, not anti-analysis. They do not disable when a debugger is attached.
Anti-debug / anti-VM / anti-sandbox
None. The only IsDebuggerPresent calls are in MSVC CRT crash handlers (__scrt_fastfail, __acrt_call_reportfault) and serve the standard purpose of routing fatal-error reporting; they do not gate the encryption logic. There is no RDTSC timing check, no CPUID hypervisor-bit check, no NtQueryInformationProcess(ProcessDebugPort), no NtQuerySystemInformation(SystemKernelDebuggerInformation), no GetTickCount-delta check, no WMI Win32_BIOS / Win32_ComputerSystem VM-vendor lookup, no MAC-prefix VM check, no installed-program enumeration.
String obfuscation
None. All strings are stored either:
- In plaintext in
.rdata(the implementation strings: error messages, JSON parse keys, registry value names, the markersTRUMPTRUMP…andPUTLERPUTLERthemselves) - In the ChaCha20-encrypted PE resource id 101 of type
SETTINGS(the IOC-bearing strings: onion URL, email, Tox ID, ransom note text, master public key)
The encryption of the resource is functionally string obfuscation but trivially reversed — the ChaCha20 key (TRUMPTRUMP…) and nonce (PUTLERPUTLER) are themselves plaintext strings in .rdata, passed by reference to ChaCha20::Init at 0x14000511D.
API hashing / dynamic resolution
None. Imports are static; no LoadLibrary/GetProcAddress walks; no PEB.Ldr traversal; no FNV/CRC32/SDBM hash table.
Anti-Analysis Summary
| Technique | Unprotect / MITRE | Address | Description |
|---|---|---|---|
| File-content encryption | U0301 / T1486 | 0x14001A2A0 |
EncryptFile orchestration |
| Cryptography (RSA + ChaCha20) | U0703 / E1027.m04 | 0x14003D210, 0x140002E90 |
RSA-2048 wrap + ChaCha20 stream |
| Encrypted PE resource (config) | T1027.013 | 0x14003AA40, 0x140005080 |
Utils::GetResource + ChaCha20 over the SETTINGS resource |
| VSS Deletion | U0305 / T1070.004 | preRunCommands shell-out |
vssadmin Delete Shadows /All /Quiet |
| Kill Process | U0403 / T1057 | preRunCommands shell-out |
13 taskkill -f -im |
| Restart Manager file unlock | T1490 | 0x140030F70 |
RmStartSession + RmShutdown |
| Parent PID Spoofing | T1134.004 | 0x14003AD60 (Util_RunProcess) |
UpdateProcThreadAttribute(PROC_THREAD_ATTRIBUTE_PARENT_PROCESS) with explorer.exe for the network-drive self-relaunch |
| WoW64 sysnative hop | (no MITRE entry) | 0x140030530 (Util_RunProcessElevated) |
64→32→64 cmd chain to break parent-child detection rules |
| Hidden Window | T1564.003 | App_Run post-init |
ShowWindow(GetConsoleWindow(), SW_HIDE) when hideConsole=true |
There is no string obfuscation, no API hashing, no runtime anti-debug, no anti-VM, no sandbox evasion. The only proper obfuscation is the single ChaCha20 layer over the PE config resource, defeated by the plaintext key two .rdata entries away.
11. Command-Line Arguments
| Argument | Effect |
|---|---|
-help |
Print usage block and exit (lists -skip_misc and -network) |
-network |
Encrypt only mapped network drives (via WNetGetConnectionW) — skips local volumes |
-skip_misc |
Skip preRunCommands, postRunCommands, recycle-bin emptying and wallpaper change |
Anything else triggers Unknown argument: <arg> log line then a normal run.
12. Static Imports Summary
| Category | Key APIs |
|---|---|
| Crypto (ADVAPI32) | CryptAcquireContextW, CryptGenKey, CryptImportKey, CryptExportKey, CryptDuplicateKey, CryptDestroyKey, CryptEncrypt, CryptGetKeyParam, CryptGenRandom, CryptReleaseContext |
| Crypto (CRYPT32) | CryptBinaryToStringA/W, CryptStringToBinaryA/W (base64) |
| File I/O | CreateFileA/W, WriteFile, ReadFile, MoveFileW, DeleteFileW, SetFileAttributesW, SetEndOfFile, SetFilePointerEx, GetFileSizeEx, FindFirstFileW, FindNextFileW, FindClose |
| Drive enum | GetLogicalDrives, GetDriveTypeW, FindFirstVolumeW, FindNextVolumeW, GetVolumePathNamesForVolumeNameW, QueryDosDeviceW, SetVolumeMountPointW, WNetGetConnectionW |
| Process | CreateProcessW, OpenProcess, TerminateProcess, CreatePipe, WaitForSingleObject, WaitForSingleObjectEx, GetExitCodeProcess, InitializeProcThreadAttributeList, UpdateProcThreadAttribute, AssignProcessToJobObject |
| Token / privilege | OpenProcessToken, GetTokenInformation |
| Threading | CreateThread, ExitThread, AcquireSRWLockExclusive/Release/Try, SleepConditionVariableSRW, WakeAllConditionVariable, InitializeCriticalSectionEx, Tls* |
| Registry | RegCreateKeyExW, RegSetValueExW, RegGetValueA/W, RegCloseKey |
| Network (HTTP only) | URLDownloadToFileW (urlmon) — api.ipify.org only |
| Restart Manager | RmStartSession, RmRegisterResources, RmGetList, RmShutdown, RmEndSession |
| GDI / wallpaper | CreateCompatibleDC, CreateCompatibleBitmap, CreateFontW, TextOutA, GetBitmapBits, SystemParametersInfoW |
| Console / window | ShowWindow, GetConsoleWindow, GetForegroundWindow, EnumWindows, IsWindowVisible, GetAsyncKeyState |
| Recycle bin | SHEmptyRecycleBinW |
| Resources | FindResourceW, LoadResource, LockResource, SizeofResource, GetModuleHandleW |
| Path | PathFileExistsW, PathFindExtensionW, PathIsDirectoryW, PathIsNetworkPathW, PathIsUNCW, StrStrIW |
13. IDA Analysis — Renamed Functions
| Address | Name | Size | Description |
|---|---|---|---|
0x14001F890 |
main |
0x2C44 | Argv parse, App_Run, drive iter, encryption orchestration |
0x1400081D0 |
App_Run |
0x830 | Runtime skipPath augmentation, settings load, key init, PCInfo |
0x140005080 |
Context__LoadSettings |
0x204C | Decrypt SETTINGS resource, parse JSON, populate globals |
0x140007940 |
Context__InitializeKeys |
0x88E | Load registry blobs or generate fresh victim RSA keypair |
0x1400070E0 |
Context__InitializePCInfo |
0x800 | Build victim ID, substitute [IDENTIFIER] in note template |
0x14003AA40 |
Utils__GetResource |
0x290 | FindResourceW(101) → LoadResource → vector |
0x140002E90 |
ChaCha20__InitState |
0x190 | DJB-layout state init (key 32B, nonce 8B, sigma) |
0x1400035A0 |
ChaCha20__Init |
0x30 | Wrapper setting state[16]=64 + state[6]=0 |
0x140003390 |
(block-fn) | inferred | 20-round ChaCha20 block computation (column+diag rounds × 10) |
0x14003C7B0 |
Crypto__GenerateBytes |
0xD0 | CryptAcquireContextW + CryptGenRandom(40) |
0x14003C890 |
Crypto__RSA__GenerateKeys |
0x410 | CryptGenKey(CALG_RSA_KEYX,2048) + CryptExportKey ×2 |
0x14003CCA0 |
Crypto__RSA__ctor |
0xA0 | RSA context constructor |
0x14003CD40 |
Crypto__RSA__InitializeKeys |
0x2D0 | CryptImportKey priv & pub from blobs |
0x14003D010 |
Crypto__RSA__Duplicate |
0x140 | CryptDuplicateKey for key copy |
0x14003D150 |
Crypto__RSA__Cleanup |
0xC0 | CryptDestroyKey ×2 + CryptReleaseContext |
0x14003D210 |
Crypto__RSA__Encrypt |
0xBD0 | Multi-block RSA-2048 PKCS#1 encrypt |
0x14001A2A0 |
EncryptFile |
0x773 | Per-file orchestration |
0x14003DDE0 |
Crypto__CryptFile |
0x220 | Range A + (optional) Range B selection |
0x14003DC60 |
ChaCha20__XorBlocks_Skip |
0x180 | Encrypt-N / skip-N inner loop |
0x14001AA20 |
DirWalker |
0x773 | StrStrIW skip + FindFirstFileW recursion |
0x140030A80 |
Drive_EnumLogical |
0x410 | GetLogicalDrives + mount unmounted volumes |
0x140039F30 |
Drive_EnumCipher |
0x230 | Drive iter for chiperDrives=true |
0x14003A160 |
Drive_EnumNetworkConnections |
0x9C0 | WNetGetConnectionW mapped drives (-network) |
0x140030F70 |
Util_RestartManagerUnlock |
0x2C0 | RmStartSession+RmRegisterResources+RmShutdown |
0x140030530 |
Util_RunProcessElevated |
0x360 | Spawn elevated cmd.exe child |
0x14003AD60 |
Util_RunProcess |
0x3A0 | Generic CreateProcessW wrapper |
0x14003B440 |
Util_RunProcess_Inner |
0x190 | Inner CreateProcessW |
0x140030890 |
Util_EmptyRecycleBin |
0x1F0 | SHEmptyRecycleBinW per drive |
0x140039C50 |
Util_SetWallpaper |
0x2E0 | SystemParametersInfoW(SPI_SETDESKWALLPAPER) |
0x140033040 |
Util_GetPCInfo |
0x150 | Compose hostname + IP for victim ID |
0x140032060 |
Util_GetExternalIP_ipify |
0x5B0 | URLDownloadToFileW api.ipify.org |
0x140004A00 |
Util_SaveKeysToRegistry |
0x320 | RegSetValueExW PAIDMEMES.{PUBLIC,PRIVATE} |
0x140003E80 |
Util_LoadKeysFromRegistry |
0xA0 | RegGetValueW |
0x140004D30 |
Util_LoadKeyEntry_PUBLIC_PRIVATE |
0x2D0 | Helper that fetches both PUBLIC and PRIVATE values |
0x14001EB90 |
Thread_HotkeyMonitor |
0xC50 | VK_INSERT/VK_HOME polling loop |
14. Indicators of Compromise (IOCs)
Hashes
| Type | Value |
|---|---|
| SHA-256 | 86b4d075d5bd0c49cbb21fd43935789b6612a2165273cc158dd0607b68941d04 |
| MD5 | 7ae9c8b779ce3114199f4fd6335f681d |
Network
| Type | Value |
|---|---|
| Tor file server | t33zoj4qwv455fog7qnb2azi5xcdxkixughmmduzbw2rtdgryqfbh6id.onion |
nhuvgh@outlook.com |
|
| Tox (qtox) ID | 7C564920870C0D33535D2012ECDDE389FE25BAF7AF427DD584EE39C04AF8CF024F8BFA93D8DB |
| Outbound HTTP | https://api.ipify.org (victim external IP discovery) |
Files
| Indicator | Value |
|---|---|
| Ransom note filename | WHATS_HAPPEND.txt (typo "HAPPEND") |
| Encrypted extension | .BAVACAI (uppercase, appended) |
| Footer signature (last 8 bytes) | 06 00 00 00 00 00 00 00 (constant per file) |
| Footer total size | 1544 bytes (1280 + 256 + 8) |
| Wallpaper artefact | none in this build (backgroundImage=false) |
| External-IP scratch file | xxxxxxxxx.crypt (literal 11 chars + .crypt — 11-char placeholder, see below) |
Registry
| Key | Description |
|---|---|
HKLM\SOFTWARE\PAIDMEMES |
Root for victim key persistence |
HKLM\SOFTWARE\PAIDMEMES\PUBLIC |
Victim PUBLICKEYBLOB (REG_BINARY base64) |
HKLM\SOFTWARE\PAIDMEMES\PRIVATE |
RSA(masterPubkey, victim PRIVATEKEYBLOB) — 1280 B base64 |
Behavioural
- Spawns 30 child
cmd.exeprocesses within seconds of launch (all from the staticpreRunCommandslist) - 13×
taskkill -f -imof MSSQL/MSDTS/SSMS/PostgreSQL processes - 7×
net stopof MSSQL service variants (instancesISARS,MSFW) - 1×
vssadmin Delete Shadows /All /Quiet, 3×wbadmin delete, 1×wmic.exe SHADOWCOPY, 2×bcdedit.exe /set {default}— note 3 of these silently fail (typos) RegCreateKeyExW HKLM\SOFTWARE\PAIDMEMES, thenRegSetValueExWof PUBLIC and PRIVATE blobs- File handles released via
RmStartSessionfor any locked target file SHEmptyRecycleBinWon every drive at end of run- In-place encryption only — no shadow copy of original;
MoveFileWrename to<orig>.BAVACAI - 32 simultaneous encryption threads (
threadPoolMaxThreadsdefault) - Outbound HTTPS to
api.ipify.orgexactly once per run
Distinctive Strings (.rdata, plaintext)
"TRUMPTRUMPTRUMPTRUMPTRUMPTRUMPTRUMP"(35 chars — ChaCha20 key string)"PUTLERPUTLER"(12 chars — ChaCha20 nonce string)"SOFTWARE\\PAIDMEMES"(registry root)"[IDENTIFIER]"(note placeholder)"--------------------------------"(32 dashes — log separator)"J:\\edu\\niggerProject\\bin\\Publish\\locker_win_x64_encrypter.pdb"(PDB path)"Crypto::RSA::GenerateKeys","Crypto::RSA::Encrypt","Context::InitializeKeys","EncryptFile","CryptFile"(verbose log function names)"chiperDrives"(typo "cipher")"skipPathes"(typo "Paths")
Distinctive Strings (encrypted JSON config — requirementsFileDataUTF8)
"DON'T PANIC!!! YOUR FILES ARE PERFECT AND SAFE!"(note opening line)"WHATS_HAPPEND.txt"(note filename)"qtox - 7C564920870C0D33535D2012ECDDE389FE25BAF7AF427DD584EE39C04AF8CF024F8BFA93D8DB"(full Tox ID)
15. MITRE ATT&CK Mapping
| ID | Technique | Implementation |
|---|---|---|
| T1486 | Data Encrypted for Impact | RSA-2048 wrap + ChaCha20 per-file in EncryptFile |
| T1490 | Inhibit System Recovery | vssadmin Delete Shadows, wbadmin delete backup, wmic.exe shadowcopy, bcdedit /set {default} bootstatuspolicy ignoreallfailures, Restart Manager file unlock, recycle-bin emptying |
| T1489 | Service Stop | net stop of 7 MSSQL service variants from preRunCommands |
| T1057 | Process Discovery | implicit via taskkill -f -im (no enumeration code, fixed list) |
| T1083 | File and Directory Discovery | FindFirstFileW/FindNextFileW recursion in DirWalker |
| T1135 | Network Share Discovery | WNetGetConnectionW for mapped drives in non-elevated branch |
| T1134.004 | Access Token Manipulation: Parent PID Spoofing | Util_RunProcess (0x14003AD60) sets explorer.exe as parent for the self-relaunched <own_path> -network -skip_misc child |
| T1082 | System Information Discovery | Util_GetPCInfo: hostname, DNS domain, CPU model, total RAM, physical disk count |
| T1016 | System Network Configuration Discovery | URLDownloadToFileW("https://api.ipify.org") for external IPv4 |
| T1071.001 | Application Layer Protocol: Web Protocols | single GET to api.ipify.org (no C2 channel) |
| T1112 | Modify Registry | RegCreateKeyExW HKLM\SOFTWARE\PAIDMEMES + RegSetValueExW for victim key persistence |
| T1480.001 | Execution Guardrails: Environmental Keying | hostname, domain and external IP enter the victim-ID derivation |
| T1027.013 | Encrypted/Encoded Files: Encrypted Resources | ChaCha20 over PE resource id 101 (type SETTINGS) holding the JSON config |
| T1485 | Data Destruction | SHEmptyRecycleBinW per drive at end of run |
| T1564.003 | Hide Artifacts: Hidden Window | ShowWindow(GetConsoleWindow(), SW_HIDE) when hideConsole=true |
| T1059.003 | Command and Scripting Interpreter: Windows Cmd | All recovery-inhibition + kills go through cmd.exe /c <cmd> (with the WoW64 sysnative hop in Util_RunProcessElevated) |
16. Summary
BAVACAI is a single-stage Windows x64 ransomware distributed as a self-contained PE32+ console executable (~735 KB). Encryption combines RSA-2048 (Microsoft legacy CryptoAPI, PKCS#1 v1.5, e=65537) for key wrapping and a custom ChaCha20 implementation (DJB original 8-byte nonce variant, no Poly1305) for bulk file content. The cipher key for every encrypted file is freshly generated from CryptGenRandom, wrapped with the victim's per-machine RSA-2048 public key, and appended along with the master-pub-wrapped victim PRIVATEKEYBLOB into a 1544-byte footer — yielding a self-contained recovery envelope per file that requires only the master private key.
The binary's operator surface is rich: 30 pre-run shell commands, 13 process kills + 7 service stops, multi-vector recovery inhibition (vssadmin / wbadmin / wmic / bcdedit), Restart Manager file-handle release, recycle-bin emptying, optional wallpaper change, optional console hide, mount-unmounted-volume support, mapped network drive enumeration, IPv4 external IP fingerprinting via api.ipify.org, and a 32-thread producer-consumer pool prioritising .sql / .bak / .VHDX. Configuration is read from a single ChaCha20-encrypted PE resource keyed by the plaintext strings "TRUMPTRUMPTRUMPTRUMPTRUMPTRUMPTRUMP" / "PUTLERPUTLER" — trivially decoded.
Cryptographic engineering is mostly correct but unauthenticated (no MAC over file content, vulnerable to bit-flip on the encrypted stream, but in practice unrecoverable for the victim because the per-file key is unrecoverable without the master). The engineering quality is otherwise amateur: the PDB path includes a slur as folder name, JSON keys contain typos (skipPathes, chiperDrives), the recovery-inhibition shell list contains 2 typos that silently fail, the kill list contains 2 malformed entries that never run, and the verbose error logs leak full class::method names (Crypto::RSA::Encrypt, Context::InitializeKeys, …) that map directly back to the source layout.
Operational sophistication is moderate: the kill-list is curated for MSSQL/MSDTS/SSMS environments specifically (instance names ISARS, MSFW are historical Forefront/ISA Server), the threadPoolPriorityExtensions includes .VHDX (Hyper-V) and .bak (SQL Server backups), and Restart Manager is correctly used to recover locked database files — these are deliberate choices targeting Windows Server / Hyper-V / SQL Server estates. Network-share encryption is opt-in via -network; lateral movement, AD enumeration, PowerShell invocation, RDP enumeration, scheduled-task creation, service install — none are present. There is no exfiltration component, despite the "72 hours if you don't contact us" leak claim in the note.
The infrastructure is single-handle (one Tor file server, one outlook.com mailbox, one Tox ID), the per-victim ID is reproducible-by-hostname (no random salt), and the binary contains no anti-debug, no anti-VM, no anti-sandbox, no string obfuscation, no API hashing, no packing — making this an inventory-friendly low-effort variant suitable for IR triage but with no in-place recovery option for the victim: every file's key is wrapped exclusively against the master key.
Appendix A — Master RSA-2048 Public Key (PEM)
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0dtZGZGXTx1FWagCvFRC
Hr616k+7WrBT9CYCbyUVGSvuJ2Oe1pgW/U2N4LTmWxdYlZVFXEdRdKChfBC9biu5
meLxps2zpdsvV/9IAKUYIfSvSE3UBVaw7EUaYAS1ykj6o+FLTwEp+jqro001P7vn
GoiMIADie/v1oibQk1AQjKByEMmaZqGjD7wfpVeV/wdcLhPt+r6TGz7IldayOG6o
3IM52DFtjihWBL3g0iJYHPZW0CHb23fTz+EEh8JHshuH1G4lEvmZBTfaQjpFpCHo
2Rm2meNODI968tHKJ6ZMzGx0SUaVvXmuKckbCUg7Ve3TjvdMT/3NEFEeDJ9WBbn4
yQIDAQAB
-----END PUBLIC KEY-----
Modulus (BE): d1db59 19 91 97 4f 1d 45 59 a8 02 bc 54 42 1e be b5 ea 4f bb 5a b0 53 f4 26 02 6f 25 15 19 2b ee 27 … (full 256 B in bavacai_rsa_pubkey.der).
Appendix B — Wallpaper rendering (build-disabled)
Although backgroundImage=false in this build, the code path at 0x140039C50 (Util_SetWallpaper) and 0x140039530 (rendering helper) is fully present and would activate on a build that flips the flag. The output is a fixed 1280×1024 BMP:
| Property | Value |
|---|---|
| Background | Solid black (CreateSolidBrush(0) + FillRect) |
| Font | Tahoma (L"Tahoma"), height 50, weight 700 (bold), OUT_TT_PRECIS=4 |
| Text colour | White (SetTextColor(0xFFFFFF)) |
| Text background | Transparent (SetBkMode(TRANSPARENT=1)) |
| Text source | requirementsFileDataUTF8 from JSON config, split by '\n' into lines |
| Layout | Each line centered horizontally (1280 − text_width) / 2, vertical block centered with a 60-pixel line height |
| Output format | BMP (BITMAPFILEHEADER 14 B + BITMAPINFOHEADER 40 B + 32-bit RGB raster from GetBitmapBits) |
| Application | SystemParametersInfoW(SPI_SETDESKWALLPAPER=0x14, 0, <bmp_path>, SPIF_UPDATEINIFILE \| SPIF_SENDWININICHANGE=3) |
The output BMP path is a global initialised in App_Run (pvParam / xmmword_1400B0DE0).