├── LocalShellExtParse.py └── README.md /LocalShellExtParse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ####################################################################### 3 | ## 4 | ## Local Shell Extension Parser 5 | ## 6 | ## Build a timeline of the first load time for all 7 | ## shell extensions loaded by the user. 8 | ## 9 | ## List all shell extensions installed only for the current user. 10 | ## 11 | ####################################################################### 12 | 13 | 14 | import datetime 15 | import struct 16 | import operator 17 | import sys 18 | import argparse 19 | 20 | try: 21 | import hivex 22 | except: 23 | print >>sys.stderr, 'Error - Please ensure you install the Hivex library, part of libguestfs, before running this script (http://libguestfs.org/).' 24 | sys.exit(1) 25 | 26 | def getFiletime(int_time): 27 | """ 28 | Returns int64 time as datetime 29 | """ 30 | microseconds = int_time / 10 31 | seconds, microseconds = divmod(microseconds, 1000000) 32 | days, seconds = divmod(seconds, 86400) 33 | return datetime.datetime(1601, 1, 1) + datetime.timedelta(days, seconds, microseconds) 34 | 35 | def getCacheExtList(dat_location): 36 | """ 37 | Parse Shell Extensions in HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Shell Extensions\Cached 38 | 39 | dat_location String: path to NTUSER.DAT file 40 | 41 | Return Dictionary: f(CLSID) = timestamp 42 | """ 43 | ext_list={} 44 | 45 | try: 46 | h = hivex.Hivex(dat_location) 47 | except: 48 | print >>sys.stderr, 'Error - Unable to open supplied NTUSER.DAT file' 49 | sys.exit(1) 50 | 51 | key = h.root() 52 | key = h.node_get_child(key,"Software") 53 | key = h.node_get_child(key,"Microsoft") 54 | key = h.node_get_child(key,"Windows") 55 | key = h.node_get_child(key,"CurrentVersion") 56 | key = h.node_get_child(key,"Shell Extensions") 57 | key = h.node_get_child(key,"Cached") 58 | 59 | cached_values = h.node_values(key) 60 | for entry in cached_values: 61 | #parse the Shell Extension CLSID from the entry 62 | entry_name = h.value_key(entry) 63 | extension_CLSID = entry_name.split(' ')[0] 64 | 65 | #parse the first load time from the entry 66 | entry_value = h.value_value(entry)[1] 67 | if len(entry_value) == 16: 68 | bin_time = entry_value[8:] 69 | int_time = struct.unpack('>sys.stderr, 'Error - Unable to parse timestamp value from Cache entry: %s' % entry_name 72 | continue 73 | ext_list[extension_CLSID] = int_time 74 | return ext_list 75 | 76 | def getUserExtList(dat_location, loaded_ext): 77 | """ 78 | Find all Shell Extensions from input list that are located in in HKEY_CURRENT_USER\Software\Classes\CLSID 79 | 80 | dat_location String: path to UsrClass.dat file 81 | 82 | loaded_ext Dictonary: f(CLSID)=timestamp. Each key is a string representing a Shell Extension CLSID that has been loaded. 83 | 84 | Return Dictionary: F(CLSID) = "path to Extension Handler DLL" 85 | """ 86 | 87 | ext_list={} 88 | 89 | try: 90 | h = hivex.Hivex(dat_location) 91 | except: 92 | print >>sys.stderr, 'Error - Unable to open supplied UsrClass.dat file' 93 | sys.exit(1) 94 | 95 | key = h.root() 96 | key = h.node_get_child(key,"CLSID") 97 | 98 | for ext_key in loaded_ext.keys(): 99 | try: 100 | tmp_key = h.node_get_child(key, ext_key) 101 | tmp_key = h.node_get_child(tmp_key,'InprocServer32') 102 | val = h.node_get_value(tmp_key,'') 103 | ext_path = h.value_string(val) 104 | ext_list[ext_key] = ext_path 105 | except: 106 | continue 107 | return ext_list 108 | 109 | 110 | 111 | 112 | def main(): 113 | desc=''' 114 | This script can be used to parse the first load time for all Shell Extensions loaded by a user. 115 | It can also prase out Shell Extensions that have been installed only for the user. 116 | This will catch malware persistence mechanisms that rely on per-user installed Shell Extensions. 117 | ''' 118 | parser = argparse.ArgumentParser(description=desc) 119 | parser.add_argument('--ntuser', action="store", dest="ntuser_dat",help="NTUSER.DAT file to parse") 120 | parser.add_argument('--usrclass', action="store", dest="usrclass_dat",help="UsrClass file to parse") 121 | parser.add_argument('-c','--cached',dest="cached_only",action='store_true',default=False,help="If you only want to get a timeline for the first load Shell Extensions. You only need to supply NTUSER.DAT with this option.") 122 | args = parser.parse_args() 123 | 124 | if args.cached_only: 125 | if not args.ntuser_dat: 126 | print >>sys.stderr, 'Error - You must supply an NTUSER.DAT file.\n\n' 127 | parser.print_help() 128 | sys.exit(1) 129 | else: 130 | cached_ext_list = getCacheExtList(args.ntuser_dat) 131 | else: 132 | if not args.ntuser_dat: 133 | print >>sys.stderr, 'Error - You must supply an NTUSER.DAT file.\n\n' 134 | parser.print_help() 135 | sys.exit(1) 136 | elif not args.usrclass_dat: 137 | print >>sys.stderr, 'Error - You must supply an UsrClass3.dat file.\n\n' 138 | parser.print_help() 139 | sys.exit(1) 140 | else: 141 | cached_ext_list = getCacheExtList(args.ntuser_dat) 142 | local_ext_list = getUserExtList(args.usrclass_dat, cached_ext_list) 143 | 144 | #print the results 145 | #print a chronological list of all loaded Shell Extensions 146 | print "\n\n============ Shell Extensions First Load Times ============\n" 147 | sorted_ext_list = sorted(cached_ext_list.items(), key=operator.itemgetter(1)) 148 | for entry in sorted_ext_list: 149 | file_time = getFiletime(entry[1]) 150 | print entry[0]+": "+ format(file_time, '%a, %d %B %Y %H:%M:%S %Z') 151 | 152 | if not args.cached_only: 153 | #print the Shell Extensions that have been found local 154 | print "\n\n============ Shell Extensions Installed for Current User ============\n" 155 | for entry in local_ext_list.keys(): 156 | print entry +': '+ local_ext_list[entry] 157 | 158 | 159 | 160 | if __name__ == '__main__': 161 | main() 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LocalShellExtParse # 2 | LocalShellExtParse is an "offline" forensics script that will generating a “first loaded” timeline for Shell Extensions and identifying Shell Extensions that are only installed for the current user. This is a useful way to identify malware that is using a Shell Extension as a persistence mechanism. 3 | 4 | More information can be found in this blog post: http://herrcore.blogspot.com/2015/06/malware-persistence-with.html 5 | 6 | **NOTE**: Regripper has been updated to detect this persistence mechanism. Full details are here: http://windowsir.blogspot.ca/2015/06/links.html. 7 | 8 | ## Dependencies ## 9 | The script requires you to have installed Hivex with the the Python bindings (https://github.com/libguestfs/hivex). Hivex is part of the libguestfs suite of tools. 10 | 11 | Installation for Linux can be found here: http://libguestfs.org/ 12 | 13 | If you are using OSX you can use the brew tap here: https://github.com/anarchivist/homebrew-forensics 14 | 15 | ## Data Collection ## 16 | The script parses entries from the *NTUSER.DAT* and *UsrClass.DAT* files. To use the tool you will first need to collect the files from the host that you want to analyze. I prefer FTK Imager (http://accessdata.com/product-download) but any tool that allows you to carve system files will work. 17 | 18 | Everyone knows that NTUSER.DAT is located in %userprofile% but UsrClass.DAT may be less well understood. When viewing a live registry under HKEY_CURRENT_USER\Software\ there is a key called “CLSID” that shows all the CLSIDs for the current user. The data for this key is not stored in NTUSER.DAT it’s actually stored in the UsrClass.DAT file located in; `%userprofile%\AppData\Local\\Microsoft\Windows\UsrClass.dat` 19 | 20 | ## Data Parsing ## 21 | Once the files have been collected the can be parsed by LocalShellExtParse.py to produce; 22 | * a timeline of the first time each Shell Extension has been loaded by the user 23 | * a list of all Shell Extensions that have been loaded by the user and are only installed for that user. 24 | 25 | ## Using The Script ## 26 | By default the script will attempt to parse out both the first load timeline and the Current User Shell Extensions. If run in this mode both the UsrClass.dat and NTUSER.DAT files must be passed as arguments. 27 | 28 | `python LocalShellExtParse.py --ntuser NTUSER.DAT --usrclass UsrClass.dat` 29 | 30 | The script also supports a *--cached* option that only parses the Shell Extensions' first load times. If run in this mode only the NTUSER.DAT file needs to be supplied. 31 | 32 | `python LocalShellExtParse.py --cached --ntuser NTUSER.DAT` 33 | 34 | --------------------------------------------------------------------------------