Extracting data with the parse_values() function

The parse_values() function takes the list of GUID dictionaries as its input and uses struct to parse the binary data. As we've discussed, there are two types of UserAssist keys we will support: Windows XP and Windows 7. The following two tables break down the relevant data structures we will parse. Windows XP-based keys are 16 bytes in length and contain a Session ID, Count, and FILETIME timestamp:

Byte offset

Value

Object

0-3

Session ID

Integer

4-7

Count

Integer

8-15

FILETIME

Integer

 

Windows 7 artifacts are 72 bytes in length containing a session ID, count, focus count/time, and FILETIME timestamp:

Byte offset

Value

Object

0-3

Session ID

Integer

4-7

Count

Integer

8-11

Focus count

Integer

12-15

Focus time

Integer

16-59

???

N/A

60-67

FILETIME

Integer

68-71

???

N/A

 

On lines 143 through 146, we set up our function by instantiating the ua_type variable and logging execution status. This ua_type variable will be used to document which type of UserAssist value we're working with. On lines 148 and 149, we loop through each value in each dictionary to identify its type and parse it:

134 def parse_values(data):
135 """
136 The parse_values function uses struct to unpack the raw value
137 data from the UA key
138 :param data: A list containing dictionaries of UA
139 application data
140 :return: ua_type, based on the size of the raw data from
141 the dictionary values.
142 """
143 ua_type = -1
144 msg = 'Parsing UserAssist values.'
145 print('[+]', msg)
146 logging.info(msg)
147
148 for dictionary in data:
149 for v in dictionary.keys():

On lines 151 and 159, we use the len() function to identify the type of UserAssist key. For Windows XP-based data, we need to extract two 4-byte integers followed by an 8-byte integer. We also want to interpret this data in little endian using standard sizes. We accomplish this on line 152 with <2iq as the struct format string. The second argument we pass to the unpack method is the raw binary data for the particular key from the GUID dictionary:

150             # WinXP based UA keys are 16 bytes
151 if len(dictionary[v]) == 16:
152 raw = struct.unpack('<2iq', dictionary[v])
153 ua_type = 0
154 KEYS.append({'Name': get_name(v), 'Path': v,
155 'Session ID': raw[0], 'Count': raw[1],
156 'Last Used Date (UTC)': raw[2],
157 'Focus Time (ms)': '', 'Focus Count': ''})

The Windows 7-based data is slightly more complicated. There are bytes in the middle and end of the binary data that we are not interested in parsing and yet, because of the nature of struct, we must account for them in our format. The format string we use for this task is <4i44xq4x, which accounts for the four 4-byte integers, the 44 bytes of intervening space, the 8-byte integer, and the remaining 4 bytes we will ignore:

158             # Win7 based UA keys are 72 bytes
159 elif len(dictionary[v]) == 72:
160 raw = struct.unpack('<4i44xq4x', dictionary[v])
161 ua_type = 1
162 KEYS.append({'Name': get_name(v), 'Path': v,
163 'Session ID': raw[0], 'Count': raw[1],
164 'Last Used Date (UTC)': raw[4],
165 'Focus Time (ms)': raw[3],'Focus Count': raw[2]})

As we parse UserAssist records, we append them to the KEYS list for storage. When we append the parsed values, we wrap them in curly braces to create our inner dictionary object. We also call the get_name() function on the UserAssist value name to separate the executable from its path. Note that regardless of the type of UserAssist key, we still create the same keys in our dictionary. This will ensure that all our dictionaries have the same structure and will help streamline our CSV and XLSX output functions.

If a UserAssist value is not 16 or 72 bytes (which can happen), then that value is skipped and the user is notified of the name and size that was passed over. From our experience, these values were not forensically relevant, and so we decided to pass on them. On line 173, the UserAssist type is returned to the main() function:

166             else:
167 # If the key is not WinXP or Win7 based -- ignore.
168 msg = 'Ignoring {} value that is {} bytes'.format(
169 str(v), str(len(dictionary[v])))
170 print('[-]', msg)
171 logging.info(msg)
172 continue
173 return ua_type