├── .gitignore ├── README.md └── snapdiff.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | build 3 | dist 4 | venv 5 | venv.bat 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # snapdiff 2 | Windows Installation Diff Tool 3 | 4 | SnapDiff captures filesystem and registry changes performed by Windows installers. 5 | 6 | You run SnapDiff, specifying what subdirectories and registry keys to 'snapshot', and it recursively captures the list of files and keys and values at those locations. 7 | Then it waits while you perform an installation of a piece of software. 8 | After the installer is complete, you return to SnapDiff and it creates a second snapshot. 9 | 10 | Finally, it creates a zip file of the files and registry keys that were added/changed between the two snapshots. 11 | 12 | This is useful for moving Windows programs to Wine under Linux, where the application may run fine, but the ability to run the installer is limited. 13 | 14 | --- 15 | 16 | Note that this tool is not written in the most 'optimal' fashion, which would involve process-level monitoring of the installer. This functionality may be offered eventually, but for now, you can expect that files and registry keys modified or changed by other processes than the installer may pollute the output zip file. These still need to be cleaned up by hand. This is a known limitation at this time and may be resolved in future releases. 17 | -------------------------------------------------------------------------------- /snapdiff.py: -------------------------------------------------------------------------------- 1 | import _winreg 2 | import os 3 | import sys 4 | import zipfile 5 | import tempfile 6 | import argparse 7 | import hashlib 8 | import codecs 9 | import re 10 | import ctypes 11 | 12 | wow64key = 0 13 | re_excludedir = [] 14 | re_excludereg = [] 15 | 16 | 17 | def Is64Windows(): 18 | return 'PROGRAMFILES(X86)' in os.environ 19 | 20 | if Is64Windows(): 21 | wow64key = _winreg.KEY_WOW64_64KEY 22 | Wow64DisableWow64FsRedirection = ctypes.windll.kernel32.Wow64DisableWow64FsRedirection 23 | old_value = ctypes.c_long() 24 | Wow64DisableWow64FsRedirection(ctypes.byref(old_value)) 25 | 26 | 27 | def subkeys(key, numsubkeys): 28 | 29 | i = 0 30 | 31 | while i < numsubkeys: 32 | try: 33 | subkey = _winreg.EnumKey(key, i) 34 | yield subkey 35 | except WindowsError as e: 36 | pass 37 | i += 1 38 | 39 | 40 | def walk_registry(rootkey, keypath, key=None, full_keypath=None): 41 | 42 | if args.verbose: 43 | print u"{0}\\{1}".format(reghivestr[rootkey], full_keypath) 44 | 45 | if key is None: 46 | key = rootkey 47 | if full_keypath is None: 48 | full_keypath = keypath 49 | 50 | if keypath: 51 | keypaths = keypath.split("\\") 52 | for kp in keypaths: 53 | try: 54 | key = _winreg.OpenKey(key, kp, 0, wow64key | _winreg.KEY_READ) 55 | except WindowsError as e: 56 | return 57 | 58 | info = _winreg.QueryInfoKey(key) 59 | modft = info[2] 60 | 61 | yield (full_keypath, key, modft) 62 | 63 | subkeynames = [] 64 | for subkeyname in subkeys(key, info[0]): 65 | subkeynames.append(subkeyname) 66 | 67 | for subkeyname in subkeynames: 68 | 69 | if full_keypath: 70 | next_full_keypath = full_keypath + "\\" + subkeyname 71 | else: 72 | next_full_keypath = subkeyname 73 | 74 | # Don't do this recursively! 75 | if keypath.endswith("Wow6432Node") and subkeyname == "Wow6432Node": 76 | continue 77 | 78 | for x in walk_registry(rootkey, 79 | subkeyname, 80 | key, 81 | (full_keypath + "\\" + subkeyname) if full_keypath else subkeyname): 82 | yield x 83 | 84 | def hash_multi_sz(x): 85 | if x is None: 86 | return None 87 | m = hashlib.md5() 88 | for s in x: 89 | m.update(bytearray(s, "utf-16-le")) 90 | return m.digest() 91 | 92 | _valdatahashes={} 93 | _valdatahashes[_winreg.REG_BINARY] = lambda x: None if x is None else hashlib.md5(x).digest() 94 | _valdatahashes[_winreg.REG_DWORD] = lambda x: None if x is None else x 95 | _valdatahashes[_winreg.REG_DWORD_LITTLE_ENDIAN] = lambda x: None if x is None else x 96 | _valdatahashes[_winreg.REG_DWORD_BIG_ENDIAN] = lambda x: None if x is None else x 97 | _valdatahashes[_winreg.REG_EXPAND_SZ] = lambda x: None if x is None else hashlib.md5(bytearray(x, "utf-16-le")).digest() 98 | _valdatahashes[_winreg.REG_LINK] = lambda x: None if x is None else hashlib.md5(x).digest() 99 | _valdatahashes[_winreg.REG_MULTI_SZ] = hash_multi_sz 100 | _valdatahashes[_winreg.REG_NONE] = lambda x: None if x is None else hashlib.md5(x).digest() 101 | _valdatahashes[_winreg.REG_RESOURCE_LIST] = lambda x: None if x is None else hashlib.md5(x).digest() 102 | _valdatahashes[_winreg.REG_FULL_RESOURCE_DESCRIPTOR] = lambda x: None if x is None else hashlib.md5(x).digest() 103 | _valdatahashes[_winreg.REG_RESOURCE_REQUIREMENTS_LIST] = lambda x: None if x is None else hashlib.md5(x).digest() 104 | _valdatahashes[_winreg.REG_SZ] = lambda x: None if x is None else hashlib.md5(bytearray(x, "utf-16-le")).digest() 105 | _valdatahashes[11] = lambda x: None if x is None else x 106 | 107 | def valdatahash(data, type): 108 | if type not in _valdatahashes: 109 | return None 110 | return _valdatahashes[type](data) 111 | 112 | 113 | def key_values(key): 114 | i = 0 115 | while True: 116 | try: 117 | valtuple = _winreg.EnumValue(key, i) 118 | yield (valtuple[0], valdatahash(valtuple[1], valtuple[2]), valtuple[2]) 119 | i += 1 120 | except WindowsError as e: 121 | break 122 | 123 | 124 | def snap_directory(dir): 125 | dir = os.path.abspath(dir) 126 | print u"Snapping directory '{0}'".format(dir) 127 | snap = [] 128 | 129 | for root, dirs, files in os.walk(dir, topdown=False): 130 | if args.verbose: 131 | for d in dirs: 132 | print u"{0}".format(os.path.join(root, d)) 133 | for f in files: 134 | print u"{0}".format(os.path.join(root, f)) 135 | snap.append((root, dirs, files)) 136 | 137 | return snap 138 | 139 | def snap_registry(reg): 140 | print u"Snapping registry '{0}'".format(reg) 141 | 142 | regparts = reg.split("\\", 1) 143 | if len(regparts) == 1: 144 | rootkey = regparts[0] 145 | rootkeypath = "" 146 | else: 147 | rootkey = regparts[0] 148 | rootkeypath = regparts[1] 149 | 150 | rootkey = reghiveval[rootkey] 151 | snap = [] 152 | 153 | for (keypath, key, modft) in walk_registry(rootkey, rootkeypath): 154 | 155 | values = [] 156 | for (vname, vhash, vtype) in key_values(key): 157 | values.append((vname, vhash, vtype)) 158 | 159 | snap.append((rootkey, keypath, modft, values)) 160 | 161 | # print "snap: {}".format(snap) 162 | 163 | return snap 164 | 165 | def snap_all(): 166 | 167 | dsnap = [] 168 | rsnap = [] 169 | 170 | for d in args.dir: 171 | s = snap_directory(d) 172 | dsnap.append(s) 173 | 174 | for r in args.reg: 175 | s = snap_registry(r) 176 | rsnap.append(s) 177 | 178 | snap = {"dirs": dsnap, "regs": rsnap} 179 | 180 | return snap 181 | 182 | def match_excludedir(p): 183 | for exc in re_excludedir: 184 | if exc.match(p): 185 | return True 186 | return False 187 | 188 | def match_excludereg(p): 189 | for exc in re_excludereg: 190 | if exc.match(p): 191 | return True 192 | return False 193 | 194 | def diff_directory(zf, dirs1, dirs2): 195 | d1set = set() 196 | 197 | # Get all paths from first snap into fast set 198 | for dir in dirs1: 199 | for (root, dirs, files) in dir: 200 | for d in dirs: 201 | d1path = os.path.join(root, d) 202 | if not match_excludedir(d1path): 203 | d1set.add(d1path) 204 | for f in files: 205 | f1path = os.path.join(root, f) 206 | if not match_excludedir(f1path): 207 | d1set.add(f1path) 208 | 209 | # Build list of all paths in the second snap that are not in the first snap 210 | diffpaths = [] 211 | for dir in dirs2: 212 | for root, dirs, files in dir: 213 | for d in dirs: 214 | dp = os.path.join(root, d) 215 | if not match_excludedir(dp): 216 | if dp not in d1set: 217 | diffpaths.append(dp) 218 | for f in files: 219 | fp = os.path.join(root, f) 220 | if not match_excludedir(fp): 221 | if fp not in d1set: 222 | diffpaths.append(fp) 223 | 224 | for dp in diffpaths: 225 | try: 226 | if os.path.isabs(dp): 227 | # If absolute path c:\foo\bar, then write to location c\foo\bar 228 | (drive, path) = dp.split(u":", 1) 229 | if args.includedrive: 230 | dpname = u"{0}{1}".format(drive.lower(), path) 231 | else: 232 | dpname = path[1:] 233 | else: 234 | dpname = dp 235 | 236 | zf.write(dp, dpname) 237 | 238 | print "Added: " + dp 239 | except: 240 | print "Skipped: " + dp 241 | 242 | def diff_values(values1, values2): 243 | 244 | vh1set = set() 245 | diffvalues = [] 246 | 247 | for (vname, vhash, vtype) in values1: 248 | vh1set.add(vhash) 249 | 250 | for (vname, vhash, vtype) in values2: 251 | if vhash not in vh1set: 252 | diffvalues.append((vname, vhash, vtype)) 253 | 254 | return diffvalues 255 | 256 | 257 | def diff_registry(zf, regs1, regs2): 258 | 259 | k1set = dict() 260 | 261 | # Get all keys in the first set 262 | for reglist in regs1: 263 | for (hkey, keypath, modft, values) in reglist: 264 | kindex = reghivestr[hkey] + u"\\" + keypath 265 | if not match_excludereg(kindex): 266 | k1set[kindex] = (modft, values) 267 | 268 | # Get all keys in the second set 269 | diffkeys = [] 270 | for reglist in regs2: 271 | for (hkey, keypath, modft, values) in reglist: 272 | kindex = reghivestr[hkey] + u"\\" + keypath 273 | if not match_excludereg(kindex): 274 | if kindex in k1set: 275 | (k1modft, k1values) = k1set[kindex] 276 | if modft != k1modft: 277 | diffvalues = diff_values(k1values, values) 278 | diffkeys.append((hkey, keypath, diffvalues)) 279 | else: 280 | diffkeys.append((hkey, keypath, values)) 281 | 282 | # Write regfile 283 | write_regfile(zf, diffkeys) 284 | 285 | reghivestr={} 286 | reghivestr[u"HKEY_CLASSES_ROOT"]=u"HKEY_CLASSES_ROOT" 287 | reghivestr[u"HKEY_CURRENT_USER"]=u"HKEY_CURRENT_USER" 288 | reghivestr[u"HKEY_LOCAL_MACHINE"]=u"HKEY_LOCAL_MACHINE" 289 | reghivestr[u"HKEY_USERS"]=u"HKEY_USERS" 290 | reghivestr[u"HKEY_CURRENT_CONFIG"]=u"HKEY_CURRENT_CONFIG" 291 | reghivestr[u"HKCR"]=u"HKEY_CLASSES_ROOT" 292 | reghivestr[u"HKCU"]=u"HKEY_CURRENT_USER" 293 | reghivestr[u"HKLM"]=u"HKEY_LOCAL_MACHINE" 294 | reghivestr[u"HKU"]=u"HKEY_USERS" 295 | reghivestr[u"HKCC"]=u"HKEY_CURRENT_CONFIG" 296 | reghivestr[_winreg.HKEY_CLASSES_ROOT]=u"HKEY_CLASSES_ROOT" 297 | reghivestr[_winreg.HKEY_CURRENT_USER]=u"HKEY_CURRENT_USER" 298 | reghivestr[_winreg.HKEY_LOCAL_MACHINE]=u"HKEY_LOCAL_MACHINE" 299 | reghivestr[_winreg.HKEY_USERS]=u"HKEY_USERS" 300 | reghivestr[_winreg.HKEY_CURRENT_CONFIG]=u"HKEY_CURRENT_CONFIG" 301 | 302 | reghiveval={} 303 | reghiveval[u"HKEY_CLASSES_ROOT"]=_winreg.HKEY_CLASSES_ROOT 304 | reghiveval[u"HKEY_CURRENT_USER"]=_winreg.HKEY_CURRENT_USER 305 | reghiveval[u"HKEY_LOCAL_MACHINE"]=_winreg.HKEY_LOCAL_MACHINE 306 | reghiveval[u"HKEY_USERS"]=_winreg.HKEY_USERS 307 | reghiveval[u"HKEY_CURRENT_CONFIG"]=_winreg.HKEY_CURRENT_CONFIG 308 | reghiveval[u"HKCR"]=_winreg.HKEY_CLASSES_ROOT 309 | reghiveval[u"HKCU"]=_winreg.HKEY_CURRENT_USER 310 | reghiveval[u"HKLM"]=_winreg.HKEY_LOCAL_MACHINE 311 | reghiveval[u"HKU"]=_winreg.HKEY_USERS 312 | reghiveval[u"HKCC"]=_winreg.HKEY_CURRENT_CONFIG 313 | reghiveval[_winreg.HKEY_CLASSES_ROOT]=_winreg.HKEY_CLASSES_ROOT 314 | reghiveval[_winreg.HKEY_CURRENT_USER]=_winreg.HKEY_CURRENT_USER 315 | reghiveval[_winreg.HKEY_LOCAL_MACHINE]=_winreg.HKEY_LOCAL_MACHINE 316 | reghiveval[_winreg.HKEY_USERS]=_winreg.HKEY_USERS 317 | reghiveval[_winreg.HKEY_CURRENT_CONFIG]=_winreg.HKEY_CURRENT_CONFIG 318 | 319 | 320 | def reghexstr(val): 321 | valstr = u"" 322 | if val: 323 | first = True 324 | for x in val: 325 | if not first: 326 | valstr += u"," 327 | else: 328 | first=False 329 | valstr += u"{0:02x}".format(x) 330 | return valstr 331 | 332 | def regquotestr(s): 333 | return u"\"" + s.replace(u"\\", u"\\\\").replace(u"\"", u"\\\"") + u"\"" 334 | 335 | 336 | def regvaluestring(val, vtype): 337 | 338 | if not val: 339 | valstr = u"hex({0:1x}):".format(vtype) 340 | elif vtype == _winreg.REG_DWORD: 341 | valstr = u"dword:{0:08x}".format(val) 342 | elif vtype == _winreg.REG_EXPAND_SZ: 343 | valstr = reghexstr(bytearray(val, "utf-16-le")) 344 | if len(valstr) > 0: 345 | valstr += u"," 346 | valstr += u"00,00" 347 | valstr = u"hex({0:1x}):".format(vtype) + valstr 348 | elif vtype == _winreg.REG_SZ: 349 | isprint = True 350 | val = unicode(val) 351 | for x in val: 352 | if ord(x) < 32: 353 | isprint = False 354 | break 355 | if isprint: 356 | valstr = regquotestr(val) 357 | else: 358 | valstr = reghexstr(bytearray(val, "utf-16-le")) 359 | if len(valstr) > 0: 360 | valstr += u"," 361 | valstr += u"00,00" 362 | valstr = u"hex({0:1x}):".format(vtype) + valstr 363 | elif vtype == _winreg.REG_MULTI_SZ: 364 | valstr = u"" 365 | for s in val: 366 | if len(valstr) > 0: 367 | valstr += u"," 368 | valstr += reghexstr(bytearray(s, "utf-16-le")) 369 | if len(valstr) > 0: 370 | valstr += u"," 371 | valstr += u"00,00" 372 | if len(valstr) > 0: 373 | valstr += u"," 374 | valstr = u"hex({0:1x}):".format(vtype) + valstr + u"00,00" 375 | else: 376 | valstr = u"hex({0:1x}):".format(vtype) + reghexstr(bytearray(val)) 377 | 378 | return valstr 379 | 380 | 381 | def write_regfile(zf, diffkeys): 382 | 383 | f = tempfile.NamedTemporaryFile(delete=False) 384 | 385 | f.write(codecs.BOM_UTF16_LE) 386 | f.write(u"Windows Registry Editor Version 5.00\r\n\r\n".encode('utf-16-le')) 387 | for (hkey, keypath, diffvalues) in diffkeys: 388 | print u"Writing registry key: {0}\\{1}".format(reghivestr[hkey], keypath) 389 | try: 390 | key = _winreg.OpenKey(hkey, keypath, 0, wow64key | _winreg.KEY_READ) 391 | except: 392 | print u"Unable to open key" 393 | continue 394 | 395 | if not keypath: 396 | f.write(u"[{0}]\r\n".format(reghivestr[hkey]).encode('utf-16-le')) 397 | else: 398 | f.write(u"[{0}\\{1}]\r\n".format(reghivestr[hkey], keypath).encode('utf-16-le')) 399 | 400 | for (vname, vhash, vtype) in diffvalues: 401 | try: 402 | val = _winreg.QueryValueEx(key, vname)[0] 403 | if not vname: 404 | f.write(u"@={0}\r\n".format(regvaluestring(val, vtype)).encode('utf-16-le')) 405 | else: 406 | f.write(u"{0}={1}\r\n".format(regquotestr(vname), regvaluestring(val, vtype)).encode('utf-16-le')) 407 | except: 408 | print u"Unable to query value" 409 | continue 410 | 411 | f.write(u"\r\n".encode('utf-16-le')) 412 | 413 | f.close() 414 | 415 | print u"Writing registry diff" 416 | zf.write(f.name, u"snapdiff.reg") 417 | 418 | os.remove(f.name) 419 | 420 | 421 | def diff_all(zf, snap1, snap2): 422 | diff_registry(zf, snap1["regs"], snap2["regs"]) 423 | diff_directory(zf, snap1["dirs"], snap2["dirs"]) 424 | 425 | def main(): 426 | 427 | snap1 = snap_all() 428 | 429 | raw_input(u"Press Enter to perform second snapshot...") 430 | 431 | snap2 = snap_all() 432 | 433 | with zipfile.ZipFile(args.out, "w", zipfile.ZIP_DEFLATED, True) as zf: 434 | print "Zipping diff to: " + args.out 435 | diff_all(zf, snap1, snap2) 436 | 437 | sys.exit(0) 438 | 439 | if __name__=="__main__": 440 | 441 | parser = argparse.ArgumentParser(description=u"SnapDiff (c) 2016 - Christien Rioux") 442 | parser.add_argument("-d", "--dir", type=unicode, action='append', default=[], 443 | help="Select filesystem directories to watch") 444 | parser.add_argument("-r", "--reg", type=unicode, action='append', default=[], 445 | help="Select registry hives/subkeys to watch") 446 | parser.add_argument("-o", "--out", type=unicode, default=u"snapdiff.zip", help="Name of output zipfile") 447 | parser.add_argument("-v", "--verbose", action="store_true", default=False, help="Print extra information about the process") 448 | parser.add_argument("--includedrive", action="store_true", default=False, help="Store drive letter in zipfile paths") 449 | parser.add_argument("--excludedir", type=unicode, action='append', default=[], help="Exclude regex patterns from filesystem") 450 | parser.add_argument("--excludereg", type=unicode, action='append', default=[], help="Exclude regex patterns from registry") 451 | 452 | parser.add_help = True 453 | args = parser.parse_args() 454 | 455 | if len(args.dir) == 0: 456 | args.dir = [u"C:\\"] 457 | if len(args.reg) == 0: 458 | args.reg = [u"HKEY_LOCAL_MACHINE"] 459 | if len(args.excludedir) == 0: 460 | args.excludedir = [ur"^C:\\ProgramData\\Package Cache.*", 461 | ur"^C:\\System\ Volume\ Information.*", 462 | ur"^C:\\Users.*", 463 | ur"^C:\\Documents\ and\ Settings.*", 464 | ur"^C:\\Windows\\Prefetch.*", 465 | ur"^C:\\Windows\\Installer.*", 466 | ur"^C:\\Windows\\Logs.*", 467 | ur"^C:\\Windows\\Servicing.*", 468 | ur"^C:\\Windows\\SoftwareDistribution.*"] 469 | if len(args.excludereg) == 0: 470 | args.excludereg = [ur"^HKEY_LOCAL_MACHINE\\COMPONENTS.*", 471 | ur"^HKEY_LOCAL_MACHINE\\Schema.*"] 472 | 473 | if len(args.dir) == 1 and (args.dir[0] == u"none" or args.dir[0] == u""): 474 | args.dir = [] 475 | if len(args.reg) == 1 and (args.reg[0] == u"none" or args.reg[0] == u""): 476 | args.reg = [] 477 | if len(args.excludedir) == 1 and (args.excludedir[0] == u"none" or args.excludedir[0] == u""): 478 | args.excludedir = [] 479 | if len(args.excludereg) == 1 and (args.excludereg[0] == u"none" or args.excludereg[0] == u""): 480 | args.excludereg = [] 481 | 482 | for exc in args.excludedir: 483 | re_excludedir.append(re.compile(exc, re.IGNORECASE)) 484 | for exc in args.excludereg: 485 | re_excludereg.append(re.compile(exc, re.IGNORECASE)) 486 | 487 | main() 488 | 489 | 490 | --------------------------------------------------------------------------------