Samir Bousseaden

使用调用堆栈揭开神秘面纱

在本文中,我们将向您展示我们如何关联规则和事件,以及如何利用调用堆栈来更好地理解您在环境中遇到的任何警报。

Peeling back the curtain with call stacks

简介

Elastic Defend 提供了超过 550 条规则(并且还在不断增加),以实时检测和阻止端点上的恶意行为。我们最近添加了内核调用堆栈扩充,以便为事件和警报提供额外的上下文。调用堆栈对于行为保护来说是多赢的,同时提高了误报率、漏报率和警报可解释性。在本文中,我们将向您展示我们如何实现这三者,以及如何利用调用堆栈来更好地理解您在环境中遇到的任何警报。

什么是调用堆栈?

当运行函数 A 的线程调用函数 B 时,CPU 会自动将当前指令的地址(在 A 内)保存到名为堆栈的线程特定内存区域。这个保存的指针被称为返回地址 - 这是 B 完成其工作后执行将恢复的位置。如果 B 调用第三个函数 C,则 B 内的返回地址也将保存到堆栈中。这些返回地址可以通过称为堆栈回溯的过程进行检索,该过程会重建导致当前线程状态的函数调用序列。堆栈回溯以相反的时间顺序列出返回地址,因此最近的函数始终位于顶部。

在 Windows 中,例如,当我们双击 notepad.exe 时,会调用以下一系列函数

  • 绿色部分与操作系统执行的基本线程初始化有关,并且通常在所有操作(文件、注册表、进程、库等)中相同
  • 红色部分是用户代码;它通常由多个模块组成,并提供有关如何到达进程创建操作的近似详细信息
  • 蓝色部分是 Win32 和本机 API 层;这是特定于操作的,包括最后 2 到 3 个中间 Windows 模块,然后才将操作详细信息转发到内核模式进行有效执行

以下屏幕截图描述了此执行链的调用堆栈

这是一个使用 notepad.exe 创建文件的示例,我们可以看到类似的模式

  • 蓝色部分列出了在将创建文件操作转发到内核模式驱动程序以进行有效执行之前,最后一个用户模式中间 Windows API
  • 红色部分包括来自 user32.dllnotepad.exe 的函数,这表明此文件操作可能是通过 GUI 发起的
  • 绿色部分表示初始线程初始化

事件可解释性

除了使用调用堆栈查找已知坏的,如具有 RWX 权限的未支持内存区域,这可能是先前代码注入的残余。调用堆栈提供非常底层的可见性,通常比日志可以提供的见解更多。

例如,在通过 WMI 查找由 WmiPrvSe.exe 启动的可疑进程执行时,您会找到此 notepad.exe 实例

查看标准事件日志字段,您可能会认为它是使用 Win32_Process 类使用 wmic.exe process call create notepad.exe 语法启动的。但是,事件详细信息描述了一系列模块和函数

蓝色部分描述了标准的中间 CreateProcess Windows API,而红色部分突出显示了更好的信息,我们可以看到调用 CreateProcessW 的第一个调用之前的 DLL 是 wbemcons.dll,并且在检查其属性时,我们可以看到它与WMI 事件使用者相关。我们可以得出结论,这个 notepad.exe 实例可能与 WMI 事件订阅相关。这将需要特定的事件响应步骤来缓解 WMI 持久性机制。

另一个很好的示例是 Windows 计划任务。执行时,它们作为计划服务的子项生成,该计划服务在 svchost.exe 主机进程中运行。现代 Windows 11 计算机可能运行 50 个或更多的 svchost.exe 进程。幸运的是,计划服务具有特定的进程参数 -s Schedule,可以区分它

在较旧的 Windows 版本中,“计划任务”服务是网络服务组的成员,并作为 netsvcs 共享 svchost.exe 实例的组件执行。并非此进程的所有子项在这些较旧的版本中都一定是计划任务

在两个版本上检查调用堆栈,我们可以看到与 CreateProcess 调用相邻的模块是相同的 ubpm.dll(统一后台进程管理器 DLL)执行导出的函数 ubpm.dll!UbpmOpenTriggerConsumer

使用以下 KQL 查询,我们可以在两个版本上查找任务执行

event.action :"start" and 
process.parent.name :"svchost.exe" and process.parent.args : netsvcs and 
process.parent.thread.Ext.call_stack_summary : *ubpm.dll*

另一个有趣的例子是当用户从使用 Windows 资源管理器打开的 ZIP 存档中双击脚本文件时。查看进程树,您将看到 explorer.exe 是父项,而子项是脚本解释器进程,如 wscript.execmd.exe

此进程树可能会与用户从文件系统上的任何位置双击脚本文件的情况混淆,这不是很可疑。但是,如果我们检查调用堆栈,我们可以看到父堆栈指向 zipfld.dll(压缩文件夹 Shell 扩展)

检测示例

现在我们对如何使用调用堆栈来更好地解释事件有了更好的了解,让我们探索一些按事件类型划分的高级检测示例。

进程

通过反射创建可疑进程

Dirty Vanity 是一种最近的代码注入技术,它滥用进程分叉在现有进程的副本中执行 shellcode。当进程分叉时,操作系统会复制现有进程,包括其地址空间和其中的任何可继承句柄。

执行时,“Dirty Vanity”会分叉目标进程(已在运行或牺牲进程)的实例,然后注入其中。使用进程创建通知回调不会记录分叉进程,因为不会执行分叉进程的初始线程。但是,在这种注入技术的情况下,将注入分叉进程,并且将启动一个线程,这将触发带有以下调用堆栈的进程启动事件日志

我们可以看到对 RtlCreateProcessReflectionRtlCloneUserProcess 的调用来分叉进程。现在我们知道这是一个分叉进程,下一个问题是“这在正常情况下常见吗?”虽然诊断上此行为似乎很常见,并且单独来看,它并不是恶意行为的强烈信号。进一步检查以查看分叉进程是否执行任何网络连接、加载 DLL 或生成子进程,发现不太常见,并为良好的检测做好了准备

// EQL detecting a forked process spawning a child process - very suspicious

process where event.action == "start" and

descendant of 
   [process where event.action == "start" and 
   _arraysearch(process.parent.thread.Ext.call_stack, $entry, 
   $entry.symbol_info: 
    ("*ntdll.dll!RtlCreateProcessReflection*", 
    "*ntdll.dll!RtlCloneUserProcess*"))] and

not (process.executable : 
      ("?:\\WINDOWS\\SysWOW64\\WerFault.exe", 
      "?:\\WINDOWS\\system32\\WerFault.exe") and
     process.parent.thread.Ext.call_stack_summary : 
      "*faultrep.dll|wersvc.dl*")
// EQL detecting a forked process loading a network DLL 
//  or performs a network connection - very suspicious

sequence by process.entity_id with maxspan=1m
 [process where event.action == "start" and
  _arraysearch(process.parent.thread.Ext.call_stack, 
  $entry, $entry.symbol_info: 
    ("*ntdll.dll!RtlCreateProcessReflection*", 
    "*ntdll.dll!RtlCloneUserProcess*"))]
 [any where
  (
   event.category : ("network", "dns") or 
   (event.category == "library" and 
    dll.name : ("ws2_32.dll", "winhttp.dll", "wininet.dll"))
  )]

以下是从分叉的 explorer.exe 实例中分叉 explore.exe 并执行生成 cmd.exe 的 shellcode 的示例

通过汇编字节的直接系统调用

进程事件的第二个也是最后一个例子是通过直接系统调用创建进程。它直接使用系统调用指令,而不是调用 NtCreateProcess API。攻击者可能会使用这种方法来规避依赖用户模式 API 钩子的安全产品(Elastic Defend 不是这种情况)。

process where event.action : "start" and 

// EQL detecting a call stack not ending with ntdll.dll 
not process.parent.thread.Ext.call_stack_summary : "ntdll.dll*" and 

/* last call in the call stack contains bytes that execute a syscall
 manually using assembly <mov r10,rcx, mov eax,ssn, syscall> */

_arraysearch(process.parent.thread.Ext.call_stack, $entry,
 ($entry.callsite_leading_bytes : ("*4c8bd1b8??????000f05", 
 "*4989cab8??????000f05", "*4c8bd10f05", "*4989ca0f05")))

此示例匹配调用堆栈中最后一个内存区域未被支持且包含以系统调用指令 (0F05) 结尾的汇编字节的情况。

文件

可疑的 Microsoft Office 嵌入对象

以下规则逻辑识别由 Microsoft Office 进程从嵌入的 OLE 流写入的可疑文件扩展名,恶意文档经常使用这些扩展名来投放初始访问的有效负载。

// EQL detecting file creation event with call stack indicating 
// OleSaveToStream call to save or load the embedded OLE object

file where event.action != "deletion" and 

process.name : ("winword.exe", "excel.exe", "powerpnt.exe") and

_arraysearch(process.thread.Ext.call_stack, $entry, $entry.symbol_info:
 ("*!OleSaveToStream*", "*!OleLoad*")) and
(
 file.extension : ("exe", "dll", "js", "vbs", "vbe", "jse", "url", 
 "chm", "bat", "mht", "hta", "htm", "search-ms") or

 /* PE & HelpFile */
 file.Ext.header_bytes : ("4d5a*", "49545346*")
 )

匹配示例

从不支持的内存中进行的的可疑文件重命名

某些勒索软件可能会在开始加密例程之前注入到签名进程中。文件重命名和修改事件将显示为源自受信任的进程,这可能会绕过一些排除签名进程的启发式方法,因为它们被认为是误报。以下 KQL 查询查找来自签名二进制文件且具有可疑调用堆栈的文档文件重命名。

file where event.action : "rename" and 
  
process.code_signature.status : "trusted" and file.extension != null and 

file.Ext.original.name : ("*.jpg", "*.bmp", "*.png", "*.pdf", "*.doc", 
"*.docx", "*.xls", "*.xlsx", "*.ppt", "*.pptx") and

not file.extension : ("tmp", "~tmp", "diff", "gz", "download", "bak", 
"bck", "lnk", "part", "save", "url", "jpg",  "bmp", "png", "pdf", "doc", 
"docx", "xls", "xlsx", "ppt", "pptx") and 

process.thread.Ext.call_stack_summary :
("ntdll.dll|kernelbase.dll|Unbacked",
 "ntdll.dll|kernelbase.dll|kernel32.dll|Unbacked", 
 "ntdll.dll|kernelbase.dll|Unknown|kernel32.dll|ntdll.dll", 
 "ntdll.dll|kernelbase.dll|Unknown|kernel32.dll|ntdll.dll", 
 "ntdll.dll|kernelbase.dll|kernel32.dll|Unknown|kernel32.dll|ntdll.dll", 
 "ntdll.dll|kernelbase.dll|kernel32.dll|mscorlib.ni.dll|Unbacked", 
 "ntdll.dll|wow64.dll|wow64cpu.dll|wow64.dll|ntdll.dll|kernelbase.dll|
 Unbacked", "ntdll.dll|wow64.dll|wow64cpu.dll|wow64.dll|ntdll.dll|
 kernelbase.dll|Unbacked|kernel32.dll|ntdll.dll", 
 "ntdll.dll|Unbacked", "Unbacked", "Unknown")

以下是一些匹配示例,其中 explorer.exe (Windows 资源管理器) 被 KNIGHT/CYCLOPS 勒索软件注入。

由未签名的服务 DLL 投放的可执行文件

某些类型的恶意软件通过伪装成 Windows 服务 DLL 来维持其存在。为了被服务控制管理器识别和管理,服务 DLL 必须导出一个名为 ServiceMain 的函数。以下 KQL 查询有助于识别创建可执行文件且调用堆栈包含 ServiceMain 函数的实例。

event.category : file and 
 file.Ext.header_bytes :4d5a* and process.name : svchost.exe and 
 process.thread.Ext.call_stack.symbol_info :*!ServiceMain*

已加载的未签名打印监视器驱动程序

以下 EQL 查询标识打印后台处理服务加载未签名库的情况,其中调用堆栈指示加载来自 SplAddMonitor。攻击者可能会使用端口监视器在系统启动期间运行攻击者提供的 DLL,以实现持久性或权限提升。

library where
process.executable : ("?:\\Windows\\System32\\spoolsv.exe", 
"?:\\Windows\\SysWOW64\\spoolsv.exe") and not dll.code_signature.status : 
"trusted" and _arraysearch(process.thread.Ext.call_stack, $entry, 
$entry.symbol_info: "*localspl.dll!SplAddMonitor*")

匹配示例

通过 ROP Gadget 加载库的可能性

此 EQL 规则识别从不寻常的 win32untdll 偏移加载库的情况。这可能表明尝试使用返回导向编程 (ROP) 汇编 Gadget 来执行来自受信任模块的系统调用指令,从而绕过 API 监控。

library where
// adversaries try to use ROP gadgets from ntdll.dll or win32u.dll 
// to construct a normal-looking call stack

process.thread.Ext.call_stack_summary : ("ntdll.dll|*", "win32u.dll|*") and 

// excluding normal Library Load APIs - LdrLoadDll and NtMapViewOfSection
not _arraysearch(process.thread.Ext.call_stack, $entry, 
 $entry.symbol_info: ("*ntdll.dll!Ldr*", 
 "*KernelBase.dll!LoadLibrary*", "*ntdll.dll!*MapViewOfSection*"))

此示例匹配 AtomLdr 使用来自 win32u.dll 的 ROP Gadget 加载 DLL,而不是使用 ntdll 的加载库 API(LdrLoadDllNtMapViewOfSection)。

通过 LdrpKernel32 覆盖进行规避

[LdrpKernel32(https://github.com/rbmm/LdrpKernel32DllName)规避是一种有趣的技术,通过覆盖 ntdll.dll 内存中引用的引导 DLL 名称,在引导阶段劫持进程的早期执行,强制进程加载恶意 DLL。

library where 
 
// BaseThreadInitThunk must be exported by the rogue bootstrap DLL
 _arraysearch(process.thread.Ext.call_stack, $entry, $entry.symbol_info :
  "*!BaseThreadInitThunk*") and

// excluding kernel32 that exports normally exports BasethreadInitThunk
not _arraysearch(process.thread.Ext.call_stack, $entry, $entry.symbol_info
 ("?:\\Windows\\System32\\kernel32.dll!BaseThreadInitThunk*", 
 "?:\\Windows\\SysWOW64\\kernel32.dll!BaseThreadInitThunk*", 
 "?:\\Windows\\WinSxS\\*\\kernel32.dll!BaseThreadInitThunk*", 
 "?:\\Windows\\WinSxS\\Temp\\PendingDeletes\\*!BaseThreadInitThunk*", 
 "\\Device\\*\\Windows\\*\\kernel32.dll!BaseThreadInitThunk*"))

匹配示例

可疑的远程注册表修改

与计划任务示例类似,远程注册表服务托管在 svchost.exe 中。我们可以使用调用堆栈来检测注册表修改,方法是监控远程注册表服务何时指向可执行文件或脚本文件。这可能表明尝试通过远程配置更改进行横向移动。

registry where 

event.action == "modification" and 

user.id : ("S-1-5-21*", "S-1-12-*") and 

 process.name : "svchost.exe" and 

// The regsvc.dll in call stack indicate that this is indeed the 
// svchost.exe instance hosting the Remote registry service

process.thread.Ext.call_stack_summary : "*regsvc.dll|rpcrt4.dll*" and

 (
  // suspicious registry values
  registry.data.strings : ("*:\\*\\*", "*.exe*", "*.dll*", "*rundll32*", 
  "*powershell*", "*http*", "* /c *", "*COMSPEC*", "\\\\*.*") or
  
  // suspicious keys like Services, Run key and COM
  registry.path :
         ("HKLM\\SYSTEM\\ControlSet*\\Services\\*\\ServiceDLL",
          "HKLM\\SYSTEM\\ControlSet*\\Services\\*\\ImagePath",
          "HKEY_USERS\\*Classes\\*\\InprocServer32\\",
          "HKEY_USERS\\*Classes\\*\\LocalServer32\\",
          "H*\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\*") or
  
  // potential attempt to remotely disable a service 
  (registry.value : "Start" and registry.data.strings : "4")
  )

此示例匹配通过远程注册表服务远程修改 Run 键注册表值的情况。

结论

正如我们所演示的,调用堆栈不仅可用于查找已知的错误模式,还可用于减少标准 EDR 事件中的歧义并简化行为解释。我们在此处提供的示例仅代表通过对同一数据集应用增强的丰富功能可以实现的潜在检测可能性的一小部分。