Skip to content

Instantly share code, notes, and snippets.

@wumb0
Last active February 20, 2024 23:13
Show Gist options
  • Star 24 You must be signed in to star a gist
  • Fork 14 You must be signed in to fork a gist
  • Save wumb0/9542469e3915953f7ae02d63998d2553 to your computer and use it in GitHub Desktop.
Save wumb0/9542469e3915953f7ae02d63998d2553 to your computer and use it in GitHub Desktop.
a script for applying MS patch deltas
from ctypes import (windll, wintypes, c_uint64, cast, POINTER, Union, c_ubyte,
LittleEndianStructure, byref, c_size_t)
import zlib
# types and flags
DELTA_FLAG_TYPE = c_uint64
DELTA_FLAG_NONE = 0x00000000
DELTA_APPLY_FLAG_ALLOW_PA19 = 0x00000001
# structures
class DELTA_INPUT(LittleEndianStructure):
class U1(Union):
_fields_ = [('lpcStart', wintypes.LPVOID),
('lpStart', wintypes.LPVOID)]
_anonymous_ = ('u1',)
_fields_ = [('u1', U1),
('uSize', c_size_t),
('Editable', wintypes.BOOL)]
class DELTA_OUTPUT(LittleEndianStructure):
_fields_ = [('lpStart', wintypes.LPVOID),
('uSize', c_size_t)]
# functions
ApplyDeltaB = windll.msdelta.ApplyDeltaB
ApplyDeltaB.argtypes = [DELTA_FLAG_TYPE, DELTA_INPUT, DELTA_INPUT,
POINTER(DELTA_OUTPUT)]
ApplyDeltaB.rettype = wintypes.BOOL
DeltaFree = windll.msdelta.DeltaFree
DeltaFree.argtypes = [wintypes.LPVOID]
DeltaFree.rettype = wintypes.BOOL
gle = windll.kernel32.GetLastError
def apply_patchfile_to_buffer(buf, buflen, patchpath, legacy):
with open(patchpath, 'rb') as patch:
patch_contents = patch.read()
# most (all?) patches (Windows Update MSU) come with a CRC32 prepended to the file
# we don't really care if it is valid or not, we just need to remove it if it is there
# we only need to calculate if the file starts with PA30 or PA19 and then has PA30 or PA19 after it
magic = [b"PA30"]
if legacy:
magic.append(b"PA19")
if patch_contents[:4] in magic and patch_contents[4:][:4] in magic:
# we have to validate and strip the crc instead of just stripping it
crc = int.from_bytes(patch_contents[:4], 'little')
if zlib.crc32(patch_contents[4:]) == crc:
# crc is valid, strip it, else don't
patch_contents = patch_contents[4:]
elif patch_contents[4:][:4] in magic:
# validate the header strip the CRC, we don't care about it
patch_contents = patch_contents[4:]
# check if there is just no CRC at all
elif patch_contents[:4] not in magic:
# this just isn't valid
raise Exception("Patch file is invalid")
applyflags = DELTA_APPLY_FLAG_ALLOW_PA19 if legacy else DELTA_FLAG_NONE
dd = DELTA_INPUT()
ds = DELTA_INPUT()
dout = DELTA_OUTPUT()
ds.lpcStart = buf
ds.uSize = buflen
ds.Editable = False
dd.lpcStart = cast(patch_contents, wintypes.LPVOID)
dd.uSize = len(patch_contents)
dd.Editable = False
status = ApplyDeltaB(applyflags, ds, dd, byref(dout))
if status == 0:
raise Exception("Patch {} failed with error {}".format(patchpath, gle()))
return (dout.lpStart, dout.uSize)
if __name__ == '__main__':
import sys
import base64
import hashlib
import argparse
ap = argparse.ArgumentParser()
mode = ap.add_mutually_exclusive_group(required=True)
output = ap.add_mutually_exclusive_group(required=True)
mode.add_argument("-i", "--input-file",
help="File to patch (forward or reverse)")
mode.add_argument("-n", "--null", action="store_true", default=False,
help="Create the output file from a null diff "
"(null diff must be the first one specified)")
output.add_argument("-o", "--output-file",
help="Destination to write patched file to")
output.add_argument("-d", "--dry-run", action="store_true",
help="Don't write patch, just see if it would patch"
"correctly and get the resulting hash")
ap.add_argument("-l", "--legacy", action='store_true', default=False,
help="Let the API use the PA19 legacy API (if required)")
ap.add_argument("patches", nargs='+', help="Patches to apply")
args = ap.parse_args()
if not args.dry_run and not args.output_file:
print("Either specify -d or -o", file=sys.stderr)
ap.print_help()
sys.exit(1)
if args.null:
inbuf = b""
else:
with open(args.input_file, 'rb') as r:
inbuf = r.read()
buf = cast(inbuf, wintypes.LPVOID)
n = len(inbuf)
to_free = []
try:
for patch in args.patches:
buf, n = apply_patchfile_to_buffer(buf, n, patch, args.legacy)
to_free.append(buf)
outbuf = bytes((c_ubyte*n).from_address(buf))
if not args.dry_run:
with open(args.output_file, 'wb') as w:
w.write(outbuf)
finally:
for buf in to_free:
DeltaFree(buf)
finalhash = hashlib.sha256(outbuf)
print("Applied {} patch{} successfully"
.format(len(args.patches), "es" if len(args.patches) > 1 else ""))
print("Final hash: {}"
.format(base64.b64encode(finalhash.digest()).decode()))
@wumb0
Copy link
Author

wumb0 commented Sep 20, 2021

Huh interesting, I'll try it with mshtml and see if I can reproduce.

@wumb0
Copy link
Author

wumb0 commented Sep 20, 2021

Seems to work with the ones on my machine:

python Q:\Patches\tools\delta_patch.py -i C:\windows\winsxs\amd64_microsoft-windows-i..tmlrendering-legacy_31bf3856ad364e35_11.0.19041.1165_none_cabae23990f3fe32\mshtml.dll -o C:\Users\wumb0\mshtml.dll  C:\windows\winsxs\amd64_microsoft-windows-i..tmlrendering-legacy_31bf3856ad364e35_11.0.19041.1165_none_cabae23990f3fe32\r\mshtml.dll C:\windows\WinSxS\amd64_microsoft-windows-i..tmlrendering-legacy_31bf3856ad364e35_11.0.19041.1237_none_caace3df90ff0160\f\mshtml.dll
Applied 2 patches successfully
Final hash: PufXLDs9MB58xOb2Hx76eArqSsvJW6uiMlkMdefH0No=

The f file I'm using hash a sha256 hash of 7D6AC08B1EA46589DA5BD9C1025B72E8F3228C063D38238F156A9B3F15286747
You should be able to pull that out of windows10.0-kb5005565-x64_5b36501e18065cb2ef54d4bb02b0e2b27cd683d0.msu. I haven't checked it yet, but it should be in there.

@redl17
Copy link

redl17 commented Sep 21, 2021 via email

@m417z
Copy link

m417z commented Sep 16, 2022

Thanks for sharing the code. I added it to Winbindex to unpack null differential files, which allows me to index extra data for these files.

I stumbled upon a bug, though. If the CRC happens to contain the "PA" bytes, the script breaks. For example (that's a real file from an update package):

image

I fixed it by assuming the CRC is always there, which works for my use case. For a more general fix, you might want to calculate the CRC checksum before checking the header, and remove it only if it matches.

@wumb0
Copy link
Author

wumb0 commented Sep 20, 2022

@m417z woah thanks. Love your project and glad I could contribute, if only accidentally :)
Good catch on that bug, I'll fix it. I use gist too much, I should have this in a repo so it's easier for people to help fix stuff with PRs.
Cheers.

@pivotman319-owo
Copy link

pivotman319-owo commented Feb 11, 2024

GE_RELEASE just added support for PA31 deltas, any chance on updating that script when possible?

@m417z
Copy link

m417z commented Feb 11, 2024

For PA31 support you have to use a recent version of UpdateCompression.dll instead of msdelta.dll. You can download it here, place it in the script's folder, and apply this change:

diff --git a/delta_patch.py b/delta_patch.py
index fe04da2..69c9049 100644
--- a/delta_patch.py
+++ b/delta_patch.py
@@ -1,5 +1,5 @@
 from ctypes import (windll, wintypes, c_uint64, cast, POINTER, Union, c_ubyte,
-                    LittleEndianStructure, byref, c_size_t)
+                    LittleEndianStructure, byref, c_size_t, CDLL)
 import zlib
 
 
@@ -26,11 +26,12 @@ class DELTA_OUTPUT(LittleEndianStructure):
 
 
 # functions
-ApplyDeltaB = windll.msdelta.ApplyDeltaB
+msdelta = CDLL('UpdateCompression.dll')
+ApplyDeltaB = msdelta.ApplyDeltaB
 ApplyDeltaB.argtypes = [DELTA_FLAG_TYPE, DELTA_INPUT, DELTA_INPUT,
                         POINTER(DELTA_OUTPUT)]
 ApplyDeltaB.rettype = wintypes.BOOL
-DeltaFree = windll.msdelta.DeltaFree
+DeltaFree = msdelta.DeltaFree
 DeltaFree.argtypes = [wintypes.LPVOID]
 DeltaFree.rettype = wintypes.BOOL
 gle = windll.kernel32.GetLastError

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment