Samir Bousseaden

通过调用栈揭开幕后

在这篇文章中,我们将向您展示我们如何将规则和事件上下文化,以及如何利用调用栈更好地理解您在环境中遇到的任何警报。

阅读时间:13分钟安全运营安全研究检测科学
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.dll**和**notepad.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.exe**或**cmd.exe**。

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

检测示例

现在我们已经更好地了解了如何使用调用栈来更好地解释事件,让我们探索每个事件类型的某些高级检测示例。

进程

通过反射进行的可疑进程创建

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

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

我们可以看到对**RtlCreateProcessReflection**和**RtlCloneUserProcess**的调用来分叉进程。现在我们知道这是一个分叉的进程,下一个问题是“这在正常情况下常见吗?”虽然在诊断上这种行为似乎很常见,并且单独来看,它并不是恶意行为的强烈信号。进一步检查以查看分叉的进程是否执行任何网络连接、加载 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"))
  )]

这是一个分叉**explore.exe**并执行 shellcode 的示例,该 shellcode 从分叉的**explorer.exe**实例生成**cmd.exe**:

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

进程事件的第二个也是最后一个示例是通过直接系统调用创建进程。这直接使用系统调用指令,而不是调用**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")

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

未签名的服务 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 小工具加载潜在库

此 EQL 规则识别从异常的win32untdll偏移量加载库的情况。这可能表示尝试使用面向返回编程 (ROP) 汇编小工具绕过 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 小工具加载 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 事件中的歧义,并简化行为解释。我们在此提供的示例仅代表通过将增强的丰富功能应用于相同数据集可以实现的潜在检测可能性的一小部分。