Python 数字取证秘籍(三)
原文:
zh.annas-archive.org/md5/941c711b36df2129e5f7d215d3712f03译者:飞龙
第七章:基于日志的工件配方
本章涵盖了以下配方:
-
关于时间
-
使用 RegEx 解析 IIS weblogs
-
去探险
-
解释每日日志
-
将
daily.out解析添加到 Axiom -
使用 YARA 扫描指标
介绍
这些天,遇到配备某种形式的事件或活动监控软件的现代系统并不罕见。这种软件可能被实施以协助安全、调试或合规要求。无论情况如何,这些宝贵的信息宝库通常被广泛利用于各种类型的网络调查。日志分析的一个常见问题是需要筛选出感兴趣的子集所需的大量数据。通过本章的配方,我们将探索具有很大证据价值的各种日志,并演示快速处理和审查它们的方法。具体来说,我们将涵盖:
-
将不同的时间戳格式(UNIX、FILETIME 等)转换为人类可读的格式
-
解析来自 IIS 平台的 Web 服务器访问日志
-
使用 Splunk 的 Python API 摄取、查询和导出日志
-
从 macOS 的
daily.out日志中提取驱动器使用信息 -
从 Axiom 执行我们的
daily.out日志解析器 -
使用 YARA 规则识别感兴趣的文件的奖励配方
访问www.packtpub.com/books/conte…下载本章的代码包。
关于时间
配方难度:简单
Python 版本:2.7 或 3.5
操作系统:任何
任何良好日志文件的一个重要元素是时间戳。这个值传达了日志中记录的活动或事件的日期和时间。这些日期值可以以许多格式出现,并且可以表示为数字或十六进制值。除了日志之外,不同的文件和工件以不同的方式存储日期,即使数据类型保持不变。一个常见的区分因素是纪元值,即格式从中计算时间的日期。一个常见的纪元是 1970 年 1 月 1 日,尽管其他格式从 1601 年 1 月 1 日开始计算。在不同格式之间不同的因素是用于计数的间隔。虽然常见的是看到以秒或毫秒计数的格式,但有些格式计算时间块,比如自纪元以来的 100 纳秒数。因此,这里开发的配方可以接受原始日期时间输入,并将格式化的时间戳作为其输出。
入门
此脚本中使用的所有库都包含在 Python 的标准库中。
如何做...
为了在 Python 中解释常见的日期格式,我们执行以下操作:
-
设置参数以获取原始日期值、日期来源和数据类型。
-
开发一个为不同日期格式提供通用接口的类。
-
支持处理 Unix 纪元值和 Microsoft 的
FILETIME日期。
它是如何工作的...
我们首先导入用于处理参数和解析日期的库。具体来说,我们需要从datetime库中导入datetime类来读取原始日期值,以及timedelta类来指定时间戳偏移量。
from __future__ import print_function
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
from datetime import datetime as dt
from datetime import timedelta
这个配方的命令行处理程序接受三个位置参数,date_value、source和type,分别代表要处理的日期值、日期值的来源(UNIX、FILETIME 等)和类型(整数或十六进制值)。我们使用choices关键字来限制用户可以提供的选项。请注意,源参数使用自定义的get_supported_formats()函数,而不是预定义的受支持日期格式列表。然后,我们获取这些参数并初始化ParseDate类的一个实例,并调用run()方法来处理转换过程,然后将其timestamp属性打印到控制台。
if __name__ == '__main__':
parser = ArgumentParser(
description=__description__,
formatter_class=ArgumentDefaultsHelpFormatter,
epilog="Developed by {} on {}".format(
", ".join(__authors__), __date__)
)
parser.add_argument("date_value", help="Raw date value to parse")
parser.add_argument("source", help="Source format of date",
choices=ParseDate.get_supported_formats())
parser.add_argument("type", help="Data type of input value",
choices=('number', 'hex'), default='int')
args = parser.parse_args()
date_parser = ParseDate(args.date_value, args.source, args.type)
date_parser.run()
print(date_parser.timestamp)
让我们看看ParseDate类是如何工作的。通过使用一个类,我们可以轻松地扩展和在其他脚本中实现这段代码。从命令行参数中,我们接受日期值、日期源和值类型的参数。这些值和输出变量timestamp在__init__方法中被定义:
class ParseDate(object):
def __init__(self, date_value, source, data_type):
self.date_value = date_value
self.source = source
self.data_type = data_type
self.timestamp = None
run()方法是控制器,很像我们许多食谱中的main()函数,并根据日期源选择要调用的正确方法。这使我们能够轻松扩展类并轻松添加新的支持。在这个版本中,我们只支持三种日期类型:Unix 纪元秒,Unix 纪元毫秒和 Microsoft 的 FILETIME。为了减少我们需要编写的方法数量,我们将设计 Unix 纪元方法来处理秒和毫秒格式的时间戳。
def run(self):
if self.source == 'unix-epoch':
self.parse_unix_epoch()
elif self.source == 'unix-epoch-ms':
self.parse_unix_epoch(True)
elif self.source == 'windows-filetime':
self.parse_windows_filetime()
为了帮助未来想要使用这个库的人,我们添加了一个查看支持的格式的方法。通过使用@classmethod装饰器,我们可以在不需要先初始化类的情况下公开这个函数。这就是我们可以在命令行处理程序中使用get_supported_formats()方法的原因。只需记住在添加新功能时更新它!
@classmethod
def get_supported_formats(cls):
return ['unix-epoch', 'unix-epoch-ms', 'windows-filetime']
parse_unix_epoch()方法处理处理 Unix 纪元时间。我们指定一个可选参数milliseconds,以在处理秒和毫秒值之间切换此方法。首先,我们必须确定数据类型是"hex"还是"number"。如果是"hex",我们将其转换为整数,如果是"number",我们将其转换为浮点数。如果我们不认识或不支持此方法的数据类型,比如string,我们向用户抛出错误并退出脚本。
在转换值后,我们评估是否应将其视为毫秒值,如果是,则在进一步处理之前将其除以1,000。随后,我们使用datetime类的fromtimestamp()方法将数字转换为datetime对象。最后,我们将这个日期格式化为人类可读的格式,并将这个字符串存储在timestamp属性中。
def parse_unix_epoch(self, milliseconds=False):
if self.data_type == 'hex':
conv_value = int(self.date_value)
if milliseconds:
conv_value = conv_value / 1000.0
elif self.data_type == 'number':
conv_value = float(self.date_value)
if milliseconds:
conv_value = conv_value / 1000.0
else:
print("Unsupported data type '{}' provided".format(
self.data_type))
sys.exit('1')
ts = dt.fromtimestamp(conv_value)
self.timestamp = ts.strftime('%Y-%m-%d %H:%M:%S.%f')
parse_windows_filetime()类方法处理FILETIME格式,通常存储为十六进制值。使用与之前相似的代码块,我们将"hex"或"number"值转换为 Python 对象,并对任何其他提供的格式引发错误。唯一的区别是在进一步处理之前,我们将日期值除以10而不是1,000。
在之前的方法中,datetime库处理了纪元偏移,这次我们需要单独处理这个偏移。使用timedelta类,我们指定毫秒值,并将其添加到代表 FILETIME 格式纪元的datetime对象中。现在得到的datetime对象已经准备好供我们格式化和输出给用户了:
def parse_windows_filetime(self):
if self.data_type == 'hex':
microseconds = int(self.date_value, 16) / 10.0
elif self.data_type == 'number':
microseconds = float(self.date_value) / 10
else:
print("Unsupported data type '{}' provided".format(
self.data_type))
sys.exit('1')
ts = dt(1601, 1, 1) + timedelta(microseconds=microseconds)
self.timestamp = ts.strftime('%Y-%m-%d %H:%M:%S.%f')
当我们运行这个脚本时,我们可以提供一个时间戳,并以易于阅读的格式查看转换后的值,如下所示:
还有更多...
这个脚本可以进一步改进。我们提供了一个或多个建议如下:
-
为其他类型的时间戳(OLE,WebKit 等)添加支持
-
通过
pytz添加时区支持 -
使用
dateutil处理难以阅读的日期格式
使用 RegEx 解析 IIS Web 日志
食谱难度:中等
Python 版本:3.5
操作系统:任何
来自 Web 服务器的日志对于生成用户统计信息非常有用,为我们提供了有关使用的设备和访问者的地理位置的深刻信息。它们还为寻找试图利用 Web 服务器或未经授权使用的用户的审查人员提供了澄清。虽然这些日志存储了重要的细节,但以一种不便于高效分析的方式进行。如果您尝试手动分析,字段名称被指定在文件顶部,并且在阅读文本文件时需要记住字段的顺序。幸运的是,有更好的方法。使用以下脚本,我们展示了如何遍历每一行,将值映射到字段,并创建一个正确显示结果的电子表格 - 使得快速分析数据集变得更加容易。
入门
此脚本中使用的所有库都包含在 Python 的标准库中。
如何做...
为了正确制作这个配方,我们需要采取以下步骤:
-
接受输入日志文件和输出 CSV 文件的参数。
-
为日志的每一列定义正则表达式模式。
-
遍历日志中的每一行,并以一种我们可以解析单独元素并处理带引号的空格字符的方式准备每一行。
-
验证并将每个值映射到其相应的列。
-
将映射的列和值写入电子表格报告。
它是如何工作的...
我们首先导入用于处理参数和日志记录的库,然后是我们需要解析和验证日志信息的内置库。这些包括re正则表达式库和shlex词法分析器库。我们还包括sys和csv来处理日志消息和报告的输出。我们通过调用getLogger()方法初始化了该配方的日志对象。
from __future__ import print_function
from argparse import ArgumentParser, FileType
import re
import shlex
import logging
import sys
import csv
logger = logging.getLogger(__file__)
在导入之后,我们为从日志中解析的字段定义模式。这些信息在日志之间可能会有所不同,尽管这里表达的模式应该涵盖日志中的大多数元素。
您可能需要添加、删除或重新排序以下定义的模式,以正确解析您正在使用的 IIS 日志。这些模式应该涵盖 IIS 日志中常见的元素。
我们将这些模式构建为名为iis_log_format的元组列表,其中第一个元组元素是列名,第二个是用于验证预期内容的正则表达式模式。通过使用正则表达式模式,我们可以定义数据必须遵循的一组规则以使其有效。这些列必须按它们在日志中出现的顺序来表达,否则代码将无法正确地将值映射到列。
iis_log_format = [
("date", re.compile(r"\d{4}-\d{2}-\d{2}")),
("time", re.compile(r"\d\d:\d\d:\d\d")),
("s-ip", re.compile(
r"((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|$)){4}")),
("cs-method", re.compile(
r"(GET)|(POST)|(PUT)|(DELETE)|(OPTIONS)|(HEAD)|(CONNECT)")),
("cs-uri-stem", re.compile(r"([A-Za-z0-1/\.-]*)")),
("cs-uri-query", re.compile(r"([A-Za-z0-1/\.-]*)")),
("s-port", re.compile(r"\d*")),
("cs-username", re.compile(r"([A-Za-z0-1/\.-]*)")),
("c-ip", re.compile(
r"((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|$)){4}")),
("cs(User-Agent)", re.compile(r".*")),
("sc-status", re.compile(r"\d*")),
("sc-substatus", re.compile(r"\d*")),
("sc-win32-status", re.compile(r"\d*")),
("time-taken", re.compile(r"\d*"))
]
此配方的命令行处理程序接受两个位置参数,iis_log和csv_report,分别表示要处理的 IIS 日志和所需的 CSV 路径。此外,此配方还接受一个可选参数l,指定配方日志文件的输出路径。
接下来,我们初始化了该配方的日志实用程序,并为控制台和基于文件的日志记录进行了配置。这一点很重要,因为我们应该以正式的方式注意到当我们无法为用户解析一行时。通过这种方式,如果出现问题,他们不应该在错误的假设下工作,即所有行都已成功解析并显示在生成的 CSV 电子表格中。我们还希望记录运行时消息,包括脚本的版本和提供的参数。在这一点上,我们准备调用main()函数并启动脚本。有关设置日志对象的更详细解释,请参阅第一章中的日志配方,基本脚本和文件信息配方。
if __name__ == '__main__':
parser = ArgumentParser(
description=__description__,
epilog="Developed by {} on {}".format(
", ".join(__authors__), __date__)
)
parser.add_argument('iis_log', help="Path to IIS Log",
type=FileType('r'))
parser.add_argument('csv_report', help="Path to CSV report")
parser.add_argument('-l', help="Path to processing log",
default=__name__ + '.log')
args = parser.parse_args()
logger.setLevel(logging.DEBUG)
msg_fmt = logging.Formatter("%(asctime)-15s %(funcName)-10s "
"%(levelname)-8s %(message)s")
strhndl = logging.StreamHandler(sys.stdout)
strhndl.setFormatter(fmt=msg_fmt)
fhndl = logging.FileHandler(args.log, mode='a')
fhndl.setFormatter(fmt=msg_fmt)
logger.addHandler(strhndl)
logger.addHandler(fhndl)
logger.info("Starting IIS Parsing ")
logger.debug("Supplied arguments: {}".format(", ".join(sys.argv[1:])))
logger.debug("System " + sys.platform)
logger.debug("Version " + sys.version)
main(args.iis_log, args.csv_report, logger)
logger.info("IIS Parsing Complete")
main()函数处理了脚本中大部分的逻辑。我们创建一个列表parsed_logs,用于在迭代日志文件中的行之前存储解析后的行。在for循环中,我们剥离行并创建一个存储字典log_entry,以存储记录。通过跳过以注释(或井号)字符开头的行或者行为空,我们加快了处理速度,并防止了列匹配中的错误。
虽然 IIS 日志存储为以空格分隔的值,但它们使用双引号来转义包含空格的字符串。例如,useragent字符串是一个单一值,但通常包含一个或多个空格。使用shlex模块,我们可以使用shlex()方法解析带有双引号的空格的行,并通过正确地在空格值上分隔数据来自动处理引号转义的空格。这个库可能会减慢处理速度,因此我们只在包含双引号字符的行上使用它。
def main(iis_log, report_file, logger):
parsed_logs = []
for raw_line in iis_log:
line = raw_line.strip()
log_entry = {}
if line.startswith("#") or len(line) == 0:
continue
if '\"' in line:
line_iter = shlex.shlex(line_iter)
else:
line_iter = line.split(" ")
将行正确分隔后,我们使用enumerate函数逐个遍历记录中的每个元素,并提取相应的列名和模式。使用模式,我们在值上调用match()方法,如果匹配,则在log_entry字典中创建一个条目。如果值不匹配模式,我们记录一个错误,并在日志文件中提供整行。在遍历每个列后,我们将记录字典附加到初始解析日志记录列表,并对剩余行重复此过程。
for count, split_entry in enumerate(line_iter):
col_name, col_pattern = iis_log_format[count]
if col_pattern.match(split_entry):
log_entry[col_name] = split_entry
else:
logger.error("Unknown column pattern discovered. "
"Line preserved in full below")
logger.error("Unparsed Line: {}".format(line))
parsed_logs.append(log_entry)
处理完所有行后,我们在准备write_csv()方法之前向控制台打印状态消息。我们使用一个简单的列表推导表达式来提取iis_log_format列表中每个元组的第一个元素,这代表一个列名。有了提取的列,让我们来看看报告编写器。
logger.info("Parsed {} lines".format(len(parsed_logs)))
cols = [x[0] for x in iis_log_format]
logger.info("Creating report file: {}".format(report_file))
write_csv(report_file, cols, parsed_logs)
logger.info("Report created")
报告编写器使用我们之前探讨过的方法创建一个 CSV 文件。由于我们将行存储为字典列表,我们可以使用csv.DictWriter类的四行代码轻松创建报告。
def write_csv(outfile, fieldnames, data):
with open(outfile, 'w', newline="") as open_outfile:
csvfile = csv.DictWriter(open_outfile, fieldnames)
csvfile.writeheader()
csvfile.writerows(data)
当我们查看脚本生成的 CSV 报告时,我们会在样本输出中看到以下字段:
还有更多...
这个脚本可以进一步改进。以下是一个建议:
- 虽然我们可以像在脚本开头看到的那样定义正则表达式模式,但我们可以使用正则表达式管理库来简化我们的生活。一个例子是
grok库,它用于为模式创建变量名。这使我们能够轻松地组织和扩展模式,因为我们可以按名称而不是字符串值来表示它们。这个库被其他平台使用,比如 ELK 堆栈,用于管理和实现正则表达式。
进行洞穴探险
菜谱难度:中等
Python 版本:2.7
操作系统:任何
由于保存的详细级别和时间范围,日志文件很快就会变得相当庞大。正如您可能已经注意到的那样,先前菜谱的 CSV 报告很容易变得过大,以至于我们的电子表格应用程序无法有效地打开或浏览。与其在电子表格中分析这些数据,一个替代方法是将数据加载到数据库中。
Splunk是一个将 NoSQL 数据库与摄取和查询引擎结合在一起的平台,使其成为一个强大的分析工具。它的数据库的操作方式类似于 Elasticsearch 或 MongoDB,允许存储文档或结构化记录。因此,我们不需要为了将记录存储在数据库中而提供具有一致键值映射的记录。这就是使 NoSQL 数据库对于日志分析如此有用的原因,因为日志格式可能根据事件类型而变化。
在这个步骤中,我们学习将上一个步骤的 CSV 报告索引到 Splunk 中,从而可以在平台内部与数据交互。我们还设计脚本来针对数据集运行查询,并将响应查询的结果子集导出到 CSV 文件。这些过程分别处理,因此我们可以根据需要独立查询和导出数据。
入门
这个步骤需要安装第三方库splunk-sdk。此脚本中使用的所有其他库都包含在 Python 的标准库中。此外,我们必须在主机操作系统上安装 Splunk,并且由于splunk-sdk库的限制,必须使用 Python 2 来运行脚本。
要安装 Splunk,我们需要转到Splunk.com,填写表格,并选择 Splunk Enterprise 免费试用下载。这个企业试用版允许我们练习 API,并且可以每天上传 500MB。下载应用程序后,我们需要启动它来配置应用程序。虽然有很多配置可以更改,但现在使用默认配置启动,以保持简单并专注于 API。这样做后,服务器的默认地址将是localhost:8000。通过在浏览器中导航到这个地址,我们可以首次登录,设置账户和(请执行此操作)更改管理员密码。
新安装的 Splunk 的默认用户名和密码是admin和changeme。
在 Splunk 实例激活后,我们现在可以安装 API 库。这个库处理从 REST API 到 Python 对象的转换。在撰写本书时,Splunk API 只能在 Python 2 中使用。splunk-sdk库可以使用pip安装:
pip install splunk-sdk==1.6.2
要了解更多关于splunk-sdk库的信息,请访问dev.splunk.com/python。
如何做到...
现在环境已经正确配置,我们可以开始开发代码。这个脚本将新数据索引到 Splunk,对该数据运行查询,并将响应我们查询的数据子集导出到 CSV 文件。为了实现这一点,我们需要:
-
开发一个强大的参数处理接口,允许用户指定这些选项。
-
构建一个处理各种属性方法的操作类。
-
创建处理索引新数据和创建数据存储索引的过程的方法。
-
建立运行 Splunk 查询的方法,以便生成信息丰富的报告。
-
提供一种将报告导出为 CSV 格式的机制。
它是如何工作的...
首先导入此脚本所需的库,包括新安装的splunklib。为了防止由于用户无知而引起不必要的错误,我们使用sys库来确定执行脚本的 Python 版本,并在不是 Python 2 时引发错误。
from __future__ import print_function
from argparse import ArgumentParser, ArgumentError
from argparse import ArgumentDefaultsHelpFormatter
import splunklib.client as client
import splunklib.results as results
import os
import sys
import csv
if sys.version_info.major != 2:
print("Invalid python version. Must use Python 2 due to splunk api "
"library")
下一个逻辑块是开发步骤的命令行参数处理程序。由于这段代码有很多选项和操作需要在代码中执行,我们需要在这一部分花费一些额外的时间。而且因为这段代码是基于类的,所以我们必须在这一部分设置一些额外的逻辑。
这个步骤的命令行处理程序接受一个位置输入action,表示要运行的操作(索引、查询或导出)。此步骤还支持七个可选参数:index、config、file、query、cols、host和port。让我们开始看看所有这些选项都做什么。
index参数实际上是一个必需的参数,用于指定要从中摄取、查询或导出数据的 Splunk 索引的名称。这可以是现有的或新的index名称。config参数是指包含 Splunk 实例的用户名和密码的配置文件。如参数帮助中所述,此文件应受保护并存储在代码执行位置之外。在企业环境中,您可能需要进一步保护这些凭据。
if __name__ == '__main__':
parser = ArgumentParser(
description=__description__,
formatter_class=ArgumentDefaultsHelpFormatter,
epilog="Developed by {} on {}".format(
", ".join(__authors__), __date__)
)
parser.add_argument('action', help="Action to run",
choices=['index', 'query', 'export'])
parser.add_argument('--index-name', help="Name of splunk index",
required=True)
parser.add_argument('--config',
help="Place where login details are stored."
" Should have the username on the first line and"
" the password on the second."
" Please Protect this file!",
default=os.path.expanduser("~/.splunk_py.ini"))
file参数将用于提供要index到平台的文件的路径,或用于指定要将导出的query数据写入的文件名。例如,我们将使用file参数指向我们希望从上一个配方中摄取的 CSV 电子表格。query参数也具有双重作用,它可以用于从 Splunk 运行查询,也可以用于指定要导出为 CSV 的查询 ID。这意味着index和query操作只需要其中一个参数,但export操作需要两个参数。
parser.add_argument('--file', help="Path to file")
parser.add_argument('--query', help="Splunk query to run or sid of "
"existing query to export")
最后一组参数允许用户修改配方的默认属性。例如,cols参数可用于指定从源数据中导出的列及其顺序。由于我们将查询和导出 IIS 日志,因此我们已经知道可用的列,并且对我们感兴趣。您可能希望根据正在探索的数据类型指定替代默认列。我们的最后两个参数包括host和port参数,每个参数默认为本地服务器,但可以配置为允许您与替代实例进行交互。
parser.add_argument(
'--cols',
help="Speficy columns to export. comma seperated list",
default='_time,date,time,sc_status,c_ip,s_ip,cs_User_Agent')
parser.add_argument('--host', help="hostname of server",
default="localhost")
parser.add_argument('--port', help="help", default="8089")
args = parser.parse_args()
确定了我们的参数后,我们可以解析它们并在执行配方之前验证所有要求是否满足。首先,我们必须打开并读取包含身份验证凭据的config文件,其中username在第一行,password在第二行。使用这些信息,我们创建一个包含登录详细信息和服务器位置的字典conn_dict,并将该字典传递给splunklib的client.connect()方法。请注意,我们使用del()方法删除包含这些敏感信息的变量。虽然用户名和密码仍然可以通过service对象访问,但我们希望限制存储这些详细信息的区域数量。在创建service变量后,我们测试是否在 Splunk 中安装了任何应用程序,因为默认情况下至少有一个应用程序,并将其用作成功验证的测试。
with open(args.config, 'r') as open_conf:
username, password = [x.strip() for x in open_conf.readlines()]
conn_dict = {'host': args.host, 'port': int(args.port),
'username': username, 'password': password}
del(username)
del(password)
service = client.connect(**conn_dict)
del(conn_dict)
if len(service.apps) == 0:
print("Login likely unsuccessful, cannot find any applications")
sys.exit()
我们继续处理提供的参数,将列转换为列表并创建Spelunking类实例。要初始化该类,我们必须向其提供service变量、要执行的操作、索引名称和列。使用这些信息,我们的类实例现在已经准备就绪。
cols = args.cols.split(",")
spelunking = Spelunking(service, args.action, args.index_name, cols)
接下来,我们使用一系列if-elif-else语句来处理我们预期遇到的三种不同操作。如果用户提供了index操作,我们首先确认可选的file参数是否存在,如果不存在则引发错误。如果我们找到它,我们将该值分配给Spelunking类实例的相应属性。对于query和export操作,我们重复这种逻辑,确认它们也使用了正确的可选参数。请注意,我们使用os.path.abspath()函数为类分配文件的绝对路径。这允许splunklib在系统上找到正确的文件。也许这是本书中最长的参数处理部分,我们已经完成了必要的逻辑,现在可以调用类的run()方法来启动特定操作的处理。
if spelunking.action == 'index':
if 'file' not in vars(args):
ArgumentError('--file parameter required')
sys.exit()
else:
spelunking.file = os.path.abspath(args.file)
elif spelunking.action == 'export':
if 'file' not in vars(args):
ArgumentError('--file parameter required')
sys.exit()
if 'query' not in vars(args):
ArgumentError('--query parameter required')
sys.exit()
spelunking.file = os.path.abspath(args.file)
spelunking.sid = args.query
elif spelunking.action == 'query':
if 'query' not in vars(args):
ArgumentError('--query parameter required')
sys.exit()
else:
spelunking.query = "search index={} {}".format(args.index_name,
args.query)
else:
ArgumentError('Unknown action required')
sys.exit()
spelunking.run()
现在参数已经在我们身后,让我们深入研究负责处理用户请求操作的类。这个类有四个参数,包括service变量,用户指定的action,Splunk 索引名称和要使用的列。所有其他属性都设置为None,如前面的代码块所示,如果它们被提供,将在执行时适当地初始化。这样做是为了限制类所需的参数数量,并处理某些属性未使用的情况。所有这些属性都在我们的类开始时初始化,以确保我们已经分配了默认值。
class Spelunking(object):
def __init__(self, service, action, index_name, cols):
self.service = service
self.action = action
self.index = index_name
self.file = None
self.query = None
self.sid = None
self.job = None
self.cols = cols
run()方法负责使用get_or_create_index()方法从 Splunk 实例获取index对象。它还检查在命令行指定了哪个动作,并调用相应的类实例方法。
def run(self):
index_obj = self.get_or_create_index()
if self.action == 'index':
self.index_data(index_obj)
elif self.action == 'query':
self.query_index()
elif self.action == 'export':
self.export_report()
return
get_or_create_index()方法,顾名思义,首先测试指定的索引是否存在,并连接到它,或者如果没有找到该名称的索引,则创建一个新的索引。由于这些信息存储在service变量的indexes属性中,作为一个类似字典的对象,我们可以很容易地通过名称测试索引的存在。
def get_or_create_index(self):
# Create a new index
if self.index not in self.service.indexes:
return service.indexes.create(self.index)
else:
return self.service.indexes[self.index]
要从文件中摄取数据,比如 CSV 文件,我们可以使用一行语句将信息发送到index_data()方法中的实例。这个方法使用splunk_index对象的upload()方法将文件发送到 Splunk 进行摄取。虽然 CSV 文件是一个简单的例子,说明我们可以如何导入数据,但我们也可以使用前面的方法从原始日志中读取数据到 Splunk 实例,而不需要中间的 CSV 步骤。为此,我们希望使用index对象的不同方法,允许我们逐个发送每个解析的事件。
def index_data(self, splunk_index):
splunk_index.upload(self.file)
query_index()方法涉及更多,因为我们首先需要修改用户提供的查询。如下面的片段所示,我们需要将用户指定的列添加到初始查询中。这将使在导出阶段未使用的字段在查询中可用。在修改后,我们使用service.jobs.create()方法在 Splunk 系统中创建一个新的作业,并记录查询 SID。这个 SID 将在导出阶段用于导出特定查询作业的结果。我们打印这些信息,以及作业在 Splunk 实例中到期之前的时间。默认情况下,这个生存时间值是300秒,或五分钟。
def query_index(self):
self.query = self.query + "| fields + " + ", ".join(self.cols)
self.job = self.service.jobs.create(self.query, rf=self.cols)
self.sid = self.job.sid
print("Query job {} created. will expire in {} seconds".format(
self.sid, self.job['ttl']))
正如之前提到的,export_report()方法使用前面方法中提到的 SID 来检查作业是否完成,并检索要导出的数据。为了做到这一点,我们遍历可用的作业,如果我们的作业不存在,则发出警告。如果找到作业,但is_ready()方法返回False,则作业仍在处理中,尚未准备好导出结果。
def export_report(self):
job_obj = None
for j in self.service.jobs:
if j.sid == self.sid:
job_obj = j
if job_obj is None:
print("Job SID {} not found. Did it expire?".format(self.sid))
sys.exit()
if not job_obj.is_ready():
print("Job SID {} is still processing. "
"Please wait to re-run".format(self.sir))
如果作业通过了这两个测试,我们从 Splunk 中提取数据,并使用write_csv()方法将其写入 CSV 文件。在这之前,我们需要初始化一个列表来存储作业结果。接下来,我们检索结果,指定感兴趣的列,并将原始数据读入job_results变量。幸运的是,splunklib提供了一个ResultsReader,它将job_results变量转换为一个字典列表。我们遍历这个列表,并将每个字典附加到export_data列表中。最后,我们提供文件路径、列名和要导出到 CSV 写入器的数据集。
export_data = []
job_results = job_obj.results(rf=self.cols)
for result in results.ResultsReader(job_results):
export_data.append(result)
self.write_csv(self.file, self.cols, export_data)
这个类中的write_csv()方法是一个@staticmethod。这个装饰器允许我们在类中使用一个通用的方法,而不需要指定一个实例。这个方法无疑会让那些在本书的其他地方使用过的人感到熟悉,我们在那里打开输出文件,创建一个DictWriter对象,然后将列标题和数据写入文件。
@staticmethod
def write_csv(outfile, fieldnames, data):
with open(outfile, 'wb') as open_outfile:
csvfile = csv.DictWriter(open_outfile, fieldnames,
extrasaction="ignore")
csvfile.writeheader()
csvfile.writerows(data)
在我们的假设用例中,第一阶段将是索引前一个食谱中 CSV 电子表格中的数据。如下片段所示,我们提供了前一个食谱中的 CSV 文件,并将其添加到 Splunk 索引中。接下来,我们寻找所有用户代理为 iPhone 的条目。最后,最后一个阶段涉及从查询中获取输出并创建一个 CSV 报告。
成功执行这三个命令后,我们可以打开并查看过滤后的输出:
还有更多...
这个脚本可以进一步改进。我们提供了一个或多个建议,如下所示:
- Python 的 Splunk API(以及一般)还有许多其他功能。此外,还可以使用更高级的查询技术来生成我们可以将其转换为技术和非技术最终用户的图形的数据。了解更多 Splunk API 提供的许多功能。
解释 daily.out 日志
食谱难度:中等
Python 版本:3.5
操作系统:任意
操作系统日志通常反映系统上软件、硬件和服务的事件。这些细节可以在我们调查事件时帮助我们,比如可移动设备的使用。一个可以证明在识别这种活动中有用的日志的例子是在 macOS 系统上找到的daily.out日志。这个日志记录了大量信息,包括连接到机器上的驱动器以及每天可用和已使用的存储量。虽然我们也可以从这个日志中了解关机时间、网络状态和其他信息,但我们将专注于随时间的驱动器使用情况。
入门
此脚本中使用的所有库都包含在 Python 的标准库中。
如何做...
这个脚本将利用以下步骤:
-
设置参数以接受日志文件和写入报告的路径。
-
构建一个处理日志各个部分解析的类。
-
创建一个提取相关部分并传递给进一步处理的方法。
-
从这些部分提取磁盘信息。
-
创建一个 CSV 写入器来导出提取的细节。
它是如何工作的...
我们首先导入必要的库来处理参数、解释日期和写入电子表格。在 Python 中处理文本文件的一个很棒的地方是你很少需要第三方库。
from __future__ import print_function
from argparse import ArgumentParser, FileType
from datetime import datetime
import csv
这个食谱的命令行处理程序接受两个位置参数,daily_out和output_report,分别代表 daily.out 日志文件的路径和 CSV 电子表格的期望输出路径。请注意,我们通过argparse.FileType类传递一个打开的文件对象进行处理。随后,我们用日志文件初始化ProcessDailyOut类,并调用run()方法,并将返回的结果存储在parsed_events变量中。然后我们调用write_csv()方法,使用processor类对象中定义的列将结果写入到所需输出目录中的电子表格中。
if __name__ == '__main__':
parser = ArgumentParser(
description=__description__,
epilog="Developed by {} on {}".format(
", ".join(__authors__), __date__)
)
parser.add_argument("daily_out", help="Path to daily.out file",
type=FileType('r'))
parser.add_argument("output_report", help="Path to csv report")
args = parser.parse_args()
processor = ProcessDailyOut(args.daily_out)
parsed_events = processor.run()
write_csv(args.output_report, processor.report_columns, parsed_events)
在ProcessDailyOut类中,我们设置了用户提供的属性,并定义了报告中使用的列。请注意,我们添加了两组不同的列:disk_status_columns和report_columns。report_columns只是disk_status_columns,再加上两个额外的字段来标识条目的日期和时区。
class ProcessDailyOut(object):
def __init__(self, daily_out):
self.daily_out = daily_out
self.disk_status_columns = [
'Filesystem', 'Size', 'Used', 'Avail', 'Capacity', 'iused',
'ifree', '%iused', 'Mounted on']
self.report_columns = ['event_date', 'event_tz'] + \
self.disk_status_columns
run()方法首先遍历提供的日志文件。在从每行的开头和结尾去除空白字符后,我们验证内容以识别部分中断。"-- End of daily output --"字符串中断了日志文件中的每个条目。每个条目包含几个由新行分隔的数据部分。因此,我们必须使用几个代码块来分割和处理每个部分。
在这个循环中,我们收集来自单个事件的所有行,并将其传递给process_event()方法,并将处理后的结果追加到最终返回的parsed_events列表中。
def run(self):
event_lines = []
parsed_events = []
for raw_line in self.daily_out:
line = raw_line.strip()
if line == '-- End of daily output --':
parsed_events += self.process_event(event_lines)
event_lines = []
else:
event_lines.append(line)
return parsed_events
在process_event()方法中,我们将定义变量,以便我们可以分割事件的各个部分以进行进一步处理。为了更好地理解代码的下一部分,请花一点时间查看以下事件的示例:
在此事件中,我们可以看到第一个元素是日期值和时区,后面是一系列子部分。每个子部分标题都是以冒号结尾的行;我们使用这一点来拆分文件中的各种数据元素,如下面的代码所示。我们使用部分标题作为键,其内容(如果存在)作为值,然后进一步处理每个子部分。
def process_event(self, event_lines):
section_header = ""
section_data = []
event_data = {}
for line in event_lines:
if line.endswith(":"):
if len(section_data) > 0:
event_data[section_header] = section_data
section_data = []
section_header = ""
section_header = line.strip(":")
如果部分标题行不以冒号结尾,我们检查行中是否恰好有两个冒号。如果是这样,我们尝试将此行验证为日期值。为了处理此日期格式,我们需要从日期的其余部分单独提取时区,因为已知 Python 3 版本在解析带有%Z格式化程序的时区时存在已知错误。对于感兴趣的人,可以在bugs.python.org/issue22377找到有关此错误的更多信息。
为了将时区与日期值分开,我们在空格值上分隔字符串,在这个示例中将时区值(元素4)放入自己的变量中,然后将剩余的时间值连接成一个新的字符串,我们可以使用datetime库解析。如果字符串没有至少5个元素,可能会引发IndexError,或者如果datetime格式字符串无效,可能会引发ValueError。如果没有引发这两种错误类型,我们将日期分配给event_data字典。如果我们收到这些错误中的任何一个,该行将附加到section_data列表中,并且下一个循环迭代将继续。这很重要,因为一行可能包含两个冒号,但不是日期值,所以我们不希望将其从脚本的考虑中移除。
elif line.count(":") == 2:
try:
split_line = line.split()
timezone = split_line[4]
date_str = " ".join(split_line[:4] + [split_line[-1]])
try:
date_val = datetime.strptime(
date_str, "%a %b %d %H:%M:%S %Y")
except ValueError:
date_val = datetime.strptime(
date_str, "%a %b %d %H:%M:%S %Y")
event_data["event_date"] = [date_val, timezone]
section_data = []
section_header = ""
except ValueError:
section_data.append(line)
except IndexError:
section_data.append(line)
此条件的最后一部分将任何具有内容的行附加到section_data变量中,以根据需要进行进一步处理。这可以防止空白行进入,并允许我们捕获两个部分标题之间的所有信息。
else:
if len(line):
section_data.append(line)
通过调用任何子部分处理器来关闭此函数。目前,我们只处理磁盘信息子部分,使用process_disk()方法,尽管可以开发代码来提取其他感兴趣的值。此方法接受事件信息和事件日期作为其输入。磁盘信息作为处理过的磁盘信息元素列表返回,我们将其返回给run()方法,并将值添加到处理过的事件列表中。
return self.process_disk(event_data.get("Disk status", []),
event_data.get("event_date", []))
要处理磁盘子部分,我们遍历每一行,如果有的话,并提取相关的事件信息。for循环首先检查迭代号,并跳过行零,因为它包含数据的列标题。对于任何其他行,我们使用列表推导式,在单个空格上拆分行,去除空白,并过滤掉任何空字段。
def process_disk(self, disk_lines, event_dates):
if len(disk_lines) == 0:
return {}
processed_data = []
for line_count, line in enumerate(disk_lines):
if line_count == 0:
continue
prepped_lines = [x for x in line.split(" ")
if len(x.strip()) != 0]
接下来,我们初始化一个名为disk_info的字典,其中包含了此快照的日期和时区详细信息。for循环使用enumerate()函数将值映射到它们的列名。如果列名包含"/Volumes/"(驱动器卷的标准挂载点),我们将连接剩余的拆分项。这样可以确保保留具有空格名称的卷。
disk_info = {
"event_date": event_dates[0],
"event_tz": event_dates[1]
}
for col_count, entry in enumerate(prepped_lines):
curr_col = self.disk_status_columns[col_count]
if "/Volumes/" in entry:
disk_info[curr_col] = " ".join(
prepped_lines[col_count:])
break
disk_info[curr_col] = entry.strip()
最内层的for循环通过将磁盘信息附加到processed_data列表来结束。一旦磁盘部分中的所有行都被处理,我们就将processed_data列表返回给父函数。
processed_data.append(disk_info)
return processed_data
最后,我们简要介绍了write_csv()方法,它使用DictWriter类来打开文件并将标题行和内容写入 CSV 文件。
def write_csv(outfile, fieldnames, data):
with open(outfile, 'w', newline="") as open_outfile:
csvfile = csv.DictWriter(open_outfile, fieldnames)
csvfile.writeheader()
csvfile.writerows(data)
当我们运行这个脚本时,我们可以在 CSV 报告中看到提取出的细节。这里展示了这个输出的一个例子:
将 daily.out 解析添加到 Axiom
教程难度:简单
Python 版本:2.7
操作系统:任意
使用我们刚刚开发的代码来解析 macOS 的daily.out日志,我们将这个功能添加到 Axiom 中,由Magnet Forensics开发,用于自动提取这些事件。由于 Axiom 支持处理取证镜像和松散文件,我们可以提供完整的获取或只是daily.out日志的导出作为示例。通过这个工具提供的 API,我们可以访问和处理其引擎发现的文件,并直接在 Axiom 中返回审查结果。
入门
Magnet Forensics 团队开发了一个 API,用于 Python 和 XML,以支持在 Axiom 中创建自定义 artifact。截至本书编写时,Python API 仅适用于运行 Python 版本 2.7 的IronPython。虽然我们在这个平台之外开发了我们的代码,但我们可以按照本教程中的步骤轻松地将其集成到 Axiom 中。我们使用了 Axiom 版本 1.1.3.5726 来测试和开发这个教程。
我们首先需要在 Windows 实例中安装 Axiom,并确保我们的代码稳定且可移植。此外,我们的代码需要在沙盒中运行。Axiom 沙盒限制了对第三方库的使用以及对可能导致代码与应用程序外部系统交互的一些 Python 模块和函数的访问。因此,我们设计了我们的daily.out解析器,只使用在沙盒中安全的内置库,以演示使用这些自定义 artifact 的开发的便利性。
如何做...
要开发和实现自定义 artifact,我们需要:
-
在 Windows 机器上安装 Axiom。
-
导入我们开发的脚本。
-
创建
Artifact类并定义解析器元数据和列。 -
开发
Hunter类来处理 artifact 处理和结果报告。
工作原理...
对于这个脚本,我们导入了axiom库和 datetime 库。请注意,我们已经删除了之前的argparse和csv导入,因为它们在这里是不必要的。
from __future__ import print_function
from axiom import *
from datetime import datetime
接下来,我们必须粘贴前一个教程中的ProcessDailyOut类,不包括write_csv或参数处理代码,以在这个脚本中使用。由于当前版本的 API 不允许导入,我们必须将所有需要的代码捆绑到一个单独的脚本中。为了节省页面并避免冗余,我们将在本节中省略代码块(尽管它在本章附带的代码文件中存在)。
下一个类是DailyOutArtifact,它是 Axiom API 提供的Artifact类的子类。在定义插件的名称之前,我们调用AddHunter()方法,提供我们的(尚未显示的)hHunter类。
class DailyOutArtifact(Artifact):
def __init__(self):
self.AddHunter(DailyOutHunter())
def GetName(self):
return 'daily.out parser'
这个类的最后一个方法CreateFragments()指定了如何处理已处理的 daily.out 日志结果的单个条目。就 Axiom API 而言,片段是用来描述 artifact 的单个条目的术语。这段代码允许我们添加自定义列名,并为这些列分配适当的类别和数据类型。这些类别包括日期、位置和工具定义的其他特殊值。我们 artifact 的大部分列将属于None类别,因为它们不显示特定类型的信息。
一个重要的分类区别是DateTimeLocal与DateTime:DateTime将日期呈现为 UTC 值呈现给用户,因此我们需要注意选择正确的日期类别。因为我们从 daily.out 日志条目中提取了时区,所以在这个示例中我们使用DateTimeLocal类别。FragmentType属性是所有值的字符串,因为该类不会将值从字符串转换为其他数据类型。
def CreateFragments(self):
self.AddFragment('Snapshot Date - LocalTime (yyyy-mm-dd)',
Category.DateTimeLocal, FragmentType.DateTime)
self.AddFragment('Snapshot Timezone', Category.None,
FragmentType.String)
self.AddFragment('Volume Name',
Category.None, FragmentType.String)
self.AddFragment('Filesystem Mount',
Category.None, FragmentType.String)
self.AddFragment('Volume Size',
Category.None, FragmentType.String)
self.AddFragment('Volume Used',
Category.None, FragmentType.String)
self.AddFragment('Percentage Used',
Category.None, FragmentType.String)
接下来的类是我们的Hunter。这个父类用于运行处理代码,并且正如你将看到的,指定了将由 Axiom 引擎提供给插件的平台和内容。在这种情况下,我们只想针对计算机平台和一个单一名称的文件运行。RegisterFileName()方法是指定插件将请求哪些文件的几种选项之一。我们还可以使用正则表达式或文件扩展名来选择我们想要处理的文件。
class DailyOutHunter(Hunter):
def __init__(self):
self.Platform = Platform.Computer
def Register(self, registrar):
registrar.RegisterFileName('daily.out')
Hunt()方法是魔法发生的地方。首先,我们获取一个临时路径,在沙箱内可以读取文件,并将其分配给temp_daily_out变量。有了这个打开的文件,我们将文件对象交给ProcessDailyOut类,并使用run()方法解析文件,就像上一个示例中一样。
def Hunt(self, context):
temp_daily_out = open(context.Searchable.FileCopy, 'r')
processor = ProcessDailyOut(temp_daily_out)
parsed_events = processor.run()
在收集了解析的事件信息之后,我们准备将数据“发布”到软件并显示给用户。在for循环中,我们首先初始化一个Hit()对象,使用AddValue()方法向新片段添加数据。一旦我们将事件值分配给了一个 hit,我们就使用PublishHit()方法将 hit 发布到平台,并继续循环直到所有解析的事件都被发布:
for entry in parsed_events:
hit = Hit()
hit.AddValue(
"Snapshot Date - LocalTime (yyyy-mm-dd)",
entry['event_date'].strftime("%Y-%m-%d %H:%M:%S"))
hit.AddValue("Snapshot Timezone", entry['event_tz'])
hit.AddValue("Volume Name", entry['Mounted on'])
hit.AddValue("Filesystem Mount", entry["Filesystem"])
hit.AddValue("Volume Size", entry['Size'])
hit.AddValue("Volume Used", entry['Used'])
hit.AddValue("Percentage Used", entry['Capacity'])
self.PublishHit(hit)
最后一部分代码检查文件是否不是None,如果是,则关闭它。这是处理代码的结尾,如果在系统上发现另一个daily.out文件,可能会再次调用它!
if temp_daily_out is not None:
temp_daily_out.close()
最后一行注册了我们的辛勤工作到 Axiom 的引擎,以确保它被框架包含和调用。
RegisterArtifact(DailyOutArtifact())
要在 Axiom 中使用新开发的工件,我们需要采取一些步骤来导入并针对图像运行代码。首先,我们需要启动 Axiom Process。这是我们将加载、选择并针对提供的证据运行工件的地方。在工具菜单下,我们选择管理自定义工件选项:
在管理自定义工件窗口中,我们将看到任何现有的自定义工件,并可以像这样导入新的工件:
我们将添加我们的自定义工件,更新的管理自定义工件窗口应该显示工件的名称:
现在我们可以按下确定并继续进行 Axiom,添加证据并配置我们的处理选项。当我们到达计算机工件选择时,我们要确认选择运行自定义工件。可能不用说:我们应该只在机器运行 macOS 或者在其上有 macOS 分区时运行这个工件:
完成剩余的配置选项后,我们可以开始处理证据。处理完成后,我们运行 Axiom Examine 来查看处理结果。如下截图所示,我们可以导航到工件审查的自定义窗格,并看到插件解析的列!这些列可以使用 Axiom 中的标准选项进行排序和导出,而无需我们额外的代码:
使用 YARA 扫描指示器
配方难度:中等
Python 版本:3.5
操作系统:任何
作为一个额外的部分,我们将利用强大的Yet Another Recursive Algorithm(YARA)正则表达式引擎来扫描感兴趣的文件和妥协指标。YARA 是一种用于恶意软件识别和事件响应的模式匹配实用程序。许多工具使用此引擎作为识别可能恶意文件的基础。通过这个示例,我们学习如何获取 YARA 规则,编译它们,并在一个或多个文件夹或文件中进行匹配。虽然我们不会涵盖形成 YARA 规则所需的步骤,但可以从他们的文档中了解更多关于这个过程的信息yara.readthedocs.io/en/latest/writingrules.html。
入门
此示例需要安装第三方库yara。此脚本中使用的所有其他库都包含在 Python 的标准库中。可以使用pip安装此库:
pip install yara-python==3.6.3
要了解更多关于yara-python库的信息,请访问yara.readthedocs.io/en/latest/。
我们还可以使用项目如 YaraRules (yararules.com),并使用行业和 VirusShare (virusshare.com)的预构建规则来使用真实的恶意软件样本进行分析。
如何做...
此脚本有四个主要的开发步骤:
-
设置和编译 YARA 规则。
-
扫描单个文件。
-
遍历目录以处理单个文件。
-
将结果导出到 CSV。
它是如何工作的...
此脚本导入所需的库来处理参数解析、文件和文件夹迭代、编写 CSV 电子表格,以及yara库来编译和扫描 YARA 规则。
from __future__ import print_function
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
import os
import csv
import yara
这个示例的命令行处理程序接受两个位置参数,yara_rules和path_to_scan,分别表示 YARA 规则的路径和要扫描的文件或文件夹。此示例还接受一个可选参数output,如果提供,将扫描结果写入电子表格而不是控制台。最后,我们将这些值传递给main()方法。
if __name__ == '__main__':
parser = ArgumentParser(
description=__description__,
formatter_class=ArgumentDefaultsHelpFormatter,
epilog="Developed by {} on {}".format(
", ".join(__authors__), __date__)
)
parser.add_argument(
'yara_rules',
help="Path to Yara rule to scan with. May be file or folder path.")
parser.add_argument(
'path_to_scan',
help="Path to file or folder to scan")
parser.add_argument(
'--output',
help="Path to output a CSV report of scan results")
args = parser.parse_args()
main(args.yara_rules, args.path_to_scan, args.output)
在main()函数中,我们接受yara规则的路径、要扫描的文件或文件夹以及输出文件(如果有)。由于yara规则可以是文件或目录,我们使用ios.isdir()方法来确定我们是否在整个目录上使用compile()方法,或者如果输入是一个文件,则使用filepath关键字将其传递给该方法。compile()方法读取规则文件或文件并创建一个我们可以与我们扫描的对象进行匹配的对象。
def main(yara_rules, path_to_scan, output):
if os.path.isdir(yara_rules):
yrules = yara.compile(yara_rules)
else:
yrules = yara.compile(filepath=yara_rules)
一旦规则被编译,我们执行类似的if-else语句来处理要扫描的路径。如果要扫描的输入是一个目录,我们将其传递给process_directory()函数,否则,我们使用process_file()方法。两者都使用编译后的 YARA 规则和要扫描的路径,并返回包含任何匹配项的字典列表。
if os.path.isdir(path_to_scan):
match_info = process_directory(yrules, path_to_scan)
else:
match_info = process_file(yrules, path_to_scan)
正如你可能猜到的,如果指定了输出路径,我们最终将把这个字典列表转换为 CSV 报告,使用我们在columns列表中定义的列。然而,如果输出参数是None,我们将以不同的格式将这些数据写入控制台。
columns = ['rule_name', 'hit_value', 'hit_offset', 'file_name',
'rule_string', 'rule_tag']
if output is None:
write_stdout(columns, match_info)
else:
write_csv(output, columns, match_info)
process_directory()函数本质上是遍历目录并将每个文件传递给process_file()函数。这减少了脚本中冗余代码的数量。返回的每个处理过的条目都被添加到match_info列表中,因为返回的对象是一个列表。一旦我们处理了每个文件,我们将完整的结果列表返回给父函数。
def process_directory(yrules, folder_path):
match_info = []
for root, _, files in os.walk(folder_path):
for entry in files:
file_entry = os.path.join(root, entry)
match_info += process_file(yrules, file_entry)
return match_info
process_file() 方法使用了 yrules 对象的 match() 方法。返回的匹配对象是一个可迭代的对象,包含了一个或多个与规则匹配的结果。从匹配结果中,我们可以提取规则名称、任何标签、文件中的偏移量、规则的字符串值以及匹配结果的字符串值。这些信息加上文件路径将形成报告中的一条记录。总的来说,这些信息对于确定匹配结果是误报还是重要的非常有用。在微调 YARA 规则以确保只呈现相关结果进行审查时也非常有帮助。
def process_file(yrules, file_path):
match = yrules.match(file_path)
match_info = []
for rule_set in match:
for hit in rule_set.strings:
match_info.append({
'file_name': file_path,
'rule_name': rule_set.rule,
'rule_tag': ",".join(rule_set.tags),
'hit_offset': hit[0],
'rule_string': hit[1],
'hit_value': hit[2]
})
return match_info
write_stdout() 函数如果用户没有指定输出文件,则将匹配信息报告到控制台。我们遍历 match_info 列表中的每个条目,并以冒号分隔、换行分隔的格式打印出 match_info 字典中的每个列名及其值。在每个条目之后,我们打印 30 个等号来在视觉上将条目分隔开。
def write_stdout(columns, match_info):
for entry in match_info:
for col in columns:
print("{}: {}".format(col, entry[col]))
print("=" * 30)
write_csv() 方法遵循标准约定,使用 DictWriter 类来写入标题和所有数据到表格中。请注意,这个函数已经调整为在 Python 3 中处理 CSV 写入,使用了 'w' 模式和 newline 参数。
def write_csv(outfile, fieldnames, data):
with open(outfile, 'w', newline="") as open_outfile:
csvfile = csv.DictWriter(open_outfile, fieldnames)
csvfile.writeheader()
csvfile.writerows(data)
使用这段代码,我们可以在命令行提供适当的参数,并生成任何匹配的报告。以下截图显示了用于检测 Python 文件和键盘记录器的自定义规则:
这些规则显示在输出的 CSV 报告中,如果没有指定报告,则显示在控制台中,如下所示:
第八章:处理数字取证容器配方
在本章中,我们将涵盖以下配方:
-
打开收购
-
收集收购和媒体信息
-
遍历文件
-
处理容器内的文件
-
搜索哈希
介绍
Sleuth Kit 及其 Python 绑定pytsk3可能是最知名的 Python 数字取证库。该库提供了丰富的支持,用于访问和操作文件系统。借助支持库(如pyewf),它们可以用于处理 EnCase 流行的E01格式等常见数字取证容器。如果没有这些库(以及许多其他库),我们在数字取证中所能完成的工作将受到更多限制。由于其作为一体化文件系统分析工具的宏伟目标,pytsk3可能是我们在本书中使用的最复杂的库。
出于这个原因,我们专门制定了一些配方,探索了这个库的基本原理。到目前为止,配方主要集中在松散文件支持上。这种惯例到此为止。我们将会经常使用这个库来与数字取证证据进行交互。了解如何与数字取证容器进行交互将使您的 Python 数字取证能力提升到一个新的水平。
在本章中,我们将学习如何安装pytsk3和pyewf,这两个库将允许我们利用 Sleuth Kit 和E01镜像支持。此外,我们还将学习如何执行基本任务,如访问和打印分区表,遍历文件系统,按扩展名导出文件,以及在数字取证容器中搜索已知的不良哈希。您将学习以下内容:
-
安装和设置
pytsk3和pyewf -
打开数字取证收购,如
raw和E01文件 -
提取分区表数据和
E01元数据 -
递归遍历活动文件并创建活动文件列表电子表格
-
按文件扩展名从数字取证容器中导出文件
-
在数字取证容器中搜索已知的不良哈希
访问www.packtpub.com/books/conte…下载本章的代码包。
打开收购
配方难度:中等
Python 版本:2.7
操作系统:Linux
使用pyewf和pytsk3将带来一整套新的工具和操作,我们必须首先学习。在这个配方中,我们将从基础知识开始:打开数字取证容器。这个配方支持raw和E01镜像。请注意,与我们之前的脚本不同,由于在使用这些库的 Python 3.X 版本时发现了一些错误,这些配方将使用 Python 2.X。也就是说,主要逻辑在两个版本之间并没有区别,可以很容易地移植。在学习如何打开容器之前,我们需要设置我们的环境。我们将在下一节中探讨这个问题。
入门
除了一些脚本之外,我们在本书的大部分内容中都是与操作系统无关的。然而,在这里,我们将专门提供在 Ubuntu 16.04.2 上构建的说明。在 Ubuntu 的新安装中,执行以下命令以安装必要的依赖项:
sudo apt-get update && sudo apt-get -y upgrade
sudo apt-get install python-pip git autoconf automake autopoint libtool pkg-config
除了前面提到的两个库(pytsk3和pyewf)之外,我们还将使用第三方模块tabulate来在控制台打印表格。由于这是最容易安装的模块,让我们首先完成这个任务,执行以下操作:
pip install tabulate==0.7.7
要了解更多关于 tabulate 库的信息,请访问pypi.python.org/pypi/tabulate。
信不信由你,我们也可以使用pip安装pytsk3:
pip install pytsk3==20170802
要了解更多关于pytsk3库的信息,请访问github.com/py4n6/pytsk.
最后,对于pyewf,我们必须采取稍微绕弯的方法,从其 GitHub 存储库中安装,github.com/libyal/libewf/releases。这些配方是使用libewf-experimental-20170605版本编写的,我们建议您在这里安装该版本。一旦包被下载并解压,打开提取目录中的命令提示符,并执行以下操作:
./synclibs.sh
./autogen.sh
sudo python setup.py build
sudo python setup.py install
要了解更多关于pyewf库的信息,请访问:github.com/libyal/libewf.
毋庸置疑,对于这个脚本,您需要一个raw或E01证据文件来运行这些配方。对于第一个脚本,我们建议使用逻辑图像,比如来自dftt.sourceforge.net/test2/index.html的fat-img-kw.dd。原因是这个第一个脚本将缺少一些处理物理磁盘图像及其分区所需的必要逻辑。我们将在收集获取和媒体信息配方中介绍这个功能。
操作步骤...
我们采用以下方法来打开法证证据容器:
-
确定证据容器是
raw图像还是E01容器。 -
使用
pytsk3访问图像。 -
在控制台上打印根级文件夹和文件的表格。
它是如何工作的...
我们导入了一些库来帮助解析参数、处理证据容器和文件系统,并创建表格式的控制台数据。
from __future__ import print_function
import argparse
import os
import pytsk3
import pyewf
import sys
from tabulate import tabulate
这个配方的命令行处理程序接受两个位置参数,EVIDENCE_FILE和TYPE,它们代表证据文件的路径和证据文件的类型(即raw或ewf)。请注意,对于分段的E01文件,您只需要提供第一个E01的路径(假设其他分段在同一个目录中)。在对证据文件进行一些输入验证后,我们将提供两个输入给main()函数,并开始执行脚本。
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description=__description__,
epilog="Developed by {} on {}".format(
", ".join(__authors__), __date__)
)
parser.add_argument("EVIDENCE_FILE", help="Evidence file path")
parser.add_argument("TYPE",
help="Type of evidence: raw (dd) or EWF (E01)",
choices=("raw", "ewf"))
parser.add_argument("-o", "--offset",
help="Partition byte offset", type=int)
args = parser.parse_args()
if os.path.exists(args.EVIDENCE_FILE) and \
os.path.isfile(args.EVIDENCE_FILE):
main(args.EVIDENCE_FILE, args.TYPE, args.offset)
else:
print("[-] Supplied input file {} does not exist or is not a "
"file".format(args.EVIDENCE_FILE))
sys.exit(1)
在main()函数中,我们首先检查我们正在处理的证据文件的类型。如果是E01容器,我们需要首先使用pyewf创建一个句柄,然后才能使用pytsk3访问其内容。对于raw图像,我们可以直接使用pytsk3访问其内容,而无需先执行这个中间步骤。
在这里使用pyewf.glob()方法来组合E01容器的所有段,如果有的话,并将段的名称存储在一个列表中。一旦我们有了文件名列表,我们就可以创建E01句柄对象。然后我们可以使用这个对象来打开filenames。
def main(image, img_type, offset):
print("[+] Opening {}".format(image))
if img_type == "ewf":
try:
filenames = pyewf.glob(image)
except IOError:
_, e, _ = sys.exc_info()
print("[-] Invalid EWF format:\n {}".format(e))
sys.exit(2)
ewf_handle = pyewf.handle()
ewf_handle.open(filenames)
接下来,我们必须将ewf_handle传递给EWFImgInfo类,该类将创建pytsk3对象。这里的 else 语句是为了raw图像,可以使用pytsk3.Img_Info函数来实现相同的任务。现在让我们看看EWFImgInfo类,了解 EWF 文件是如何稍有不同地处理的。
# Open PYTSK3 handle on EWF Image
img_info = EWFImgInfo(ewf_handle)
else:
img_info = pytsk3.Img_Info(image)
这个脚本组件的代码来自pyewf的 Python 开发页面的将 pyewf 与 pytsk3 结合使用部分。
了解更多关于pyewf函数的信息,请访问github.com/libyal/libewf/wiki/Development。
这个EWFImgInfo类继承自pytsk3.Img_Info基类,属于TSK_IMG_TYPE_EXTERNAL类型。重要的是要注意,接下来定义的三个函数,close()、read()和get_size(),都是pytsk3要求的,以便与证据容器进行适当的交互。有了这个简单的类,我们现在可以使用pytsk3来处理任何提供的E01文件。
class EWFImgInfo(pytsk3.Img_Info):
def __init__(self, ewf_handle):
self._ewf_handle = ewf_handle
super(EWFImgInfo, self).__init__(url="",
type=pytsk3.TSK_IMG_TYPE_EXTERNAL)
def close(self):
self._ewf_handle.close()
def read(self, offset, size):
self._ewf_handle.seek(offset)
return self._ewf_handle.read(size)
def get_size(self):
return self._ewf_handle.get_media_size()
回到main()函数,我们已经成功地为raw或E01镜像创建了pytsk3处理程序。现在我们可以开始访问文件系统。如前所述,此脚本旨在处理逻辑图像而不是物理图像。我们将在下一个步骤中引入对物理图像的支持。访问文件系统非常简单;我们通过在pytsk3处理程序上调用FS_Info()函数来实现。
# Get Filesystem Handle
try:
fs = pytsk3.FS_Info(img_info, offset)
except IOError:
_, e, _ = sys.exc_info()
print("[-] Unable to open FS:\n {}".format(e))
exit()
有了对文件系统的访问权限,我们可以遍历根目录中的文件夹和文件。首先,我们使用文件系统上的open_dir()方法,并指定根目录**/**作为输入来访问根目录。接下来,我们创建一个嵌套的列表结构,用于保存表格内容,稍后我们将使用tabulate将其打印到控制台。这个列表的第一个元素是表格的标题。
之后,我们将开始遍历图像,就像处理任何 Python 可迭代对象一样。每个对象都有各种属性和函数,我们从这里开始使用它们。首先,我们使用f.info.name.name属性提取对象的名称。然后,我们使用f.info.meta.type属性检查我们处理的是目录还是文件。如果这等于内置的TSK_FS_META_TYPE_DIR对象,则将f_type变量设置为DIR;否则,设置为FILE。
最后,我们使用更多的属性来提取目录或文件的大小,并创建和修改时间戳。请注意,对象时间戳存储在Unix时间中,如果您想以人类可读的格式显示它们,必须进行转换。提取了这些属性后,我们将数据附加到table列表中,并继续处理下一个对象。一旦我们完成了对根文件夹中所有对象的处理,我们就使用tabulate将数据打印到控制台。通过向tabulate()方法提供列表并将headers关键字参数设置为firstrow,以指示应使用列表中的第一个元素作为表头,可以在一行中完成此操作。
root_dir = fs.open_dir(path="/")
table = [["Name", "Type", "Size", "Create Date", "Modify Date"]]
for f in root_dir:
name = f.info.name.name
if f.info.meta.type == pytsk3.TSK_FS_META_TYPE_DIR:
f_type = "DIR"
else:
f_type = "FILE"
size = f.info.meta.size
create = f.info.meta.crtime
modify = f.info.meta.mtime
table.append([name, f_type, size, create, modify])
print(tabulate(table, headers="firstrow"))
当我们运行脚本时,我们可以了解到在证据容器的根目录中看到的文件和文件夹,如下截图所示:
收集获取和媒体信息
食谱难度:中等
Python 版本:2.7
操作系统:Linux
在这个食谱中,我们学习如何使用tabulate查看和打印分区表。此外,对于E01容器,我们将打印存储在证据文件中的E01获取和容器元数据。通常,我们将使用给定机器的物理磁盘镜像。在接下来的任何过程中,我们都需要遍历不同的分区(或用户选择的分区)来获取文件系统及其文件的处理。因此,这个食谱对于我们建立对 Sleuth Kit 及其众多功能的理解至关重要。
入门
有关pytsk3、pyewf和tabulate的构建环境和设置详细信息,请参阅打开获取食谱中的入门部分。此脚本中使用的所有其他库都包含在 Python 的标准库中。
如何操作...
该食谱遵循以下基本步骤:
-
确定证据容器是
raw镜像还是E01容器。 -
使用
pytsk3访问镜像。 -
如果适用,将
E01元数据打印到控制台。 -
将分区表数据打印到控制台。
它是如何工作的...
我们导入了许多库来帮助解析参数、处理证据容器和文件系统,并创建表格式的控制台数据。
from __future__ import print_function
import argparse
import os
import pytsk3
import pyewf
import sys
from tabulate import tabulate
这个配方的命令行处理程序接受两个位置参数,EVIDENCE_FILE和TYPE,它们代表证据文件的路径和证据文件的类型。此外,如果用户在处理证据文件时遇到困难,他们可以使用可选的p开关手动提供分区。这个开关在大多数情况下不应该是必要的,但作为一种预防措施已经添加。在执行输入验证检查后,我们将这三个参数传递给main()函数。
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description=__description__,
epilog="Developed by {} on {}".format(
", ".join(__authors__), __date__)
)
parser.add_argument("EVIDENCE_FILE", help="Evidence file path")
parser.add_argument("TYPE", help="Type of Evidence",
choices=("raw", "ewf"))
parser.add_argument("-p", help="Partition Type",
choices=("DOS", "GPT", "MAC", "SUN"))
args = parser.parse_args()
if os.path.exists(args.EVIDENCE_FILE) and \
os.path.isfile(args.EVIDENCE_FILE):
main(args.EVIDENCE_FILE, args.TYPE, args.p)
else:
print("[-] Supplied input file {} does not exist or is not a "
"file".format(args.EVIDENCE_FILE))
sys.exit(1)
main()函数在很大程度上与之前的配方相似,至少最初是这样。我们必须首先创建pyewf句柄,然后使用EWFImgInfo类来创建,如前面在pytsk3句柄中所示。如果您想了解更多关于EWFImgInfo类的信息,请参阅打开获取配方。但是,请注意,我们添加了一个额外的行调用e01_metadata()函数来将E01元数据打印到控制台。现在让我们来探索一下这个函数。
def main(image, img_type, part_type):
print("[+] Opening {}".format(image))
if img_type == "ewf":
try:
filenames = pyewf.glob(image)
except IOError:
print("[-] Invalid EWF format:\n {}".format(e))
sys.exit(2)
ewf_handle = pyewf.handle()
ewf_handle.open(filenames)
e01_metadata(ewf_handle)
# Open PYTSK3 handle on EWF Image
img_info = EWFImgInfo(ewf_handle)
else:
img_info = pytsk3.Img_Info(image)
e01_metadata()函数主要依赖于get_header_values()和get_hash_values()方法来获取E01特定的元数据。get_header_values()方法返回各种类型的获取和媒体元数据的键值对字典。我们使用循环来遍历这个字典,并将键值对打印到控制台。
同样,我们使用hashes字典的循环将图像的存储获取哈希打印到控制台。最后,我们调用一个属性和一些函数来打印获取大小的元数据。
def e01_metadata(e01_image):
print("\nEWF Acquisition Metadata")
print("-" * 20)
headers = e01_image.get_header_values()
hashes = e01_image.get_hash_values()
for k in headers:
print("{}: {}".format(k, headers[k]))
for h in hashes:
print("Acquisition {}: {}".format(h, hashes[h]))
print("Bytes per Sector: {}".format(e01_image.bytes_per_sector))
print("Number of Sectors: {}".format(
e01_image.get_number_of_sectors()))
print("Total Size: {}".format(e01_image.get_media_size()))
有了这些,我们现在可以回到main()函数。回想一下,在本章的第一个配方中,我们没有为物理获取创建支持(这完全是有意的)。然而,现在,我们使用Volume_Info()函数添加了对此的支持。虽然pytsk3一开始可能令人生畏,但要欣赏到目前为止我们介绍的主要函数中使用的命名约定的一致性:Img_Info、FS_Info和Volume_Info。这三个函数对于访问证据容器的内容至关重要。在这个配方中,我们不会使用FS_Info()函数,因为这里的目的只是打印分区表。
我们尝试在try-except块中访问卷信息。首先,我们检查用户是否提供了p开关,如果是,则将该分区类型的属性分配给一个变量。然后,我们将它与pytsk3句柄一起提供给Volume_Info方法。否则,如果没有指定分区,我们调用Volume_Info方法,并只提供pytsk3句柄对象。如果我们尝试这样做时收到IOError,我们将捕获异常作为e并将其打印到控制台,然后退出。如果我们能够访问卷信息,我们将其传递给part_metadata()函数,以将分区数据打印到控制台。
try:
if part_type is not None:
attr_id = getattr(pytsk3, "TSK_VS_TYPE_" + part_type)
volume = pytsk3.Volume_Info(img_info, attr_id)
else:
volume = pytsk3.Volume_Info(img_info)
except IOError:
_, e, _ = sys.exc_info()
print("[-] Unable to read partition table:\n {}".format(e))
sys.exit(3)
part_metadata(volume)
part_metadata()函数在逻辑上相对较轻。我们创建一个嵌套的列表结构,如前面的配方中所见,第一个元素代表最终的表头。接下来,我们遍历卷对象,并将分区地址、类型、偏移量和长度附加到table列表中。一旦我们遍历了分区,我们使用tabulate使用firstrow作为表头将这些数据的表格打印到控制台。
def part_metadata(vol):
table = [["Index", "Type", "Offset Start (Sectors)",
"Length (Sectors)"]]
for part in vol:
table.append([part.addr, part.desc.decode("utf-8"), part.start,
part.len])
print("\n Partition Metadata")
print("-" * 20)
print(tabulate(table, headers="firstrow"))
运行此代码时,如果存在,我们可以在控制台中查看有关获取和分区信息的信息:
遍历文件
配方难度:中等
Python 版本:2.7
操作系统:Linux
在这个配方中,我们学习如何递归遍历文件系统并创建一个活动文件列表。作为法庭鉴定人,我们经常被问到的第一个问题之一是“设备上有什么数据?”。在这里,活动文件列表非常有用。在 Python 中,创建松散文件的文件列表是一个非常简单的任务。然而,这将会稍微复杂一些,因为我们处理的是法庭图像而不是松散文件。这个配方将成为未来脚本的基石,因为它将允许我们递归访问和处理图像中的每个文件。正如您可能已经注意到的,本章的配方是相互建立的,因为我们开发的每个函数都需要进一步探索图像。类似地,这个配方将成为未来配方中的一个重要部分,用于迭代目录并处理文件。
入门
有关pytsk3和pyewf的构建环境和设置详细信息,请参考开始部分中的打开获取配方。此脚本中使用的所有其他库都包含在 Python 的标准库中。
如何做...
我们在这个配方中执行以下步骤:
-
确定证据容器是
raw图像还是E01容器。 -
使用
pytsk3访问法庭图像。 -
递归遍历每个分区中的所有目录。
-
将文件元数据存储在列表中。
-
将
active文件列表写入 CSV。
工作原理...
我们导入了许多库来帮助解析参数、解析日期、创建 CSV 电子表格,以及处理证据容器和文件系统。
from __future__ import print_function
import argparse
import csv
from datetime import datetime
import os
import pytsk3
import pyewf
import sys
这个配方的命令行处理程序接受三个位置参数,EVIDENCE_FILE、TYPE和OUTPUT_CSV,分别代表证据文件的路径、证据文件的类型和输出 CSV 文件。与上一个配方类似,可以提供可选的p开关来指定分区类型。我们使用os.path.dirname()方法来提取 CSV 文件的所需输出目录路径,并使用os.makedirs()函数,如果不存在,则创建必要的输出目录。
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description=__description__,
epilog="Developed by {} on {}".format(
", ".join(__authors__), __date__)
)
parser.add_argument("EVIDENCE_FILE", help="Evidence file path")
parser.add_argument("TYPE", help="Type of Evidence",
choices=("raw", "ewf"))
parser.add_argument("OUTPUT_CSV",
help="Output CSV with lookup results")
parser.add_argument("-p", help="Partition Type",
choices=("DOS", "GPT", "MAC", "SUN"))
args = parser.parse_args()
directory = os.path.dirname(args.OUTPUT_CSV)
if not os.path.exists(directory) and directory != "":
os.makedirs(directory)
一旦我们通过检查输入证据文件是否存在并且是一个文件来验证了输入证据文件,四个参数将被传递给main()函数。如果在输入的初始验证中出现问题,脚本将在退出之前将错误打印到控制台。
if os.path.exists(args.EVIDENCE_FILE) and \
os.path.isfile(args.EVIDENCE_FILE):
main(args.EVIDENCE_FILE, args.TYPE, args.OUTPUT_CSV, args.p)
else:
print("[-] Supplied input file {} does not exist or is not a "
"file".format(args.EVIDENCE_FILE))
sys.exit(1)
在main()函数中,我们用None实例化卷变量,以避免在脚本后面引用它时出错。在控制台打印状态消息后,我们检查证据类型是否为E01,以便正确处理它并创建有效的pyewf句柄,如在打开获取配方中更详细地演示的那样。有关更多详细信息,请参阅该配方。最终结果是为用户提供的证据文件创建pytsk3句柄img_info。
def main(image, img_type, output, part_type):
volume = None
print("[+] Opening {}".format(image))
if img_type == "ewf":
try:
filenames = pyewf.glob(image)
except IOError:
_, e, _ = sys.exc_info()
print("[-] Invalid EWF format:\n {}".format(e))
sys.exit(2)
ewf_handle = pyewf.handle()
ewf_handle.open(filenames)
# Open PYTSK3 handle on EWF Image
img_info = EWFImgInfo(ewf_handle)
else:
img_info = pytsk3.Img_Info(image)
接下来,我们尝试使用pytsk3.Volume_Info()方法访问图像的卷,通过提供图像句柄作为参数。如果提供了分区类型参数,我们将其属性 ID 添加为第二个参数。如果在尝试访问卷时收到IOError,我们将捕获异常作为e并将其打印到控制台。然而,请注意,当我们收到错误时,我们不会退出脚本。我们将在下一个函数中解释原因。最终,我们将volume、img_info和output变量传递给open_fs()方法。
try:
if part_type is not None:
attr_id = getattr(pytsk3, "TSK_VS_TYPE_" + part_type)
volume = pytsk3.Volume_Info(img_info, attr_id)
else:
volume = pytsk3.Volume_Info(img_info)
except IOError:
_, e, _ = sys.exc_info()
print("[-] Unable to read partition table:\n {}".format(e))
open_fs(volume, img_info, output)
open_fs()方法尝试以两种方式访问容器的文件系统。如果volume变量不是None,它会遍历每个分区,并且如果该分区符合某些条件,则尝试打开它。但是,如果volume变量是None,它将尝试直接在图像句柄img上调用pytsk3.FS_Info()方法。正如我们所看到的,后一种方法将适用于逻辑图像,并为我们提供文件系统访问权限,而前一种方法适用于物理图像。让我们看看这两种方法之间的区别。
无论使用哪种方法,我们都创建一个recursed_data列表来保存我们的活动文件元数据。在第一种情况下,我们有一个物理图像,我们遍历每个分区,并检查它是否大于2,048扇区,并且在其描述中不包含Unallocated、Extended或Primary Table这些词。对于符合这些条件的分区,我们尝试使用FS_Info()函数访问它们的文件系统,方法是提供pytsk3 img对象和分区的偏移量(以字节为单位)。
如果我们能够访问文件系统,我们将使用open_dir()方法获取根目录,并将其与分区地址 ID、文件系统对象、两个空列表和一个空字符串一起传递给recurse_files()方法。这些空列表和字符串将在对此函数进行递归调用时发挥作用,我们很快就会看到。一旦recurse_files()方法返回,我们将活动文件的元数据附加到recursed_data列表中。我们对每个分区重复这个过程。
def open_fs(vol, img, output):
print("[+] Recursing through files..")
recursed_data = []
# Open FS and Recurse
if vol is not None:
for part in vol:
if part.len > 2048 and "Unallocated" not in part.desc and \
"Extended" not in part.desc and \
"Primary Table" not in part.desc:
try:
fs = pytsk3.FS_Info(
img, offset=part.start * vol.info.block_size)
except IOError:
_, e, _ = sys.exc_info()
print("[-] Unable to open FS:\n {}".format(e))
root = fs.open_dir(path="/")
data = recurse_files(part.addr, fs, root, [], [], [""])
recursed_data.append(data)
对于第二种情况,我们有一个逻辑图像,卷是None。在这种情况下,我们尝试直接访问文件系统,如果成功,我们将其传递给recurseFiles()方法,并将返回的数据附加到我们的recursed_data列表中。一旦我们有了活动文件列表,我们将其和用户提供的输出文件路径发送到csvWriter()方法。让我们深入了解recurseFiles()方法,这是本教程的核心。
else:
try:
fs = pytsk3.FS_Info(img)
except IOError:
_, e, _ = sys.exc_info()
print("[-] Unable to open FS:\n {}".format(e))
root = fs.open_dir(path="/")
data = recurse_files(1, fs, root, [], [], [""])
recursed_data.append(data)
write_csv(recursed_data, output)
recurse_files()函数基于FLS工具的一个示例(github.com/py4n6/pytsk/blob/master/examples/fls.py)和 David Cowen 的工具 DFIR Wizard(github.com/dlcowen/dfirwizard/blob/master/dfirwizard-v9.py)。为了启动这个函数,我们将根目录inode附加到dirs列表中。稍后将使用此列表以避免无休止的循环。接下来,我们开始循环遍历根目录中的每个对象,并检查它是否具有我们期望的某些属性,以及它的名称既不是"**.**"也不是"**..**"。
def recurse_files(part, fs, root_dir, dirs, data, parent):
dirs.append(root_dir.info.fs_file.meta.addr)
for fs_object in root_dir:
# Skip ".", ".." or directory entries without a name.
if not hasattr(fs_object, "info") or \
not hasattr(fs_object.info, "name") or \
not hasattr(fs_object.info.name, "name") or \
fs_object.info.name.name in [".", ".."]:
continue
如果对象通过了这个测试,我们将使用info.name.name属性提取其名称。接下来,我们使用作为函数输入之一提供的parent变量手动为此对象创建文件路径。对于我们来说,没有内置的方法或属性可以自动执行此操作。
然后,我们检查文件是否是目录,并将f_type变量设置为适当的类型。如果对象是文件,并且具有扩展名,我们将提取它并将其存储在file_ext变量中。如果在尝试提取此数据时遇到AttributeError,我们将继续到下一个对象。
try:
file_name = fs_object.info.name.name
file_path = "{}/{}".format(
"/".join(parent), fs_object.info.name.name)
try:
if fs_object.info.meta.type == pytsk3.TSK_FS_META_TYPE_DIR:
f_type = "DIR"
file_ext = ""
else:
f_type = "FILE"
if "." in file_name:
file_ext = file_name.rsplit(".")[-1].lower()
else:
file_ext = ""
except AttributeError:
continue
与本章第一个示例类似,我们为对象大小和时间戳创建变量。但是,请注意,我们将日期传递给convert_time()方法。此函数用于将Unix时间戳转换为人类可读的格式。提取了这些属性后,我们使用分区地址 ID 将它们附加到数据列表中,以确保我们跟踪对象来自哪个分区。
size = fs_object.info.meta.size
create = convert_time(fs_object.info.meta.crtime)
change = convert_time(fs_object.info.meta.ctime)
modify = convert_time(fs_object.info.meta.mtime)
data.append(["PARTITION {}".format(part), file_name, file_ext,
f_type, create, change, modify, size, file_path])
如果对象是一个目录,我们需要递归遍历它,以访问其所有子目录和文件。为此,我们将目录名称附加到parent列表中。然后,我们使用as_directory()方法创建一个目录对象。我们在这里使用inode,这对于所有目的来说都是一个唯一的数字,并检查inode是否已经在dirs列表中。如果是这样,那么我们将不处理这个目录,因为它已经被处理过了。
如果需要处理目录,我们在新的sub_directory上调用recurse_files()方法,并传递当前的dirs、data和parent变量。一旦我们处理了给定的目录,我们就从parent列表中弹出该目录。如果不这样做,将导致错误的文件路径细节,因为除非删除,否则所有以前的目录将继续在路径中被引用。
这个函数的大部分内容都在一个大的try-except块中。我们传递在这个过程中生成的任何IOError异常。一旦我们遍历了所有的子目录,我们将数据列表返回给open_fs()函数。
if f_type == "DIR":
parent.append(fs_object.info.name.name)
sub_directory = fs_object.as_directory()
inode = fs_object.info.meta.addr
# This ensures that we don't recurse into a directory
# above the current level and thus avoid circular loops.
if inode not in dirs:
recurse_files(part, fs, sub_directory, dirs, data,
parent)
parent.pop(-1)
except IOError:
pass
dirs.pop(-1)
return data
让我们简要地看一下convert_time()函数。我们以前见过这种类型的函数:如果Unix时间戳不是0,我们使用datetime.utcfromtimestamp()方法将时间戳转换为人类可读的格式。
def convert_time(ts):
if str(ts) == "0":
return ""
return datetime.utcfromtimestamp(ts)
有了手头的活动文件列表数据,我们现在准备使用write_csv()方法将其写入 CSV 文件。如果我们找到了数据(即列表不为空),我们打开输出 CSV 文件,写入标题,并循环遍历data变量中的每个列表。我们使用csvwriterows()方法将每个嵌套列表结构写入 CSV 文件。
def write_csv(data, output):
if data == []:
print("[-] No output results to write")
sys.exit(3)
print("[+] Writing output to {}".format(output))
with open(output, "wb") as csvfile:
csv_writer = csv.writer(csvfile)
headers = ["Partition", "File", "File Ext", "File Type",
"Create Date", "Modify Date", "Change Date", "Size",
"File Path"]
csv_writer.writerow(headers)
for result_list in data:
csv_writer.writerows(result_list)
以下截图演示了这个示例从取证图像中提取的数据类型:
还有更多...
这个脚本可以进一步改进。我们提供了一个或多个建议,如下所示:
-
使用
tqdm或其他库创建进度条,以通知用户当前执行的进度 -
了解可以使用
pytsk3从文件系统对象中提取的附加元数据值,并将它们添加到输出 CSV 文件中
处理容器内的文件
食谱难度:中等
Python 版本:2.7
操作系统:Linux
现在我们可以遍历文件系统,让我们看看如何创建文件对象,就像我们习惯做的那样。在这个示例中,我们创建一个简单的分流脚本,提取与指定文件扩展名匹配的文件,并将它们复制到输出目录,同时保留它们的原始文件路径。
入门
有关构建环境和pytsk3和pyewf的设置详细信息,请参考入门部分中的打开收购食谱。此脚本中使用的所有其他库都包含在 Python 的标准库中。
如何做...
在这个示例中,我们将执行以下步骤:
-
确定证据容器是
raw镜像还是E01容器。 -
使用
pytsk3访问图像。 -
递归遍历每个分区中的所有目录。
-
检查文件扩展名是否与提供的扩展名匹配。
-
将具有保留文件夹结构的响应文件写入输出目录。
它是如何工作的...
我们导入了许多库来帮助解析参数、创建 CSV 电子表格,并处理证据容器和文件系统。
from __future__ import print_function
import argparse
import csv
import os
import pytsk3
import pyewf
import sys
这个示例的命令行处理程序接受四个位置参数:EVIDENCE_FILE、TYPE、EXT和OUTPUT_DIR。它们分别是证据文件本身、证据文件类型、要提取的逗号分隔的扩展名列表,以及所需的输出目录。我们还有可选的p开关,用于手动指定分区类型。
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description=__description__,
epilog="Developed by {} on {}".format(
", ".join(__authors__), __date__)
)
parser.add_argument("EVIDENCE_FILE", help="Evidence file path")
parser.add_argument("TYPE", help="Type of Evidence",
choices=("raw", "ewf"))
parser.add_argument("EXT",
help="Comma-delimited file extensions to extract")
parser.add_argument("OUTPUT_DIR", help="Output Directory")
parser.add_argument("-p", help="Partition Type",
choices=("DOS", "GPT", "MAC", "SUN"))
args = parser.parse_args()
在调用main()函数之前,我们创建任何必要的输出目录,并执行我们的标准输入验证步骤。一旦我们验证了输入,我们将提供的参数传递给main()函数。
if not os.path.exists(args.OUTPUT_DIR):
os.makedirs(args.OUTPUT_DIR)
if os.path.exists(args.EVIDENCE_FILE) and \
os.path.isfile(args.EVIDENCE_FILE):
main(args.EVIDENCE_FILE, args.TYPE, args.EXT, args.OUTPUT_DIR,
args.p)
else:
print("[-] Supplied input file {} does not exist or is not a "
"file".format(args.EVIDENCE_FILE))
sys.exit(1)
main()函数、EWFImgInfo类和open_fs()函数在之前的配方中已经涵盖过。请记住,本章采用更迭代的方法来构建我们的配方。有关每个函数和EWFImgInfo类的更详细描述,请参考之前的配方。让我们简要地再次展示这两个函数,以避免逻辑上的跳跃。
在main()函数中,我们检查证据文件是raw文件还是E01文件。然后,我们执行必要的步骤,最终在证据文件上创建一个pytsk3句柄。有了这个句柄,我们尝试访问卷,使用手动提供的分区类型(如果提供)。如果我们能够打开卷,我们将pytsk3句柄和卷传递给open_fs()方法。
def main(image, img_type, ext, output, part_type):
volume = None
print("[+] Opening {}".format(image))
if img_type == "ewf":
try:
filenames = pyewf.glob(image)
except IOError:
_, e, _ = sys.exc_info()
print("[-] Invalid EWF format:\n {}".format(e))
sys.exit(2)
ewf_handle = pyewf.handle()
ewf_handle.open(filenames)
# Open PYTSK3 handle on EWF Image
img_info = EWFImgInfo(ewf_handle)
else:
img_info = pytsk3.Img_Info(image)
try:
if part_type is not None:
attr_id = getattr(pytsk3, "TSK_VS_TYPE_" + part_type)
volume = pytsk3.Volume_Info(img_info, attr_id)
else:
volume = pytsk3.Volume_Info(img_info)
except IOError:
_, e, _ = sys.exc_info()
print("[-] Unable to read partition table:\n {}".format(e))
open_fs(volume, img_info, ext, output)
在open_fs()函数中,我们使用逻辑来支持对文件系统进行逻辑和物理获取。对于逻辑获取,我们可以简单地尝试访问pytsk3句柄上文件系统的根。另一方面,对于物理获取,我们必须迭代每个分区,并尝试访问那些符合特定条件的文件系统。一旦我们访问到文件系统,我们调用recurse_files()方法来迭代文件系统中的所有文件。
def open_fs(vol, img, ext, output):
# Open FS and Recurse
print("[+] Recursing through files and writing file extension matches "
"to output directory")
if vol is not None:
for part in vol:
if part.len > 2048 and "Unallocated" not in part.desc \
and "Extended" not in part.desc \
and "Primary Table" not in part.desc:
try:
fs = pytsk3.FS_Info(
img, offset=part.start * vol.info.block_size)
except IOError:
_, e, _ = sys.exc_info()
print("[-] Unable to open FS:\n {}".format(e))
root = fs.open_dir(path="/")
recurse_files(part.addr, fs, root, [], [""], ext, output)
else:
try:
fs = pytsk3.FS_Info(img)
except IOError:
_, e, _ = sys.exc_info()
print("[-] Unable to open FS:\n {}".format(e))
root = fs.open_dir(path="/")
recurse_files(1, fs, root, [], [""], ext, output)
不要浏览了!这个配方的新逻辑包含在recurse_files()方法中。这有点像眨眼就错过的配方。我们已经在之前的配方中做了大部分工作,现在我们基本上可以像处理任何其他 Python 文件一样处理这些文件。让我们看看这是如何工作的。
诚然,这个函数的第一部分仍然与以前相同,只有一个例外。在函数的第一行,我们使用列表推导来分割用户提供的每个逗号分隔的扩展名,并删除任何空格并将字符串规范化为小写。当我们迭代每个对象时,我们检查对象是目录还是文件。如果是文件,我们将文件的扩展名分离并规范化为小写,并将其存储在file_ext变量中。
def recurse_files(part, fs, root_dir, dirs, parent, ext, output):
extensions = [x.strip().lower() for x in ext.split(',')]
dirs.append(root_dir.info.fs_file.meta.addr)
for fs_object in root_dir:
# Skip ".", ".." or directory entries without a name.
if not hasattr(fs_object, "info") or \
not hasattr(fs_object.info, "name") or \
not hasattr(fs_object.info.name, "name") or \
fs_object.info.name.name in [".", ".."]:
continue
try:
file_name = fs_object.info.name.name
file_path = "{}/{}".format("/".join(parent),
fs_object.info.name.name)
try:
if fs_object.info.meta.type == pytsk3.TSK_FS_META_TYPE_DIR:
f_type = "DIR"
file_ext = ""
else:
f_type = "FILE"
if "." in file_name:
file_ext = file_name.rsplit(".")[-1].lower()
else:
file_ext = ""
except AttributeError:
continue
接下来,我们检查提取的文件扩展名是否在用户提供的列表中。如果是,我们将文件对象本身及其名称、扩展名、路径和所需的输出目录传递给file_writer()方法进行输出。请注意,在这个操作中,我们有逻辑,即在前面的配方中讨论过的逻辑,来递归处理任何子目录,以识别更多符合扩展名条件的潜在文件。到目前为止,一切顺利;现在让我们来看看这最后一个函数。
if file_ext.strip() in extensions:
print("{}".format(file_path))
file_writer(fs_object, file_name, file_ext, file_path,
output)
if f_type == "DIR":
parent.append(fs_object.info.name.name)
sub_directory = fs_object.as_directory()
inode = fs_object.info.meta.addr
if inode not in dirs:
recurse_files(part, fs, sub_directory, dirs,
parent, ext, output)
parent.pop(-1)
except IOError:
pass
dirs.pop(-1)
file_writer()方法依赖于文件对象的read_random()方法来访问文件内容。然而,在这之前,我们首先设置文件的输出路径,将用户提供的输出与扩展名和文件的路径结合起来。然后,如果这些目录不存在,我们就创建这些目录。接下来,我们以"w"模式打开输出文件,现在准备好将文件的内容写入输出文件。在这里使用的read_random()函数接受两个输入:文件中要开始读取的字节偏移量和要读取的字节数。在这种情况下,由于我们想要读取整个文件,我们使用整数0作为第一个参数,文件的大小作为第二个参数。
我们直接将其提供给write()方法,尽管请注意,如果我们要对这个文件进行任何处理,我们可以将其读入变量中,并从那里处理文件。另外,请注意,对于包含大文件的证据容器,将整个文件读入内存的这个过程可能并不理想。在这种情况下,您可能希望分块读取和写入这个文件,而不是一次性全部读取和写入。
def file_writer(fs_object, name, ext, path, output):
output_dir = os.path.join(output, ext,
os.path.dirname(path.lstrip("//")))
if not os.path.exists(output_dir):
os.makedirs(output_dir)
with open(os.path.join(output_dir, name), "w") as outfile:
outfile.write(fs_object.read_random(0, fs_object.info.meta.size))
当我们运行这个脚本时,我们会看到基于提供的扩展名的响应文件:
此外,我们可以在以下截图中查看这些文件的定义结构:
搜索哈希
配方难度:困难
Python 版本:2.7
操作系统:Linux
在这个配方中,我们创建了另一个分类脚本,这次专注于识别与提供的哈希值匹配的文件。该脚本接受一个文本文件,其中包含以换行符分隔的MD5、SHA-1或SHA-256哈希,并在证据容器中搜索这些哈希。通过这个配方,我们将能够快速处理证据文件,找到感兴趣的文件,并通过将文件路径打印到控制台来提醒用户。
入门
参考打开获取配方中的入门部分,了解有关build环境和pytsk3和pyewf的设置详细信息。此脚本中使用的所有其他库都包含在 Python 的标准库中。
如何做...
我们使用以下方法来实现我们的目标:
-
确定证据容器是
raw图像还是E01容器。 -
使用
pytsk3访问图像。 -
递归遍历每个分区中的所有目录。
-
使用适当的哈希算法发送每个文件进行哈希处理。
-
检查哈希是否与提供的哈希之一匹配,如果是,则打印到控制台。
工作原理...
我们导入了许多库来帮助解析参数、创建 CSV 电子表格、对文件进行哈希处理、处理证据容器和文件系统,并创建进度条。
from __future__ import print_function
import argparse
import csv
import hashlib
import os
import pytsk3
import pyewf
import sys
from tqdm import tqdm
该配方的命令行处理程序接受三个位置参数,EVIDENCE_FILE,TYPE和HASH_LIST,分别表示证据文件,证据文件类型和要搜索的换行分隔哈希列表。与往常一样,用户也可以在必要时使用p开关手动提供分区类型。
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description=__description__,
epilog="Developed by {} on {}".format(
", ".join(__authors__), __date__)
)
parser.add_argument("EVIDENCE_FILE", help="Evidence file path")
parser.add_argument("TYPE", help="Type of Evidence",
choices=("raw", "ewf"))
parser.add_argument("HASH_LIST",
help="Filepath to Newline-delimited list of "
"hashes (either MD5, SHA1, or SHA-256)")
parser.add_argument("-p", help="Partition Type",
choices=("DOS", "GPT", "MAC", "SUN"))
parser.add_argument("-t", type=int,
help="Total number of files, for the progress bar")
args = parser.parse_args()
在解析输入后,我们对证据文件和哈希列表进行了典型的输入验证检查。如果通过了这些检查,我们调用main()函数并提供用户提供的输入。
if os.path.exists(args.EVIDENCE_FILE) and \
os.path.isfile(args.EVIDENCE_FILE) and \
os.path.exists(args.HASH_LIST) and \
os.path.isfile(args.HASH_LIST):
main(args.EVIDENCE_FILE, args.TYPE, args.HASH_LIST, args.p, args.t)
else:
print("[-] Supplied input file {} does not exist or is not a "
"file".format(args.EVIDENCE_FILE))
sys.exit(1)
与以前的配方一样,main()函数、EWFImgInfo类和open_fs()函数几乎与以前的配方相同。有关这些函数的更详细解释,请参考以前的配方。main()函数的一个新添加是第一行,我们在其中调用read_hashes()方法。该方法读取输入的哈希列表并返回哈希列表和哈希类型(即MD5、SHA-1或SHA-256)。
除此之外,main()函数的执行方式与我们习惯看到的方式相同。首先,它确定正在处理的证据文件的类型,以便在图像上创建一个pytsk3句柄。然后,它使用该句柄并尝试访问图像卷。完成此过程后,变量被发送到open_fs()函数进行进一步处理。
def main(image, img_type, hashes, part_type, pbar_total=0):
hash_list, hash_type = read_hashes(hashes)
volume = None
print("[+] Opening {}".format(image))
if img_type == "ewf":
try:
filenames = pyewf.glob(image)
except IOError:
_, e, _ = sys.exc_info()
print("[-] Invalid EWF format:\n {}".format(e))
sys.exit(2)
ewf_handle = pyewf.handle()
ewf_handle.open(filenames)
# Open PYTSK3 handle on EWF Image
img_info = EWFImgInfo(ewf_handle)
else:
img_info = pytsk3.Img_Info(image)
try:
if part_type is not None:
attr_id = getattr(pytsk3, "TSK_VS_TYPE_" + part_type)
volume = pytsk3.Volume_Info(img_info, attr_id)
else:
volume = pytsk3.Volume_Info(img_info)
except IOError:
_, e, _ = sys.exc_info()
print("[-] Unable to read partition table:\n {}".format(e))
open_fs(volume, img_info, hash_list, hash_type, pbar_total)
让我们快速看一下新函数read_hashes()方法。首先,我们将hash_list和hash_type变量实例化为空列表和None对象。接下来,我们打开并遍历输入的哈希列表,并将每个哈希添加到我们的列表中。在这样做时,如果hash_type变量仍然是None,我们检查行的长度作为识别应该使用的哈希算法类型的手段。
在此过程结束时,如果hash_type变量仍然是None,则哈希列表必须由我们不支持的哈希组成,因此在将错误打印到控制台后退出脚本。
def read_hashes(hashes):
hash_list = []
hash_type = None
with open(hashes) as infile:
for line in infile:
if hash_type is None:
if len(line.strip()) == 32:
hash_type = "md5"
elif len(line.strip()) == 40:
hash_type == "sha1"
elif len(line.strip()) == 64:
hash_type == "sha256"
hash_list.append(line.strip().lower())
if hash_type is None:
print("[-] No valid hashes identified in {}".format(hashes))
sys.exit(3)
return hash_list, hash_type
open_fs()方法函数与以前的配方相同。它尝试使用两种不同的方法来访问物理和逻辑文件系统。一旦成功,它将这些文件系统传递给recurse_files()方法。与以前的配方一样,这个函数中发生了奇迹。我们还使用tqdm来提供进度条,向用户提供反馈,因为在图像中对所有文件进行哈希可能需要一段时间。
def open_fs(vol, img, hashes, hash_type, pbar_total=0):
# Open FS and Recurse
print("[+] Recursing through and hashing files")
pbar = tqdm(desc="Hashing", unit=" files",
unit_scale=True, total=pbar_total)
if vol is not None:
for part in vol:
if part.len > 2048 and "Unallocated" not in part.desc and \
"Extended" not in part.desc and \
"Primary Table" not in part.desc:
try:
fs = pytsk3.FS_Info(
img, offset=part.start * vol.info.block_size)
except IOError:
_, e, _ = sys.exc_info()
print("[-] Unable to open FS:\n {}".format(e))
root = fs.open_dir(path="/")
recurse_files(part.addr, fs, root, [], [""], hashes,
hash_type, pbar)
else:
try:
fs = pytsk3.FS_Info(img)
except IOError:
_, e, _ = sys.exc_info()
print("[-] Unable to open FS:\n {}".format(e))
root = fs.open_dir(path="/")
recurse_files(1, fs, root, [], [""], hashes, hash_type, pbar)
pbar.close()
在recurse_files()方法中,我们遍历所有子目录并对每个文件进行哈希处理。我们跳过。和..目录条目,并检查fs_object是否具有正确的属性。如果是,我们构建文件路径以在输出中使用。
def recurse_files(part, fs, root_dir, dirs, parent, hashes,
hash_type, pbar):
dirs.append(root_dir.info.fs_file.meta.addr)
for fs_object in root_dir:
# Skip ".", ".." or directory entries without a name.
if not hasattr(fs_object, "info") or \
not hasattr(fs_object.info, "name") or \
not hasattr(fs_object.info.name, "name") or \
fs_object.info.name.name in [".", ".."]:
continue
try:
file_path = "{}/{}".format("/".join(parent),
fs_object.info.name.name)
在执行每次迭代时,我们确定哪些对象是文件,哪些是目录。对于发现的每个文件,我们将其发送到hash_file()方法,以及其路径,哈希列表和哈希算法。recurse_files()函数逻辑的其余部分专门设计用于处理目录,并对任何子目录进行递归调用,以确保整个树都被遍历并且不会错过文件。
if getattr(fs_object.info.meta, "type", None) == \
pytsk3.TSK_FS_META_TYPE_DIR:
parent.append(fs_object.info.name.name)
sub_directory = fs_object.as_directory()
inode = fs_object.info.meta.addr
# This ensures that we don't recurse into a directory
# above the current level and thus avoid circular loops.
if inode not in dirs:
recurse_files(part, fs, sub_directory, dirs,
parent, hashes, hash_type, pbar)
parent.pop(-1)
else:
hash_file(fs_object, file_path, hashes, hash_type, pbar)
except IOError:
pass
dirs.pop(-1)
hash_file()方法首先检查要创建的哈希算法实例的类型,根据hash_type变量。确定了这一点,并更新了文件大小到进度条,我们使用read_random()方法将文件的数据读入哈希对象。同样,我们通过从第一个字节开始读取并读取整个文件的大小来读取整个文件的内容。我们使用哈希对象上的hexdigest()函数生成文件的哈希,然后检查该哈希是否在我们提供的哈希列表中。如果是,我们通过打印文件路径来提醒用户,使用pbar.write()来防止进度条显示问题,并将名称打印到控制台。
def hash_file(fs_object, path, hashes, hash_type, pbar):
if hash_type == "md5":
hash_obj = hashlib.md5()
elif hash_type == "sha1":
hash_obj = hashlib.sha1()
elif hash_type == "sha256":
hash_obj = hashlib.sha256()
f_size = getattr(fs_object.info.meta, "size", 0)
pbar.set_postfix(File_Size="{:.2f}MB".format(f_size / 1024.0 / 1024))
hash_obj.update(fs_object.read_random(0, f_size))
hash_digest = hash_obj.hexdigest()
pbar.update()
if hash_digest in hashes:
pbar.write("[*] MATCH: {}\n{}".format(path, hash_digest))
通过运行脚本,我们可以看到一个漂亮的进度条,显示哈希状态和与提供的哈希列表匹配的文件列表,如下面的屏幕截图所示:
还有更多...
这个脚本可以进一步改进。我们提供了一个或多个建议,如下所示:
-
而不是打印匹配项,创建一个包含匹配文件的元数据的 CSV 文件以供审查。
-
添加一个可选开关,将匹配的文件转储到输出目录(保留文件夹路径)
第九章:探索 Windows 取证工件配方-第一部分
本章将涵盖以下配方:
-
一个人的垃圾是取证人员的宝藏
-
一个棘手的情况
-
阅读注册表
-
收集用户活动
-
缺失的链接
-
四处搜寻
介绍
长期以来,Windows 一直是 PC 市场上的首选操作系统。事实上,Windows 约占访问政府网站的用户的 47%,而第二受欢迎的 PC 操作系统 macOS 仅占 8.5%。没有理由怀疑这种情况会很快改变,特别是考虑到 Windows 10 受到的热烈欢迎。因此,未来的调查很可能会继续需要分析 Windows 工件。
本章涵盖了许多类型的工件以及如何使用 Python 和各种第一方和第三方库直接从取证证据容器中解释它们。我们将利用我们在第八章中开发的框架,处理取证证据容器配方,直接处理这些工件,而不用担心提取所需文件或挂载镜像的过程。具体来说,我们将涵盖:
-
解释
$I文件以了解发送到回收站的文件的更多信息 -
从 Windows 7 系统的便笺中读取内容和元数据
-
从注册表中提取值,以了解操作系统版本和其他配置细节
-
揭示与搜索、输入路径和运行命令相关的用户活动
-
解析 LNK 文件以了解历史和最近的文件访问
-
检查
Windows.edb以获取有关索引文件、文件夹和消息的信息
要查看更多有趣的指标,请访问analytics.usa.gov/。
访问www.packtpub.com/books/conte…下载本章的代码包。
一个人的垃圾是取证人员的宝藏
配方难度:中等
Python 版本:2.7
操作系统:Linux
虽然可能不是确切的说法,但是对于大多数调查来说,取证检查回收站中已删除文件是一个重要的步骤。非技术保管人可能不明白这些发送到回收站的文件仍然存在,我们可以了解到原始文件的很多信息,比如原始文件路径以及发送到回收站的时间。虽然特定的工件在不同版本的 Windows 中有所不同,但这个配方侧重于 Windows 7 版本的回收站的$I和$R文件。
入门
这个配方需要安装三个第三方模块才能运行:pytsk3、pyewf和unicodecsv。有关安装pytsk3和pyewf模块的详细说明,请参阅第八章,处理取证证据容器配方。此脚本中使用的所有其他库都包含在 Python 的标准库中
因为我们正在用 Python 2.x 开发这些配方,我们很可能会遇到 Unicode 编码和解码错误。为了解决这个问题,我们使用unicodecsv库来写这一章节中的所有 CSV 输出。这个第三方模块负责 Unicode 支持,不像 Python 2.x 的标准csv模块,并且在这里将得到很好的应用。和往常一样,我们可以使用pip来安装unicodecsv:
pip install unicodecsv==0.14.1
要了解更多关于unicodecsv库的信息,请访问github.com/jdunck/python-unicodecsv。
除此之外,我们将继续使用从第八章开发的pytskutil模块,与取证证据容器配方一起工作,以允许与取证获取进行交互。这个模块在很大程度上类似于我们之前编写的内容,只是对一些细微的更改以更好地适应我们的目的。您可以通过导航到代码包中的实用程序目录来查看代码。
如何做...
要解析来自 Windows 7 机器的$I和$R文件,我们需要:
-
递归遍历证据文件中的
$Recycle.bin文件夹,选择所有以$I开头的文件。 -
读取文件的内容并解析可用的元数据结构。
-
搜索相关的
$R文件并检查它是文件还是文件夹。 -
将结果写入 CSV 文件进行审查。
它是如何工作的...
我们导入argparse,datetime,os和struct内置库来帮助运行脚本并解释这些文件中的二进制数据。我们还引入了我们的 Sleuth Kit 实用程序来处理证据文件,读取内容,并遍历文件夹和文件。最后,我们导入unicodecsv库来帮助编写 CSV 报告。
from __future__ import print_function
from argparse import ArgumentParser
import datetime
import os
import struct
from utility.pytskutil import TSKUtil
import unicodecsv as csv
这个配方的命令行处理程序接受三个位置参数,EVIDENCE_FILE,IMAGE_TYPE和CSV_REPORT,分别代表证据文件的路径,证据文件的类型和所需的 CSV 报告输出路径。这三个参数被传递给main()函数。
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description=__description__,
epilog="Developed by {} on {}".format(
", ".join(__authors__), __date__)
)
parser.add_argument('EVIDENCE_FILE', help="Path to evidence file")
parser.add_argument('IMAGE_TYPE', help="Evidence file format",
choices=('ewf', 'raw'))
parser.add_argument('CSV_REPORT', help="Path to CSV report")
args = parser.parse_args()
main(args.EVIDENCE_FILE, args.IMAGE_TYPE, args.CSV_REPORT)
main()函数处理与证据文件的必要交互,以识别和提供任何用于处理的$I文件。要访问证据文件,必须提供容器的路径和图像类型。这将启动TSKUtil实例,我们使用它来搜索图像中的文件和文件夹。要找到$I文件,我们在tsk_util实例上调用recurse_files()方法,指定要查找的文件名模式,开始搜索的path和用于查找文件名的字符串logic。logic关键字参数接受以下值,这些值对应于字符串操作:startswith,endswith,contains和equals。这些指定了用于在扫描的文件和文件夹名称中搜索我们的$I模式的字符串操作。
如果找到任何$I文件,我们将此列表传递给process_dollar_i()函数,以及tsk_util对象。在它们都被处理后,我们使用write_csv()方法将提取的元数据写入 CSV 报告:
def main(evidence, image_type, report_file):
tsk_util = TSKUtil(evidence, image_type)
dollar_i_files = tsk_util.recurse_files("$I", path='/$Recycle.bin',
logic="startswith")
if dollar_i_files is not None:
processed_files = process_dollar_i(tsk_util, dollar_i_files)
write_csv(report_file,
['file_path', 'file_size', 'deleted_time',
'dollar_i_file', 'dollar_r_file', 'is_directory'],
processed_files)
else:
print("No $I files found")
process_dollar_i()函数接受tsk_util对象和发现的$I文件列表作为输入。我们遍历这个列表并检查每个文件。dollar_i_files列表中的每个元素本身都是一个元组列表,其中每个元组元素依次包含文件的名称、相对路径、用于访问文件内容的句柄和文件系统标识符。有了这些可用的属性,我们将调用我们的read_dollar_i()函数,并向其提供第三个元组,文件对象句柄。如果这是一个有效的$I文件,该方法将从原始文件中返回提取的元数据字典,否则返回None。如果文件有效,我们将继续处理它,将文件路径添加到$I文件的file_attribs字典中:
def process_dollar_i(tsk_util, dollar_i_files):
processed_files = []
for dollar_i in dollar_i_files:
# Interpret file metadata
file_attribs = read_dollar_i(dollar_i[2])
if file_attribs is None:
continue # Invalid $I file
file_attribs['dollar_i_file'] = os.path.join(
'/$Recycle.bin', dollar_i[1][1:])
接下来,我们在图像中搜索相关的$R文件。为此,我们将基本路径与$I文件(包括$Recycle.bin和SID文件夹)连接起来,以减少搜索相应$R文件所需的时间。在 Windows 7 中,$I和$R文件具有类似的文件名,前两个字母分别是$I和$R,后面是一个共享标识符。通过在我们的搜索中使用该标识符,并指定我们期望找到$R文件的特定文件夹,我们已经减少了误报的可能性。使用这些模式,我们再次使用startswith逻辑查询我们的证据文件:
# Get the $R file
recycle_file_path = os.path.join(
'/$Recycle.bin',
dollar_i[1].rsplit("/", 1)[0][1:]
)
dollar_r_files = tsk_util.recurse_files(
"$R" + dollar_i[0][2:],
path=recycle_file_path, logic="startswith"
)
如果搜索$R文件失败,我们尝试查询具有相同信息的目录。如果此查询也失败,我们将附加字典值,指出未找到$R文件,并且我们不确定它是文件还是目录。然而,如果我们找到匹配的目录,我们会记录目录的路径,并将is_directory属性设置为True:
if dollar_r_files is None:
dollar_r_dir = os.path.join(recycle_file_path,
"$R" + dollar_i[0][2:])
dollar_r_dirs = tsk_util.query_directory(dollar_r_dir)
if dollar_r_dirs is None:
file_attribs['dollar_r_file'] = "Not Found"
file_attribs['is_directory'] = 'Unknown'
else:
file_attribs['dollar_r_file'] = dollar_r_dir
file_attribs['is_directory'] = True
如果搜索$R文件返回一个或多个命中,我们使用列表推导创建一个匹配文件的列表,存储在以分号分隔的 CSV 中,并将is_directory属性标记为False。
else:
dollar_r = [os.path.join(recycle_file_path, r[1][1:])
for r in dollar_r_files]
file_attribs['dollar_r_file'] = ";".join(dollar_r)
file_attribs['is_directory'] = False
在退出循环之前,我们将file_attribs字典附加到processed_files列表中,该列表存储了所有$I处理过的字典。这个字典列表将被返回到main()函数,在报告过程中使用。
processed_files.append(file_attribs)
return processed_files
让我们简要地看一下read_dollar_i()方法,用于使用struct从二进制文件中解析元数据。我们首先通过使用 Sleuth Kit 的read_random()方法来检查文件头,读取签名的前八个字节。如果签名不匹配,我们返回None来警告$I未通过验证,是无效的文件格式。
def read_dollar_i(file_obj):
if file_obj.read_random(0, 8) != '\x01\x00\x00\x00\x00\x00\x00\x00':
return None # Invalid file
如果我们检测到一个有效的文件,我们继续从$I文件中读取和解压值。首先是文件大小属性,位于字节偏移8,长度为8字节。我们使用struct解压缩这个值,并将整数存储在一个临时变量中。下一个属性是删除时间,存储在字节偏移16和8字节长。这是一个 Windows FILETIME对象,我们将借用一些旧代码来稍后将其处理为可读的时间戳。最后一个属性是以前的文件路径,我们从字节24读取到文件的末尾:
raw_file_size = struct.unpack('<q', file_obj.read_random(8, 8))
raw_deleted_time = struct.unpack('<q', file_obj.read_random(16, 8))
raw_file_path = file_obj.read_random(24, 520)
提取了这些值后,我们将整数解释为可读的值。我们使用sizeof_fmt()函数将文件大小整数转换为可读的大小,包含诸如 MB 或 GB 的大小前缀。接下来,我们使用来自第七章的日期解析配方的逻辑来解释时间戳(在适应该函数仅使用整数后)。最后,我们将路径解码为 UTF-16 并删除空字节值。然后将这些精细的细节作为字典返回给调用函数:
file_size = sizeof_fmt(raw_file_size[0])
deleted_time = parse_windows_filetime(raw_deleted_time[0])
file_path = raw_file_path.decode("utf16").strip("\x00")
return {'file_size': file_size, 'file_path': file_path,
'deleted_time': deleted_time}
我们的sizeof_fmt()函数是从StackOverflow.com借来的,这是一个充满了许多编程问题解决方案的网站。虽然我们可以自己起草,但这段代码对我们的目的来说形式良好。它接受整数num并遍历列出的单位后缀。如果数字小于1024,则数字、单位和后缀被连接成一个字符串并返回;否则,数字除以1024并通过下一次迭代。如果数字大于 1 zettabyte,它将以 yottabytes 的形式返回信息。为了你的利益,我们希望数字永远不会那么大。
def sizeof_fmt(num, suffix='B'):
# From https://stackoverflow.com/a/1094933/3194812
for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
if abs(num) < 1024.0:
return "%3.1f%s%s" % (num, unit, suffix)
num /= 1024.0
return "%.1f%s%s" % (num, 'Yi', suffix)
我们的下一个支持函数是parse_windows_filetime(),改编自第七章中的先前日期解析配方,基于日志的证据配方。我们借用这个逻辑并将代码压缩为只解释整数并返回给调用函数的格式化日期。像我们刚刚讨论的这两个通用函数一样,它们在你的工具库中是很方便的,因为你永远不知道什么时候会需要这个逻辑。
def parse_windows_filetime(date_value):
microseconds = float(date_value) / 10
ts = datetime.datetime(1601, 1, 1) + datetime.timedelta(
microseconds=microseconds)
return ts.strftime('%Y-%m-%d %H:%M:%S.%f')
最后,我们准备将处理后的结果写入 CSV 文件。毫无疑问,这个函数与我们所有其他的 CSV 函数类似。唯一的区别是它在底层使用了unicodecsv库,尽管这里使用的方法和函数名称是相同的:
def write_csv(outfile, fieldnames, data):
with open(outfile, 'wb') as open_outfile:
csvfile = csv.DictWriter(open_outfile, fieldnames)
csvfile.writeheader()
csvfile.writerows(data)
在下面的两个屏幕截图中,我们可以看到这个配方从$I和$R文件中提取的数据的示例:
一个棘手的情况
配方难度:中等
Python 版本:2.7
操作系统:Linux
计算机已经取代了纸和笔。我们已经将许多过程和习惯转移到了这些机器上,其中一个仅限于纸张的习惯,包括做笔记和列清单。一个复制真实习惯的功能是 Windows 的便利贴。这些便利贴可以让持久的便签漂浮在桌面上,可以选择颜色、字体等选项。这个配方将允许我们探索这些便利贴,并将它们添加到我们的调查工作流程中。
开始
这个配方需要安装四个第三方模块才能运行:olefile,pytsk3,pyewf和unicodecsv。有关安装pytsk3和pyewf模块的详细说明,请参阅第八章,使用法证证据容器 配方。同样,有关安装unicodecsv的详细信息,请参阅一个人的垃圾是法医检查员的宝藏配方中的入门部分。此脚本中使用的所有其他库都包含在 Python 的标准库中。
Windows 的便利贴文件存储为OLE文件。因此,我们将利用olefile库与 Windows 的便利贴进行交互并提取数据。olefile库可以通过pip安装:
pip install olefile==0.44
要了解更多关于olefile库的信息,请访问olefile.readthedocs.io/en/latest/index.html。
如何做...
为了正确制作这个配方,我们需要采取以下步骤:
-
打开证据文件并找到所有用户配置文件中的
StickyNote.snt文件。 -
解析 OLE 流中的元数据和内容。
-
将 RTF 内容写入文件。
-
创建元数据的 CSV 报告。
它是如何工作的...
这个脚本,就像其他脚本一样,以导入所需库的导入语句开始执行。这里的两个新库是olefile,正如我们讨论的,它解析 Windows 的便利贴 OLE 流,以及StringIO,一个内置库,用于将数据字符串解释为类似文件的对象。这个库将用于将pytsk文件对象转换为olefile库可以解释的流:
from __future__ import print_function
from argparse import ArgumentParser
import unicodecsv as csv
import os
import StringIO
from utility.pytskutil import TSKUtil
import olefile
我们指定一个全局变量,REPORT_COLS,代表报告列。这些静态列将在几个函数中使用。
REPORT_COLS = ['note_id', 'created', 'modified', 'note_text', 'note_file']
这个配方的命令行处理程序需要三个位置参数,EVIDENCE_FILE,IMAGE_TYPE和REPORT_FOLDER,它们分别代表证据文件的路径,证据文件的类型和期望的输出目录路径。这与之前的配方类似,唯一的区别是REPORT_FOLDER,这是一个我们将写入便利贴 RTF 文件的目录:
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description=__description__,
epilog="Developed by {} on {}".format(
", ".join(__authors__), __date__)
)
parser.add_argument('EVIDENCE_FILE', help="Path to evidence file")
parser.add_argument('IMAGE_TYPE', help="Evidence file format",
choices=('ewf', 'raw'))
parser.add_argument('REPORT_FOLDER', help="Path to report folder")
args = parser.parse_args()
main(args.EVIDENCE_FILE, args.IMAGE_TYPE, args.REPORT_FOLDER)
我们的主要函数开始方式与上一个类似,处理证据文件并搜索我们要解析的文件。在这种情况下,我们正在寻找StickyNotes.snt文件,该文件位于每个用户的AppData目录中。因此,我们将搜索限制为/Users文件夹,并寻找与确切名称匹配的文件:
def main(evidence, image_type, report_folder):
tsk_util = TSKUtil(evidence, image_type)
note_files = tsk_util.recurse_files('StickyNotes.snt', '/Users',
'equals')
然后,我们遍历生成的文件,分离用户的主目录名称,并设置olefile库所需的类文件对象。接下来,我们调用parse_snt_file()函数处理文件,并返回一个结果列表进行遍历。在这一点上,如果note_data不是None,我们使用write_note_rtf()方法写入 RTF 文件。此外,我们将从prep_note_report()处理的数据附加到report_details列表中。一旦for循环完成,我们使用write_csv()方法写入 CSV 报告,提供报告名称、报告列和我们构建的粘贴便笺信息列表。
report_details = []
for note_file in note_files:
user_dir = note_file[1].split("/")[1]
file_like_obj = create_file_like_obj(note_file[2])
note_data = parse_snt_file(file_like_obj)
if note_data is None:
continue
write_note_rtf(note_data, os.path.join(report_folder, user_dir))
report_details += prep_note_report(note_data, REPORT_COLS,
"/Users" + note_file[1])
write_csv(os.path.join(report_folder, 'sticky_notes.csv'), REPORT_COLS,
report_details)
create_file_like_obj()函数获取我们的pytsk文件对象并读取文件的大小。这个大小在read_random()函数中用于将整个粘贴便笺内容读入内存。我们将file_content传递给StringIO()类,将其转换为olefile库可以读取的类文件对象,然后将其返回给父函数:
def create_file_like_obj(note_file):
file_size = note_file.info.meta.size
file_content = note_file.read_random(0, file_size)
return StringIO.StringIO(file_content)
parse_snt_file()函数接受类文件对象作为输入,并用于读取和解释粘贴便笺文件。我们首先验证类文件对象是否是 OLE 文件,如果不是,则返回None。如果是,我们使用OleFileIO()方法打开类文件对象。这提供了一个流列表,允许我们遍历每个粘贴便笺的每个元素。在遍历列表时,我们检查流是否包含三个破折号,因为这表明流包含粘贴便笺的唯一标识符。该文件可以包含一个或多个粘贴便笺,每个粘贴便笺由唯一的 ID 标识。粘贴便笺数据根据流的第一个索引元素的值,直接读取为 RTF 数据或 UTF-16 编码数据。
我们还使用getctime()和getmtime()函数从流中读取创建和修改的信息。接下来,我们将粘贴便笺的 RTF 或 UTF-16 编码数据提取到content变量中。注意,我们必须在存储之前解码 UTF-16 编码的数据。如果有内容要保存,我们将其添加到note字典中,并继续处理所有剩余的流。一旦所有流都被处理,note字典将返回给父函数:
def parse_snt_file(snt_file):
if not olefile.isOleFile(snt_file):
print("This is not an OLE file")
return None
ole = olefile.OleFileIO(snt_file)
note = {}
for stream in ole.listdir():
if stream[0].count("-") == 3:
if stream[0] not in note:
note[stream[0]] = {
# Read timestamps
"created": ole.getctime(stream[0]),
"modified": ole.getmtime(stream[0])
}
content = None
if stream[1] == '0':
# Parse RTF text
content = ole.openstream(stream).read()
elif stream[1] == '3':
# Parse UTF text
content = ole.openstream(stream).read().decode("utf-16")
if content:
note[stream[0]][stream[1]] = content
return note
为了创建 RTF 文件,我们将便笺数据字典传递给write_note_rtf()函数。如果报告文件夹不存在,我们使用os库来创建它。在这一点上,我们遍历note_data字典,分离note_id键和stream_data值。在打开之前,note_id用于创建输出 RTF 文件的文件名。
然后将存储在流零中的数据写入输出的 RTF 文件,然后关闭文件并处理下一个粘贴便笺:
def write_note_rtf(note_data, report_folder):
if not os.path.exists(report_folder):
os.makedirs(report_folder)
for note_id, stream_data in note_data.items():
fname = os.path.join(report_folder, note_id + ".rtf")
with open(fname, 'w') as open_file:
open_file.write(stream_data['0'])
将粘贴便笺上的内容写好后,我们现在转向prep_note_report()函数处理的 CSV 报告本身,这个函数处理方式有点不同。它将嵌套字典转换为一组更有利于 CSV 电子表格的扁平字典。我们通过包括note_id键来扁平化它,并使用全局REPORT_COLS列表中指定的键来命名字段。
def prep_note_report(note_data, report_cols, note_file):
report_details = []
for note_id, stream_data in note_data.items():
report_details.append({
"note_id": note_id,
"created": stream_data['created'],
"modified": stream_data['modified'],
"note_text": stream_data['3'].strip("\x00"),
"note_file": note_file
})
return report_details
最后,在write_csv()方法中,我们创建一个csv.Dictwriter对象来创建粘贴便笺数据的概述报告。这个 CSV 写入器还使用unicodecsv库,并将字典列表写入文件,使用REPORT_COLS列的fieldnames。
def write_csv(outfile, fieldnames, data):
with open(outfile, 'wb') as open_outfile:
csvfile = csv.DictWriter(open_outfile, fieldnames)
csvfile.writeheader()
csvfile.writerows(data)
然后我们可以查看输出,因为我们有一个包含导出的粘贴便笺和报告的新目录:
打开我们的报告,我们可以查看注释元数据并收集一些内部内容,尽管大多数电子表格查看器在处理非 ASCII 字符解释时会遇到困难:
最后,我们可以打开输出的 RTF 文件并查看原始内容:
读取注册表
食谱难度:中等
Python 版本:2.7
操作系统:Linux
Windows 注册表包含许多与操作系统配置、用户活动、软件安装和使用等相关的重要细节。由于它们包含的文物数量和与 Windows 系统的相关性,这些文件经常受到严格审查和研究。解析注册表文件使我们能够访问可以揭示基本操作系统信息、访问文件夹和文件、应用程序使用情况、USB 设备等的键和值。在这个食谱中,我们专注于从SYSTEM和SOFTWARE注册表文件中访问常见的基线信息。
入门
此食谱需要安装三个第三方模块才能正常运行:pytsk3,pyewf和Registry。有关安装pytsk3和pyewf模块的详细说明,请参阅第八章,使用取证证据容器 食谱。此脚本中使用的所有其他库都包含在 Python 的标准库中。
在这个食谱中,我们使用Registry模块以面向对象的方式与注册表文件进行交互。重要的是,该模块可用于与外部和独立的注册表文件进行交互。可以使用pip安装Registry模块:
pip install python-registry==1.0.4
要了解有关Registry库的更多信息,请访问github.com/williballenthin/python-registry。
如何做...
要构建我们的注册表系统概述脚本,我们需要:
-
通过名称和路径查找要处理的注册表文件。
-
使用
StringIO和Registry模块打开这些文件。 -
处理每个注册表文件,将解析的值打印到控制台以进行解释。
它是如何工作的...
导入与本章其他食谱重叠的导入。这些模块允许我们处理参数解析,日期操作,将文件读入内存以供Registry库使用,并解压和解释我们从注册表值中提取的二进制数据。我们还导入TSKUtil()类和Registry模块以处理注册表文件。
from __future__ import print_function
from argparse import ArgumentParser
import datetime
import StringIO
import struct
from utility.pytskutil import TSKUtil
from Registry import Registry
此食谱的命令行处理程序接受两个位置参数,EVIDENCE_FILE和IMAGE_TYPE,分别表示证据文件的路径和证据文件的类型:
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description=__description__,
epilog="Developed by {} on {}".format(
", ".join(__authors__), __date__)
)
parser.add_argument('EVIDENCE_FILE', help="Path to evidence file")
parser.add_argument('IMAGE_TYPE', help="Evidence file format",
choices=('ewf', 'raw'))
args = parser.parse_args()
main(args.EVIDENCE_FILE, args.IMAGE_TYPE)
main()函数首先通过从证据中创建一个TSKUtil对象,并在/Windows/System32/config文件夹中搜索SYSTEM和SOFTWARE注册表文件。在将它们传递给各自的处理函数之前,我们使用open_file_as_reg()函数创建这些注册表文件的Registry()类实例。
def main(evidence, image_type):
tsk_util = TSKUtil(evidence, image_type)
tsk_system_hive = tsk_util.recurse_files(
'system', '/Windows/system32/config', 'equals')
tsk_software_hive = tsk_util.recurse_files(
'software', '/Windows/system32/config', 'equals')
system_hive = open_file_as_reg(tsk_system_hive[0][2])
software_hive = open_file_as_reg(tsk_software_hive[0][2])
process_system_hive(system_hive)
process_software_hive(software_hive)
要打开注册表文件,我们需要从pytsk元数据中收集文件的大小,并将整个文件从字节零到文件末尾读入变量中。然后,我们将此变量提供给StringIO()实例,该实例允许我们使用Registry()类打开类似文件的对象。我们将Registry类实例返回给调用函数进行进一步处理:
def open_file_as_reg(reg_file):
file_size = reg_file.info.meta.size
file_content = reg_file.read_random(0, file_size)
file_like_obj = StringIO.StringIO(file_content)
return Registry.Registry(file_like_obj)
让我们从SYSTEM hive 处理开始。这个 hive 主要包含在控制集中的大部分信息。SYSTEM hive 通常有两个或更多的控制集,它们充当存储的配置的备份系统。为了简单起见,我们只读取当前的控制集。为了识别当前的控制集,我们通过root键在 hive 中找到我们的立足点,并使用find_key()方法获取Select键。在这个键中,我们读取Current值,使用value()方法选择它,并在value对象上使用value()方法来呈现值的内容。虽然方法的命名有点模糊,但键中的值是有名称的,所以我们首先需要按名称选择它们,然后再调用它们所持有的内容。使用这些信息,我们选择当前的控制集键,传递一个适当填充的整数作为当前控制集(如ControlSet0001)。这个对象将在函数的其余部分用于导航到特定的subkeys和values:
def process_system_hive(hive):
root = hive.root()
current_control_set = root.find_key("Select").value("Current").value()
control_set = root.find_key("ControlSet{:03d}".format(
current_control_set))
我们将从SYSTEM hive 中提取的第一条信息是关机时间。我们从当前控制集中读取Control\Windows\ShutdownTime值,并将十六进制值传递给struct来将其转换为64 位整数。然后我们将这个整数提供给 Windows FILETIME解析器,以获得一个可读的日期字符串,然后将其打印到控制台上。
raw_shutdown_time = struct.unpack(
'<Q', control_set.find_key("Control").find_key("Windows").value(
"ShutdownTime").value()
)
shutdown_time = parse_windows_filetime(raw_shutdown_time[0])
print("Last Shutdown Time: {}".format(shutdown_time))
接下来,我们将确定机器的时区信息。这可以在Control\TimeZoneInformation\TimeZoneKeyName值中找到。这将返回一个字符串值,我们可以直接打印到控制台上:
time_zone = control_set.find_key("Control").find_key(
"TimeZoneInformation").value("TimeZoneKeyName").value()
print("Machine Time Zone: {}".format(time_zone))
接下来,我们收集机器的主机名。这可以在Control\ComputerName\ComputerName键的ComputerName值下找到。提取的值是一个字符串,我们可以打印到控制台上:
computer_name = control_set.find_key(
"Control").find_key("ComputerName").find_key(
"ComputerName").value("ComputerName").value()
print("Machine Name: {}".format(computer_name))
到目前为止,还是相当容易的,对吧?最后,对于System hive,我们解析关于最后访问时间戳配置的信息。这个registry键确定了 NTFS 卷的最后访问时间戳是否被维护,并且通常在系统上默认情况下是禁用的。为了确认这一点,我们查找Control\FileSystem键中的NtfsDisableLastAccessUpdate值,看它是否等于1。如果是,最后访问时间戳就不会被维护,并且在打印到控制台之前标记为禁用。请注意这个一行的if-else语句,虽然可能有点难以阅读,但它确实有它的用途:
last_access = control_set.find_key("Control").find_key(
"FileSystem").value("NtfsDisableLastAccessUpdate").value()
last_access = "Disabled" if last_access == 1 else "enabled"
print("Last Access Updates: {}".format(last_access))
我们的 Windows FILETIME解析器从以前的日期解析配方中借用逻辑,接受一个整数,我们将其转换为可读的日期字符串。我们还从相同的日期解析配方中借用了Unix epoch 日期解析器的逻辑,并将用它来解释来自Software hive 的日期。
def parse_windows_filetime(date_value):
microseconds = float(date_value) / 10
ts = datetime.datetime(1601, 1, 1) + datetime.timedelta(
microseconds=microseconds)
return ts.strftime('%Y-%m-%d %H:%M:%S.%f')
def parse_unix_epoch(date_value):
ts = datetime.datetime.fromtimestamp(date_value)
return ts.strftime('%Y-%m-%d %H:%M:%S.%f')
我们的最后一个函数处理SOFTWARE hive,在控制台窗口向用户呈现信息。这个函数也是通过收集 hive 的根开始,然后选择Microsoft\Windows NT\CurrentVersion键。这个键包含有关 OS 安装元数据和其他有用的子键的值。在这个函数中,我们将提取ProductName、CSDVersion、CurrentBuild number、RegisteredOwner、RegisteredOrganization和InstallDate值。虽然这些值大多是我们可以直接打印到控制台的字符串,但在打印之前我们需要使用Unix epoch 转换器来解释安装日期值。
def process_software_hive(hive):
root = hive.root()
nt_curr_ver = root.find_key("Microsoft").find_key(
"Windows NT").find_key("CurrentVersion")
print("Product name: {}".format(nt_curr_ver.value(
"ProductName").value()))
print("CSD Version: {}".format(nt_curr_ver.value(
"CSDVersion").value()))
print("Current Build: {}".format(nt_curr_ver.value(
"CurrentBuild").value()))
print("Registered Owner: {}".format(nt_curr_ver.value(
"RegisteredOwner").value()))
print("Registered Org: {}".format(nt_curr_ver.value(
"RegisteredOrganization").value()))
raw_install_date = nt_curr_ver.value("InstallDate").value()
install_date = parse_unix_epoch(raw_install_date)
print("Installation Date: {}".format(install_date))
当我们运行这个脚本时,我们可以了解到我们解释的键中存储的信息:
还有更多...
这个脚本可以进一步改进。我们提供了一个或多个以下建议:
-
添加逻辑来处理在初始搜索中找不到
SYSTEM或SOFTWAREhive 的情况 -
考虑添加对
NTUSER.DAT文件的支持,提取有关挂载点和 shell bags 查询的基本信息 -
从
Systemhive 列出基本的 USB 设备信息 -
解析
SAMhive 以显示用户和组信息
收集用户活动
配方难度:中等
Python 版本:2.7
操作系统:Linux
Windows 存储了大量关于用户活动的信息,就像其他注册表 hive 一样,NTUSER.DAT文件是调查中可以依赖的重要资源。这个 hive 存在于每个用户的配置文件中,并存储与特定用户在系统上相关的信息和配置。
在这个配方中,我们涵盖了NTUSER.DAT中的多个键,这些键揭示了用户在系统上的操作。这包括在 Windows 资源管理器中运行的先前搜索、输入到资源管理器导航栏的路径以及 Windows“运行”命令中最近使用的语句。这些工件更好地说明了用户如何与系统进行交互,并可能揭示用户对系统的正常或异常使用看起来是什么样子。
开始
这个配方需要安装四个第三方模块才能正常工作:jinja2、pytsk3、pyewf和Registry。有关安装pytsk3和pyewf模块的详细说明,请参阅第八章,使用取证证据容器 配方。同样,有关安装Registry的详细信息,请参阅入门部分读取注册表配方。此脚本中使用的所有其他库都包含在 Python 的标准库中。
我们将重新介绍jinja2,这是在第二章中首次介绍的,创建工件报告 配方,用于构建 HTML 报告。这个库是一个模板语言,允许我们使用 Python 语法以编程方式构建文本文件。作为提醒,我们可以使用pip来安装这个库:
pip install jinja2==2.9.6
如何做...
要从图像中的NTUSER.DAT文件中提取这些值,我们必须:
-
在系统中搜索所有
NTUSER.DAT文件。 -
解析每个
NTUSER.DAT文件的WordWheelQuery键。 -
读取每个
NTUSER.DAT文件的TypedPath键。 -
提取每个
NTUSER.DAT文件的RunMRU键。 -
将每个处理过的工件写入 HTML 报告。
它是如何工作的...
我们的导入方式与之前的配方相同,添加了jinja2模块:
from __future__ import print_function
from argparse import ArgumentParser
import os
import StringIO
import struct
from utility.pytskutil import TSKUtil
from Registry import Registry
import jinja2
这个配方的命令行处理程序接受三个位置参数,EVIDENCE_FILE、IMAGE_TYPE和REPORT,分别代表证据文件的路径、证据文件的类型和 HTML 报告的期望输出路径。这三个参数被传递给main()函数。
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description=__description__,
epilog="Developed by {} on {}".format(
", ".join(__authors__), __date__)
)
parser.add_argument('EVIDENCE_FILE',
help="Path to evidence file")
parser.add_argument('IMAGE_TYPE',
help="Evidence file format",
choices=('ewf', 'raw'))
parser.add_argument('REPORT',
help="Path to report file")
args = parser.parse_args()
main(args.EVIDENCE_FILE, args.IMAGE_TYPE, args.REPORT)
main()函数首先通过读取证据文件并搜索所有NTUSER.DAT文件来开始。随后,我们设置了一个字典对象nt_rec,虽然复杂,但设计得可以简化 HTML 报告生成过程。然后,我们开始迭代发现的 hive,并从路径中解析出用户名以供处理函数参考。
def main(evidence, image_type, report):
tsk_util = TSKUtil(evidence, image_type)
tsk_ntuser_hives = tsk_util.recurse_files('ntuser.dat',
'/Users', 'equals')
nt_rec = {
'wordwheel': {'data': [], 'title': 'WordWheel Query'},
'typed_path': {'data': [], 'title': 'Typed Paths'},
'run_mru': {'data': [], 'title': 'Run MRU'}
}
for ntuser in tsk_ntuser_hives:
uname = ntuser[1].split("/")[1]
接下来,我们将pytsk文件句柄传递给Registry对象以打开。得到的对象用于收集所有所需值(Software\Microsoft\Windows\CurrentVersion\Explorer)中的root键。如果未找到此键路径,我们将继续处理下一个NTUSER.DAT文件。
open_ntuser = open_file_as_reg(ntuser[2])
try:
explorer_key = open_ntuser.root().find_key(
"Software").find_key("Microsoft").find_key(
"Windows").find_key("CurrentVersion").find_key(
"Explorer")
except Registry.RegistryKeyNotFoundException:
continue # Required registry key not found for user
如果找到了键,我们调用负责每个工件的三个处理函数,并提供共享键对象和用户名。返回的数据存储在字典中的相应数据键中。我们可以通过扩展存储对象定义并添加一个与这里显示的其他函数具有相同配置文件的新函数,轻松扩展代码解析的工件数量:
nt_rec['wordwheel']['data'] += parse_wordwheel(
explorer_key, uname)
nt_rec['typed_path']['data'] += parse_typed_paths(
explorer_key, uname)
nt_rec['run_mru']['data'] += parse_run_mru(
explorer_key, uname)
在遍历NTUSER.DAT文件之后,我们通过提取数据列表中第一项的键列表来为每种记录类型设置标题。由于数据列表中的所有字典对象都具有统一的键,我们可以使用这种方法来减少传递的参数或变量的数量。这些语句也很容易扩展。
nt_rec['wordwheel']['headers'] = \
nt_rec['wordwheel']['data'][0].keys()
nt_rec['typed_path']['headers'] = \
nt_rec['typed_path']['data'][0].keys()
nt_rec['run_mru']['headers'] = \
nt_rec['run_mru']['data'][0].keys()
最后,我们将完成的字典对象和报告文件的路径传递给我们的write_html()方法:
write_html(report, nt_rec)
我们之前在上一个示例中见过open_file_as_reg()方法。作为提醒,它接受pytsk文件句柄,并通过StringIO类将其读入Registry类。返回的Registry对象允许我们以面向对象的方式与注册表交互和读取。
def open_file_as_reg(reg_file):
file_size = reg_file.info.meta.size
file_content = reg_file.read_random(0, file_size)
file_like_obj = StringIO.StringIO(file_content)
return Registry.Registry(file_like_obj)
第一个处理函数处理WordWheelQuery键,它存储了用户在 Windows 资源管理器中运行的搜索的信息。我们可以通过从explorer_key对象中按名称访问键来解析这个遗物。如果键不存在,我们将返回一个空列表,因为我们没有任何值可以提取。
def parse_wordwheel(explorer_key, username):
try:
wwq = explorer_key.find_key("WordWheelQuery")
except Registry.RegistryKeyNotFoundException:
return []
另一方面,如果这个键存在,我们遍历MRUListEx值,它包含一个包含搜索顺序的整数列表。列表中的每个数字都与键中相同数字的值相匹配。因此,我们读取列表的顺序,并按照它们出现的顺序解释剩余的值。每个值的名称都存储为两个字节的整数,所以我们将这个列表分成两个字节的块,并用struct读取整数。然后在检查它不存在后,将这个值追加到列表中。如果它存在于列表中,并且是\x00或\xFF,那么我们已经到达了MRUListEx数据的末尾,并且跳出循环:
mru_list = wwq.value("MRUListEx").value()
mru_order = []
for i in xrange(0, len(mru_list), 2):
order_val = struct.unpack('h', mru_list[i:i + 2])[0]
if order_val in mru_order and order_val in (0, -1):
break
else:
mru_order.append(order_val)
使用我们排序后的值列表,我们遍历它以提取按顺序运行的搜索词。由于我们知道使用的顺序,我们可以将WordWheelQuery键的最后写入时间作为搜索词的时间戳。这个时间戳只与最近运行的搜索相关联。所有其他搜索都被赋予值N/A。
search_list = []
for count, val in enumerate(mru_order):
ts = "N/A"
if count == 0:
ts = wwq.timestamp()
之后,在append语句中构建字典,添加时间值、用户名、顺序(作为计数整数)、值的名称和搜索内容。为了正确显示搜索内容,我们需要将键名提供为字符串并解码文本为 UTF-16。这个文本一旦去除了空终止符,就可以用于报告。直到所有值都被处理并最终返回为止,列表将被构建出来。
search_list.append({
'timestamp': ts,
'username': username,
'order': count,
'value_name': str(val),
'search': wwq.value(str(val)).value().decode(
"UTF-16").strip("\x00")
})
return search_list
下一个处理函数处理输入的路径键,与之前的处理函数使用相同的参数。我们以相同的方式访问键,并在TypedPaths子键未找到时返回空列表。
def parse_typed_paths(explorer_key, username):
try:
typed_paths = explorer_key.find_key("TypedPaths")
except Registry.RegistryKeyNotFoundException:
return []
这个键没有 MRU 值来排序输入的路径,所以我们读取它的所有值并直接添加到列表中。我们可以从这个键中获取值的名称和路径,并为了额外的上下文添加用户名值。我们通过将字典值的列表返回给main()函数来完成这个函数。
typed_path_details = []
for val in typed_paths.values():
typed_path_details.append({
"username": username,
"value_name": val.name(),
"path": val.value()
})
return typed_path_details
我们的最后一个处理函数处理RunMRU键。如果它在explorer_key中不存在,我们将像之前一样返回一个空列表。
def parse_run_mru(explorer_key, username):
try:
run_mru = explorer_key.find_key("RunMRU")
except Registry.RegistryKeyNotFoundException:
return []
由于这个键可能是空的,我们首先检查是否有值可以解析,如果没有,就返回一个空列表,以防止进行任何不必要的处理。
if len(run_mru.values()) == 0:
return []
与WordWheelQuery类似,这个键也有一个 MRU 值,我们处理它以了解其他值的正确顺序。这个列表以不同的方式存储项目,因为它的值是字母而不是整数。这使得我们的工作非常简单,因为我们直接使用这些字符查询必要的值,而无需额外的处理。我们将值的顺序追加到列表中并继续进行。
mru_list = run_mru.value("MRUList").value()
mru_order = []
for i in mru_list:
mru_order.append(i)
当我们遍历值的顺序时,我们开始构建我们的结果字典。首先,我们以与我们的WordWheelQuery处理器相同的方式处理时间戳,通过分配默认的N/A值并在我们有序列表中的第一个条目时更新它的键的最后写入时间。在此之后,我们附加一个包含相关条目的字典,例如用户名、值顺序、值名称和值内容。一旦我们处理完Run键中的所有剩余值,我们将返回这个字典列表。
mru_details = []
for count, val in enumerate(mru_order):
ts = "N/A"
if count == 0:
ts = run_mru.timestamp()
mru_details.append({
"username": username,
"timestamp": ts,
"order": count,
"value_name": val,
"run_statement": run_mru.value(val).value()
})
return mru_details
最后一个函数处理 HTML 报告的创建。这个函数首先准备代码的路径和jinja2环境类。这个类用于在库中存储共享资源,并且我们用它来指向库应该搜索模板文件的目录。在我们的情况下,我们希望它在当前目录中查找模板 HTML 文件,所以我们使用os库获取当前工作目录并将其提供给FileSystemLoader()类。
def write_html(outfile, data_dict):
cwd = os.path.dirname(os.path.abspath(__file__))
env = jinja2.Environment(loader=jinja2.FileSystemLoader(cwd))
在环境配置好后,我们调用我们想要使用的模板,然后使用render()方法创建一个带有我们传递的字典的 HTML 文件。render函数返回一个表示渲染的 HTML 输出的字符串,其中包含处理数据插入的结果,我们将其写入输出文件。
template = env.get_template("user_activity.html")
rendering = template.render(nt_data=data_dict)
with open(outfile, 'w') as open_outfile:
open_outfile.write(rendering)
让我们来看一下模板文件,它像任何 HTML 文档一样以html、head和body标签开头。虽然我们在head标签中包含了脚本和样式表,但我们在这里省略了不相关的材料。这些信息可以在代码包中完整查看。
我们用一个包含处理过的数据表和部分标题的div开始 HTML 文档。为了简化我们需要编写的 HTML 量,我们使用一个for循环来收集nt_data值中的每个嵌套字典。jinja2模板语言允许我们仍然使用 Python 循环,只要它们被包裹在花括号、百分号和空格字符中。我们还可以引用对象的属性和方法,这使我们能够在不需要额外代码的情况下遍历nt_data字典的值。
另一个常用的模板语法显示在h2标签中,我们在其中访问了main()函数中设置的 title 属性。我们希望jinja2引擎解释的变量(而不是显示为字面字符串)需要用双花括号和空格字符括起来。现在这将为我们的nt_data字典中的每个部分打印部分标题。
<html>
<head>...</head>
<body>
<div class="container">
{% for nt_content in nt_data.values() %}
<h2>{{ nt_content['title'] }}</h2>
在这个循环中,我们使用data标签设置我们的数据表,并创建一个新行来容纳表头。为了生成表头,我们遍历收集到的每个表头,并在嵌套的for循环中分配值。请注意,我们需要使用endfor语句指定循环的结束;这是模板引擎所要求的,因为(与 Python 不同)它对缩进不敏感:
<table class="table table-hover table-condensed">
<tr>
{% for header in nt_content['headers'] %}
<th>{{ header }}</th>
{% endfor %}
<tr/>
在表头之后,我们进入一个单独的循环,遍历我们数据列表中的每个字典。在每个表行内,我们使用与表头相似的逻辑来创建另一个for循环,将每个值写入行中的单元格:
{% for entry in nt_content['data'] %}
<tr>
{% for header in nt_content['headers'] %}
<td>{{ entry[header] }}</td>
{% endfor %}
</tr>
现在 HTML 数据表已经填充,我们关闭当前数据点的for循环:我们画一条水平线,并开始编写下一个工件的数据表。一旦我们完全遍历了这些,我们关闭外部的for循环和我们在 HTML 报告开头打开的标签。
{% endfor %}
</table>
<br />
<hr />
<br />
{% endfor %}
</div>
</body>
</html>
我们生成的报告如下:
还有更多...
这个脚本可以进一步改进。我们提供了以下一个或多个建议:
-
在仪表板上添加额外的
NTUser或其他易于审查的工件,以便一目了然地提供更多有用的信息 -
使用各种 JavaScript 和 CSS 元素在仪表板上添加图表、时间轴或其他交互元素
-
从仪表板提供导出选项到 CSV 或 Excel 电子表格,并附加 JavaScript
缺失的链接
食谱难度:中等
Python 版本:2.7
操作系统:Linux
快捷方式文件,也称为链接文件,在操作系统平台上很常见。它们使用户可以使用一个文件引用另一个文件,该文件位于系统的其他位置。在 Windows 平台上,这些链接文件还记录了对它们引用的文件的历史访问。通常,链接文件的创建时间代表具有该名称的文件的第一次访问时间,修改时间代表具有该名称的文件的最近访问时间。利用这一点,我们可以推断出一个活动窗口,并了解这些文件是如何以及在哪里被访问的。
入门
此食谱需要安装三个第三方模块才能正常运行:pytsk3、pyewf和pylnk。有关安装pytsk3和pyewf模块的详细说明,请参阅第八章,使用法证证据容器 食谱。此脚本中使用的所有其他库都包含在 Python 的标准库中。
导航到 GitHub 存储库并下载所需版本的pylnk库。此处使用的是pylnk-alpha-20170111版本。接下来,一旦提取了发布的内容,打开终端并导航到提取的目录,执行以下命令:
./synclibs.sh
./autogen.sh
sudo python setup.py install
要了解更多关于pylnk库的信息,请访问github.com/libyal/liblnk。
最后,我们可以通过打开 Python 解释器,导入pylnk,并运行gpylnk.get_version()方法来检查我们的库的安装,以确保我们有正确的发布版本。
如何做...
此脚本将利用以下步骤:
-
在系统中搜索所有
lnk文件。 -
遍历发现的
lnk文件并提取相关属性。 -
将所有工件写入 CSV 报告。
工作原理...
从导入开始,我们引入 Sleuth Kit 实用程序和pylnk库。我们还引入了用于参数解析、编写 CSV 报告和StringIO读取 Sleuth Kit 对象作为文件的库:
from __future__ import print_function
from argparse import ArgumentParser
import csv
import StringIO
from utility.pytskutil import TSKUtil
import pylnk
此食谱的命令行处理程序接受三个位置参数,EVIDENCE_FILE、IMAGE_TYPE和CSV_REPORT,分别代表证据文件的路径、证据文件的类型和 CSV 报告的期望输出路径。这三个参数将传递给main()函数。
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description=__description__,
epilog="Developed by {} on {}".format(
", ".join(__authors__), __date__)
)
parser.add_argument('EVIDENCE_FILE', help="Path to evidence file")
parser.add_argument('IMAGE_TYPE', help="Evidence file format",
choices=('ewf', 'raw'))
parser.add_argument('CSV_REPORT', help="Path to CSV report")
args = parser.parse_args()
main(args.EVIDENCE_FILE, args.IMAGE_TYPE, args.CSV_REPORT)
main()函数从创建TSKUtil对象开始,该对象用于解释证据文件并遍历文件系统以查找以lnk结尾的文件。如果在系统上找不到任何lnk文件,则脚本会提醒用户并退出。否则,我们指定代表我们要为每个lnk文件存储的数据属性的列。虽然还有其他可用的属性,但这些是我们在此食谱中提取的一些更相关的属性:
def main(evidence, image_type, report):
tsk_util = TSKUtil(evidence, image_type)
lnk_files = tsk_util.recurse_files("lnk", path="/", logic="endswith")
if lnk_files is None:
print("No lnk files found")
exit(0)
columns = [
'command_line_arguments', 'description', 'drive_serial_number',
'drive_type', 'file_access_time', 'file_attribute_flags',
'file_creation_time', 'file_modification_time', 'file_size',
'environmental_variables_location', 'volume_label',
'machine_identifier', 'local_path', 'network_path',
'relative_path', 'working_directory'
]
接下来,我们遍历发现的lnk文件,使用open_file_as_lnk()函数将每个文件作为文件打开。返回的对象是pylnk库的一个实例,可以让我们读取属性。我们使用文件的名称和路径初始化属性字典,然后遍历我们在main()函数中指定的列。对于每个列,我们尝试读取指定的属性值,如果无法读取,则存储N/A值。这些属性存储在lnk_data字典中,一旦提取了所有属性,就将其附加到parsed_lnks列表中。完成每个lnk文件的这个过程后,我们将此列表与输出路径和列名一起传递给write_csv()方法。
parsed_lnks = []
for entry in lnk_files:
lnk = open_file_as_lnk(entry[2])
lnk_data = {'lnk_path': entry[1], 'lnk_name': entry[0]}
for col in columns:
lnk_data[col] = getattr(lnk, col, "N/A")
lnk.close()
parsed_lnks.append(lnk_data)
write_csv(report, columns + ['lnk_path', 'lnk_name'], parsed_lnks)
要将我们的pytsk文件对象作为pylink对象打开,我们使用open_file_as_lnk()函数,该函数类似于本章中的其他同名函数。此函数使用read_random()方法和文件大小属性将整个文件读入StringIO缓冲区,然后将其传递给pylnk文件对象。以这种方式读取允许我们以文件的形式读取数据,而无需将其缓存到磁盘。一旦我们将文件加载到我们的lnk对象中,我们将其返回给main()函数:
def open_file_as_lnk(lnk_file):
file_size = lnk_file.info.meta.size
file_content = lnk_file.read_random(0, file_size)
file_like_obj = StringIO.StringIO(file_content)
lnk = pylnk.file()
lnk.open_file_object(file_like_obj)
return lnk
最后一个函数是常见的 CSV 写入器,它使用csv.DictWriter类来遍历数据结构,并将相关字段写入电子表格。在main()函数中定义的列列表的顺序决定了它们在这里作为fieldnames参数的顺序。如果需要,可以更改该顺序,以修改它们在生成的电子表格中显示的顺序。
def write_csv(outfile, fieldnames, data):
with open(outfile, 'wb') as open_outfile:
csvfile = csv.DictWriter(open_outfile, fieldnames)
csvfile.writeheader()
csvfile.writerows(data)
运行脚本后,我们可以在单个 CSV 报告中查看结果,如下两个屏幕截图所示。由于有许多可见列,我们选择仅显示一些以便阅读:
还有更多...
这个脚本可以进一步改进。我们提供了一个或多个建议如下:
-
添加检查以查看目标文件是否仍然存在
-
识别远程或可移动卷上的目标位置
-
添加对解析跳转列表的支持
四处搜寻
食谱难度:困难
Python 版本:2.7
操作系统:Linux
大多数现代操作系统都维护着系统中存储的文件和其他数据内容的索引。这些索引允许在系统卷上更有效地搜索文件格式、电子邮件和其他内容。在 Windows 上,这样的索引可以在Windows.edb文件中找到。这个数据库以可扩展存储引擎(ESE)文件格式存储,并位于ProgramData目录中。我们将利用libyal项目的另一个库来解析这个文件,以提取有关系统上索引内容的信息。
入门
此食谱需要安装四个第三方模块才能运行:pytsk3、pyewf、pyesedb和unicodecsv。有关安装pytsk3和pyewf模块的详细说明,请参阅第八章中的使用取证证据容器 食谱。同样,有关安装unicodecsv的详细信息,请参阅一个人的垃圾是取证人员的宝藏食谱中的入门部分。此脚本中使用的所有其他库都包含在 Python 的标准库中。
转到 GitHub 存储库,并下载每个库的所需版本。此食谱是使用libesedb-experimental-20170121版本开发的。提取版本的内容后,打开终端,转到提取的目录,并执行以下命令:
./synclibs.sh
./autogen.sh
sudo python setup.py install
要了解更多关于pyesedb库的信息,请访问https://github.com/libyal/libesedb。
最后,我们可以通过打开 Python 解释器,导入pyesedb,并运行epyesedb.get_version()方法来检查我们的库安装是否正确。
操作步骤...
起草此脚本,我们需要:
-
递归搜索
ProgramData目录,查找Windows.edb文件。 -
遍历发现的
Windows.edb文件(虽然实际上应该只有一个),并使用pyesedb库打开文件。 -
处理每个文件以提取关键列和属性。
-
将这些关键列和属性写入报告。
工作原理...
这里导入的库包括我们在本章大多数配方中使用的用于参数解析、字符串缓冲文件样对象和TSK实用程序的库。我们还导入unicodecsv库来处理 CSV 报告中的任何 Unicode 对象,datetime库来辅助时间戳解析,以及struct模块来帮助理解我们读取的二进制数据。此外,我们定义了一个全局变量COL_TYPES,它将pyesedb库中的列类型别名,用于帮助识别我们稍后在代码中将提取的数据类型:
from __future__ import print_function
from argparse import ArgumentParser
import unicodecsv as csv
import datetime
import StringIO
import struct
from utility.pytskutil import TSKUtil
import pyesedb
COL_TYPES = pyesedb.column_types
该配方的命令行处理程序接受三个位置参数,EVIDENCE_FILE,IMAGE_TYPE和CSV_REPORT,它们分别表示证据文件的路径,证据文件的类型以及所需的 CSV 报告输出路径。这三个参数被传递给main()函数。
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description=__description__,
epilog="Developed by {} on {}".format(
", ".join(__authors__), __date__)
)
parser.add_argument('EVIDENCE_FILE', help="Path to evidence file")
parser.add_argument('IMAGE_TYPE', help="Evidence file format",
choices=('ewf', 'raw'))
parser.add_argument('CSV_REPORT', help="Path to CSV report")
args = parser.parse_args()
main(args.EVIDENCE_FILE, args.IMAGE_TYPE, args.CSV_REPORT)
main()函数打开证据并搜索ProgramData目录中的Windows.edb文件。如果找到一个或多个文件,我们会遍历列表并打开每个 ESE 数据库,以便使用process_windows_search()函数进行进一步处理。该函数返回要使用的电子表格列标题以及包含报告中要包含的数据的字典列表。然后将此信息写入输出 CSV,供write_csv()方法审查:
def main(evidence, image_type, report):
tsk_util = TSKUtil(evidence, image_type)
esedb_files = tsk_util.recurse_files(
"Windows.edb",
path="/ProgramData/Microsoft/Search/Data/Applications/Windows",
logic="equals"
)
if esedb_files is None:
print("No Windows.edb file found")
exit(0)
for entry in esedb_files:
ese = open_file_as_esedb(entry[2])
if ese is None:
continue # Invalid ESEDB
report_cols, ese_data = process_windows_search(ese)
write_csv(report, report_cols, ese_data)
读取响应的 ESE 数据库需要open_file_as_esedb()函数。此代码块使用与之前配方类似的逻辑,将文件读入StringIO对象并使用库打开文件样对象。请注意,如果文件相当大或您的计算机内存较少,这可能会在您的系统上引发错误。您可以使用内置的tempfile库将文件缓存到磁盘上的临时位置,然后从那里读取,如果您愿意的话。
def open_file_as_esedb(esedb):
file_size = esedb.info.meta.size
file_content = esedb.read_random(0, file_size)
file_like_obj = StringIO.StringIO(file_content)
esedb = pyesedb.file()
try:
esedb.open_file_object(file_like_obj)
except IOError:
return None
return esedb
我们的process_windows_search()函数从列定义开始。虽然我们之前的配方使用了一个简单的列列表,但pyesedb库需要一个列索引作为输入,以从表中的行中检索值。因此,我们的列列表必须由元组组成,其中第一个元素是数字(索引),第二个元素是字符串描述。由于描述在函数中未用于选择列,我们将其命名为我们希望它们在报告中显示的方式。对于本配方,我们已定义了以下列索引和名称:
def process_windows_search(ese):
report_cols = [
(0, "DocID"), (286, "System_KindText"),
(35, "System_ItemUrl"), (5, "System_DateModified"),
(6, "System_DateCreated"), (7, "System_DateAccessed"),
(3, "System_Size"), (19, "System_IsFolder"),
(2, "System_Search_GatherTime"), (22, "System_IsDeleted"),
(61, "System_FileOwner"), (31, "System_ItemPathDisplay"),
(150, "System_Link_TargetParsingPath"),
(265, "System_FileExtension"), (348, "System_ComputerName"),
(34, "System_Communication_AccountName"),
(44, "System_Message_FromName"),
(43, "System_Message_FromAddress"), (49, "System_Message_ToName"),
(47, "System_Message_ToAddress"),
(62, "System_Message_SenderName"),
(189, "System_Message_SenderAddress"),
(52, "System_Message_DateSent"),
(54, "System_Message_DateReceived")
]
在我们定义感兴趣的列之后,我们访问SystemIndex_0A表,其中包含索引文件、邮件和其他条目。我们遍历表中的记录,为每个记录构建一个record_info字典,其中包含每个记录的列值,最终将其附加到table_data列表中。第二个循环遍历我们之前定义的列,并尝试提取每个记录中的列的值和值类型。
table = ese.get_table_by_name("SystemIndex_0A")
table_data = []
for record in table.records:
record_info = {}
for col_id, col_name in report_cols:
rec_val = record.get_value_data(col_id)
col_type = record.get_column_type(col_id)
使用我们之前定义的COL_TYPES全局变量,我们可以引用各种数据类型,并确保我们正确解释值。以下代码块中的逻辑侧重于根据其数据类型正确解释值。首先,我们处理日期,日期可能存储为 Windows FILETIME值。我们尝试转换FILETIME值(如果可能),或者如果不可能,则以十六进制呈现日期值。接下来的语句检查文本值,使用pyesedb的get_value_data_as_string()函数或作为 UTF-16 大端,并替换任何未识别的字符以确保完整性。
然后,我们使用pyesedb的get_value_data_as_integer()函数和一个简单的比较语句分别处理整数和布尔数据类型的解释。具体来说,我们检查rec_val是否等于"\x01",并允许根据该比较将rec_val设置为True或False。如果这些数据类型都不合法,我们将该值解释为十六进制,并在将该值附加到表之前将其与相关列名一起存储:
if col_type in (COL_TYPES.DATE_TIME, COL_TYPES.BINARY_DATA):
try:
raw_val = struct.unpack('>q', rec_val)[0]
rec_val = parse_windows_filetime(raw_val)
except Exception:
if rec_val is not None:
rec_val = rec_val.encode('hex')
elif col_type in (COL_TYPES.TEXT, COL_TYPES.LARGE_TEXT):
try:
rec_val = record.get_value_data_as_string(col_id)
except Exception:
rec_val = rec_val.decode("utf-16-be", "replace")
elif col_type == COL_TYPES.INTEGER_32BIT_SIGNED:
rec_val = record.get_value_data_as_integer(col_id)
elif col_type == COL_TYPES.BOOLEAN:
rec_val = rec_val == '\x01'
else:
if rec_val is not None:
rec_val = rec_val.encode('hex')
record_info[col_name] = rec_val
table_data.append(record_info)
然后,我们将一个元组返回给我们的调用函数,其中第一个元素是report_cols字典中列的名称列表,第二个元素是数据字典的列表。
return [x[1] for x in report_cols], table_data
借鉴我们在第七章中日期解析食谱中的逻辑,基于日志的工件食谱,我们实现了一个将 Windows FILETIME值解析为可读状态的函数。这个函数接受一个整数值作为输入,并返回一个可读的字符串:
def parse_windows_filetime(date_value):
microseconds = float(date_value) / 10
ts = datetime.datetime(1601, 1, 1) + datetime.timedelta(
microseconds=microseconds)
return ts.strftime('%Y-%m-%d %H:%M:%S.%f')
最后一个函数是 CSV 报告编写器,它使用DictWriter类将收集到的信息的列和行写入到打开的 CSV 电子表格中。虽然我们在一开始选择了一部分可用的列,但还有许多可供选择的列,可能对不同的案例类型有用。因此,我们建议查看所有可用的列,以更好地理解这个食谱,以及哪些列对您可能有用或无用。
def write_csv(outfile, fieldnames, data):
with open(outfile, 'wb') as open_outfile:
csvfile = csv.DictWriter(open_outfile, fieldnames)
csvfile.writeheader()
csvfile.writerows(data)
运行食谱后,我们可以查看这里显示的输出 CSV。由于这份报告有很多列,我们在接下来的两个屏幕截图中突出显示了一些有趣的列:
还有更多...
这个脚本可以进一步改进。我们提供了一个或多个以下建议:
-
添加支持以检查引用文件和文件夹的存在。
-
使用 Python 的
tempfile库将我们的Windows.edb文件写入临时位置,以减轻解析大型数据库时的内存压力 -
在表中添加更多列或创建单独的(有针对性的)报告,使用表中超过 300 个可用列中的更多列