通过滥用一个Windows中的糟糕假设来检测调试器
翻译 by 银雁冰
原文链接
这篇博客将会审视一个微软在十多年前处理软件断点时所用的假设,利用这一点可以检测到大部分(市面上所有的?)用户态和内核态调试器。
x86
架构可以用多种方式编码一条特定的汇编指令。举个例子,将两个寄存器eax
和ebx
累加,并且将结果存储到eax中可采用如下助记符:add eax, ebx
。这可以被编码成字节序列0x03 0xC3
或0x01 0xD8
.这两组机器码代表同一个汇编操作。
如果你只关心反调试技巧(并不想结合上下文理解它的原理),请将进度条滚动到这篇博客的最下面。对于那些想要勇敢地读完整篇文章的人,请系好安全带(老司机要开车了)。
int 3
的漫长历史
一个int 3
指令可以被编码成一个单字节指令0xCC
,也可以通过不常见的方式被编码成多指令序列0xCD 0x03
:
来自《Intel指令手册
》(第2卷
,第3章
,第3.2节
)
所以,如果Windows
遇到一个多字节的int 3
指令会发生什么呢?我们写一个简单的C++
程序来看一下:
/*
* Module Name:
* int3.cpp
*
* Abstract:
* Examines the difference in operation between a
* multi-byte int 3 (0xCD 0x03) and a single-byte
* int 3 (0xCC).
*
* Author:
* Nemanja (Nemi) Mulasmajic <nm@triplefault.io>
* http://triplefault.io
*/
#pragma warning(disable: 4710)
#pragma warning(push, 0)
#include <Windows.h>
#include <stdio.h>
#pragma warning(pop)
// The size of an architecture page on x86/x64.
#define PAGE_SIZE 0x1000
// Single-byte int 3 stub.
BYTE _Int3[] =
{
0xCC, /* int 3*/
0xC3 /* ret */
};
// Multi-byte int 3 stub.
BYTE _LongInt3[] =
{
0xCD, 0x03, /* int 3 */
0xC3 /* ret */
};
/*
* Handles exception processing for our int 3s.
*/
DWORD WINAPI ExceptionFilter(_In_ PEXCEPTION_POINTERS ExceptionInformation)
{
// Malformed exception information.
if (!ExceptionInformation || !ExceptionInformation->ExceptionRecord || !ExceptionInformation->ContextRecord)
return EXCEPTION_EXECUTE_HANDLER;
// This is the only type of exception we should see...
if (ExceptionInformation->ExceptionRecord->ExceptionCode != EXCEPTION_BREAKPOINT)
return EXCEPTION_EXECUTE_HANDLER;
printf("[+] ExceptionRecord->ExceptionAddress: 0x%p.\n", ExceptionInformation->ExceptionRecord->ExceptionAddress);
#if defined(_M_AMD64)
#define IP Rip
#elif defined(_M_IX86)
#define IP Eip
#else
#error "Compiling for unhandled architecture."
#endif
const BYTE* InstructionPointer = (const BYTE*)ExceptionInformation->ContextRecord->IP;
printf("[+] ContextRecord->IP: 0x%p.\n", InstructionPointer);
printf("\t@ [0: 0x%X...]\n", InstructionPointer[0]);
#undef IP
return EXCEPTION_EXECUTE_HANDLER;
}
/*
* The entry point.
*/
int main(void)
{
int status = -1;
// Allocate executable memory.
PBYTE Memory = (PBYTE)VirtualAlloc(NULL, PAGE_SIZE, (MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE);
if (!Memory)
{
fprintf(stderr, "[-] ERROR: Failed to allocate memory.\n");
goto Cleanup;
}
typedef void(*Function)();
// Place our simple breakpoint stubs in executable memory.
// Our single-byte int 3 will be at the start of this region.
Function Int3 = (Function)&Memory[0];
size_t BufferSize = sizeof(_Int3);
memcpy(Int3, _Int3, BufferSize);
printf("[+] Single-byte 'int 3' buffer: 0x%p.\n", Int3);
// Our multi-byte int 3 will be immediately after.
Function LongInt3 = (Function)&Memory[BufferSize];
BufferSize = sizeof(_LongInt3);
memcpy(LongInt3, _LongInt3, BufferSize);
printf("[+] Multi-byte 'int 3' buffer: 0x%p.\n", LongInt3);
// Execute both variants of int 3.
printf("\n[+] Executing single-byte variant.\n");
__try
{
Int3();
}
__except (ExceptionFilter(GetExceptionInformation())) { }
printf("\n[+] Executing multi-byte variant.\n");
__try
{
LongInt3();
}
__except (ExceptionFilter(GetExceptionInformation())) {}
status = 0;
Cleanup:
// Free allocated memory.
if (Memory)
{
VirtualFree(Memory, 0, MEM_FREE);
Memory = NULL;
}
// Wait for [ENTER] key press to terminate the program.
getchar();
return status;
}
在你运行这个程序之后,你应该看到与之类似的输出:
一个单字节的int 3(0xCC)
和预期的一致。代码块开始处位于0x000001BE94B90000
。当这段代码被执行后,异常处理例程启动,我们看到_EXCEPTION_RECORD.ExceptionAddress
和_CONTEXT.Rip
都位于0x000001BE94B90000
,这就是int 3
指令的开始处。完美!
多字节的int 3(0xCD 0x03)
的代码块开始处位于0x000001BE94B90002
。当这段代码被执行后,异常处理例程认为_EXCEPTION_RECORD.ExceptionAddress
和_CONTEXT.Rip
都位于0x000001BE94B90003
。这是int 3
指令的中间位置。为什么?哪里出问题了?
那个假设
注意:从现在开始,所有的汇编代码和伪代码都从Windows x64 10.0.15063
(创意者更新版本)提供的系统文件中重新构建。如果你想要跟着做下去,请确保你使用和我相同的操作系统版本!
微软假设所有的int 3
都源自单字节的情况。
这个假设发生在中断处理过程很前面的时候。顾名思义,当一个中断发生的时候,例如当一个int 3
被处理器执行时,控制流被CPU
重定向到一个注册在IDT
(中断控制描述符表)的例程。在Windows
操作系统中,软件中断对应的例程可以在nt!KiBreakpointTrap
的符号中找到:
.text:0000000140174EC0 ; void __fastcall __noreturn KiBreakpointTrap()
.text:0000000140174EC0 KiBreakpointTrap proc near ; DATA XREF: .pdata:000000014039F540 o
.text:0000000140174EC0 ; INITDATA:000000014082A1A8 o
.text:0000000140174EC0
.text:0000000140174EC0 TrapInformation = _KTRAP_FRAME ptr -168h
.text:0000000140174EC0
.text:0000000140174EC0 sub rsp, 8
.text:0000000140174EC4 push rbp
.text:0000000140174EC5 sub rsp, 158h
.text:0000000140174ECC lea rbp, [rsp+168h+TrapInformation._Xmm1]
.text:0000000140174ED4 mov [rbp+0E8h+TrapInformation.ExceptionActive], 1
.text:0000000140174ED8 mov [rbp+0E8h+TrapInformation._Rax], rax
.text:0000000140174EDC mov [rbp+0E8h+TrapInformation._Rcx], rcx
.text:0000000140174EE0 mov [rbp+0E8h+TrapInformation._Rdx], rdx
.text:0000000140174EE4 mov [rbp+0E8h+TrapInformation._R8], r8
.text:0000000140174EE8 mov [rbp+0E8h+TrapInformation._R9], r9
.text:0000000140174EEC mov [rbp+0E8h+TrapInformation._R10], r10
.text:0000000140174EF0 mov [rbp+0E8h+TrapInformation._R11], r11
.text:0000000140174EF4 test byte ptr [rbp+0E8h+TrapInformation.SegCs], 1
.text:0000000140174EFB jz short ExecutingInKernelModeContext
.text:0000000140174EFD swapgs
.text:0000000140174F00 mov r10, gs:_KPCR.Prcb.CurrentThread
.text:0000000140174F09 test [r10+_ETHREAD.Tcb.Header.___u0.__s6.DebugActive], 80h
.text:0000000140174F0E jz short DebugIsActive
.text:0000000140174F10 mov ecx, 0C0000102h ; IA32_KERNEL_GS_BASE
.text:0000000140174F15 rdmsr
.text:0000000140174F17 shl rdx, 20h
.text:0000000140174F1B or rax, rdx
.text:0000000140174F1E cmp [r10+_ETHREAD.Tcb.Teb], rax
.text:0000000140174F25 jz short DebugIsActive
.text:0000000140174F27 mov rdx, [r10+_ETHREAD.Tcb.___u35.__s8.Ucb]
.text:0000000140174F2E bts [r10+_ETHREAD.Tcb.___u16.MiscFlags], 8
.text:0000000140174F34 dec [r10+_ETHREAD.Tcb.___u35.__s4.SpecialApcDisable]
.text:0000000140174F3C mov [rdx+80h], rax ; Something indexed into Ucb structure...
.text:0000000140174F43
.text:0000000140174F43 DebugIsActive: ; CODE XREF: KiBreakpointTrap+4E j
.text:0000000140174F43 ; KiBreakpointTrap+65 j
.text:0000000140174F43 test [r10+_ETHREAD.Tcb.Header.___u0.__s6.DebugActive], 3
.text:0000000140174F48 mov word ptr [rbp+0E8h+TrapInformation.Dr7], 0
.text:0000000140174F51 jz short ExecutingInKernelModeContext
.text:0000000140174F53 call KiSaveDebugRegisterState
.text:0000000140174F58
.text:0000000140174F58 ExecutingInKernelModeContext: ; CODE XREF: KiBreakpointTrap+3B j
.text:0000000140174F58 ; KiBreakpointTrap+91 j
.text:0000000140174F58 cld
.text:0000000140174F59 stmxcsr [rbp+0E8h+TrapInformation._MxCsr]
.text:0000000140174F5D ldmxcsr gs:_KPCR.Prcb._MxCsr
.text:0000000140174F66 movaps xmmword ptr [rbp+0E8h+TrapInformation._Xmm0.Low], xmm0
.text:0000000140174F6A movaps xmmword ptr [rbp+0E8h+TrapInformation._Xmm1.Low], xmm1
.text:0000000140174F6E movaps xmmword ptr [rbp+0E8h+TrapInformation._Xmm2.Low], xmm2
.text:0000000140174F72 movaps xmmword ptr [rbp+0E8h+TrapInformation._Xmm3.Low], xmm3
.text:0000000140174F76 movaps xmmword ptr [rbp+0E8h+TrapInformation._Xmm4.Low], xmm4
.text:0000000140174F7A movaps xmmword ptr [rbp+0E8h+TrapInformation._Xmm5.Low], xmm5
.text:0000000140174F7E test [rbp+0E8h+TrapInformation.EFlags], 200h ; Check for the interrupt flag in EFLAGS.
.text:0000000140174F88 jz short DontEnableInterrupts
.text:0000000140174F8A sti
.text:0000000140174F8B
.text:0000000140174F8B DontEnableInterrupts: ; CODE XREF: KiBreakpointTrap+C8 j
.text:0000000140174F8B mov ecx, 80000003h ; ExceptionCode
.text:0000000140174F90 mov edx, 1 ; NumberOfParameters
.text:0000000140174F95 mov r8, [rbp+0E8h+TrapInformation._Rip]
.text:0000000140174F9C dec r8 ; ExceptionAddress
.text:0000000140174F9F mov r9d, 0 ; Parameter1
.text:0000000140174FA5 call KiExceptionDispatch
.text:0000000140174FA5 KiBreakpointTrap endp
nt!KiBreakpointTrap
做的第一件事是在栈上生成一个用来传递给后续例程的陷阱帧(_KTRAP_FRAME
)。这个结构体的其中一个定义如下:
kd> dt nt!_KTRAP_FRAME -b
+0x000 P1Home : Uint8B
+0x008 P2Home : Uint8B
+0x010 P3Home : Uint8B
+0x018 P4Home : Uint8B
+0x020 P5 : Uint8B
+0x028 PreviousMode : Char
+0x029 PreviousIrql : UChar
+0x02a FaultIndicator : UChar
+0x02b ExceptionActive : UChar
+0x02c MxCsr : Uint4B
+0x030 Rax : Uint8B
+0x038 Rcx : Uint8B
+0x040 Rdx : Uint8B
+0x048 R8 : Uint8B
+0x050 R9 : Uint8B
+0x058 R10 : Uint8B
+0x060 R11 : Uint8B
+0x068 GsBase : Uint8B
+0x068 GsSwap : Uint8B
+0x070 Xmm0 : _M128A
+0x000 Low : Uint8B
+0x008 High : Int8B
+0x080 Xmm1 : _M128A
+0x000 Low : Uint8B
+0x008 High : Int8B
+0x090 Xmm2 : _M128A
+0x000 Low : Uint8B
+0x008 High : Int8B
+0x0a0 Xmm3 : _M128A
+0x000 Low : Uint8B
+0x008 High : Int8B
+0x0b0 Xmm4 : _M128A
+0x000 Low : Uint8B
+0x008 High : Int8B
+0x0c0 Xmm5 : _M128A
+0x000 Low : Uint8B
+0x008 High : Int8B
+0x0d0 FaultAddress : Uint8B
+0x0d0 ContextRecord : Uint8B
+0x0d8 Dr0 : Uint8B
+0x0e0 Dr1 : Uint8B
+0x0e8 Dr2 : Uint8B
+0x0f0 Dr3 : Uint8B
+0x0f8 Dr6 : Uint8B
+0x100 Dr7 : Uint8B
+0x108 DebugControl : Uint8B
+0x110 LastBranchToRip : Uint8B
+0x118 LastBranchFromRip : Uint8B
+0x120 LastExceptionToRip : Uint8B
+0x128 LastExceptionFromRip : Uint8B
+0x130 SegDs : Uint2B
+0x132 SegEs : Uint2B
+0x134 SegFs : Uint2B
+0x136 SegGs : Uint2B
+0x138 TrapFrame : Uint8B
+0x140 Rbx : Uint8B
+0x148 Rdi : Uint8B
+0x150 Rsi : Uint8B
+0x158 Rbp : Uint8B
+0x160 ErrorCode : Uint8B
+0x160 ExceptionFrame : Uint8B
+0x168 Rip : Uint8B
+0x170 SegCs : Uint2B
+0x172 Fill0 : UChar
+0x173 Logging : UChar
+0x174 Fill1 : Uint2B
+0x178 EFlags : Uint4B
+0x17c Fill2 : Uint4B
+0x180 Rsp : Uint8B
+0x188 SegSs : Uint2B
+0x18a Fill3 : Uint2B
+0x18c Fill4 : Uint4B
kd> ?? sizeof(nt!_KTRAP_FRAME)
unsigned int64 0x190
这个结构体的部分成员在中断发生时自动被CPU
填写,具体地说,是 +0x160 (_KTRAP_FRAME.ErrorCode)
到 +0x188 (_KTRAP_FRAME.SegSs)
这一区间内的成员。
来自《Intel指令手册
》(第3卷
,第6章
,第6.12节
)
_KTRAP_FRAME
事实上是被CPU
存储在栈上的成员的一个扩展。它的目的是提供一个地方,去存储那些易失性寄存器,这些寄存器在调用C语言
编译生成的函数时会被破坏。
需要指出的一件非常重要的事是,被CPU
保存在栈上的指令寄存器(eip
) (_KTRAP_FRAME.Rip
)会被设为引发异常指令的下一句指令。在我们的场景中,这意味着_KTRAP_FRAME.Rip
成员会被设为紧跟int 3
的下一条指令,在上面的例子中,这个指令是ret(0xC3)
。
在易失性寄存器的值被保存后,nt!KiBreakpointTrap
执行一次快速的检查,以判断中断是由用户态(ring3
)还是内核态(ring0
)发生。如果执行流来自ring3
,一个swapgs
语句需要被执行,同时填写一些另外的调试寄存器。
最终,控制流会恢复,然后易失性浮点寄存器也会被保存到_KTRAP_FRAME
。在进入更多的异常处理逻辑前,指令指针会被从_KTRAP_FRAME.Rip
(在前面进入nt!KiBreakpointTrap
时被CPU
所保存)取出,并且减1
,然后作为一个参数被传入nt!KiExceptionDispatch
。另外,异常代码,EXCEPTION_BREAKPOINT(0x80000003)
,也会被传入。nt!KiExceptionDispatch
的声明如下:
void KiExceptionDispatch(DWORD ExceptionCode, DWORD NumberOfParameters, PVOID ExceptionAddress, ...);
需要指出的是nt!KiExceptionDispatch
(和nt!KiBreakpointTrap
一样)是用手工汇编写成。它假设ecx
包含异常代码,edx
是异常参数的数量(最多3个
),r8
包含异常发生的地址,r9
是第一个异常参数(如果有存在),r10
是第二个异常参数(如果存在),r11
是第三个异常参数(如果存在),rbp
指向_KTRAP_FRAME
结构体中的一个段(位于偏移+0x80
处)
.text:00000001401778C0 ; void KiExceptionDispatch(DWORD ExceptionCode, DWORD NumberOfParameters, PVOID ExceptionAddress, ...)
.text:00000001401778C0 KiExceptionDispatch proc near ; CODE XREF: KiDivideErrorFault+D9 p
.text:00000001401778C0 ; KiDebugTrapOrFault+193 p ...
.text:00000001401778C0
.text:00000001401778C0 ExceptionFrame = _KEXCEPTION_FRAME ptr -1D8h
.text:00000001401778C0 ExceptionRecord = _EXCEPTION_RECORD ptr -98h
.text:00000001401778C0
.text:00000001401778C0 sub rsp, 1D8h
.text:00000001401778C7 lea rax, [rsp+1D8h+ExceptionFrame._Rbx]
.text:00000001401778CF movaps xmmword ptr [rsp+1D8h+ExceptionFrame._Xmm6.Low], xmm6
.text:00000001401778D4 movaps xmmword ptr [rsp+1D8h+ExceptionFrame._Xmm7.Low], xmm7
.text:00000001401778D9 movaps xmmword ptr [rsp+1D8h+ExceptionFrame._Xmm8.Low], xmm8
.text:00000001401778DF movaps xmmword ptr [rsp+1D8h+ExceptionFrame._Xmm9.Low], xmm9
.text:00000001401778E5 movaps xmmword ptr [rsp+1D8h+ExceptionFrame._Xmm10.Low], xmm10
.text:00000001401778EB movaps xmmword ptr [rax-80h], xmm11 ; ExceptionFrame._Xmm11.Low
.text:00000001401778F0 movaps xmmword ptr [rax-70h], xmm12 ; ExceptionFrame._Xmm12.Low
.text:00000001401778F5 movaps xmmword ptr [rax-60h], xmm13 ; ExceptionFrame._Xmm13.Low
.text:00000001401778FA movaps xmmword ptr [rax-50h], xmm14 ; ExceptionFrame._Xmm14.Low
.text:00000001401778FF movaps xmmword ptr [rax-40h], xmm15 ; ExceptionFrame._Xmm15.Low
.text:0000000140177904 mov [rax+_KEXCEPTION_FRAME_GP._Rbx], rbx ; Part of the same ExceptionFrame structure on the stack.
.text:0000000140177907 mov [rax+_KEXCEPTION_FRAME_GP._Rdi], rdi
.text:000000014017790B mov [rax+_KEXCEPTION_FRAME_GP._Rsi], rsi
.text:000000014017790F mov [rax+_KEXCEPTION_FRAME_GP._R12], r12
.text:0000000140177913 mov [rax+_KEXCEPTION_FRAME_GP._R13], r13
.text:0000000140177917 mov [rax+_KEXCEPTION_FRAME_GP._R14], r14
.text:000000014017791B mov [rax+_KEXCEPTION_FRAME_GP._R15], r15
.text:000000014017791F mov rax, gs:_KPCR.Prcb.CurrentThread
.text:0000000140177928 bt [rax+_ETHREAD.Tcb.___u16.MiscFlags], 8
.text:000000014017792D jnb short IsKernelMode
.text:000000014017792F test byte ptr [rbp+0F0h], 1 ; _KTRAP_FRAME.SegCs
.text:0000000140177936 jz short IsKernelMode
.text:0000000140177938 call KiUmsExceptionEntry
.text:000000014017793D
.text:000000014017793D IsKernelMode: ; CODE XREF: KiExceptionDispatch+6D j
.text:000000014017793D ; KiExceptionDispatch+76 j
.text:000000014017793D lea rdi, [rsp+1D8h+ExceptionFrame.Return]
.text:0000000140177945 mov ebx, ecx
.text:0000000140177947 mov rcx, 14h
.text:000000014017794E xor eax, eax
.text:0000000140177950 rep stosq ; Zero out _EXCEPTION_RECORD structure.
.text:0000000140177953 sub rdi, 0A0h
.text:000000014017795A mov [rdi+_EXCEPTION_RECORD.ExceptionCode], ebx
.text:000000014017795C mov [rdi+_EXCEPTION_RECORD.ExceptionAddress], r8
.text:0000000140177960 mov [rdi+_EXCEPTION_RECORD.NumberParameters], edx
.text:0000000140177963 mov [rdi+_EXCEPTION_RECORD.ExceptionInformation], r9 ; ExceptionInformation[0]
.text:0000000140177967 mov [rdi+(_EXCEPTION_RECORD.ExceptionInformation+8)], r10 ; ExceptionInformation[1]
.text:000000014017796B mov [rdi+(_EXCEPTION_RECORD.ExceptionInformation+10h)], r11 ; ExceptionInformation[2]
.text:000000014017796F mov r9b, [rbp+0F0h] ; _KTRAP_FRAME.SegCs
.text:0000000140177976 and r9b, 1 ; PreviousMode
.text:000000014017797A mov byte ptr [rsp+20h], 1 ; FirstChance
.text:000000014017797F lea r8, [rbp-80h] ; TrapFrame
.text:0000000140177983 mov rdx, rsp ; ExceptionFrame
.text:0000000140177986 mov rcx, rdi ; ExceptionRecord
.text:0000000140177989 call KiDispatchException
.text:000000014017798E lea rcx, [rsp+1D8h+ExceptionFrame._Rbx]
.text:0000000140177996 movaps xmm6, xmmword ptr [rsp+1D8h+ExceptionFrame._Xmm6.Low]
.text:000000014017799B movaps xmm7, xmmword ptr [rsp+1D8h+ExceptionFrame._Xmm7.Low]
.text:00000001401779A0 movaps xmm8, xmmword ptr [rsp+1D8h+ExceptionFrame._Xmm8.Low]
.text:00000001401779A6 movaps xmm9, xmmword ptr [rsp+1D8h+ExceptionFrame._Xmm9.Low]
.text:00000001401779AC movaps xmm10, xmmword ptr [rsp+1D8h+ExceptionFrame._Xmm10.Low]
.text:00000001401779B2 movaps xmm11, xmmword ptr [rcx-80h] ; ExceptionFrame._Xmm11.Low
.text:00000001401779B7 movaps xmm12, xmmword ptr [rcx-70h] ; ExceptionFrame._Xmm12.Low
.text:00000001401779BC movaps xmm13, xmmword ptr [rcx-60h] ; ExceptionFrame._Xmm13.Low
.text:00000001401779C1 movaps xmm14, xmmword ptr [rcx-50h] ; ExceptionFrame._Xmm14.Low
.text:00000001401779C6 movaps xmm15, xmmword ptr [rcx-40h] ; ExceptionFrame._Xmm15.Low
.text:00000001401779CB mov rbx, [rcx+_KEXCEPTION_FRAME_GP._Rbx]
.text:00000001401779CE mov rdi, [rcx+_KEXCEPTION_FRAME_GP._Rdi]
.text:00000001401779D2 mov rsi, [rcx+_KEXCEPTION_FRAME_GP._Rsi]
.text:00000001401779D6 mov r12, [rcx+_KEXCEPTION_FRAME_GP._R12]
.text:00000001401779DA mov r13, [rcx+_KEXCEPTION_FRAME_GP._R13]
.text:00000001401779DE mov r14, [rcx+_KEXCEPTION_FRAME_GP._R14]
.text:00000001401779E2 mov r15, [rcx+_KEXCEPTION_FRAME_GP._R15]
.text:00000001401779E6 cli
.text:00000001401779E7 test byte ptr [rbp+0F0h], 1 ; _KTRAP_FRAME.SegCs
.text:00000001401779EE jz IsKernelMode_2
.text:00000001401779F4 mov rcx, gs:_KPCR.Prcb.CurrentThread
.text:00000001401779FD cmp [rcx+_ETHREAD.Tcb.___u27.ApcState.UserApcPending], 0
.text:0000000140177A04 jz short SkipApcProcessing
.text:0000000140177A06 mov ecx, 1
.text:0000000140177A0B mov cr8, rcx
.text:0000000140177A0F sti
.text:0000000140177A10 call KiInitiateUserApc
.text:0000000140177A15 cli
.text:0000000140177A16 mov ecx, 0
.text:0000000140177A1B mov cr8, rcx
.text:0000000140177A1F
.text:0000000140177A1F SkipApcProcessing: ; CODE XREF: KiExceptionDispatch+144 j
.text:0000000140177A1F mov rcx, gs:_KPCR.Prcb.CurrentThread
.text:0000000140177A28 test dword ptr [rcx+_ETHREAD.Tcb.Header.___u0.__s2.Type], 8000000h
.text:0000000140177A2E jz short SkipSetContextState
.text:0000000140177A30 call KiRestoreSetContextState
.text:0000000140177A35
.text:0000000140177A35 SkipSetContextState: ; CODE XREF: KiExceptionDispatch+16E j
.text:0000000140177A35 mov rcx, gs:_KPCR.Prcb.CurrentThread
.text:0000000140177A3E test dword ptr [rcx+_ETHREAD.Tcb.Header.___u0.__s2.Type], 40010000h
.text:0000000140177A44 jz short SkipUms
.text:0000000140177A46 test [rcx+_ETHREAD.Tcb.Header.___u0.__s7.DpcActive], 1
.text:0000000140177A4A jz short SkipCopyCounters
.text:0000000140177A4C call KiCopyCounters
.text:0000000140177A51 mov rcx, gs:_KPCR.Prcb.CurrentThread
.text:0000000140177A5A
.text:0000000140177A5A SkipCopyCounters: ; CODE XREF: KiExceptionDispatch+18A j
.text:0000000140177A5A test [rcx+_ETHREAD.Tcb.Header.___u0.__s6.DebugActive], 40h
.text:0000000140177A5E jz short SkipUms
.text:0000000140177A60 lea rsp, [rbp-80h] ; _KTRAP_FRAME.
.text:0000000140177A64 mov cl, 1
.text:0000000140177A66 call KiUmsExit
.text:0000000140177A6B
.text:0000000140177A6B SkipUms: ; CODE XREF: KiExceptionDispatch+184 j
.text:0000000140177A6B ; KiExceptionDispatch+19E j
.text:0000000140177A6B ldmxcsr dword ptr [rbp-54h] ; _KTRAP_FRAME._MxCsr
.text:0000000140177A6F cmp word ptr [rbp+80h], 0 ; _KTRAP_FRAME._Dr7
.text:0000000140177A77 jz short RestoreTrapFrame
.text:0000000140177A79 call KiRestoreDebugRegisterState
.text:0000000140177A7E
.text:0000000140177A7E RestoreTrapFrame: ; CODE XREF: KiExceptionDispatch+1B7 j
.text:0000000140177A7E movaps xmm0, xmmword ptr [rbp-10h] ; _KTRAP_FRAME._Xmm0
.text:0000000140177A82 movaps xmm1, xmmword ptr [rbp+0] ; _KTRAP_FRAME._Xmm1
.text:0000000140177A86 movaps xmm2, xmmword ptr [rbp+10h] ; _KTRAP_FRAME._Xmm2
.text:0000000140177A8A movaps xmm3, xmmword ptr [rbp+20h] ; _KTRAP_FRAME._Xmm3
.text:0000000140177A8E movaps xmm4, xmmword ptr [rbp+30h] ; _KTRAP_FRAME._Xmm4
.text:0000000140177A92 movaps xmm5, xmmword ptr [rbp+40h] ; _KTRAP_FRAME._Xmm5
.text:0000000140177A96 mov r11, [rbp-20h] ; _KTRAP_FRAME._R11
.text:0000000140177A9A mov r10, [rbp-28h] ; _KTRAP_FRAME._R10
.text:0000000140177A9E mov r9, [rbp-30h] ; _KTRAP_FRAME._R9
.text:0000000140177AA2 mov r8, [rbp-38h] ; _KTRAP_FRAME._R8
.text:0000000140177AA6 mov rdx, [rbp-40h] ; _KTRAP_FRAME._Rdx
.text:0000000140177AAA mov rcx, [rbp-48h] ; _KTRAP_FRAME._Rcx
.text:0000000140177AAE mov rax, [rbp-50h] ; _KTRAP_FRAME._Rax
.text:0000000140177AB2 mov rsp, rbp
.text:0000000140177AB5 mov rbp, [rbp+0D8h] ; _KTRAP_FRAME.Rbp
.text:0000000140177ABC add rsp, 0E8h ; Restore stack back to how it was when the CPU transferred
.text:0000000140177ABC ; execution to the interrupt routine (exclude error code).
.text:0000000140177ABC ;
.text:0000000140177ABC ; Basically, prepare for iretq.
.text:0000000140177AC3 swapgs ; Exchange gs back to usermode gs.
.text:0000000140177AC6 iretq
.text:0000000140177AC8 ; ---------------------------------------------------------------------------
.text:0000000140177AC8
.text:0000000140177AC8 IsKernelMode_2: ; _KTRAP_FRAME._MxCsr
.text:0000000140177AC8 ldmxcsr dword ptr [rbp-54h]
.text:0000000140177ACC movaps xmm0, xmmword ptr [rbp-10h] ; _KTRAP_FRAME._Xmm0
.text:0000000140177AD0 movaps xmm1, xmmword ptr [rbp+0] ; _KTRAP_FRAME._Xmm1
.text:0000000140177AD4 movaps xmm2, xmmword ptr [rbp+10h] ; _KTRAP_FRAME._Xmm2
.text:0000000140177AD8 movaps xmm3, xmmword ptr [rbp+20h] ; _KTRAP_FRAME._Xmm3
.text:0000000140177ADC movaps xmm4, xmmword ptr [rbp+30h] ; _KTRAP_FRAME._Xmm4
.text:0000000140177AE0 movaps xmm5, xmmword ptr [rbp+40h] ; _KTRAP_FRAME._Xmm5
.text:0000000140177AE4 mov r11, [rbp-20h] ; _KTRAP_FRAME._R11
.text:0000000140177AE8 mov r10, [rbp-28h] ; _KTRAP_FRAME._R10
.text:0000000140177AEC mov r9, [rbp-30h] ; _KTRAP_FRAME._R9
.text:0000000140177AF0 mov r8, [rbp-38h] ; _KTRAP_FRAME._R8
.text:0000000140177AF4 mov rdx, [rbp-40h] ; _KTRAP_FRAME._Rdx
.text:0000000140177AF8 mov rcx, [rbp-48h] ; _KTRAP_FRAME._Rcx
.text:0000000140177AFC mov rax, [rbp-50h] ; _KTRAP_FRAME._Rax
.text:0000000140177B00 mov rsp, rbp
.text:0000000140177B03 mov rbp, [rbp+0D8h] ; _KTRAP_FRAME._Rbp
.text:0000000140177B0A add rsp, 0E8h
.text:0000000140177B11 iretq
.text:0000000140177B11 KiExceptionDispatch endp
在nt!KiExceptionDispatch
的入口处,第一件发生的事情是生成一个_KEXCEPTION_FRAME
。_KTRAP_FRAME
用来存储易失性寄存器,_KEXCEPTION_FRAME
则提供一个地方用来存储所有非易失性寄存器:
kd> dt nt!_KEXCEPTION_FRAME -b
+0x000 P1Home : Uint8B
+0x008 P2Home : Uint8B
+0x010 P3Home : Uint8B
+0x018 P4Home : Uint8B
+0x020 P5 : Uint8B
+0x028 Spare1 : Uint8B
+0x030 Xmm6 : _M128A
+0x000 Low : Uint8B
+0x008 High : Int8B
+0x040 Xmm7 : _M128A
+0x000 Low : Uint8B
+0x008 High : Int8B
+0x050 Xmm8 : _M128A
+0x000 Low : Uint8B
+0x008 High : Int8B
+0x060 Xmm9 : _M128A
+0x000 Low : Uint8B
+0x008 High : Int8B
+0x070 Xmm10 : _M128A
+0x000 Low : Uint8B
+0x008 High : Int8B
+0x080 Xmm11 : _M128A
+0x000 Low : Uint8B
+0x008 High : Int8B
+0x090 Xmm12 : _M128A
+0x000 Low : Uint8B
+0x008 High : Int8B
+0x0a0 Xmm13 : _M128A
+0x000 Low : Uint8B
+0x008 High : Int8B
+0x0b0 Xmm14 : _M128A
+0x000 Low : Uint8B
+0x008 High : Int8B
+0x0c0 Xmm15 : _M128A
+0x000 Low : Uint8B
+0x008 High : Int8B
+0x0d0 TrapFrame : Uint8B
+0x0d8 OutputBuffer : Uint8B
+0x0e0 OutputLength : Uint8B
+0x0e8 Spare2 : Uint8B
+0x0f0 MxCsr : Uint8B
+0x0f8 Rbp : Uint8B
+0x100 Rbx : Uint8B
+0x108 Rdi : Uint8B
+0x110 Rsi : Uint8B
+0x118 R12 : Uint8B
+0x120 R13 : Uint8B
+0x128 R14 : Uint8B
+0x130 R15 : Uint8B
+0x138 Return : Uint8B
kd> ?? sizeof(nt!_KEXCEPTION_FRAME)
unsigned int64 0x140
nt!KiExceptionDispatch
还会在栈上创建一个_EXCEPTION_RECORD
.aspx)结构体。如果你写过任何Windows
上的错误处理例程(可以是用户模式或内核模式),你会很熟悉这个数据结构,因为它包含一个子结构体_EXCEPTION_POINTERS
.aspx)。我们在上面的例子里面同时使用了这两个结构体。
更进一步,这解释了我们谜团中的第一部分,顾名思义,为什么_EXCEPTION_RECORD.ExceptionAddress
是不正确的。回想一下_EXCEPTION_RECORD.ExceptionAddress
是从给nt!KiExceptionDispatch
的r8寄存器参数中传递过来的,而这个值来自nt!KiBreakpointTrap
。这个值正是被减1
后的_KTRAP_FRAME.Rip
成员的一份拷贝。
为了找出_CONTEXT.Rip
成员是如何被填充的,我们需要将兔子洞再挖得深一点。
nt!KiExceptionDispatch
会调用nt!KiDispatchException
(是的,这两个单词的顺序被有意翻转),同时传入刚创建的_EXCEPTION_RECORD
和_KEXCEPTION_FRAME
:
void __fastcall KiDispatchException(_EXCEPTION_RECORD *ExceptionRecord, _KEXCEPTION_FRAME *ExceptionFrame, _KTRAP_FRAME *TrapFrame, KPROCESSOR_MODE PreviousMode, BOOLEAN FirstChance);
这个函数会在_KTRAP_FRAME和_KEXCEPTION_FRAME
之外构造一个_CONTEXT
:通过调用辅助例程KeContextFromKFrame
。在_CONTEXT
被创建后,会对_EXCEPTION_RECORD.ExceptionCode
做一次检查(作为nt!KiExceptionDispatch
的一个参数被接收),以判断它是否是STATUS_BREAKPOINT (0x80000003)
。如果是,_CONTEXT.Rip
成员会被减1:
...
KeContextFromKframes(TrapFrame, ExceptionFrame, &ContextFrame);
if ( ExceptionRecord->ExceptionCode == STATUS_BREAKPOINT /* 0x80000003 */ )
--ContextFrame.Rip;
...
这解释了最后一个谜团,从而导致_CONTEXT.Rip
的值也被破坏。
反调试技巧
知道了Windows
如何处理不同的int 3
类型后,就可以利用这一差异来检测反调试吗?答案是肯定的。
调试器会在异常发生的时候显示程序的状态。既然Windows
会不正确地假设我们的int 3
异常来自单字节的情况,完全可以迷惑调试器,让它读取“额外”内存。我们利用这种不一致来进行一趟到“守护页”的旅行。
正如我们在我们的第一个例子里看到的那样(见本文的开始),当多字节的int 3
出现时,_EXCEPTION_RECORD.ExceptionAddress
和_CONTEXT.Rip
会位于我们多字节指令的中间而不是开始。这意味着调试器将会不正确地认为抛出软件断点的那条指令开始于操作码0xC3
。当我们引用靠谱的Intel指令手册
时,我们可以看到这个操作码代表一个2字节
的add
指令:
来自《Intel指令手册
》(第2卷
,第3章
,第3.2节
)
如果我们将我们的多字节int 3
指令放在一个内存页的最后会发生什么?
当操作系统提示(我们附加的)调试器一个断点异常发生时,指令指针会被指向(被操作系统)错误解释为一个add(0x03)
指令开始处的内存地址。这会导致调试器去反汇编相邻页的数据(既然这是一个2字节
长度的指令),然后有效阅读一个我们“合法”内存外的字节。
我们的技巧依赖于Windows
上的一个事实:作为一种优化手段,Windows
并不会将虚拟内存提交到物理内存,除非它必须需要它。也就是说大多数内存,尤其在用户态,是被分页的。当不在物理内存中的内存需要被使用时,一个缺页异常会发生。想要了解更多关于内存管理的知识,可以阅读我们网站上的下列文章:Introduction to IA-32e hardware paging
和Exploring Windows virtual memory management
.
所以,我们可以通过调用QueryWorkingSetEx
.aspx) 检测到读取相邻页的内存,因为这个过程会插入对应的PTE
(页表入口)。如果相邻页位于我们进程的工作集中(例如,被调试器映射到我们的进程中),_PSAPI_WORKING_SET_EX_BLOCK
.aspx) 中的有效位就会被设定。
PoC
|| GTFO
一个完整的例子如下:
/*
* Module Name:
* antidebug_long_int3.cpp
*
* Abstract:
* Attempts to detect the presence of a debugger
* by issuing a multi-byte int 3 and inspecting
* page PTE mappings.
*
* Author:
* Nemanja (Nemi) Mulasmajic <nm@triplefault.io>
* http://triplefault.io
*/
#pragma warning(disable: 4710)
#pragma warning(push, 0)
#include <windows.h>
#include <stdio.h>
#include <Psapi.h>
#pragma warning(pop)
#pragma comment(lib, "psapi.lib")
// The size of an architecture page on x86/x64.
#define PAGE_SIZE 0x1000
// A pseudo-handle that represents the current process.
#define NtCurrentProcess() ((HANDLE)-1)
// Multi-byte int 3.
BYTE _LongInt3[] = { 0xCD, 0x03 };
int main(void)
{
int status = -1;
// We allocate 2 contiguous pages of executable virtual memory.
PBYTE Memory = (PBYTE)VirtualAlloc(NULL, (PAGE_SIZE * 2), (MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE);
if (!Memory)
{
fprintf(stderr, "[-] ERROR: Failed to allocate memory.\n");
goto Cleanup;
}
// The first page, "page 1", will have the multi-byte form of int 3
// embedded at the very end of the page. This is done so that the next
// byte immediately after the multi-byte int 3 will span into "page 2".
//
// e.g:
// | Page 1 | Page 2
// [.....................0xCD 0x03][............................]
PBYTE CodeLocation = &Memory[PAGE_SIZE - sizeof(_LongInt3)];
PBYTE DeadPageLocation = &Memory[PAGE_SIZE];
// Add the int 3 to the very end of page 1.
memcpy(CodeLocation, _LongInt3, sizeof(_LongInt3));
// Page 2 should never be accessed and therefore should not be
// present in our process' working set.
PSAPI_WORKING_SET_EX_INFORMATION wsi;
wsi.VirtualAddress = DeadPageLocation;
if (!QueryWorkingSetEx(NtCurrentProcess(), &wsi, sizeof(wsi)))
{
fprintf(stderr, "[-] ERROR: QueryWorkingSetEx failed with error: 0x%lx.\n", GetLastError());
goto Cleanup;
}
if (wsi.VirtualAttributes.Valid)
{
fprintf(stderr, "[-] ERROR: Page is expected to be invalid. Make sure you have not inadvertently accessed this page.\n");
goto Cleanup;
}
__try
{
// Invoke the long form of int 3.
((void(*)())CodeLocation)();
}
__except (EXCEPTION_EXECUTE_HANDLER) { }
// If a debugger caught the exception, even if it was passed to the
// application via "go unhandled", it will have, most likely, mapped
// page 2 into the application's memory space.
if (!QueryWorkingSetEx(NtCurrentProcess(), &wsi, sizeof(wsi)))
{
fprintf(stderr, "[-] ERROR: QueryWorkingSetEx failed with error: 0x%lx.\n", GetLastError());
goto Cleanup;
}
printf("[+] Debugger %s.\n", ((wsi.VirtualAttributes.Valid) ? "detected" : "not detected"));
status = 0;
Cleanup:
// Free allocated memory.
if (Memory)
{
VirtualFree(Memory, 0, MEM_FREE);
Memory = NULL;
}
// Wait for [ENTER] key press to terminate the program.
getchar();
return status;
}
[课程]FART 脱壳王!加量不加价!FART作者讲授!