导语:这篇文章(第一部分)将讨论现有的检测技术,名为Get-InjectedThread的PowerShell脚本,以确保所有读者具有相同的基础知识。
简介
欢迎来到我们的系列博客的第一部分,题为“Defenders Think in Graphs Too!”。在本系列中,Roberto Rodriguez(@cyb3rward0g)和我(@jaredcatkinson)将通过一个专门研究检测进程注入的案例来讨论我们对数据采集、数据质量和数据分析的看法。这篇文章(第一部分)将讨论现有的检测技术,名为Get-InjectedThread的PowerShell脚本,以确保所有读者具有相同的基础知识。在随后的文章中,我们将通过Get-InjectedThread的数据质量来分析问题,确定如何改进当前脚本,涵盖与HELK项目的集成以及研究使用可视化技术来改进我们的产品。
线程注入检测
在去年新奥尔良举行的SANS威胁狩猎峰会上,Endgame的Joe Desimone和我发布了Get-InjectedThread,这是一个PowerShell脚本,通过检测机器上的线程来检测代码注入。如果您有兴趣了解更多有关这种检测方法的信息,请查看相关视频或查看我进一步的解释。对于那些只关注脚本实际运行的用户,可以查看Get-InjectedThread的演示视频,PowerShell Empire客户端迁移到LSASS进程后也能检测。
与此同时,让我们看看Get-InjectedThread的实效。本文我们将关注Get-InjectedThread的功能而不是实用程序,因此我们使用良性测试函数作为示例。Joe编写了一个名为ThreadStart.exe的快速测试二进制文件,它模仿代码注入行为来测试我们脚本的功能。作为PowerShell迷,我决定在PowerShell中重写ThreadStart,以使它更易适用于我们的例子。所以第一步是从PowerShell进程中执行New-InjectedThread函数。New-InjectedThread的代码可以在GitHub上的PSReflect-Functions存储库中找到。
让我们来快速回顾一下New-InjectedThread函数的功能:
· 以当前PowerShell进程的Id为参数执行Get-Process cmdlet。生成的System.Diagnostics.Process实例将包含一个稍后使用的进程句柄。
· 使用VirtualAllocEx分配内存写入“恶意”代码。
· 使用WriteProcessMemory将shellcode写入新分配的内存区域。这一步对于测试是不必要的,但我们目前正在写MZ作为头两字节来模仿PE头。
· 调用CreateThread并设置CREATE_SUSPENDED标志并指向VirtualAllocEx分配的内存区域。
· 由于我们不再使用它,因此使用CloseHandle关闭线程句柄。
· 输出验证“恶意”线程检测所需的信息。
当执行New-InjectedThread时,你会看到类似下图的结果:
使用New-InjectedThread创建一个测试注入线程
运行New-InjectedThread后,它看起来像我们有一个示例线程来检测。注意该函数报告“恶意”线程的ProcessId(1008),ThreadId(9220)和Memory Base Address(2144746340352)。
接下来,我们可以在终端上运行Get-InjectedThread,并期望检测出至少一个代码注入实例。这与将Get-InjectedThread.ps1加载到当前PowerShell的运行空间并调用不带参数的Get-InjectedThread函数一样简单,如下所示:
使用Get-InjectedThread检测注入
很棒!Get-InjectedThread检测到在powershell.exe进程中有注入。经过深入调查,我们注意到ThreadId和BaseAddress字段与预期的基于New-InjectedThread报告的内容相符。
让我们花点时间来了解一下Get-InjectedThread如何工作。 Get-InjectedThread是基于Matt Graeber的PSReflect模块构建的PowerShell脚本。PSReflect将Reflection的复杂性抽象出来,围绕Win32 API构建函数、枚举和结构体,供PowerShell在内存中访问。Get-InjectedThread专门使用PSReflect来收集有关进程、线程、访问令牌和登录会话的信息。然后将这些信息组成一个自定义的psobject实例并输出到PowerShell管道。在下面,可以看到通过PSReflect收集到的信息InjectedThread对象的可视化。
Get-InjectedThread数据结构
对于那些对技术细节感兴趣的人,下面是对Get-InjectedThread中细节更加技术性的解释:
· 使用ProcessId 为0(所有进程)并将Flag参数设置为4(TH32CS_SNAPTHREAD)调用Create Toolhelp Snapshot。这将返回所有当前正在运行的线程的快照。
· 使用Thread32First和Thread32Next处理快照中的所有线程。
以下每个步骤都将在每个线程上执行:
· 调用OpenThread以接收内核中的Thread对象的句柄。
· 使用Thread句柄,为ThreadInformationClass参数指定值为9(ThreadQuerySetWin32StartAddress)的NtQueryInformationThread。这将返回线程的内存起始地址。
· 调用OpenProcess来接收当前线程拥有进程的句柄。
· 将进程句柄和线程起始地址传递给VirtualQueryEx以查询目标内存页面。这将返回一个MEMORY_BASIC_INFORMATION结构。
· 检查返回结构中的State和Type字段。
· 所有线程的状态应该是MEM_COMMIT
· 所有线程的类型应为MEM_IMAGE
· 如果Type不等于MEM_IMAGE,那么你有一个正在运行代码的线程,这个线程不会被磁盘上的文件(又名注入)所支持。
针对所有“注入的线程”执行以下步骤:
· 使用ReadProcessMemory在BaseAddress中读取内容。基地址的前100个字节将从该函数返回。
· 使用OpenThreadToken获取应用于线程的访问令牌的句柄。如果此函数失败,请使用OpenProcessToken获取进程“主”访问令牌的句柄。默认情况下,如果不使用模拟,线程将使用进程访问令牌。
· 调用GetTokenInformation来查询有关访问令牌的信息,例如用户,特权,模拟级别,完整性级别以及许多其他属性。
现在,我们都了解了Get-InjectedThread是如何工作的,并且有一个简单的测试例子来验证我们的逻辑。
改进注入线程检测
这个PowerShell脚本的接收量比我预期的要大。让社区拥有以前专为商业EDR解决方案或重要分析技术(如Memory Forensics)而保留的功能感觉很棒。借助Get-InjectedThread,任何安全从业人员都可以开始在企业内部寻找内存驻留攻击。
最近,我开始想着如何改进Get-InjectedThread。我仍然认为底层的功能非常好,但我觉得可以在数据方面做的更多。就目前而言,Get-InjectedThread在微观层次检测代码注入,但在终端上错过了更大的Context。例如,Get-InjectedThread只会为检测到已注入的线程返回输出。这意味着计算资源正在使用,但我们的分析师没有收到这些数据。如果这些遗漏的数据可用于其他检测,该怎么办?在Get-InjectedThread的情况下,我们必须为其他检测工作运行一个完全独立的集合。主要解决方案是不在终端上执行分析工作。相反,收集所有相关数据并转发到集中日志源(例如HELK)来进行分析。
结论
多年来,我一直专注于数据收集,Roberto专注于数据分析。我们相信,我们已经整合出一套非常好的方法来处理整个过程的数据。本系列博客是我们的尝试,通过实际使用案例在社区分享研究结果。在第二部分中,我们将深入探讨Get-InjectedThread的问题,并讨论如何对脚本集进行改进。之后的博客将从数据质量、相关性和分析角度审视这些数据。本文旨在作为本系列的入门介绍,敬请关注第二部分!