-
Notifications
You must be signed in to change notification settings - Fork 0
/
parseUSBs.py
445 lines (393 loc) · 16.6 KB
/
parseUSBs.py
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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
#!/bin/python
# Registry parser, to extract USB connection artifacts from SYSTEM, SOFTWARE, and NTUSER.dat hives
# Author: Kathryn Hedley, khedley@khyrenz.com
# Copyright 2023 Kathryn Hedley, Khyrenz Ltd
# Uses regipy offline hive parser library from Martin G. Korman: https://github.com/mkorman90/regipy/tree/master/regipy
# Extracts from the following keys/values:
## SYSTEM\Select\Current -> to get CurrentControlSet
## SYSTEM\CurrentControlSet\Enum\USB
## SYSTEM\CurrentControlSet\Enum\USBSTOR
## SYSTEM\CurrentControlSet\Enum\SCSI
## SYSTEM\MountedDevices
## SOFTWARE\Microsoft\Windows Portable Devices\Devices
## SOFTWARE\Microsoft\Windows Search\VolumeInfoCache
## NTUSER.DAT\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders\Desktop
## NTUSER.DAT\Software\Microsoft\Windows\CurrentVersion\Explorer\MountPoints2
# Dependencies:
## pip3 install regipy
# Limitations:
## Only parses provided Registry hives; does not parse any other artefacts
## Will only replay transaction logs if they're in the same folder as the provided Hive
# Importing libraries
import sys, os
from datetime import datetime,timedelta
from regipy.registry import RegistryHive
from regipy.recovery import apply_transaction_logs
from regipy.utils import calculate_xor32_checksum
from binascii import hexlify
#Defining object for a USB device
class ExternalDevice:
# initialising new object to empty values
def __init__(self):
self.name = ""
self.iSerialNumber = ""
self.firstConnected = ""
self.lastConnected = ""
self.lastRemoved = ""
self.otherConnection = []
self.lastDriveLetter = ""
self.volumeName = ""
self.diskId = ""
self.userAccounts = []
# method to set other connection time
def addOtherConnection(self, khyoc):
self.otherConnection.append(khyoc)
# method to get other connection timestamps
def getOtherConnections(self):
return self.otherConnection
# method to set last drive letter
def setLastDriveLetter(self, khydl):
self.lastDriveLetter = khydl
# method to set volume name
def setVolumeName(self, khyvn):
self.volumeName = khyvn
# method to set disk ID
def setDiskId(self, khydi):
self.diskId = khydi
# method to get disk ID
def getDiskId(self):
return self.diskId
# method to add user account
def addUser(self, khyu):
self.userAccounts.append(khyu)
# method to get user accounts
def getUsers(self):
return self.userAccounts
# Function to display help info
def printHelp():
print('Usage: python3 parseUSBs.py <options>')
print('Options:')
print(' -h Print this help message')
print(' -s <SYSTEM hive> Parse this SYSTEM hive')
print(' -u <NTUSER.dat hive> Parse this NTUSER.DAT hive. This argument is optional & multiple can be provided.')
print(' If omitted, connections to user accounts won\'t be made')
print(' -v <drive letter> Parse this mounted volume')
print(' Use either this "-v" option or the individual hive options.')
print(' If this option is provided, "-s|-u|-w" options will be ignored')
print(' -w <SOFTWARE hive> Parse this SOFTWARE HIVE. This argument is optional.')
print(' If omitted, some drive letters and volume names may be missing in the output')
print(' -o <csv|keyval> Output to either CSV or key-value pair format. Default is key-value pairs')
print()
print('Example commands:')
print('python3 parseUSBs.py -s C:/Windows/System32/config/SYSTEM -w C:/Windows/System32/config/SOFTWARE -u C:/Users/user1/NTUSER.DAT -o csv')
print('python3 parseUSBs.py -s SYSTEM -w SOFTWARE -u NTUSER.DAT_user1 -u NTUSER.DAT_user2')
print('(In Windows CMD:) python3 parseUSBs.py -v F:')
print('(On WSL:) python3 parseUSBs.py -v /mnt/f')
print()
print('Copyright 2023 Kathryn Hedley, Khyrenz Ltd')
print()
# Function to convert Key Last Write timestamp to readable format
# Usage - convertWin64time(kusbstorkey.header.last_modified)
def convertWin64time(khyts):
return (datetime(1601, 1, 1) + timedelta(microseconds=(khyts//10))).isoformat()
# Function to get timestamp value (if present) as readable timestamp
def getTime(reg, regkey):
try:
khyconn = reg.get_key(regkey).get_value('(default)').isoformat()
except:
khyconn = ""
return khyconn
# Function to output parsed data as CSV
def outputCSV(dev):
print('Value:,Device Friendly Name,iSerialNumber,FirstConnected,LastConnected,LastRemoved,OtherConnections,LastDriveLetter,VolumeName,UserAccounts')
print('Key:,USBSTOR-FriendlyName,USBSTOR,USBSTOR-0064,USBSTOR-0066,USBSTOR-0067,SOFTWARE-VolumeInfoCache,MountedDevices/Windows Portable Devices,Windows Portable Devices,NTUSER-MountPoints2')
for khyd in dev:
uacc=""
for khyu in khyd.userAccounts:
if uacc == "":
uacc = khyu
else:
uacc += "|"+khyu
oconn=""
for khyocn in khyd.otherConnection:
if oconn == "":
oconn = khyocn
else:
oconn += "|"+khyocn
print(','+khyd.name+','+khyd.iSerialNumber+','+khyd.firstConnected+','+khyd.lastConnected+','+khyd.lastRemoved+','+oconn+','+khyd.lastDriveLetter+','+khyd.volumeName+','+uacc)
# Function to output parsed data as Key/Value pairs
def outputKV(dev):
for khyd in dev:
print("Device Friendly Name:", khyd.name)
print("iSerialNumber:", khyd.iSerialNumber)
print("First Connected:", khyd.firstConnected)
print("Last Connected:", khyd.lastConnected)
print("Last Removed:", khyd.lastRemoved)
for khyocn in khyd.otherConnection:
print("Other Connection:", khyocn)
print("Last Drive Letter:", khyd.lastDriveLetter)
print("Volume Name:", khyd.volumeName)
for khyu in khyd.userAccounts:
print("User Account:", khyu)
print()
# Function to check if iSerialNumber in array of ExternalDevice objects
def snInDevArray(ksn, kdevarr):
for khyd in kdevarr:
if ksn == khyd.iSerialNumber:
return True
return False
# Function to check for dirty Registry Hive
def is_dirty(khv):
if khv.header.primary_sequence_num != khv.header.secondary_sequence_num:
print(khv.name.split('\\')[-1] + " is dirty! Sequence numbers don't match; applying transaction logs...")
return True
chksum = calculate_xor32_checksum(khv._stream.read(508))
if khv.header.checksum != chksum:
print(khv.name.split('\\')[-1] + " is dirty! Checksum doesn't match; applying transaction logs...")
return True
print(khv.name.split('\\')[-1] + " is clean")
return False
# Function to replay transaction logs
# Uses regipy apply_transaction_logs(hive_path, primary_log_path, secondary_log_path=None, restored_hive_path=None, verbose=False)
def replay_logs(khvpath):
#Looking for log files in same path as hive
print("Looking for LOG files: "+khvpath+".LOG1 & "+khvpath+".LOG2 in same location as "+khvpath)
log1=log2=""
logsexist=False
if os.path.exists(khvpath+".LOG1"):
log1=khvpath+".LOG1"
logsexist=True
if os.path.exists(khvpath+".LOG2"):
log2=khvpath+".LOG2"
logsexist=True
if logsexist:
updatedhive=None
updatedhive, dirtypagecount = apply_transaction_logs(khvpath, log1, log2, updatedhive, False)
print("Updated hive created: "+updatedhive)
return RegistryHive(updatedhive)
else:
print("Log files not found - dirty hive is being processed")
return RegistryHive(khvpath)
### MAIN function ###
print("Registry parser, to extract USB connection artifacts from SYSTEM, SOFTWARE, and NTUSER.dat hives")
print("Author: Kathryn Hedley, khedley@khyrenz.com")
print("Copyright 2023 Kathryn Hedley, Khyrenz Ltd")
# Check & parse passed-in arguments
next=""
sysHive=""
swHive=""
userHives=[]
kmtvol=""
ntuflag=False
swflag=False
csvout=False
kvout=True
for karg in sys.argv:
if next == 'system':
sysHive=karg
next=""
if next == 'software':
swHive=karg
next=""
if next == 'ntuser':
userHives.append(karg)
next=""
if next == 'output':
if karg == "csv":
csvout=True
kvout=False
if next == 'volume':
kmtvol=karg
next=""
if karg == "-h":
printHelp()
sys.exit()
if karg == "-s":
next='system'
if karg == "-w":
next='software'
if karg == "-u":
next='ntuser'
if karg == "-o":
next='output'
if karg == "-v":
next='volume'
#if volume option is provided, find Registry hives
if kmtvol:
if not kmtvol.endswith("/"):
kmtvol = kmtvol + "/"
sysHive=kmtvol+"Windows/System32/config/SYSTEM"
swHive=kmtvol+"Windows/System32/config/SOFTWARE"
userHives=[]
if os.path.exists(kmtvol+"Users"):
userfolders = [f.path for f in os.scandir(kmtvol+"Users") if f.is_dir()]
for usrdir in userfolders:
userHives.append(usrdir+"/NTUSER.DAT")
# Checking hives exist & opening to extract keys & values
if os.path.isfile(sysHive):
SYSTEM = RegistryHive(sysHive)
#Checking if hive is dirty
if is_dirty(SYSTEM):
SYSTEM = replay_logs(sysHive)
else:
print("SYSTEM Hive '"+sysHive+" ' does not exist")
print()
printHelp()
sys.exit()
if os.path.isfile(swHive):
SOFTWARE = RegistryHive(swHive)
swflag=True
#Checking if hive is dirty
if is_dirty(SOFTWARE):
SOFTWARE = replay_logs(swHive)
else:
print("SOFTWARE Hive not being parsed")
NTUSER=[]
if not userHives:
print("User hives not being parsed")
for kuh in userHives:
if os.path.isfile(kuh) and not "Default" in kuh:
nthv=RegistryHive(kuh)
#Checking if hive is dirty
if is_dirty(nthv):
nthv = replay_logs(kuh)
#Appending hive to list
NTUSER.append(nthv)
ntuflag=True
#Checking if hive is dirty
#initialising empty array to store device values & removing empty value that's added
devices = []
# Getting currentcontrolset value
currentVal = SYSTEM.get_key('SYSTEM\\Select').get_value('Current')
khycurrentcontrolset = 'ControlSet00' + str(currentVal)
print("currentcontrolset identified as " + khycurrentcontrolset)
# Iterating over SYSTEM\currentcontrolset\Enum\USBSTOR key...
for kusbstorkey in SYSTEM.get_key("SYSTEM\\" + khycurrentcontrolset + "\\Enum\\USBSTOR").iter_subkeys():
for kusbstorsnkey in SYSTEM.get_key("SYSTEM\\" + khycurrentcontrolset + "\\Enum\\USBSTOR\\" + kusbstorkey.name).iter_subkeys():
newDev=ExternalDevice()
#Get device friendly name
newDev.name = kusbstorsnkey.get_value('FriendlyName')
#Get device serial number, removing all after the last '&' character, including the '&' itself
amp = kusbstorsnkey.name.find('&', 2)
remove = len(kusbstorsnkey.name)-amp
if amp > 0:
newDev.iSerialNumber = kusbstorsnkey.name[:-remove]
else:
newDev.iSerialNumber = kusbstorsnkey.name
#Get device timestamps (if present)
newDev.firstConnected = getTime(SYSTEM, "SYSTEM\\" + khycurrentcontrolset + "\\Enum\\USBSTOR\\" + kusbstorkey.name + "\\" + kusbstorsnkey.name + "\\Properties\\{83da6326-97a6-4088-9453-a1923f573b29}\\0064")
newDev.lastConnected = getTime(SYSTEM, "SYSTEM\\" + khycurrentcontrolset + "\\Enum\\USBSTOR\\" + kusbstorkey.name + "\\" + kusbstorsnkey.name + "\\Properties\\{83da6326-97a6-4088-9453-a1923f573b29}\\0066")
newDev.lastRemoved = getTime(SYSTEM, "SYSTEM\\" + khycurrentcontrolset + "\\Enum\\USBSTOR\\" + kusbstorkey.name + "\\" + kusbstorsnkey.name + "\\Properties\\{83da6326-97a6-4088-9453-a1923f573b29}\\0067")
#Adding device to array if serial number not blank
if newDev.iSerialNumber:
devices.append(newDev)
# Iterating over SYSTEM\currentcontrolset\Enum\USB key looking for SCSI devices...
for kusbkey in SYSTEM.get_key("SYSTEM\\" + khycurrentcontrolset + "\\Enum\\USB").iter_subkeys():
for kusbsubkey in SYSTEM.get_key("SYSTEM\\" + khycurrentcontrolset + "\\Enum\\USB\\" + kusbkey.name).iter_subkeys():
if kusbsubkey.name.startswith('MSFT30'):
#SCSI device!
khyzDev=ExternalDevice()
#Set iSerialNumber
khyzDev.iSerialNumber = kusbsubkey.name[6:]
#Get ParentIdPrefix to map to SCSI key
kdevParentId = kusbsubkey.get_value('ParentIdPrefix')
# Iterating over SYSTEM\currentcontrolset\Enum\SCSI key...
for kscsikey in SYSTEM.get_key("SYSTEM\\" + khycurrentcontrolset + "\\Enum\\SCSI").iter_subkeys():
for kscsisubkey in SYSTEM.get_key("SYSTEM\\" + khycurrentcontrolset + "\\Enum\\SCSI\\" + kscsikey.name).iter_subkeys():
#Only adding if has Parent ID - e.g. vmware devices can be added here without a Parent ID - can't link to SCSI key
if kdevParentId is not None and kscsisubkey.name.startswith(kdevParentId):
#Get device friendly name
khyzDev.name = kscsisubkey.get_value('FriendlyName')
#Get Disk ID to map to Volume name
khyzDev.setDiskId(SYSTEM.get_key("SYSTEM\\" + khycurrentcontrolset + "\\Enum\\SCSI\\" + kscsikey.name + "\\" + kscsisubkey.name + "\\Device Parameters\\Partmgr").get_value('DiskId'))
#Get device timestamps (if present)
khyzDev.firstConnected = getTime(SYSTEM, "SYSTEM\\" + khycurrentcontrolset + "\\Enum\\SCSI\\" + kscsikey.name + "\\" + kscsisubkey.name + "\\Properties\\{83da6326-97a6-4088-9453-a1923f573b29}\\0064")
khyzDev.lastConnected = getTime(SYSTEM, "SYSTEM\\" + khycurrentcontrolset + "\\Enum\\SCSI\\" + kscsikey.name + "\\" + kscsisubkey.name + "\\Properties\\{83da6326-97a6-4088-9453-a1923f573b29}\\0066")
khyzDev.lastRemoved = getTime(SYSTEM, "SYSTEM\\" + khycurrentcontrolset + "\\Enum\\SCSI\\" + kscsikey.name + "\\" + kscsisubkey.name + "\\Properties\\{83da6326-97a6-4088-9453-a1923f573b29}\\0067")
#Adding device to array if serial number is not blank & not already in array
if khyzDev.iSerialNumber and not snInDevArray(khyzDev.iSerialNumber, devices):
devices.append(khyzDev)
# Iterating over SYSTEM\MountedDevices key to determine last mounted drive letters...
for kmdval in SYSTEM.get_key("SYSTEM\MountedDevices").get_values():
if kmdval.name.startswith('\DosDevices\\'):
try:
khexmd=hexlify(kmdval.value)
for d in devices:
khexsn=bytes(d.iSerialNumber.encode('utf-16le').hex(), 'utf8')
if khexsn in khexmd:
#Last drive letter found - add to devices info
d.setLastDriveLetter(kmdval.name[-2:]+'\\')
except:
#empty value
continue
#Extracting disk GUID values to search NTUSER hive, only if NTUSER.dat hive provided & valid
if ntuflag:
if kmdval.name.startswith('\??\Volume{'):
try:
khexmd=hexlify(kmdval.value)
for d in devices:
khexsn=bytes(d.iSerialNumber.encode('utf-16le').hex(), 'utf8')
if khexsn in khexmd:
#Disk GUID found - compare against NTUSER hive
diskGuid=kmdval.name[-38:]
#Iterating NTUSER.DAT hives
for NTU in NTUSER:
#Getting user account name from NTUSER.DAT\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders\Desktop
kusername=NTU.get_key('NTUSER.DAT\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders').get_value('Desktop')
#Output is of format C:\Users\<user>\Desktop -> extracting username
kusername=kusername.split('\\')[2]
#Checking NTUSER.DAT\Software\Microsoft\Windows\CurrentVersion\Explorer\MountPoints2 for disk GUID
for khymp in NTU.get_key('NTUSER.DAT\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\MountPoints2').iter_subkeys():
if khymp.name == diskGuid:
d.addUser(kusername)
break
except:
#empty value
continue
# Iterating over SOFTWARE\Microsoft\Windows Portable Devices\Devices to determine volume name or last drive letter...
for kwpdkey in SOFTWARE.get_key("SOFTWARE\\Microsoft\\Windows Portable Devices\\Devices").iter_subkeys():
for kdev in devices:
if kdev.iSerialNumber.lower() in kwpdkey.name.lower():
#Match to USB device in array
volName = kwpdkey.get_value('FriendlyName')
if ":\\" in volName:
#Drive letter, not volume name - add to devices info if not already added
if kdev.lastDriveLetter == "":
kdev.setLastDriveLetter(volName)
else: #Volume name
kdev.setVolumeName(volName)
elif kdev.getDiskId().lower() and kdev.getDiskId().lower() in kwpdkey.name.lower():
#Match to USB device on Disk ID (SCSI)
volName = kwpdkey.get_value('FriendlyName')
if ":\\" in volName:
#Drive letter, not volume name - add to devices info if not already added
if kdev.lastDriveLetter == "":
kdev.setLastDriveLetter(volName)
else: #Volume name
kdev.setVolumeName(volName)
# Iterating over SOFTWARE\Microsoft\Windows Search\VolumeInfoCache to try & match up drive letter with known volume name...
for kvickey in SOFTWARE.get_key("SOFTWARE\\Microsoft\\Windows Search\\VolumeInfoCache").iter_subkeys():
#Get Drive Letter & Volume name for device
kdletter = kvickey.name + '\\'
kvname = kvickey.get_value('VolumeLabel')
#Getting another potential connection time for device
klwtime = convertWin64time(kvickey.header.last_modified)
#Attempt to link on volume name to assign drive letter & other connection time
for kdv in devices:
if kdv.volumeName == kvname:
if kdv.lastDriveLetter == "":
kdv.setLastDriveLetter(kdletter)
#Only adding other connection time to list if not already present
exists=False
for c in d.getOtherConnections():
if c == klwtime:
exists=True
if not exists:
d.addOtherConnection(klwtime)
break
#Print output in CSV or key-value pair format
print()
if csvout:
outputCSV(devices)
if kvout:
outputKV(devices)