要点
1. Avast发现了一种在野外利用的零日漏洞,该漏洞存在于appid.sys AppLocker驱动程序中,此前未知。
2. 由于Avast及时报告,微软在二月的“补丁星期二”更新中将此漏洞标识为CVE-2024-21338。
3. 攻击活动由臭名昭著的Lazarus集团策划,最终目标是建立内核 读/写 原语。
4. 这个原语使Lazarus能够在其数据仅根工具包的更新版本中执行直接内核对象操作,此前的版本曾由ESET和AhnLab进行分析(https://www.virusbulletin.com/uploads/pdf/conference/vb2022/papers/VB2022-Lazarus-and-BYOVD-evil-to-the-Windows-core.pdf)、(https://download.ahnlab.com/global/brochure/Analysis-Report-on-Lazarus-Groups-Rootkit-Attack-Using-BYOVD.pdf)。
5. 在彻底反向工程这个更新的根工具包变种后,Avast发现在功能和隐秘性方面都取得了实质性进展,新增了四种新的根工具包技术和三种更新的技术。
6. 一个重要的进展是,根工具包现在采用了一种新的句柄表条目操纵技术,试图暂停与Microsoft Defender、CrowdStrike Falcon和HitmanPro相关的PPL(受保护进程轻量级)受保护进程。
7. 另一个重大进展是利用零日漏洞,Lazarus以前利用的是更为喧闹的BYOVD(自带可漏洞驱动程序)技术来越过管理员到内核的边界。
8. Avast的调查还恢复了导致部署根工具包的感染链的大部分内容,从而发现了Lazarus归因的新远程访问木马(RAT)。
9. 关于RAT和初始感染向量的技术细节将在后续的博客文章中发布,计划与我们的Black Hat Asia 2024简报一同发布(https://www.blackhat.com/asia-24/briefings/schedule/#from-byovd-to-a–day-unveiling-advanced-exploits-in-cyber-recruiting-scams-37786)
简介
在Windows安全领域,管理员和内核之间存在着微妙的界限。微软的安全服务标准(https://www.microsoft.com/en-us/msrc/windows-security-servicing-criteria)长期以来一直强调“管理员到内核不是安全边界”,这意味着微软保留了自行决定修补管理员到内核漏洞的权利。
因此,Windows安全模型并不保证它会阻止管理员级别的攻击者直接访问内核。这不仅仅是理论上的关注点。实际上,具有管理员权限的攻击者(https://www.rapid7.com/blog/post/2021/12/13/driver-based-attacks-past-and-present/#:~:text=Known%20usage%20in%20the%20wild)经常通过利用已知的易受攻击的驱动程序实现内核级访问,这种技术称为BYOVD(https://www.welivesecurity.com/2022/01/11/signed-kernel-drivers-unguarded-gateway-windows-core/)(携带自己的易受攻击的驱动程序)。
然而,微软并没有放弃保护管理员到内核的边界。相反,它在加固这个边界方面取得了很大的进展。深度防御保护,如DSE(https://learn.microsoft.com/en-us/windows-hardware/drivers/install/driver-signing)(驱动程序签名强制执行)或HVCI(https://learn.microsoft.com/en-us/windows-hardware/design/device-experiences/oem-hvci-enablement)(Hypervisor-Protected Code Integrity),使得攻击者越来越难以执行自定义代码在内核中,迫使大多数攻击者转向数据攻击(他们仅通过读写内核内存来实现其恶意目的)。
其他防御措施,如驱动程序屏蔽(https://learn.microsoft.com/en-us/windows/security/application-security/application-control/windows-defender-application-control/design/microsoft-recommended-driver-block-rules),迫使攻击者转向利用较少知名的易受攻击的驱动程序,导致攻击复杂性增加。尽管这些防御措施尚未达到我们可以正式称之为管理员到内核的安全边界的程度(BYOVD攻击仍然可行,因此称之为安全边界只会让用户产生错误的安全感),但它们显然代表了正确方向上的步骤。
从攻击者的角度来看,从管理员到内核的跨越开启了全新的可能性领域(https://github.com/wavestone-cdt/EDRSandblast)。通过内核级别访问,攻击者可能破坏安全软件、隐藏感染指标(包括文件、网络活动、进程等)、禁用内核模式遥测、关闭缓解措施等。
此外,由于PPL(受保护进程轻量级)(https://learn.microsoft.com/en-us/windows/win32/services/protecting-anti-malware-services-)的安全性依赖于管理员到内核的边界,我们假设的攻击者还可以修改受保护进程或向任意进程添加保护。如果lsass受到RunAsPPL保护,那么绕过PPL(https://learn.microsoft.com/en-us/windows-server/security/credentials-protection-and-management/configuring-additional-lsa-protection)可能会使攻击者能够转储否则无法到达的凭据,这可能尤为强大。
关于攻击者可能想要通过内核级别访问实现的具体目标的更多示例,请继续阅读本博客-在后半部分,我们将深入探讨FudModule根工具包中实现的所有技术。
活用现有资源:易受攻击的驱动程序版本
随着看似不断增长的攻击者寻求滥用前面提到的一些内核功能,防御者别无选择,只能大力搜索驱动程序漏洞。因此,希望针对防御良好的网络的攻击者也必须提高自己的水平,如果他们希望避免被发现的话。我们可以广泛地将管理员到内核驱动程序漏洞分为三类,每一类都代表着攻击难度和隐蔽性之间的权衡。
N-Day BYOVD Exploits
在最简单的情况下,攻击者可以利用 BYOVD 来利用公开已知的 n-day 漏洞。这很容易实现,因为有大量针对各种漏洞的公开概念证明攻击。然而,这也相对容易检测,因为攻击者必须首先将已知的易受攻击的驱动程序放入文件系统,然后将其加载到内核中,从而产生两个很好的检测机会。更重要的是,一些系统可能启用了 Microsoft 的易受攻击的驱动程序黑名单(https://learn.microsoft.com/en-us/windows/security/application-security/application-control/windows-defender-application-control/design/microsoft-recommended-driver-block-rules#microsoft-vulnerable-driver-blocklist),这将阻止一些最常见的易受攻击的驱动程序加载。以前版本(https://www.virusbulletin.com/uploads/pdf/conference/vb2022/papers/VB2022-Lazarus-and-BYOVD-evil-to-the-Windows-core.pdf)(https://www.virusbulletin.com/uploads/pdf/conference/vb2022/papers/VB2022-Lazarus-and-BYOVD-evil-to-the-Windows-core.pdf)的 FudModule rootkit 可以放在这个类别中,最初利用 dbutil_2_3.sys (https://www.virustotal.com/gui/file/0296e2ce999e67c76352613a718e11516fe1b0efc3ffdb8918fc999dd76a73a5/detection)中已知的漏洞,然后在后续版本中转向 ene.sys(https://www.virustotal.com/gui/file/175eed7a4c6de9c3156c7ae16ae85c554959ec350f1c8aaa6dfe8c7e99de3347)。
零日 BYOVD 攻击
在更复杂的情况下,攻击者将利用 BYOVD 来利用第三方签名驱动程序中的零日漏洞。自然而然,这需要攻击者首先发现这样一个零日漏洞,这可能一开始看起来是一项艰巨的任务。然而,请注意,任何一个签名驱动程序中的可利用漏洞都可以,而且不幸的是,低质量的第三方驱动程序并不少见。因此,发现这样一个漏洞的难度可能没有最初看起来那么高。也许只需扫描一组驱动程序以寻找已知的漏洞模式就足够了,正如 Carbon Black 研究人员最近所做的,他们使用大量静态分析方法,在 200 多个签名驱动程序中发现了 34 个独特的漏洞(https://blogs.vmware.com/security/2023/10/hunting-vulnerable-kernel-drivers.html)
这种零日 BYOVD 攻击比 n day攻击更隐蔽,因为防御者不再能依靠已知易受攻击驱动程序的哈希进行检测。然而,仍然存在一些检测机会,因为加载随机驱动程序代表了一个可疑事件,可能需要进行更深入的调查。作为此类别攻击的一个示例,考虑间谍软件供应商 Candiru(https://decoded.avast.io/janvojtesek/the-return-of-candiru-zero-days-in-the-middle-east/),我们发现他们正在利用 hw.sys(https://www.virustotal.com/gui/file/6a4875ae86131a594019dec4abd46ac6ba47e57a88287b814d07d929858fe3e5) 中的零日漏洞来进行其浏览器攻击链的最终提权阶段。
超越 BYOVD
最后,管理员到内核的终极目标是通过利用已知已安装在目标机器上的驱动程序的零日漏洞来超越 BYOVD。为了尽可能使攻击具有普适性,这里最明显的目标是已经是操作系统一部分的内置 Windows 驱动程序。
在这样一个驱动程序中发现一个可利用的漏洞要比前面 BYOVD 方案更具挑战性,原因有两个。首先,可能的目标驱动程序数量要小得多,导致攻击面大大减小。第二,内置驱动程序的代码质量可能比随机的第三方驱动程序高,使得漏洞难以发现。值得注意的是,虽然修补程序往往无法阻止 BYOVD 攻击(即使供应商对其驱动程序进行了修补,攻击者仍然可以利用旧版本的未修补驱动程序),但对内置驱动程序进行修补将使漏洞无法再用于此类零日攻击。
如果一个攻击者尽管面临所有这些障碍,还是设法利用内置驱动程序的零日漏洞,他们将获得一种通过标准 BYOVD 利用无法匹敌的隐蔽性。通过利用这样的漏洞,攻击者在某种程度上就像是在利用已有资源,无需带、放或加载任何自定义驱动程序,这使得内核攻击真正无文件化成为可能。这不仅可以避开大多数检测机制,还可以在已启用驱动程序白名单的系统上进行攻击(这可能有点讽刺,因为 CVE-2024-21338 涉及一个 AppLocker 驱动程序)。(https://www.crowdstrike.com/cybersecurity-101/living-off-the-land-attacks-lotl/)
虽然我们只能推测 Lazarus 选择采用这种超越 BYOVD 的第三种方法来跨越管理员到内核边界的动机,但我们相信,隐蔽性是他们的主要动机。考虑到他们的臭名昭著程度,他们必须在有人揭露他们当前使用的 BYOVD 技术时不断更换漏洞。也许他们还推理说,通过超越 BYOVD,他们可以通过保持更长时间的未被发现来最小化更换的需要。
CVE-2024-21338
就零日漏洞而言,CVE-2024-21338 相对于理解和利用都比较直接。漏洞位于 appid.sys 中的 IOCTL(输入输出控制)分发器中,该驱动程序是 Windows 内置的应用程序白名单(https://www.tiraniddo.dev/2019/11/the-internals-of-applocker-part-1.html)技术 AppLocker 的核心驱动程序(https://learn.microsoft.com/en-us/windows/security/application-security/application-control/windows-defender-application-control/applocker/applocker-overview)。易受攻击的控制代码 0x22A018 被设计用于计算可执行映像文件的智能哈希。这个 IOCTL 提供了一些灵活性,允许调用者指定驱动程序应该如何查询和读取哈希文件。问题在于,这种灵活性是通过期望 IOCTL 输入缓冲区中引用的两个内核函数指针实现的:一个包含一个回调指针以查询哈希文件的大小,另一个包含一个回调指针以读取要哈希的数据。
由于用户模式通常不会处理内核函数指针,这种设计表明 IOCTL 可能最初是设计为从内核调用的。事实上,虽然我们没有找到任何合法的用户模式调用者,但 IOCTL 确实会被其他 AppLocker 驱动程序调用。例如,applockerfltr.sys 中有一个 ZwDeviceIoControlFile 调用,传递了 SmpQueryFile 和 SmpReadFile 作为回调指针。除此之外,appid.sys 本身也使用了这个功能,传递了 AipQueryFileHandle 和 AipReadFileHandle(这基本上只是 ZwQueryInformationFile 和 ZwReadFile 的包装器)。
尽管有这种设计,但易受攻击的 IOCTL 仍然可以从用户空间访问,这意味着用户空间攻击者可以滥用它,从而基本上欺骗内核调用任意指针。更重要的是,攻击者还部分控制了传递给调用的回调函数的第一个参数所引用的数据。这提供了一个理想的利用场景,允许攻击者对第一个参数具有高度控制权的情况下调用任意内核函数。
具有触发漏洞的 WinDbg 会话,可追踪到任意回调调用。 请注意,攻击者控制要调用的函数指针(本会话中为 0xdeadbeefdeadbeef)和第一个参数(0xbaadf00dbaadf00d)指向的数据
虽然利用听起来很简单,但请注意,此漏洞允许攻击者调用哪些指针存在一些限制。
当然,在存在 SMEP(Supervisor Mode Execution Prevention)(https://j00ru.vexillium.org/2011/06/smep-what-is-it-and-how-to-beat-it-on-windows/)的情况下,攻击者不能仅提供用户模式的 shellcode 指针。更重要的是,回调调用是一种间接调用,可能受到 kCFG(Kernel Control Flow Guard)(https://learn.microsoft.com/en-us/windows/win32/secbp/control-flow-guard)的保护,要求提供的内核指针代表有效的 kCFG 调用目标。在实践中,这并不会阻止利用,因为攻击者可以找到一些符合 kCFG 的 gadget 函数,将其转换为另一种基元,例如(有限的)读/写。对 IOCTL 输入缓冲区还有一些其他约束必须解决才能达到易受攻击的回调调用。但是,这些约束相对较容易满足,因为攻击者只需要伪造一些内核对象并提供正确的值,以便 IOCTL 处理程序通过所有必要的检查,同时不会使内核崩溃。
易受攻击的 IOCTL 通过名为 \Device\AppId 的设备对象公开(https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/defining-i-o-control-codes)。分解 0x22A018 控制代码并提取 RequiredAccess 字段后发现,调用它需要具有写访问权限的句柄。检查设备的 ACL(访问控制列表;见下面的截图),其中包含本地服务、管理员和 appidsvc 条目。虽然管理员条目不授予写访问权限,但本地服务条目确实授予此权限。因此,为了更准确地描述 CVE-2024-21338,我们应该将其标记为本地服务到内核而不是管理员到内核。值得注意的是,appid.sys 可能会创建两个额外的设备对象,即 \Device\AppidEDPPlugin 和 \Device\SrpDevice。尽管这些对象的 ACL 更加宽松,但通过它们无法访问易受攻击的 IOCTL 处理程序,因此对于利用目的来说它们是不相关的。
\Device\AppId 的访问控制条目显示,虽然允许本地服务进行写访问,但管理员却不允许
由于本地服务帐户(https://learn.microsoft.com/en-us/windows/win32/services/localservice-account)与管理员相比权限较低,这也使得该漏洞的影响略高于标准的管理员到内核漏洞。这可能是微软将 CVE 归类为特权要求:低的原因,考虑到本地服务进程并不总是必须以更高的完整性级别运行。然而,对于本博客的目的,我们仍然选择主要将 CVE-2024-21338 视为管理员到内核漏洞,因为我们认为这更好地反映了它在野外的使用方式——Lazarus 已经以提升的特权运行,然后在调用 IOCTL 之前模拟本地服务帐户。
漏洞是在 Win10 1703(RS2/15063)引入的,当时首次实现了 0x22A018 IOCTL 处理程序。旧版本不受影响,因为它们不支持易受攻击的 IOCTL。有趣的是,Lazarus 的利用如果遇到早于 Win10 1809(RS5/17763)的版本则会退出,完全忽略了三个完全易受攻击的 Windows 版本。至于较新版本,漏洞一直延伸到最新版本,包括 Win11 23H2。IOCTL 有一些轻微的更改,包括在输入缓冲区中预期的额外参数,但没有任何防止利用的东西。
我们开发了一个自定义的 PoC(概念验证)漏洞利用,并在 2023 年 8 月作为漏洞报告的一部分提交给了微软,这导致了 CVE-2024-21338 的一项公告(https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-21338),在 2 月的 Patch Tuesday 更新中解决了该漏洞。更新通过在 IOCTL 处理程序中添加 ExGetPreviousMode 检查来解决漏洞(见下面的补丁)。这旨在防止用户模式发起的 IOCTL 触发任意回调。
修补后的 IOCTL 处理程序。 如果启用功能 2959575357,尝试使用 PreviousMode==UserMode 调用 IOCTL 应立即导致 STATUS_INVALID_DEVICE_REQUEST,甚至无法到达 AipSmartHashImageFile
尽管这个漏洞可能勉强符合微软的安全服务标准,但我们认为修补是正确的选择,并要感谢微软最终解决了这个问题。修补无疑会扰乱 Lazarus 的攻击行动,迫使他们要么找到一个新的管理员到内核的零日漏洞,要么恢复使用 BYOVD 技术。虽然发现一个管理员到内核的零日漏洞可能不像发现更具吸引力的攻击面(如标准用户到内核,甚至沙箱到内核)那么具有挑战性,但我们认为找到一个仍然需要 Lazarus 投入大量资源,这可能会使他们的注意力从攻击一些其他不幸的目标上转移。
开发
Lazarus 的利用开始于初始化阶段,该阶段对利用和 rootkit(两者已编译到同一个模块中)进行一次性设置。初始化首先通过动态解析所有必要的 Windows API 函数开始,然后对 PEB.BeingDebugged 进行了简单的反调试检查。接下来,利用检查构建号,看看是否运行在支持的 Windows 版本上。如果是,则加载针对当前构建定制的硬编码常量。有趣的是,常量的选择有时取决于更新版本号(UBR),显示了在确保代码在各种目标机器上顺利运行方面的高度奉献精神。
反编译的代码片段,加载特定于版本的硬编码常量。 此特定示例包含 Win10 1809 的偏移量和系统调用号
初始化过程继续通过调用 NtQuerySystemInformation 使用 SystemModuleInformation 类泄漏三个内核模块的基址:ntoskrnl、netio 和 fltmgr。当前执行线程的 KTHREAD 地址也以类似的方式泄漏,通过复制当前线程伪句柄,然后使用 SystemExtendedHandleInformation 系统信息类找到相应的内核对象地址。最后,利用漏洞手动将 ntoskrnl 映像加载到用户地址空间中,只是为了扫描一些感兴趣的函数的相对虚拟地址(RVAs)。
由于 appid.sys 驱动程序不必已加载到目标机器上,因此利用可能首先需要自行加载它。它选择以间接方式实现这一点,通过向一个特定的 AppLocker 相关 ETW(Windows 事件跟踪)提供程序写入事件。一旦加载了 appid.sys,利用通过使用 ThreadImpersonationToken 线程信息类对 NtSetInformationThread 进行直接 syscall 来模拟本地服务账户。通过模拟本地服务,它现在可以获得对 \Device\AppId 的读/写句柄。有了这个句柄,利用最终准备好 IOCTL 输入缓冲区,并使用 NtDeviceIoControlFile syscall 触发漏洞。
整个漏洞利用过程中大量使用了直接系统调用
Exploit的制作方式使得易受攻击的回调函数实际上是一个执行从IOCTL输入缓冲区到任意目标地址的64位复制的“gadget”。选择的地址是为了破坏当前线程的 PreviousMode(https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/previousmode)。通过确保IOCTL输入缓冲区中对应的字节为零,复制将清除 PreviousMode 字段,有效地将其值解释为 KernelMode。像这样针对 PreviousMode (https://research.nccgroup.com/2020/05/25/cve-2018-8611-exploiting-windows-ktm-part-5-5-vulnerability-detection-and-a-better-read-write-primitive/#previousmode-abuse:~:text=into%20PreviousMode%20further.-,PreviousMode%20%E2%80%93%20a%20%22god%20mode%22%20primitive%3F,-PreviousMode%20on%2064)的目标化是一种广受欢迎的利用技术,因为在 KTHREAD 结构中损坏此一个字节可以绕过 syscalls 内部的内核模式检查,比如 NtReadVirtualMemory 或 NtWriteVirtualMemory,从而允许用户模式攻击者读取和写入任意内核内存。请注意,尽管在某些 Windows Insider Builds 上对这种技术进行了缓解(https://x.com/GabrielLandau/status/1597001955909697536),但此缓解在撰写本文时尚未普遍可用。
有趣的是,Exploit 可能会尝试两次触发易受攻击的 IOCTL。这是由于 Win11 22H2 中添加了一个额外的参数。因此,更新版本的 IOCTL 处理程序期望输入缓冲区的大小为0x20字节,而之前期望的大小只有0x18。Exploit 不会选择当前构建的正确输入缓冲区大小,而只是尝试两次调用 IOCTL:首先使用输入缓冲区大小为0x18,然后(如果不成功)使用0x20。这是一个有效的方法,因为 IOCTL 处理程序的第一个动作是检查输入缓冲区的大小,如果与期望的大小不匹配,它将立即返回 STATUS_INVALID_PARAMETER。
为了检查是否成功,Exploit 使用 NtWriteVirtualMemory syscall,尝试读取当前线程的 PreviousMode(Lazarus 避免使用 NtReadVirtualMemory,稍后会详细介绍)。如果 Exploit 成功,syscall 应该返回 STATUS_SUCCESS,并且泄漏的 PreviousMode 字节应该等于0(表示 KernelMode)。否则,syscall 应该返回错误状态码,因为在没有损坏 PreviousMode 的情况下不可能读取内核内存。
在我们的 Exploit 分析中,我们有意选择了省略一些关键细节,比如回调 gadget 函数的选择。这个决定是为了在帮助防御者检测方面取得平衡,但又不使利用过于普遍可用。对于那些需要更多信息以进行防御的人,我们可以根据具体情况分享更多细节。
FudModule Rootkit
整个管理员到内核的利用的目标是破坏当前线程的 PreviousMode。这样就可以实现强大的内核读/写基元,受影响的用户模式线程可以使用 Nt(Read|Write)VirtualMemory syscalls 读取和写入任意内核内存。拥有了这个基元,FudModule rootkit 使用直接内核对象操作(DKOM)技术来破坏各种内核安全机制。值得重申的是,FudModule 是一个仅限数据的 rootkit,这意味着它完全从用户空间执行,所有的内核篡改都是通过读/写基元完成的。
FudModule rootkit 的最初几个变种由AhnLab和ESET研究团队分别于2022年9月发现,两者都发表了详细的分析报告。该 rootkit 被命名为 FudModule.dll 字符串,该字符串用作其导出表中的名称。虽然这个特征现在已经不存在了,但毫无疑问,我们发现的是同一 rootkit 的更新版本。AhnLab 的报告(https://download.ahnlab.com/global/brochure/Analysis-Report-on-Lazarus-Groups-Rootkit-Attack-Using-BYOVD.pdf)记录了2022年初的一个样本,该样本采用了七个仅限数据的 rootkit 技术,并通过对 ene.sys(https://www.virustotal.com/gui/file/175eed7a4c6de9c3156c7ae16ae85c554959ec350f1c8aaa6dfe8c7e99de3347) 的 BYOVD 漏洞进行利用启用。ESET 的报告(https://www.virusbulletin.com/uploads/pdf/conference/vb2022/papers/VB2022-Lazarus-and-BYOVD-evil-to-the-Windows-core.pdf)审查了稍早一些的 2021 年末的一个变种,也具有七个 rootkit 技术,但是利用了 dbutil_2_3.sys (https://www.virustotal.com/gui/file/0296e2ce999e67c76352613a718e11516fe1b0efc3ffdb8918fc999dd76a73a5/detection)中的不同 BYOVD 漏洞。相比之下,我们的发现涉及到一个包含九种 rootkit 技术的样本,并利用了一个之前未知的管理员到内核漏洞。在这九种技术中,有四种是新的,三种是改进的,而两种保持与之前变种相同。这使得原始七种技术中有两种已被弃用,并不再出现在最新的变种中。
每个 rootkit 技术都被分配一个位,范围从 0x1 到 0x200(当前变种中留出了未使用的 0x20 位)。FudModule 按照分配的位的升序顺序依次执行这些技术。这些位用于报告每个技术的成功与否。在执行过程中,FudModule 将构造一个整数值(在下面的反编译中命名为 bitfield_techniques),其中只设置了成功执行的技术所对应的位。最终,这个整数会被写入一个名为 tem1245.tmp 的文件中,报告 rootkit 的成功情况。有趣的是,我们没有在任何其他 Lazarus 样本中找到这个文件名的引用,这表明该文件仅通过手动键盘活动进行检查,可能是通过 RAT(远程访问木马)命令。这支持了我们的信念,即 FudModule 仅与 Lazarus 的其他恶意软件生态系统松散集成,Lazarus 非常谨慎地使用这个 rootkit,只在适当的情况下根据需求部署它。
Rootkit 的“主要”功能,执行各个 Rootkit 技术。 请注意缺少的 0x20 技术
基于大量的更新,看来 FudModule 仍在积极开发中。最新的变种似乎更加健壮,避免了一些早期变种中可能存在的问题实践。由于一些技术针对未记录的内核内部机制进行操作,这是我们之前未曾遇到过的,我们相信 Lazarus 必须在进行他们自己的内核研究。此外,尽管这个 rootkit 显然在技术上是复杂的,我们仍然发现了一些零零散散的错误。这些错误可能会限制 rootkit 的预期功能,甚至在适当的条件下导致内核 bug 检查。虽然我们发现其中一些错误非常有趣,而且很愿意分享详细信息,但我们不愿意将免费的漏洞报告提供给威胁行为者,所以我们暂时会保留它们,并在以后可能分享一些信息,如果这些错误被修复的话。
有趣的是,FudModule 在读写内核内存时都利用了 NtWriteVirtualMemory syscall,消除了调用 NtReadVirtualMemory 的需要。这利用了这样一个特性,即当限制在单一虚拟地址空间时,NtReadVirtualMemory 和 NtWriteVirtualMemory 基本上是相对的操作,关于源 Buffer 和目标 BaseAddress 参数(http://undocumented.ntinternals.net/index.html?page=UserMode%2FUndocumented%20Functions%2FMemory%20Management%2FVirtual%20Memory%2FNtWriteVirtualMemory.html)的值。换句话说,写入内核内存可以被视为从用户模式的 Buffer 写入到内核模式的 BaseAddress,而从内核内存读取则可以通过交换参数来实现,即从内核模式的 Buffer 写入到用户模式的 BaseAddress。Lazarus 的实现利用了这一点,这似乎是一个有意的设计决定,因为大多数开发人员可能更喜欢使用 NtReadVirtualMemory 来读取内核内存,而使用 NtWriteVirtualMemory 来写入内核内存。我们只能猜测为什么 Lazarus 选择了这种方法,但这可能是又一个增强隐蔽性的特性。通过他们的实现,他们只需要使用一个可疑的 syscall,而不是两个,这可能会减少检测机会的数量。
调试打印
在我们深入讨论实际的 rootkit 技术之前,有一件值得讨论的事情。令我们最初感到惊讶的是,Lazarus 在编译的代码中留下了一些纯文本的调试打印。对于恶意软件研究人员来说,这样的打印通常是最好的事情之一,因为它们往往会显著加速反向工程过程。然而,在这种情况下,一些打印产生了相反的效果,有时甚至让我们怀疑自己是否正确理解了代码。
举个例子,让我们提一下字符串“get rop function addresses failed”。假设“rop”代表返回导向编程,在利用的上下文中,这个字符串在理论上是完全合理的,如果不是因为事实上在 exploit 中没有一个返回地址被破坏。
在 rootkit 中找到的纯文本调试字符串。 疫苗一词用于指安全软件
虽然使用英语编写,但调试字符串表明它们的作者不是以英语为母语的人,有时甚至暗示其源自韩语。这在 rootkit 中频繁使用术语“vaccine”时最为明显。起初,这让我们感到困惑,因为不清楚“vaccine”如何与 rootkit 的功能相关联。然而,很快就变得明显,该术语用于指代安全软件(https://medium.com/s2wblog/detailed-analysis-of-darkgate-investigating-new-top-trend-backdoor-malware-0545ecf5f606#:~:text=use%20are%20different.-,Vaccine%20Detection,-DarkGate%20detects%20installed)。这可能源自于“바이러스 백신”(病毒疫苗)这一常见的韩语翻译(https://translate.google.com/?sl=en&tl=ko&text=antivirus&op=translate),这是一个字面意思为“病毒疫苗”的复合词。请注意,即使是朝鲜的“自家”防病毒软件也被称为 SiliVaccine(https://research.checkpoint.com/2018/silivaccine-a-look-inside-north-koreas-anti-virus/),据我们所知,其他语言(如日语)不会像这样使用“vaccine”这个术语。此外,这并不是韩语威胁行为者第一次使用这个术语。例如,安全公司AhnLab最近关于 Kimsuky 的报告提到了以下显著的命令(https://asec.ahnlab.com/en/59387/):
cmd.exe /U /c wmic /namespace:\\root\securitycenter2 path antivirusproduct get displayname > vaccine.txt
还有一个谜团是缩写“pvmode”,我们认为它指的是“PreviousMode”。对“pvmode”的谷歌搜索结果是零,我们怀疑大多数英语使用者会选择不同的缩写,比如“prvmode”或“prevmode”。然而,在与语言专家协商后,我们了解到,对于韩语使用者来说,使用“pvmode”这个缩写也是不寻常的。
最后,还有一个调试消息是“disableV3Protection passed”。从上下文来看,“V3”这个相当通用的术语指的是AhnLab V3 Endpoint Security。考虑到地缘政治局势,朝鲜的黑客组织可能对韩国的AhnLab非常熟悉,因此他们内部使用这样一个不太具体的缩写是完全合理的。
0x01 – 注册表回调
首先的 rootkit 技术旨在处理注册表回调(https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/filtering-registry-calls)。这是一个已记录的 Windows 机制,允许安全解决方案监视注册表操作。安全解决方案的内核模式组件可以调用 CmRegisterCallbackEx 例程来注册回调,每当系统上执行注册表操作时,就会通知该回调。更重要的是,由于回调是同步调用的,即在实际操作执行之前(或之后),回调甚至可以阻止或修改禁止/恶意操作。
FudModule 在这里的目标是移除现有的注册表回调,从而破坏依赖该机制的安全解决方案。
回调的移除本身是通过直接修改内核管理的一些内部数据结构来执行的。这在以前的版本中也是如此,正如 ESET(https://www.virusbulletin.com/uploads/pdf/conference/vb2022/papers/VB2022-Lazarus-and-BYOVD-evil-to-the-Windows-core.pdf) 和 AhnLab (https://download.ahnlab.com/global/brochure/Analysis-Report-on-Lazarus-Groups-Rootkit-Attack-Using-BYOVD.pdf)所记录的那样。在那里,rootkit 找到了 nt!CallbackListHead 的地址(其中包含所有现有注册表回调的双向链接的循环列表),然后通过将其指向自身来简单地清空它。
在 FudModule 的当前版本中,这个技术得到了改进,以留下一些选定的回调,可能使 rootkit 更隐匿。这个更新的版本开始与以前的版本相同:通过找到 nt!CallbackListHead 的地址。这是通过解析 CmUnRegisterCallback(此解析通过迭代内存中 ntoskrnl 的导出表,通过名称执行)来完成的,然后在函数体中扫描 lea rcx,[nt!CallbackListHead] 指令,并从指令的操作码中提取的偏移量计算最终地址。
有了 nt!CallbackListHead 地址,FudModule 可以遍历注册表回调链表。它检查每个条目,并确定回调例程是否在 ntoskrnl.exe、applockerfltr.sys 或 bfs.sys 中实现。如果是,则保持回调不变。否则,rootkit 用 ObIsKernelHandle 的指针替换回调例程指针,然后继续取消链接回调条目。
0x02 – 对象回调
对象回调(https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-obregistercallbacks)允许驱动程序在响应线程、进程和桌面句柄操作时执行自定义代码。它们通常用于自我防御,因为它们代表了一种方便的方式来保护关键进程免受篡改。由于保护是在内核级别执行的,这应该可以防止甚至提升了权限的攻击者,只要他们保持在用户模式下。另外,对象回调也非常有用于监视和检测可疑活动。
无论用例如何,都可以使用 ObRegisterCallbacks 例程设置对象回调。FudModule 自然会尝试做完全相反的事情:即删除所有已注册的对象回调。这可以让它绕过自我防御机制,并规避基于对象回调的检测/遥测。
自上一版本以来,这个 rootkit 技术的实现保持不变,因此不需要详细说明。首先,rootkit 扫描 ObGetObjectType 例程的主体以获取 nt!ObTypeIndexTable 的地址。其中包含一个指向 _OBJECT_TYPE 结构的指针数组,每个结构代表一个不同的对象类型,例如 Process、Token 或 SymbolicLink。FudModule 遍历这个数组(跳过前两个具有特殊意义的元素),并检查每个 _OBJECT_TYPE.CallbackList,其中包含为特定对象类型注册的对象回调的双向链接列表。然后,rootkit 通过使每个节点的前向和后向指针指向自身来清空 CallbackList。
0x04 – 进程、线程和映像内核回调
这个下一个 rootkit 技术旨在禁用另外三种类型的内核回调:进程、线程和映像回调。顾名思义,它们用于在创建新进程(https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddk/nf-ntddk-pssetcreateprocessnotifyroutine)、生成新线程(https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddk/nf-ntddk-pssetcreatethreadnotifyroutine)或加载新映像(https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddk/nf-ntddk-pssetloadimagenotifyroutine)(例如加载到进程中的 DLL)时执行自定义内核代码。这些回调非常有用于检测恶意活动。例如,进程回调允许 AV 和 EDR 在要创建的每个新进程上执行各种检查。注册这些回调非常简单。所需的只是将新的回调例程作为参数传递给 PsSetCreateProcessNotifyRoutine、PsSetCreateThreadNotifyRoutine 或 PsSetLoadImageNotifyRoutine。这些例程也有它们更新的 Ex 变体,甚至在 PsSetCreateProcessNotifyRoutineEx2 的情况下还有 Ex2。
进程、线程和映像回调由内核以几乎相同的方式管理,这使得 FudModule 可以使用基本相同的代码来禁用所有这三种回调。我们发现自上一个版本以来,这段代码基本没有变化,主要区别是新增了一些不受影响的驱动程序的列表。
FudModule 首先找到 nt!PspNotifyEnableMask、nt!PspLoadImageNotifyRoutine、nt!PspCreateThreadNotifyRoutine 和 nt!PspCreateProcessNotifyRoutine 的地址。这些地址再次通过扫描导出例程的代码获得,具体的扫描方法会根据 Windows 构建版本略有变化。在执行任何修改之前,rootkit 清除 nt!PspNotifyEnableMask 并休眠一段短时间。这个掩码包含当前启用的回调类型的位字段,因此清除它会禁用所有回调。虽然一些 EDR 绕过会止步于此(https://overlayhack.com/edr-bypass-evasion),但 FudModule 的目标不是无差别地禁用所有回调,因此 nt!PspNotifyEnableMask 的修改只是临时的,FudModule 最终会将其恢复到原始值。我们认为这个临时修改的想法是为了降低可能导致 bug check 的竞争条件的几率。
上述的 nt!Psp(LoadImage|CreateThread|CreateProcess)NotifyRoutine 全局变量都被组织成一个指向 _EX_CALLBACK_ROUTINE_BLOCK 结构的 _EX_FAST_REF 指针数组(至少在 ReactOS(https://github.com/reactos/reactos/blob/e0c17c3f462e3b62bf0c4ca2479c1e5c6b8ff496/sdk/include/ndk/extypes.h#L535) 中是这样的,Microsoft 在这里没有共享符号名称)。FudModule 遍历所有这些结构,并检查 _EX_CALLBACK_ROUTINE_BLOCK.Function(实际的回调例程指针)是否实现在下面列出的模块之一中。如果是,则指针将被附加到一个新数组中,该数组将用于替换原始数组。这有效地移除了除了那些在下面列出的模块中实现的回调之外的所有回调。
ntoskrnl.exe |
ahcache.sys |
mmcss.sys |
cng.sys |
ksecdd.sys |
tcpip.sys |
iorate.sys |
ci.dll |
dxgkrnl.sys |
peauth.sys |
wtd.sys |
删除进程、线程和图像回调过程中允许的内核模块
0x08 – Minifilter 驱动程序
文件系统 minifilter 提供了一种机制(https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/filter-manager-concepts),允许驱动程序拦截文件系统操作。它们用于各种场景,包括加密、压缩、复制、监视、防病毒扫描或文件系统虚拟化。例如,加密 minifilter 在数据写入存储设备之前对数据进行加密,并在读取后对数据进行解密。FudModule 正试图摆脱所有监视和防病毒 minifilter,同时保留其余部分不受影响(毕竟,某些 minifilter 对系统运行至关重要)。关于保留哪些 minifilter、哪些 minifilter 删除的选择主要基于 minifilter 的高度,这是一个整数值,用于决定在多个 minifilter 附加到同一操作时的处理顺序。Microsoft 定义了应该遵循的高度范围(https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/load-order-groups-and-altitudes-for-minifilter-drivers),以便行为良好的 minifilter。不幸的是,这些范围也是 FudModule 区分防恶意软件 minifilter 与其他 minifilter 的一种非常方便的方式。
在其以前的版本中,FudModule 通过直接修改其过程函数的 prologue 来禁用 minifilter。在当今,这将被认为是非常不寻常的,因为 HVCI(Hypervisor-Protected Code Integrity)(https://learn.microsoft.com/en-us/windows-hardware/drivers/bringup/device-guard-and-credential-guard)变得更加普及,甚至在 Windows 11 上默认启用。由于 HVCI 是一项旨在防止在内核中执行任意代码的安全功能,它将阻止 FudModule 尝试修改过滤器函数。这迫使 Lazarus 完全重新实现了这个 rootkit 技术,因此 FudModule 的当前版本通过全新的数据攻击禁用文件系统 minifilter。
这种攻击始于解析 FltEnumerateFilters,并使用它找到 FltGlobals.FrameList.rList。这是一个 FLTMGR!_FLTP_FRAME 结构的链表,每个结构代表一个单独的过滤器管理器框架(https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/filter-manager-concepts#:~:text=Each%20of%20FltMgr%27s%20filter%20device%20objects%20is%20called%20a%20frame)。从这里开始,FudModule 接着另一个链表_FLTP_FRAME.AttachedVolumes.rList。这个链表由 FLTMGR!_FLT_VOLUME 结构组成,描述了附加到特定文件系统卷的 minifilter。有趣的是,rootkit 执行一项健全性检查,以确保与_FLT_VOLUME 分配相关联的池标记等于 FMvo。通过满足健全性检查,FudModule 遍历_FLT_VOLUME.Callbacks.OperationsLists,这是由 IRP 主要功能代码索引的 FLTMGR!_CALLBACK_NODE 结构的链表数组。例如,OperationsLists[IRP_MJ_READ] 是描述在特定卷上读取操作的所有过滤器的链表。
FudModule 确保 _FLT_VOLUME 块的池标记等于 FMvo
对于每个 _CALLBACK_NODE,FudModule 获取相应的 FLTMGR!_FLT_INSTANCE 和 FLTMGR!_FLT_FILTER 结构,并使用它们来决定是否取消链接回调节点。第一个检查基于过滤器背后驱动程序的名称。如果它是 hmpalert.sys(与 HitmanPro 反恶意软件解决方案相关联),则回调将立即取消链接。相反,如果驱动程序的名称与以下列表中的条目匹配,则保留回调:
bindflt.sys |
storqosflt.sys |
wcifs.sys |
cldflt.sys |
filecrypt.sys |
luafv.sys |
npsvctrig.sys |
wof.sys |
fileinfo.sys |
applockerfltr.sys |
bfs.sys |
列入白名单以保留其文件系统微过滤器的内核模块
如果没有驱动程序名称匹配,FudModule 将使用 _FLT_FILTER.DefaultAltitude 来做出最终决定。如果默认高度属于 [320000, 329999] 范围(由 Microsoft 定义为 FSFilter Anti-Virus(https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/load-order-groups-and-altitudes-for-minifilter-drivers#:~:text=recover%20deleted%20files.-,FSFilter%20Anti%2DVirus,-320000%2D329999))或 [360000, 389999] 范围(FSFilter Activity Monitor),则取消链接回调。除了取消链接回调节点,FudModule 还会清除相应 _FLT_INSTANCE 结构中的整个 _FLT_INSTANCE.CallbackNodes 数组。
0x10 – Windows 过滤平台
Windows 过滤平台(WFP)(https://learn.microsoft.com/en-us/windows/win32/fwp/windows-filtering-platform-start-page)是一组为基于主机的网络流量过滤而设计的文档化 API。WFP API 提供了深度数据包检查的功能,以及在网络堆栈的各个层面修改或丢弃数据包的能力。这是非常有用的功能,因此它为许多 Windows 网络安全软件提供了基础,包括入侵检测/预防系统、防火墙和网络监控工具。WFP API 在用户空间和内核空间均可访问,而内核部分提供了更强大的功能。具体而言,内核 API 允许安装所谓的调用驱动程序(https://learn.microsoft.com/en-us/windows-hardware/drivers/network/introduction-to-windows-filtering-platform-callout-drivers),这些驱动程序可以钩入网络堆栈并对处理的网络流量执行任意操作。FudModule 正试图干扰已安装的调用例程,以破坏其提供的安全性。
只有当 Kaspersky 驱动程序(klam.sys、klif.sys、klwfp.sys、klwtp.sys、klboot.sys)同时存在于目标系统上,而 Symantec/Broadcom 驱动程序(symevnt.sys、bhdrvx64.sys、srtsp64.sys)不存在时,才会执行此 rootkit 技术。这个检查似乎是当前版本 FudModule 的一个新添加。在其他方面,我们的分析显示,这个技术的核心思想与 ESET 研究人员(https://www.virusbulletin.com/uploads/pdf/conference/vb2022/papers/VB2022-Lazarus-and-BYOVD-evil-to-the-Windows-core.pdf)在他们对先前版本的分析中描述的结果相匹配。
最初,FudModule 解析 netio!WfpProcessFlowDelete 以定位 netio!gWfpGlobal 的地址。顾名思义,这是用于存储与 WFP 相关的全局变量的设计。尽管其确切布局未记录,但很容易找到(https://codemachine.com/articles/find_wfp_callouts.html)存储指向 WFP 调用结构数组的指针的构建特定偏移量(此数组的长度存储在指针之前的偏移量中)。FudModule 跟随该指针并遍历数组,跳过所有在 ndu.sys、tcpip.sys、mpsdrv.sys 或 wtd.sys 中实现的调用。对于剩余的调用,FudModule 访问调用结构的标志,并设置存储在最低有效位中的标志。虽然调用结构本身未记录,但另一个结构中记录了这个特定的 0x01 标志(https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/fwpsk/ns-fwpsk-fwps_callout2_),称为 FWP_CALLOUT_FLAG_CONDITIONAL_ON_FLOW。文档中写道“如果指定了此标志,过滤引擎仅在数据流上关联了上下文时才调用调用驱动程序的 classifyFn2 调用函数”。换句话说,设置此标志将有条件地禁用调用,在没有可用的流上下文的情况下(参见 netio!IsActiveCallout 的实现)。
FWP_CALLOUT_FLAG_CONDITIONAL_ON_FLOW 标志的含义可以在 netio!IsActiveCallout 中很好地看出。 如果设置了此标志并且无法获取流上下文,IsActiveCallout 将返回 false(请参阅条件的突出显示部分)
尽管这个 rootkit 技术有干扰一些 WFP 调用的潜力,但它不足以完全干扰所有调用。许多安全厂商注册的 WFP 调用已经设计为设置了 FWP_CALLOUT_FLAG_CONDITIONAL_ON_FLOW 标志,因此它们不会受到这个技术的影响。考虑到最初的驱动程序检查,这个技术似乎直接针对卡巴斯基。虽然卡巴斯基确实安装了数十个 WFP 调用,但其中约一半是用于处理流的,并且已经设置了 FWP_CALLOUT_FLAG_CONDITIONAL_ON_FLOW 标志。由于我们避免逆向工程我们竞争对手的产品,因此这个 rootkit 技术的实际影响仍然不清楚。
0x20 – 缺失
到目前为止,我们分析的 rootkit 技术与 ESET 在早期 rootkit 变种的论文中详细描述的技术类似。但是,从现在开始,我们进入了全新的领域。用于处理 Windows 事件跟踪(ETW)的 0x20 技术已被弃用,使得 0x20 位未被使用。相反,有两种新的替代技术针对 ETW,索引为 0x40 和 0x80。索引以前到达了 0x40,这是一种通过禁用预取文件创建来阻碍法医分析的技术。然而,现在位数一直延伸到 0x200,其中包括两种额外的新技术,我们将在本文稍后深入讨论。
0x40 – Windows 事件跟踪:系统记录器
Windows 事件跟踪(ETW)(https://learn.microsoft.com/en-us/windows/win32/etw/about-event-tracing)作为一个高性能机制,专门用于跟踪和记录事件。简而言之,它的主要目的是将提供程序(生成一些日志事件)与使用程序(处理生成的事件)连接起来。使用程序可以定义它们想要消耗哪些事件,例如,通过选择一些感兴趣的特定提供程序。操作系统内置了一些提供程序,例如 Microsoft-Windows-Kernel-Process,它生成与进程相关的事件,例如进程创建或终止。然而,第三方应用程序也可以定义自定义提供程序。
虽然许多内置提供程序与安全无关,但一些生成的事件对于检测目的非常有用。例如,Microsoft-Windows-Threat-Intelligence 提供程序使得可以监视可疑事件,例如写入另一个进程的内存。此外,各种安全产品通过定义自定义提供程序和使用程序利用 ETW。FudModule 干扰 ETW 内部,试图拦截可疑事件,从而逃避检测。
这个 rootkit 技术背后的主要思想是通过将 EtwpActiveSystemLoggers 清零来禁用系统记录器。如何找到该地址的具体实现因目标 Windows 版本而异。在较新的版本中,首先解析 nt!EtwSendTraceBuffer 例程,并用它来找到 nt!EtwpHostSiloState。这指向一个 _ETW_SILODRIVERSTATE 结构,并使用一个硬编码的构建特定偏移量,rootkit 可以访问 _ETW_SILODRIVERSTATE.SystemLoggerSettings.EtwpActiveSystemLoggers。在较旧的版本中,rootkit 首先扫描整个 ntoskrnl .text 节,搜索特定于 EtwTraceKernelEvent prologue 的操作码字节。然后,rootkit 从紧跟其后的 mov ebx, cs:EtwpActiveSystemLoggers 指令中提取目标地址。
要了解此技术的影响,我们可以看一下内核中如何使用 EtwpActiveSystemLoggers。按位访问,其最低有效的八位可能在 EtwpStartLogger 例程中设置。这表明该值本身是一个位字段,每个位表示一个特定系统记录器是否活动。查看对 EtwpActiveSystemLoggers 的其他引用,会出现一个明显的模式。在读取其值后,通常会有一个由 bsf 指令(位扫描向前)保护的循环。循环内通常是对与 ETW 相关的例程的调用,可能会生成一个日志事件。该循环的目的是遍历 EtwpActiveSystemLoggers 的已设置位。当 rootkit 清除所有位时,循环体将永远不会被执行,这意味着事件将不会被记录。
EtwpTraceKernelEventWithFilter 反编译示例。 Rootkit 将 EtwpActiveSystemLoggers 清零后,EtwpLogKernelEvent 将永远不会从循环内部调用,因为保护循环的条件将始终评估为零
0x80 – Windows 事件跟踪:提供程序 GUID
与前一项技术相辅相成,0x80 技术也旨在盲目地影响 ETW,但采用了不同的方法。虽然 0x40 技术相当通用,目的是禁用所有系统记录器,但这项技术的操作更加精准。它包含一个硬编码的 GUID 列表(https://github.com/avast/ioc/tree/master/FudModule#targeted-etw-provider-guids),共有 95 个 GUID,每个代表一个特定的 ETW 提供程序的标识符。rootkit 遍历所有这些 GUID,并尝试禁用相应的提供程序。虽然这种方法需要攻击者投入一些精力来组装 GUID 列表,但它也为他们提供了更精细的控制,可以选择性地破坏哪些 ETW 提供程序。这使他们可以有选择地针对存在较高检测风险的提供程序,忽略其余提供程序,以最小化 rootkit 对目标系统的影响。
此技术首先获取 EtwpHostSiloState(或旧版本中的 EtwSiloState)的地址。如果 EtwpHostSiloState 在之前的技术中已经解析,rootkit 就会重用该地址。如果没有,则 rootkit 沿着引用链 PsGetCurrentServerSiloName -> PsGetCurrentServerSiloGlobals -> PspHostSiloGlobals -> EtwSiloState 进行操作。在这两种情况下,结果是 rootkit 刚刚获得了指向 _ETW_SILODRIVERSTATE 结构的指针,其中包含一个名为 EtwpGuidHashTable 的成员。顾名思义,这是一个包含 ETW GUIDs(_ETW_GUID_ENTRY)的哈希表。
然后,FudModule 遍历其硬编码的 GUID 列表,并尝试在哈希表中定位每个 GUID。尽管哈希表内部未正式记录,但 Yarden Shafir 在她关于利用 ETW 漏洞的博客中提供了一个很好的描述。简而言之,哈希是通过将 128 位的 GUID 拆分为四个 32 位部分并将它们进行异或运算而计算的。通过将哈希与 0x3F 进行 AND 运算,可以获得相关哈希桶(_ETW_HASH_BUCKET)的索引。桶中包含三个 _ETW_GUID_ENTRY 结构的链接列表,每个列表分配给不同类型的 GUID。FudModule 总是选择第一个(EtwTraceGuidType)并遍历它,寻找相关的 _ETW_GUID_ENTRY 结构。
有了指向感兴趣的 GUID 对应的 _ETW_GUID_ENTRY 的指针后,FudModule 就会继续清除 _ETW_GUID_ENTRY.ProviderEnableInfo.IsEnabled。这种修改的目的似乎很明显:FudModule 试图禁用 ETW 提供程序。为了更好地理解这是如何工作的,让我们看一下 nt!EtwEventEnabled(见下面的反编译代码)。这是一个经常作为 if 条件的例程,在调用 nt!EtwWrite(或 nt!EtwWriteEx)之前执行。
从反编译中可以看到,有两个 return 1 语句。将 ProviderEnableInfo.IsEnabled 设置为零可以确保永远不会到达第一个 return 语句。然而,第二个 return 语句仍然有可能执行。为了确保这不会发生,rootkit 还遍历了 _ETW_GUID_ENTRY.RegListHead 链表中的所有 _ETW_REG_ENTRY 结构。对于每个结构,它进行单个 doubleword 写入以清零四个掩码,即 EnableMask、GroupEnableMask、HostEnableMask 和 HostGroupEnableMask(在旧版本中只清零 EnableMask 和 GroupEnableMask,因为后两个掩码尚未引入)。
nt!EtwEventEnabled 的反编译。 Rootkit 完成其工作后,对于与目标 GUID 相关的事件,此例程将始终返回 false。 这是因为 Rootkit 清除了 _ETW_GUID_ENTRY.ProviderEnableInfo.IsEnabled 和 _ETW_REG_ENTRY.GroupEnableMask,强制突出显示的条件失败
清除这些掩码还有一个额外的效果,超出了使 EtwEventEnabled 始终返回 false 的作用。这四个掩码也都在 EtwWriteEx 中进行了检查,这种修改有效地使该例程中和,因为当没有为特定事件注册对象设置任何掩码时,执行永远不会转到更低级别的例程(nt!EtwpEventWriteFull),在那里实现了实际的事件写入逻辑。
0x100 – 图像验证回调
图像验证回调是 FudModule 干扰的另一种回调机制。与进程/线程/图像回调类似,图像验证回调应该在将新的驱动程序映像加载到内核内存时调用。这对于反恶意软件软件是有用的功能,它们可以利用这些回调来阻止已知的恶意或易受攻击的驱动程序(尽管由于回调的异步调用,可能会有一些问题)。此外,图像验证回调还提供了宝贵的遥测来源,提供了对可疑驱动程序加载事件的可见性。可以使用 SeRegisterImageVerificationCallback 例程注册回调,这是公开未记录的。由于这种未记录的性质,此处的使用主要限于深层根植的反恶意软件软件。例如,Windows Defender 注册了一个名为 WdFilter!MpImageVerificationCallback 的回调。
由于内核在内部管理图像验证回调的方式与我们已经探索过的其他一些回调类似,因此 rootkit 的移除实现无疑会显得很熟悉。首先,rootkit 解析 nt!SeRegisterImageVerificationCallback 例程,并扫描其主体以定位 nt!ExCbSeImageVerificationDriverInfo。解引用此地址,它获取一个指向 _CALLBACK_OBJECT 结构的指针,该结构在 _CALLBACK_OBJECT.RegisteredCallbacks 链接列表中保存回调。此列表由 _CALLBACK_REGISTRATION 结构组成,其中实际的回调函数指针可以在 _CALLBACK_REGISTRATION.CallbackFunction 中找到。FudModule 通过使 RegisteredCallbacks 头 LIST_ENTRY 直接指向自身来清除整个列表。此外,它还遍历原始链接列表,并类似地绕过列表中的每个 _CALLBACK_REGISTRATION 条目。
这种 rootkit 技术是在当前版本的 FudModule 中新实现的,我们只能推测其动机。它似乎是设计用来在加载易受攻击或恶意驱动程序时避免检测。然而,很难理解为什么 Lazarus 应该想要加载另一个驱动程序,如果他们已经控制了内核。他们加载易受攻击的驱动程序没有任何意义,因为他们已经通过利用预安装的 Windows 驱动程序中的零日漏洞建立了他们的内核读/写基元。此外,即使他们一开始就利用了易受攻击的驱动程序(就像 FudModule 的上一个版本中的情况一样),现在取消链接回调也为时已晚。在执行此 rootkit 技术时,易受攻击的驱动程序的图像验证回调已经被调用。因此,我们认为最可能的解释是,威胁行为者正在为以后加载某些恶意驱动程序做准备。也许他们的想法是,他们只是想在以后决定部署一些附加的内核模式载荷时得到保护。
0x200 – 对安全软件的直接攻击
到目前为止,我们探讨的 rootkit 技术都有点通用。每个技术都针对某个安全相关的系统组件,并通过该组件间接干扰了所有依赖该组件的安全软件。相比之下,这最后一项技术直截了当地直接禁用特定的安全软件。具体而言,目标安全解决方案是 AhnLab V3 Endpoint Security、Windows Defender、CrowdStrike Falcon 和 HitmanPro。
这次攻击从 rootkit 获取其自己的 _EPROCESS 结构的地址开始。这是通过使用 NtDuplicateHandle 来复制当前进程的伪句柄,然后调用 NtQuerySystemInformation 来获取 SystemExtendedHandleInformation 完成的(https://windows-internals.com/kaslr-leaks-restriction/)。通过扩展句柄信息,rootkit 寻找对应于复制句柄的条目,并从中获取 _EPROCESS 指针。使用 NtQuerySystemInformation 泄漏内核指针是一个众所周知的技术,微软正在逐步建立防御措施来限制。但是,能够在高完整性级别启用 SeDebugPrivilege 的攻击者不受这些防御措施的限制,因此 FudModule 可以继续使用这种技术,即使在即将推出的 24H2 版本上也是如此。有了 _EPROCESS 指针,FudModule 通过将 _EPROCESS.MitigationFlags 设置为零来禁用缓解措施。然后,它还清除了 _EPROCESS.ObjectTable.Flags 中的 EnableHandleExceptions 标志。我们认为这是为了增加稳定性,以防以后在处理句柄表条目时出现问题,稍后我们将描述该技术。
关于用于攻击安全解决方案的具体技术,与其他三个目标不同,AhnLab 的处理方式不同。FudModule 首先检查 AhnLab 是否正在运行,方法是遍历 ActiveProcessLinks 链接列表,并查找一个名为 asdsvc.exe 的进程(AhnLab Smart Defense Service),其 _EPROCESS.Token.AuthenticationId 设置为 SYSTEM_LUID。如果找到这样的进程,FudModule 清除其 _EPROCESS.Protection 字节, effectively toggling off PPL protection for the process。虽然在通常情况下,asdsvc.exe 进程旨在受到标准 PsProtectedSignerAntimalware 级别的保护,但此修改使其成为一个普通的非受保护进程。这使得它容易受到来自用户模式的进一步攻击,现在即使是其他特权的非受保护进程也可以篡改它。然而,我们怀疑这种技术背后的主要想法可能是破坏 AhnLab 的用户模式和内核模式组件之间的链接。通过移除服务的 PPL 保护,内核模式组件可能不再将其识别为合法的 AhnLab 组件。然而,这只是一个推测,因为我们没有测试此技术的真实影响。
处理句柄表条目操纵技术
FudModule 用于攻击 Defender、CrowdStrike 和 HitmanPro 的技术要复杂得多:它尝试使用一种新的句柄表条目操纵技术来暂停它们。为了更好地理解这项技术,让我们首先简要介绍一下句柄表(https://imphash.medium.com/windows-process-internals-a-few-concepts-to-know-before-jumping-on-memory-forensics-part-5-a-2368187685e)。当用户模式代码与内核对象(如进程、文件或互斥体)交互时,通常不直接使用对象。相反,它们通过句柄间接引用它们。在内部,内核必须能够将句柄转换为相应的对象,这就是句柄表的作用。这个每个进程的表,在 _EPROCESS.ObjectTable.TableCode 处可用,作为从句柄到底层对象的映射。它被组织为一个数组,由句柄的整数值索引。每个元素都是 _HANDLE_TABLE_ENTRY 类型,并包含两个关键信息:指向对象头(nt!_OBJECT_HEADER)的(压缩的)指针和与句柄关联的访问位。
由于这种句柄设计,内核对象访问检查通常分为两个独立的逻辑步骤。第一步发生在进程尝试获取句柄时(例如使用 CreateFile 打开文件)。在此步骤中,通常会检查当前线程的令牌与目标对象的安全描述符,以确保线程被允许使用所需访问掩码获取句柄。第二个检查发生在进程使用已经获取的句柄执行操作时(例如使用 WriteFile 写入文件)。这通常仅涉及验证句柄是否足够强大(即具有请求操作的正确访问位)。
FudModule 作为一个非受保护进程运行,所以理论上它不应该能够获取对 PPL 保护的进程(如 CrowdStrike Falcon Service)的强大句柄。然而,借助内核读/写原语,FudModule 有能力直接访问句柄表。这使得它可以创建一个自定义的句柄表条目,控制引用的对象和访问位。通过这种方式,它可以创造出任意对象的句柄,完全绕过了通常需要句柄获取的检查。更重要的是,如果设置句柄的访问位正确,它还将满足执行所需操作时的后续句柄检查。
为了准备好句柄表条目操纵技术,FudModule 创建了一个只是立即将自己置于睡眠状态的虚拟线程。线程本身并不重要。重要的是,通过调用 CreateThread,rootkit 刚刚获得了一个具有 THREAD_ALL_ACCESS 权限的线程句柄。这个句柄将被操纵其句柄表条目。由于它已经具有非常强大的访问位,rootkit 甚至不需要触及其 _HANDLE_TABLE_ENTRY.GrantedAccessBits。它所需要做的就是覆盖 _HANDLE_TABLE_ENTRY.ObjectPointerBits,将句柄重定向到其选择的任意对象。这将使句柄引用该对象,并使 rootkit 能够对其执行特权操作。请注意,ObjectPointerBits 不是对象的整个指针:它只表示 64 位指针的 44 位。但由于 ObjectPointerBits 指向的 _OBJECT_HEADER 被保证对齐(这意味着最低有效四位必须为零)并且位于内核地址空间(这意味着最高有效十六位必须为 0xFFFF),剩下的 20 位可以很容易地推断出来。
一个虚拟线程,其句柄将成为句柄表条目操作的主题
这项技术具体针对的进程是 MsSense.exe、MsMpEng.exe、CSFalconService.exe 和 hmpalert.exe。FudModule 首先找到它们各自的 _EPROCESS 结构,采用与找到 AhnLab 服务相同的算法。然后,它执行一个合理性检查,以确保虚拟线程句柄不会太高,方法是将其与 _EPROCESS.ObjectTable.NextHandleNeedingPool 进行比较(它保存了关于给定当前句柄表分配大小的最大可能句柄值的信息)。通过满足合理性检查,FudModule 访问句柄表本身(EPROCESS.ObjectTable.TableCode),并修改虚拟线程的 _HANDLE_TABLE_ENTRY,使其指向目标 _EPROCESS 的 _OBJECT_HEADER。最后,rootkit 使用重定向的句柄调用 NtSuspendProcess,以挂起目标进程。
可能会觉得奇怪的是,被操纵的句柄曾经是一个线程句柄,但现在被用作进程句柄。在实践中,这没有任何问题,因为句柄表本身不包含对象类型信息。对象类型存储在 _OBJECT_HEADER.TypeIndex 中,因此当 rootkit 重定向句柄时,它也有效地改变了句柄对象类型。至于访问位,原始的 THREAD_ALL_ACCESS 在新的上下文中被重新解释为 PROCESS_ALL_ACCESS,因为这两个常量共享相同的基础值。
被操纵的虚拟线程句柄(0x168),现在引用进程对象
虽然暂停目标进程可能起初看起来是一个完成的任务,但 FudModule 并没有就此停止。在睡眠五秒钟后,它还尝试遍历目标进程中的所有线程,逐个将它们挂起。当所有线程都被挂起时,FudModule 使用 NtResumeProcess 来恢复被挂起的进程。此时,虽然进程本身在技术上已经恢复了,但其各个线程仍然处于挂起状态,这意味着进程仍然有效地处于挂起状态。我们只能猜测为什么 Lazarus 以这种方式实现了进程暂停,但这似乎是一种使技术更隐蔽的尝试。毕竟,一个被暂停的进程比只有几个线程的挂起计数更加显眼。
为了枚举线程,FudModule 使用 SystemExtendedHandleInformation 类调用 NtQuerySystemInformation。在返回的句柄信息上进行迭代,FudModule 搜索来自目标进程的线程句柄。所有者进程通过将目标进程的 PID 与 SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX.UniqueProcessId 进行比较来检查,并通过将 SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX.ObjectTypeIndex 与线程类型索引进行比较来检查类型,此索引之前是使用 NtQueryObject 来获取 ObjectTypesInformation 而获得的。对于每个枚举的线程(可能会多次包含某些线程,因为可能会存在对同一线程的多个打开句柄),FudModule 操纵虚拟线程句柄,使其指向枚举的线程,并通过对操纵句柄调用 SuspendThread 来将其挂起。最后,在所有线程都被挂起并且进程恢复之后,FudModule 将操纵句柄恢复为其原始状态,再次引用虚拟睡眠线程。
总结
总的来说,朝鲜黑客组织Lazarus Group的FudModule根套件展示了他们持续致力于提升其网络攻击能力的决心。尽管他们的标志性战术和技术已经被广泛认知,但他们仍然偶尔能够以意想不到的技术复杂性给人们带来惊喜。最新的更新在本文中进行了审查,展示了Lazarus致力于不断发展这一根套件,专注于提升潜伏性和功能性的努力。
随着他们的管理员到内核的零日漏洞被曝光,Lazarus面临着重大挑战。他们可以选择发现新的零日漏洞,或者回归到以往的BYOVD技术。无论他们做出何种选择,我们都将继续密切监视他们的活动,期待看到他们如何应对这些新的情况。
作为网络安全专业人员,我们需要保持警惕,并积极采取措施防御朝鲜黑客组织等高级威胁。持续监控他们的活动并分析他们的工具和战术对于保持在这些恶意活动前沿至关重要。通过保持信息更新并与广泛的网络安全社区合作,我们可以更好地防范新兴威胁,并减轻Lazarus等高级威胁行为带来的风险。