黑帽python第二版(Black Hat Python 2nd Edition)读书笔记 之 第十一章 攻击性取证
文章目录
前言
司法取证人员通常是在违规事件发生后被叫来的人,或者是为了确定是否发生了“事件”。他们通常需要受影响机器RAM的快照,以便捕获仅存在于内存中的密码密钥或其他信息。幸运的是,一个有才华的开发团队已经创建了一个完整的Python框架,名为Volatility,适合于此任务,并被称为高级内存取证框架。事件响应者、司法取证员和恶意软件分析人员也可以将Volatility用于各种其他任务,包括检查内核对象、检查和转储进程等。
尽管Volatility是防守方的软件,但任何足够强大的工具都可以用于进攻或防守。我们将使用Volatility对目标用户进行侦察,并编写我们自己的攻击性插件来搜索运行在虚拟机(VM)上的弱防御进程。
假设我们潜入一台机器,发现用户雇佣了一个虚拟机进行敏感工作。用户很有可能还将虚拟机做了快照,以防出现问题能够快速恢复。我们将使用易失性内存分析框架分析快照,以了解虚拟机的使用方式和运行的进程。我们还将调查可能存在的漏洞,以便进一步利用。
安装
Volatility已经存在好几年了,刚刚经历了彻底的改写。不仅代码库现在建立在Python 3上,而且整个框架已经被重构,使得组件是独立的;运行插件所需的所有状态都是自包含的。
让我们为我们的Volatility工作创建一个虚拟环境。对于本例,我们在PowerShell终端中的Windows计算机上使用Python 3。如果我们也在Windows计算机上工作,请确保安装了git。可以在https://git-scm.com/downloads/获取。
Volatility已经存在好几年了,刚刚经历了彻底的改写。不仅代码库现在建立在Python 3上,而且整个框架已经被重构,使得组件是独立的;运行插件所需的所有状态都是自包含的。
让我们为我们的Volatility工作创建一个虚拟环境。对于本例,我们在PowerShell终端中的Windows计算机上使用Python 3。如果我们也在Windows计算机上工作,请确保安装了git。可以在https://git-scm.com/downloads/获取。
说明:这里要说明一下,按照原书中的命令行(下图)是没办法进入虚拟环境并激活的。
需要参照前面第二章中在paramiko使用SSH的章节中介绍的方式(使用Set-ExecutionPolicy RemoteSigned命令),如下图。
接下来再依次执行下面几条命令即可。
PS> cd vol3
PS> git clone https://github.com/volatilityfoundation/volatility3.git
PS> cd volatility3
PS> python setup.py install
PS> pip install pycryptodome
上述命令行中,我们创建了一个名为vol3的新虚拟环境并将其激活。接下来,我们在虚拟环境目录中从GitHub克隆Volatility3并将其安装到虚拟环境中,最后安装我们稍后将会用到的pycryptodome。
要查看Volatility提供的插件以及选项列表,请在Windows上使用以下命令:
PS> vol --help
在Linux或Mac上,使用虚拟环境中的Python可执行文件,如下所示:
$> python vol.py --help
在本章中,我们将从命令行使用Volatility,但是我们可能会遇到各种各样的情况。例如,请参阅Volatility的Volumetric项目,这是一个免费的基于web的Volatility GUI(https://github.com/volatilityfoundation/volumetric/). 大家可以深入研究Volumetric项目中的代码示例,了解如何在自己的程序中使用Volatility。此外,我们还可以使用volshell接口,该接口为我们提供了对Volatility框架的访问,并作为普通的交互式Python shell工作。
在下面的示例中,我们将使用Volatility命令行。为了节省空间,已编辑输出以仅显示讨论的内容,因此请注意,我们实际的输出将有更多的行和列。
现在让我们深入研究一些代码,看看框架内部:
此列表显示了Windows插件目录中的Python文件。我们强烈建议花一些时间查看这些文件中的代码。这将看到形成Volatility插件结构的循环模式。这将有助于我们理解框架,但更重要的是,它将让我们了解一名后卫的心态和意图。通过了解防御者的能力以及他们如何实现目标,将会使我们称为一名更有能力的黑客,并更好地了解如何保护自己免受检测。
现在我们已经准备好了分析框架,我们需要一些内存镜像来分析。最简单的获取方法是对自己的Windows 10虚拟机进行快照。
首先,启动Windows VM并启动几个进程(例如,记事本、计算器和浏览器);我们将检查内存并跟踪这些进程是如何开始的。然后,使用我们选择的虚拟机监控程序拍摄快照。在虚拟机监控程序存储虚拟机的目录中,我们将看到新的快照文件,其名称以.vmem或.mem结尾。我们开始侦查吧!
请注意,我们还可以找到许多网上的镜像。我们将在本章中看到由PassMark Software提供的一个镜像,网址为https://www.osforensics.com/tools/volatility-workbench.html/。另外Volatility Foundation站点也有一些镜像:https://github.com/volatilityfoundation/volatility/wiki/Memory-Samples/
一般侦察
让我们概述一下我们正在分析的机器。Windows.info插件显示内存示例的操作系统和内核信息。为了验证,我把自己本地的VMware下的windows虚拟机的snapshot文件拿来进行了分析,执行下面的命令:
PS> vol -f .\Windows10-Snapshot1.vmem windows.info
运行分析的结果如下。
我们使用-f参数来指定要使用的快照文件名以及Windows插件Windows.info。Volatility读取并分析内存文件,并输出有关此Windows计算机的一般信息。我们可以看到,我们正在处理一个Windows 10.0虚拟机,它有2个处理器和1个内存层。
在查看插件代码时,我们可能会发现在内存映像文件上尝试几个插件很有教育意义。花时间阅读代码并查看相应的输出将向我们展示代码应该如何工作以及防御者的一般心态。
接下来,使用registry.printkey插件,我们可以打印注册表中键的值。注册表中有丰富的信息,Volatility提供了一种找到我们想要的任何注册表键值的方法。在这里,我们查找已安装的服务。键/ControlSet001/Services显示服务控制管理器数据库,其中列出了所有已安装的服务:
PS> vol -f .\Windows10-Snapshot1.vmem windows.registry.printkey --key 'ControlSet001\Services'
输出结果还真是不少,下图仅仅展示了开始的一小部分。
此输出显示计算机上已安装服务的列表。
用户侦查
现在让我们对VM的用户进行一些侦察。cmdline插件列出了每个进程在创建快照时运行的命令行参数。这些过程为我们提供了关于用户行为和意图的提示信息。
PS> vol -f .\Windows10-Snapshot1.vmem windows.cmdline
执行结果如下图所示。
该列表显示进程ID、进程名称和命令行以及启动进程的参数。我们可以看到,大多数进程都是由系统本身启动的,很可能是在启动时启动的。cmd.exe和notepad.exe进程是用户启动的典型进程
让我们使用pslist插件更深入地研究正在运行的进程,该插件列出了快照时正在运行的进程:
PS> vol -f .\Windows10-Snapshot1.vmem windows.pslist
执行结果如下图所示。
这里我们看到了实际的进程及其内存偏移。为了节省空间,省略了一些列。
最好将进程视为一个层次结构,这样我们就可以知道是哪个进程启动了其他子进程。为此,我们将使用pstree插件:
PS> vol -f .\Windows10-Snapshot1.vmem windows.pstree
执行结果如下图所示。
现在我们有了更清晰的画面。每行中的星号表示流程的父子关系。例如,services进程(PID 644)生成了dllhost.exe进程。同样,svchost.exe进程(PID 780)启动TextInputHost.exe进程(PID 5636)。
现在让我们使用hashdump插件查看密码:
PS> vol -f .\Windows10-Snapshot1.vmem windows.hashdump
关于这一块,我的输出结果跟原书中的表述有些差异,我本地的输出结果如下。
而原书中的输出结果如下图所示。
上面显示了用户名及其密码的LM和NT哈希。渗透之后恢复Windows计算机上的密码散列是攻击者的共同目标,这些哈希可以离线破解,以试图恢复目标的密码,也可以用于传递哈希攻击,以获得对其他网络资源的访问。无论目标是只在虚拟机上执行高风险操作的偏执狂用户,还是试图将其用户的某些活动包含到虚拟机中的企业,查看系统上的虚拟机或快照都是在访问主机硬件后尝试恢复这些哈希值的最佳时机。
Volatility使这一恢复过程变得非常容易。
我们混淆了输出中的散列,我们可以使用自己的输出作为哈希破解工具的输入,以找到进入VM的方法。另外,有几个在线的哈希破解网站;或者,我们可以在自己的Kali机器上使用John the Ripper。
漏洞侦查
现在,我们使用Volatility来发现目标VM是否存在我们可以利用的漏洞。malfind插件检查可能包含注入代码的进程的内存范围。Potential是插件寻找具有读、写和执行权限的内存区域的关键词。我们值得去调查这些过程,因为它们可能使我们能够利用一些已经可用的恶意软件,或者我们可以用自己的恶意软件覆盖这些区域。
PS> vol -f .\Windows10-Snapshot3.vmem windows.malfind
输出内容如下图所示。
用搜索引擎,您会发现进程MsMpEng.exe(PID 2172)是反恶意软件服务。尽管这些进程包含可写和可执行内存区域,但它们看起来并不危险。也许我们可以通过将外壳代码写入这些内存区域来使这些进程变得危险,因此值得注意。
另外,最底下也扫描除了我们的微信小程序进程(PID 3320),可能存在一些漏洞,如下图。
netscan插件提供了快照时计算机拥有的所有网络连接的列表,如下所示,任何看起来可疑的东西我们都可以在攻击中利用。
PS> vol -f .\Windows10-Snapshot3.vmem windows.netscan
执行结果如下图所示。
我们看到一些本地机器的连接,有些连接现在已关闭。更重要的是标有“LISTENING”的连接。由可识别的Windows进程(svchost、lsass、wininit)拥有的进程可能还可以,但其它一些未知进程是非常可疑的,通过使用第2章中的netcat替代来探测该这些进程的监听端口,非常值得深入研究。
volshell接口
除了命令行界面,我们还可以在带有volshell命令的自定义Python shell中使用Volatility。这为我们提供了Volatility的所有功能以及完整的Python shell。在接下来的导入pslist插件的过程中,报错了,如下图所示。
这个问题一直没有解决掉,没有得到原书中下图所示的预期结果,待后续详细研究Volatility之后在来解决。
在这个简短的示例中,我们使用-w告诉Volatility我们正在分析一个Windows映像,使用-f开关指定映像本身。一旦我们进入了volshell接口,我们就像普通的Pythonshell一样使用它。也就是说,您可以像平常一样导入包或编写函数,但现在您的shell中也嵌入了Volatility。我们导入pslist插件并从插件显示输出(dpo函数)。
另外可以通过输入volshell–help找到有关使用volshell的更多信息。
自定义Volatility插件
我们刚刚看到了如何使用Volatility插件来分析VM快照中的现有漏洞,通过检查正在使用的命令和进程来分析用户,并转储密码散列。但是,由于我们可以编写自己的自定义插件,因此只有我们的想象力限制了我们对Volatility的操作。如果我们需要基于从标准插件中找到的线索的其他信息,我们可以创建自己的插件。
只要遵循他们的模式,Volatility团队就可以轻松创建插件。我们甚至可以让我们的新插件调用其他插件,以使我们的工作更加轻松。
让我们看一下典型的插件的框架:
这里的主要步骤是创建要从PluginInterface继承的新类,定义插件的需求,定义run方法,以及定义生成器方法。生成器方法是可选的,但将其与run方法分离是一种有用的模式,我们将在许多插件中看到。通过将其分离并将其用作Python生成器,可以获得更快的结果,并使代码更易于理解。
让我们按照这个通用模式创建一个自定义插件,该插件将检查不受地址空间布局随机化(ASLR)保护的进程。ASLR混淆了易受攻击进程的地址空间,这会影响堆、堆栈和其他操作系统分配的虚拟内存位置。这意味着攻击者无法确定攻击时受害者进程的地址空间的布局。Windows Vista是第一个支持ASLR的Windows版本。在旧的内存映像(如Windows XP)中,默认情况下不会启用ASLR保护。现在,使用最新的计算机(Windows 10),几乎所有进程都受到保护。
ASLR并不意味着攻击者已经破产,但它使工作变得更加复杂。作为侦察进程的第一步,我们将创建一个插件来检查进程是否受ASLR保护。
现在我们开始,创建一个名为plugins的目录,在该目录下创建一个windows目录,以包含windows计算机的自定义插件。如果我们创建插件以Mac或Linux计算机为目标,请分别创建一个名为Mac或Linux的目录。
现在,在plugins/windows目录中,我们编写ASLR检查插件aslrcheck.py:
from typing import Callable, List
from volatility.framework import constants, exceptions, interfaces, renderers
from volatility.framework.configuration import requirements
from volatility.framework.renderers import format_hints
from volatility.framework.symbols import intermed
from volatility.framework.symbols.windows import extensions
from volatility.plugins.windows import pslist
import io, logging, os, pefile
vollog = logging.getLogger(__name__)
IMAGE_DLL_CHAEACTERISTICS_DYNAMIC_BASE = 0x0040
IMAGE_FILE_RELOCS_STRIPPED = 0x0001
我们首先处理需要的导入(因为版本问题,这里导入库的时候都要变成volatility3),以及用于分析可移植可执行文件(PE)文件的pefile库。现在让我们编写一个支持函数来进行分析:
def check_aslr(pe):
pe.parse_data_directories([
pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG']
])
dynamic = False
stripped = False
if (pe.OPTIONAL_HEADER_DllCharacteristics &
IMAGE_DLL_CHAEACTERISTICS_DYNAMIC_BASE):
dynamic = True
if pe.FILE_HEADER.Characteristics & IMAGE_FILE_RELOCS_STRIPPED:
stripped = True
if not dynamic or (dynamic and stripped):
aslr = False
else:
aslr = True
return aslr
我们将一个PE文件对象传递给check_aslr函数,解析它,然后检查它是否已使用DYNAMIC基础设置编译,以及文件重新定位数据是否已被删除。如果它不是动态的,或者可能被编译为动态的,但去掉了重新定位数据,那么PE文件就不受ASLR保护。
准备好check_aslr支持函数后,让我们创建AslrCheck类:
class AslrCheck(interfaces.plugins.PluginInterface):
@classmethod
def get_requirements(cls):
return [
requirements.TranslationLayerRequirement(
name='primary', description='Memory layer for the kernel',
architectures=["Intel32", "Intel64"]),
requirements.SymbolTableRequirement(
name="nt_symbols", description="Windows kernel symbols"),
requirements.PluginRequirement(
name='pslist', plugin=pslist.PsList, version=(1, 0, 0)),
requirements.ListRequirement(name = 'pid',
element_type = int,
description = "Process ID to include (all others are excluded)",
optional = True),]
创建插件的第一步是从PluginInterface对象继承。接下来,定义requirements。通过查看其他插件,我们可以很好地了解我们需要什么。每个插件都需要内存层,我们首先定义这个需求。除了内存层,我们还需要符号表。我们会发现几乎所有插件都使用这两个requirements。
我们还需要pslist插件作为一个requirements,以便从内存中获取所有进程并从进程中重新创建PE文件。然后,我们将从每个进程传递重新创建的PE文件,并检查它是否有ASLR保护。
我们可能希望在给定进程ID的情况下检查单个进程,因此我们创建了另一个可选设置,该设置允许我们传入进程ID列表,仅限于检查这些进程。
@classmethod
def create_pid_filter(cls, pid_list: List[int] = None) -> Callable[[interfaces.objects.ObjectInterface], bool]:
filter_func = lambda _: False
pid_list = pid_list or []
filter_list = [x for x in pid_list if x is not None]
if filter_list:
filter_func = lambda x: x.UniqueProcessId not in filter_list
return filter_func
为了处理可选的进程ID,我们使用一个类方法创建一个过滤函数,该函数为列表中的每个进程ID返回False;也就是说,我们问filter函数是否过滤掉一个进程,所以我们只在PID不在列表中时返回True:
def generator(self, procs):
pe_table_name = intermed.IntermediateSymbolTable.create(
self.context,
self.config_path,
"windows",
"pe",
class_types = extensions.pe.class_types)
procnames = list()
for proc in procs:
procname = proc.ImageFileName.cast("string",
max_length=proc.ImageFileName.vol.count, errors='replace')
if procname in procnames:
continue
procnames.append(procname)
proc_id = "Unknown"
try:
proc_id = proc.UniqueProcessId
proc_layer_name = proc.add_process_layer()
except exceptions.InvalidAddressException as e:
vollog.error(f"Process {proc_id}: invalid assress {e} in layer {e.layer_name}")
continue
peb = self.context.object(
self.config['nt_symbols'] + constants.BANG + "_PEB",
layer_name = proc_layer_name,
offset = proc.Peb)
try:
dos_header = self.context.object(
pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER",
offset = peb.ImageBaseAddress,
layer_name = proc_layer_name)
except Exception as e:
continue
pe_data = io.BytesIO()
for offset, data in dos_header.reconstruct():
pe_data.seek(offset)
pe_data.write(data)
pe_data_raw = pe_data.getvalue()
pe_data.close()
try:
pe = pefile.PE(data=pe_data_raw)
except Exception as e:
continue
aslr = check_aslr(pe)
yield (0, (proc_id,
procname,
format_hints.Hex(pe.OPTIONAL_HEADER.ImageBase),
aslr,
))
我们创建了一个名为pe_table_name的特殊数据结构,以便在遍历内存中的每个进程时使用。然后,我们获得与每个进程关联的进程环境块(PEB)内存区域,并将其放入一个对象中。PEB是当前进程的数据结构,包含大量进程信息。我们将该区域写入类文件对象(pe_data),使用pefile库创建pe对象,并将其传递给check_aslr方法。最后,我们生成包含进程ID、进程名称、进程内存地址和ASLR保护检查的布尔结果的信息元组。
接下来我们创建run方法,它不需要参数,因为所有的settings都填充在config对象中:
def run(self):
procs = pslist.PsList.list_processes(self.context,
self.config["primary"],
self.config["nt_symbols"],
filter_func =
self.create_pid_filter(self.config.get('pid', None)))
return renderers.TreeGrid([
("PID", int),
("filemame", str),
("Base", format_hints.Hex),
("ASLR", bool)],
self._generator(procs))
我们使用pslist插件获取进程列表,并使用TreeGrid渲染器从生成器返回数据。TreeGrid渲染器被许多插件使用。它确保我们为每个分析的过程获得一行结果。
小试牛刀
让我们来看看Volatility网站上提供的其中一个image:恶意软件——Cridex(在这个站点可以下载到: https://github.com/volatilityfoundation/volatility/wiki/Memory-Samples)。对于自定义插件,请使用-p参数提供插件文件夹的路径。但是我执行了的时候,这里出现了错误,尽管跟原书的代码是一致的,但是仍然存在错误,如下图。
初步估计是版本兼容性问题,后续再研究一下,下图是原书中的运行结果。
如我们所见,这是一台Windows XP机器,任何进程都没有ASLR保护。
下面是一台干净、最新的Windows 10计算机的结果(仍然是上述的错误,这里盗用一下原书的输出结果):
在这里看不到太多。每个列出的进程都受ASLR保护。然而,我们也看到了内存污点。当存储器的内容随着内存镜像的拍摄而改变时,会出现内存拖尾。结果是
在内存表中描述与内存本身不匹配;或者,虚拟内存指针可以引用无效数据。想黑入很难,正如错误描述所示,我们可以尝试重新获取图像(查找或创建新快照)。
让我们检查PassMark Windows 10示例内存映像(继续盗图):
几乎所有进程都受到保护。只有单一进程SynTPEnh.exe不受ASLR保护。在线搜索显示,这是Synaptics Pointing Device的一个软件组件,可能用于触摸屏。只要该进程安装在c:\Program Files中,它可能是可以的,但它可能值得稍后进行模糊测试。
附上最终源代码。
# Search all processes and check for ASLR protection
#
from typing import Callable, List
from volatility3.framework import constants, exceptions, interfaces, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints
from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.windows import extensions
from volatility3.plugins.windows import pslist
import io
import logging
import os
import pefile
vollog = logging.getLogger(__name__)
IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE = 0x0040
IMAGE_FILE_RELOCS_STRIPPED = 0x0001
def check_aslr(pe):
pe.parse_data_directories([pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG']])
dynamic = False
stripped = False
if pe.OPTIONAL_HEADER.DllCharacteristics & IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE:
dynamic = True
if pe.FILE_HEADER.Characteristics & IMAGE_FILE_RELOCS_STRIPPED:
stripped = True
if not dynamic or (dynamic and stripped):
aslr = False
else:
aslr = True
return aslr
class AslrCheck(interfaces.plugins.PluginInterface):
@classmethod
def get_requirements(cls):
return [
requirements.TranslationLayerRequirement(
name='primary', description='Memory layer for the kernel',
architectures=["Intel32", "Intel64"]),
requirements.SymbolTableRequirement(
name="nt_symbols", description="Windows kernel symbols"),
requirements.PluginRequirement(
name='pslist', plugin=pslist.PsList, version=(1, 0, 0)),
requirements.ListRequirement(name = 'pid',
element_type = int, description = "Process ID to include (all other processes are excluded)",
optional = True),
]
@classmethod
def create_pid_filter(cls, pid_list: List[int] = None) -> Callable[[interfaces.objects.ObjectInterface], bool]:
filter_func = lambda _: False
pid_list = pid_list or []
filter_list = [x for x in pid_list if x is not None]
if filter_list:
filter_func = lambda x: x.UniqueProcessId not in filter_list
return filter_func
def _generator(self, procs):
pe_table_name = intermed.IntermediateSymbolTable.create(
self.context,
self.config_path,
"windows",
"pe",
class_types=extensions.pe.class_types)
procnames = list()
for proc in procs:
procname = proc.ImageFileName.cast("string", max_length=proc.ImageFileName.vol.count, errors='replace')
if procname in procnames:
continue
procnames.append(procname)
proc_id = "Unknown"
try:
proc_id = proc.UniqueProcessId
proc_layer_name = proc.add_process_layer()
except exceptions.InvalidAddressException as e:
vollog.error(f"Process {proc_id}: invalid address {e} in layer {e.layer_name}")
continue
peb = self.context.object(
self.config['nt_symbols'] + constants.BANG + "_PEB",
layer_name = proc_layer_name,
offset = proc.Peb)
try:
dos_header = self.context.object(
pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER",
offset=peb.ImageBaseAddress,
layer_name=proc_layer_name)
except Exception as e:
continue
pe_data = io.BytesIO()
for offset, data in dos_header.reconstruct():
pe_data.seek(offset)
pe_data.write(data)
pe_data_raw = pe_data.getvalue()
pe_data.close()
try:
pe = pefile.PE(data=pe_data_raw)
except Exception as e:
continue
aslr = check_aslr(pe)
yield (0, (proc_id,
procname,
format_hints.Hex(pe.OPTIONAL_HEADER.ImageBase),
aslr,
))
def run(self):
procs = pslist.PsList.list_processes(self.context,
self.config["primary"],
self.config["nt_symbols"],
filter_func = self.create_pid_filter(self.config.get('pid', None)))
return renderers.TreeGrid([
("PID", int),
("Filename", str),
("Base", format_hints.Hex),
("ASLR", bool)],
self._generator(procs))
在本章中,我们看到了可以利用Volatility框架的强大功能来查找有关用户行为和连接的更多信息,以及分析运行内存的任何进程的数据。我们可以利用这些信息更好地了解目标用户和机器,以及了解防守方的心态。
向前
我们现在应该已经注意到,Python是一种很好的黑客语言,特别是当我们考虑到现有的许多库和基于Python的框架时。虽然黑客工具太多了,但实际上没有任何东西可以替代我们自己的工具,因为这可以让我们更深入地了解其他工具在做什么。
继续,快速为我们的特殊需求编写自定义工具。无论是用于Windows的SSH客户端、web刮板还是命令和控制系统,Python都可以为我们提供。