Using WinSxS to Retrace Windows Update History

Introduction

Should one be asked about %windir%\WinSxS, the first thing springing to mind is “a place to look for a specific version of some library” for this is the directory utilized by the WinSxS (Windows side-by-side assembly) technology designed to solve the .dll isolation problem (which, in broader terms, is referred to as DLL hell). Whether WinSxS is effective in accomplishing this goal is a topic for another discussion (a discussion one can get prepared for by reading this remarkably well-written blog post), but if there are multiple versions of the same component in the system, they are likely to reside in %windir%\WinSxS.

With the introduction of a new type of Windows updates, another use case for the directory emerged. Unlike the “delta compression of the past” facilitating the efficiency of express updates, the type of differential compression adopted recently cannot decompress the file on-site based solely on its previous version and a delta – it requires an additional piece of data that goes by the name of reverse differential. The latter, along with the file to be updated, is stored %windir%\WinSxS.

I intentionally refer to the item being decompressed by a general term of “file” since modules are not the only OS constituents that need updating. For one, the purpose behind looking into the new update technology on my part was to recover old versions of dbxupdate.bin (for this project), which is not an executable image at all, but a binary file containing a new value of an NVRAM variable pertaining to secure boot. Working on the project, I succumbed to the vice of excessive automation and wrote a python script that would traverse the %windir%\WinSxS folder listing all available versions of a given file together with other, potentially useful, information. The way I see it (and, no doubt, a fair number of people will share the sentiment): why spend two minutes running a search over the file system when the task can be automated in under 24 hours?

In this post, I am presenting a few bits and bobs concerning WinSxS structure and its use in Windows updates I learned while implementing the utility.

Not intended as a fully-fledged article, this work, while assuming familiarity with WinSxS and Windows updates, does not provide sufficient information on the subject, even in the form of an overview. Instead, the reader is referred to the sources listed below.

  • The WinSxS technology, its raison d’être and operation specifics, are described in this post.
  • The new type of Window updates that involve an application of reverse and forward differentials is introduced in this whitepaper.
  • Notes on differential compression and internal organization of Windows update packages (.msu) can be found in this post by Jaime Geiger (one of my posts also touches upon the subject).

Theory

The structure of %windir%\WinSxS would be an apt starting point of this little discourse; while there are subdirectories of functional significance in WinSxS, let us focus on the entries that correspond to assemblies themselves. The term assembly will be used in the most general sense as “a collection of resources with a manifest” (the definition courtesy of omnicognate), which is perfectly illustrated by the amd64_microsoft-windows-userexperience-desktop_31bf3856ad364e35_10.0.19041.1741_none_fb3f58b37ea27c55 assembly. Take a look at the files (“resources”) the assembly contains (subfolders pertaining to updates: .\f, .\r, .\n - are omitted).

microsoft-windows-userexperience-desktop files
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
AppListBackup.dll
AppxBlockmap.xml
AppxManifest.xml
AppxSignature.p7x
Assets\BadgeLogo.scale-100.png
Assets\BadgeLogo.scale-125.png
Assets\BadgeLogo.scale-150.png
Assets\BadgeLogo.scale-200.png
Assets\BadgeLogo.scale-400.png
Assets\Dictation\default.css
Assets\Dictation\index.html
Assets\Fonts\AXPMDL2.ttf
Assets\Fonts\CloudAnimation.ttf
Assets\Fonts\ECMDL2.ttf
Assets\Fonts\ESIPMDL2.ttf
Assets\Fonts\GetSMDL.ttf
Assets\Fonts\HandwritingMixedInput.ttf
Assets\KbdAccentPicker.wav
Assets\KbdFunction.wav
Assets\KbdKeyTap.wav
Assets\KbdSpacebar.wav
Assets\KbdSwipeGesture.wav
Assets\LockScreenLogo.scale-200.png
Assets\Ninja\CategorySticker.png
Assets\SplashScreen.scale-100.png
Assets\SplashScreen.scale-125.png
Assets\SplashScreen.scale-150.png
Assets\SplashScreen.scale-200.png
Assets\SplashScreen.scale-400.png
Assets\Square150x150Logo.scale-200.png
Assets\Square44x44Logo.scale-200.png
Assets\Square44x44Logo.targetsize-24_altform-unplated.png
Assets\SquareLogo150x150.scale-100.png
Assets\SquareLogo150x150.scale-200.png
Assets\SquareLogo150x150.scale-400.png
Assets\SquareLogo310x310.scale-100.png
Assets\SquareLogo310x310.scale-200.png
Assets\SquareLogo310x310.scale-400.png
Assets\SquareLogo44x44.scale-100.png
Assets\SquareLogo44x44.scale-200.png
Assets\SquareLogo44x44.scale-400.png
Assets\SquareLogo71x71.scale-100.png
Assets\SquareLogo71x71.scale-200.png
Assets\SquareLogo71x71.scale-400.png
Assets\StoreLogo.png
Assets\StoreLogo.scale-100.png
Assets\StoreLogo.scale-125.png
Assets\StoreLogo.scale-150.png
Assets\StoreLogo.scale-200.png
Assets\StoreLogo.scale-400.png
Assets\Wide310x150Logo.scale-200.png
Assets\WideLogo310x150.scale-100.png
Assets\WideLogo310x150.scale-200.png
Assets\WideLogo310x150.scale-400.png
InputApp.dll
InputApp\Assets\BadgeLogo.scale-100.png
InputApp\Assets\BadgeLogo.scale-125.png
InputApp\Assets\BadgeLogo.scale-150.png
InputApp\Assets\BadgeLogo.scale-200.png
InputApp\Assets\BadgeLogo.scale-400.png
InputApp\Assets\Fonts\AXPMDL2.ttf
InputApp\Assets\Fonts\CloudAnimation.ttf
InputApp\Assets\Fonts\ECMDL2.ttf
InputApp\Assets\Fonts\ESIPMDL2.ttf
InputApp\Assets\Fonts\GetSMDL.ttf
InputApp\Assets\Fonts\HandwritingMixedInput.ttf
InputApp\Assets\KbdAccentPicker.wav
InputApp\Assets\KbdFunction.wav
InputApp\Assets\KbdKeyTap.wav
InputApp\Assets\KbdSpacebar.wav
InputApp\Assets\KbdSwipeGesture.wav
InputApp\Assets\Ninja\CategorySticker.png
InputApp\Assets\SplashScreen.scale-100.png
InputApp\Assets\SplashScreen.scale-125.png
InputApp\Assets\SplashScreen.scale-150.png
InputApp\Assets\SplashScreen.scale-200.png
InputApp\Assets\SplashScreen.scale-400.png
InputApp\Assets\SquareLogo150x150.scale-100.png
InputApp\Assets\SquareLogo150x150.scale-200.png
InputApp\Assets\SquareLogo150x150.scale-400.png
InputApp\Assets\SquareLogo310x310.scale-100.png
InputApp\Assets\SquareLogo310x310.scale-200.png
InputApp\Assets\SquareLogo310x310.scale-400.png
InputApp\Assets\SquareLogo44x44.scale-100.png
InputApp\Assets\SquareLogo44x44.scale-200.png
InputApp\Assets\SquareLogo44x44.scale-400.png
InputApp\Assets\SquareLogo71x71.scale-100.png
InputApp\Assets\SquareLogo71x71.scale-200.png
InputApp\Assets\SquareLogo71x71.scale-400.png
InputApp\Assets\StoreLogo.scale-100.png
InputApp\Assets\StoreLogo.scale-125.png
InputApp\Assets\StoreLogo.scale-150.png
InputApp\Assets\StoreLogo.scale-200.png
InputApp\Assets\StoreLogo.scale-400.png
InputApp\Assets\WideLogo310x150.scale-100.png
InputApp\Assets\WideLogo310x150.scale-200.png
InputApp\Assets\WideLogo310x150.scale-400.png
IrisService.dll
LayoutData.dll
LayoutData.winmd
ScreenClipping.dll
ScreenClipping.winmd
ScreenClippingHost.exe
ScreenClipping\Assets\Fonts\strgmdl2.2.42.ttf
ScreenClipping\Assets\LockScreenLogo.scale-200.png
ScreenClipping\Assets\Sounds\camerashutter.wav
ScreenClipping\Assets\SplashScreen.scale-200.png
ScreenClipping\Assets\Square150x150Logo.scale-200.png
ScreenClipping\Assets\Square44x44Logo.scale-200.png
ScreenClipping\Assets\Square44x44Logo.targetsize-24_altform-unplated.png
ScreenClipping\Assets\StoreLogo.png
ScreenClipping\Assets\Wide310x150Logo.scale-200.png
SuggestionUI.dll
SuggestionUI.winmd
TextInput.dll
TextInput.winmd
TextInputCommon.dll
TextInputCommon.winmd
TextInputHost.exe
concrt140_app.dll
msvcp140_1_app.dll
msvcp140_2_app.dll
msvcp140_app.dll
msvcp140_codecvt_ids_app.dll
resources.pri
vcamp140_app.dll
vccorlib140_app.dll
vcomp140_app.dll
vcruntime140_1_app.dll
vcruntime140_app.dll

The assembly is characterized by a relatively complex internal structure, with multiple levels of subdirectories holding files of various kinds: dynamic-link libraries, fonts, graphic and audio files, etc. To complete the picture, there is a matching manifest file in %windir%\WinSxS\Manifests (manifests can be decompressed using this utility).

microsoft-windows-userexperience-desktop manifest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<assembly xmlns = "urn:schemas-microsoft-com:asm.v3" 
          manifestVersion = "1.0" 
          copyright = "Copyright (c) Microsoft Corporation. All Rights Reserved.">

    <assemblyIdentity name = "Microsoft-Windows-UserExperience-Desktop"
                      version = "10.0.19041.2311"
                      processorArchitecture = "amd64"
                      language = "neutral"
                      buildType = "release"
                      publicKeyToken = "31bf3856ad364e35"
                      versionScope = "nonSxS" />

    <file name = "AppListBackup.dll" 
          destinationPath = "$(runtime.windows)\SystemApps\MicrosoftWindows.Client.CBS_cw5n1h2txyewy\" 
          sourceName = "" 
          importPath = "$(build.nttree)\WindowsUserExperience.Desktop\">
	
        <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>uuxnXwPnRozKyXgN2x6hsNkTS4G70fhsTaUXHG2bkvE=</dsig:DigestValue>
	
        </asmv2:hash>
    </file>
<!-- [...] -->
</assembly>	

Its manifest is what establishes an identity for the assembly. In his post, Jon Wiswall defines an assembly identity as “a property bag of string triplets - namespace, name, and value - for each attribute”, then goes on to describe the way the identity translates into a unique name for the directory where the assembly resources are stored. The folder name being necessarily unique, one can talk about a key form of assembly identity generated based on the assembly attributes. The key form is structured as follows:

1
<cpu arch>_<assembly name>_<public key token>_<version>_<locale>_<identity hash>

Obviously, <cpu arch> is a CPU architecture; among possible values one can find, expectedly, x86, amd64, arm, arm64, msil, and, surprisingly, wow64. The latter has to do with emulation of 32-bit code on 64-bit platforms, the task performed by the WoW64 (why it requires a separate <cpu arch> is, although an interesting question, sadly, outside the scope of this work). <public key token> holds the last 8 bytes (written in hexadecimal format) of the sha1 hash of the public key complementing the private key used to sign the assembly. <version>, a quadruplet <major>.<minor>.<build>.<revision>, represents the assembly version. <locale> corresponds to a notion known by many different names: “locale”, “culture”, or, simply, “language” (for example, en-us); when an assembly is language-agnostic, the none placeholder is used.

A careful examination of the manifest will convince the reader that only a subset of assembly attributes is used to generate its key form, hence there arises the problem of ensuring key forms’ uniqueness, a problem that is solved by appending an 8 byte-long hash of the assembly identity written in hexadecimal notation (at the time of writing, the hashing algorithm remains undisclosed). The fact that Microsoft chose to err on the side of caution means that repeats in the values of <assembly name> are possible; however, this attribute is still likely to be different for the assemblies different in the functionality they provide and, for the sake of simplicity, my script will treat it as such.

Now that we have established what the assembly identity is and how it translates into the folder names in %windir%\WinSxS, let us consider the same, but from the versioning standpoint. It seems reasonable to treat <assembly name> as a “unit of functionality” by assuming that two assemblies sharing <assembly name> implement the same functionality. Taking the assumption a step further, two assemblies with the same <assembly name>, but different <cpu arch>, are designed to perform the same function, but on different platforms. Likewise, two assemblies sharing an <assembly name>, but not <locale>, differ in that their textual data (such as string tables in DLLs) are in two different languages. Equally reasonable it is to assume that several language- and architecture-specific variants of the same assembly are simultaneously present in the system (e.g. 32-bit and 64-bit versions of the same application), hence each triplet (<assembly name>, <cpu arch>, <locale>) should be versioned independently and, consequently, separate updates should be issued for each such triplet. The latter affects the way update histories are reconstructed, which brings us to the next topic in this discussion, installation of differential-compression-based updates.

The technology Microsoft refers to as “express updates” has been superseded by updates of a new type. Binary deltas included in express update packages were computed based on the latest version of the files, subject to the update, residing on the computer. As a result, a large number of update packages, one for each possible combination of files’ versions, had to be computed. With the new update technology, deltas are computed relative to a common base version of the assembly being updated. For details, the reader is referred to this whitepaper; presented here is only an overview of this technique.

NOTE: I will describe the process in terms of assemblies; it is important to keep in mind that an update package may modify only some resources within an assembly or multiple assemblies at the same time.

To begin with, let us suppose for a moment that the update package is installing a hitherto non-existent assembly; in this case, it will include so-called null differentials for each “resource” file, which amount to nothing more than these files compressed using an ordinary lossless compression algorithm. The update installer will create an assembly’s identity key form-named subdirectory in %windir%\WinSxS holding the decompressed files along with the null differentials used to generate them. The latter are placed in a subfolder named n. This version of the assembly will be considered a base version. This is not the only way an assembly’s base version could be brought into existence. It may also arrive in a setup for a major OS release or come along with the computer, preinstalled by the manufacturer; naturally, there will be no null differentials in WinSxS in this case.

Now consider a package updating some assembly already present in the system. Such a package will contain so-called forward differentials (one per every “resource” file being updated). A forward differential is a delta that, when applied to a base version of the assembly, will produce the desired, updated, files. If the base version is the thing being updated, then we are golden. When, however, the assembly is “at an advanced version” and the initial, base, variant is long gone, forward differentials are not applicable directly; in this case, the assembly must be accompanied by reverse differentials. The update installer, having located the latest assembly in WinSxS, will find stored as part of it, in a subdirectory named r, a set of reverse differentials. A reverse differential is a difference (in terms of differential compression) between a file base and its current version, thereby it becomes possible to recover a base version of the assembly and then go from there.

I will illustrate the process by an example (should this one be insufficient, there is another, a real-life, example here). Say, there is an update package intended to advance the file library.dll from its current revision 42 to revision 43; in order to accomplish it, the package must contain, among other files, a manifest describing the new “incarnation” of the assembly, a forward differential calculated as a difference between the 43th and first revisions of library.dll (i.e. x.x.x.1 + <forward differential> = x.x.x.43) and, though it is not strictly necessary to carry out the update, a reverse differential, which is the same as its “forward” counterpart, but “with an opposite sign”, so to speak (i.e. x.x.x.43 + <reverse differential> = x.x.x.1). This reverse differential will make it possible to install the next update to library.dll. The diagram below shows how various versions of library.dll and the accompanying differentials are related.

?

Here is an approximate sequence of steps involved in installing the update:

  1. With the help of enclosed manifest, the installer locates the current version of library.dll (marked by a revision number of 42) in WinSxS and in the same directory – the subdirectory r containing a reverse differential.
  2. The reverse differential %windir%\WinSxS\x86_..._x.x.x.42_...\r\library.dll is then applied to library.dll in order to reconstruct its base version (we assume its revision number to be 1).
  3. The installer decompresses a new version of library.dll (rev. 43) by applying the forward differential extracted from the update package to the base version of this file generated at step 2.
  4. A new directory in WinSxS is created and the decompressed file along with the forward and reverse differentials from the update package are copied there (into the f and r subdirectories respectively).
  5. Auxiliary steps required to create a new assembly (such as placing its manifest in the Manifests subfolder) are performed.

Note that both reverse and forward differentials that come in the update package are saved in WinSxS. The reverse differential, of course, will come in handy when the next update package arrives. Whether Windows has any use for the forward differential, apart from that of a keepsake, a perfect reminder of the glorious updates gone by, I have not yet figured out, but it will be utilized by my script to retrace the update history (which is what we are going to discuss next).

Demonstration

The reason I needed a script traversing %windir%\WinSxS in the first place was to recover earlier versions of a particular file, dbxupdate.bin, so this became its primary use case (although, it also allows for dumping the entire content of WinSxS). The algorithm identifies all the assemblies containing a given file and, for each value of tuple (<assembly name>, <cpu arch>, <locale>), builds a sequence of versions, while checking that the sequence elements, indeed, belong to the same file by leveraging reverse and forward differentials. The latter is accomplished with the help of Jaime Geiger’s utility, which, in turn, employs Microsoft Delta Compression API to apply differentials meaning that its use (and, consequently, that of my module) is limited to Windows. The Delta Compression API seems to come equipped with a safeguard against improper application of differentials. Pass a wrong (i.e. computed for a file other than the one in the input buffer) differential to ApplyDeltaB() and, in all probability, the function will fail, with GetLastError() returning 13 (which translates into the “The data is invalid” message). This feature is what enables my script to perform the check. What it cannot check is whether the file sequence is contiguous (due to the fact that deltas are computed relative to a base and not the most recent version of the file). Assembly’s version attribute is of no help here either since more often than not there would be a gap in revision numbers, even between two consecutive updates to the same file.

To see the script in action, let us launch it passing, say, shlwapi.dll as a parameter.

win_read_winsxs's output for shlwapi.dll
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
>python win_read_winsxs.py -f shlwapi.dll

microsoft-windows-shlwapi (arch = amd64, locale = none)
~~~~~~~~~~~~~~
microsoft-windows-shlwapi
         files: {'shlwapi.dll'}
         fwd: {'shlwapi.dll'}
         rev: {'shlwapi.dll'}
         null: set()
         arch: amd64
         ver: 10.0.19041.1706
         ts: Fri May 13 18:30:55 2022
         loc: none
         token: 31bf3856ad364e35
         hash: 6e6374325a0e351e

microsoft-windows-shlwapi
         files: {'shlwapi.dll'}
         fwd: {'shlwapi.dll'}
         rev: {'shlwapi.dll'}
         null: set()
         arch: amd64
         ver: 10.0.19041.2075
         ts: Thu Dec 15 07:58:23 2022
         loc: none
         token: 31bf3856ad364e35
         hash: 6eb63e5a59cf066e


shlwapi.dll: 10.0.19041.1706 <==> 10.0.19041.2075


microsoft-windows-shlwapi (arch = wow64, locale = none)
~~~~~~~~~~~~~~
microsoft-windows-shlwapi
         files: {'shlwapi.dll'}
         fwd: {'shlwapi.dll'}
         rev: {'shlwapi.dll'}
         null: set()
         arch: wow64
         ver: 10.0.19041.1706
         ts: Fri May 13 18:31:31 2022
         loc: none
         token: 31bf3856ad364e35
         hash: 78b81e848e6ef719

microsoft-windows-shlwapi
         files: {'shlwapi.dll'}
         fwd: {'shlwapi.dll'}
         rev: {'shlwapi.dll'}
         null: set()
         arch: wow64
         ver: 10.0.19041.2075
         ts: Thu Dec 15 08:01:05 2022
         loc: none
         token: 31bf3856ad364e35
         hash: 790ae8ac8e2fc869


shlwapi.dll: 10.0.19041.1706 <==> 10.0.19041.2075

shlwapi.dll was found among resources of the microsoft-windows-shlwapi assembly; of the latter, there are two variants: for amd64 and wow64. Each variant exists in two versions: 10.0.19041.1706 and 10.0.19041.2075 (the base version was not preserved) amounting to four different “renditions” of shlwapi.dll. shlwapi.dll, revision 2075 is derived from shlwapi.dll, revision 1706 by a successive application of first reverse and then forward differentials (as indicated by a <==> symbol).

Allow me to demonstrate this process in detail. But first, let us get acquainted with the classes defined in win_read_winsxs by replicating the output above.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
>>> from win_read_winsxs import WinSxS, WinSxSFileId, Sequence
>>> wss = WinSxS()
>>> fid = WinSxSFileId("wow64", "none", "microsoft-windows-shlwapi", "shlwapi.dll")
>>> wss.versioned_files[fid]
[microsoft-windows-shlwapi, microsoft-windows-shlwapi]
>>> lst = wss.versioned_files[fid]
>>> print(*lst, sep = "\n")
microsoft-windows-shlwapi
         files: {'shlwapi.dll'}
         fwd: {'shlwapi.dll'}
         rev: {'shlwapi.dll'}
         null: set()
         arch: wow64
         ver: 10.0.19041.1706
         ts: Fri May 13 18:31:31 2022
         loc: none
         token: 31bf3856ad364e35
         hash: 78b81e848e6ef719
microsoft-windows-shlwapi
         files: {'shlwapi.dll'}
         fwd: {'shlwapi.dll'}
         rev: {'shlwapi.dll'}
         null: set()
         arch: wow64
         ver: 10.0.19041.2075
         ts: Thu Dec 15 08:01:05 2022
         loc: none
         token: 31bf3856ad364e35
         hash: 790ae8ac8e2fc869

>>> print(Sequence(lst, 'shlwapi.dll'))
shlwapi.dll: 10.0.19041.1706 <==> 10.0.19041.2075

We begin by applying the reverse differential stored alongside shlwapi.dll, version 10.0.19041.1706.

1
2
3
4
5
6
7
>>> from win_read_winsxs import apply_differential_to_file
>>> buf = apply_differential_to_file(lst[0].get_file_path('shlwapi.dll'),
...                                  lst[0].get_rev_path('shlwapi.dll'))
>>> with open('shlwapi_current_base.dll', 'wb') as f:
...     f.write(buf)
...
275280

As a result, we obtain a base version of shlwapi.dll (take a note of its revision number).

?

The next step is to apply a forward differential (it is a part of the latest microsoft-windows-shlwapi).

1
2
3
4
5
6
>>> buf = apply_differential_to_file('shlwapi_current_base.dll',
...                                  lst[1].get_fwd_path('shlwapi.dll'))
>>> with open('shlwapi_current.dll', 'wb') as f:
...     f.write(buf)
...
276840

And there you have it: we generated the 2075th revision of shlwapi.dll.

?

In effect, there are not two but three versions of shlwapi.dll recoverable from WinSxS. It would be possible to reconstruct the base assembly if it were not for the hashing algorithm remaining undisclosed. Still, if needs must, we will do our best.

For the sake of demonstration, I created a fake WinSxS entry for the base assembly using cafecafecafecafe as a substitute for the unknown hash, and placed all three assemblies (the wow64 variant) in a directory named .\MiniWinSxS.

All available variants of wow64_microsoft-windows-shlwapi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
>python win_read_winsxs.py -f shlwapi.dll -p .\MiniWinSxS

microsoft-windows-shlwapi (arch = wow64, locale = none)
~~~~~~~~~~~~~~
microsoft-windows-shlwapi
         files: {'shlwapi.dll'}
         fwd: set()
         rev: set()
         null: set()
         arch: wow64
         ver: 10.0.19041.1
         ts: Thu Dec 29 08:54:18 2022
         loc: none
         token: 31bf3856ad364e35
         hash: cafecafecafecafe

microsoft-windows-shlwapi
         files: {'shlwapi.dll'}
         fwd: {'shlwapi.dll'}
         rev: {'shlwapi.dll'}
         null: set()
         arch: wow64
         ver: 10.0.19041.1706
         ts: Thu Dec 29 09:03:03 2022
         loc: none
         token: 31bf3856ad364e35
         hash: 78b81e848e6ef719

microsoft-windows-shlwapi
         files: {'shlwapi.dll'}
         fwd: {'shlwapi.dll'}
         rev: {'shlwapi.dll'}
         null: set()
         arch: wow64
         ver: 10.0.19041.2075
         ts: Thu Dec 29 09:03:03 2022
         loc: none
         token: 31bf3856ad364e35
         hash: 790ae8ac8e2fc869


shlwapi.dll: 10.0.19041.1○ ==> 10.0.19041.1706 <==> 10.0.19041.2075

Notice that the “update strand” grew by one entry. The 1706th revision of shlwapi.dll can be derived from the first revision of the same (which, a little circle next to it tells us, was recognized as base) by applying a forward differential only (hence the ==> symbol instead of <==>).

A Fun Experiment for The Curious

The idea of collecting earlier versions of system components from WinSxS is all well and good, but the fact that nothing prevents Windows from removing assemblies that are no longer referenced challenges its viability as a strategy to recover updates history. How long will Windows keep old files for? While I cannot give you an explicit answer, identifying the longest sequence of files will, implicitly, provide a decent estimate. Below is a code snippet that would do just that.

Identifying the longest sequence
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from win_read_winsxs import WinSxS, Sequence, Relation

ws = WinSxS()

mln = 0
mid = 0
for id, lst in ws.versioned_files.items():
    s = Sequence(lst, id.file)
    ln = 0
    for i in range(1, len(lst)):
        r = s.get_relation_to_parent(i)
        if (r == Relation.Forward) or (r == Relation.ReverseForward):
            ln += 1
            if ln > mln:
                mid = id
                mln = ln
        else:
            ln = 0

It must be an impressive piece of chronicling taking us from the very origin, wheels and sector gears, all the way up to the present day. A drum roll, please!

1
2
3
>>> print(Sequence(ws.versioned_files[mid], mid.file))

TpmTasks.dll: 10.0.19041.1880 <==> 10.0.19041.1889 <==> 10.0.19041.2311

Having counted a (recoverable) base version in for good measure, we conclude that the longest file sequence on the test computer is four assembly versions long. One should keep in mind that the possibility of assemblies being deleted is not the only thing that affects the sequence length; the new type of updates was adopted relatively recently, in late 2018, therefore the assemblies released before that will not be included.

Postscriptum

To those of my readers who are troubleshooting their system after an unfortunate update or, on the contrary, are looking into some vulnerability fixed by the recent security patch and have been lured here by the false promises of a clickbaity title: it is quite possible you do not need to touch WinSxS at all. Try Winbindex instead; the files you need may very well be there.

– Ry Auscitte

References

  1. Windows Updates using forward and reverse differentials, Microsoft Docs
  2. Jaime Geiger, Extracting and Diffing Windows Patches in 2020
  3. Everything you Never Wanted to Know about WinSxS, A Blog About Stuff
  4. Jon Wiswall, What’s that awful directory name under Windows\WinSxS?, Nothing Ventured, Nothing Gained
  5. Michael Maltsev, Winbindex: The Windows Binaries Index
  6. wcpex: A tool to extract Windows Manifest files that can be found in the WinSxS folder
  7. Ry Auscitte, Inner Workings of UEFI Secure Boot Signature Revocation List (DBX) Updates (2023), Notes of an Innocent Bystander (with a Chainsaw in Hand)