Windows catalog updates
Microsoft Cabinet archive files have MSCF
magic at the beginning:
00000000: 4d53 4346 0000 0000 e3aa 0100 0000 0000 MSCF............
00000010: 4400 0000 0000 0000 0301 0100 1200 0400 D...............
00000020: 0000 0000 1400 0000 0000 1000 e3aa 0100 ................
00000030: e025 0000 0000 0000 0000 0000 2d07 0000 .%..........-...
Windows updates use forward and reverse differentials. They are located in the /f/
and /r/
folders respectively.
Microsoft docs describe how it works.
In short there is a base version (a selected major software release) of an updateable file against which the
deltas are calculated. An update for a specific version of Windows first applies the r
(reverse) delta to obtain the
base version, then it applies the f
(forward) delta to produce the target (updated) version of the file.
For example here is a part of the directory structure from one of the updates (KB5012170
):
├── amd64_microsoft-windows-s..boot-firmwareupdate_31bf3856ad364e35_10.0.19041.1880_none_294d9e3cbae1ff57
│ ├── f
│ │ ├── dbupdate.bin
│ │ └── dbxupdate.bin
│ └── r
│ ├── dbupdate.bin
│ └── dbxupdate.bin
└── amd64_microsoft-windows-s..boot-firmwareupdate_31bf3856ad364e35_10.0.19041.1880_none_294d9e3cbae1ff57.manifest
(yes those are ..
characters in the directory name).
46 Jul 13 2022 f/dbupdate.bin
7169 Jul 13 2022 f/dbxupdate.bin
46 Jul 13 2022 r/dbupdate.bin
1064 Jul 13 2022 r/dbxupdate.bin
As you can see, the f
and r
directories contain files with the same names, but dbxupdate.bin
files have different
sizes. The reverse differential is much shorter (1064) than the forward (7169). This doesn't necessarily mean that the
file changed, because the reverse differential may just describe the following operation:
delete COUNT bytes from offset X. Whereas the forward diff needs to contain the specific bytes that are needed to be
appended or inserted to the source file.
The *.manifest
file describes how the related files need to be treated:
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<assembly ...>
<!--
reducted for sake of simplicity
-->
<directories>
<directory destinationPath="$(runtime.system32)\SecureBootUpdates" owner="true">
<securityDescriptor name="WRP_DIR_DEFAULT_SDDL" />
</directory>
</directories>
<file name="dbupdate.bin" destinationPath="$(runtime.system32)\SecureBootUpdates\" sourceName="dbupdate.bin" importPath="$(build.nttree)\" sourcePath=".\">
<securityDescriptor name="WRP_FILE_DEFAULT_SDDL" />
<asmv2:hash xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
<dsig:Transforms>
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
</dsig:Transforms>
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
<dsig:DigestValue>/6Yl+eMBve0mGQA+aiFg4qvQ7GUCV5eGvIcoRf4mEPw=</dsig:DigestValue>
</asmv2:hash>
</file>
<file name="dbxupdate.bin" destinationPath="$(runtime.system32)\SecureBootUpdates\" sourceName="dbxupdate.bin" importPath="$(build.nttree)\" sourcePath=".\">
<securityDescriptor name="WRP_FILE_DEFAULT_SDDL" />
<asmv2:hash xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
<dsig:Transforms>
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
</dsig:Transforms>
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256" />
<dsig:DigestValue>UlftZMySTmmlvwwOF9qD/RzOLIowjtvzkrluVmhhTqA=</dsig:DigestValue>
</asmv2:hash>
</file>
<!--
reducted for sake of simplicity
-->
</assembly>
It describes where the related files can be found and what their target SHA256 hash is.
echo -n -E 'UlftZMySTmmlvwwOF9qD/RzOLIowjtvzkrluVmhhTqA=' | base64 -d | xxd -p -c 0
5257ed64cc924e69a5bf0c0e17da83fd1cce2c8a308edbf392b96e5668614ea0
One can use the delta_patch.py
script (Windows only
as it loads ApplyDeltaB
and DeltaFree
from msdelta.dll
) to apply patches. So the patch operation may be the
following:
delta_patch.py -i /tmp/dbxupdate-original.bin -o /tmp/dbxupdate-base.bin "${local}/r/dbxupdate.bin"
delta_patch.py -i /tmp/dbxupdate-base.bin -o /tmp/dbxupdate-new.bin "${update}/f/dbxupdate.bin"
# 5257ed64cc924e69a5bf0c0e17da83fd1cce2c8a308edbf392b96e5668614ea0 dbxupdate-original.bin
# 528728c4a643d366445d953c6357a45656795396c09ac93b8a984b74c4bda9c3 dbxupdate-base.bin
# 5257ed64cc924e69a5bf0c0e17da83fd1cce2c8a308edbf392b96e5668614ea0 dbxupdate-new.bin
The -original
and -new
versions match. Why?! Probably because r
version is stored locally so that on a new update,
the client can calculate the base version. But because in our case, the original version was in fact the target version
as well, our local r
matches the one in the patch. We can even check the hashes on
Winbindex, they refer to consecutive versions of dbxupdate.bin
.
There are n
(null) updates that don't have base versions, or more precisely it's the b""
(empty file).
For example dbxupdate2024.bin
is such a file in
KB5036892
introducing Microsoft 2023 DB and KEK
certificates.
- Next post: HTB Business CTF 2024 - pwn - abyss