Chapter 24

Side-by-Side Assemblies

Manifest-based DLL versioning, the WinSxS store, activation contexts, and how SxS lookup order creates a DLL redirection vector used in targeted intrusions

Scenario

An attacker drops a malicious comctl32.dll into the same directory as a legitimate application. The application has a manifest requesting version 6 of the common controls library. The SxS system resolves version 6 from the WinSxS store and loads it, not the malicious file. But if the application lacks a manifest, or if the attacker creates a local directory assembly with a matching manifest, the redirection changes. Understanding which DLL wins requires understanding how activation contexts resolve assembly references.

The DLL Hell Problem

Before SxS, installing an application that shipped its own version of a system DLL (e.g., comctl32.dll) could overwrite the system copy, breaking other applications that depended on the previous version. This "DLL Hell" problem accumulated over years of Windows 9x/NT installs. Windows XP introduced Side-by-Side assemblies to solve it: multiple versions of the same DLL coexist on disk, and each application gets the version it was built against.

Application Manifests

An application manifest is an XML file that describes what assemblies an application needs. It can be embedded as the RT_MANIFEST resource (resource ID 1 for the application, ID 2 for DLLs) in the PE file, or provided as a separate appname.exe.manifest file alongside the executable.

<!-- A minimal application manifest requesting comctl32 v6 -->
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <assemblyIdentity
      version="1.0.0.0"
      processorArchitecture="amd64"
      name="MyApp"
      type="win32"/>
  <!-- Request common controls version 6 (visual styles, themed controls) -->
  <dependency>
    <dependentAssembly>
      <assemblyIdentity
          type="win32"
          name="Microsoft.Windows.Common-Controls"
          version="6.0.0.0"
          processorArchitecture="amd64"
          publicKeyToken="6595b64144ccf1df"
          language="*"/>
    </dependentAssembly>
  </dependency>
  <!-- Request execution level -->
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
    <security>
      <requestedPrivileges>
        <requestedExecutionLevel level="asInvoker" uiAccess="false"/>
      </requestedPrivileges>
    </security>
  </trustInfo>
</assembly>

The WinSxS Store

C:\Windows\WinSxS contains versioned assembly directories. Each subdirectory name encodes the assembly identity: architecture, name, version, public key token, and language. For example:

C:\Windows\WinSxS\
  amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.19041.1202_none_e4b4ccf8c3dea5a0\
    comctl32.dll    ← version 6.0.19041.1202 for x64
  x86_microsoft.windows.common-controls_6595b64144ccf1df_6.0.19041.1202_none_...\
    comctl32.dll    ← same version for x86
  amd64_microsoft.windows.common-controls_6595b64144ccf1df_5.82.19041.1202_none_...\
    comctl32.dll    ← version 5.82 for backward compat
WinSxS size

WinSxS can reach 10–30 GB on a fully patched Windows 11 install. It isn't redundant: Windows uses hard links to share unchanged files across versions. The actual disk usage is much lower than the apparent directory size. The DISM /Online /Cleanup-Image /StartComponentCleanup command removes superseded components after Windows Updates.

Activation Contexts

The activation context (ACTCTX) is the runtime mechanism that maps assembly names to their concrete directories. The loader creates an activation context for the process at startup by parsing the application manifest, and pushes/pops contexts as the application loads DLLs with their own manifests.

At process startup:
  1. Loader reads application manifest (RT_MANIFEST or .manifest file)
  2. Creates activation context (ActivateActCtx)
  3. For each referenced assembly, locates directory in WinSxS
  4. When LoadLibrary("comctl32.dll") is called:
     a. Check active activation context for a redirect
     b. If redirect found → load from WinSxS directory (BEFORE search order)
     c. If no redirect → use normal DLL search order

Activation context override sequence (higher wins):
  [process manifest] > [DLL manifest] > [SxS store] > [normal search order]

SxS Abuse: DLL Redirection

Attackers abuse SxS by creating a local assembly — a directory named like a WinSxS assembly alongside the target executable:

C:\TargetApp\
  TargetApp.exe
  TargetApp.exe.manifest    ← drop this to control assembly resolution
  Microsoft.Windows.Common-Controls\
    comctl32.dll            ← malicious DLL; loaded instead of WinSxS version
    Microsoft.Windows.Common-Controls.manifest  ← makes this a valid local assembly

For this to work, the application manifest must reference the assembly and the attacker must be able to write the local assembly directory. This technique was used in early APT attacks against Windows XP systems; modern systems with WDAC/AppLocker policies restrict it, but misconfigured or legacy applications remain vulnerable.

AttackWhat the attacker dropsOutcome
SxS local assembly Local assembly directory + manifest alongside .exe Malicious DLL loaded instead of WinSxS version
Manifest injection Crafted .manifest file alongside an app that has no manifest Introduces assembly references that redirect future DLL loads
WinSxS component tampering Replace file in WinSxS directory (requires admin / SYSTEM) All apps using that version affected; very high privilege

Detection

# Detect local SxS assemblies (directories that look like assembly identities)
# alongside executables in non-system directories
import os, re

ASSEMBLY_NAME_PATTERN = re.compile(
    r'^(?:x86|amd64|wow64)_[\w.\-]+_[0-9a-f]{16}_[\d.]+_'
    r'(?:none|[a-z]{2}(?:-[A-Z]{2})?)_[0-9a-f]+$',
    re.IGNORECASE
)

def find_local_assemblies(root: str):
    for dirpath, dirnames, filenames in os.walk(root):
        if os.path.abspath(dirpath).lower().startswith(
                r"c:\windows\winsxs"):
            dirnames.clear()
            continue
        for d in dirnames:
            if ASSEMBLY_NAME_PATTERN.match(d):
                full = os.path.join(dirpath, d)
                print(f"[!] Local assembly outside WinSxS: {full}")
                for f in os.listdir(full):
                    print(f"      {f}")

Q & A

What happens when a .NET application is run — does SxS apply?

Yes and no. .NET assemblies have their own assembly resolution system (the GAC — Global Assembly Cache, and the Runtime assembly binder). They don't use the WinSxS / Win32 SxS mechanism for .NET-to-.NET resolution. However, .NET processes still use Win32 SxS for their unmanaged DLL dependencies: the CLR host (e.g., mscoree.dll, clr.dll) and any native interop DLLs are resolved via the standard Win32 SxS mechanism. The application manifest embedded in a .NET executable still controls the Win32 activation context for those native DLLs. For .NET-to-.NET assembly resolution: .NET 1.x/2.x used binding redirects in app.config + the GAC; .NET Core / .NET 5+ use a completely different runtime-host based resolution without the GAC. In security contexts, .NET has its own DLL hijacking vector: the AssemblyResolve event, which allows an attacker (or malware) to inject a custom assembly resolver into the CLR that redirects any assembly load to a malicious path.

How does WDAC interact with SxS assembly loading to prevent DLL redirection?

Windows Defender Application Control (WDAC) enforces code integrity at the kernel level before any user-mode loader code runs. When the loader attempts to load a DLL — whether from WinSxS, the application directory, or a local SxS assembly — the kernel calls CI.dll to verify the PE's Authenticode signature and check it against the WDAC policy. If the loaded DLL doesn't match an allowed signer or file rule, the load fails with STATUS_INVALID_IMAGE_HASH. This means: (1) A local SxS assembly with a malicious unsigned DLL fails even if the SxS manifest resolution correctly redirects to it — CI rejects the unsigned binary. (2) The WDAC policy can also whitelist only DLLs from specific known signers, preventing any non-Microsoft DLL from loading into protected processes. (3) WDAC operates independently of the SxS/manifest system; it's a lower-level gate. The combination of Code Integrity + WDAC is the reason that SxS attacks that were viable on Windows XP are much harder on WDAC-protected Windows 11 systems. Organizations that have SxS attacks in their threat model should enforce WDAC or at minimum Authenticode signing requirements via AppLocker.