New Process Injection Techniques Using Windows Thread Pools

The Pool Party You Will Never Forget: New Process Injection Techniques Using Windows Thread Pools

了解 SafeBreach 实验室的研究人员如何开发一套全新的高度灵活的过程注入技术,这些技术能够完全绕过领先的端点检测和响应(EDR)解决方案。

在网络攻击中,恶意行为者通常会使用漏洞利用和网络钓鱼等策略来破坏组织的外围安全。
一旦进入,他们就会试图通过该组织的网络来升级他们的特权,窃取或加密数据——但在这里,他们经常面临复杂的端点检测和响应(EDR)系统,该系统旨在识别和防止这类活动。为了逃避检测,威胁参与者采用了进程注入技术,允许他们将恶意代码注入计算机系统的合法进程中。然后,代码由目标进程(而不是攻击者)执行,这使得组织很难从取证的角度识别和跟踪代码。

虽然进程注入技术过去更为流行,但大多数操作系统(OS)和EDR供应商都加强了安全措施,要么完全阻止已知技术,要么严格限制其影响。因此,近年来出现的技术越来越少,直到现在,仍然在野外看到的技术只适用于特定的过程状态。

SafeBreach Labs团队开始探索使用Windows线程池(Microsoft Windows os的一个未被分析的领域)作为进程注入的新攻击载体的可行性。
在这个过程中,发现了 8 种新的进程注入技术,称之为 Pool Party 变体,它们能够由于完全合法的操作而触发恶意执行。
这些技术能够在没有任何限制的情况下跨所有进程工作,这使得它们比现有的进程注入技术更加灵活。
更重要的是,在对五种领先的EDR解决方案进行测试时,这些技术被证明是完全无法检测到的。

下面我们将分享我们的研究背后的细节,该研究首次在 Black Hat Europe 2023 展示。我们将首先对进程注入如何工作以及端点安全控制如何检测当前已知技术进行高级概述。
然后,我们将解释 Windows 线程池的体系结构和相关组件,并讨论导致我们成功利用它们开发 8 种独特的进程注入技术的研究过程。
最后,我们将重点介绍我们测试的 EDR 解决方案,并确定 SafeBreach 如何与更广泛的安全社区共享这些信息,以帮助组织保护自己。

Background

Process Injection

作为一种用于在目标进程中执行任意代码的规避技术,进程注入通常由三个原语组成:

  1. Allocation primitive (分配原语): 用于在目标进程分配内存
  2. Writing primitive (写原语): 向已分配的内存写入恶意代码
  3. Execution primitive (执行原语): 用于执行已写入的恶意代码
    在这里插入图片描述

最基本的注入技术将使用 VirtualAllocEX() 进行分配,WriteProcessMemory() 进行写入,CreateRemoteThread() 进行执行。
这种注入技术,公开称为 CreateRemoteThread注入 ,非常简单和强大,但有一个缺点:它可以被所有现代的 edr 检测到。
我们的研究试图发现是否有可能创造完全无法检测的进程注入技术 。

通过这个过程,我们试图了解 edr 是否能够有效区分功能的合法使用和恶意使用。
我们还想了解当前 edr 使用的检测方法是否足够通用,可以检测到新的和从未见过的进程注入。

EDR Detection Approach

为了回答这些问题,我们需要回顾目前 edr 对进程注入的检测方法。
对不同原语的实验使我们得出这样的结论: edr 的检测主要基于执行原语。
最重要的是,不会检测到写和分配原语(以最基本的形式)。
在这里插入图片描述

根据这一发现,如果只基于分配和写原语创建执行原语,会发生什么情况?
此外,如果执行是由合法的操作(例如,写入无害的文件)触发的,并且可能触发受害进程上的 shellcode,该怎么办?
这样的功能将使进程注入更加难以检测。

Windows User-Mode Thread Pools

在寻找有助于实现研究目标的合适组件时,我们遇到了Windows用户模式线程池。这最终成为一个完美的目标,因为:

  1. 默认情况下,所有Windows进程都有一个线程池,这意味着滥用线程池将适用于所有Windows进程。
  2. Work items 和 thread pools 由结构表示,这增加了基于分配和写原语的执行原语的可能性。
  3. 支持多种 work item 类型 , 这意味着更多的机会 。
  4. 线程池是一个相当复杂的组件,包含内核和用户模式代码,这扩大了攻击面。

Architecture

线程池包含三个不同的工作队列,每个队列专用于不同类型的工作项。
工作线程对不同的队列进行操作,以从队列中取出工作项并执行它们。
此外,线程池还包含一个工作工厂对象(worker factory object),该对象负责管理工作线程(worker threads)。
在这里插入图片描述

基于这种架构,线程池中很少有可能被进程注入滥用的潜在区域:

  1. Worker factory
  2. Task queue
  3. I/O completion queue
  4. Timer queue

我们知道,插入到其中一个队列中的有效工作项 (a valid work item )将由工作线程执行。
除了队列之外,还可以使用作为工作线程管理器的工作工厂来接管工作线程。

Attacking Worker Factories

工作工厂是负责管理线程池(thread pool ) 工作线程(worker threads) 的 Windows对象。
它通过监视活动的(active)或阻塞的(blocking)工作线程来管理工作线程,并根据监视结果创建或终止工作线程。
工人工厂不执行任何工作项目(work items)的调度或执行;
它的存在是为了确保工作线程的数量足够。
在这里插入图片描述

内核公开了7个系统调用来与 worker factory objects 交互:

  • NtCreateWorkerFactory
  • NtShutdownWorkerFactory
  • NtQueryInformationWorkerFactory
  • NtSetInformationWorkerFactory
  • NtWorkerFactoryWorkerReady
  • NtWaitForWorkViaWorkerFactory
  • NtReleaseWorkerFactoryWorker

有了接管工作线程的目标,相关的目标将是启动例程(start routine)。
启动例程(start routine)基本上是工作线程的入口点,通常这个例程充当线程池调度器,负责退出队列并执行工作项。

启动例程可以在 worker factory 创建系统调用中控制,更有趣的是,系统调用接受一个要为其创建 worker factory 的进程句柄:
在这里插入图片描述

查看内核中系统调用的实现,我们注意到有一个验证以确保没有为当前进程以外的进程创建 worker factories:
在这里插入图片描述

一般来说,系统调用得到的参数只有一个可能的值,这有点奇怪。
默认情况下,所有进程都有一个线程池,因此,默认情况下也有一个 worker factory .

我们可以简单地利用 DuplicateHandle() API来访问属于目标进程的 worker factory ,而不是经历创建一个 worker factory 的麻烦。
在这里插入图片描述

访问现有的 worker 工厂并不能让我们控制 start 例程的值,因为这个值是常量,在对象初始化后不能自然地改变。
话虽如此,如果我们能够确定开始例程的值,我们就可以用恶意 shellcode 覆盖例程代码。

要获取工人工厂信息,可以使用 NtQueryWorkerFactoryInformation 系统调用:
在这里插入图片描述

查询系统调用仅支持的信息类是 basic worker factory information .
在这里插入图片描述

在这种情况下,这就足够了,因为基本的 worker 工厂信息包括 start 例程值:
在这里插入图片描述

获取了启动例程后,我们可以用 shellcode 覆盖例程内容 .

start 例程保证在某个时刻运行,但如果我们也能触发它的执行而不是等待它,那就更好了。
为了实现这一点,我们查看了 NtSetInformationWorkerFactory 系统调用:
在这里插入图片描述

设置系统调用比查询系统调用支持更多的信息类,最适合我们需要的是WorkerFactoryThreadMinimum信息类:
在这里插入图片描述

将最小工作线程数设置为当前正在运行的线程数 + 1,这将创建一个新的工作线程,这意味着start例程被执行:
在这里插入图片描述

有了这些,我们成功地开发了我们的第一个 Pool Party 变体:
添加链接描述
NtQueryInformationProcess : Get handle table
DuplicateHandle : Duplicate Worker Factory handle
NtQueryInformationWorkerFactory : Get Worker Factory information
WriteProcessMemory : Write shellcode to start routine
NtSetWorkerFactoryInformation : Increase worker factory minimum threads

Attacking Thread pools

在攻击线程池时,我们的目标是将工作项插入到目标进程中,因此我们关注如何将工作项插入到线程池中。
我们知道,如果我们正确地插入一个工作项,它将被工作线程执行。
我们将假设我们已经拥有对目标线程池的工人工厂(worker factory)的访问权,正如我们在前一节中证明的那样,可以通过复制工人工厂句柄(worker factory handle )来授予这种访问权。

Work Item Types

支持的工作项可以分为三种类型:

  • 常规的工作项,它们被排队API调用立即排队。
  • 异步工作项,在操作完成时排队,例如,当写文件操作完成时。
  • 计时器工作项,它由排队API调用立即排队,但在计时器到期时执行。
    在这里插入图片描述

Queue Types

对于这三种类型的工作项,也有三个队列:

  • 常规工作项排队进入任务队列,驻留在主线程池结构TP_POOL中。
  • 异步工作项排队到 I/O 完成队列,这是一个 Windows 对象。
  • 计时器工作项排队到计时器队列,也驻留在主线程池结构中。
    在这里插入图片描述

主线程池结构驻留在进程内存地址空间的用户模式中,因此可以通过内存写原语对其队列进行修改。
I/O 完成队列是一个 Windows 对象,因此该队列驻留在内核中,可以由其公开的系统调用操作。

Helper Structures

在我们深入研究每个工作项类型的排队机制之前,重要的是要注意工作项回调不是由工作线程直接执行的。
相反,每个工作项都有一个用于执行工作项回调的助手回调。排队的结构是 helper 结构。
在这里插入图片描述

Attacking Thread Pools: TP_WORK

通过查看TP_WORK工作项结构,我们发现它的助手结构是TP_TASK结构。
我们知道任务结构是插入到线程池结构中的任务队列中的。
在这里插入图片描述

负责提交 TP_WORK 工作项的 API 名为 SubmitThreadpoolWork。
沿着 SubmitThreadpoolWork 的调用链,我们到达了名为 TpPostTask 的排队 API。

TpPostTask API 负责将任务插入到任务队列中,任务队列由一个双链表表示。
它按优先级检索相应的任务队列,并将任务插入到任务队列的尾部。
在这里插入图片描述

给定目标进程的线程池结构,我们可以篡改它的任务队列以向其中注入恶意任务。
要获取目标进程的线程池结构,可以使用 NtQueryInformationWorkerFactory 。
基本的 worker 工厂信息包括启动例程的开始参数,这个开始参数本质上是一个指向 TP_POOL 结构的指针。
我们有了第二个 Pool Party 变体: 远程 TP_WORK Work Item 插入
添加链接描述

  • NtQueryInformationProcess : Get handle table
  • DuplicateHandle : Duplicate Worker Factory handle
  • NtQueryInformationWorkerFactory : Get Worker Factory information
  • ReadProcessMemory : Read TP_POOL
  • CreateThreadpoolWork : TP_WORK
  • VirtualAllocEx : Allocate TP_WORK memory
  • WriteProcessMemory : Write TP_WORK memory ; Insert TP_WORK to TP_POOL task queue .

Attacking Thread Pools: TP_IO

回顾队列类型,异步工作项排队到I/O完成队列。I/O完成队列是一个Windows对象,用作已完成I/O操作的队列。一旦I/O操作完成,通知就会插入队列中.
在这里插入图片描述

当异步工作项的操作完成时,线程池依赖于 I/O 完成队列来接收通知。

注意:微软将I/O完成队列称为I/O完成端口。这个对象本质上是一个内核队列(KQUEUE),因此为了避免混淆,我们将其称为I/O完成队列。

内核公开了8个系统调用来与I/O完成队列交互:

  • NtCreateIoCompletion
  • NtOpenIoCompletion
  • NtQueryIoCompletion
  • NtQueryIoCompletionEx
  • NtSetIoCompletion
  • NtSetIoCompletionEx
  • NtRemoveIoCompletion
  • NtRemoveIoCompletionEx

请记住,NtSetIoCompletion 系统调用用于将通知排队到队列。我们稍后会回到这个系统调用。

有了一些I/O完成背景,我们就可以直接进入异步工作项的排队机制。我们将使用 TP_IO 工作项作为示例,但请注意,相同的概念适用于其他异步工作项。

TP_IO 工作项是一个在文件操作(如读和写)完成时执行的工作项。TP_IO 工作项的辅助结构是 TP_DIRECT 结构,因此我们期望该结构排在完成队列的队列中。
在这里插入图片描述

当异步工作项排队到I/O完成队列时,我们寻找将工作项关联到线程池的 I/O 完成队列的函数。
查看 CreateThreadpoolIo 的调用链,我们找到了感兴趣的函数: TpBindFileToDirect 函数。
这个函数将文件完成队列设置为 线程池的I/O 完成队列,并将文件完成键设置为直接结构:
在这里插入图片描述

在文件对象上调用 TpBindFileToDirect 会导致文件对象的完成队列指向线程池的I/O完成队列,完成键指向直接结构。
在这里插入图片描述

此时,I/O 完成队列仍然为空,因为没有对文件进行任何操作。函数调用之后对文件的任何操作(例如writefile)都将导致完成键排在I/O完成队列中。
在这里插入图片描述

综上所述,异步工作项排队到I/O完成队列,直接结构是排队的字段。
拥有目标进程的I/O完成队列的句柄使我们能够将通知排队给它。
这个句柄可以使用DuplicateHandle API复制,类似于我们复制worker工厂句柄的方式。
这样,我们就有了 Pool Party 的第三个变体: 远程 TP_IO work item 插入
添加链接描述

  1. NtQueryInformationProcess : Get handle table
  2. DuplicateHandle : Duplicate I/O Completion queue handle
  3. CreateFile : FILE
  4. CreateThreadpoolIo : TP_IO
  5. VirtualAllocEx : Allocate TP_IO memory
  6. WriteProcessMemory : Write TP_IO memory
  7. NtSetInformationFile : Associate TP_IO with target I/O completion queue
  8. WriteFile : Queue notification to I/O completion queue

另外 4 种 Pool Party

我们还如何插入 ALPC、JOB 和 WAIT 工作项? 任何排队到I/O完成队列的有效 TP_DIRECT 结构都将被执行。
这完全取决于我们如何将 TP_DIRECT 结构排到 I/O 完成队列中。

排队的方式有以下几种:
1.利用Windows对象,类似于 TP_IO 滥用。这将涉及将对象与目标进程的I/O完成队列相关联,然后该对象上的任何操作完成都会将通知排队。
2.使用NtSetIoCompletion将通知直接放入完成队列。

考虑到这一点,我们可以通过将底层 Windows 对象与目标线程池的I/O完成队列相关联,并将其完成键设置为指向恶意工作项,
从而注入其余的异步工作项,TP_WAIT、TP_ALPC 和 TP_JOB。
最重要的是,我们可以直接注入一个恶意的 TP_DIRECT 结构,而不需要通过Windows对象来代理它,这涉及到使用NtSetIoCompletion系统调用。
这让我们能够创造另外4种 Pool Party 变体:

  • PoolParty Variant 4 – Remote TP_WAIT Work Item Insertion
  • PoolParty Variant 5 – Remote TP_ALPC Work Item Insertion
  • PoolParty Variant 6 – Remote TP_JOB Work Item Insertion
  • PoolParty Variant 7 – Remote TP_DIRECT Insertion

Attacking Thread Pools: TP_TIMER

首先,在查看 Timer 工作项的创建和提交API时,我们注意到没有提供 Timer 句柄。
提交 API SetThreadpoolTimer 接受一些定时器配置,比如DueTime,但是不清楚实际的定时器对象在哪里。
在这里插入图片描述

结果是,Timer 工作项对驻留在 Timer 队列中的现有 Timer 对象进行操作。
一旦调用了SubmitThreadpoolTimer API,就会将工作项插入到队列中,并且使用用户提供的配置配置驻留在队列中的 Timer 对象。
在这里插入图片描述

Timer 过期后,将调用一个出队列函数,该函数将工作项从队列中脱队列并执行它。
在这里插入图片描述

一般来说,计时器对象本身不支持在过期时执行回调。
只需要知道线程池使用 TP_WAIT 工作项实现它,它支持计时器。
因此,如果我们将计时器队列设置为过期,则会调用出队列函数。
现在的问题是,我们如何正确地将计时器排队到队列中?

定时器和定时器队列之间的连接器是 TP_TIMER 的 windowwendlinks 和 WindowStartLinks 字段。
为了简单起见,我们可以将这两个字段视为双链表的列表项。
在这里插入图片描述

沿着 SetThreadpoolTimer 的调用链,我们到达了名为 TppEnqueueTimer 的排队函数。
在这里插入图片描述

TppEnqueueTimer 将 TP_TIMER 的 WindowStartLinks 插入到队列 WindowStart 字段,将 windowwendlinks 插入到队列 WindowEnd 字段。
在这里插入图片描述

SetThreadpoolTimer API负责两个动作:
1.将计时器工作项排队到计时器队列。
2.配置驻留在队列中的定时器对象。

这两个操作的结果是,一旦计时器对象过期,将执行出队列函数,出队列并执行排队计时器工作项。
给定目标进程的线程池结构,我们可以篡改它的计时器队列,将恶意计时器工作项注入其中。
在排队之后,我们需要设置队列用来过期的计时器对象。
设置计时器需要一个句柄,这样的句柄可以使用DuplicateHandle API复制。
这样,我们就有了Pool Party 的第八种变体: 远程 TP_TIMER 工作项插入
添加链接描述

  1. NtQueryInformationProcess : Get handle table
  2. DuplicateHandle : Duplicate Worker Factory handle
  3. NtQueryInformationWorkerFactory : Get Worker Factory information
  4. ReadProcessMemory : Read TP_POOL
  5. CreateThreadpoolTimer : TP_TIMER
  6. VirtualAllocEx : Allocate TP_TIMER memory
  7. WriteProcessMemory : Write TP_TIMER memory ; Insert TP_TIMER to TP_POOL timer queue
  8. DuplicateHandle : Queue queue timer handle
  9. NtSetTimer2 : Set queue timer to expire

更令人惊讶的是,在设置计时器后,攻击者可以退出进程并从系统中删除其身份。
结果,系统看起来是干净的,并且恶意代码只在计时器结束时激活。

Tested EDR Solutions

作为研究过程的一部分,每个Pool Party 变体都针对五种领先的EDR解决方案进行了测试,包括:

  • Palo Alto Cortex
  • SentinelOne EDR
  • CrowdStrike Falcon
  • Microsoft Defender For Endpoint
  • Cybereason EDR

我们达到了100%的成功率,因为没有一个edr能够检测或阻止Pool Party攻击。
我们向每个供应商报告了这些发现,并相信他们正在进行更新以更好地检测这些类型的技术。

值得注意的是,虽然我们已经尽了最大的努力来测试我们能接触到的EDR产品,但对我们来说,测试市场上的每一种产品是不可行的。
通过向安全社区提供这些信息,我们希望最大限度地减少恶意参与者利用这些技术的能力,并为EDR供应商和用户提供他们自己立即采取行动所需的知识。

Key Takeaways

我们认为,基于这项研究的发现,有一些重要的结论:
1.尽管edr已经得到了发展,但目前大多数解决方案所使用的检测方法无法普遍检测出像我们在这里开发的这些新进程注入技术。
虽然我们的研究表明了我们是如何能够专门滥用线程池的,但恶意行为者无疑会找到以类似方式利用的其他特性。
我们认为,EDR供应商开发和实施通用检测方法来主动防御这些可能性至关重要。

2.我们还认为,对于单个组织来说,加强对异常检测的关注是很重要的,而不是完全信任仅仅基于其身份的过程。
我们的研究表明,代表受信任进程执行代码可能不会被EDR检测到。
这强调了深入检查的重要性,以确保这些进程所进行的行动的合法性。

Conclusion

尽管现代edr已经发展到可以检测已知的进程注射技术,但我们的研究已经证明,仍然有可能开发出无法检测到的新技术,并有可能产生毁灭性的影响。
复杂的威胁参与者将继续探索新的和创新的进程注入方法,安全工具供应商和从业者必须积极主动地防御它们。

为了帮助减轻这些技术的潜在影响,我们有:

  • 负责任地向微软、Palo Alto Networks、CrowdStrike、SentinelOne和Cybereason披露了我们的研究结果。
  • 在这里和我们最近的黑帽演讲中,与更广泛的安全社区公开分享我们的研究,以提高对这些问题的认识。
  • 提供了一个研究 GitHub 存储库,其中包含有关我们发现的详细信息,以作为进一步研究和开发的基础。
  • 在SafeBreach平台中添加了原始攻击内容,使我们的客户能够验证针对这些流的安全控制,并显着降低风险。

有关这项研究的更深入信息,请:

  • 如果您是当前的SafeBreach客户,请联系您的客户成功代表
  • 安排与安全漏洞专家进行一对一的讨论
  • 媒体咨询请联系凯塞林公关

About the Researcher

阿隆·莱维耶夫是一位自学成才的安全研究员,有着多种背景。阿隆的职业生涯始于一名蓝队操作员,他专注于网络安全的防御方面。
随着他对研究的热情日益高涨,阿隆加入了SafeBreach,担任安全研究员。他的主要兴趣包括操作系统内部、逆向工程和漏洞研究。
在加入网络安全领域之前,阿隆是一名专业的巴西柔术运动员,在那里他赢得了几个世界和欧洲冠军。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值