REPORT
AI & Pentesting Pulse 2026: The 6 things elite security teams do differently to close the risk gap
REPORT
AI & Pentesting Pulse 2026: The 6 things elite security teams do differently to close the risk gap

Part 2 — Decoding Windows CBS Manifests: Building the Decoder

This is Part 2 of our previous blog post, Decoding Windows CBS Manifests: Reversing the DCM/PA30 Delta Format.

The Decode Pipeline

The decoding pipeline has four steps:

  1. Extract resource 0x266 from wcp.dll - this is the delta dictionary
  2. Read the DCM-wrapped manifest and strip the 4-byte DCM\x01 header
  3. Call ApplyDeltaB in UpdateCompression.dll or msdelta.dll with the dictionary as the source and the stripped manifest as the delta
  4. Output: plain XML manifest

The following Python snippet calls ApplyDeltaB via ctypes to decode a single manifest:

import ctypes

import ctypes.wintypes


# DELTA_FLAG_TYPE is a 64-bit flags field (ULONGLONG on x64)

DELTA_FLAG_TYPE = ctypes.c_uint64


class DELTA_INPUT(ctypes.Structure):

# On x64: lpStart(8) + uSize(8) + Editable(4) + padding(4) = 24 bytes

_fields_ = [

("lpStart", ctypes.c_void_p), # pointer to buffer (8 bytes on x64)

("uSize", ctypes.c_size_t), # buffer length (8 bytes on x64)

("Editable", ctypes.c_int), # BOOL = c_int (4 bytes + 4 padding)

]


class DELTA_OUTPUT(ctypes.Structure):

_fields_ = [

("lpStart", ctypes.c_void_p), # allocated by ApplyDeltaB

("uSize", ctypes.c_size_t), # output length

]


def apply_delta_b(dll, source_bytes: bytes, delta_bytes: bytes):

"""

Calls ApplyDeltaB(flags, source, delta, &output).

source_bytes = dictionary (resource 0x266 from wcp.dll)

delta_bytes = manifest payload with DCM header already stripped

dll = loaded msdelta.dll or UpdateCompression.dll handle

"""

# Pin buffers in memory so ctypes pointers remain valid

src_buf = ctypes.create_string_buffer(source_bytes)

dlt_buf = ctypes.create_string_buffer(delta_bytes)


src = DELTA_INPUT()

src.lpStart = ctypes.cast(src_buf, ctypes.c_void_p)

src.uSize = len(source_bytes)

src.Editable = False


dlt = DELTA_INPUT()

dlt.lpStart = ctypes.cast(dlt_buf, ctypes.c_void_p)

dlt.uSize = len(delta_bytes)

dlt.Editable = False


out = DELTA_OUTPUT()


fn = dll.ApplyDeltaB

fn.restype = ctypes.wintypes.BOOL

fn.argtypes = [DELTA_FLAG_TYPE, DELTA_INPUT, DELTA_INPUT,

ctypes.POINTER(DELTA_OUTPUT)]


# flags = 0 means no special options (no DELTA_FLAG_NONE required)

ok = fn(DELTA_FLAG_TYPE(0), src, dlt, ctypes.byref(out))


if not ok:

return False, ctypes.GetLastError(), b""


# Copy output before freeing - DeltaFree releases the internal buffer

result = ctypes.string_at(out.lpStart, out.uSize)

dll.DeltaFree.restype = ctypes.wintypes.BOOL

dll.DeltaFree.argtypes = [ctypes.c_void_p]

dll.DeltaFree(out.lpStart)


return True, 0, result

Here is the output of running the decode script against a real DCM-encoded manifest extracted from the KB5066793 LCU WIM.

The scripts used in this research are available on my GitHub: Win-CBS-Manifest-Decoder

Verification: msdelta.dll vs UpdateCompression.dll

To verify that both DLLs produce the same result, I decoded the same manifest from KB5066793 using msdelta.dll and UpdateCompression.dll. The following output shows both runs:

The two outputs are byte-for-byte identical. Same size (4,027 bytes), same XML content, same hashes. Both DLLs produce the same result for PA30 deltas, despite being different codebases internally.

Two practical consequences follow. First, an offline decoder is not tied to whichever DLL the running servicing stack happens to load. msdelta.dll and UpdateCompression.dll can be used interchangeably for CBS manifests, so a researcher uses whichever is available on the analysis host and switches freely if a future Windows build moves the servicing stack between them. Second, byte-equal output from two unrelated codebases is a free correctness check on the PA30 reading itself: a match between two codebases that share an API but no source is strong evidence the decode is right, not an artefact of one particular library.

So the --dll flag doesn't affect the output. Use whichever DLL you have.

CBS Filenames and Path Handling

CBS component names get truncated by 7-Zip during WIM extraction, producing filenames like:

x86_microsoft-windows-w..tion-wiatwaincompat_31bf3856ad364e35_10.0.22621.3527_none_dea2a2a3b3313ebb.manifest

The “..” in “w..tion” is not directory traversal - it's a literal part of the filename. But Python's pathlib.Path doesn't know that. When you pass a relative path containing this filename, Path.resolve() and os.path.normpath() will collapse the .. as if it means "go up one directory", breaking the path silently.

The fix: resolve the parent directory and the filename separately.

def safe_resolve(raw_path: str) -> Path:

p = Path(raw_path)

parent = p.parent.resolve() # resolve ".." for real directory traversal

return parent / p.name # keep the filename as-is

This is easy to miss, but it will show up immediately when you try to batch-process manifests from an extracted WIM.

Conclusion

The CBS manifest decompression pipeline has three components:

  • wcp.dll orchestrates the decompression
  • UpdateCompression.dll (not msdelta.dll) provides the delta engine
  • PE resource 0x266 embedded in wcp.dll supplies the dictionary

Extracting this dictionary and calling ApplyDeltaB directly is enough to decode any DCM-wrapped manifest. The advantage over wcpex is that ApplyDeltaB is a stable, exported Win32 function present across Windows versions, while wcpex depends on undocumented mangled C++ symbols tied to a specific wcp.dll build.

Along the way, reversing the pipeline revealed two details I did not find covered in existing public resources:

  • On the tested build (10.0.26100.8112), the Windows servicing stack uses UpdateCompression.dll, not msdelta.dll, as its delta engine.
  • UpdateCompression.dll and msdelta.dll are separate implementations of the same API. Both produce identical output for PA30 deltas, but msdelta.dll additionally supports the legacy PA19 format through a distinct code path.

What this enables

With decoded manifests, the patch diffing workflow becomes scriptable: extract manifests from both a patched and vulnerable LCU, decode them to XML, and diff the component versions and file hashes to identify which binaries changed. Instead of diffing gigabytes of binaries, you diff text files and get a short list of what to analyze.

In a follow-up post, we'll apply this pipeline to a real CVE - walking through the full process from downloading two LCU packages to identifying the patched binaries and examining the actual code changes.

AI and Pentesting Pulse Report 2026 CTA

Back to Blog
About Martino Sani
Martino Sani is a cybersecurity professional with 14+ years of experience. His background spans offensive security, application security, and cybersecurity leadership as a CISO. Outside of his day-to-day work, he conducts independent security research through technical publications and hands-on projects. More By Martino Sani
Decoding the Differences: Black, Gray, and White-Box Pentests
Different types of pentesting methods have designated colors with the key differences.
Blog
Mar 1, 2023