From 3676858bc0962db4794900511fc360ce97e4df52 Mon Sep 17 00:00:00 2001 From: speyrefitte Date: Thu, 18 Apr 2019 14:06:06 +0200 Subject: [PATCH 1/4] Add shimcache parser --- regrippy/plugins/shimcache.py | 24 ++ regrippy/thirdparty/ShimCacheParser.py | 541 +++++++++++++++++++++++++ regrippy/thirdparty/__init__.py | 0 setup.py | 8 +- tests/test_shimcache.py | 29 ++ 5 files changed, 600 insertions(+), 2 deletions(-) create mode 100644 regrippy/plugins/shimcache.py create mode 100755 regrippy/thirdparty/ShimCacheParser.py create mode 100644 regrippy/thirdparty/__init__.py create mode 100644 tests/test_shimcache.py diff --git a/regrippy/plugins/shimcache.py b/regrippy/plugins/shimcache.py new file mode 100644 index 0000000..b0c7371 --- /dev/null +++ b/regrippy/plugins/shimcache.py @@ -0,0 +1,24 @@ +from regrippy import BasePlugin, PluginResult +from regrippy.thirdparty.ShimCacheParser import read_cache + + +class Plugin(BasePlugin): + """Parse shim cache to show all executed binaries on machine""" + __REGHIVE__ = "SYSTEM" + + def run(self): + key = self.open_key(self.get_currentcontrolset_path() + r"\Control\Session Manager\AppCompatCache") or \ + self.open_key(self.get_currentcontrolset_path() + r"\Control\Session Manager\AppCompatibility") + + if not key: + return + + for entry in read_cache(key.value("AppCompatCache").value()): + res = PluginResult(key=key, value=None) + res.custom["date"] = entry[0] + res.custom["path"] = entry[2] + yield res + + def display_human(self, result): + print(result.custom["date"] + "\t" + result.custom["path"]) + diff --git a/regrippy/thirdparty/ShimCacheParser.py b/regrippy/thirdparty/ShimCacheParser.py new file mode 100755 index 0000000..a511e3b --- /dev/null +++ b/regrippy/thirdparty/ShimCacheParser.py @@ -0,0 +1,541 @@ +#!/usr/bin/env python +# ShimCacheParser.py +# +# Andrew Davis, andrew.davis@mandiant.com +# Copyright 2012 Mandiant +# +# Mandiant licenses this file to you under the Apache License, Version +# 2.0 (the "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. +# +# Identifies and parses Application Compatibility Shim Cache entries for forensic data. + +import struct +import datetime +import codecs +from io import BytesIO +from csv import writer +import logging + +logging.basicConfig() +logger = logging.getLogger("shimcacheparser") +logger.setLevel("ERROR") + +# Values used by Windows 5.2 and 6.0 (Server 2003 through Vista/Server 2008) +CACHE_MAGIC_NT5_2 = 0xbadc0ffe +CACHE_HEADER_SIZE_NT5_2 = 0x8 +NT5_2_ENTRY_SIZE32 = 0x18 +NT5_2_ENTRY_SIZE64 = 0x20 + +# Values used by Windows 6.1 (Win7 and Server 2008 R2) +CACHE_MAGIC_NT6_1 = 0xbadc0fee +CACHE_HEADER_SIZE_NT6_1 = 0x80 +NT6_1_ENTRY_SIZE32 = 0x20 +NT6_1_ENTRY_SIZE64 = 0x30 +CSRSS_FLAG = 0x2 + +# Values used by Windows 5.1 (WinXP 32-bit) +WINXP_MAGIC32 = 0xdeadbeef +WINXP_HEADER_SIZE32 = 0x190 +WINXP_ENTRY_SIZE32 = 0x228 +MAX_PATH = 520 + +# Values used by Windows 8 +WIN8_STATS_SIZE = 0x80 +WIN8_MAGIC = b'00ts' + +# Magic value used by Windows 8.1 +WIN81_MAGIC = b'10ts' + +# Values used by Windows 10 +WIN10_STATS_SIZE = 0x30 +WIN10_CREATORS_STATS_SIZE = 0x34 +WIN10_MAGIC = b'10ts' +CACHE_HEADER_SIZE_NT6_4 = 0x30 +CACHE_MAGIC_NT6_4 = 0x30 + +bad_entry_data = 'N/A' +g_verbose = False +g_usebom = False +output_header = ["Last Modified", "Last Update", "Path", "File Size", "Exec Flag"] + +# Date Formats +DATE_MDY = "%m/%d/%y %H:%M:%S" +DATE_ISO = "%Y-%m-%d %H:%M:%S" +g_timeformat = DATE_ISO + +# Shim Cache format used by Windows 5.2 and 6.0 (Server 2003 through Vista/Server 2008) +class CacheEntryNt5(object): + + def __init__(self, is32bit, data=None): + + self.is32bit = is32bit + if data != None: + self.update(data) + + def update(self, data): + + if self.is32bit: + entry = struct.unpack('<2H 3L 2L', data) + else: + entry = struct.unpack('<2H 4x Q 2L 2L', data) + self.wLength = entry[0] + self.wMaximumLength = entry[1] + self.Offset = entry[2] + self.dwLowDateTime = entry[3] + self.dwHighDateTime = entry[4] + self.dwFileSizeLow = entry[5] + self.dwFileSizeHigh = entry[6] + + def size(self): + + if self.is32bit: + return NT5_2_ENTRY_SIZE32 + else: + return NT5_2_ENTRY_SIZE64 + +# Shim Cache format used by Windows 6.1 (Win7 through Server 2008 R2) +class CacheEntryNt6(object): + + def __init__(self, is32bit, data=None): + + self.is32bit = is32bit + if data != None: + self.update(data) + + def update(self, data): + + if self.is32bit: + entry = struct.unpack('<2H 7L', data) + else: + entry = struct.unpack('<2H 4x Q 4L 2Q', data) + self.wLength = entry[0] + self.wMaximumLength = entry[1] + self.Offset = entry[2] + self.dwLowDateTime = entry[3] + self.dwHighDateTime = entry[4] + self.FileFlags = entry[5] + self.Flags = entry[6] + self.BlobSize = entry[7] + self.BlobOffset = entry[8] + + def size(self): + + if self.is32bit: + return NT6_1_ENTRY_SIZE32 + else: + return NT6_1_ENTRY_SIZE64 + +# Convert FILETIME to datetime. +# Based on http://code.activestate.com/recipes/511425-filetime-to-datetime/ +def convert_filetime(dwLowDateTime, dwHighDateTime): + + try: + date = datetime.datetime(1601, 1, 1, 0, 0, 0) + temp_time = dwHighDateTime + temp_time <<= 32 + temp_time |= dwLowDateTime + return date + datetime.timedelta(microseconds=temp_time/10) + except OverflowError: + return None + +# Return a unique list while preserving ordering. +def unique_list(li): + + ret_list = [] + for entry in li: + if entry not in ret_list: + ret_list.append(entry) + return ret_list + +# Write the Log. +def write_it(rows, outfile=None): + + try: + + if not rows: + logger.error("[-] No data to write...") + return + + if not outfile: + for row in rows: + print(" ".join(["%s"%x for x in row])) + else: + logger.debug("[+] Writing output to %s..."%outfile) + try: + f = open(outfile, 'wb') + if g_usebom: + f.write(codecs.BOM_UTF8) + csv_writer = writer(f, delimiter=',') + csv_writer.writerows(rows) + f.close() + except IOError as err: + logger.error("[-] Error writing output file: %s" % str(err)) + return + + except UnicodeEncodeError as err: + logger.error("[-] Error writing output file: %s" % str(err)) + return + +# Read the Shim Cache format, return a list of last modified dates/paths. +def read_cache(cachebin): + + if len(cachebin) < 16: + # Data size less than minimum header size. + return None + + try: + # Get the format type + magic = struct.unpack(" WIN8_STATS_SIZE and cachebin[WIN8_STATS_SIZE:WIN8_STATS_SIZE+4] == WIN8_MAGIC: + logger.debug("[+] Found Windows 8/2k12 Apphelp Cache data...") + return read_win8_entries(cachebin, WIN8_MAGIC) + + # Windows 8.1 will use a different magic dword, check for it + elif len(cachebin) > WIN8_STATS_SIZE and cachebin[WIN8_STATS_SIZE:WIN8_STATS_SIZE+4] == WIN81_MAGIC: + logger.debug("[+] Found Windows 8.1 Apphelp Cache data...") + return read_win8_entries(cachebin, WIN81_MAGIC) + + # Windows 10 will use a different magic dword, check for it + elif len(cachebin) > WIN10_STATS_SIZE and cachebin[WIN10_STATS_SIZE:WIN10_STATS_SIZE+4] == WIN10_MAGIC: + logger.debug("[+] Found Windows 10 Apphelp Cache data...") + return read_win10_entries(cachebin, WIN10_MAGIC) + + # Windows 10 Creators Update will use a different STATS_SIZE, account for it + elif len(cachebin) > WIN10_CREATORS_STATS_SIZE and cachebin[WIN10_CREATORS_STATS_SIZE:WIN10_CREATORS_STATS_SIZE+4] == WIN10_MAGIC: + logger.debug("[+] Found Windows 10 Creators Update Apphelp Cache data...") + return read_win10_entries(cachebin, WIN10_MAGIC, creators_update=True) + + else: + logger.error("[-] Got an unrecognized magic value of 0x%x... bailing" % magic) + return None + + except (RuntimeError, TypeError, NameError) as err: + logger.error("[-] Error reading Shim Cache data: %s" % err) + return None + +# Read Windows 8/2k12/8.1 Apphelp Cache entry formats. +def read_win8_entries(bin_data, ver_magic): + offset = 0 + entry_meta_len = 12 + entry_list = [] + + # Skip past the stats in the header + cache_data = bin_data[WIN8_STATS_SIZE:] + + data = BytesIO(cache_data) + while data.tell() < len(cache_data): + header = data.read(entry_meta_len) + # Read in the entry metadata + # Note: the crc32 hash is of the cache entry data + magic, crc32_hash, entry_len = struct.unpack('<4sLL', header) + + # Check the magic tag + if magic != ver_magic: + raise Exception("Invalid version magic tag found: 0x%x" % struct.unpack(" 0: + # Just skip past the package data if present (for now) + entry_data.seek(package_len, 1) + + # Read the remaining entry data + flags, unk_1, low_datetime, high_datetime, unk_2 = struct.unpack(' 3: + contains_file_size = True + break + + # Now grab all the data in the value. + for offset in range(CACHE_HEADER_SIZE_NT5_2, (num_entries * entry_size) + CACHE_HEADER_SIZE_NT5_2, + entry_size): + + entry.update(bin_data[offset:offset+entry_size]) + + last_mod_date = convert_filetime(entry.dwLowDateTime, entry.dwHighDateTime) + try: + last_mod_date = last_mod_date.strftime(g_timeformat) + except ValueError: + last_mod_date = bad_entry_data + path = bin_data[entry.Offset:entry.Offset + entry.wLength].decode('utf-16le', 'replace') + + # It contains file size data. + if contains_file_size: + hit = [last_mod_date, 'N/A', path, str(entry.dwFileSizeLow), 'N/A'] + if hit not in entry_list: + entry_list.append(hit) + + # It contains flags. + else: + # Check the flag set in CSRSS + if (entry.dwFileSizeLow & CSRSS_FLAG): + exec_flag = 'True' + else: + exec_flag = 'False' + + hit = [last_mod_date, 'N/A', path, 'N/A', exec_flag] + if hit not in entry_list: + entry_list.append(hit) + + return entry_list + + except (RuntimeError, ValueError, NameError) as err: + logger.error("[-] Error reading Shim Cache data: %s..." % err) + return None + +# Read the Shim Cache Windows 7/2k8-R2 entry format, +# return a list of last modifed dates/paths. +def read_nt6_entries(bin_data, entry): + + try: + entry_list = [] + exec_flag = "" + entry_size = entry.size() + num_entries = struct.unpack(' 1.1.0' + ], + dependency_links=[ + 'git+https://github.com/williballenthin/python-registry#egg=python-registry-1.2.0' ], entry_points=generate_entry_points() ) diff --git a/tests/test_shimcache.py b/tests/test_shimcache.py new file mode 100644 index 0000000..f398db6 --- /dev/null +++ b/tests/test_shimcache.py @@ -0,0 +1,29 @@ +from Registry.Registry import RegBin + +from tests.reg_mock import RegistryValueMock +from .reg_mock import RegistryMock, RegistryKeyMock, LoggerMock +import pytest + +from regrippy.plugins.shimcache import Plugin as plugin + +_entry_win10_ = b"\x00" * 0x30 + b"10ts" + b"\x00\x00\x00\x00\x2a\x01\x00\x00\x1c\x00t\x00e\x00s\x00t\x00_\x00s\x00h\x00i\x00m\x00c\x00a\x00c\x00h\x00e\x00\x1f\xb1\xb8\x6f\xaf\xf5\xd4\x01" + +@pytest.fixture +def mock_reg(): + key = RegistryKeyMock.build("ControlSet001\\Control\\Session Manager\\AppCompatCache") + reg = RegistryMock("SYSTEM", "system", key.root()) + reg.set_ccs(1) + + val = RegistryValueMock("AppCompatCache", _entry_win10_, RegBin) + key.add_value(val) + + return reg + + +def test_shimcache(mock_reg): + p = plugin(mock_reg, LoggerMock(), "SYSTEM", "-") + + results = list(p.run()) + + assert (len(results) == 1), "There should only be 1 results" + assert (results[0].custom["path"] == "test_shimcache") From f17061b62ecc4420caa7333e50787d89be3c7f4d Mon Sep 17 00:00:00 2001 From: speyrefitte Date: Thu, 9 May 2019 10:24:58 +0200 Subject: [PATCH 2/4] update python-registry version --- setup.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.py b/setup.py index c362373..3ffe088 100644 --- a/setup.py +++ b/setup.py @@ -65,8 +65,5 @@ def generate_entry_points(): 'wheel', 'python-registry > 1.1.0' ], - dependency_links=[ - 'git+https://github.com/williballenthin/python-registry#egg=python-registry-1.2.0' - ], entry_points=generate_entry_points() ) From 83758f536df909543a5a2d897d9a44f1555307ca Mon Sep 17 00:00:00 2001 From: Simon Garrelou Date: Thu, 9 May 2019 11:19:32 +0200 Subject: [PATCH 3/4] Fix encoding bug + add mactime support --- regrippy/plugins/shimcache.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/regrippy/plugins/shimcache.py b/regrippy/plugins/shimcache.py index b0c7371..c9cb6cf 100644 --- a/regrippy/plugins/shimcache.py +++ b/regrippy/plugins/shimcache.py @@ -1,4 +1,6 @@ -from regrippy import BasePlugin, PluginResult +from datetime import datetime + +from regrippy import BasePlugin, PluginResult, mactime from regrippy.thirdparty.ShimCacheParser import read_cache @@ -16,9 +18,14 @@ def run(self): for entry in read_cache(key.value("AppCompatCache").value()): res = PluginResult(key=key, value=None) res.custom["date"] = entry[0] - res.custom["path"] = entry[2] + res.custom["path"] = entry[2].decode("utf8") yield res def display_human(self, result): print(result.custom["date"] + "\t" + result.custom["path"]) + def display_machine(self, result): + date = datetime.strptime(result.custom["date"], "%Y-%m-%d %H:%M:%S") + atime = int(date.timestamp()) + + print(mactime(name=result.custom["path"], mtime=result.mtime, atime=atime)) From cdfd235c3b007c2b9fb4fa2a6fcd351d49268d5a Mon Sep 17 00:00:00 2001 From: Simon Garrelou Date: Thu, 9 May 2019 12:41:43 +0200 Subject: [PATCH 4/4] Fix encoding issue: read_cache can return "str" or "bytes" depending on the OS version --- regrippy/plugins/shimcache.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/regrippy/plugins/shimcache.py b/regrippy/plugins/shimcache.py index c9cb6cf..83796f1 100644 --- a/regrippy/plugins/shimcache.py +++ b/regrippy/plugins/shimcache.py @@ -18,7 +18,10 @@ def run(self): for entry in read_cache(key.value("AppCompatCache").value()): res = PluginResult(key=key, value=None) res.custom["date"] = entry[0] - res.custom["path"] = entry[2].decode("utf8") + if type(entry[2]) == bytes: + res.custom["path"] = entry[2].decode("utf8") + else: + res.custom["path"] = entry[2] yield res def display_human(self, result):