前言
各大论坛、博客讲利用NMI插中断检查堆栈的时候普遍有一个错误:在NMI回调中直接调用了RtlWalkFrameChain或者RtlCaptureStackBackTrace。比如R0g大佬这里百密一疏:[原创]2024鹅厂游戏安全技术竞赛决赛题解-PC客户端-CTF对抗-看雪论坛-安全社区|非营利性质技术交流社区

64位下,这两个函数会扫描PE文件的UNWIND_INFO来解析栈帧信息,此时如果触发Page Fault那就蓝屏GG
NMI中断
正确的思路是在KPCR里找到NMI的异常栈地址,解析MACHINE_FRAME去拿RIP和RSP,再来判断是否rootkit
IST Index是记录在IDT表项中,触发中断/异常时的异常栈指针索引号

NMI中断在机器上映射的中断向量号总是2:

找到对应的IDT表项,这里的03就是IST Index,说明NMI触发时使用了Ist3这个栈指针

Ist3具体的值保存在TSS段中,如下:
1//0x68 bytes (sizeof)
2struct _KTSS64
3{
4 ULONG Reserved0; //0x0
5 ULONGLONG Rsp0; //0x4
6 ULONGLONG Rsp1; //0xc
7 ULONGLONG Rsp2; //0x14
8 ULONGLONG Ist[8]; //0x1c
9 ULONGLONG Reserved1; //0x5c
10 USHORT Reserved2; //0x64
11 USHORT IoMapBase; //0x66
12};
TSS在KPCR中:
1struct _KPCR
2{
3 union
4 {
5 struct _NT_TIB NtTib; //0x0
6 struct
7 {
8 union _KGDTENTRY64* GdtBase; //0x0
9 struct _KTSS64* TssBase; // <--- TSS //0x8
10 ...
11 }
12 ...
13 }
14 ...
15}
触发中断时操作系统会把返回地址的结构MACHINE_FRAME压入栈中以便IRETQ,从这里拿到RIP && RSP
1typedef struct _MACHINE_FRAME
2{
3 UINT64 rip;
4 UINT64 cs;
5 UINT64 eflags;
6 UINT64 rsp;
7 UINT64 ss;
8
9} MACHINE_FRAME, *PMACHINE_FRAME;
10
11BOOLEAN
12NmiCallback(_In_ BOOLEAN Handled)
13{
14 UNREFERENCED_PARAMETER(Handled);
15
16 UINT64 kpcr = 0;
17 TASK_STATE_SEGMENT_64* tss = NULL;
18 PMACHINE_FRAME machineFrame = NULL;
19
20
21 kpcr = __readmsr(IA32_GS_BASE);// 0xC0000101
22 tss = *(TASK_STATE_SEGMENT_64**)(kpcr + KPCR_TSS_BASE_OFFSET);// 0x8
23 machineFrame = tss->Ist3 - sizeof(MACHINE_FRAME);
24
25 {
26 // 在这里对堆栈检查,比如是否在有效模块内
27 // 不在的话则认为是内核shellcode
28 CheckStack(machineFrame->rip,machineFrame->rsp);
29
30 }
31}
如果查到返回地址不在任何驱动模块内,则可以判断是类似Kdmapper这样的手动映射加载驱动,或者是内核shellcode
结语
个人感觉这种方法用在反作弊上效果是比较好的;因为外挂会高频读取游戏数据,用中断的方法很容易命中外挂的线程。