精通-Pyrhon-取证-全-

精通 Pyrhon 取证(全)

原文:annas-archive.org/md5/89513dcba4f06502fb29f333da05d5b3

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

今天,信息技术已成为几乎所有事物的一部分,围绕我们而存在。这些系统是我们穿戴的设备,也是支撑我们建设和运营城市、公司、个人在线购物之旅以及人际关系的系统。这些系统具有吸引力,并且容易被滥用。因此,所有犯罪领域,如盗窃、欺诈、勒索等,都扩展到了信息技术领域。如今,这已经成为一个价值数十亿的、全球性的犯罪阴影产业。

一个人能发现由一个价值数十亿、犯罪、全球性的阴影产业所进行的犯罪或可疑活动的痕迹吗?嗯,有时候你是可以的。要分析现代犯罪,你不需要放大镜或从酒瓶上提取指纹。相反,我们将看到如何运用你的 Python 技能,深入查看文件系统中最有前景的地方,并从黑客留下的痕迹中提取数字指纹。

作为作者,我们相信实际示例的力量,而非陈旧的理论。这就是为什么我们提供法医工具和脚本的示例,这些示例足够简短,普通的 Python 程序员也能理解,但又是可以在实际 IT 法医工作中使用的工具和构建模块。

你准备好将怀疑转化为确凿的事实了吗?

本书内容概览

第一章,设置实验室与 Python ctypes 介绍,介绍了如何设置你的环境,以便跟随本书中的示例进行学习。我们将了解支持法医分析的各种 Python 模块。通过 ctypes,我们提供了一种超越 Python 模块的手段,利用本地系统库的能力。

第二章,法医算法,为你提供了数字版的指纹提取方法。就像经典的指纹一样,我们将向你展示如何将数字指纹与已知的好坏样本的大型数据库进行比较。这将帮助你聚焦分析,并提供法医有效性的证据。

第三章,使用 Python 进行 Windows 和 Linux 法医分析,是你理解数字证据之旅的第一步。我们将提供示例,帮助你检测 Windows 和 Linux 系统中存在的妥协迹象。最后,我们将通过一个例子展示如何在法医分析中使用机器学习算法。

第四章,使用 Python 进行网络法医分析,专注于捕获和分析网络流量。通过提供的工具,你可以搜索和分析网络流量,寻找外泄迹象或恶意软件通信的特征。

第五章,使用 Python 进行虚拟化取证,解释了现代虚拟化概念如何被攻击者和取证分析师使用。因此,我们将展示如何在虚拟机管理程序级别找到恶意行为的痕迹,并利用虚拟化层作为可靠的取证数据来源。

第六章,使用 Python 进行移动取证,将向你展示如何从移动设备中获取和分析取证数据。示例将包括分析 Android 设备和 Apple iOS 设备。

第七章,使用 Python 进行内存取证,展示了如何获取内存快照,并使用 Linux 和 Android 对这些 RAM 镜像进行取证分析。在 LiME 和 Volatility 等工具的帮助下,我们将演示如何从系统内存中提取信息。

本书的需求

本书所需的所有设备是一台配置有 Python 2.7 环境并具备正常互联网连接的 Linux 工作站。第一章,实验室设置及 Python ctypes 简介,将指导你安装额外的 Python 模块和工具。我们使用的所有工具均可从互联网免费下载。我们的示例源代码可从 Packt Publishing 获取。

若要跟随第五章,使用 Python 进行虚拟化取证的示例,你可能需要设置一个 VMware vSphere 虚拟化环境。所需的软件可以从 VMware 获取,作为时间有限的试用版,没有功能限制。

虽然并非严格要求,但我们建议在废弃的移动设备上尝试第六章,使用 Python 进行移动取证中的一些示例。作为首次实验,请避免使用正在使用的个人或商务手机。

本书适合的读者

本书适合 IT 管理员、IT 运维人员和分析师,他们希望在数字证据的收集和分析方面获得深厚的技能。如果你已经是取证专家,本书将帮助你扩展在虚拟化或移动设备等新领域的知识。

为了最大限度地利用本书,你应该具备一定的 Python 技能,并至少了解一些你取证目标的内部工作原理。例如,一些文件系统的细节。

术语约定

本书中你会看到多种文本样式,以区分不同类型的信息。以下是这些样式的一些示例及其含义的解释。

文本中的代码字、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 账号会这样显示:“请注意,在 Windows 中,msvcrt 是 MS 标准 C 库,包含大多数标准 C 函数,并使用 cdecl 调用约定(在 Linux 系统中,相似的库是 libc.so.6)。”

代码块设置如下:

def multi_hash(filename):
    """Calculates the md5 and sha256 hashes
       of the specified file and returns a list
       containing the hash sums as hex strings."""

当我们希望引起您对代码块中特定部分的注意时,相关行或项目会以粗体显示:

<Event ><System><Provider Name="Microsoft-Windows-Security-Auditing" Guid="54849625-5478-4994-a5ba-3e3b0328c30d"></Provider>
<EventID Qualifiers="">4724</EventID>
<Version>0</Version>
<Level>0</Level>
<Task>13824</Task>

任何命令行输入或输出如下所示:

user@lab:~$ virtualenv labenv
New python executable in labenv/bin/python
Installing setuptools, pip...done.

新术语重要词汇以粗体显示。您在屏幕上看到的词汇,例如在菜单或对话框中,文本中会这样显示:“当要求选择系统日志时,确保选择了所有日志类型。”

注意

警告或重要提示会以类似这样的框显示。

提示

提示和技巧以这样的形式出现。

读者反馈

我们始终欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢或不喜欢的地方。读者反馈对我们非常重要,它帮助我们开发出您真正能从中获益的书籍。

要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及书名。

如果您在某个话题上有专长,并且有兴趣编写或参与编写书籍,请查看我们的作者指南:www.packtpub.com/authors

客户支持

现在,您是 Packt 书籍的骄傲拥有者,我们有一些方法可以帮助您充分利用您的购买。

下载示例代码

您可以从您的帐户下载所有已购买的 Packt 出版书籍的示例代码文件,网址是www.packtpub.com。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

勘误

虽然我们已尽最大努力确保内容的准确性,但错误仍然会发生。如果您在我们的一本书中发现错误——无论是文本还是代码中的错误——我们将非常感激您能向我们报告。通过这样做,您可以避免其他读者的困扰,并帮助我们改进后续版本的书籍。如果您发现任何勘误,请访问www.packtpub.com/submit-errata报告,选择您的书籍,点击勘误提交表单链接,并输入勘误的详细信息。一旦您的勘误被验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该书籍的勘误列表中。

要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索框中输入书名。相关信息将在勘误部分显示。

盗版

互联网版权材料的盗版问题在各类媒体中普遍存在。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上发现任何形式的非法复制作品,请立即向我们提供网址或网站名称,以便我们采取措施。

请通过<copyright@packtpub.com>与我们联系,并附上涉嫌盗版材料的链接。

感谢您的帮助,保护我们的作者以及让我们能为您提供有价值的内容。

问题

如果您在本书的任何方面遇到问题,可以通过<questions@packtpub.com>联系我们,我们将尽力解决问题。

第一章:搭建实验室并介绍 Python ctypes

网络安全数字取证是两个日益重要的话题。尤其是数字取证,它变得越来越重要,不仅在执法调查中扮演着重要角色,在事件响应领域也越来越关键。在所有前述的调查中,了解安全漏洞的根本原因、系统故障或犯罪行为至关重要。数字取证在克服这些挑战中起着关键作用。

在本书中,我们将教你如何搭建自己的实验室,并借助 Python 进行深入的数字取证调查,这些调查涉及多个平台和系统。我们将从常见的 Windows 和 Linux 桌面机器开始,然后进阶到云平台和虚拟化平台,最终涉及到手机。我们不仅会展示如何检查静态数据或传输中的数据,还将深入探讨易失性内存。

Python 提供了一个出色的开发平台,适合构建自己的调查工具,因为它简化了复杂性、提高了效率、拥有大量的第三方库,并且易于阅读和编写。在阅读本书的过程中,你不仅会学习如何使用最常见的 Python 库和扩展来分析证据,还会学到如何编写自己的脚本和辅助工具,以便在需要分析大量证据的案件或事件中更加高效。

让我们通过搭建实验室环境来开始掌握 Python 取证的旅程,接着简要介绍 Python ctypes。

如果你已经使用过 Python ctypes并且有一个可用的实验室环境,可以跳过第一章,直接开始阅读其他章节。第一章之后,其他章节相对独立,可以按任意顺序阅读。

搭建实验室

作为我们脚本和调查的基础,我们需要一个全面且强大的实验室环境,能够处理大量不同类型和结构的文件,并支持与移动设备的连接。为了实现这个目标,我们将使用最新的 Ubuntu LTS 版本 14.04.2,并将其安装在虚拟机(VM)中。在接下来的章节中,我们将解释虚拟机的设置,并介绍 Python virtualenv,我们将用它来建立我们的工作环境。

Ubuntu

为了在类似的实验室环境中工作,我们建议你从www.ubuntu.com/download/desktop/下载最新的 Ubuntu LTS 桌面版,最好是 32 位版本。该发行版提供了一个易于使用的界面,并且已经安装并预配置了 Python 2.7.6 环境。在本书中,我们将使用 Python 2.7.x,而不是更新的 3.x 版本。本书中的一些示例和案例研究将依赖于已经包含在 Ubuntu 发行版中的工具或库。当本书的某一章节或部分需要第三方软件包或库时,我们会提供如何在virtualenv(该环境的设置将在下一节中解释)或在 Ubuntu 上安装它的额外信息。

为了更好的系统性能,我们建议用于实验室的虚拟机至少配备 4 GB 的临时内存和约 40 GB 的存储空间。

Ubuntu

图 1:Atom 编辑器

要编写你的第一个 Python 脚本,你可以使用简单的编辑器,如vi,或是功能强大但杂乱的集成开发环境(IDE),如eclipse。作为一个非常强大的替代方案,我们建议你使用atom,一个非常简洁但高度可定制的编辑器,您可以从atom.io/免费下载。

Python 虚拟环境(virtualenv)

根据官方的 Python 文档,虚拟环境是通过为不同的项目创建虚拟 Python 环境来将它们所需的依赖项存储在不同的位置。它解决了“项目 X 依赖于 1.x 版本,而项目 Y 需要 4.x 版本”的困境,并保持你的全局 site-packages 目录干净且易于管理。

这也是我们在接下来的章节中使用的方式,以确保所有读者都能拥有统一的环境,并避免出现兼容性问题。首先,我们需要安装virtualenv包。可以通过以下命令完成安装:

user@lab:~$ pip install virtualenv

现在我们将在用户的主目录下创建一个文件夹,用于我们的虚拟 Python 环境。这个目录将包含可执行的 Python 文件和 pip 库的副本,后者可以用于在环境中安装其他软件包。虚拟环境的名称(在我们的例子中为labenv)可以由你选择。我们可以通过执行以下命令来创建我们的虚拟实验室环境:

user@lab:~$ virtualenv labenv
New python executable in labenv/bin/python
Installing setuptools, pip...done.

要开始使用新的实验室环境,首先需要激活它。可以通过以下方式进行激活:

user@lab:~$ source labenv/bin/activate
(labenv)user@lab:~$

现在,你可以看到命令提示符以我们激活的虚拟环境名称开始。从现在起,你使用 pip 安装的任何软件包将被放置在labenv文件夹中,与底层 Ubuntu 中的全局 Python 安装隔离。

在本书中,我们将使用这个虚拟 Python 环境,并不时地在其中安装新包和库。因此,每次你尝试回顾示例时,记得或者挑战自己在运行脚本之前切换到 labenv 环境。

如果你目前已经完成了虚拟环境中的工作,并且想返回到你的“正常” Python 环境,可以通过执行以下命令来停用虚拟环境:

(labenv)user@lab:~$ deactivate
user@lab:~$

这会将你带回系统默认的 Python 解释器,并保留其所有已安装的库和依赖项。

如果你在多个虚拟机或物理机上进行调查,虚拟环境可以帮助你保持所有工作站的库和包同步。为了确保你的环境一致,最好“冻结”当前环境包的状态。为此,只需运行:

(labenv)user@lab:~$ pip freeze > requirenments.txt

这将创建一个 requirements.txt 文件,文件中包含当前环境中所有包及其各自版本的简单列表。如果你想在另一台机器上安装相同版本的相同包,只需将 requirements.txt 文件复制到目标机器,创建如前所述的 labenv 环境并执行以下命令:

(labenv)user@lab:~$ pip install -r requirements.txt

现在,你将在所有机器上拥有一致的 Python 环境,无需担心不同的库版本或其他依赖项。

在我们创建了带有专用实验室环境的 Ubuntu 虚拟机后,我们几乎准备好开始第一次取证分析。但在此之前,我们需要更多关于有用的 Python 库和背景知识。因此,我们将从下面的 Python ctypes 介绍开始。

Python ctypes 介绍

根据官方 Python 文档,ctypes 是一个外部函数库,提供 C 兼容的数据类型,并允许调用 DLL 或共享库中的函数。外部函数库意味着 Python 代码可以仅使用 Python 调用 C 函数,而无需特殊或定制的扩展。

该模块是 Python 开发者可以使用的最强大的库之一。ctypes 库不仅可以用于调用动态链接库中的函数(如前所述),还可以用于低级别的内存操作。了解如何使用 ctypes 库的基础非常重要,因为它将在本书的许多示例和实际案例中使用。

在接下来的章节中,我们将介绍一些 Python ctypes 的基本特性以及如何使用它们。

使用动态链接库

Python 的ctypes导出了cdll和 Windows 上的windlloledll对象,用于加载请求的动态链接库。动态链接库是一个在运行时与可执行主进程链接的已编译二进制文件。在 Windows 平台上,这些二进制文件被称为动态链接库DLL),在 Linux 上,则称为共享对象SO)。你可以通过将这些链接库作为cdllwindlloledll对象的属性来加载它们。现在,我们将演示一个简短的例子,展示如何在 Windows 和 Linux 上直接通过time函数获取当前时间(该函数位于libc库中,这个库定义了系统调用和其他基本设施,如openprintfexit)。

请注意,在 Windows 的情况下,msvcrt是包含大多数标准 C 函数的 MS 标准 C 库,并使用cdecl调用约定(在 Linux 系统上,相似的库是libc.so.6):

C:\Users\Admin>python

>>> from ctypes import *
>>> libc = cdll.msvcrt
>>> print libc.time(None)
1428180920

Windows 会自动附加常见的.dll文件后缀。在 Linux 上,必须指定文件名,包括扩展名,以加载所选的库。可以使用 DLL 加载器的LoadLibrary()方法,或者通过调用构造函数创建CDLL实例来加载库,如下代码所示:

(labenv)user@lab:~$ python

>>> from ctypes import *
>>> libc = CDLL("libc.so.6")
>>> print libc.time(None)
1428180920

如这两个示例所示,调用动态库并使用导出的函数非常简单。你将在整本书中多次使用这种技术,因此理解它的工作原理非常重要。

C 数据类型

详细查看前一部分的两个示例时,你会发现我们在动态链接的 C 库中使用了None作为其中一个参数。这是可能的,因为Noneintegerslongsbyte stringsunicode strings是可以直接作为这些函数调用参数使用的本地 Python 对象。None被当作 C 语言的NULL 指针byte stringsunicode strings作为指向包含数据的内存块的指针(char *wchar_t *)传递。Python 的integerslongs作为平台的默认 C 语言int 类型传递,它们的值会被掩码以适应 C 类型。Python 类型及其对应的 ctype 类型的完整概览可以见于表 1

ctypes 类型 C 类型 Python 类型
c_bool (docs.python.org/2/library/ctypes.html#ctypes.c_bool) _Bool 布尔值(1)
c_char (docs.python.org/2/library/ctypes.html#ctypes.c_char) char 1 个字符的字符串
c_wchar (docs.python.org/2/library/ctypes.html#ctypes.c_wchar) wchar_t 1 个字符的 Unicode 字符串
c_byte (docs.python.org/2/library/ctypes.html#ctypes.c_byte) 字符型 int/long
c_ubyte (docs.python.org/2/library/ctypes.html#ctypes.c_ubyte) 无符号字符型 int/long
c_short (docs.python.org/2/library/ctypes.html#ctypes.c_short) 短整型 int/long
c_ushort (docs.python.org/2/library/ctypes.html#ctypes.c_ushort) 无符号短整型 int/long
c_int (docs.python.org/2/library/ctypes.html#ctypes.c_int) 整型 int/long
c_uint (docs.python.org/2/library/ctypes.html#ctypes.c_uint) 无符号整型 int/long
c_long (docs.python.org/2/library/ctypes.html#ctypes.c_long) 长整型 int/long
c_ulong (docs.python.org/2/library/ctypes.html#ctypes.c_ulong) 无符号长整型 int/long
c_longlong (docs.python.org/2/library/ctypes.html#ctypes.c_longlong) __int64 或长长整型 int/long
c_ulonglong (docs.python.org/2/library/ctypes.html#ctypes.c_ulonglong) 无符号 __int64 或无符号长长整型 int/long
c_float (docs.python.org/2/library/ctypes.html#ctypes.c_float) 浮动型 浮动型
c_double (docs.python.org/2/library/ctypes.html#ctypes.c_double) 双精度型 浮动型
c_longdouble (docs.python.org/2/library/ctypes.html#ctypes.c_longdouble) 长双精度 浮动型
c_char_p (docs.python.org/2/library/ctypes.html#ctypes.c_char_p) 字符串 *(以 NUL 结尾) 字符串或 None
c_wchar_p (docs.python.org/2/library/ctypes.html#ctypes.c_wchar_p) wchar_t *(以 NUL 结尾) unicode 或 None
c_void_p (docs.python.org/2/library/ctypes.html#ctypes.c_void_p) void * int/long 或 None

表 1:基本数据类型

这张表格非常有用,因为除 整数字符串unicode 字符串 外,所有 Python 类型都必须包装在相应的 ctypes 类型中,以便它们可以转换为链接库中所需的 C 数据类型,而不会抛出 TypeError 异常,如以下代码所示:

(labenv)user@lab:~$ python

>>> from ctypes import *
>>> libc = CDLL("libc.so.6")
>>> printf = libc.printf

>>> printf("An int %d, a double %f\n", 4711, 47.11)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
ctypes.ArgumentError: argument 3: <type 'exceptions.TypeError'>: Don't know how to convert parameter 3

>>> printf("An int %d, a double %f\n", 4711, c_double(47.11))
An int 4711, a double 47.110000

定义联合体和结构体

联合体结构体是重要的数据类型,因为它们在 Linux 上的 libc 以及 Microsoft Win32 API 中经常被使用。

联合体只是一个变量的集合,这些变量可以是相同或不同的数据类型,所有成员共享相同的内存位置。通过这种方式存储变量,联合体允许你以不同的类型指定相同的值。对于接下来的示例,我们将从交互式 Python shell 切换到 Ubuntu 实验环境中的 atom 编辑器。你只需要打开 atom 编辑器,输入以下代码,并将其保存为 new_evidence.py

from ctypes import *

class case(Union):
        _fields_ = [
        ("evidence_int", c_int),
        ("evidence_long", c_long),
        ("evidence_char", c_char * 4)
        ]

value = raw_input("Enter new evidence number:")
new_evidence = case(int(value))
print "Evidence number as a int: %i" % new_evidence.evidence_int
print "Evidence number as a long: %ld" % new_evidence.evidence_long
print "Evidence number as a char: %s" % new_evidence.evidence_char

如果你为 evidence 联合体的成员变量 evidence_int 赋值 42,你就可以使用 evidence_char 成员来显示该数字的字符表示,如下例所示:

(labenv)user@lab:~$ python new_evidence.py

Enter new evidence number:42

Evidence number as a long: 42
Evidence number as a int: 42
Evidence number as a char: *

正如你在前面的示例中看到的,通过为联合体分配一个值,你可以得到该值的三种不同表示方式。对于 intlong,显示的输出是显而易见的,但对于 evidence_char 变量来说,可能会有点令人困惑。在这种情况下,'*' 是 ASCII 字符,其值等于十进制的 42evidence_char 成员变量是如何在 ctypes 中定义 array 的一个很好例子。在 ctypes 中,数组是通过将类型乘以你想要在数组中分配的元素数量来定义的。在这个例子中,为成员变量 evidence_char 定义了一个四元素的字符数组。

结构体与联合体非常相似,但成员之间不共享同一内存位置。你可以使用点符号访问结构体中的任何成员变量,比如 case.name。这将访问 case 结构体中的 name 变量。以下是一个非常简短的示例,演示如何创建一个包含三个成员的 结构体(或称 struct):namenumberinvestigator_name,以便通过点符号访问它们:

from ctypes import *

class case(Structure):
        _fields_ = [
        ("name", c_char * 16),
        ("number", c_int),
        ("investigator_name", c_char * 8)
        ]

提示

下载示例代码

你可以从你在 www.packtpub.com 的帐户下载所有你购买的 Packt Publishing 书籍的示例代码文件。如果你是在其他地方购买的这本书,你可以访问 www.packtpub.com/support 并注册,让文件直接通过电子邮件发送给你。

概述

在第一章,我们创建了实验环境:一个运行Ubuntu 14.04.2 LTS的虚拟机。这个步骤非常重要,因为你现在可以在处理真实证据之前创建快照,并且在调查完成后可以恢复到一个干净的机器状态。这尤其在处理被破坏的系统备份时非常有用,这样你可以确保在处理不同的案例时系统是干净的。

在本章的第二部分,我们演示了如何使用 Python 的虚拟环境(virtualenv),这些环境将在全书中被使用和扩展。

在本章的最后一节,我们向你介绍了 Python 的ctypes,这是一个非常强大的库,供 Python 开发者使用。通过这些ctypes,你不仅能够调用动态链接库中的函数(如微软 Win32 API 或常见的 Linux 共享对象),还可以用于低级内存操作。

完成本章后,你将创建一个基本环境,供本书后续章节使用,同时你也将理解 Python ctypes的基础知识,这在接下来的章节中将非常有帮助。

第二章。法医算法

法医算法是法医调查员的基本工具。无论具体实现如何,这些算法描述了法医程序的细节。在本章的第一节中,我们将介绍用于法医调查的不同算法,包括它们的优缺点。

算法

在本节中,我们将描述MD5SHA256SSDEEP之间的主要区别——这些是法医调查中最常用的算法。我们将解释这些算法的使用场景,以及它们背后的局限性和威胁。这将帮助你理解为什么使用 SHA256 比使用 MD5 更好,以及在什么情况下 SSDEEP 可以帮助你进行调查。

在深入探讨不同的哈希函数之前,我们将简要总结一下什么是加密哈希函数。

哈希函数是一个将任意大量数据映射到一个固定长度值的函数。哈希函数确保相同的输入始终产生相同的输出,这个输出称为哈希值。因此,哈希值是特定数据的特征。

加密哈希函数是一个被认为在实践中几乎不可能被反转的哈希函数。这意味着,不可能通过除非尝试所有可能的输入值(即暴力破解)以外的任何其他方式创建具有预定义哈希值的输入数据。因此,这类算法被称为单向加密算法。

理想的加密哈希函数有四个主要特性,具体如下:

  1. 必须能够轻松地计算给定输入的哈希值。

  2. 必须不可行从哈希值生成原始输入。

  3. 必须不可行在不改变哈希值的情况下修改输入。

  4. 必须不可行找到两个不同的输入,它们具有相同的哈希值(抗碰撞)。

在理想情况下,如果你对给定的输入创建哈希并且只更改了一个比特,那么新计算出的哈希将完全不同,如下所示:

user@lab:~$ echo -n This is a test message | md5sum
fafb00f5732ab283681e124bf8747ed1

user@lab:~$ echo -n This is A test message | md5sum
aafb38820e0a3788eb41e9f5805e088e

如果满足前面提到的所有特性,则该算法是加密上正确的哈希函数,可以用来比较文件等,以证明它们在分析或被攻击者篡改时没有被修改。

MD5

MD5 消息摘要算法曾是最常用(并且仍然广泛使用)的加密哈希函数,它产生一个 128 位(16 字节)的哈希值,通常以 32 位十六进制数字的文本格式表示(如前面的示例所示)。这种消息摘要已广泛应用于各种加密应用中,并常用于法医调查中验证数据完整性。该算法由 Ronald Rivest 于 1991 年设计,并自那时以来被广泛使用。

MD5 的一个重要优点是计算速度快且生成的哈希值较小。当您需要在法医调查中存储数千个这些哈希时,小哈希是一个主要关注点。想象一下普通个人电脑硬盘上可能会有多少文件。如果您需要计算每个文件的哈希并将其存储在数据库中,那么每个计算出的哈希长度为 16 字节或 32 字节将产生巨大差异。

如今,MD5 的主要缺点是它不再被认为是抗碰撞的。这意味着可以从两个不同的输入计算出相同的哈希。请记住,无法仅通过比较调查中不同阶段的文件的 MD5 哈希来保证文件未被修改。目前可以非常快速地创建碰撞(参见 www.win.tue.nl/hashclash/On%20Collisions%20for%20MD5%20-%20M.M.J.%20Stevens.pdf),但仍然很难修改文件,使其成为原文件的恶意版本,并保持原始文件的 MD5 哈希。

非常著名的密码学家布鲁斯·施奈尔曾经写道 (www.schneier.com/blog/archives/2008/12/forging_ssl_cer.html):

"我们早就知道 MD5 是一种已经被攻破的哈希函数","没有人应该再使用 MD5 了"。

我们不会走得那么远(特别是因为许多工具和服务仍在使用 MD5),但在关键情况下,您应尝试切换到 SHA256 或至少使用不同哈希函数来双重检查您的结果。在关键时刻,建议使用多种哈希算法来证明数据的完整性。

SHA256

SHA-2 是由美国国家安全局(NSA)设计的一组密码哈希函数,代表安全哈希算法第二代。它于 2001 年由美国国家标准与技术研究院(NIST)发布为美国联邦标准(FIPS)。SHA-2 家族包括几种哈希函数,其摘要(哈希值)长度介于 224 位到 512 位之间。SHA256 和 SHA512 是使用 32 位和 64 位字计算的最常见版本的 SHA-2 哈希函数。

尽管这些算法计算速度较慢且计算出的哈希值较大(与 MD5 相比),但它们应该是在法医调查中用于完整性检查的首选算法。如今,SHA256 是广泛使用的密码哈希函数,仍然具有抗碰撞能力和完全可信任。

SSDEEP

MD5SHA256SSDEEP 最大的区别在于 SSDEEP 并不被认为是一种 密码哈希函数,因为当输入变化一个比特时它只会略微改变。例如:

user@lab:~$ echo -n This is a test message | ssdeep
ssdeep,1.1--blocksize:hash:hash,filename
3:hMCEpFzA:hurs,"stdin"

user@lab:~$ echo -n This is A test message | ssdeep
ssdeep,1.1--blocksize:hash:hash,filename
3:hMCkrzA:hOrs,"stdin"

SSDEEP 软件包可以按照以下网址描述的步骤下载并安装:ssdeep.sourceforge.net/usage.html#install

这种行为不是 SSDEEP 的弱点,而是该功能的一个主要优点。实际上,SSDEEP 是一个计算和匹配上下文触发的分段哈希CTPH)值的程序。CTPH 是一种也被称为模糊哈希的技术,能够匹配具有同源性的输入。具有同源性的输入在给定的顺序中有相同的字节序列,中间则是完全不同的字节。这些字节之间的内容和长度可以不同。CTPH 最初基于Andrew Tridgell 博士的研究,由Jesse Kornblum进行了改进,并在 2006 年在 DFRWS 会议上以《使用上下文触发的分段哈希识别几乎相同的文件》为题发表;参见dfrws.org/2006/proceedings/12-Kornblum.pdf

SSDEEP 可以用来检查两个文件的相似性以及文件中差异所在的部分。这个功能通常用于检查移动设备上的两个不同应用是否具有共同的代码基础,如下所示:

user@lab:~$ ssdeep -b malware-sample01.apk > signature.txt

user@lab:~$ cat signature.txt
Ssdeep,1.1--blocksize:hash:hash,filename
49152:FTqSf4xGvFowvJxThCwSoVpzPb03++4zlpBFrnInZWk:JqSU4ldVVpDIcz3BFr8Z7,"malware-sample01.apk"

user@lab:~$ ssdeep –mb signature.txt malware-sample02.apk
malware-sample02.apk matches malware-sample01.apk (75)

在之前的示例中,你可以看到第二个样本与第一个样本非常相似。这些匹配表明潜在的源代码重用,或者至少表明 apk 文件中的大量文件是相同的。需要手动检查相关文件,才能确切知道代码或文件的哪些部分是相同的;然而,我们现在知道这两个文件是相似的。

支持链条证据的完整性

法医调查的结果可能对组织和个人产生严重影响。根据你的工作领域,你的调查可能成为法庭上的证据。

因此,法医证据的完整性不仅在收集证据时需要确保,而且在整个处理和分析过程中也要得到保证。通常,法医调查的第一步是收集证据。通常,这是通过对原始介质进行逐位复制来完成的。所有后续的分析工作都在这个法医副本上进行。

创建完整磁盘映像的哈希值

为了确保法医副本与原始介质完全相同,需要对介质和法医副本进行哈希值计算。这些哈希值必须匹配,以证明副本与原始数据完全相同。如今,至少使用两种不同的加密哈希算法已成为常见做法,以最小化哈希碰撞的风险,并增强整个过程对哈希碰撞攻击的防护能力。

使用 Linux,可以轻松地从驱动器或多个文件创建 MD5 和 SHA256 哈希。在下面的示例中,我们将计算两个文件的 MD5 值和 SHA256 值,以提供相同内容的证明:

user@lab:~$ md5sum /path/to/originalfile /path/to/forensic_copy_of_sdb.img

user@lab:~$ sha256sum /path/to/originalfile /path/to/forensic_copy_of_sdb.img

这种相同内容的证明是支持保全链的必要条件,即证明分析的数据与磁盘上的原始数据完全相同。术语sdb指的是连接到法医工作站的驱动器(在 Linux 中,第二硬盘被称为sdb)。为了进一步支持保全链,强烈建议在证据和法医工作站之间使用写保护设备,以避免任何意外修改证据。第二个参数表示证据的逐位副本的位置。命令输出原始硬盘和副本的哈希值。如果 MD5 值和 SHA256 值都匹配,那么副本可以被视为法医上有效的。

尽管前面示例中的方法有效,但它有一个很大的缺点——证据及其副本需要被读取两次以计算哈希值。如果硬盘是 1 TB 的大硬盘,这会使整体过程延长几个小时。

以下 Python 代码只读取一次数据,并将其输入到两个哈希计算中。因此,这个 Python 脚本的运行速度几乎是运行md5sum再运行sha256sum的两倍,并且产生与这些工具完全相同的哈希值:

#!/usr/bin/env python

import hashlib
import sys

def multi_hash(filename):
    """Calculates the md5 and sha256 hashes
       of the specified file and returns a list
       containing the hash sums as hex strings."""

    md5 = hashlib.md5()
    sha256 = hashlib.sha256()

    with open(filename, 'rb') as f:
        while True:
            buf = f.read(2**20)
            if not buf:
                break
            md5.update(buf)
            sha256.update(buf)

    return [md5.hexdigest(), sha256.hexdigest()]

if __name__ == '__main__':
    hashes = []
    print '---------- MD5 sums ----------'
    for filename in sys.argv[1:]:
        h = multi_hash(filename)
        hashes.append(h)
        print '%s  %s' % (h[0], filename)

    print '---------- SHA256 sums ----------'
    for i in range(len(hashes)):
        print '%s  %s' % (hashes[i][1], sys.argv[i+1])

在以下脚本调用中,我们计算了一些常见 Linux 工具的哈希值:

user@lab:~$ python multihash.py /bin/{bash,ls,sh}
---------- MD5 sums ----------
d79a947d06958e7826d15a5c78bfaa05  /bin/bash
fa97c59cc414e42d4e0e853ddf5b4745  /bin/ls
c01bc66da867d3e840814ec96a137aef  /bin/sh
---------- SHA256 sums ----------
cdbcb2ef76ae464ed0b22be346977355c650c5ccf61fef638308b8da60780bdd  /bin/bash
846ac0d6c40d942300de825dbb5d517130d8a0803d22115561dcd85efee9c26b  /bin/ls
e9a7e1fd86f5aadc23c459cb05067f49cd43038f06da0c1d9f67fbcd627d622c  /bin/sh

在法医报告中记录原始数据和法医副本的哈希值非常重要。独立方可以读取相同的证据并确认你分析的数据与证据中的数据完全一致。

创建目录树的哈希值

一旦完整的镜像被复制,其内容应该被索引,并为每个文件创建哈希值。借助先前定义的multi_hash函数和 Python 标准库,可以创建一个报告模板,其中包含所有文件的名称、大小和哈希值,如下所示:

#!/usr/bin/env python

from datetime import datetime
import os
from os.path import join, getsize
import sys
from multihash import multi_hash

def dir_report(base_path, reportfilename):
    """Creates a report containing file integrity information.

    base_path -- The directory with the files to index
    reportfilename -- The file to write the output to"""

    with open(reportfilename, 'w') as out:
        out.write("File integrity information\n\n")
        out.write("Base path:      %s\n" % base_path)
        out.write("Report created: %s\n\n" % datetime.now().isoformat())
        out.write('"SHA-256","MD5","FileName","FileSize"')
        out.write("\n")

        for root, dirs, files in os.walk(base_path):
            write_dir_stats(out, root, files)

        out.write("\n\n--- END OF REPORT ---\n")

def write_dir_stats(out, directory, files):
    """Writes status information on all specified files to the report.

    out -- open file handle of the report file
    directory -- the currently analyzed directory
    files -- list of files in that directory"""

    for name in files:
        fullname = join(directory, name)
        hashes = multi_hash(fullname)
        size = getsize(fullname)
        out.write('"%s","%s","%s",%d' % (hashes[1], hashes[0], fullname, size))
        out.write("\n")

if __name__ == '__main__':
    if len(sys.argv) < 3:
        print "Usage: %s reportfile basepath\n" % sys.argv[0]
        sys.exit(1)

    dir_report(sys.argv[2], sys.argv[1])

这个 Python 脚本可以生成一个目录树的完整性信息,包括文件大小、文件名和哈希值(SHA256、MD5)。以下是我们脚本目录中的一个示例调用:

user@lab:/home/user/dirhash $ python dirhash.py report.txt .
user@lab:/home/user/dirhash $ cat report.txt
File integrity information

Base path:      .
Report created: 2015-08-23T21:50:45.460940

"SHA-256","MD5","FileName","FileSize"
"a14f7e644d76e2e232e94fd720d35e59707a2543f01af4123abc46e8c10330cd","9c0d1f70fffe5c59a7700b2b9bfd50cc","./multihash.py",879
"a4168e4cc7f8db611b339f4f8a949fbb57ad893f02b9a65759c793d2c8b9b4aa","bcf5a41a403bb45974dd0ee331b1a0aa","./dirhash.py",1494
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","d41d8cd98f00b204e9800998ecf8427e","./report.txt",0
"03047d8a202b03dfc5a310a81fd8358f37c8ba97e2fff8a0e7822cf7f36b5c83","416699861031e0b0d7b6d24b3de946ef","./multihash.pyc",1131

--- END OF REPORT ---

然而,生成的报告文件本身没有任何完整性保护。建议对生成的报告进行签名,例如,使用GnuPG,如下所示:

user@lab:~$ gpg --clearsign report.txt

如果你从未使用过gpg,你需要先生成一个私钥,然后才能对文档进行签名。可以使用gpg --gen-key命令来完成这一步。有关 GnuPG 及其使用的更多详细信息,请参考www.gnupg.org/documentation。这将生成一个额外的report.txt.asc文件,包含原始报告和数字签名。对该文件的任何后续修改都会使数字签名无效。

注意

这里描述的技术仅仅是如何支持证据链的示例。如果法医分析将用于法院,强烈建议寻求法律咨询,以了解您所在法域关于证据链的要求。

真实世界场景

本节将展示一些使用前述算法和技术来支持调查员的用例。对于本章,我们使用了两个非常常见且有趣的示例,移动恶意软件国家软件参考库NSRL)。

移动恶意软件

在本示例中,我们将检查安卓智能手机上已安装的应用程序,并与一个在线分析系统Mobile-Sandbox进行对比。Mobile-Sandbox 是一个提供免费安卓文件病毒检查或可疑行为检查的网站,www.mobilesandbox.org。它与VirusTotal相连接,VirusTotal 使用最多 56 种不同的杀毒产品和扫描引擎来检查用户的杀毒软件可能遗漏的病毒,或验证任何虚假阳性。此外,Mobile-Sandbox 使用自定义技术来检测可能具有恶意行为的应用程序。Mobile-Sandbox 背后的杀毒软件供应商、开发者和研究人员可以接收文件副本,以帮助改进他们的软件和技术。

在本示例中,我们将使用两个步骤成功地将已安装的应用程序与 Mobile-Sandbox 网络服务上已测试的应用程序进行比较。

第一步是获取设备上已安装应用程序的哈希值。这个步骤非常重要,因为这些值可以帮助识别应用程序,并将其与在线服务进行比对。在本示例中,我们将使用来自 Google Play 的一个应用程序,AppExtractplay.google.com/store/apps/details?id=de.mspreitz.appextract)。获取这些值的法医正确方法可以参考第六章,使用 Python 进行移动取证

移动恶意软件

AppExtract for Android 生成一份已安装和正在运行的应用程序列表,并附带大量元数据,帮助识别不需要的甚至恶意的应用程序。这些元数据包含应用程序包的 SHA256 哈希值、指示应用程序是由用户安装还是系统安装的标志,以及许多额外的数据,有助于判断应用程序是否为良性。这些列表可以通过您喜欢的邮件应用程序进行转发,供进一步分析。一旦收到包含生成列表的纯文本邮件,您只需将包含所有已安装应用程序的列表复制到 CSV 文件中。该文件可以用于自动化分析,或者在实验室环境中用LibreOffice Calc打开。您可以在下面看到当前版本的 Android Chrome 浏览器的元数据:

Type;App_Name;md5;TargetSdkVersion;Package_Name;Process_Name;APK_Location;Version_Code;Version_Name;Certificate_Info;Certificate_SN;InstallTime;LastModified

SystemApp;Chrome;4e4c56a8a7d8d6b1ec3e0149b3918656;21;com.android.chrome;com.android.chrome;/data/app/com.android.chrome-2.apk;2311109;42.0.2311.109;CN=Android, OU=Android, O=Google Inc., L=Mountain View, ST=California, C=US;14042372374541250701;unknown;unknown

第二步是将设备的哈希值(CSV 文件中的第三列)与 Mobile-Sandbox 数据库进行比较。可以使用以下脚本完成,我们将其保存为get_infos_mobilesandbox.py

#!/usr/bin/env python

import sys, requests

# Authentication Parameters
# if you need an API key and user name please contact @m_spreitz
API_FORMAT = 'json'
API_USER = ''
API_KEY = ''

# parsing input parameters
if (len(sys.argv) < 3):
    print "Get infos to a specific Android app from the Mobile-Sandbox."
    print "Usage: %s requests [type (md5,sha256)] [value]" % sys.argv[0]
    sys.exit(0)

# building the payload
payload = {'format':API_FORMAT,
           'username':API_USER,
           'api_key':API_KEY,
           'searchType':str(sys.argv[1]),   # has to be md5 or sha256
           'searchValue':str(sys.argv[2])}

# submitting sample hash and getting meta data
print "--------------------------------"
r = requests.get("http://mobilesandbox.org/api/bot/queue/get_info/", params=payload)

# printing result and writing report file to disk
if not r.status_code == requests.codes.ok:
    print "query result: \033[91m" + r.text + "\033[0m"
else:
    for key, value in r.json().iteritems():
        print key + ": \033[94m" + str(value) + "\033[0m"
print "--------------------------------"

脚本可以如下所示使用:

(labenv)user@lab:~$ ./get_infos_mobilesandbox.py md5 4e4c56a8a7d8d6b1ec3e0149b3918656

--------------------------------
status: done
min_sdk_version: 0
package_name: com.android.chrome
apk_name: Chrome.apk
AV_detection_rate: 0 / 56
drebin_score: benign (1.38173)
sample_origin: user upload
android_build_version: Android 1.0
ssdeep: 196608:ddkkKqfC+ca8eE/jXQewwn5ux1aDn9PpvPBic6aQmAHQXPOo:dBKZaJYXQE5u3ajtpvpeaQm1
sha256: 79de1dc6af66e6830960d6f991cc3e416fd3ce63fb786db6954a3ccaa7f7323c
malware_family: ---
md5: 4e4c56a8a7d8d6b1ec3e0149b3918656
--------------------------------

利用这三个工具,可以快速检查移动设备上的应用程序是否可能被感染(请参阅响应中的突出部分),或者至少在应用程序之前未经测试时可以从何处开始手动调查。

NSRL 查询

为了提高取证分析效率,重要的是筛选出属于已知软件且未被修改的任何文件。国家软件参考库NSRL)维护多个已知内容的哈希值列表。NSRL 是美国国土安全部的一个项目,更多细节请参阅www.nsrl.nist.gov/。重要的是要理解,这些哈希值列表仅表明文件与提交给 NSRL 的版本相比未被修改。因此,在取证调查期间要分析的许多文件可能不在 NSRL 中列出。另一方面,即使列出的文件也可能被攻击者用作工具。例如,psexec.exe 这样的工具是微软提供的用于远程管理的程序,在 NSRL 中有列出。然而,攻击者可能将其用于恶意目的。

提示

应该使用哪个 NSRL 列表?

NSRL 包括几个哈希集合。建议首先使用最小集。这个集合每个文件只包含一个哈希值,这意味着只知道一个文件版本。

最小集可以在 NIST 主页免费下载。下载包括一个单独的 ZIP 文件,其中包含哈希列表和支持的软件产品列表作为最突出的内容。

这些哈希值存储在NSRLFile.txt文件中,每行一个文件哈希值,例如:

"3CACD2048DB88F4F2E863B6DE3B1FD197922B3F2","0BEA3F79A36B1F67B2CE0F595524C77C","C39B9F35","TWAIN.DLL",94784,14965,"358",""

此记录的字段如下:

  • 使用 SHA-1 计算的文件哈希值,这是早期的 SHA-256 算法描述之前的前身。

  • 使用 MD5 计算的文件哈希值。

  • 文件的 CRC32 校验和。

  • 文件名。

  • 文件的字节大小。

  • 产品代码,表示该文件所属的软件产品。NSRLProd.txt 文件包含所有产品的列表,可以用来查找产品代码。在前面的示例中,代码14965表示微软的 Picture It!。

  • 此文件所在的操作系统。操作系统代码列表可以在NSRLOS.txt中找到。

  • 一个指示符,用于指示该文件是否被视为正常("")、恶意文件("N")或特殊文件("S")。尽管此标志是规范的一部分,当前 NSRL 最小集中的所有文件均设置为正常。

有关文件规范的更多细节可以在 www.nsrl.nist.gov/Documents/Data-Formats-of-the-NSRL-Reference-Data-Set-16.pdf 中找到。

下载并安装 nsrlsvr

当前,NSRL 数据库包含超过 4000 万个不同的哈希值,属于最小集合。即使在最新的工作站上,基于文本的搜索也需要几分钟。因此,进行高效的数据库查找至关重要。Rob Hanson 的工具 nsrlsvr 提供了一个支持高效查找的服务器。可以在 rjhansen.github.io/nsrlsvr/ 上找到该工具。

注意

互联网上也有公共的 NSRL 服务器可供使用。这些服务器通常是 按现状 提供的。但是,若要测试较小的哈希集,可以使用 Robert Hanson 的公共服务器 nsrllookup.com,然后继续阅读下一部分内容。

要在 Linux 系统上编译软件,必须安装 automake、autoconf 和 c++ 编译工具。包括所有要求的详细安装说明请参见 INSTALL 文件。

提示

在非默认目录中安装 nsrlsvr

nsrlsvr 的安装目录可以通过调用 configure 脚本并使用 --prefix 参数来更改。该参数的值表示目标目录。如果指定了用户可写目录,则安装不需要 root 权限,并且可以通过删除安装目录完全移除。

nsrlsrv 会维护一个包含所有 NSRL 数据库 MD5 哈希值的副本。因此,必须初始化哈希数据库。所需的 nsrlupdate 工具随 nsrlsrv 提供。

user@lab:~$ nsrlupdate your/path/to/NSRLFile.txt

在数据库完全填充后,可以通过简单地调用以下命令启动服务器:

user@lab:~$ nsrlsvr

如果一切安装正确,此命令将无输出并且服务器开始监听 TCP 端口 9120 以接收请求。

使用 Python 编写 nsrlsvr 客户端

还有一个用于使用 nsrlsvr 的客户端工具,名为 nsrllookup。该客户端是用 C++ 编写的,并可在 rjhansen.github.io/nsrllookup/ 上获取。然而,也可以很容易地在原生 Python 中实现与 nsrlsvr 交互的客户端。本节将解释该协议并展示一个客户端的示例实现。

nsrlsvr 在其网络端口 9120 上实现了一个面向文本的协议。每个命令由一行文本组成,并以换行符(CR LF)结束。支持以下命令:

  • 版本: 2.0:版本命令用于 nsrl 客户端与 nsrlsvr 之间的初步握手。客户端应在冒号后提供其版本号。服务器始终会响应 OK 并换行。

  • query 5CB360EF546633691912089DB24A82EE 908A54EB629F410C647A573F91E80775 BFDD76C4DD6F8C0C2474215AD5E193CF:查询命令用于实际从服务器查询 NSRL 数据库。关键字 query 后跟一个或多个 MD5 哈希值。服务器将返回 OK,后跟一串零和一。1 表示该 MD5 哈希值在数据库中找到,而 0 表示没有匹配。例如,之前显示的查询将导致以下结果:

    OK 101
    
    

    这意味着第一个和最后一个 MD5 哈希值在 NSRL 中找到了,但中间的哈希值没有找到。

  • BYE:bye 命令终止与 nsrlsvr 的连接。

因此,以下 Python 代码足以高效地查询 NSRL 数据库:

#!/usr/bin/env python

import socket

NSRL_SERVER='127.0.0.1'
NSRL_PORT=9120

def nsrlquery(md5hashes):
    """Query the NSRL server and return a list of booleans.

    Arguments:
    md5hashes -- The list of MD5 hashes for the query.
    """

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((NSRL_SERVER, NSRL_PORT))

    try:
        f = s.makefile('r')
        s.sendall("version: 2.0\r\n")
        response = f.readline();
        if response.strip() != 'OK':
            raise RuntimeError('NSRL handshake error')

        query = 'query ' + ' '.join(md5hashes) + "\r\n"
        s.sendall(query)
        response = f.readline();

        if response[:2] != 'OK':
            raise RuntimeError('NSRL query error')

        return [c=='1' for c in response[3:].strip()]
    finally:
        s.close()

使用这个模块和这里展示的一样简单:

import nsrlquery
hashes = ['86d3d86902b09d963afc08ea0002a746',
          '3dcfe9688ca733a76f82d03d7ef4a21f',
          '976fe1fe512945e390ba10f6964565bf']
nsrlquery.nsrlquery(hashes)

这段代码查询 NSRL 服务器并返回一个布尔值列表,每个布尔值表示相应的 MD5 哈希值是否在 NSRL 文件列表中找到。

总结

本章概述了法医领域及每个领域的示例算法。我们还展示了如何将安装在 Android 设备上的应用程序与 Mobile-Sandbox 等 Web 服务进行比较。在第二个实际示例中,我们演示了如何从 Windows 系统中筛选出良性和已知文件,以减少需要手动分析的数据量。借助 NSRLquery,法医调查可以专注于新内容或已修改的内容,而不必浪费时间在标准应用程序的广泛已知内容上。

在接下来的章节中,这些算法将应用于各种设备类型、操作系统和应用程序,以供法医调查使用。

第三章:使用 Python 进行 Windows 和 Linux 取证

本章将重点介绍与操作系统特定的取证调查部分。我们选择了桌面和服务器系统中最广泛使用的操作系统——微软 Windows 和 Linux。

对于这两种操作系统,我们选择了一些有趣的证据示例以及如何使用 Python 自动化分析它们。因此,在本章中,您将学习以下内容:

  • 分析 Windows 事件日志的基础,选择有趣的部分并自动解析它们

  • 组织 Windows 注册表并高效地搜索 妥协指示器IOC

  • 搜索 Linux 本地账户信息以查找 IOC

  • 理解、使用和解析 Linux 文件元数据,使用 POSIX ACL 和文件基础功能作为标准元数据的最显著扩展

分析 Windows 事件日志

Windows 包含许多监控和日志记录功能,并跟踪操作系统中大量和多样化活动的数据和事件。事件数量庞大,这些事件的记录既使管理员难以识别具体的重要事件,也不利于取证调查员找到妥协指示器。因此,我们将从本节开始,简要介绍 Windows 事件日志及其格式随时间的变化,接着描述应帮助调查员快速在大量其他事件中找到可疑行为的关键事件类型。在本章的最后一节中,我们将演示如何解析事件日志并自动找到潜在的 IOC(例如,用户登录、服务创建等)。

Windows 事件日志

根据微软的说法,Windows 事件日志文件是特殊的文件,用于记录重要事件,例如用户登录计算机时或程序发生错误时(参考windows.microsoft.com/en-us/windows/what-information-event-logs-event-viewer#1TC=windows-7)。每当这些类型的事件发生时,Windows 会在事件日志中记录该事件,可以通过事件查看器或类似工具读取。

随着 Windows 7 和 Windows Server 2008 的发布,微软对其事件日志技术进行了重大改变。他们从经典的 Windows 事件日志EVT)转变为较新的 Windows XML 事件日志EVTX)。在接下来的段落中,我们将解释这两种日志文件类型之间的一些主要区别。

由于微软不再支持 Windows XP,并且 Windows Server 2003 正处于扩展支持阶段(这意味着它很快将停止支持),现在仍然存在一些 XP 和 2003 系统。因此,一些调查人员仍然需要了解旧版 EVT 和新版 EVTX 之间的差异,以及在分析这些文件时可能出现的问题。

除了记录本身和事件日志文件中的二进制差异外,这些日志文件的数量也有所不同。在 Windows XP/2003 系统中,有三个主要的事件日志文件:系统应用程序安全性。它们存储在 C:\Windows\system32\config 目录下。操作系统的服务器版本可能会根据服务器的功能维护额外的事件日志(如 DNS 服务器、目录服务、文件复制服务等)。在当前的 Windows 7 系统中,你可以找到超过 143 个充满事件日志的文件。如果与 Microsoft Windows 的较新服务器版本进行比较,这个数字会更多。

EVT 日志记录只包含非常少量的可读内容,通过分析时使用的工具(如事件查看器)使其变得人类可读。这些工具将通常存储在系统 DLL 或 EXE 文件中的预定义日志模板与 EVT 文件本身存储的数据结合起来。当其中的某个日志查看工具显示日志记录时,它必须确定哪些 DLL 文件会存储消息模板。这个元信息存储在 Windows 注册表中,并且对之前提到的三种主要事件日志文件(系统、应用程序和安全性)中的每一种都是特定的。

前面提到的所有细节都表明,EVT 文件在没有对应的元文件时并不真正有用,因为元文件存储了日志的核心意义。这就产生了两个主要的分析问题:

  • 首先,攻击者可以修改 DLL 文件或 Windows 注册表,以改变事件日志的含义,而不必触及 EVT 文件。

  • 其次,当系统上的软件被卸载时,可能会导致 EVT 记录失去其上下文。

作为调查人员,在分析 EVT 日志以及将这些日志写入远程系统以便稍后分析时,必须仔细考虑这些问题。关于 EVT 记录的更详细分析可以在 ForensicsWiki 上找到,forensicswiki.org/wiki/Windows_Event_Log_(EVT)

与 EVT 文件相比,EVTX 文件以二进制 XML 文件格式存储。在较新的 Windows 系统中,可以通过事件查看器或大量其他程序和工具查看和分析事件日志(在接下来的章节中,我们也会描述一些可以使用的 Python 脚本)。在使用事件查看器时,必须记住该程序可以以两种不同的格式显示 EVTX 文件:常规详细。常规(有时称为格式化)视图可能会隐藏存储在事件记录中的重要事件数据,而这些数据只能在详细视图中看到。因此,如果你计划使用事件查看器分析 EVTX 文件,请始终选择详细选项来显示文件。

如果你对 EVTX 文件格式的详细分析感兴趣,可以查看 ForensicsWiki,forensicswiki.org/wiki/Windows_XML_Event_Log_(EVTX)。另一个对 EVTX 文件格式细节的精彩解释是 Andreas Schuster 在 DFRWS 2007 上提供的,参考 www.dfrws.org/2007/proceedings/p65-schuster_pres.pdf。如果你想理解二进制 XML 格式的细节或编写自己的 EVTX 文件解析器,这个演讲非常有帮助。

如果你需要在 Windows 7 或更高版本的系统上打开 EVT 文件,最好在打开之前将旧的 EVT 文件转换为 EVTX 语法。可以通过多种方式完成此操作,具体方法可参考 technet.com 博客文章,blogs.technet.com/b/askperf/archive/2007/10/12/windows-vista-and-exported-event-log-files.aspx

有趣的事件

可以在 Microsoft 的知识库文章中找到 Windows 系统中新版本事件的完整列表,support.microsoft.com/en-us/kb/947226。随着每个新版本的系统和每个新安装的应用程序,事件数量不断增加,你可以在单个 Windows 系统上找到几百种不同的事件类型。鉴于这一事实,我们尝试筛选出一些在分析系统或重建用户事件时可能有用的有趣事件类型(关于在什么情况下哪些事件日志有用的更详细解释也可以在 TSA-13-1004-SG 中找到,www.nsa.gov/ia/_files/app/spotting_the_adversary_with_windows_event_log_monitoring.pdf)。

  • EMET(1,2):如果组织正在积极使用 Microsoft 增强型缓解体验工具包EMET),则这些日志在调查过程中非常有用。

  • Windows-Update-Failure (20, 24, 25, 31, 34, 35):更新失败问题应得到解决,以避免操作系统或应用程序中存在的问题或漏洞被延长。有时,这也有助于识别系统感染。

  • Microsoft-Windows-Eventlog (104, 1102):在正常操作过程中,事件日志数据被清除的可能性较小,更可能的是恶意攻击者会尝试通过清除事件日志来掩盖其痕迹。当事件日志被清除时,这是可疑的。

  • Microsoft-Windows-TaskScheduler (106):它显示新注册的计划任务。如果你正在寻找恶意软件感染的迹象,这可能非常有帮助。

  • McAfee-Log-Event (257):McAfee 恶意软件检测—McAfee 杀毒软件可能检测到恶意软件行为,而不一定检测到 EXE 文件本身。这对于确定恶意软件是如何进入系统非常有价值。一般来说,已安装的 AV 解决方案的事件日志在开始分析潜在被破坏的系统时非常有价值。因此,你应该记得在哪里找到这些日志。

  • Microsoft-Windows-DNS-Client (1014):DNS 名称解析超时;这一事件类型在搜索恶意软件时非常有用,或者在试图找出用户是否尝试连接到特定网站或服务时。

  • Firewall-Rule-Add/Change/Delete (2004, 2005, 2006, 2033):如果客户端工作站利用内置的主机防火墙,则收集事件以跟踪防火墙状态非常有价值。普通用户不应该修改本地计算机的防火墙规则。

  • Microsoft-Windows-Windows Defender (3004):Windows Defender 恶意软件检测日志。

  • Microsoft-Windows-Security-Auditing (4720, 4724, 4725, 4728, 4732, 4635, 4740, 4748, 4756):在这些日志中,你可以找到诸如远程桌面登录、被添加到特权组的用户、账户锁定等信息。应该非常密切地审计被提升到特权组的用户,以确保这些用户确实应该在特权组中。未授权的特权组成员身份是发生恶意活动的强烈迹象。

  • Service-Control-Manager (7030, 7045):它监控服务是否被配置为与桌面交互,或者是否已在系统上安装。

  • App-Locker-Block/Warning (8003, 8004, 8006, 8007):应收集应用程序白名单事件,查看哪些应用程序被阻止执行。任何被阻止的应用程序可能是恶意软件,或者用户尝试运行未批准的软件。

哈兰·卡维 在他的博客文章中指出(windowsir.blogspot.de/2014/10/windows-event-logs.html),在新版 Windows(特别是 Windows 7)中,除了单独的事件记录(来源/ID 配对)之外,还有一个特点就是默认会记录许多事件,且跨多个事件日志文件进行记录。因此,当一些事件发生时,多个事件记录会存储在不同类型的事件日志中,并且通常跨不同的事件日志文件。例如,当用户在控制台登录到系统时,安全事件日志中会记录一个事件,一些事件会记录在 Microsoft-Windows-TerminalServices-LocalSessionManager/Operational 日志中,还有一些事件会记录在 Microsoft-Windows-TaskScheduler/Operational 日志中。

事件日志还可以用来检测攻击者是否使用了某种反取证技术。其中一种技术是通过改变系统时间来误导调查人员。为了检测这种修改,调查人员必须按顺序号和生成时间列出所有可用的事件日志记录。如果系统时间被回滚,某个事件的生成时间将会早于前一个事件。关于如何通过 Windows 事件日志检测反取证技术的更多示例,可以参考 哈兰·卡维 的博客文章,地址是 windowsir.blogspot.de/2013/07/howto-determinedetect-use-of-anti.html

解析事件日志以获取 IOC

在谈论事件日志并使用 Python 分析这些日志时,python-evtx 是绕不开的。这些脚本(github.com/williballenthin/python-evtx)是使用 Python 编程语言的 2.7+ 标签开发的。由于它完全是用 Python 编写的,这个模块在不同平台上都能很好地工作。该代码不依赖于任何需要单独编译的模块,能够在 Windows 操作系统上运行,特别是 Windows Vista 及以后版本的事件日志文件 EVTX。

我们想要介绍的第二个工具是plaso,(参见 plaso.kiddaland.net/)。该工具集源自 log2timeline,现在构建于 Python 之上。借助这个工具集,你可以创建系统事件和其他日志文件(例如 Apache)的有意义时间轴。log2timeline 也有一份非常好的备忘单,digital-forensics.sans.org/media/log2timeline_cheatsheet.pdf,它展示了这个工具的真正威力。该工具集的一个大优势是,你甚至可以在系统的完整镜像上运行它,以生成所有用户在该系统上执行的操作的时间轴,之后再创建镜像。

在接下来的章节中,我们将展示如何使用 python-evtx 查找 Windows 事件日志中的 IOC,以及 plaso 如何帮助你识别更多 IOC 并以良好的格式展示它们的时间轴。

python-evtx 解析器

首先,我们要从将 EVTX 文件的二进制 XML 格式转换为可读的 XML 文件开始。这可以通过使用 evtxdump.pygithub.com/williballenthin/python-evtx 实现,这也将是我们接下来脚本的基础:

#!/usr/bin/env python
import mmap
import contextlib
import argparse

from Evtx.Evtx import FileHeader
from Evtx.Views import evtx_file_xml_view

def main():
    parser = argparse.ArgumentParser(description="Dump a binary EVTX file into XML.")
    parser.add_argument("--cleanup", action="store_true", help="Cleanup unused XML entities (slower)"),
    parser.add_argument("evtx", type=str, help="Path to the Windows EVTX event log file")
    args = parser.parse_args()

    with open(args.evtx, 'r') as f:
        with contextlib.closing(mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)) as buf:

            fh = FileHeader(buf, 0x0)
            print "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>"
            print "<Events>"
            for xml, record in evtx_file_xml_view(fh):
                print xml
            print "</Events>"

if __name__ == "__main__":
    main()

在使用之前提到的脚本帮助下转储登录事件(事件 ID 4724),结果将类似于以下内容:

<Event ><System><Provider Name="Microsoft-Windows-Security-Auditing" Guid="54849625-5478-4994-a5ba-3e3b0328c30d"></Provider>
<EventID Qualifiers="">4724</EventID>
<Version>0</Version>
<Level>0</Level>
<Task>13824</Task>
<Opcode>0</Opcode>
<Keywords>0x8020000000000000</Keywords>
<TimeCreated SystemTime="2013-11-21 10:40:51.552799"></TimeCreated>
<EventRecordID>115</EventRecordID>
<Correlation ActivityID="" RelatedActivityID=""></Correlation>
<Execution ProcessID="452" ThreadID="1776"></Execution>
<Channel>Security</Channel>
<Computer>windows</Computer>
<Security UserID=""></Security>
</System>
<EventData><Data Name="TargetUserName">mspreitz</Data>
<Data Name="TargetDomainName">windows</Data>
<Data Name="TargetSid">S-1-5-21-3147386740-1191307685-1965871575-1000</Data>
<Data Name="SubjectUserSid">S-1-5-18</Data>
<Data Name="SubjectUserName">WIN-PC9VCSAQB0H$</Data>
<Data Name="SubjectDomainName">WORKGROUP</Data>
<Data Name="SubjectLogonId">0x00000000000003e7</Data>
</EventData>
</Event>

当使用evtxdump.pygithub.com/williballenthin/python-evtx,处理一个大型 Windows 事件日志文件时,输出将非常庞大,因为你将会在生成的 XML 文件中找到所有记录的日志。对于分析师来说,通常需要快速进行筛查或快速搜索特定事件。因此,我们修改了脚本,使其可以仅提取特定事件,具体如下所示:

#!/usr/bin/env python
import mmap
import contextlib
import argparse
from xml.dom import minidom

from Evtx.Evtx import FileHeader
from Evtx.Views import evtx_file_xml_view

def main():
    parser = argparse.ArgumentParser(description="Dump specific event ids from a binary EVTX file into XML.")
    parser.add_argument("--cleanup", action="store_true", help="Cleanup unused XML entities (slower)"),
    parser.add_argument("evtx", type=str, help="Path to the Windows EVTX event log file")
    parser.add_argument("out", type=str, help="Path and name of the output file")
    parser.add_argument("--eventID", type=int, help="Event id that should be extracted")
    args = parser.parse_args()

    outFile = open(args.out, 'a+')
    with open(args.evtx, 'r') as f:
        with contextlib.closing(mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)) as buf:
            fh = FileHeader(buf, 0x0)
            outFile.write("<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>")
            outFile.write("<Events>")
            for xml, record in evtx_file_xml_view(fh):
                xmldoc = minidom.parseString(xml)
                event_id = xmldoc.getElementsByTagName('EventID')[0].childNodes[0].nodeValue
                if event_id == str(args.eventID):
                    outFile.write(xml)
                else:
                    continue
            outFile.write("</Events>")

if __name__ == "__main__":
    main()

如果你现在想从 Windows 系统的安全事件日志中提取所有登录事件,只需按如下方式执行脚本:

user@lab:~$ ./evtxdump.py security.evtx logon_events.xml –eventID 4724

plaso 和 log2timeline 工具

在本节中,我们将演示如何在终端服务器上查找登录和注销事件。可以使用plasm标记终端服务的登录和注销事件,并使用psort进行过滤,以便快速查看哪些用户何时何地登录了机器。这些信息在寻找安全漏洞时非常有用。首先,你需要使用 plaso 标记所有数据。使用 plaso 标记非常简单,具体如下所示:

user@lab:~$ ./plasm.py tag --tagfile=tag_windows.txt storage_file

标记成功后,你可以使用以下命令在存储文件中搜索标签:

user@lab:~$ ./psort.py storage_file "tag contains 'Session logon succeeded'"

执行此命令后,将显示系统上所有成功的登录事件。在搜索已启动的服务或 EMET 故障时,可以执行类似的命令。

现在,您已经看到从 Windows 事件日志中能够提取的数据,我们将展示 Microsoft Windows 的第二个组件,这在搜索 IOC 或试图重建用户行为时非常有帮助。

分析 Windows 注册表

Windows 注册表是当前 Microsoft Windows 操作系统的核心组件之一,因此也是法医调查中非常重要的一个点。它为 Windows 操作系统执行两个关键任务。首先,它是 Windows 操作系统和安装在系统上的应用程序的设置存储库。其次,它是所有已安装硬件的配置数据库。微软对注册表的定义如下:

“一个在 Microsoft Windows 98、Windows CE、Windows NT 和 Windows 2000 中使用的中央层次数据库,用于存储配置一个或多个用户、应用程序和硬件设备所必需的信息。”(微软计算机词典)

在接下来的章节中,我们将解释 Windows 注册表中的几个重要元素,这些元素可能对法医调查员很有帮助,并有助于理解在哪里找到最有价值的指示器。我们将从注册表结构的概述开始,帮助您在大量数据中找到正确的方向。之后,我们将演示一些有用的脚本来提取妥协指示器(IOC)。

Windows 注册表结构

在 Windows 操作系统中,Windows 注册表逻辑地组织为多个根键。Windows 7 系统中的 Windows 注册表有五个逻辑根键,如下所示:

Windows 注册表结构

上图显示了 Windows 7 系统中通过 Windows 注册表编辑器(这是最常用的查看和检查 Windows 注册表的工具)显示的五个根键。

根键有两种类型:易失性和非易失性。只有两个根键存储在系统的硬盘上,并且是主存储器中持久存在的数据:HKEY_LOCAL_MACHINEHKEY_USERS。其他根键要么是这些键的子集,要么是只能在运行时或在开始分析系统镜像之前转储系统内存时检查的易失性键。

Windows 操作系统将注册表组织在多个 hive 文件中。根据微软的定义(参考 msdn.microsoft.com/en-us/library/windows/desktop/ms724877%28v=vs.85%29.aspx),hive 的定义如下:

Hive 是注册表中键、子键和值的逻辑分组,具有一组包含其数据备份的支持文件。

如果新用户登录到 Windows 机器,将创建一个用户配置文件 hive。此 hive 包含特定的注册表信息(例如,应用程序设置、桌面环境、网络连接和打印机),并位于 HKEY_USERS 键下。

每个 hive 都有额外的支持文件,这些文件存储在 %SystemRoot%\System32\Config 目录中。每次用户登录时,这些文件都会被更新,这些目录中文件的扩展名表示它们所包含的数据类型。更多详细信息请参阅下表(参考来源:msdn.microsoft.com/en-us/library/windows/desktop/ms724877%28v=vs.85%29.aspx):

扩展名 描述
none hive 数据的完整副本。
.alt 关键 HKEY_LOCAL_MACHINE\System hive 的备份副本。只有 System 键具有 .alt 文件。
.log 对 hive 中键和值条目的更改的事务日志。
.sav hive 的备份副本。

在接下来的部分中,我们将讨论在哪里找到有趣的 hive,并如何借助 Python 工具进行分析。

解析注册表以获取 IOC

在本节中,我们将讨论在搜索 IOC 时哪些注册表 hive 是重要的。以下子节将包括以下主题:

  • 连接的 USB 设备:本节将展示哪些设备曾经连接到系统以及何时连接。这有助于识别通过系统用户进行数据泄露或外泄的可能途径。

  • 用户历史:本节将展示如何找到已打开文件的历史记录。

  • 启动程序:本节将展示系统启动时将执行哪些程序。这在尝试识别受感染的系统时非常有帮助。

  • 系统信息:本节将展示如何找到目标系统的重要信息(例如,用户名)。

  • Shim Cache 解析器:本节将展示如何通过常见的 Python 工具(如 Mandiant 的 Shim Cache 解析器)来获取重要的 IOC,参考 www.mandiant.com/blog/leveraging-application-compatibility-cache-forensic-investigations/

连接的 USB 设备

事件响应人员最常遇到的一个问题是,是否有用户从系统中外泄了机密数据,或者系统是否因用户连接的恶意 USB 设备而遭到入侵。为了回答这个问题,Windows 注册表是一个很好的起点。

每当一个新的 USB 设备连接到系统时,它会在注册表中留下信息。这些信息可以唯一标识每个已连接的 USB 设备。注册表会存储每个已连接 USB 设备的供应商 ID、产品 ID、修订版和序列号。这些信息可以在HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Enum\USBSTOR注册表蜂巢中找到,Windows 法医分析Harlan CarveyDave KleimanSyngress Publishing,这也可以在以下截图中查看:

已连接的 USB 设备

用户历史

在 Windows 系统中,注册表中有多个列表可以帮助识别最近的用户活动(例如,最近访问的网页或最近打开的 Microsoft Word 文件)。下表展示了其中一些列表及其对应的 Windows 注册表子键,所有列表及其 Windows 注册表子键请参考ro.ecu.edu.au/cgi/viewcontent.cgi?article=1071&context=adf

历史列表 相关的 Windows 注册表子键
在 Microsoft Internet Explorer 中输入的 URL HKEY_USERS\S-1-5-21-[用户标识符]\Software\Microsoft\Internet Explorer\TypedURLs
最近使用的 Microsoft Office 文件 HKEY_USERS\S-1-5-21-[用户标识符]\Software\Microsoft\Office\12.0\Office_App_Name\File MRU
最近映射的网络驱动器 HKEY_USERS\S-1-5-21-[用户标识符]\Software\Microsoft\Windows\CurrentVersion\Explorer\Map Network Drive MRU
在 RUN 对话框中最近输入的命令 HKEY_USERS\S-1-5-21-[用户标识符]\Software\Microsoft\Windows\CurrentVersion\Explorer\RunMRU
最近的文件夹 HKEY_USERS\S-1-5-21-[用户标识符]\Software\Microsoft\Windows\CurrentVersion\Explorer\RecentDocs\Folder

启动程序

在某些调查中,重要的是找出哪些软件是在启动时自动运行的,哪些软件是用户手动启动的。为了解答这个问题,Windows 注册表中的HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run可以再次提供帮助。启动程序列表如下图所示,并列在Windows 注册表蜂巢中,图表来自《Windows 注册表快速参考》Farmer, D. J

启动程序

系统信息

在本节中,我们将看到一些在分析系统时可能非常重要的注册表蜂巢。首先,注册表中存储了大量关于用户账户的信息,如下所示:

  • 用户账户列表

  • 每个账户的最后登录时间

  • 账户是否需要密码

  • 账户是否被禁用或启用

  • 用于计算密码哈希值的哈希技术

所有这些信息都保存在以下注册表键中:

HKEY_LOCAL_MACHINE\SAM\Domains\Account\Users

Windows 注册表中有许多其他有趣的数据;然而,有一种信息在法医调查中可能非常有用:系统最后一次关机的时间。该信息存储在以下哈希中的ShutdownTime值中:

HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Windows

这个信息通常在服务器系统中很有趣,因为它可能表明上次更新的时间,或者是否有任何系统的非计划重启,这也可能是攻击者导致的。

Shim 缓存解析器

Windows 注册表包含应用兼容性问题和大量元数据(如文件大小、文件的最后修改时间以及最后执行时间,这取决于操作系统版本),这些可能对应用兼容性 Shim 缓存中的应用运行时非常重要。

提示

应用兼容性 Shim 缓存的实现和结构可能因操作系统而异。因此,请彻底检查您的发现。

有关应用兼容性和运行时问题的数据,在事件响应或其他类型的法医调查中非常有用,可以帮助识别潜在感染的系统,并创建潜在感染发生的时间线。Mandiant 发布了一款提取这种证据的工具:Shim 缓存解析器,(更多信息请参阅github.com/mandiant/ShimCacheParser

Shim 缓存解析器将自动确定缓存数据的格式并输出其内容。它支持多种输入,包括系统注册表哈希、原始二进制数据或当前系统的注册表。

该工具可以用于导出的注册表哈希或正在运行的系统。使用时,只需执行以下命令:

C:\tools\mandiant> python ShimCacheParser.py –l –o out.csv

[+] Dumping Shim Cache data from the current system...
[+] Found 64bit Windows 7/2k8-R2 Shim Cache data...
[+] Found 64bit Windows 7/2k8-R2 Shim Cache data...
[+] Found 64bit Windows 7/2k8-R2 Shim Cache data...
[+] Writing output to out.csv...

在查看生成的 CSV 输出时,您可以找到已安装的应用程序及其首次运行时间,具体如下:

Last Modified,Last Update,Path,File Size,Exec Flag
05/04/11 05:19:28,N/A,C:\Windows\system32\SearchFilterHost.exe,N/A,True
05/24/15 16:44:45,N/A,C:\Program Files (x86)\Avira\AntiVir Desktop\avwsc.exe,N/A,True
11/21/10 03:24:15,N/A,C:\Windows\system32\wbem\wmiprvse.exe,N/A,True
05/30/14 08:07:49,N/A,C:\Windows\TEMP\40F00A21-D2E7-47A3-AE16-0AFB8E6C1F87\dismhost.exe,N/A,True
07/14/09 01:39:02,N/A,C:\Windows\system32\DeviceDisplayObjectProvider.exe,N/A,True
07/26/13 02:24:56,N/A,C:\Windows\System32\shdocvw.dll,N/A,False
05/24/15 16:46:22,N/A,C:\Program Files (x86)\Google\Update\1.3.27.5\GoogleCrashHandler.exe,N/A,True
05/07/15 21:42:59,N/A,C:\Windows\system32\GWX\GWX.exe,N/A,True
03/26/15 20:57:08,N/A,C:\Program Files (x86)\Parallels\Parallels Tools\prl_cc.exe,N/A,True
10/07/14 16:29:54,N/A,C:\Program Files (x86)\KeePass Password Safe 2\KeePass.exe,N/A,True
10/07/14 16:44:13,N/A,C:\ProgramData\Avira\Antivirus\TEMP\SELFUPDATE\updrgui.exe,N/A,True
04/17/15 21:03:48,N/A,C:\Program Files (x86)\Avira\AntiVir Desktop\avwebg7.exe,N/A,True

通过查看之前的数据,可以发现用户在 2015-05-24 安装或更新了 Avira AntiVir,并在 2014-07-10 安装了 KeePass。此外,您还可以找到一些线索,表明该系统似乎是一个虚拟系统,因为可以看到 Parallels 的线索,这是一个 Mac OS X 虚拟化平台。

如果考虑到前面描述的工具以及 Windows 事件日志和 Windows 注册表中包含的信息,就会发现,在法医调查中,并非所有关于系统的问题都可以通过这些信息源来回答。

实施特定于 Linux 的检查

在本节中,我们将描述如何实施一些完整性检查,以帮助发现 Linux 和类似系统(例如,BSD 系统)中的系统操作迹象。

这些检查包括以下内容:

  • 在本地用户管理中搜索异常

  • 理解和分析文件元数据中的特殊权限和特权

  • 使用文件元数据的聚类算法获取进一步检查的指示

检查本地用户凭据的完整性

Linux 中的本地用户信息大多数存储在两个文件中:/etc/passwd/etc/shadow。后者是可选的,所有关于本地用户的信息,包括哈希密码,最初都存储在 /etc/passwd 中。很快,将密码信息存储在所有用户可读的文件中被认为是一个安全问题。因此,/etc/passwd 中的密码哈希被一个表示密码哈希需要在 /etc/shadow 中查找的 x 字符所替代。

这一演变过程的副作用是 /etc/passwd 中的密码哈希仍然被支持,并且 /etc/passwd 中的所有设置可能会覆盖 /etc/shadow 中的凭据。

这两个文件都是文本文件,每行包含一个条目。一个条目由多个字段组成,字段之间用冒号分隔。

/etc/passwd 的格式如下:

  • 用户名:此字段包含人类可读的用户名。用户名不必唯一。然而,大多数用户管理工具会强制要求用户名唯一。

  • 密码哈希:此字段包含根据 Posix crypt() 函数对密码进行编码后的形式。如果此字段为空,则对应的用户无需密码即可登录系统。如果此字段包含无法通过哈希算法生成的值,例如感叹号,那么该用户无法使用密码登录。然而,这种情况并不会使账户失效。即便密码被锁定,用户仍然可以使用其他认证机制登录,例如 SSH 密钥。

    如前所述,特殊值 x 表示密码哈希必须在 shadow 文件中查找。

    从系统库 glibc2 开始,crypt() 函数支持多种哈希算法。在这种情况下,密码哈希具有以下格式:

    $id$salt$encrypted
    
    

    该 ID 标识已用于对密码进行编码的哈希算法,例如,1 表示 md5,5 表示 sha256,6 表示 sha512。盐是一个随机生成的字符串,用来修改哈希算法。因此,即使密码相同,也会产生不同的哈希值。子字段“加密”保存了密码的实际哈希(经过盐的影响进行修改)。

  • 数字用户 ID:此字段表示用户的 ID。内核内部仅使用此数字 ID。特殊的 ID 0 分配给具有管理权限的 root 用户。默认情况下,用户 ID 为 0 的用户拥有系统上的无限制权限。

  • 数字组 ID:此字段指的是用户的主组。

  • 备注字段:此字段可以包含关于用户的任意信息,通常用于存储用户的全名。有时,它还包含以逗号分隔的全名、电话号码等信息。

  • 用户主目录:用户主目录是系统文件系统中的一个目录。登录后,新的进程会以此目录作为工作目录启动。

  • 默认命令 shell:此可选字段表示成功登录后将启动的默认 shell。

/etc/shadow 的格式如下:

  • 用户名字段将条目与具有相同用户名的 passwd 条目关联起来。

  • 密码哈希字段包含以与 passwd 文件相同的格式编码的密码。

  • 接下来的五个字段包含关于密码过期的信息,例如上次密码更改的日期、密码的最小使用期限、最大使用期限、密码警告期和密码非活动期。

  • 如果账户过期日期字段不为空,它将被解释为账户的过期日期。这个日期是自 1970 年 1 月 1 日以来的天数。

通过这种格式描述,一个简单的 Python 脚本就足以将文件解析成一个包含多个条目的列表,每个条目包含一组字段,如下所示:

def read_passwd(filename):
    """Reads entries from shadow or passwd files and
       returns the content as list of entries.
       Every entry is a list of fields."""

    content = []
    with open(filename, 'r') as f:
        for line in f:
            entry = line.strip().split(':')
            content.append(entry)

    return content

使用此脚本时,可能会检测到这些文件中的典型操控。

我们要描述的第一个操控技巧是创建多个共享相同数字 ID 的用户。攻击者可以使用这种技巧将后门植入系统。通过为现有的 ID 创建一个额外的用户,攻击者可以创建一个带有单独密码的别名。合法账户的拥有者并不会意识到存在另一个用户名/密码组合可以登录该账户。

一个简单的 Python 脚本可以检测这种操作,如下所示:

def detect_aliases(passwd):
    """Prints users who share a user id on the console

       Arguments:
       passwd -- contents of /etc/passwd as read by read_passwd"""

    id2user = {}
    for entry in passwd:
        username = entry[0]
        uid = entry[2]
        if uid in id2user:
            print 'User "%s" is an alias for "%s" with uid=%s' % (username, id2user[uid], uid)
        else:
            id2user[uid] = username

在正常操作中,/etc/passwd/etc/shadow 文件中的信息是同步的,也就是说,每个用户应该出现在这两个文件中。如果有用户只出现在其中一个文件中,那么这表明操作系统的用户管理机制可能被绕过了。类似的操作可以通过以下脚本检测到:

def detect_missing_users(passwd, shadow):
    """Prints users of /etc/passwd missing in /etc/shadow
       and vice versa.

       Arguments:
       passwd -- contents of /etc/passwd as read by read_passwd
       shadow -- contents of /etc/shadow as read by read_passwd"""

    passwd_users = set([e[0] for e in passwd])
    shadow_users = set([e[0] for e in shadow])

    missing_in_passwd = shadow_users - passwd_users
    if len(missing_in_passwd) > 0:
        print 'Users missing in passwd: %s' % ', '.join(missing_in_passwd)

    missing_in_shadow = passwd_users - shadow_users
    if len(missing_in_shadow) > 0:
        print 'Users missing in shadow: %s' % ', '.join(missing_in_shadow)

就像第一个函数一样,正常系统中这个函数不应产生任何输出。如果输出类似于 Users missing in shadow: backdoor,那么系统中有一个名为“backdoor”的用户账户,但在 shadow 文件中没有相关记录。

在正常系统中,不应该存在没有密码的用户。此外,所有密码哈希应保存在 shadow 文件中,而 passwd 文件中的所有条目应指向对应的 shadow 条目。以下脚本检测违反这一规则的情况:

def detect_unshadowed(passwd, shadow):
    """Prints users who are not using shadowing or have no password set

       Arguments:
       passwd -- contents of /etc/passwd as read by read_passwd
       shadow -- contents of /etc/shadow as read by read_passwd"""

    nopass = [e[0] for e in passwd if e[1]=='']
    nopass.extend([e[0] for e in shadow if e[1]==''])
    if len(nopass) > 0:
        print 'Users without password: %s' % ', '.join(nopass)

    unshadowed = [e[0] for e in passwd if e[1] != 'x' and e[1] != '']
    if len(unshadowed) > 0:
        print 'Users not using password-shadowing: %s' % \
              ', '.join(unshadowed)

我们最后一个绕过操作系统进行用户帐户创建和操作的示例是 检测非标准哈希算法在多个用户帐户间重用盐值。虽然 Linux 系统允许为 shadow 文件中的每个条目指定哈希算法,但通常所有用户的密码都是使用相同的算法进行哈希的。偏离的算法是一个信号,表示该条目被写入 shadow 文件时未使用操作系统工具,这意味着系统被篡改。如果一个盐值在多个密码哈希中被重用,则该盐值可能被硬编码到操作工具中,或者系统的加密例程可能已被破坏,例如通过篡改盐值生成的熵源。

以下 Python 脚本能够检测这种类型的操作:

import re
def detect_deviating_hashing(shadow):
    """Prints users with non-standard hash methods for passwords

       Arguments:
       shadow -- contents of /etc/shadow as read by read_passwd"""

    noalgo = set()
    salt2user = {}
    algorithms = set()
    for entry in shadow:
        pwhash = entry[1]
        if len(pwhash) < 3:
            continue

        m = re.search(r'^\$([^$]{1,2})\$([^$]+)\$', pwhash)
        if not m:
            noalgo.add(entry[0])
            continue

        algo = m.group(1)
        salt = m.group(2)

        if salt in salt2user:
            print 'Users "%s" and "%s" share same password salt "%s"' % \
                  (salt2user[salt], entry[0], salt)
        else:
            salt2user[salt] = entry[0]

        algorithms.add(algo)

    if len(algorithms) > 1:
        print 'Multiple hashing algorithms found: %s' % ', '.join(algorithms)

    if len(noalgo) > 0:
        print 'Users without hash algorithm spec. found: %s' % \
              ', '.join(noalgo)

提示

正则表达式

最后一个示例使用 re 模块进行 正则表达式 匹配,以从密码哈希中提取算法规范和盐值。正则表达式提供了一种快速而强大的文本搜索、匹配、拆分和替换方法。因此,我们强烈建议熟悉正则表达式。re 模块的文档可以在线访问 docs.python.org/2/library/re.html。书籍《精通 Python 正则表达式》由 Felix LopezVictor Romero 编著,Packt Publishing 出版,提供了更多的见解和关于如何使用正则表达式的示例。

本节中的所有检测方法都是异常检测方法的示例。根据系统环境,可以通过遵循示例的模式,使用和实现更具体的异常检测方法。例如,在服务器系统中,设置密码的用户数量应该较少。因此,统计所有设置了密码的用户,可以作为分析此类系统的合理步骤。

分析文件元信息

本节将讨论文件元信息,并提供如何在取证分析中使用它的示例。

理解 inode

Linux 系统将文件元信息存储在称为 inode索引节点)的结构中。在 Linux 文件系统中,每个对象都由一个 inode 表示。每个 inode 存储的数据取决于实际的文件系统类型。inode 的典型内容如下:

  • 索引号是 inode 的标识符。索引号在每个文件系统中都是唯一的。如果两个文件共享相同的索引号,那么这两个文件是 硬链接。因此,硬链接文件仅在文件名上有所不同,内容和元信息始终相同。

  • 文件所有者由用户的数字 ID(UID)定义。每个文件只能有一个所有者。用户 ID 应该与/etc/passwd中的条目对应。然而,并不保证所有文件都对应于/etc/passwd中现有的条目。文件可以通过管理员权限转移给不存在的用户。此外,文件的所有者可能已经从系统中删除,这会导致文件成为孤儿文件。对于可移动介质上的文件,例如 USB 驱动器,并没有将用户 ID 从一个系统映射到另一个系统的机制。因此,当 USB 驱动器连接到具有不同/etc/passwd的新系统时,文件的所有者似乎发生了变化。此外,如果在连接 USB 驱动器的系统中不存在该 UID,这也可能导致孤儿文件的出现。

  • 文件组由相应组的数字 ID(GID)定义。一个文件始终分配给恰好一个组。系统的所有组应该在/etc/groups中定义。然而,可能存在组 ID 不在/etc/groups中列出的文件。这表明对应的组已经从系统中删除,介质已经从另一个系统转移过来(该系统上该组仍然存在),或者具有管理员权限的用户将文件重新分配给了一个不存在的组。

  • 文件模式也称为“保护位”)定义了对相应文件的一种简单的访问权限形式。它是一个位掩码,用于定义文件所有者、属于文件分配的组的用户以及所有其他用户的访问权限。对于这几种情况,定义了以下位:

    • 读取r):如果这个位在常规文件上设置,则受影响的用户被允许读取文件内容。如果这个位在目录上设置,则受影响的用户被允许列出该目录中内容的名称。读取访问不包括元信息,即目录条目的 inode 数据。因此,目录的读取权限不足以读取该目录中的文件,因为这需要访问文件的 inode 数据。

    • 写入w):如果这个位在常规文件上设置,则受影响的用户被允许以任意方式修改文件内容,包括操纵和删除内容。如果这个位在目录条目上设置,则受影响的用户被允许在该目录中创建、删除和重命名条目。目录中的现有文件有自己的保护位,用于定义它们的访问权限。

    • 执行x):对于常规文件,此标志允许受影响的用户将文件作为程序启动。如果文件是编译后的二进制文件,例如 ELF 格式,那么执行权限足以运行该程序。如果文件是必须解释的脚本,则运行脚本时还需要读取权限(r)。原因是 Linux 内核会确定如何加载程序。如果它检测到文件包含脚本,它会以当前用户的权限加载脚本解释器。对于目录,此标志授予权限读取目录内容的元信息,但不包括条目的名称。因此,这允许受影响的用户将工作目录更改为该目录。

    • 粘滞t):此位每个 inode 仅存在一次。当它在目录上设置时,它限制删除和重命名条目的权限,仅限于拥有该条目的用户。在常规文件上,此标志被忽略或具有文件系统特定的效果。当设置在可执行文件上时,此标志用于防止生成的进程从 RAM 中被交换出去。然而,粘滞位的这一用途已经被弃用,Linux 系统不再遵守可执行文件上的粘滞位。

    • 执行时设置标识s):这个位存在于用户和组上。当在可执行文件上为用户设置(SUID 位)时,相关文件总是以其所有者作为有效用户运行。因此,程序以拥有可执行文件的用户权限运行,而不依赖于实际启动程序的用户。如果文件属于 root 用户(UID 0),则该可执行文件始终以无限权限运行。当为组设置该位(SGID 位)时,程序始终以文件的组作为有效组启动。

  • 文件的大小(以字节为单位)。

  • 为该文件分配的块数。

  • 标记文件内容最后一次更改的时间戳(mtime)。

  • 标记最后一次读取文件内容的时间戳(atime)。

    通过挂载选项noatime可以禁用访问时间戳跟踪,以限制对媒体的写访问(例如,延长 SD 卡的使用寿命)。此外,文件系统的只读访问(挂载选项ro)会阻止 atime 跟踪。因此,在分析 atime 信息之前,应检查是否为该文件系统启用了 atime 跟踪。相应的初始挂载选项可以在/etc/fstab中找到。

  • 标记文件 inode 数据最后一次更改的时间戳(ctime)。

使用 Python 读取基本文件元数据

docs.python.org/2/library/os.html#os.stat上提供了关于os.stat()os.lstat()的详细说明。这还包括平台相关属性的示例。

结果对象是平台相关的;然而,以下信息始终可用:st_mode(保护位)、st_ino(inode 号)、st_dev(包含文件系统对象的设备标识符)、st_nlink(硬链接数)、st_uid(所有者用户 ID)、st_gid(所有者组 ID)、st_size(文件大小,以字节为单位)、st_mtime(最后修改时间)、st_atime(最后访问时间)、st_ctime(最后 inode 更改时间)。这些信息对应于前面部分描述的 inode 数据。

这些标准条目的显著扩展是POSIX 访问控制列表POSIX ACLs)。这些访问控制列表受到主要 Linux 文件系统的支持,并允许指定除了三个类(用户、组和其他)之外的额外访问条目。这些条目允许为额外的用户和组定义额外的访问权限(先前列出的位 r、w 和 x)。将详细讨论评估 POSIX ACLs 的内容。

此代码清单输出lstat调用的常见返回值。典型输出类似于以下内容:

另一个扩展包括向可执行文件添加能力标志的规范。这用于比使用 SUID 位更细粒度地指定权限。而不是给根用户拥有的可执行文件设置 SUID 位并允许它拥有无限的权限,可以指定一组所需的权限。能力标志也将在单独的部分详细处理。

Python 提供了内置功能来使用os模块读取文件状态信息。通过文件名指定的标准功能来检索元数据是os.lstat()。与更常用的os.stat()相比,此函数不评估符号链接的目标,而是检索有关链接本身的信息。因此,它不容易遇到由循环符号链接引起的无限循环。此外,它不会在缺少链接目标的链接上引发任何错误。

st_mtimest_atimest_ctime时间戳采用 Unix 时间戳格式,即自 1970 年 1 月 1 日以来的秒数。使用 datetime 模块,可以将此时间格式转换为人类可读的形式,如以下脚本所示:

from datetime import datetime as dt
from os import lstat

stat_info = lstat('/etc/passwd')

atime = dt.utcfromtimestamp(stat_info.st_atime)
mtime = dt.utcfromtimestamp(stat_info.st_mtime)
ctime = dt.utcfromtimestamp(stat_info.st_ctime)

print 'File mode bits:      %s' % oct(stat_info.st_mode)
print 'Inode number:        %d' % stat_info.st_ino
print '# of hard links:     %d' % stat_info.st_nlink
print 'Owner UID:           %d' % stat_info.st_uid
print 'Group GID:           %d' % stat_info.st_gid
print 'File size (bytes)    %d' % stat_info.st_size
print 'Last read (atime)    %s' % atime.isoformat(' ')
print 'Last write (mtime)   %s' % mtime.isoformat(' ')
print 'Inode change (ctime) %s' % ctime.isoformat(' ')

注意

File mode bits:      0100644
Inode number:        1054080
# of hard links:     1
Owner UID:           0
Group GID:           0
File size (bytes)    2272
Last read (atime)    2015-05-15 09:25:15.991190
Last write (mtime)   2014-09-20 10:40:46.389162
Inode change (ctime) 2014-09-20 10:40:46.393162

该示例输出表示在实验室系统中,/etc/passwd 是一个常规文件,所有用户都有读取权限。此信息来自结果的 st_mode 成员。在使用 Python 的 oct() 函数时,它被转换为八进制表示,即 输出的每个十进制数字恰好表示保护位的三位二进制数字。输出中的前导零是八进制表示的常见标志。

输出中的后三个数字(示例中的 644)始终表示文件所有者的访问权限(示例中的 6),属于文件所在组的用户的权限(示例中的左 4),以及所有其他用户的权限(最后一位)。

提示

如何解读文件模式位?

在其八进制形式中,三个最不重要的数字的位值表示文件所有者、组和其他用户(最后一位)的访问权限。对于每一位,读权限(r)具有位值 4,写权限(w)具有位值 2,执行权限(x)具有位值 1。

因此,在我们的示例中,数字 6 表示文件所有者的读写权限(4 + 2)。组 0 的成员和所有其他用户只有读权限(4)。

从右侧起的下一个数字表示粘滞位(值为 1)、SGID 位(值为 2)和 SUID 位(值为 4)。

注意

stat 模块定义了 st_mode 所有位的常量。其文档可以在 docs.python.org/2/library/stat.html 查看。

这些常量可以作为位掩码从 st_mode 中检索信息。之前的示例可以扩展以检测 SGID、SUID 和粘滞模式,如下所示:

import stat

if stat.S_ISUID & stat_info.st_mode:
    print 'SUID mode set!'

if stat.S_ISGID & stat_info.st_mode:
    print 'SGID mode set!'

if stat.S_ISVTX & stat_info.st_mode:
    print 'Sticky mode set!'

要测试代码,可以使用示例来评估标准 Linux 系统上 /etc/passwd/tmp/usr/bin/sudo 的模式。通常,/tmp 设置了粘滞标志,/usr/bin/sudo 设置了 SUID,而 /etc/passwd 没有设置任何特殊位。

剩余的位表示文件类型。标准 Linux 文件系统上可能出现以下文件类型:

文件类型 stat 模块中的检查函数 描述
常规 S_ISREG() 用于存储任意数据
目录 S_ISDIR() 用于存储其他文件的列表
符号链接 S_ISLNK() 这是通过名称引用一个目标文件
字符设备 S_ISCHR() 这是文件系统中用于访问字符型硬件的接口,例如终端
块设备 S_ISBLK() 这是文件系统中用于访问块设备的接口,例如磁盘分区
fifo S_ISFIFO() 这是文件系统中命名的单向进程间接口的表示
套接字 S_ISSOCK() 这是文件系统中命名的双向进程间接口的表示

硬链接不是由特殊的文件类型表示的,而只是同一文件系统中多个共享相同 inode 的目录项。

与 SGID、SUID 和粘滞位的测试不同,文件类型检查作为stat模块的函数实现。这些函数需要文件模式位作为参数,例如:

from os import readlink,lstat
import stat

path = '/etc/rc5.d/S99rc.local'

stat_info = lstat(path)

if stat.S_ISREG(stat_info.st_mode):
    print 'File type: regular file'

if stat.S_ISDIR(stat_info.st_mode):
    print 'File type: directory'

if stat.S_ISLNK(stat_info.st_mode):
    print 'File type: symbolic link pointing to ',
    print readlink(path)

在此示例中,os.readlink()函数用于提取目标文件名,如果遇到符号链接。符号链接可以指向绝对路径或从符号链接在文件系统中的位置开始的相对路径。绝对符号链接的目标以字符/开头,即目标从系统的根目录开始搜索。

注释

如果你在实验环境中挂载了证据的副本进行分析,绝对符号链接要么损坏,要么指向你的实验工作站中的一个文件!只要它们的目标位于与链接相同的分区中,相对符号链接将保持完整。

上述示例代码的可能输出为- 文件类型:指向../init.d/rc.local 的符号链接 -,这是一个相对链接的示例。

使用 Python 评估 POSIX ACLs

文件模式位在文件的 inode 中定义,只允许三个权限接收者:文件所有者、属于文件组的用户以及其他所有人。

如果需要更细粒度的权限设置,传统的解决方案是创建一个包含所有应有访问权限的用户的组,并将文件转移到该组。然而,创建此类组存在重大缺点。首先,组的列表可能变得过于庞大。其次,创建此类组需要管理员权限,因此会破坏 Linux/Unix 的自主访问控制概念。

注释

自主访问控制是指允许信息的拥有者,即文件拥有者,决定谁应被允许访问该信息。在自主访问控制中,拥有权是授予或撤销访问资源的唯一要求。

最后但同样重要的是,如果没有匹配的组来授权用户,文件所有者可能会将文件和目录开放给系统上的所有人。这破坏了最小权限原则,即不授予系统操作所需的权限以外的任何权限。

为了保持自主访问控制以及最小权限原则,文件访问模式定义了一个可选扩展,即POSIX ACL。除了允许文件所有者、组和其他人对文件进行读、写和执行权限外,POSIX ACL 还允许指定以下内容:

  • 任意用户的特定读、写和执行权限

  • 任意组的特定读、写和执行权限

  • 访问掩码中未设置的每个权限都不会被授予。只有文件所有者和其他用户的权限不会受到访问掩码的影响。

在命令行中,可以使用 getfaclsetfacl 工具分别读取和修改 POSIX ACL 条目:

user@lab:~$ touch /tmp/mytest
user@lab:~$ getfacl /tmp/mytest
getfacl: Removing leading '/' from absolute path names
# file: tmp/mytest
# owner: user
# group: user
user::rw-
group::r--
other::r--

此示例还表明,标准权限集在 POSIX ACL 中得到了反映。因此,如果文件系统支持 POSIX ACL,则完整的权限集包含在 POSIX ACL 中。

让我们撤销对 other 用户的读取访问,并为用户 games 添加读取/写入访问,如下所示:

user@lab:~$ setfacl -m o::0 -m u:games:rw /tmp/mytest
user@lab:~$ getfacl /tmp/mytest
getfacl: Removing leading '/' from absolute path names
# file: tmp/mytest
# owner: user
# group: user
user::rw-
user:games:rw-
group::r--
mask::rw-
other::---
user@lab:~$ ls -l /tmp/mytest
-rw-rw----+ 1 user user 0 May 16 16:59 /tmp/mytest

-m o::0 参数删除了 other 用户的所有权限,而 –m u:games:rw 为用户 games 授予了读取/写入权限。随后的 getfacl 调用显示了 user:games 的附加条目和 other 的更改条目。此外,mask 条目会自动创建,以限制所有列出组和用户(除了文件所有者)的读取/写入访问权限。

ls 命令的输出通过加号 + 来表示存在额外的 ACL 条目。正如 ls 输出所示,只有评估文件模式位的工具无法识别这些额外的权限。例如,用户 games 的附加访问权限在 ls 或其他文件管理应用的标准输出中不会显示。

注意

如果取证工具没有查找和解释 POSIX ACL 条目,可能会错过 ACL 条目所引入的附加访问权限!因此,调查员可能会对严格的有效权限产生错误印象。

幸运的是,Python 库 pylibacl 可以用来读取和评估 POSIX ACL,从而避免这个陷阱。该库引入了 posix1e 模块,这是首次提到 POSIX ACL 的初稿的参考。关于此库的详细文档可以在 pylibacl.k1024.org/ 查阅。

以下脚本是一个示例,演示如何查找具有额外 ACL 条目的文件:

#!/usr/bin/env python

import os
from os.path import join
import posix1e
import re
import stat
import sys

def acls_from_file(filename, include_standard = False):
    """Returns the extended ACL entries from the given
       file as list of the text representation.

       Arguments:
       filename -- the file name to get the ACLs from
       include_standard -- if True, ACL entries representing 
                           standard Linux permissions will be
                           included"""
    result = []
    try:
 acl = posix1e.ACL(file=filename)
    except:
        print 'Error getting ACLs from %s' % filename
        return []

    text = acl.to_any_text(options=posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS)

    for entry in text.split("\n"):
        if not include_standard and \
           re.search(r'^[ugo]::', entry) != None:
            continue
        result.append(entry)

    return result

def get_acl_list(basepath, include_standard = False):
    """Collects all POSIX ACL entries of a directory tree.

    Arguments:
    basepath -- directory to start from
    include_standard -- if True, ACL entries representing 
                        standard Linux permissions will be
                        included"""
    result = {}

 for root, dirs, files in os.walk(basepath):
        for f in dirs + files:
            fullname = join(root, f)

            # skip symbolic links (target ACL applies)
 if stat.S_ISLNK(os.lstat(fullname).st_mode):
                continue

            acls = acls_from_file(fullname, include_standard)
            if len(acls) > 0:
                result[fullname] = acls

    return result

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print 'Usage %s root_directory' % sys.argv[0]
        sys.exit(1)

    acl_list = get_acl_list(sys.argv[1], False)

    for filename, acls in acl_list.iteritems():
        print "%s: %s" % (filename, ','.join(acls))

posix1e.ACL 类表示文件系统中某个特定对象的所有权限。当其构造函数以文件名作为 file 参数被调用时,它表示该文件的 ACL。在 acls_from_file() 函数中,使用正则表达式来检测并可选地从 ACL 集的文本表示中过滤掉标准权限。

os.walk() 函数用于遍历文件系统的子树。如果像示例中那样遍历 os.walk(),你会在每次迭代中得到一个三元组,表示以下内容:

  • 当前访问的目录

  • 包含所有子目录的列表(相对于当前访问的目录)

  • 包含所有非目录条目的列表,例如文件和软链接(相对于当前访问的目录)

脚本中最后一行的检查是评估文件类型信息的一个例子,如前一节所述。它用于检测并跳过符号链接。符号链接始终使用其目标的 ACL,因此,符号链接上的 POSIX ACL 不被支持。

当我们在实验机器上使用 /tmp 作为参数调用时,它生成以下输出:

/tmp/mytest: u:5:rw-,m::rw-

该输出显示脚本检测到了我们第一次使用 POSIX ACL 进行测试时的剩余信息:为用户(u)ID 5(即实验机器上的 games 用户)提供的额外的读/写权限,以及限制有效权限为读/写的掩码(m)条目。脚本输出数值型的用户 ID,因为如果不这样做,pylibacl 会使用你工作站的 /etc/passwd 来查找用户名。

如果你在包含证据的文件系统副本上运行此脚本,它将列出所有超出 Linux 标准权限集的文件系统对象。

注意

大多数标准 Linux 系统及其应用程序不使用 POSIX ACL。因此,如果在调查过程中遇到任何额外的 POSIX ACL 条目,建议彻底检查这些 POSIX ACL 是否是合法且无害的系统操作的结果。

使用 Python 读取文件能力

在 Linux 中,传统上有两种类型的管理员权限:root 权限和非 root 权限。如果一个进程被授予 root 权限,即它以 UID 0 运行,那么它可以绕过 Linux 内核的所有安全限制。另一方面,如果一个进程没有这些 root 权限,那么所有内核的安全限制都适用。

为了用更精细的系统替代这种全有或全无的机制,引入了Linux 能力。相应的手册页面描述如下:

为了执行权限检查,传统的 UNIX 实现将进程分为两类:特权进程(其有效用户 ID 为 0,称为超级用户或 root),以及非特权进程(其有效 UID 非零)。

特权进程绕过所有内核权限检查,而非特权进程则根据进程的凭证(通常是:有效 UID、有效 GID 和补充组列表)接受完全的权限检查。

从内核 2.2 开始,Linux 将传统上与超级用户相关联的权限划分为独立的单元,称为能力,这些能力可以独立启用或禁用。能力是每个线程的属性。

提示

有哪些能力存在?

Linux 能力的列表可以在标准 Linux 系统的/usr/include/linux/capability.h文件中找到。更易读的形式可以通过能力的 man 页面查看。可以通过man 7 capabilities命令查看。Linux 能力包括授予 root 用户的每项特殊权限,例如,覆盖文件权限、使用原始网络连接等。

能力可以在进程执行期间分配给线程,也可以分配给文件系统中的可执行文件。在这两种情况下,总是有三组能力:

  • 允许集 (p): 允许集包含线程可能请求的所有能力。如果一个可执行文件被启动,它的允许集将用于初始化进程的允许集。

  • 可继承能力集 (i): 执行集的可继承能力集定义了哪些能力可以从线程转发给子进程。然而,只有在子进程的可执行文件的可继承能力集中定义的能力才会被转发给子进程。因此,只有当能力存在于父进程的可继承能力集和子进程可执行文件的文件属性中时,这个能力才会被继承。

  • 有效能力集 (e): 这是 Linux 内核在请求特权操作时,实际检查的能力集。通过调用cap_set_proc(),进程可以禁用或启用这些能力。只有在允许集(p)中的能力才能被启用。在文件系统中,有效能力集由一个位表示。如果这个位被设置,执行文件将以所有允许的能力为有效能力启动。如果该位未设置,新进程将启动时没有有效能力。

    注意

    能力赋予可执行文件管理员特权,而无需在文件模式中设置 SUID 位。因此,在进行取证调查时,所有文件能力应该被记录。

使用 Python 的 ctypes,可以利用共享的libcap.so.2库来检索来自目录树的所有文件能力,如下所示:

#!/usr/bin/env python

import ctypes
import os
from os.path import join
import sys

# load shared library
libcap2 = ctypes.cdll.LoadLibrary('libcap.so.2')

class cap2_smart_char_p(ctypes.c_char_p):
    """Implements a smart pointer to a string allocated
       by libcap2.so.2"""
    def __del__(self):
        libcap2.cap_free(self)

# note to ctypes: cap_to_text() returns a pointer
# that needs automatic deallocation
libcap2.cap_to_text.restype = cap2_smart_char_p

def caps_from_file(filename):
    """Returns the capabilities of the given file as text"""

 cap_t = libcap2.cap_get_file(filename)
    if cap_t == 0:
        return ''
    return libcap2.cap_to_text(cap_t, None).value

def get_caps_list(basepath):
    """Collects file capabilities of a directory tree.

    Arguments:
    basepath -- directory to start from"""

    result = {}
    for root, dirs, files in os.walk(basepath):
        for f in files:
            fullname = join(root, f)
            caps = caps_from_file(fullname)
            if caps != '':
                result[fullname] = caps

    return result

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print 'Usage %s root_directory' % sys.argv[0]
        sys.exit(1)

    capabilities = get_caps_list(sys.argv[1])

    for filename, caps in capabilities.iteritems():
        print "%s: %s" % (filename, caps)

第一行突出显示加载了libcap.so.2库,以便在 Python 中直接使用。由于能力的文本表示的内存是在这个库中分配的,因此由调用者——也就是我们的脚本——负责在使用后释放这块内存。为此任务选择的解决方案是扩展ctype默认表示的字符指针,即ctype.c_char_p。最终的cap2_smart_char_p类是所谓智能指针的简单版本:如果该类对象的 Python 表示被销毁,对象将自动调用cap_free()来释放之前由libcap.so.2分配的相应资源。

使用cap_get_file()库函数,可以获取文件的能力。随后的调用cap_to_text()将这种内部表示转换为人类可读的文本。

如果脚本保存为chap03_capabilities.py,则可以按照如下方式在实验室机器上调用:

user@lab:~$ python chap03_capabilities.py /usr

当然,输出结果高度依赖于 Linux 版本和发行版。它可能看起来像以下内容:

/usr/bin/gnome-keyring-daemon: = cap_ipc_lock+ep

该输出意味着只有一个可执行文件在/usr中设置了特殊能力:/usr/bin/gnome-keyring-daemon。该能力的名称由常量cap_ipc_lock给出,这项能力在许可集内,并在启动该程序时立即生效,表示为+ep

为了理解cap_ipc_lock的含义,我们将调用以下内容:

user@lab:~$ man 7 capabilities

然后,我们将搜索 CAP_IPC_LOCK。这表明该能力赋予了锁定进程内存部分或全部到 RAM 中的权限,并防止该进程被换出。由于gnome-keyring-daemon将用户凭证存储在 RAM 中,从安全角度看,具有防止这些凭证写入交换空间的权限是非常建议的。

注意

目前,大多数标准 Linux 发行版几乎不使用文件能力功能。因此,发现的文件能力——尤其是那些对正常操作不必要的能力——可能是系统操控的首个指示。

聚类文件信息

在前一节中,我们向您展示了如何从 Linux/Unix 文件系统中检索和收集文件元数据。在本节中,我们将提供一些示例,帮助定位文件系统元数据中的变化,这可能对进一步的调查者检查有意义。

创建直方图

创建直方图是将数据聚类到大小相等的区间,并绘制这些区间的大小的过程。使用 Python,可以通过 Python 的matplotlib模块轻松绘制这些直方图。包括用例、示例和 Python 源代码的详细文档可以在matplotlib.org/找到。

以下 Python 脚本可用于生成并显示目录树中文件访问时间和文件修改时间的直方图:

#!/usr/bin/env python

from datetime import datetime
from matplotlib.dates import DateFormatter
import matplotlib.pyplot as plt
import os
from os.path import join
import sys

# max. number of bars on the histogram
NUM_BINS = 200

def gen_filestats(basepath):
    """Collects metadata about a directory tree.

    Arguments:
    basepath -- root directory to start from

    Returns:
    Tuple with list of file names and list of
    stat results."""

    filenames = []
    filestats = []

    for root, dirs, files in os.walk(basepath):
        for f in files:
            fullname = join(root, f)
            filenames.append(fullname)
            filestats.append(os.lstat(fullname))
    return (filenames, filestats)

def show_date_histogram(times, heading='', block=False):
    """Draws and displays a histogram over the given timestamps.

    Arguments:
    times -- array of time stamps as seconds since 1970-01-01
    heading -- heading to write to the drawing
    block --- if True, the graph window waits for user interaction"""

    fig, ax = plt.subplots()

 times = map(lambda x: datetime.fromtimestamp(x).toordinal(), times)

 ax.hist(times, NUM_BINS)
    plt.xlabel('Date')
    plt.ylabel('# of files')
    plt.title(heading)

    ax.autoscale_view()

    ax.xaxis.set_major_formatter(DateFormatter('%Y-%m-%d'))
    fig.autofmt_xdate()

    fig.show()
    if block:
        plt.show()

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print 'Usage %s base_directory' % sys.argv[0]
        sys.exit(1)

    path = sys.argv[1]

    (names, stats) = gen_filestats(path)

    # extract time stamps
    mtimes = map(lambda x: x.st_mtime, stats)
    atimes = map(lambda x: x.st_atime, stats)

    show_date_histogram(mtimes, 'mtimes of ' + path)
    show_date_histogram(atimes, 'atimes of ' + path, True)

gen_filestats()函数遍历目录树并收集所有 inode 数据。show_date_histogram()函数用于生成并显示数据的直方图。

在代码的第一行中,时间戳的编码方式发生了变化。这是必要的,因为 inode 数据给出了自 1970 年 1 月 1 日以来的秒数的时间戳。这个格式正是datetime.fromtimestamp()所期望的。然而,Matplotlib 需要的时间戳是自公历 0001 年 1 月 1 日以来的天数。幸运的是,datetime类可以通过其toordinal()方法提供这种表示。

下一行突出显示的是实际生成和绘制下图中的直方图。show_date_histogram()的其他所有语句仅仅是为了在图形中添加标签和格式化。

以下是标准 Linux 桌面系统中/sbin目录的示例结果:

创建直方图

在这里,主要的系统更新日期非常明显。调查员应当意识到,文件元数据和这些直方图不包含历史文件信息。因此,从前面的直方图中不能推断出 2011 年 12 月之前几乎没有或没有进行过安全更新。更可能的情况是,2011 年 12 月之前打过补丁的大部分文件后来被修改了,从而在直方图中掩盖了旧的补丁。

让我们来看一下这个目录的访问时间分布:

创建直方图

这个直方图提供了一些关于该目录访问模式的见解。首先,系统上启用了 atime 时间戳跟踪。否则,直方图中不会显示当前的访问时间戳。大约一半的文件最近被读取过。这些信息可以用来验证获取证据的时间或系统管理员声称将系统下线的时间。

此外,这个目录的内容很可能没有定期进行病毒扫描,也未最近被打包成压缩档案。因为这两种操作通常会更新 atime 时间戳。

如果在系统中执行以下命令,则会对/sbin进行病毒扫描。当然,扫描程序必须读取该目录中的每个文件以扫描其内容:

user@lab:~$ clamscan –i /sbin

/sbin的 atime 图反映了如下变化:

创建直方图

变化是显而易见的:大部分条形图在当前时间(即病毒扫描时刻)合并成一个。时间尺度被拉伸到了单一天。结果,左侧的条形图也可以被视为病毒扫描的结果。

注意

如果一个目录的所有atime时间戳都显示在同一天,那么这个目录最近可能被复制、病毒扫描过,或者被打包成压缩档案。当然,具备足够权限的情况下,时间戳也可能是手动设置的。

高级直方图技术

在前一节中,直方图被用来了解文件系统的元数据。然而,这些直方图有一些缺点,具体如下:

  • 所有的直方图条形图宽度相同

  • 条形图没有按照数据的实际聚集情况进行排列,例如,一个聚集可能被分布在两个条形图上。

  • 异常值消失了,即低条形图容易与空条形图混淆。

因此,本节展示了如何使用简单的机器学习算法对数据进行更智能的聚类。一个广泛使用的 Python 机器学习库是scikit-learn。在多个领域中,它提供了几种用于聚类输入数据的算法。我们推荐访问scikit-learn.org以获取所有算法的概述及其使用示例。以下 Python 脚本使用 scikit-learn 中的DBSCAN算法来生成给定宽度(以天为单位)的聚类:

#!/usr/bin/python

from datetime import date
import numpy as np
import os
from os.path import join
from sklearn.cluster import DBSCAN
import sys

def gen_filestats(basepath):
    """Collects metadata about a directory tree.

    Arguments:
    basepath -- root directory to start from

    Returns:
    Tuple with list of file names and list of
    stat results."""

    filenames = []
    filestats = []

    for root, dirs, files in os.walk(basepath):
        for f in files:
            fullname = join(root, f)
            filenames.append(fullname)
            filestats.append(os.lstat(fullname))
    return (filenames, filestats)

def _calc_clusters(data, eps, minsamples):
    samples = np.array(data)
 db = DBSCAN(eps=eps, min_samples=minsamples).fit(samples)
    return (db.labels_, db.core_sample_indices_)

def calc_atime_clusters(stats, days=1, mincluster=5):
    """Clusters files regarding to their 'last access' date.

    Arguments:
    stats -- file metadata as returned as 2nd element by gen_filestats
    days  -- approx. size of a cluster (default: accessed on same day)
    mincluster -- min. number of files to make a new cluster

    Returns:
    Tuple with array denoting cluster membership
    and indexes of representatives of cluster cores"""

    atimes = map(lambda x: [x.st_atime], stats)
    return _calc_clusters(atimes, days * 24 * 3600, mincluster)

def calc_mtime_clusters(stats, days=1, mincluster=5):
    """Clusters files regarding to their 'last modified' date.

    Arguments:
    stats -- file metadata as returned as 2nd element by gen_filestats
    days  -- approx. size of a cluster (default: accessed on same day)
    mincluster -- min. number of files to make a new cluster

    Returns:
    Tuple with array denoting cluster membership
    and indexes of representatives of cluster cores"""

    mtimes = map(lambda x: [x.st_mtime], stats)
    return _calc_clusters(mtimes, days * 24 * 3600, mincluster)

def calc_histogram(labels, core_indexes, timestamps):
    # reserve space for outliers (label -1), even if there are none
    num_entries = len(set(labels)) if -1 in labels else len(set(labels))+1

    counters = [0] * num_entries
    coredates = [0] * num_entries

    for c in core_indexes:
        i = int(c)
        coredates[int(labels[i])+1] = timestamps[i]

    for l in labels:
        counters[int(l)+1] += 1

    return zip(coredates, counters)

def print_histogram(histogram):
    # sort histogram by core time stamps
    sort_histo = sorted(histogram, cmp=lambda x,y: cmp(x[0],y[0]))

    print '[date around] [number of files]'
    for h in sort_histo:
        if h[0] == 0:
            print '<outliers>',
        else:
            t = date.fromtimestamp(h[0]).isoformat()
            print t,
        print '    %6d' % h[1]

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print 'Usage %s base_directory [number of days in one cluster]' % sys.argv[0]
        sys.exit(1)

    days = 1
    if len(sys.argv) > 2:
        days = int(sys.argv[2])

    names, stats = gen_filestats(sys.argv[1])

    print '%d files to analyze...' % len(names)

    atime_labels, atime_cores = calc_atime_clusters(stats, days)
    mtime_labels, mtime_cores = calc_mtime_clusters(stats, days)

    atimes = map(lambda x: x.st_atime, stats)
    mtimes = map(lambda x: x.st_mtime, stats)

    ahisto = calc_histogram(atime_labels, atime_cores, atimes)
    mhisto = calc_histogram(mtime_labels, mtime_cores, mtimes)

    print "\n=== Access time histogram ==="
    print_histogram(ahisto)

    print "\n=== Modification time histogram ==="
    print_histogram(mhisto)

gen_filestats()函数与上一节用于基本直方图的版本相同。calc_atime_clusters()calc_mtime_clusters()函数提取收集信息的访问时间和修改时间,并将其传递给_calc_clusters中的聚类生成。DBSCAN使用两个参数初始化:聚类的大小(eps,单位为秒)和构成聚类的最小样本数(min_samples)。设置完算法的参数后,数据将通过fit()方法进行聚类处理。

该聚类的结果是一个元组,包含标签和每个标签的索引列表。标签与输入数据中找到的聚类相关联。其值是聚类中所有日期的中心,即平均日期。特殊标签-1作为所有离群点的容器,表示无法分配到任何聚类的数据。

calc_histogram()函数计算每个聚类的大小并返回直方图,即标签和条目的数量,作为一个二维数组。

我们可以在/sbin目录上运行此 Python 脚本,如下所示:

user@lab:~$ python timecluster.py /sbin

输出可能类似于以下内容:

202 files to analyze...

=== Access time histogram ===
[date around] [number of files]
<outliers>          0
2015-05-24        202

=== Modification time histogram ===
[date around] [number of files]
<outliers>         64
2011-11-20          9
2012-02-09          5
2012-03-02          6
2012-03-31         11
2012-07-26          6
2012-09-11         10
2013-01-18         15
2013-01-26          6
2013-03-07          8
2014-06-18         29
2014-11-20          7
2015-02-16         19
2015-05-01          7

这里,访问时间直方图只显示一个条目,反映了我们之前对该目录的扫描。此外,所有最近的主要系统更新都显示在修改时间直方图中。

使用此工具,研究人员能够对文件系统信息进行聚类,以检测目录的扫描或提取,以及忽略的安全补丁。此外,特殊的聚类-1可以进行分析,从中获取在主要系统更新之外被修改的文件名。

总结

在本章中,我们看到了一些微软 Windows 和 Linux(以及类 Linux)系统特有属性的突出示例。现在,您已经能够从 Windows 事件日志、Windows 注册表、Linux 文件和 Linux 文件系统中提取信息。利用 Python,所有这些信息都可以自动或半自动地分析,以查找妥协指标、重建最近的系统活动和外泄迹象。

此外,读取文件系统功能让我们了解如何使用 ctype 加载本地库来辅助文件系统分析。

在文件信息的聚类中,我们提供了第一个示例,演示如何使用基本的机器学习算法来支持取证分析。

现在我们已经了解了本地系统,我们将进入下一章,看看网络流量以及如何在其中搜索妥协指示器(IOC)。

第四章:使用 Python 进行网络取证

本章将重点介绍网络层特定的取证调查部分。我们将选择一个最广泛使用的 Python 包,Scapy,用于处理和分析网络流量,以及美国陆军研究实验室发布的一个新开源框架 Dshell。对于这两个工具包,我们选择了有趣证据的示例。本章将教你以下内容:

  • 如何在网络流量中搜索 IOC

  • 如何提取文件进行进一步分析

  • 如何通过 服务器消息块SMB)监控访问的文件

  • 如何构建自己的端口扫描器

在调查过程中使用 Dshell

Dshell 是一个基于 Python 的网络取证分析工具包,由美国陆军研究实验室开发,并在 2014 年末作为开源发布。它可以帮助简化网络层的取证调查。该工具包配备了大量可以开箱即用的解码器,非常有用。以下是其中一些解码器:

  • dns: 提取并总结 DNS 查询/响应

  • reservedips: 识别落在保留 IP 空间中的 DNS 解析

  • large-flows: 显示至少传输了 1MB 的网络流

  • rip-http: 从 HTTP 流量中提取文件

  • protocols: 识别非标准协议

  • synrst: 检测连接失败的尝试(SYN 后跟 RST/ACK)

可以通过从 GitHub 克隆源代码来安装 Dshell 到我们的实验环境,地址为 github.com/USArmyResearchLab/Dshell,然后运行 install-ubuntu.py。此脚本将自动下载缺失的包,并构建之后所需的可执行文件。Dshell 可以用于处理在事件期间记录的 pcap 文件,或作为 IDS 警报的结果。数据包捕获pcap)文件是由 libpcap(在 Linux 上)或 WinPcap(在 Windows 上)创建的。

在接下来的章节中,我们将通过展示来自 malware-traffic-analysis.net 的真实场景,说明调查员如何利用 Dshell 工具包。

第一个示例是用户通过邮件链接遇到的恶意 ZIP 文件。用户登录到 Gmail 并点击邮件中的下载链接。这可以通过 Dshell 的 web 解码器轻松查看,如下所示:

user@lab:~$ source labenv/bin/activate
(labenv)user@lab:~$ ./dshell

(labenv)user@lab:~$ Dshell> decode -d web infected_email.pcap

web 2015-05-29 16:23:44     10.3.162.105:62588 ->   74.125.226.181:80    ** GET mail.google.com/ HTTP/1.1 // 200 OK  2015-05-29 14:23:40 **

web 2015-05-29 16:24:15     10.3.162.105:62612 <-    149.3.144.218:80    ** GET sciclubtermeeuganee.it/wp-content/plugins/feedweb_data/pdf_efax_message_3537462.zip HTTP/1.1 // 200 OK  2015-05-28 14:00:22 **

查看之前的流量提取时,ZIP 文件可能是第一个妥协指示器(Indicator of Compromise)。因此,我们应当更深入地分析它。最简单的方法是将 ZIP 文件从 pcap 文件中提取出来,并将其 md5 哈希值与 VirusTotal 数据库进行比较:

(labenv)user@lab:~$ Dshell> decode -d rip-http --bpf "tcp and port 62612" infected_email.pcap

rip-http 2015-05-29 16:24:15     10.3.162.105:62612 <-    149.3.144.218:80    ** New file: pdf_efax_message_3537462.zip (sciclubtermeeuganee.it/wp-content/plugins/feedweb_data/pdf_efax_message_3537462.zip) **
 --> Range: 0 - 132565
rip-http 2015-05-29 16:24:15     10.3.162.105:62612 <-    149.3.144.218:80    ** File done: ./pdf_efax_message_3537462.zip (sciclubtermeeuganee.it/wp-content/plugins/feedweb_data/pdf_efax_message_3537462.zip) **

(labenv)user@lab:~$ Dshell> md5sum pdf_efax_message_3537462.zip 
9cda66cba36af799c564b8b33c390bf4  pdf_efax_message_3537462.zip

在这个简单的案例中,我们的第一次猜测是正确的,因为下载的 ZIP 文件包含了另一个可执行文件,该文件是一个信息窃取恶意软件工具包的一部分,如下图所示:

在调查过程中使用 Dshell

另一个非常好的例子是通过 SMB 协议搜索网络共享上的访问文件。当试图找出攻击者是否能够访问甚至外泄数据时,这可以非常有帮助——如果成功的话——哪些数据可能已经泄露:

(labenv)user@lab:~$ Dshell> decode -d smbfiles exfiltration.pcap

smbfiles 2005-11-19 04:31:58    192.168.114.1:52704 ->  192.168.114.129:445   ** VNET3\administrator \\192.168.114.129\TEST\torture_qfileinfo.txt (W) **
smbfiles 2005-11-19 04:31:58    192.168.114.1:52704 ->  192.168.114.129:445   ** VNET3\administrator \\192.168.114.129\TESTTORTUR~1.TXT (-) **
smbfiles 2005-11-19 04:31:58    192.168.114.1:52705 ->  192.168.114.129:445   ** VNET3\administrator \\192.168.114.129\TEST\testsfileinfo\fname_test_18.txt (W) **

rip-smb-uploads解码器的帮助下,Dshell 还能够自动提取记录的 pcap 文件中的所有上传文件。另一个有趣的例子是通过 Snort 规则帮助搜索 IOC,这也可以通过 Dshell 完成,如下所示:

(labenv)user@lab:~$ Dshell> decode -d snort malicious-word-document.pcap --snort_rule 'alert tcp any 443 -> any any (msg:"ET CURRENT_EVENTS Tor2Web .onion Proxy Service SSL Cert (1)"; content:"|55 04 03|"; content:"*.tor2web.";)' –snort_alert

snort 2015-02-03 01:58:26      38.229.70.4:443   --  192.168.120.154:50195 ** ET CURRENT_EVENTS Tor2Web .onion Proxy Service SSL Cert (1) **
snort 2015-02-03 01:58:29      38.229.70.4:443   --  192.168.120.154:50202 ** ET CURRENT_EVENTS Tor2Web .onion Proxy Service SSL Cert (1) **
snort 2015-02-03 01:58:32      38.229.70.4:443   --  192.168.120.154:50204 ** ET CURRENT_EVENTS Tor2Web .onion Proxy Service SSL Cert (1) **

在这个例子中,我们打开了一个可能含有恶意软件的 Word 文档,该文档是通过垃圾邮件收到的。这个 Word 文档试图下载Vawtrak恶意软件,并通过Tor网络进行通信。我们使用的 Snort 规则来源于 Emerging Threats(参考 www.emergingthreats.net/),它正在搜索已知的 Tor2Web 服务的 SSL 证书(一个允许用户在不使用 Tor 浏览器的情况下访问 Tor Onion 服务的服务)。可以使用所有可用的 Snort 规则进行类似的检查,如果你在网络中寻找特定的攻击,这些规则会非常有帮助。

作为示例 pcap 文件的替代方案,所有展示的例子也可以在活动网络连接上运行,通过使用 –i interface_name 标志,如下所示:

(labenv)user@lab:~$ Dshell> decode -d netflow -i eth0

2015-05-15 21:35:31.843922   192.168.161.131 ->    85.239.127.88  (None -> None)  TCP   52007      80     0      0        0        0  5.1671s
2015-05-15 21:35:31.815329   192.168.161.131 ->    85.239.127.84  (None -> None)  TCP   46664      80     0      0        0        0  5.1976s
2015-05-15 21:35:32.026244   192.168.161.131 ->    208.91.198.88  (None -> None)  TCP   40595      80     9     25     4797   169277  6.5642s
2015-05-15 21:35:33.562660   192.168.161.131 ->    208.91.198.88  (None -> None)  TCP   40599      80     9     19     4740    85732  5.2030s
2015-05-15 21:35:32.026409   192.168.161.131 ->    208.91.198.88  (None -> None)  TCP   40596      80     7      8     3843   121616  6.7580s
2015-05-15 21:35:33.559826   192.168.161.131 ->    208.91.198.88  (None -> None)  TCP   40597      80     5     56     2564   229836  5.2732s

在这个例子中,我们正在生成一个活动连接的 NetFlow 数据。Dshell 完全用 Python 编写,这使得它非常适应法医调查人员的各种需求,也可以与其他工具或预定义流程一起使用。

如果你想测试这个,可以从 www.emergingthreats.net/ 下载示例文件。

在调查过程中使用 Scapy

另一个很棒的基于 Python 的工具是 Scapy,它可以分析和操控网络流量。根据开发者网站,www.secdev.org/projects/scapy/

“Scapy 是一个强大的交互式数据包操控程序。它能够伪造或解码多种协议的数据包,发送它们到网络中,捕获它们,匹配请求和回复,等等。”

Scapy 与标准工具(以及 Dshell)不同,它提供了一个功能,允许调查人员编写小型 Python 脚本,来操控或分析网络流量——无论是已录制的形式还是实时的。此外,Scapy 还具备执行深度数据包解析、被动操作系统指纹识别或通过第三方工具(如 GnuPlot)绘图的能力,因为内建功能已经可以使用。

以下 Python 脚本摘自Grow Your Own Forensic Tools: A Taxonomy of Python Libraries Helpful for Forensic AnalysisSANS Institute InfoSec Reading Room,是一个简短的示例,展示了 Scapy 的强大功能:

import scapy, GeoIP
from scapy import *

geoIp = GeoIP.new(GeoIP.GEOIP_MEMORY_CACHE)
def locatePackage(pkg):
  src=pkg.getlayer(IP).src
  dst=pkg.getlayer(IP).dst
  srcCountry = geoIp.country_code_by_addr(src)
  dstCountry = geoIp.country_code_by_addr(dst)
  print src+"("+srcCountry+") >> "+dst+"("+dstCountry+")\n"
try:
  while True:
    sniff(filter="ip", prn=locatePackage, store=0)
except KeyboardInterrupt:
  print "\n" + "Scan Aborted!"

本脚本记录了关于 IP 地址源和正在进行的网络连接目的地的地理位置统计信息。将 Scapy 包导入我们的 Python 脚本后,我们调用 sniff 函数并使用过滤器只检测 IP 数据包。如果计划长时间运行 Scapy 脚本,sniff 函数中的最后一个参数非常重要。通过 store 参数的帮助,可以告诉 Scapy 在运行时不要将所有数据包缓存到内存中,从而使脚本更快速且节省资源。随后的函数查询从每个数据包中提取的源和目的地 IP 地址的地理位置。

在下一个示例中,我们将说明如何在 Scapy 的帮助下构建一个非常简单的端口扫描器,具体如下:

#!/usr/bin/env python

import sys
from scapy.all import *

targetRange = sys.argv[1]
targetPort = sys.argv[2]
conf.verb=0

p=IP(dst=targetRange)/TCP(dport=int(targetPort), flags="S")
ans,unans=sr(p, timeout=9)

for answers in ans:
        if answers[1].flags == 2:
                print answers[1].src

这个小脚本能够扫描给定开放端口的整个 IP 范围。如果你正在寻找监听在端口 80 上的 Web 服务器,你可以使用如下的脚本:

(labenv)user@lab:~$ ./scanner.py 192.168.161.1/24 80
WARNING: No route found for IPv6 destination :: (no default route?)
Begin emission:..........
192.168.161.12
192.168.161.34
192.168.161.111
....

我们还可以使用地址解析协议ARP)对我们的系统连接的整个网络范围进行侦察。借助以下脚本,我们可以获得一个漂亮打印的表格,列出所有在线的 IP 地址及其相应的 MAC 地址:

#! /usr/bin/env python

import sys
from scapy.all import srp,Ether,ARP,conf

if len(sys.argv) != 2:
        print "Usage: arp_ping <net> (e.g.,: arp_ping 192.168.1.0/24)"
        sys.exit(1)

conf.verb=0
ans,unans=srp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst=sys.argv[1]),
timeout=9)

print r"+------------------+-----------------+"
print r"|       MAC        |        IP       |"
print r"+------------------+-----------------+"
for snd,rcv in ans:
        print rcv.sprintf(r" %Ether.src% | %ARP.psrc%")
print r"+------------------+-----------------+"

执行脚本时,我们将收到类似以下内容的信息:

(labenv)user@lab:~$ ./arp_ping.py 192.168.161.131/24
WARNING: No route found for IPv6 destination :: (no default route?)
+------------------+-----------------+
|       MAC        |        IP       |
+------------------+-----------------+
 00:50:56:c0:00:08 | 192.168.161.1
 00:50:56:f5:d3:83 | 192.168.161.2
 00:50:56:f1:2d:28 | 192.168.161.254
+------------------+-----------------+

像这两个脚本一样的脚本,在系统上没有可用的端口扫描器,或者你希望将端口扫描器与其他基于 Python 的脚本串联以进行调查时非常有用。

摘要

本章提供了基于网络的取证调查领域的概述,并展示了使用 Dshell 和 Scapy 的示例。我们演示了如何查找可疑的 HTTP 连接(如文件下载)或如何通过 SMB 协议搜索泄露的数据。第二部分,我们利用 Scapy 创建了我们自己的端口扫描器,并用它来收集更多关于潜在被攻破系统的信息。

在我们讨论了取证算法、Windows 和 Unix 系统以及网络层之后,下一章将讨论虚拟化系统和虚拟机监控程序(hypervisor),这些已经成为每个公司不可或缺的一部分。

第五章:使用 Python 进行虚拟化取证

当前,虚拟化是现代 IT 领域最热门的概念之一。对于取证分析,它既带来了新的挑战,也引入了新的技术。

在本章中,我们将展示虚拟化如何引入以下内容:

  • 新的攻击途径

  • 收集证据的新机会

  • 取证分析的新目标,例如虚拟化层

  • 取证数据的新来源

将虚拟化视为一个新的攻击面

在开始取证分析之前,理解该寻找什么是非常重要的。通过虚拟化,出现了新的攻击途径和场景。在接下来的章节中,我们将描述一些场景,并讲解如何寻找相应的证据。

作为附加抽象层的虚拟化

虚拟化是模拟 IT 系统(如服务器、工作站、网络和存储)的技术。负责虚拟硬件仿真的组件被定义为虚拟化管理程序。下图展示了当前使用的两种主要的系统虚拟化类型:

作为附加抽象层的虚拟化

左侧的架构被称为裸机虚拟化管理程序架构,也被称为Type 1虚拟化管理程序。在这种架构中,虚拟化管理程序替代了操作系统,直接运行在裸机硬件上。Type I 虚拟化管理程序的例子包括 VMware ESXi 和 Microsoft Hyper-V。

图片右侧的架构通常被称为桌面虚拟化Type 2虚拟化管理程序。在这种架构中,有一个标准操作系统运行在硬件上,例如标准的 Windows 8 或 Linux 桌面系统。虚拟化管理程序在该操作系统上与其他本地应用程序一起运行。虚拟化管理程序的一些功能可能会直接与底层硬件交互,例如通过提供特殊的驱动程序。对于 Type 2 虚拟化管理程序,直接运行在硬件上的操作系统被称为主机操作系统,而运行在虚拟机上的操作系统被称为客户操作系统。Type 2 虚拟化管理程序架构的例子包括 Oracle VirtualBox 和 VMware Workstation。这些虚拟化管理程序可以像其他应用程序一样在现有操作系统上安装。

注意

尽管 Hyper-V 看起来像 Type 2 虚拟化管理程序,实际上它在安装过程中将主机操作系统转换为另一个客户操作系统,从而建立了 Type 1 架构。

几乎所有虚拟化环境的一个共同特点是能够创建快照。虚拟系统的快照包含系统在某一时刻的冻结状态。所有在快照创建之后对系统的更改,可以通过虚拟机监控器(hypervisor)回滚到创建快照时的状态。此外,大多数系统允许对单个系统拥有多个快照,并在不同的快照之间进行回滚和前进。快照可以作为法医数据的来源,我们将在使用虚拟化作为证据来源一节中演示这一点。

提示

对于法医分析,快照应当被视为独立的机器!

如果虚拟系统需要进行法医分析,始终检查该系统是否为虚拟系统以及是否存在快照。如果存在快照,法医分析必须针对每一个快照进行重复,就像每个快照都是一个独立的虚拟机。这样做的理由是,很可能不知道系统何时被攻破,攻击者何时试图销毁证据,最重要的是,攻击发生时运行的是哪个版本的虚拟机。

大多数虚拟化环境由多个虚拟机监控器组成。为了简化多个虚拟机监控器的管理,并启用额外的功能,例如:在虚拟机监控器之间移动机器以实现故障转移、负载均衡以及节省电力,这些环境提供了一个集中管理所有虚拟机监控器的组件。在 VMware vSphere 的情况下,这个管理组件叫做vCenter Server,如下所示:

虚拟化作为额外的抽象层

如果使用vCenter Server,那么所有的管理任务应该通过这个vCenter Server实例来处理。

这个新的虚拟机监控器层是如何影响攻击场景和法医分析的?

新的虚拟机监控器层的引入也带来了一个新的层次,这个层次可以被用来在不被察觉的情况下操控虚拟系统,并且增加了一个新的层次,可能会成为攻击的目标。在接下来的章节中,我们将提供一些通过虚拟机监控器实施的攻击示例。

恶意虚拟机的创建

如果攻击者能够访问虚拟机监控器,他可能会创建新的虚拟资源。这些资源可以作为网络中的桥头堡,或者只是窃取环境中的内存和计算资源。因此,在进行虚拟机监控器环境的法医分析时,提取虚拟资源的创建和处置过程至关重要。

幸运的是,每个广泛使用的虚拟化环境都提供了 API 和语言绑定,可以列举该环境中的虚拟机及其他虚拟资源。在本章中,我们选择了 VMware vSphere 作为虚拟化环境的代表性示例。

注意

VMware vSphere 是最常用的本地虚拟化环境之一。它的基本结构由一个中央管理实例(称为 vCenter Server)和一个或多个实际托管虚拟环境(虚拟化管理程序,简称ESXi)的系统组成。要通过 Python 编程控制 vSphere 环境,使用的是 pyVmomi。这个 Python SDK 可以在 GitHub 上找到,网址为 github.com/vmware/pyvmomi

在接下来的内容中,我们将使用pyVmomi创建所有虚拟机的列表。建议定期运行此类清单扫描,将现有虚拟资产的列表与本地库存数据库进行对比。

我们建议使用pip安装 pyVmomi:

user@lab:~$ pip install --upgrade pyVmomi

提示

pyVmomi 的示例代码

在 GitHub 上有一个社区提供的关于pyVmomi的示例代码项目。更多关于这些示例的信息可以在 vmware.github.io/pyvmomi-community-samples/ 查阅。

然后,可以使用以下示例脚本来枚举 vSphere 环境中的所有系统:

#!/usr/bin/env python

from pyVim import connect
from pyVmomi import vmodl
import sys

def print_vm_info(vm):
    """
    Print the information for the given virtual machine.
    If vm is a folder, recurse into that folder.
    """

    # check if this a folder...
    if hasattr(vm, 'childEntity'):
        vms = vm.childEntity
        for child in vms:
            print_vm_info(child)

    vm_info = vm.summary

    print 'Name:      ', vm_info.config.name
    print 'State:     ', vm_info.runtime.powerState
    print 'Path:      ', vm_info.config.vmPathName
    print 'Guest:     ', vm_info.config.guestFullName
    print 'UUID:      ', vm_info.config.instanceUuid
    print 'Bios UUID: ', vm_info.config.uuid
    print "----------\n"

if __name__ == '__main__':
    if len(sys.argv) < 5:
        print 'Usage: %s host user password port' % sys.argv[0]
        sys.exit(1)

    service = connect.SmartConnect(host=sys.argv[1],
                                   user=sys.argv[2],
                                   pwd=sys.argv[3],
                                   port=int(sys.argv[4]))

    # access the inventory
    content = service.RetrieveContent()
    children = content.rootFolder.childEntity

    # iterate over inventory
    for child in children:
        if hasattr(child, 'vmFolder'):
            dc = child
        else:
            # no folder containing virtual machines -> ignore
            continue

        vm_folder = dc.vmFolder
        vm_list = vm_folder.childEntity
        for vm in vm_list:
            print_vm_info(vm)

该脚本创建了一个与 vCenter Server 平台的连接。然而,它也可以用来连接到单个 ESXi 虚拟化管理程序实例。这是因为该脚本所提供的 API 在两种管理模式下是相同的。

注意

pyVmomi使用的 API 是vSphere Web Service API。在 vSphere Web Services SDK 中有详细的描述,网址为 www.vmware.com/support/developer/vc-sdk/

高亮的行表明脚本使用了递归来枚举所有虚拟机。这是必要的,因为在 VMware vSphere 中,虚拟机可以被放入嵌套组中。

以下是此脚本的示例调用,以及输出的单个虚拟机信息:

user@lab:~$ python enumerateVMs.py 192.168.167.26 'readonly' 'mypwd' 443
Name:     vCenterServer
State:      poweredOff
Path:      [datastore1] vCenterServer/vCenterServer.vmx
Guest:     Microsoft Windows Server 2012 (64-bit)
UUID:      522b96ec-7987-a974-98f1-ee8c4199dda4
Bios UUID: 564d8ec9-1b42-d235-a67c-d978c5107179
----------

输出列出了虚拟机的名称、当前状态、配置文件路径、操作系统提示信息,以及实例和 BIOS 配置的唯一 ID。路径信息尤其重要,因为它显示了虚拟机配置文件和数据文件的存储位置。

系统克隆

在上一节中,我们使用了虚拟化管理程序的 API 来获取取证数据。在本节中,我们将寻找滥用此 API 的痕迹。因此,我们将分析 vSphere 安装的日志信息。

注意

在中央日志系统中收集日志信息

在本节中,我们假设日志信息是按照 vSphere 安装的默认设置存储的。然而,在设置系统时,我们建议将日志信息存储在专用的日志系统中。这可以使攻击者更难以篡改系统日志,因为他不仅需要访问目标系统,还需要访问中央日志收集系统。许多中央日志收集系统的另一个优势是内置的日志分析功能。

虽然强烈建议为法医分析保留所有系统日志的副本,但也可以使用 VMware vSphere 的事件浏览器单独查看单个事件,如下所示:

系统克隆

vSphere 环境提供收集并存储所有日志文件的归档功能。请按照以下步骤获取所有可用日志数据的归档:

  • 使用 Windows 版本的 vSphere Web 客户端登录到vCenter Server

  • 管理菜单中,选择导出系统日志

  • 选择一个或多个 vCenter Server 导出日志,如下所示:系统克隆

  • 在提示选择系统日志时,确保选择所有日志类型,如下所示:系统克隆

日志文件以压缩归档的形式保存。每个归档代表一个系统的日志信息,即 vCenter Server 或 ESXi 主机。

首先,我们将使用 tar 提取收集的日志文件,命令如下:

user@lab:~$ tar xfz 192.168.167.26-vcsupport-2015-07-05@11-21-54.tgz

该归档的文件名遵循格式:主机/IP—vcsupport(用于 vCenter Server)—时间戳。该归档中的目录遵循 vc-主机名-时间戳命名规则,例如 vc-winserver-2015-07-05--02.19。归档名称和其中包含的目录的时间戳通常不匹配。这可能是由于时钟漂移以及传输和压缩日志所需的时间造成的。

在接下来的内容中,我们将使用 vCenter Server 的日志重建表示虚拟机克隆的事件。在这个例子中,我们将利用日志的冗余性,使用 vCenter Server 核心服务之一的日志数据:vpxd,即核心 vCenter 守护进程:

#!/usr/bin/env python

import gzip
import os
from os.path import join
import re
import sys

# used to map session IDs to users and source IPs
session2user_ip = {}

def _logopen(filename):
    """Helper to provide transparent decompressing of compressed logs,
       if indicated by the file name.
    """
    if re.match(r'.*\.gz', filename):
        return gzip.open(filename, 'r')

    return open(filename, 'r')

def collect_session_data(vpxlogdir):
    """Uses vpx performance logs to map the session ID to
       source user name and IP"""
    extract = re.compile(r'SessionStats/SessionPool/Session/Id=\'([^\']+)\'/Username=\'([^\']+)\'/ClientIP=\'([^\']+)\'')

    logfiles = os.listdir(vpxlogdir)
    logfiles = filter(lambda x: 'vpxd-profiler-' in x, logfiles)
    for fname in logfiles:
        fpath = join(vpxlogdir, fname)
        f = _logopen(fpath)

        for line in f:
            m = extract.search(line)
            if m:
                session2user_ip[m.group(1)] = (m.group(2), m.group(3))

        f.close()

def print_cloning_hints(basedir):
    """Print timestamp, user, and IP address for VM cloning without
       by reconstructing from vpxd logs instead of accessing
       the 'official' event logs"""
    vpxlogdir = join(basedir, 'ProgramData',
                              'vCenterServer',
                              'logs',
                              'vmware-vpx')
    collect_session_data(vpxlogdir)

    extract = re.compile(r'^([^ ]+).*BEGIN task-.*?vim\.VirtualMachine\.clone -- ([0-9a-f-]+).*')

    logfiles = os.listdir(vpxlogdir)
    logfiles = filter(lambda x: re.match('vpxd-[0-9]+.log(.gz)?', x), logfiles)
    logfiles.sort()

    for fname in logfiles:
        fpath = join(vpxlogdir, fname)
        f = _logopen(fpath)

        for line in f:
            m = extract.match(line)
            if m == None:
                continue

            timestamp = m.group(1)
            session = m.group(2)
            (user, ip) = session2user_ip.get(session, ('***UNKNOWN***', '***UNKNOWN***'))
            print 'Hint for cloning at %s by %s from %s' % (timestamp, user, ip)

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print 'Usage: %s vCenterLogDirectory' % sys.argv[0]
        sys.exit(1)

    print_cloning_hints(sys.argv[1])

首先,这个脚本读取所谓的 vpxd 性能日志。该日志包含有关客户端会话的数据,我们使用它提取从唯一会话标识符到客户端用户名及客户端连接的 IP 地址的映射。第二步,搜索 vpxd 的主日志,查找 vim.VirtualMachine.clone 类型任务的开始,即在服务器端克隆虚拟机。然后,在从性能日志中提取的映射中查找会话信息,以获取可能的克隆事件数据,如下所示:

user@lab:~$ python extractCloning.py vc-winserver-2015-07-05--02.19/
Hint for cloning at 2015-07-05T01:30:01.071-07:00 by VSPHERE.LOCAL\Administrator from 192.168.167.26

在这个例子中,脚本揭示了Administrator账户被用来克隆虚拟机。这个提示可以与 vCenter Server 的事件日志相关联,并且在那里也会显示出来。如果没有显示,那么这就是一个强烈的迹象,表明环境可能已被入侵。

注意

根据你的系统环境,像克隆和导出虚拟机这样的操作可能是日常操作的一部分。在这种情况下,可以使用之前的脚本或其变体来检测执行这些操作的异常用户或来源 IP。

类似的搜索和关联也可以应用于其他感兴趣的事件。数据存储区文件的复制或虚拟机的导出是值得关注的候选项。

搜索虚拟资源的滥用

我们不仅仅是在寻找有动机的攻击者。通过虚拟化,还有合法的虚拟基础设施管理员,通过改变一些规则来简化自己的工作。此外,攻击者可能利用虚拟化的强大功能,根据自己的需求重塑基础设施的拓扑。在接下来的章节中,我们将展示一些场景和检测方法。

检测恶意网络接口

网络虚拟化允许操作人员在静态的物理网络中创建几乎任意的网络基础设施。这种能力有时被称为数据中心即服务DCaaS)。DCaaS 允许客户利用物理数据中心的定义部分,在软件中定义虚拟数据中心。

由于恶意访问此功能或人为错误,结果可能导致网络配置暴露内部资源到互联网,绕过防火墙,或允许访问恶意服务。

因此,我们将展示一种简单的方式,使用 Python 编程获取 vSphere 环境的网络配置。

提示

可视化虚拟网络

大多数虚拟化环境都具备可视化虚拟网络设置的内建能力。例如,VMware vSphere 可以创建网络拓扑图。在取证分析中,这可以作为起点,并支持将下一步的焦点集中在最有潜力的资源上。

检测恶意网络接口

这张图是通过 Windows 客户端软件为 VMware vCenter Server 生成的,展示了我们的测试设置。显然,EvesMachine未能正确连接,也就是说,它能够绕过防火墙

pyVmomi的社区示例脚本已经提供了一个脚本,用于遍历所有网络接口,github.com/vmware/pyvmomi-community-samples/blob/master/samples/getvnicinfo.py,并显示虚拟机的连接。因此,我们修改了这个脚本,仅显示那些有多个网络连接的虚拟机,如下所示:

#!/usr/bin/env python

from pyVim import connect
from pyVmomi import vmodl
from pyVmomi import vim
import sys

def generate_portgroup_info(content):
    """Enumerates all hypervisors to get
       network infrastructure information"""
    host_view = content.viewManager.CreateContainerView(content.rootFolder,
                                        [vim.HostSystem],
                                        True)
    hostlist = [host for host in host_view.view]
    host_view.Destroy()

    hostPgDict = {}
    for host in hostlist:
        pgs = host.config.network.portgroup
        hostPgDict[host] = pgs

    return (hostlist, hostPgDict)

def get_vms(content, min_nics=1):
    vm_view = content.viewManager.CreateContainerView(content.rootFolder,
                                        [vim.VirtualMachine],
                                        True)
    vms = [vm for vm in vm_view.view]
    vm_view.Destroy()

    vm_with_nics = []
    for vm in vms:
        num_nics = 0
        for dev in vm.config.hardware.device:
            # ignore non-network devices
            if not isinstance(dev, vim.vm.device.VirtualEthernetCard):
                continue

            num_nics = num_nics + 1
            if num_nics >= min_nics:
                vm_with_nics.append(vm)
                break

    return vm_with_nics

def print_vm_info(vm, hosts, host2portgroup, content):
    print "\n=== %s ===" % vm.name

    for dev in vm.config.hardware.device:
        if not isinstance(dev, vim.vm.device.VirtualEthernetCard):
            continue

        dev_backing = dev.backing
        if hasattr(dev_backing, 'port'):
            # NIC is connected to distributed vSwitch
            portGroupKey = dev.backing.port.portgroupKey
            dvsUuid = dev.backing.port.switchUuid
            try:
                dvs = content.dvSwitchManager.QueryDvsByUuid(dvsUuid)
            except:
                portGroup = 'ERROR: DVS not found!'
                vlanId = 'N/A'
                vSwitch = 'N/A'
            else:
                pgObj = dvs.LookupDvPortGroup(portGroupKey)
                portGroup = pgObj.config.name
                vlObj = pgObj.config.defaultPortConfig.vlan
                if hasattr(vlObj, 'pvlanId'):
                    vlanId = str(pgObj.config.defaultPortConfig.vlan.pvlanId)
                else:
                    vlanId = str(pgObj.config.defaultPortConfig.vlan.vlanId)
                vSwitch = str(dvs.name)
        else:
            # NIC is connected to simple vSwitch
            portGroup = dev.backing.network.name
            vmHost = vm.runtime.host

            # look up the port group from the
            # matching host
            host_pos = hosts.index(vmHost)
            viewHost = hosts[host_pos]
            pgs = host2portgroup[viewHost]

            for p in pgs:
                if portgroup in p.key:
                    vlanId = str(p.spec.vlanId)
                    vSwitch = str(p.spec.vswitchName)

        if portGroup is None:
            portGroup = 'N/A'

        print '%s -> %s @ %s -> %s (VLAN %s)' % (dev.deviceInfo.label,
                                                 dev.macAddress,
                                                 vSwitch,
                                                 portGroup,
                                                 vlanId)

def print_dual_homed_vms(service):
    """Lists all virtual machines with multiple
       NICs to different networks"""

    content = service.RetrieveContent()
    hosts, host2portgroup = generate_portgroup_info(content)
    vms = get_vms(content, min_nics=2)
    for vm in vms:
        print_vm_info(vm, hosts, host2portgroup, content)

if __name__ == '__main__':
    if len(sys.argv) < 5:
        print 'Usage: %s host user password port' % sys.argv[0]
        sys.exit(1)

    service = connect.SmartConnect(host=sys.argv[1],
                                   user=sys.argv[2],
                                   pwd=sys.argv[3],
                                   port=int(sys.argv[4]))
    print_dual_homed_vms(service)

首先,这个脚本遍历所有(虚拟机监控器)主机,以收集每个 ESXi 系统上存在的虚拟交换机的信息。然后,它遍历所有虚拟机,收集那些拥有多个网络接口卡的虚拟机。接着,虚拟网络卡的信息与虚拟交换机的信息结合,推导出关于网络连接的信息。

这是我们实验环境中的示例输出,如前所述:

user@lab:~$ python listDualHomed.py 192.168.167.26 readonly 'mypwd' 443
=== EvesMachine ===
Network adapter 1 -> 00:50:56:ab:04:38 @ dvSwitch -> dvInternalNetwork (VLAN 8)
Network adapter 2 -> 00:50:56:ab:23:50 @ dvSwitch -> dvDMZ (VLAN 0)

=== Firewall ===
Network adapter 1 -> 00:50:56:ab:12:e6 @ dvSwitch -> dvInternalNetwork (VLAN 8)
Network adapter 2 -> 00:50:56:ab:4b:62 @ dvSwitch -> dvDMZ (VLAN 0)

我们的脚本正确识别了两个系统,EvesMachineFirewall,它们同时连接到不同的网络。在这个特定的案例中,两个系统都可以用来在同一个虚拟交换机 dvSwitch 上连接 VLAN 0VLAN 8

检测直接硬件访问

听起来可能像是自相矛盾,但大多数虚拟化技术允许直接硬件访问。允许虚拟系统直接访问某些硬件,而不必通过虚拟机监控器提供服务的合法理由如下:

  • 应连接到虚拟机的特殊硬件:特殊硬件,如虚拟时间服务器的无线时钟或作为复制保护机制一部分的加密狗。

  • 虚拟系统临时使用物理介质:有时,这种功能用于从虚拟环境中访问物理系统的介质,例如从物理介质恢复备份到虚拟系统。通常,应该优先使用网络附加存储系统,而不是将物理介质附加到虚拟系统。

  • 虚拟机永久使用虚拟机监控器的驱动器:如果虚拟系统使用的是通过物理介质提供的软件,因此需要访问真实的物理驱动器来安装和更新软件,这种方式可能是有用的。然而,应该考虑使用下载版本或 ISO 镜像,而不是授予直接访问虚拟机监控器硬件的权限。

    正如你可能猜到的,根据这个列表,直接硬件访问在现代虚拟化数据中心中更像是例外而非常规。此外,直接访问虚拟机监控器硬件会破坏虚拟化的一个基本原则。

    注意

    直接硬件访问绕过了虚拟化环境的安全机制,也就是说,所有虚拟硬件都由虚拟机监控器控制。因此,直接硬件访问总是带来虚拟机监控器资源篡改、数据泄漏和系统不稳定的风险。

以下是一些最可能是恶意的直接附加硬件的示例:

  • 网络设备(创建对虚拟机监控器不可见的网络连接)

  • 键盘、鼠标等(创建对虚拟机监控器不可见的控制台访问)

  • 虚拟机监控器磁盘分区

后者特别危险。如果虚拟机设法获得对虚拟机监控器的原始磁盘访问,它可以操控虚拟化环境。后果包括对虚拟化环境的完全控制,并可以访问所有虚拟机、所有虚拟网络,具备创建新恶意资源的能力,并重塑整个网络拓扑。

注意

对于 VMware vSphere,直接硬件访问权限存储在虚拟机的配置中。因此,从未知或不受信任的来源(以 vSphere 的本地格式)导入虚拟机可能会创建恶意硬件访问权限。

以下脚本连接到 VMware vSphere 实例,并列出所有具有直接硬件访问权限的虚拟机:

#!/usr/bin/env python

from pyVim import connect
from pyVmomi import vmodl
from pyVmomi import vim
import re
import sys

def get_vms(content):
    """Returns a list of all virtual machines."""
    vm_view = content.viewManager.CreateContainerView(content.rootFolder,
                                                      [vim.VirtualMachine],
                                                      True)
    vms = [vm for vm in vm_view.view]
    vm_view.Destroy()
    return vms

def print_vm_hardware_access(vm):
    findings = []

    for dev in vm.config.hardware.device:
        if isinstance(dev, vim.vm.device.VirtualUSB):
            findings.append('USB access to host device ' + dev.backing.deviceName)
        elif isinstance(dev, vim.vm.device.VirtualSerialPort):
            findings.append('Serial port access')
        elif isinstance(dev, vim.vm.device.VirtualCdrom):
            if not dev.backing is None:
                if 'vmfs/devices/cdrom' in dev.backing.deviceName:
                    findings.append('Access to CD/DVD drive')
        elif isinstance(dev, vim.vm.device.VirtualDisk):
            if dev.backing is None or \
               dev.backing.fileName is None or \
               re.match(r'.*\.vmdk', dev.backing.fileName) is None:
               findings.append('Suspicious HDD configuration')

    if len(findings) > 0:
        print '=== %s hardware configuration findings ===' % vm.name
        for l in findings:
            print l
        print "\n"

def print_direct_hardware_access(content):
    vms = get_vms(content)
    for vm in vms:
        print_vm_hardware_access(vm)

if __name__ == '__main__':
    if len(sys.argv) < 5:
        print 'Usage: %s host user password port' % sys.argv[0]
        sys.exit(1)

    service = connect.SmartConnect(host=sys.argv[1],
                                   user=sys.argv[2],
                                   pwd=sys.argv[3],
                                   port=int(sys.argv[4]))

    # access the inventory
    content = service.RetrieveContent()
    print_direct_hardware_access(content)

这个脚本非常急功近利,即它不会检查设备是否实际上处于连接状态,或是否可以通过设备访问媒体。然而,类似以下的输出提示需要更深入的检查:

user@lab:~$ python listHardwareAccess.py 192.168.167.26 readonly pwd 443
=== EvesMachine hardware configuration findings ===
Access to CD/DVD drive
Serial port access
USB access to host device path:2/0 version:2

=== DeveloperBox hardware configuration findings ===
Access to CD/DVD drive

=== dmzBox hardware configuration findings ===
Access to CD/DVD drive

EvesMachine 似乎具有对其虚拟机监控器系统上附加的 USB 设备的直接访问权限。此外,似乎还有与虚拟机监控器的串口的直接连接。通常不应允许访问虚拟机监控器的 CD/DVD 驱动器。然而,对于许多安装,人们往往会使用虚拟机监控器的光驱来安装或更新软件。

提示

从 VMX 文件中提取硬件配置

使用如前所述的脚本需要访问虚拟环境。因此,这类脚本的主要目的是缩小取证调查的范围。为了确保证据和记录的永久性,应该从数据存储区复制虚拟机的目录。在那里,VMX 文件包含所有与虚拟机相关的配置设置,包括硬件访问权限。

在本节和上一节中,虚拟化被视为额外的攻击面。在接下来的章节中,我们将概述虚拟化技术如何真正支持取证调查。

将虚拟化作为证据来源

虚拟化不仅在取证调查中是危险且具有挑战性的,它还具有将虚拟化作为收集取证证据的工具的潜力。在接下来的章节中,您将看到可能导致证据的各种来源。

创建内存内容的取证副本

通常,创建系统内存内容的副本需要访问目标系统、登录、安装所需工具,并将内存转储复制到外部媒体。这些步骤都是侵入性的,即改变系统的状态,并可能被攻击者或其恶意软件检测到。此外,具有管理员权限的攻击者可能会通过操控内存分配和保护算法来隐藏部分系统内存,例如,隐藏内存转储中的一部分内容。

为了克服这种方法的缺点,可以利用虚拟机监控器层来获取虚拟系统内存的完整、未篡改的副本。以下脚本可用于创建包含虚拟机 RAM 内容的快照:

#!/usr/bin/env python

from pyVim import connect
from pyVmomi import vim
from datetime import datetime
import sys

def make_snapshot(service, vmname):
    """Creates a snapshot of all virtual machines with the given name"""

    snap_name = 'Memory_Snapshot'
    snap_desc = 'Snapshot for investigation taken at ' + datetime.now().isoformat()

    content = service.RetrieveContent()
    vm_view = content.viewManager.CreateContainerView(content.rootFolder,
                                                      [vim.VirtualMachine],
                                                      True)
    vms = [vm for vm in vm_view.view if vm.name==vmname]
    vm_view.Destroy()

    for vm in vms:
        print 'Taking snapshot from VM UUID=%s' % vm.summary.config.uuid
        vm.CreateSnapshot_Task(name = snap_name,
                               description = snap_desc,
 memory = True,
                               quiesce=False)
        print "Done.\n"

if __name__ == '__main__':
    if len(sys.argv) < 6:
        print 'Usage: %s host user password port vmname' % sys.argv[0]
        sys.exit(1)

    service = connect.SmartConnect(host=sys.argv[1],
                                   user=sys.argv[2],
                                   pwd=sys.argv[3],
                                   port=int(sys.argv[4]))

    make_snapshot(service, sys.argv[5])

此脚本会搜索指定名称的虚拟机并创建快照。突出显示的参数会使 vSphere 将虚拟机的 RAM 内容与其他快照数据文件一起写入数据存储区。

这些 RAM 转储文件位于虚拟机目录中。本章中的枚举脚本显示了该目录的路径。此外,vSphere 客户端允许浏览和下载虚拟机的数据存储区。

RAM 内容存储在扩展名为 .vmem 的文件中,例如 EvesMachine-Snapshot2.vmem

将快照作为磁盘镜像使用

对于物理系统,创建取证磁盘镜像通常需要将系统下线,关机,拆卸硬盘并进行复制。显然,在此过程中系统无法正常运行,因此,业务所有者通常不愿意批准这些停机时间,因为他们对可能发生的安全漏洞存在模糊的怀疑。

另一方面,创建虚拟机快照基本上不会造成任何停机,但结果是一个具有取证严谨性的虚拟资产磁盘镜像。

提示

始终检查系统是否为虚拟机!

由于为虚拟系统创建取证数据比为物理系统创建要容易得多,因此取证调查的第一步应该是检查目标系统是否为虚拟系统。

快照的创建与上一节中的脚本相同。对于 VMware vSphere 5,必须从虚拟机监控器的数据存储目录复制所有文件,以获取硬盘的完整转储。如果虚拟系统仍在运行,则某些文件可能无法复制,因为虚拟机监控器在这些文件正在使用时不允许读取访问。通常,这不是问题,因为这些文件仅供快照使用,也就是说,自快照创建以来的所有更改都存储在特殊的快照文件中。

在 VMware vSphere 6 中,快照机制已发生变化。创建快照后所做的磁盘更改不会写入快照文件,而是直接写入表示虚拟硬盘的文件中。快照文件用于保留磁盘驱动器的原始内容(写时复制行为)。

因此,从 VMware vSphere 6 环境中复制的文件将包含虚拟机目录的所有条目。

对于取证分析,捕获的磁盘镜像可以连接到虚拟取证工作站。在那里,这些镜像可以像任何其他物理硬盘一样进行处理。当然,原始副本必须保持完好,以确保取证的严谨性。

捕获网络流量

虚拟化环境不仅代表虚拟机和网络接口卡NIC),还代表了连接这些系统所需的虚拟网络设备。通过在虚拟交换机上添加一个监控端口并连接一个系统,可以捕获整个虚拟网络的所有网络流量。

注意

如果 VMware vSphere 中的虚拟系统被允许将 NIC 切换为混杂模式,那么这将自动使相应的交换机端口进入监控模式。

此外,VMware vSphere 的企业版提供了一种虚拟交换机的高级版本,称为vSphere 分布式交换机VDS)。该交换机可以更像物理交换机,并提供端口镜像功能,将选定端口的流量镜像到指定端口以进行流量分析。此外,该交换机还能够将 NetFlow 日志提供给指定端口。

对于标准虚拟交换机,需要执行以下步骤以监控网络流量:

  • 在此交换机上创建一个新的端口组进行监控。虽然这并非严格要求,但强烈建议这么做。如果没有一个专用的端口组进行监控,那么交换机上的所有虚拟系统将能够监控交换机的所有流量。

  • 修改此端口组的安全设置,并将混杂模式更改为接受

  • 将虚拟捕获系统的网络卡配置到新的端口组。此系统现在可以捕获该交换机的所有网络流量。

具体步骤可能会根据虚拟交换机的类型和版本有所不同。然而,核心信息是,虚拟化环境可以简化网络流量捕获的任务。此外,物理交换机和虚拟交换机的行为有所不同,例如,它们会对连接的网络卡的配置变化做出反应。

在下一章中,我们将看到如何生成和分析这些捕获的网络流量。

总结

在本章中,我们概述了虚拟化如何改变不仅仅是 IT 运维的格局,也改变了攻击者和取证专家的工作方式。系统可以因善意或恶意的原因被创建、重构和复制。

我们提供了如何检测 vSphere 虚拟化环境中可能存在的恶意行为或配置的示例。此外,我们演示了虚拟化如何有助于从应分析的系统中获取未经篡改的 RAM 转储。在下一章中,您将看到如何分析这些 RAM 转储的示例。

通过这些知识,您现在已经准备好在取证分析中分析和利用虚拟环境。

第六章:使用 Python 进行移动法医分析

尽管标准计算机硬件(如硬盘)的法医分析已发展为一门稳定的学科,并有许多参考资料,例如《文件系统法医分析》一书,作者为Brian Carrier,由Addison-Wesley Professional出版,以及我们之前的章节,关于分析非标准硬件或瞬态证据的技术仍然存在许多争议。尽管智能手机在数字调查中的作用日益增加,但由于其异质性,智能手机仍被视为非标准设备。在所有调查中,遵循基本的法医原则是必要的。法医调查的两大原则如下:

  • 必须非常小心,以确保证据被尽可能少地篡改或改变。

  • 数字调查的过程必须易于理解,并且接受审查。最好是,调查结果必须能够被独立调查员复现。

特别是第一个原则,在智能手机的情况下尤其具有挑战性,因为大多数智能手机采用特定的操作系统和硬件保护方法,这些方法会阻止对系统中数据的无限制访问。

硬盘的数据保存在大多数情况下是一个简单且熟知的程序。调查员将硬盘从计算机或笔记本中取出,借助写保护器(例如,Tableau TK35)将其连接到工作站,并使用知名且经过认证的软件解决方案进行分析。与此相比,智能手机世界显然没有这样的程序。几乎每款智能手机都有其自己构建存储的方式,因此对于每款智能手机,调查员需要有自己的方法来获取存储的转储数据。虽然从智能手机中获取数据非常困难,但与数据的多样性相比,可以获取更多的数据。智能手机存储的数据,除了常见的文件(例如,照片和文档),还包括 GPS 坐标以及智能手机在关闭之前所连接的移动基站的位置。

考虑到由此产生的机会,结果表明,额外的支出对于调查员而言是值得的。

本章将涵盖以下主题:

  • Eoghan Casey提出的调查模型被智能手机采用

  • Android 智能手机的分析(手动分析以及通过Android 数据提取器 LiteADEL)进行自动化分析)

  • iOS 智能手机的分析

智能手机的调查模型

Eoghan Casey调查过程模型,也被称为楼梯模型,提供了一个实用的、系统的逐步指南,以进行有效的数字调查。该模型被描绘为一系列上升的楼梯,从事件警报或指控开始,到证言结束。这些步骤的设计尽可能通用。该模型试图融合警方职责和法医专家的任务。以下要点解释了调查过程模型的每个步骤以及处理智能手机与计算机之间的差异:

  • 事件警报或指控:指控是整个过程的起始信号。在此阶段,来源将被评估并请求详细的询问。

  • 价值评估:在价值评估范围内,公诉方的利益与起诉犯罪行为所需的成本进行比较。对于公司而言,这通常会导致不提起诉讼(至少对于较小的事件)。起诉的优势在于可能的赔偿、提升自身安全性以及一定的威慑效应。起诉的劣势在于需要资源、在调查过程中可能出现的系统停机无法进行生产性使用,以及大多数时候带来的负面公众扩散效应。

  • 事件或犯罪现场协议:在经典的刑事学中,通常要求对犯罪现场进行大范围封闭。Eoghan Casey 表达了以下观点:

    “冻结”证据并为随后的所有活动提供“真实依据”。

    对于不同种类的数字痕迹,必须根据个别情况检查冷冻过程的具体定义。总的来说,必须最大限度地减少改变痕迹的风险。对于智能手机,这意味着它们必须放入一个连接到外部电源的法拉第袋中。

  • 身份识别或扣押:在传统的扣押过程中,所有可能作为证据的物品和对象都会被收取。在这里,重要的是不能对证据进行任何更改。此外,证据的环境可能具有重大意义。扣押时,证据链同时开始。关于扣押的推荐读物是由美国司法部出版的小册子《电子犯罪现场调查:初步响应者指南》。该小册子为非技术人员提供了准确且详细的建议。另一个好的来源是文件《搜查和扣押计算机及获取电子证据的刑事调查》,同样由美国司法部出版。

  • 保留:在确保证据的过程中,必须确保这些证据未被修改。这就是为什么所有证据都会被记录、拍照、封存,然后锁起来。在数字证据的情况下,这意味着首先创建证据的副本;进一步的调查仅在副本上进行。为了证明证据副本的真实性,使用加密哈希函数。通常情况下,这对于手机取证是最困难的部分,因为某些类型的手机无法创建一对一的副本。我们将在接下来的部分中展示如何创建可以在调查中使用的备份。

  • 恢复Eoghan Casey将恢复描述为撒网。特别是这一阶段包括恢复已删除、隐藏、掩盖或以其他方式无法访问的证据。建议利用与其他证据的协同作用。例如,在需要读取加密数据的情况下,合理的做法是测试是否在犯罪现场找到了包含密码的便签。

  • 采集:在证据分析过程中,需要一个结构良好的组织来处理大量的数据。因此,应该首先调查元数据,而不是实际数据。例如,数据可以根据文件类型或访问时间进行分组。这直接引导到下一个阶段——数据缩减

  • 缩减:缩减任务的目的是消除不相关的数据。也可以使用元数据来实现这一目的。例如,数据可以根据数据类型进行缩减。一个合适的场景是,如果指控允许,可以将所有数据缩减为图像数据。该阶段的结果是——根据Eoghan Casey的说法:

    最小的数字信息集合,具有最高的潜力来包含有证明价值的数据。

    这意味着找到最小的、最有可能相关且具有证明价值的数据。在这种情况下,已知文件的哈希数据库,例如国家软件参考库NIST),有助于排除已知文件(我们在第二章中描述了如何使用这个库,法医算法)。

  • 组织与搜索:组织的方面包括结构化数据并使其能够进行扫描。因此,通常会创建索引和概览,或者根据其类型将文件排序到有意义的目录中。这简化了后续步骤中数据的引用。

  • 分析:此阶段包括对文件内容的详细分析。其中特别需要绘制数据与人员之间的联系,以确定负责人员。此外,内容和背景的评估是根据手段、动机和机会进行的。在此步骤中,实验对于确定未记录的行为和开发新方法是有帮助的。所有结果需要经过测试,并且应该能够通过科学方法进行验证。

  • 报告:报告不仅仅是呈现结果,还需要展示如何得出这些结果。为此,所有考虑的规则和标准应当记录下来。此外,所有得出的结论需要有依据,并且需要讨论其他解释模型。

  • 说服与证词:最后,证人出庭作证,提供关于该主题的权威意见。最重要的方面是该权威的可信度。例如,技术排斥的听众或辩护律师提出的困难类比可能会成为问题。

通过查看前面描述的过程,可以发现处理智能手机时,与其他类型证据的处理几乎没有太大变化。然而,对于调查人员来说,了解在哪些步骤上需要特别注意是非常重要的。

Android

我们将通过 Python 帮助检查的第一个移动操作系统是 Android。在第一小节中,我们将展示如何手动检查智能手机,接下来是使用 ADEL 的自动化方法。最后,我们将演示如何将分析结果的数据合并,创建移动轨迹。

手动检查

第一步是获取智能手机的 root 权限。这样做是为了绕过内部系统保护并访问所有数据。获取 root 权限对大多数手机来说是不同的,并且强烈依赖于操作系统版本。最好的方法是创建自己的恢复镜像并通过内置的恢复模式启动手机。

获取 root 权限后,下一步是尝试获取明文屏幕锁,因为这个秘密通常用于不同的保护措施(例如,屏幕锁可以作为手机应用程序的密码)。破解 PIN 码或密码的屏幕锁可以通过以下脚本完成:

import os, sys, subprocess, binascii, struct
import sqlite3 as lite

def get_sha1hash(backup_dir):

    # dumping the password/pin from the device
    print "Dumping PIN/Password hash ..."
    password = subprocess.Popen(['adb', 'pull', '/data/system/password.key', backup_dir], 
        stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
    password.wait()

    # cutting the HASH within password.key
    sha1hash = open(backup_dir + '/password.key', 'r').readline()[:40]
    print "HASH: \033[0;32m" + sha1hash + "\033[m"

    return sha1hash

def get_salt(backup_dir):

    # dumping the system DB containing the SALT
    print "Dumping locksettings.db ..."
    saltdb = subprocess.Popen(['adb', 'pull', '/data/system/locksettings.db', backup_dir], 
        stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
    saltdb.wait()
    saltdb2 = subprocess.Popen(['adb', 'pull', '/data/system/locksettings.db-wal', backup_dir], 
        stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
    saltdb2.wait()
    saltdb3 = subprocess.Popen(['adb', 'pull', '/data/system/locksettings.db-shm', backup_dir], 
        stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
    saltdb3.wait()

    # extract the SALT
    con = lite.connect(backup_dir + '/locksettings.db')
    cur = con.cursor()    
    cur.execute("SELECT value FROM locksettings WHERE name='lockscreen.password_salt'")
    salt = cur.fetchone()[0]
    con.close()

    # convert SALT to Hex
    returnedsalt =  binascii.hexlify(struct.pack('>q', int(salt) ))
    print "SALT: \033[0;32m" + returnedsalt + "\033[m"

    return returnedsalt

def write_crack(salt, sha1hash, backup_dir):

    crack = open(backup_dir + '/crack.hash', 'a+')

    # write HASH and SALT to cracking file
    hash_salt = sha1hash + ':' + salt
    crack.write(hash_salt)
    crack.close()

if __name__ == '__main__':

    # check if device is connected and adb is running as root
    if subprocess.Popen(['adb', 'get-state'], stdout=subprocess.PIPE).communicate(0)[0].split("\n")[0] == "unknown":
        print "no device connected - exiting..."
        sys.exit(2)

    # starting to create the output directory and the crack file used for hashcat
    backup_dir = sys.argv[1]

    try:
        os.stat(backup_dir)
    except:
        os.mkdir(backup_dir)

    sha1hash = get_sha1hash(backup_dir)
    salt = get_salt(backup_dir)
    write_crack(salt, sha1hash, backup_dir)

这个脚本会生成一个名为crack.hash的文件,可以用于向hashcat提供数据以进行屏幕锁的暴力破解。如果智能手机的用户使用了 4 位数的 PIN 码,执行 hashcat 的命令如下:

user@lab:~$ ./hashcat -a 3 -m 110 out/crack.hash -1 ?d ?1?1?1?1
Initializing hashcat v0.50 with 4 threads and 32mb segment-size...

Added hashes from file crack.hash: 1 (1 salts)
Activating quick-digest mode for single-hash with salt

c87226fed37977772be870d722c449f915844922:256c05b54b73308b:0420

All hashes have been recovered

Input.Mode: Mask (?1?1?1?1) [4]
Index.....: 0/1 (segment), 10000 (words), 0 (bytes)
Recovered.: 1/1 hashes, 1/1 salts
Speed/sec.: - plains, 7.71k words
Progress..: 7744/10000 (77.44%)
Running...: 00:00:00:01
Estimated.: --:--:--:--

Started: Sat Jul 20 17:14:52 2015
Stopped: Sat Jul 20 17:14:53 2015

通过查看输出中的标记行,可以看到 sha256 哈希值,后面跟着盐值和用于解锁屏幕的暴力破解的 PIN 码。

如果智能手机用户使用了手势解锁,你可以使用一个预生成的彩虹表和以下脚本:

import hashlib, sqlite3, array, datetime
from binascii import hexlify

SQLITE_DB = "GestureRainbowTable.db"

def crack(backup_dir):

    # dumping the system file containing the hash
    print "Dumping gesture.key ..."

    saltdb = subprocess.Popen(['adb', 'pull', '/data/system/gesture.key', backup_dir], 
        stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)

    gesturehash = open(backup_dir + "/gesture.key", "rb").readline()
    lookuphash = hexlify(gesturehash).decode()
    print "HASH: \033[0;32m" + lookuphash + "\033[m"

    conn = sqlite3.connect(SQLITE_DB)
    cur = conn.cursor()
    cur.execute("SELECT pattern FROM RainbowTable WHERE hash = ?", (lookuphash,))
    gesture = cur.fetchone()[0]

    return gesture

if __name__ == '__main__':

    # check if device is connected and adb is running as root
    if subprocess.Popen(['adb', 'get-state'], stdout=subprocess.PIPE).communicate(0)[0].split("\n")[0] == "unknown":
        print "no device connected - exiting..."
        sys.exit(2)

    # starting to create the output directory and the crack file used for hashcat
    backup_dir = sys.argv[1]

    try:
        os.stat(backup_dir)
    except:
        os.mkdir(backup_dir)

    gesture = crack(backup_dir)

    print "screenlock gesture: \033[0;32m" + gesture + "\033[m""

在寻找潜在感染设备时,另一个可能非常重要的因素是已安装应用的列表及其哈希值,以便将它们与AndroTotalMobile-Sandbox进行对比。可以通过以下脚本完成此操作:

import os, sys, subprocess, hashlib

def get_apps():

    # dumping the list of installed apps from the device
    print "Dumping apps meta data ..."

    meta = subprocess.Popen(['adb', 'shell', 'ls', '-l', '/data/app'], 
        stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
    meta.wait()

    apps = []
    while True:
        line = meta.stdout.readline()
        if line != '':
            name = line.split(' ')[-1].rstrip()
            date = line.split(' ')[-3]
            time = line.split(' ')[-2]
            if name.split('.')[-1] == 'apk':
                app = [name, date, time]
            else:
                continue
        else:
            break
        apps.append(app)

    return apps

def dump_apps(apps, backup_dir):

    # dumping the apps from the device
    print "Dumping the apps ..."

    for app in apps:
        app = app[0]
        subprocess.Popen(['adb', 'pull', '/data/app/' + app, backup_dir], 
            stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)

def get_hashes(apps, backup_dir):

    # calculating the hashes
    print "Calculating the sha256 hashes ..."

    meta = []
    for app in apps:
        sha256 = hashlib.sha256(open(backup_dir + '/' + app[0], 'rb').read()).hexdigest()
        app.append(sha256)
        meta.append(app)

    return meta

if __name__ == '__main__':

    # check if device is connected and adb is running as root
    if subprocess.Popen(['adb', 'get-state'], stdout=subprocess.PIPE).communicate(0)[0].split("\n")[0] == "unknown":
        print "no device connected - exiting..."
        sys.exit(2)

    # starting to create the output directory
    backup_dir = sys.argv[1]

    try:
        os.stat(backup_dir)
    except:
        os.mkdir(backup_dir)

    apps = get_apps()
    dump_apps(apps, backup_dir)
    meta = get_hashes(apps, backup_dir)

    # printing the list of installed apps
    print 'Installed apps:'
    for app in meta:
        print "\033[0;32m" + ' '.join(app) + "\033[m"

执行前述打印脚本后,您将得到以下输出,包括重要的元数据:

user@lab:~$ ./get_installed_apps.py out

Dumping apps meta data ...
Dumping the apps ...
Calculating the sha256 hashes ...

Installed apps:
com.android.SSLTrustKiller-1.apk 2015-05-18 17:11 52b4d6a1888a6514b62f6607cebf8c2c2aa4e4857319ec67b24be601db5243fb
com.android.chrome-2.apk 2015-06-16 20:50 191cd720626df38eaedf3301826e72330493cdeb8c45da4e309939cfe5633d61
com.android.vending-1.apk 2015-07-25 12:05 7be9f8f99e8c1a6c3be1edb01d84aba14619e3c67c14856755523413ba8e2d98
com.google.android.GoogleCamera-2.apk 2015-06-16 20:49 6936f3c17948c767550c206ff0ae0f44f1f4da0fcb85125da722e0c709787894
com.google.android.apps.authenticator2-1.apk 2015-06-05 10:14 11bcfcf1c853b1eb567c9453507c3413b09a1d70fd3085013f4a091719560ab6
...

通过这些信息,您可以将应用程序与在线服务进行对比,以了解它们是否安全可用,或是否潜在恶意。如果您不想提交它们,您可以结合使用apk_analyzer.py脚本和Androguard进行快速分析,这通常能揭示出重要信息。

在获取所有已安装应用的列表并检查它们是否存在恶意行为后,获取设备的所有分区和挂载点信息也非常有用。可以通过以下脚本实现这一点:

import sys, subprocess

def get_partition_info():

    # dumping the list of installed apps from the device
    print "Dumping partition information ..."

    partitions = subprocess.Popen(['adb', 'shell', 'mount'], 
        stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
    partitions.wait()

    while True:
        line = partitions.stdout.readline().rstrip()
        if line != '':
            print "\033[0;32m" + line + "\033[m"
        else:
            break

if __name__ == '__main__':

    # check if device is connected and adb is running as root
    if subprocess.Popen(['adb', 'get-state'], stdout=subprocess.PIPE).communicate(0)[0].split("\n")[0] == "unknown":
        print "no device connected - exiting..."
        sys.exit(2)

    get_partition_info()

根手机的输出可能如下所示:

user@lab:~$ ./get_partitions.py 

Dumping partition information ...
rootfs / rootfs rw,relatime 0 0
tmpfs /dev tmpfs rw,seclabel,nosuid,relatime,mode=755 0 0
devpts /dev/pts devpts rw,seclabel,relatime,mode=600 0 0
proc /proc proc rw,relatime 0 0
sysfs /sys sysfs rw,seclabel,relatime 0 0
selinuxfs /sys/fs/selinux selinuxfs rw,relatime 0 0
debugfs /sys/kernel/debug debugfs rw,relatime 0 0
none /acct cgroup rw,relatime,cpuacct 0 0
none /sys/fs/cgroup tmpfs rw,seclabel,relatime,mode=750,gid=1000 0 0
tmpfs /mnt/asec tmpfs rw,seclabel,relatime,mode=755,gid=1000 0 0
tmpfs /mnt/obb tmpfs rw,seclabel,relatime,mode=755,gid=1000 0 0
none /dev/cpuctl cgroup rw,relatime,cpu 0 0
/dev/block/platform/msm_sdcc.1/by-name/system /system ext4 ro,seclabel,relatime,data=ordered 0 0
/dev/block/platform/msm_sdcc.1/by-name/userdata /data ext4 rw,seclabel,nosuid,nodev,noatime,nomblk_io_submit,noauto_da_alloc,errors=panic,data=ordered 0 0
/dev/block/platform/msm_sdcc.1/by-name/cache /cache ext4 rw,seclabel,nosuid,nodev,noatime,nomblk_io_submit,noauto_da_alloc,errors=panic,data=ordered 0 0
/dev/block/platform/msm_sdcc.1/by-name/persist /persist ext4 rw,seclabel,nosuid,nodev,relatime,nomblk_io_submit,nodelalloc,errors=panic,data=ordered 0 0
/dev/block/platform/msm_sdcc.1/by-name/modem /firmware vfat ro,relatime,uid=1000,gid=1000,fmask=0337,dmask=0227,codepage=cp437,iocharset=iso8859-1,shortname=lower,errors=remount-ro 0 0
/dev/fuse /mnt/shell/emulated fuse rw,nosuid,nodev,relatime,user_id=1023,group_id=1023,default_permissions,allow_other 0 0

在本节的最后,我们将向您展示如何收集更多有关基于安卓智能手机使用情况的详细信息。在以下示例中,我们将使用联系人数据库,该数据库还存储着通话记录。这个示例可以轻松地被调整,以获取日历条目或从任何其他已安装应用的数据库中提取内容:

import os, sys, subprocess
import sqlite3 as lite
from prettytable import from_db_cursor

def dump_database(backup_dir):

    # dumping the password/pin from the device
    print "Dumping contacts database ..."

    contactsDB = subprocess.Popen(['adb', 'pull', '/data/data/com.android.providers.contacts/databases/contacts2.db', 
        backup_dir], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
    contactsDB.wait()

def get_content(backup_dir):

    # getting the content from the contacts database
    con = lite.connect(backup_dir + '/contacts2.db')
    cur = con.cursor()    
    cur.execute("SELECT contacts._id AS _id,contacts.custom_ringtone AS custom_ringtone, name_raw_contact.display_name_source AS display_name_source, name_raw_contact.display_name AS display_name, name_raw_contact.display_name_alt AS display_name_alt, name_raw_contact.phonetic_name AS phonetic_name, name_raw_contact.phonetic_name_style AS phonetic_name_style, name_raw_contact.sort_key AS sort_key, name_raw_contact.phonebook_label AS phonebook_label, name_raw_contact.phonebook_bucket AS phonebook_bucket, name_raw_contact.sort_key_alt AS sort_key_alt, name_raw_contact.phonebook_label_alt AS phonebook_label_alt, name_raw_contact.phonebook_bucket_alt AS phonebook_bucket_alt, has_phone_number, name_raw_contact_id, lookup, photo_id, photo_file_id, CAST(EXISTS (SELECT _id FROM visible_contacts WHERE contacts._id=visible_contacts._id) AS INTEGER) AS in_visible_group, status_update_id, contacts.contact_last_updated_timestamp, contacts.last_time_contacted AS last_time_contacted, contacts.send_to_voicemail AS send_to_voicemail, contacts.starred AS starred, contacts.pinned AS pinned, contacts.times_contacted AS times_contacted, (CASE WHEN photo_file_id IS NULL THEN (CASE WHEN photo_id IS NULL OR photo_id=0 THEN NULL ELSE 'content://com.android.contacts/contacts/'||contacts._id|| '/photo' END) ELSE 'content://com.android.contacts/display_photo/'||photo_file_id END) AS photo_uri, (CASE WHEN photo_id IS NULL OR photo_id=0 THEN NULL ELSE 'content://com.android.contacts/contacts/'||contacts._id|| '/photo' END) AS photo_thumb_uri, 0 AS is_user_profile FROM contacts JOIN raw_contacts AS name_raw_contact ON(name_raw_contact_id=name_raw_contact._id)")
    pt = from_db_cursor(cur)
    con.close()

    print pt    

if __name__ == '__main__':

    # check if device is connected and adb is running as root
    if subprocess.Popen(['adb', 'get-state'], stdout=subprocess.PIPE).communicate(0)[0].split("\n")[0] == "unknown":
        print "no device connected - exiting..."
        sys.exit(2)

    # starting to create the output directory
    backup_dir = sys.argv[1]

    try:
        os.stat(backup_dir)
    except:
        os.mkdir(backup_dir)

    dump_database(backup_dir)
    get_content(backup_dir)

在您了解如何手动分析智能手机后,我们将在接下来的部分向您展示如何借助 ADEL 自动执行相同的操作。

借助 ADEL 进行自动化检查

我们开发了一个名为 ADEL 的工具。最初,这个工具是为 Android 2.x 版本开发的,但随后更新以适应分析 Android 4.x 智能手机的需求。该工具能够自动从 Android 设备中转储所选的 SQLite 数据库文件,并提取存储在转储文件中的内容。作为进一步的选项,ADEL 还能够分析那些事先手动转储的数据库。此选项的实现是为了支持那些由于安全功能(如锁定引导加载程序)使 ADEL 无法访问设备文件系统的智能手机。在接下来的部分,我们将描述 ADEL 的主要任务以及该工具实际执行的步骤。

系统背后的理念

在 ADEL 的开发过程中,我们主要考虑了以下设计指南:

  • 取证原则:ADEL 旨在以取证正确的方式处理数据。这个目标是通过以下方式实现的:操作不是直接在手机上进行,而是在数据库的副本上进行。此过程确保数据不会被 ADEL 的用户或受损操作系统修改。为了提供 ADEL 取证正确性的证明,分析前后会计算哈希值,以确保转储数据在分析过程中未被修改。

  • 可扩展性:ADEL 是模块化构建的,包含两个独立的模块:分析模块和报告模块。这两个模块之间存在预定义的接口,并且都可以通过附加功能轻松修改。模块化结构使得你能够轻松地转储和分析更多智能手机的数据库,同时为未来系统的更新提供便利。

  • 可用性:ADEL 的使用目标是尽可能简单,以便专业人员和非专家都能使用。最好是,手机的分析能够自动进行,以便用户不会收到任何内部过程的通知。此外,报告模块会生成一份详细的报告,报告以可读的形式包含所有解码后的数据。在执行过程中,ADEL 可选地会写入一份详细的日志文件,记录执行的所有重要步骤。

实现和系统工作流程

显示 ADEL 结构的流程图如下所示:

实现和系统工作流程

ADEL 使用 Android 软件开发工具包 (Android SDK) 将数据库文件转储到调查员的计算机中。为了提取 SQLite 数据库文件中的内容,ADEL 会解析底层数据结构。在以只读模式打开要解析的数据库文件后,ADEL 会读取数据库头部(文件的前 100 字节)并提取每个头部字段的值。并非所有字段值都需要,但某些头部字段的值对于解析数据库文件的其余部分是必要的。一个重要的值是数据库文件中页面的大小,这对于解析 B 树结构(逐页面)至关重要。在读取完数据库头部字段后,ADEL 会解析包含 sqlite_master 表的 B 树,其中数据库的第一页面始终是根页面。对于每个数据库表,都会提取 SQL CREATE 语句和 B 树根页面的页面编号。此外,SQL CREATE 语句还会进一步分析,以提取对应表中每列的名称和数据类型。

最后,完整的 B 树结构会为每个表解析,从 sqlite_master 表提取的 B 树根页面开始。通过跟踪所有内部页面的指针,可以识别出 B 树的每个叶子页面。最终,每个表的行内容会从属于该表 B 树的任何叶子页面中的单元格提取出来。

在接下来的章节中,我们将介绍报告模块及其功能。在当前开发阶段,以下数据库将进行取证处理和解析:

  • 电话和 SIM 卡信息(例如 国际移动用户身份 (IMSI) 和序列号)

  • 电话簿和通话记录

  • 日历条目

  • 短信消息

  • Google 地图

通过这种方式检索到的数据会被报告模块写入一个 XML 文件,以便于后续使用和数据呈现。与分析模块类似,它可以轻松更新,以适应未来 Android 版本或底层数据库模式的可能变化。因此,我们创建了不同的元组——例如,[表,行,列]——来定义在两个模块之间交换的数据。如果未来数据库设计发生变化,只需要调整元组即可。报告模块会自动为每种之前列出的数据类型创建 XML 文件。此外,还会生成一份报告,包含从分析的数据库中提取的所有数据。通过 XSL 文件的帮助,报告将被图形化展示。所有由 ADEL 创建的文件都会存储在当前项目的子文件夹中。

为了访问智能手机上的必要数据库和系统文件夹,ADEL 需要设备的 root 权限。

与 ADEL 一起使用

在我们描述了 ADEL 是什么以及它如何工作之后,接下来我们将进入本节的实际部分并开始使用它。你可以从以下网址下载 ADEL:mspreitz.github.io/ADEL

你需要做的就是检查设备是否已经包含在 ADEL 的配置文件 /xml/phone_config.xml 中。如果设备缺失,有两种方式可以继续操作:

  1. 选择一个相同 Android 版本的不同设备(这会生成一个警告,但在大多数情况下是有效的)。

  2. 生成一个新的设备配置,匹配目标设备的设备类型和 Android 版本。

如果选择第二种方式,你可以复制一个已经正常工作的设备的配置,并采用 XML 文件中的数字。这些数字表示已记录数据库中的表和列。更准确地说,如果你尝试采用 SMS 数据库,你必须检查以下表和列的数字:

<sms>
  <db_name>mmssms.db</db_name>
  <table_num>10</table_num>
  <sms_entry_positions> 
    <id>0</id>
    <thread_id>1</thread_id>
    <address>2</address>
    <person>3</person>
    <date>4</date>
    <read>7</read>
    <type>9</type>
    <subject>11</subject>
    <body>12</body>
  </sms_entry_positions>
</sms>

table_num 标签的数字必须设置为与名为 sms 的表对应的数字。以下数字必须与 sms 表中名称相同的列匹配。前面的打印示例适用于 Nexus 5 和 Android 4.4.4。其他数据库也需要进行相同的操作。

在一台已 root 的 Nexus 5 上运行 ADEL,设备安装了 Android 4.4.4 并填充了测试数据——将生成以下输出:

user@lab:~$./adel.py -d nexus5 -l 4

 _____  ________  ___________.____
 /  _  \ \______ \ \_   _____/|    |
 /  /_\  \ |    |  \ |    __)_ |    |
 /    |    \|    `   \|        \|    |___
 \____|__  /_______  /_______  /|_______ \ 
 \/        \/        \/         \/
 Android Data Extractor Lite v3.0

ADEL MAIN:     ----> starting script....
ADEL MAIN:     ----> Trying to connect to smartphone or emulator....
dumpDBs:       ----> opening connection to device: 031c6277f0a6a117
dumpDBs:       ----> evidence directory 2015-07-20__22-53-22__031c6277f0a6a117 created
ADEL MAIN:     ----> log file 2015-07-20__22-53-22__031c6277f0a6a117/log/adel.log created
ADEL MAIN:     ----> log level: 4
dumpDBs:       ----> device is running Android OS 4.4.4
dumpDBs:       ----> dumping all SQLite databases....
dumpDBs:       ----> auto dict doesn't exist!
dumpDBs:       ----> weather database doesn't exist!
dumpDBs:       ----> weather widget doesn't exist!
dumpDBs:       ----> Google-Maps navigation history doesn't exist!
dumpDBs:       ----> Facebook database doesn't exist!
dumpDBs:       ----> Cached geopositions within browser don't exist!
dumpDBs:       ----> dumping pictures (internal_sdcard)....
dumpDBs:       ----> dumping pictures (external_sdcard)....
dumpDBs:       ----> dumping screen captures (internal_sdcard)....
dumpDBs:       ----> dumping screen captures (internal_sdcard)....
dumpDBs:       ----> all SQLite databases dumped
Screenlock:    ----> Screenlock Hash: 6a062b9b3452e366407181a1bf92ea73e9ed4c48
Screenlock:    ----> Screenlock Gesture: [0, 1, 2, 4, 6, 7, 8]
LocationInfo:  ----> Location map 2015-07-20__22-53-22__031c6277f0a6a117/map.html created
analyzeDBs:    ----> starting to parse and analyze the databases....
parseDBs:      ----> starting to parse smartphone info
parseDBs:      ----> starting to parse calendar entries
parseDBs:      ----> starting to parse SMS messages
parseDBs:      ----> starting to parse call logs
parseDBs:      ----> starting to parse address book entries
analyzeDBs:    ----> all databases parsed and analyzed....
createReport:  ----> creating report....
ADEL MAIN:     ----> report 2015-07-20__22-53-22__031c6277f0a6a117/xml/report.xml created
compareHash:   ----> starting to compare calculated hash values
ADEL MAIN:     ----> stopping script....

 (c) m.spreitzenbarth & s.schmitt 2015

在此输出中,你可以看到所有数据存储的文件夹名称,以及生成的报告所在位置。此外,你还可以看到自动提取的屏幕锁手势,并与预先生成的彩虹表进行比较,如下所示:

与 ADEL 一起使用

移动配置文件

除了关于个别通信的数据外,2006 年的欧盟指令还要求网络运营商保留某些位置数据。特别是,该指令要求以下数据至少保留六个月:

  • 用户开始电话通话时所在无线电小区的身份和准确的 GPS 坐标

  • GPRS 数据传输开始时活动的无线电小区的身份和坐标

  • 与这些数据相关的时间戳

这些信息可以帮助调查人员创建嫌疑人的运动档案。此外,这些信息还可以用来定位和监视嫌疑人。

许多欧盟成员国已将此指令纳入国家法律。然而,在一些国家,关于这些法律的公众辩论非常激烈,特别是与隐私威胁相关的讨论。在德国,这些讨论因德国政治家马尔特·斯皮茨提供的数据集而愈加激烈。该数据集包含了六个月的位置信息,这些数据在数据保留法下由他的移动网络运营商保存。一家德国报纸创建了一个图形界面,使用户能够直观地重播斯皮茨的详细行动。

总体来说,有人认为,保留大量数据会带来新的滥用风险。此外,要求存储与数百万无辜人相关的数据,与执法部门仅在少数情况下使用这些数据的情况不成比例。因此,在 2011 年,德国宪法法院驳回了要求数据保留的原始立法。同时,寻找不那么侵入性的技术来分析犯罪分子的活动仍在继续。

近年来,许多新型手机(智能手机)涌入市场。由于它们本质上是小型个人计算机,因此它们提供的功能远远超过了打电话和上网。越来越多的用户使用应用程序(主要是直接安装在手机上的第三方应用程序),并通过社交网络如 Facebook、Google+ 和 Twitter 与朋友和家人进行沟通。

出于性能等原因,移动设备会在本地存储位置数据。2011 年 4 月,报道称安卓和 iOS 系统存储敏感的地理数据。这些数据保存在系统缓存文件中,并定期发送给平台开发者。然而,生成地理数据并不限于操作系统——许多提供基于位置服务的应用程序也会创建和存储此类数据。例如,本福德曾展示过用 iPhone 拍摄的照片包含拍摄位置的 GPS 坐标。这些数据具有敏感性,因为它们可以用于创建运动档案,如下图所示。与网络运营商保留的位置信息不同,存储在智能手机上的位置数据可以通过公开扣押供执法机构访问。

运动档案

Apple iOS

在我们了解了如何检查 Android 系统的智能手机后,接下来我们将展示如何在基于 iOS 的设备上执行类似的调查。在第一部分,我们使用 Secure ShellSSH)连接到设备,并向您展示如何从越狱的 iOS 设备的钥匙串中获取存储的数据。

在本节的第二部分,我们将使用 libimobiledevice。这个库是一个跨平台的库,使用协议支持 iOS 设备,并允许您轻松访问设备的文件系统,检索设备及其内部信息,备份/恢复设备,管理已安装的应用程序,检索个人信息管理(PIM)数据以及书签等等。最重要的一点是,iOS 设备不需要越狱即可使用 libimobiledevice。

从越狱的 iDevice 获取钥匙串

在许多情况下,获取用户在 iDevice 上使用的帐户的用户名和密码会非常有帮助。这类数据位于 iOS 钥匙串中,并可以通过以下脚本从 iDevice 中提取:

import os, sys, subprocess

def get_kc(ip, backup_dir):

    # dumping the keychain
    print "Dumping the keychain ..."

    kc = subprocess.Popen(['scp', 'root@' + ip + ':/private/var/Keychains/keychain-2.db', backup_dir], 
        stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
    kc.communicate()

def push_kcd(ip):

    # dumping the keychain
    print "Pushing the Keychain Dumper to the device ..."

    kcd = subprocess.Popen(['scp', 'keychain_dumper' 'root@' + ip + ':~/'], 
        stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
    kcd.communicate()

def exec_kcd(ip, backup_dir):
    # pretty print keychain
    kcc = subprocess.Popen(['ssh', 'root@' + ip, './keychain_dumper'], 
        stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
    kcc.communicate()
    kcc.stdout

if __name__ == '__main__':

    # starting to create the output directory
    backup_dir = sys.argv[1]

    try:
        os.stat(backup_dir)
    except:
        os.mkdir(backup_dir)

    # get the IP of the iDevice from user input
    ip = sys.argv[2]

    get_kc(ip, backup_dir)
    push_kcd(ip)
    exec_kcd(ip, backup_dir)

在前面脚本的输出中,您还可以找到设备注册的 Apple 帐户的密码:

Generic Password
----------------
Service: com.apple.account.AppleAccount.password
Account: 437C2D8F-****-****-****-************
Entitlement Group: apple
Label: (null)
Generic Field: (null)
Keychain Data: *************************

使用 libimobiledevice 进行手动检查

这个库使用常见的 iOS 协议来进行调查者的机器与连接的 iDevice 之间的通信。为了正确工作,设备必须解锁并配对,因为否则设备上的大量数据仍然是加密的,从而受到保护。

通过以下脚本,您可以创建设备的完整备份(类似于 iTunes 备份)。之后,脚本将解包备份并打印备份中所有文件和文件夹的层级列表。根据 iDevice 的大小,脚本可能会运行几分钟。

import os, sys, subprocess

def get_device_info():

    # getting the udid of the connected device
    udid = subprocess.Popen(['idevice_id', '-l'], stdout=subprocess.PIPE).stdout.readline().rstrip()

    print "connected device: \033[0;32m" + udid + "\033[m"
    return udid

def create_backup(backup_dir):

    # creating a backup of the connected device
    print "creating backup (this can take some time) ..."

    backup = subprocess.Popen(['idevicebackup2', 'backup', backup_dir], stdout=subprocess.PIPE)
    backup.communicate()

    print "backup successfully created in ./" + backup_dir + "/"

def unback_backup(udid, backup_dir):

    # unpacking the backup
    print "unpacking the backup ..."

    backup = subprocess.Popen(['idevicebackup2', '-u', udid, 'unback', backup_dir], stdout=subprocess.PIPE)
    backup.communicate()

    print "backup successfully unpacked and ready for analysis"

def get_content(backup_dir):

    # printing content of the created backup
    content = subprocess.Popen(['tree', backup_dir + '/_unback_/'], stdout=subprocess.PIPE).stdout.read()
    f = open(backup_dir + '/filelist.txt', 'a+')
    f.write(content)
    f.close

    print "list of all files and folders of the backup are stored in ./" + backup_dir + "/filelist.txt"

if __name__ == '__main__':

    # check if device is connected
    if subprocess.Popen(['idevice_id', '-l'], stdout=subprocess.PIPE).communicate(0)[0].split("\n")[0] == "":
        print "no device connected - exiting..."
        sys.exit(2)

    # starting to create the output directory
    backup_dir = sys.argv[1]

    try:
        os.stat(backup_dir)
    except:
        os.mkdir(backup_dir)

    udid = get_device_info()
    create_backup(backup_dir)
    unback_backup(udid, backup_dir)
    get_content(backup_dir)

该脚本的最终输出将类似于以下内容:

user@lab:~$ ./create_ios_backup.py out

connected device: 460683e351a265a7b9ea184b2802cf4fcd02526d
creating backup (this can take some time) ...
backup successfully created in ./out
unpacking the backup ...
backup successfully unpacked and ready for analysis
list of all files and folders of the backup are stored in ./out/filelist.txt

借助文件和文件夹的列表,您可以使用常见的工具,如 plist 文件查看器或 SQLite 浏览器,开始分析备份。搜索该生成文件中的 Cydia App Store 也有助于识别智能手机是否已被用户或攻击者越狱。

总结

在本章中,我们介绍了 Eoghan Casey 的调查过程模型,并将其应用到智能手机的案例中。随后,我们通过 Python 脚本和 ADEL 框架以手动和自动化的方式对 Android 智能手机进行了分析。在最后一节中,我们介绍了基于 iOS 的智能手机的分析。

在处理智能手机的取证调查后,我们完成了物理和虚拟获取及分析,并将在下一章中将调查转移到设备的易失性区域。

第七章. 使用 Python 进行内存取证

现在你已经在基础设施中进行了调查(参见第四章,使用 Python 进行网络取证),常见的 IT 设备(参见第三章,使用 Python 进行 Windows 和 Linux 取证),甚至在虚拟化环境(参见第五章,使用 Python 进行虚拟化取证)和移动世界(参见第六章,使用 Python 进行移动取证)中进行了调查,在本章中,我们将向你展示如何使用基于 Python 的取证框架 Volatility,在以下平台上对易失性内存进行调查:

  • Android

  • Linux

在向你展示了一些适用于 Android 和 Linux 的基本 Volatility 插件,并说明如何获取所需的 RAM 转储进行分析之后,我们将开始在 RAM 中寻找恶意软件。因此,我们将使用基于模式匹配的 YARA 规则,并将其与 Volatility 的强大功能结合起来。

理解 Volatility 基础

一般来说,内存取证遵循与其他取证调查相同的模式:

  1. 选择调查目标。

  2. 获取取证数据。

  3. 取证分析。

在前面的章节中,我们已经介绍了多种选择调查目标的技术,例如,从虚拟化层中具有异常设置的系统开始。

内存分析的取证数据获取高度依赖于环境,我们将在本章的在 Linux 上使用 Volatility在 Android 上使用 Volatility部分进行讨论。

提示

始终将虚拟化层视为数据源

从正在运行的操作系统中获取内存始终需要对该系统的管理员权限,并且这是一个侵入性的过程,也就是说,数据获取过程会改变内存数据。此外,先进的恶意软件能够操控操作系统的内存管理,以防止其被获取。因此,始终按照第五章,使用 Python 进行虚拟化取证中所描述的方法,检查并尽量在虚拟机监控程序层面获取内存。

到目前为止,用于内存数据分析的最重要工具是Volatility。Volatility 可在Volatility Foundation网站上获取。

该工具用 Python 编写,可以在 GNU 通用公共许可证GPL)第 2 版的条款下免费使用。Volatility 能够读取多种文件格式的内存转储,例如,休眠文件、原始内存转储、VMware 内存快照文件,以及将会在本章后面讨论的由 LiME 模块生成的 Linux 内存提取器LiME)格式。

Volatility 世界中最重要的术语如下:

  • 配置文件:配置文件帮助 Volatility 解释内存偏移量和内存结构。配置文件取决于操作系统,尤其是操作系统内核、机器和 CPU 架构。Volatility 包含许多适用于最常见用例的配置文件。在本章的 在 Linux 上使用 Volatility 部分中,我们将介绍如何创建您的配置文件。

  • 插件:插件用于对内存转储执行操作。您使用的每个 Volatility 命令都会调用一个插件来执行相应的操作。例如,要获取在 Linux 系统内存转储期间运行的所有进程的列表,可以使用 linux_pslist 插件。

Volatility 提供了全面的文档,我们建议您熟悉所有模块描述,以便充分利用 Volatility。

在 Android 上使用 Volatility

要分析 Android 设备的易失性内存,首先需要 LiME。LiME 是一个可加载内核模块LKM),它可以访问设备的整个 RAM,并将其转储到物理 SD 卡或网络中。在使用 LiME 获取易失性内存转储后,我们将向您展示如何安装和配置 Volatility 以解析 RAM 转储。在最后一节中,我们将演示如何从 RAM 转储中提取特定信息。

LiME 和恢复映像

LiME 是一个可加载内核模块(LKM),它允许从 Linux 和基于 Linux 的设备(如 Android)获取易失性内存。这使得 LiME 非常独特,因为它是第一个可以在 Android 设备上进行完整内存捕获的工具。它还最小化了在获取过程中用户空间和内核空间进程之间的交互,从而使其生成的内存捕获比其他为 Linux 内存获取设计的工具更加法医可靠。

为了在 Android 上使用 LiME,必须为设备上使用的内核进行交叉编译。在接下来的章节中,我们将展示如何在 Nexus 4 上为 Android 4.4.4 执行这些步骤(不过,这种方法可以适配到任何 Android 设备,只要该设备的内核——或者至少是内核配置——作为开源提供)。

首先,我们需要在实验室系统上安装一些额外的软件包,具体如下:

user@lab:~$ sudo apt-get install bison g++-multilib git gperf libxml2-utils make python-networkx zlib1g-dev:i386 zip openjdk-7-jdk

安装完所有必要的软件包后,我们现在需要配置对 USB 设备的访问。在 GNU/Linux 系统下,普通用户默认无法直接访问 USB 设备。系统需要配置以允许这种访问。通过以 root 用户身份创建名为 /etc/udev/rules.d/51-android.rules 的文件,并在其中插入以下内容来实现这一点:

# adb protocol on passion (Nexus One)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e12", MODE="0600", OWNER="user"
# fastboot protocol on passion (Nexus One)
SUBSYSTEM=="usb", ATTR{idVendor}=="0bb4", ATTR{idProduct}=="0fff", MODE="0600", OWNER="user"
# adb protocol on crespo/crespo4g (Nexus S)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e22", MODE="0600", OWNER="user"
# fastboot protocol on crespo/crespo4g (Nexus S)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e20", MODE="0600", OWNER="user"
# adb protocol on stingray/wingray (Xoom)
SUBSYSTEM=="usb", ATTR{idVendor}=="22b8", ATTR{idProduct}=="70a9", MODE="0600", OWNER="user"
# fastboot protocol on stingray/wingray (Xoom)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="708c", MODE="0600", OWNER="user"
# adb protocol on maguro/toro (Galaxy Nexus)
SUBSYSTEM=="usb", ATTR{idVendor}=="04e8", ATTR{idProduct}=="6860", MODE="0600", OWNER="user"
# fastboot protocol on maguro/toro (Galaxy Nexus)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e30", MODE="0600", OWNER="user"
# adb protocol on panda (PandaBoard)
SUBSYSTEM=="usb", ATTR{idVendor}=="0451", ATTR{idProduct}=="d101", MODE="0600", OWNER="user"
# adb protocol on panda (PandaBoard ES)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="d002", MODE="0600", OWNER="user"
# fastboot protocol on panda (PandaBoard)
SUBSYSTEM=="usb", ATTR{idVendor}=="0451", ATTR{idProduct}=="d022", MODE="0600", OWNER="user"
# usbboot protocol on panda (PandaBoard)
SUBSYSTEM=="usb", ATTR{idVendor}=="0451", ATTR{idProduct}=="d00f", MODE="0600", OWNER="user"
# usbboot protocol on panda (PandaBoard ES)
SUBSYSTEM=="usb", ATTR{idVendor}=="0451", ATTR{idProduct}=="d010", MODE="0600", OWNER="user"
# adb protocol on grouper/tilapia (Nexus 7)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e42", MODE="0600", OWNER="user"
# fastboot protocol on grouper/tilapia (Nexus 7)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e40", MODE="0600", OWNER="user"
# adb protocol on manta (Nexus 10)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4ee2", MODE="0600", OWNER="user"
# fastboot protocol on manta (Nexus 10)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4ee0", MODE="0600", OWNER="user"

现在最耗时的部分来了——检查正在使用的 Android 版本的源代码。根据硬盘和互联网连接的速度,这一步可能需要几个小时,因此请提前规划。此外,请记住源代码文件非常大,所以请使用至少 40 GB 空闲空间的第二个分区。我们按如下方式安装 Android 4.4.4 的源代码:

user@lab:~$ mkdir ~/bin

user@lab:~$ PATH=~/bin:$PATH

user@lab:~$ curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo

user@lab:~$ chmod a+x ~/bin/repo

user@lab:~$ repo init -u https://android.googlesource.com/platform/manifest -b android-4.4.4_r1

user@lab:~$ repo sync

在我们安装了 Android 4.4.4 的源代码之后,我们现在需要设备上运行的内核源代码。对于我们在此使用的 Nexus 4,正确的内核是 mako 内核。可以在 source.android.com/source/building-kernels.html 找到所有可用的 Google 手机内核的列表。

user@lab:~$ git clone https://android.googlesource.com/device/lge/mako-kernel/kernel

user@lab:~$ git clone https://android.googlesource.com/kernel/msm.git

现在我们已经获得了交叉编译 LiME 所需的所有源代码,接下来是获取 LiME 本身:

user@lab:~$ git clone https://github.com/504ensicsLabs/LiME.git

在将 git 仓库克隆到实验机器上之后,我们需要设置一些在构建过程中需要的环境变量:

user@lab:~$ export SDK_PATH=/path/to/android-sdk-linux/

user@lab:~$ export NDK_PATH=/path/to/android-ndk/

user@lab:~$ export KSRC_PATH=/path/to/kernel-source/

user@lab:~$ export CC_PATH=$NDK_PATH/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86/bin/

user@lab:~$ export LIME_SRC=/path/to/lime/src

接下来,我们需要获取目标设备的当前内核配置,并将其复制到 LiME 源代码的正确位置。在我们的 Nexus 4 上,可以通过输入以下命令来完成:

user@lab:~$ adb pull /proc/config.gz

user@lab:~$ gunzip ./config.gz

user@lab:~$ cp config $KSRC_PATH/.config

user@lab:~$ cd $KSRC_PATH 

user@lab:~$ make ARCH=arm CROSS_COMPILE=$CC_PATH/arm-eabi-modules_prepare

在我们构建 LiME 内核模块之前,我们需要编写我们定制的 Makefile:

obj-m := lime.o
lime-objs := main.o tcp.o disk.o
KDIR := /path/to/kernel-source
PWD := $(shell pwd)
CCPATH := /path/to/android-ndk/toolchains/arm-linux-androideabi-4.4.4/prebuilt/linux-x86/bin/
default:
 $(MAKE) ARCH=arm CROSS_COMPILE=$(CCPATH)/arm-eabi- -C $(KDIR) M=$(PWD) modules

借助这个 Makefile,我们可以构建从 Android 设备中提取易失性内存所需的内核模块。输入 make 可以启动该过程。

在接下来的示例中,我们将演示如何将我们新生成的内核模块推送到目标设备,并通过 TCP 将整个易失性内存转储到我们的实验环境中。

如果你的设备的内核不允许动态加载模块,你应该考虑创建自己的恢复镜像(例如,定制版的 TWRPCWM),将 LiME 内核模块包含其中,并将其刷入相关设备。如果在刷机操作过程中足够快速,几乎不会丢失数据(更多信息,请参考 www1.informatik.uni-erlangen.de/frost)。

LiME 模块提供了三种不同的镜像格式,可用于将捕获的内存镜像保存到磁盘上:raw、padded 和 lime。第三种格式——lime——在本文中将详细讨论,因为它是我们首选的格式。lime 格式专门开发用于与 Volatility 配合使用,旨在使得使用 Volatility 进行分析变得更加简便,且为处理该格式,增加了特定的地址空间。基于 lime 格式的每个内存转储都有一个固定大小的头部,包含每个内存范围的特定地址空间信息。这消除了仅为了填充未映射或内存映射 I/O 区域而需要额外填充的需求。LiME 头部规范如下所示:

typedef struct {
  unsigned int magic;         // Always 0x4C694D45 (LiME)
  unsigned int version;         // Header version number
  unsigned long long s_addr;     // Starting address of physical RAM
  unsigned long long e_addr;     // Ending address of physical RAM
  unsigned char reserved[8];     // Currently all zeros
  } __attribute__ ((__packed__)) lime_mem_range_header;

要从相关 Android 设备获取这样的转储,首先通过adb连接到 Android 设备,然后输入以下命令:

user@lab:~$ adb push lime.ko /sdcard/lime.ko
user@lab:~$ adb forward tcp:4444 tcp:4444
user@lab:~$ adb shell
nexus4:~$ su
nexus4:~$ insmod /sdcard/lime.ko "path=tcp:4444 format=lime"

在实验室机器上,输入以下命令,以接受通过 TCP 端口 4444 从 Android 设备发送到本地实验室机器的数据:

user@lab:~$ nc localhost 4444 > nexus4_ram.lime

如果前述命令执行成功,您将得到一个 RAM 转储文件,可以借助 Volatility 或其他工具进行进一步分析(请参见下一节)。

Android 的 Volatility

在通过我们在上一节中创建的工具获取表示目标系统物理内存的转储文件后,我们打算从中提取数据工件。如果不对 Android 的内存结构进行深入分析,我们只能提取已知的文件格式,如 JPEG,或仅提取包含 EXIF 数据的 JPEG 头部(使用工具如PhotoRec),或者提取存储为连续格式的简单 ASCII 字符串(使用常见的 Linux 工具如strings),这些字符串可以用来对相关设备的密码进行暴力破解。这种方法非常有限,因为它适用于任何磁盘或内存转储,但并不专注于操作系统和应用程序特定的结构。由于我们打算从 Android 系统中提取完整的数据对象,因此我们将使用流行的易失性内存取证框架:Volatility

在本节中,我们将使用一个支持 ARM 架构的 Volatility 版本(至少需要版本 2.3)。给定一个内存镜像,Volatility 可以提取正在运行的进程、打开的网络套接字、每个进程的内存映射以及内核模块。

注意

在分析内存镜像之前,必须创建一个 Volatility 配置文件,并将其作为命令行参数传递给 Volatility 框架。这样的 Volatility 配置文件是一组vtype定义和可选的符号地址,Volatility 用它们来定位敏感信息并解析。

基本上,配置文件是一个压缩档案,其中包含以下两个文件:

  • System.map文件包含 Linux 内核中静态数据结构的符号名称和地址。对于 Android 设备,该文件可以在内核编译后,在内核源码树中找到。

  • module.dwarf文件是在编译模块并针对目标内核提取 DWARF 调试信息时生成的。

为了创建module.dwarf文件,需要使用名为dwarfdump的工具。Volatility 源代码树中包含tools/linux目录。如果在该目录下运行make命令,该命令会编译模块并生成所需的 DWARF 文件。创建实际的配置文件只需运行以下命令:

user@lab $ zip Nexus4.zip module.dwarf System.map

生成的 ZIP 文件需要复制到volatility/plugins/overlays/linux目录下。成功复制文件后,配置文件将在 Volatility 帮助输出的配置文件部分显示。

尽管 Volatility 对 Android 的支持相对较新,但已有大量的 Linux 插件在 Android 上也能完美运行。例如:

  • linux_pslist:它枚举系统中所有正在运行的进程,类似于 Linux 的 ps 命令

  • linux_ifconfig:该插件模拟 Linux 的ifconfig命令

  • linux_route_cache:它读取并打印路由缓存,存储最近使用的路由条目在哈希表中的信息

  • linux_proc_maps:该插件获取每个独立进程的内存映射

如果你对如何编写自定义 Volatility 插件并解析达尔文虚拟机DVM)中的未知结构感兴趣,请查看我和我的同事所写的以下论文:冷启动 Android 设备的事后内存分析(参考www1.informatik.uni-erlangen.de/filepool/publications/android.ram.analysis.pdf)。

在下一部分,我们将示范如何借助 LiME 和 Volatility 重建特定的应用数据。

为 Android 重建数据

现在,我们将展示如何在 Volatility 的帮助下以及通过自定义插件重建应用程序数据。因此,我们选择了通话历史和键盘缓存。如果你正在调查一个普通的 Linux 或 Windows 系统,已经有大量的插件可以使用,正如你将在本章的最后部分看到的那样。不幸的是,在 Android 上,你必须编写自己的插件。

通话历史

我们的目标之一是从 Android 内存转储中恢复最近的来电和去电电话列表。此列表在打开电话应用时加载。负责电话应用和通话历史记录的进程是com.android.contacts。该进程加载PhoneClassDetails.java类文件,该文件建模了所有电话通话的数据,保存在历史结构中。每个历史记录条目对应一个类实例。每个实例的数据字段是电话的典型元信息,如下所示:

  • 类型(来电、去电或未接)

  • 时长

  • 日期和时间

  • 电话号码

  • 联系人姓名

  • 联系人指定的照片

为了自动提取并显示这些元数据,我们提供了一个 Volatility 插件,名为dalvik_app_calllog,如下所示:

class dalvik_app_calllog(linux_common.AbstractLinuxCommand):

     def __init__(self, config, *args, **kwargs):
          linux_common.AbstractLinuxCommand.__init__(self, config, *args, **kwargs)
          dalvik.register_option_PID(self._config)
          dalvik.register_option_GDVM_OFFSET(self._config)
          self._config.add_option('CLASS_OFFSET', short_option = 'c', default = None,
          help = 'This is the offset (in hex) of system class PhoneCallDetails.java', action = 'store', type = 'str')

     def calculate(self):
          # if no gDvm object offset was specified, use this one
          if not self._config.GDVM_OFFSET:
               self._config.GDVM_OFFSET = str(hex(0x41b0))

          # use linux_pslist plugin to find process address space and ID if not specified
          proc_as = None
          tasks = linux_pslist.linux_pslist(self._config).calculate()
          for task in tasks:
               if str(task.comm) == "ndroid.contacts":
                    proc_as = task.get_process_address_space()
                    if not self._config.PID:
                         self._config.PID = str(task.pid)
                    break

          # use dalvik_loaded_classes plugin to find class offset if not specified
          if not self._config.CLASS_OFFSET:
              classes = dalvik_loaded_classes.dalvik_loaded_classes(self._config).calculate()
              for task, clazz in classes:
                   if (dalvik.getString(clazz.sourceFile)+"" == "PhoneCallDetails.java"):
                        self._config.CLASS_OFFSET = str(hex(clazz.obj_offset))
                        break

          # use dalvik_find_class_instance plugin to find a list of possible class instances
          instances = dalvik_find_class_instance.dalvik_find_class_instance(self._config).calculate()
          for sysClass, inst in instances:
               callDetailsObj = obj.Object('PhoneCallDetails', offset = inst, vm = proc_as)
               # access type ID field for sanity check
               typeID = int(callDetailsObj.callTypes.contents0)
               # valid type ID must be 1,2 or 3
               if (typeID == 1 or typeID == 2 or typeID == 3):
                    yield callDetailsObj

     def render_text(self, outfd, data):
          self.table_header(outfd, [    ("InstanceClass", "13"),
                                        ("Date", "19"),
                                        ("Contact", "20"),
                                        ("Number", "15"),
                                        ("Duration", "13"),
                                        ("Iso", "3"),
                                        ("Geocode", "15"),
                                        ("Type", "8")                                      
                                        ])
          for callDetailsObj in data:
               # convert epoch time to human readable date and time
               rawDate = callDetailsObj.date / 1000
               date =    str(time.gmtime(rawDate).tm_mday) + "." + \
                         str(time.gmtime(rawDate).tm_mon) + "." + \
                         str(time.gmtime(rawDate).tm_year) + " " + \
                         str(time.gmtime(rawDate).tm_hour) + ":" + \
                         str(time.gmtime(rawDate).tm_min) + ":" + \
                         str(time.gmtime(rawDate).tm_sec)

               # convert duration from seconds to hh:mm:ss format
               duration =     str(callDetailsObj.duration / 3600) + "h " + \
                              str((callDetailsObj.duration % 3600) / 60) + "min " + \
                              str(callDetailsObj.duration % 60) + "s"

               # replace call type ID by string
               callType = int(callDetailsObj.callTypes.contents0)
               if callType == 1:
                    callType = "incoming"
               elif callType == 2:
                    callType = "outgoing"
               elif callType == 3:
                    callType = "missed"
               else:
                    callType = "unknown"

               self.table_row(     outfd,
                                   hex(callDetailsObj.obj_offset),
                                   date,
                                   dalvik.parseJavaLangString(callDetailsObj.name.dereference_as('StringObject')),
                                   dalvik.parseJavaLangString(callDetailsObj.formattedNumber.dereference_as('StringObject')),
                                   duration,               
                                   dalvik.parseJavaLangString(callDetailsObj.countryIso.dereference_as('StringObject')),
                                   dalvik.parseJavaLangString(callDetailsObj.geoCode.dereference_as('StringObject')),
                                   callType)

该插件接受以下命令行参数:

  • -o:用于指向 gDvm 对象的偏移量

  • -p:用于进程 ID(PID)

  • -c:用于指向 PhoneClassDetails 类的偏移量

如果知道并传递这些参数给插件,插件的运行时间将显著减少。否则,插件必须在 RAM 中自行查找这些值。

键盘缓存

现在,我们想查看默认键盘应用程序的缓存。假设在解锁屏幕后没有其他输入,并且智能手机受 PIN 保护,则该 PIN 等于最后的用户输入,可以在 Android 内存转储中找到该输入作为 UTF-16 Unicode 字符串。最后的用户输入的 Unicode 字符串是由com.android.inputmethod.latin进程中的RichInputConnection类创建的,并存储在名为mCommittedTextBeforeComposingText的变量中。这个变量就像一个键盘缓冲区,存储了屏幕键盘最后输入并确认的按键。为了恢复最后的用户输入,我们提供了一个 Volatility 插件,名为dalvik_app_lastInput,如下所示:

class dalvik_app_lastInput(linux_common.AbstractLinuxCommand):

     def __init__(self, config, *args, **kwargs):
          linux_common.AbstractLinuxCommand.__init__(self, config, *args, **kwargs)
          dalvik.register_option_PID(self._config)
          dalvik.register_option_GDVM_OFFSET(self._config)
          self._config.add_option('CLASS_OFFSET', short_option = 'c', default = None,
          help = 'This is the offset (in hex) of system class RichInputConnection.java', action = 'store', type = 'str')

     def calculate(self):

          # if no gDvm object offset was specified, use this one
          if not self._config.GDVM_OFFSET:
               self._config.GDVM_OFFSET = str(0x41b0)

          # use linux_pslist plugin to find process address space and ID if not specified
          proc_as = None     
          tasks = linux_pslist.linux_pslist(self._config).calculate()
          for task in tasks:
               if str(task.comm) == "putmethod.latin":                    
                    proc_as = task.get_process_address_space()
                    self._config.PID = str(task.pid)
                    break

          # use dalvik_loaded_classes plugin to find class offset if not specified
          if not self._config.CLASS_OFFSET:
              classes = dalvik_loaded_classes.dalvik_loaded_classes(self._config).calculate()
              for task, clazz in classes:
                   if (dalvik.getString(clazz.sourceFile)+"" == "RichInputConnection.java"):
                        self._config.CLASS_OFFSET = str(hex(clazz.obj_offset))
                        break

          # use dalvik_find_class_instance plugin to find a list of possible class instances
          instance = dalvik_find_class_instance.dalvik_find_class_instance(self._config).calculate()
          for sysClass, inst in instance:
               # get stringBuilder object
               stringBuilder = inst.clazz.getJValuebyName(inst, "mCommittedTextBeforeComposingText").Object.dereference_as('Object')
               # get superclass object
               abstractStringBuilder = stringBuilder.clazz.super.dereference_as('ClassObject')

               # array object of super class
               charArray = abstractStringBuilder.getJValuebyName(stringBuilder, "value").Object.dereference_as('ArrayObject')
               # get length of array object
               count = charArray.length
               # create string object with content of the array object
               text = obj.Object('String', offset = charArray.contents0.obj_offset,
               vm = abstractStringBuilder.obj_vm, length = count*2, encoding = "utf16")
               yield inst, text

     def render_text(self, outfd, data):
          self.table_header(outfd, [    ("InstanceClass", "13"),
                                        ("lastInput", "20")                                 
                                        ])
          for inst, text in data:

               self.table_row(     outfd,
                                   hex(inst.obj_offset),
                                   text)

实际上,这个插件不仅恢复 PIN 码,还恢复最后一次给出的任意用户输入;在许多情况下,这可能是数字证据中的一个有趣的证据。与前面的插件类似,它接受相同的三个命令行参数:gDvm offsetPIDclass file offset。如果这些参数中的任何一个或全部没有提供,插件也可以自动确定缺失的值。

在 Linux 上使用 Volatility

在接下来的部分,我们将介绍内存获取技术和使用 Volatility 进行 Linux 内存取证的示例用例。

内存获取

如果系统未虚拟化,因此无法从虚拟化层直接获取内存;即使在 Linux 系统中,我们首选的工具仍然是 LiME。

然而,与 Android 不同的是,工具的安装和操作要简单得多,因为我们直接在 Linux 系统上生成并运行 LiME;但正如你将在接下来的段落中注意到的,许多步骤是非常相似的。

首先,确定正在分析的系统上运行的确切内核版本。如果没有足够的文档支持,可以运行以下命令来获取内核版本:

user@forensic-target $ uname –a
Linux forensic-target 3.2.0-88-generic #126-Ubuntu SMP Mon Jul 6 21:33:03 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux

提示

在企业环境中使用配置管理

企业环境中通常运行配置管理系统,能够显示目标系统的内核版本和 Linux 发行版。要求客户提供这些数据,甚至是提供一台具有相同内核版本和软件环境的系统,能够帮助你减少 LiME 模块与取证目标之间的兼容性风险。

在实验环境中,准备 LiME 内核模块进行内存采集。要编译该模块,请确保你拥有正确版本的目标内核源代码,然后在 LiME 的src目录中执行以下构建命令:

user@lab src $ make -C /usr/src/linux-headers-3.2.0-88-generic M=$PWD

这应在当前目录中创建lime.ko模块。

在目标系统上,可以使用这个内核模块将内存转储到磁盘,如下所示:

user@forensic-target $ sudo insmod lime.ko path=/path/to/dump.lime format=lime

注意

我们建议选择网络路径来写入镜像。这样,做出的本地系统更改会很少。也可以通过网络传输镜像。只需按照在 Android 上使用 Volatility部分中的描述操作。

Linux 上的 Volatility

Volatility 提供了各种配置文件。这些配置文件由 Volatility 用于解释内存转储。不幸的是,由于 Linux 内核、系统架构和内核配置的种类繁多,无法为所有版本的 Linux 内核提供配置文件。

提示

列出所有 Volatility 配置文件

可以通过vol.py --info命令获取所有可用配置文件的列表。

因此,可能需要创建你自己的配置文件,以便与取证目标完美匹配。Volatility 框架通过提供一个虚拟模块来支持这一步骤,该模块必须针对目标系统的内核头文件进行编译。这个模块可以在 Volatility 分发版中的tools/linux子目录找到。将其编译——类似于 LiME——但启用调试设置:

user@lab src $ make -C /usr/src/linux-headers-3.2.0-88-generic CONFIG_DEBUG_INFO=y M=$PWD

这将创建module.ko。无需加载此模块;我们所需要的是其调试信息。我们使用dwarfdump工具,该工具在大多数 Linux 发行版中作为安装包提供,用于提取这些调试信息:

user@lab $ dwarfdump -di module.ko > module.dwarf

创建我们配置文件的下一步是获取目标系统或具有相同架构、内核版本和内核配置的系统的System.map文件。System.map文件通常位于/boot目录中。通常,内核版本会包含在文件名中,因此务必选择适用于取证目标系统运行内核的System.map文件。

module.dwarfSystem.map放入一个压缩档案中,这将成为我们的 Volatility 配置文件,如下所示:

user@lab $ zip Ubuntu3.2.0-88.zip module.dwarf System.map

如示例所示,ZIP 文件的名称应反映发行版和内核版本。

注意

确保不要向压缩档案中添加额外的路径信息。否则,Volatility 可能无法加载配置文件数据。

将新配置文件复制到 Volatility 的 Linux 配置文件目录,如下所示:

user@lab $ sudo cp Ubuntu3.2.0-88.zip /usr/local/lib/python2.7/dist-packages/volatility-2.4-py2.7.egg/volatility/plugins/overlays/linux/

除了使用系统范围的配置文件目录外,你还可以选择一个新的目录,并将--plugins=/path/to/profiles选项添加到 Volatility 命令行。

最后,你需要获取新配置文件的名称以供进一步使用。因此,使用以下命令:

user@lab $ vol.py --info

输出应包含一行额外的内容,显示新的配置文件,如下所示:

Profiles
--------
LinuxUbuntu3_2_0-88x64 - A Profile for Linux Ubuntu3.2.0-88 x64

要使用此配置文件,请在所有后续调用vol.py时,作为命令行参数添加--profile=LinuxUbuntu3_2_0-88x64

重建 Linux 的数据

所有分析 Linux 内存转储的插件都有linux_前缀。因此,你应使用 Linux 版本的插件。否则,可能会出现错误消息,提示所选配置文件不支持该模块。

分析进程和模块

分析内存转储的一个典型第一步是列出所有正在运行的进程和加载的内核模块。

以下是如何通过 Volatility 从内存转储中提取所有正在运行的进程:

user@lab $ vol.py --profile=LinuxUbuntu3_2_0-88x64 --file=memDump.lime linux_pslist
Volatility Foundation Volatility Framework 2.4

Offset             Name                 Pid             Uid             Gid    DTB                Start Time
------------------ -------------------- --------------- --------------- ------ ------------------ ----------
0xffff8802320e8000 init                 1               0               0      0x000000022f6c0000 2015-08-16 09:51:21 UTC+0000
0xffff8802320e9700 kthreadd             2               0               0      ------------------ 2015-08-16 09:51:21 UTC+0000

0xffff88022fbc0000 cron                 2500            0               0      0x000000022cd38000 2015-08-16 09:51:25 UTC+0000
0xffff88022fbc1700 atd                  2501            0               0      0x000000022fe28000 2015-08-16 09:51:25 UTC+0000
0xffff88022f012e00 irqbalance           2520            0               0      0x000000022df39000 2015-08-16 09:51:25 UTC+0000
0xffff8802314b5c00 whoopsie             2524            105             114    0x000000022f1b0000 2015-08-16 09:51:25 UTC+0000
0xffff88022c5c0000 freshclam            2598            119             131    0x0000000231fa7000 2015-08-16 09:51:25 UTC+0000

如输出所示,linux_pslist插件通过描述活动进程来迭代内核结构,即它从init_task符号开始,迭代task_struct->tasks链表。该插件获取所有正在运行的进程的列表,包括它们在内存中的偏移地址、进程名称、进程 ID(PID)、进程的用户和组的数值 ID(UID 和 GID),以及启动时间。目录表基址DTB)可用于进一步分析,将物理地址转换为虚拟地址。空的 DTB 条目最有可能与内核线程相关。例如,在我们的示例输出中是kthreadd

分析网络信息

内存转储包含有关法医目标系统网络活动的各种信息。以下示例展示了如何利用 Volatility 推导出最近的网络活动信息。

地址解析协议ARP缓存用于将 MAC 地址映射到 IP 地址。在建立本地网络上的网络通信之前,Linux 内核会发送 ARP 请求以获取给定目标 IP 地址对应的 MAC 地址信息。响应会被缓存到内存中,以便重新使用并与该 IP 地址在本地网络上进一步通信。因此,ARP 缓存条目指示了法医目标系统与本地网络上的哪些系统进行了通信。

要从 Linux 内存转储中读取 ARP 缓存,请使用以下命令:

user@lab $ vol.py --profile=LinuxUbuntu3_2_0-88x64 --file=memDump.lime linux_arp
[192.168.167.22                            ] at 00:00:00:00:00:00    on eth0
[192.168.167.20                            ] at b8:27:eb:01:c2:8f    on eth0

该输出提取显示系统为目标地址192.168.167.20保留了一个缓存条目,对应的 MAC 地址是b8:27:eb:01:c2:8f。第一个条目很可能是由于一次不成功的通信尝试而产生的缓存条目,也就是说,192.168.167.22的通信伙伴没有对系统发出的 ARP 请求做出响应,因此,相应的 ARP 缓存条目保持其初始值00:00:00:00:00:00。可能是通信伙伴无法访问,或者它根本不存在。

注意

如果你在 ARP 缓存中看到本地子网的大部分系统显示出多个 MAC 地址为 00:00:00:00:00:00 的条目,那么这表明存在扫描活动,也就是说,系统尝试在本地网络上探测其他系统。

为了进一步的网络分析,可能值得将从 ARP 缓存中获取的 MAC 地址列表与本地子网中应该存在的系统进行对比。虽然这种方法并非万无一失(因为 MAC 地址可以伪造),但它可能有助于发现不明的网络设备。

注意

查找 MAC 地址的硬件供应商

MAC 地址的前缀揭示了相应网络硬件的硬件供应商。像www.macvendorlookup.com这样的网站提供了网络卡硬件供应商的相关信息。

如果我们查找示例中b8:27:eb:01:c2:8f MAC 地址的硬件供应商,它显示该设备是由树莓派基金会制造的。在标准的办公室或数据中心环境中,这些嵌入式设备很少使用,因此检查该设备是否为良性设备是非常值得的。

为了概览创建内存转储时的网络活动,Volatility 提供了模拟 linux_netstat 命令的方法,如下所示:

user@lab $ vol.py --profile=LinuxUbuntu3_2_0-88x64 --file=memDump.lime linux_netstat
TCP      192.168.167.21  :55622 109.234.207.112  :  143 ESTABLISHED           thunderbird/3746
UNIX 25129          thunderbird/3746
TCP      0.0.0.0         : 7802 0.0.0.0         :    0 LISTEN                      skype/3833

这三行只是该命令典型输出的一个小片段。第一行显示 thunderbird 进程(PID 为 3746)与 IMAP 服务器(TCP 端口 143)通过 109.234.207.112 IP 地址建立了一个活动的 ESTABLISHED 网络连接。第二行仅显示一个 UNIX 类型的套接字,用于进程间通信IPC)。最后一行显示 skype(PID 为 3833)正在等待 LISTEN 状态,准备接收来自 TCP 端口 7802 的传入连接。

Volatility 还可以用来将进程列表缩小到那些具有原始网络访问权限的进程。通常,这种访问仅对动态主机配置协议DHCP)客户端、网络诊断工具以及当然的恶意软件有用,目的是在网络接口上构造任意数据包,例如进行所谓的 ARP 缓存中毒攻击。以下展示了如何列出具有原始网络套接字的进程:

user@lab $ vol.py --profile=LinuxUbuntu3_2_0-88x64 --file=memDump.lime linux_list_raw
Process          PID    File Descriptor Inode 
---------------- ------ --------------- ------------------
dhclient           2817               5              15831

这里,仅检测到 DHCP 客户端拥有原始网络访问权限。

提示

Rootkit 检测模块

Volatility 提供了多种机制来检测典型的 rootkit 行为,例如中断钩取、网络栈的操作和隐藏的内核模块。我们建议熟悉这些模块,因为它们可以加速你的分析。此外,定期检查模块更新,以利用 Volatility 内置的新恶意软件检测机制。

一些通用的方法和启发式技术用于恶意软件检测,并已结合在linux_malfind模块中。该模块会查找可疑的进程内存映射,并生成可能恶意进程的列表。

使用 YARA 进行恶意软件狩猎

YARA 本身是一个工具,能够在任意文件和数据集中的匹配给定的模式。相应的规则,也称为签名,是在硬盘或内存转储中搜索已知恶意文件的好方法。

在本节中,我们将展示如何在获取的 Linux 机器内存转储中搜索给定的恶意软件。因此,您可以使用我们将在接下来的内容中讨论的两种不同程序:

  • 直接使用 YARA 帮助搜索内存转储

  • 使用linux_yarascan和 Volatility

第一个选项有一个很大的缺点;正如我们所知,内存转储包含的是通常连续的碎片化数据。这一事实使得如果您在搜索已知签名时遇到失败的风险,因为它们不一定按您搜索的顺序排列。

第二个选项—使用linux_yarascan—更具容错性,因为它使用 Volatility 并了解获取的内存转储的结构。借助这些知识,它能够解决碎片化问题并可靠地搜索已知签名。虽然我们在 Linux 上使用linux_yarascan,但该模块也可用于 Windows(yarascan)和 Mac OS X(mac_yarascan)。

该模块的主要功能如下:

  • 在内存转储中扫描给定进程以查找给定的 YARA 签名

  • 扫描完整的内核内存范围

  • 将包含符合给定 YARA 规则的正面结果的内存区域提取到磁盘

输入vol.py linux_yarascan –h即可查看完整的命令行选项列表

基本上,您可以通过多种方式进行搜索。使用此模块的最简单方法是通过在内存转储中搜索给定的 URL。可以通过输入以下命令来完成此操作:

user@lab $ vol.py --profile=LinuxUbuntu3_2_0-88x64 --file=memDump.lime linux_yarascan –-yara-rules="microsoft.com" --wide

Task: skype pid 3833 rule r1 addr 0xe2be751f
0xe2be751f  6d 00 69 00 63 00 72 00 6f 00 73 00 6f 00 66 00   m.i.c.r.o.s.o.f.
0xe2be752f  74 00 2e 00 63 00 6f 00 6d 00 2f 00 74 00 79 00   t...c.o.m./.t.y.
0xe2be753f  70 00 6f 00 67 00 72 00 61 00 70 00 68 00 79 00   p.o.g.r.a.p.h.y.
0xe2be754f  2f 00 66 00 6f 00 6e 00 74 00 73 00 2f 00 59 00   /.f.o.n.t.s./.Y.
0xe2be755f  6f 00 75 00 20 00 6d 00 61 00 79 00 20 00 75 00   o.u...m.a.y...u.
0xe2be756f  73 00 65 00 20 00 74 00 68 00 69 00 73 00 20 00   s.e...t.h.i.s...
0xe2be757f  66 00 6f 00 6e 00 74 00 20 00 61 00 73 00 20 00   f.o.n.t...a.s...
0xe2be758f  70 00 65 00 72 00 6d 00 69 00 74 00 74 00 65 00   p.e.r.m.i.t.t.e.
0xe2be759f  64 00 20 00 62 00 79 00 20 00 74 00 68 00 65 00   d...b.y...t.h.e.
0xe2be75af  20 00 45 00 55 00 4c 00 41 00 20 00 66 00 6f 00   ..E.U.L.A...f.o.
0xe2be75bf  72 00 20 00 74 00 68 00 65 00 20 00 70 00 72 00   r...t.h.e...p.r.
0xe2be75cf  6f 00 64 00 75 00 63 00 74 00 20 00 69 00 6e 00   o.d.u.c.t...i.n.
0xe2be75df  20 00 77 00 68 00 69 00 63 00 68 00 20 00 74 00   ..w.h.i.c.h...t.
0xe2be75ef  68 00 69 00 73 00 20 00 66 00 6f 00 6e 00 74 00   h.i.s...f.o.n.t.
0xe2be75ff  20 00 69 00 73 00 20 00 69 00 6e 00 63 00 6c 00   ..i.s...i.n.c.l.
0xe2be760f  75 00 64 00 65 00 64 00 20 00 74 00 6f 00 20 00   u.d.e.d...t.o...
Task: skype pid 3833 rule r1 addr 0xedfe1267
0xedfe1267  6d 00 69 00 63 00 72 00 6f 00 73 00 6f 00 66 00   m.i.c.r.o.s.o.f.
0xedfe1277  74 00 2e 00 63 00 6f 00 6d 00 2f 00 74 00 79 00   t...c.o.m./.t.y.
0xedfe1287  70 00 6f 00 67 00 72 00 61 00 70 00 68 00 79 00   p.o.g.r.a.p.h.y.
0xedfe1297  2f 00 66 00 6f 00 6e 00 74 00 73 00 2f 00 59 00   /.f.o.n.t.s./.Y.
0xedfe12a7  6f 00 75 00 20 00 6d 00 61 00 79 00 20 00 75 00   o.u...m.a.y...u.
0xedfe12b7  73 00 65 00 20 00 74 00 68 00 69 00 73 00 20 00   s.e...t.h.i.s...
0xedfe12c7  66 00 6f 00 6e 00 74 00 20 00 61 00 73 00 20 00   f.o.n.t...a.s...
0xedfe12d7  70 00 65 00 72 00 6d 00 69 00 74 00 74 00 65 00   p.e.r.m.i.t.t.e.
0xedfe12e7  64 00 20 00 62 00 79 00 20 00 74 00 68 00 65 00   d...b.y...t.h.e.
0xedfe12f7  20 00 45 00 55 00 4c 00 41 00 20 00 66 00 6f 00   ..E.U.L.A...f.o.
0xedfe1307  72 00 20 00 74 00 68 00 65 00 20 00 70 00 72 00   r...t.h.e...p.r.
0xedfe1317  6f 00 64 00 75 00 63 00 74 00 20 00 69 00 6e 00   o.d.u.c.t...i.n.
0xedfe1327  20 00 77 00 68 00 69 00 63 00 68 00 20 00 74 00   ..w.h.i.c.h...t.
0xedfe1337  68 00 69 00 73 00 20 00 66 00 6f 00 6e 00 74 00   h.i.s...f.o.n.t.
0xedfe1347  20 00 69 00 73 00 20 00 69 00 6e 00 63 00 6c 00   ..i.s...i.n.c.l.
0xedfe1357  75 00 64 00 65 00 64 00 20 00 74 00 6f 00 20 00   u.d.e.d...t.o...

一种更复杂但更实际的方式是搜索给定的 YARA 规则。以下 YARA 规则是用来确定系统是否感染了Derusbi恶意软件家族:

rule APT_Derusbi_Gen
{
meta:
  author = "ThreatConnect Intelligence Research Team"
strings:
  $2 = "273ce6-b29f-90d618c0" wide ascii
  $A = "Ace123dx" fullword wide ascii
  $A1 = "Ace123dxl!" fullword wide ascii
  $A2 = "Ace123dx!@#x" fullword wide ascii
  $C = "/Catelog/login1.asp" wide ascii
  $DF = "~DFTMP$$$$$.1" wide ascii
  $G = "GET /Query.asp?loginid=" wide ascii
  $L = "LoadConfigFromReg failded" wide ascii
  $L1 = "LoadConfigFromBuildin success" wide ascii
  $ph = "/photoe/photo.asp HTTP" wide ascii
  $PO = "POST /photos/photo.asp" wide ascii
  $PC = "PCC_IDENT" wide ascii
condition:
  any of them
}

如果我们将此规则保存为apt_derusbi_gen.rule,我们可以通过输入以下命令在获取的内存转储中进行搜索:

user@lab $ vol.py --profile=LinuxUbuntu3_2_0-88x64 --file=memDump.lime linux_yarascan --yara-file=apt_derusbi_gen.rule --wide

结果只会显示一个简短的预览,您可以通过使用--size选项来放大它。

如果您正在调查预定义的场景(例如,如果您已经知道系统已被已知的攻击组攻击),您可以将所有规则复制到一个规则文件中,并一次性在内存转储中搜索该文件中的所有规则。Volatility 和linux_yarascan将显示每个匹配的结果及其对应的规则编号。这使得扫描已知恶意行为在内存转储中变得更快。

有大量可用于 YARA 签名的来源,这些来源在野外可用,我们这里只提及一些最重要的来源,以帮助你开始恶意软件猎杀,具体如下:

摘要

在本章中,我们概述了如何使用 Volatility 框架进行内存取证。在示例中,我们展示了 Android 和 Linux 系统的内存获取技术,并展示了如何在这两个系统上使用 LiME。我们使用 Volatility 获取了有关正在运行的进程、加载的模块、可能的恶意活动和最近的网络活动的信息。后者对于通过网络追踪攻击者的活动非常有用。

在本章的最后一个示例中,我们演示了如何在这样的内存转储中搜索给定的恶意软件签名或其他高度灵活的基于模式的规则。这些 YARA 签名或规则有助于快速识别可疑活动或文件。

此外,我们演示了如何获取 Android 设备的键盘缓存和通话历史。

接下来该做什么

如果你想测试从本书中获得的工具和知识,我们给你以下两条建议:

  • 创建一个包含两台虚拟机的实验室——MetasploitMetasploitable。尝试入侵你的Metasploitable系统,并随后进行取证分析。你能重建这次攻击并收集所有的妥协指标吗?

  • 获取一些旧的硬盘,这些硬盘已经不再使用,但过去曾经经常使用。对这些硬盘进行取证分析,并尽量重建尽可能多的数据。你能重建这些硬盘上的历史操作吗?

如果你想增强对本书中一些主题的了解,以下几本书是非常好的选择:

  • 实用移动取证Satish BommisettyRohit TammaHeather MahalikPackt Publishing出版

  • 记忆取证的艺术:在 Windows、Linux 和 Mac 内存中检测恶意软件和威胁Michael Hale LighAndrew CaseJamie LevyAAron Walters 编写,Wiley India出版

  • 数字取证与调查手册Eoghan Casey 编写,Academic Press出版

posted @ 2025-07-07 14:34  绝不原创的飞龙  阅读(12)  评论(0)    收藏  举报