最后一个分支记录和分支跟踪





5.00/5 (5投票s)
Intel/AMD 扩展处理器功能的用法。
引言
在很大程度上,似乎大量恶意软件分析和逆向工程领域都理所当然地认为处理器提供了一些扩展功能。这篇文档将解释 AMD 和 Intel 的系统调试 MSR(模型特定寄存器)的详细信息,以及如何将这些功能用于用户模式调试器,而不仅仅是运行在 CPL 0 级别的代码。
需要注意的是,某些调试功能 MSR 在 Intel 和 AMD 处理器之间有所不同。例如,某些 Intel CPU 提供多达 15 个 Last Branch 记录,而 AMD 则没有。但无论哪种情况,都无法从 CPL 3 运行的代码中利用它们,这也不在我们讨论的范围之内。
背景
作者假设您对 Windows 调试器 API、Windows 内部结构和汇编有相当的了解。Last Branch Recording 和 Branch Tracing 应该在恶意软件分析师或软件逆向工程师的分析环 3 代码的战术库中占有重要地位。Windows 操作系统本身提供了几个后门来利用这些技术从用户模式进行访问。本文的目标是提供一个关于如何使用这些功能并将它们集成到您自己的调试器和分析工具中的良好解释。
分支跟踪
分支是指可以条件或无条件地转移控制流的指令。例如,任何条件跳转、无条件跳转、调用、返回、远调用、远跳转、iret、retf、int n、syscall、sysexit、icebp 等。
'分支已采取'(branch taken)一词表示由于分支而实际改变了控制流。在无条件分支指令中,分支总是会被采取。然而,对于条件跳转(例如,在进行位比较后),分支并不总是会被采取,而是取决于先前比较的结果。
正如您希望已经知道的那样,处理器单步执行功能(EFLAGS.TF=1
)会在到达每个指令边界后触发一个 #DB 异常。这种异常称为陷阱(trap),这意味着推送到中断处理程序堆栈上的指令指针将指向要执行的下一条指令。
简单的 x86 示例
pushfd
or dword ptr [esp], 0x100
popfd
inc eax //<--after this instruction has finished execution,
// the address pushed onto the handler stack is the next instruction
push ebx
DebugCtl MSR 提供了一个位,当该位与 EFLAGS.TF=1
一起设置时,只有在分支指令边界到达后才会引发 #DB 陷阱(single_step
),而不是在每个指令后都触发。这仅在分支被采取时发生。然后推送到处理程序堆栈上的指令是分支目标的指令,在 Windows 调试器的 CONTEXT
结构中,这自然就是您的指令指针 EIP/RIP。
简单的 x86 示例
(EFLAGS.TF=1 and DebugCtl.BTF=1
)
push ebx //<----even though trap flag is set, nothing.
push eax //<----even though trap flag is set, nothing.
call ecx //<----will raise a #DB exception, IP on handler stack will be the destination of the call. In this case ECX.
xor eax, eax //<----even though trap flag is set, nothing.
inc ebx //<----even though trap flag is set, nothing.
pop eax //<----even though trap flag is set, nothing.
pop ebx //<----even though trap flag is set, nothing.
ret //<----will raise a #DB exception, IP on handler stack will
//be the destination of the return. In this case, the contents pointed to by ESP.
那么,我们如何从用户模式访问 DebugCtl
呢?很简单,Windows 通过 DR7 的第 8 位和第 9 位为我们提供了对 DebugCtl
的 BTF 和 LBR 位(DebugCtl
的第 0 位和第 1 位)的访问。如果您有兴趣,请参阅 KiRestoreDebugRegisterState
。
- DR7 的第 8 位代表
DebugCtl
的第 0 位。这是 LBR 位。(Last Branch Record,稍后解释) - DR7 的第 9 位代表
DebugCtl
的第 1 位。这是 BTF 位。(在分支时单步执行)
正如我所能想象的那样,这可以极大地加快跟踪速度。因为理论上,当寻找代码控制流的差异或 bug 时,我们的答案很可能取决于哪些分支被采取,哪些未被采取,而仅跟踪分支时,您每秒可以跟踪数十万条指令,而不是在每个指令边界后都生成中断。
现在,您可能已经注意到,也可能没有注意到,这给我们留下了一个问题。推送到处理程序堆栈上的指令指针是分支指令的目标。因此,您用户模式 CONTEXT
结构中的 RIP/EIP 将是目标指令的地址。如果我们想知道分支指令本身的位置怎么办?这时就需要 Last Branch Record Stack,也称为 LBR。
假设您已经在用用户模式调试器或分析工具跟踪程序的执行。您已经设置了 DR7 的第 9 位来启用分支跟踪,并且也设置了陷阱标志。以下是需要做的事情:另外,通过 DR7 设置 LBR 位(如上所示,为第 8 位)。当由于分支已采取而发生 #DB 异常时,请在您的 CONTEXT
中分析 EIP/RIP。正如前面所说的,这是目标指令。现在到了文章的精彩部分:分支指令本身的地址被 Windows 隐藏在 EXCEPTION_RECORD->ExceptionInformation[0]
中,当然前提是您已正确启用了 LBR。然后,这就是分支到您当前指令指针位置的分支指令本身的虚拟地址。
我在这方面发挥了一点创意,在网上找不到关于这些功能的深入文章,于是我决定自己写一篇。我当时正在为我的朋友分析一小段软件,我注意到在调用 ws32.send()
之前,它会清空堆栈,并设置一个假的返回地址,然后 `JMP` 到 `send()`,以避免将原始返回地址推送到堆栈上,这使得查找其原始来源非常困难。
LBR 救星
以下是我们如何轻松克服这个问题的方法,有些人可能已经知道了,但请继续阅读以获取重要细节。
- 首先,我们必须在我们要分析的线程上初始化 LBR。在这种情况下,我们不需要 BTF 功能。所以为该线程设置 DR7 的第 8 位。
- 接下来,我们必须在
ws32.send()
或您正在分析的任何其他代码上设置一个断点。这里是关键部分:引发的异常类型必须是 #DB 异常。
这是因为唯一会将 LastBranchFromIp
插入 EXCEPTION_RECORD->ExceptionInformation[0]
的 Windows 中断处理程序是 Windows 的 int 01
处理程序。
Windows 的 int 3
处理程序不会为我们做这件事,如果您使用 int 3
,您的 ExceptionInformation[0]
成员将为空。
您可以使用调试寄存器断点或 ICEBP
(int 01,无 DPL 检查)。我个人推荐 ICEBP
,原因如下:代码可能会在设置了恢复标志(resume flag)的情况下 `IRET` 到 `send()`。如果您的调试器用户在 `send()` 的第一条指令上设置了断点,它将被忽略!
而且,坦白说,我们中的许多人都喜欢在 API 的第一条指令上设置断点。
关注点 - VM 检测
除了分析一个带有伪造堆栈设置的分支到函数之外,LBR 功能还可以作为检测您的程序是否运行在虚拟机(hyper-visor)上的一个相当不错的方法。这是因为大多数虚拟化软件,包括 VMware 和 Vbox,都不使用 LBR 虚拟化(尽管它是可能的并且受支持)。
这是一个粗略的示例。异常处理程序的逻辑留给您来完成。
RunningInHyperVisor PROC
//eax = CONTEXT pointer
mov [eax], 0x10 //CONTEXT_DEBUG_REGISTERS
lea ebx, [eax+0x18]
mov [ebx], 0x100 //LBR bit @ DR7
push eax
push ecx
call SetThreadContext
_emit 0xeb //previous branch
_emit 0x00
_emit 0xf1 //icebp
RunningInHyperVisor ENDP
此时,您将检查 ExceptionInformation[0]
的内容。如果是在虚拟机下运行,它将不包含前一个分支的虚拟地址。