前言

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

image-20250922213245093

64位下,这两个函数会扫描PE文件的UNWIND_INFO来解析栈帧信息,此时如果触发Page Fault那就蓝屏GG

NMI中断

正确的思路是在KPCR里找到NMI的异常栈地址,解析MACHINE_FRAME去拿RIPRSP,再来判断是否rootkit

IST Index是记录在IDT表项中,触发中断/异常时的异常栈指针索引号

image-20250922204640032

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

image-20250922204503469

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

image-20250922204341448

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

结语

个人感觉这种方法用在反作弊上效果是比较好的;因为外挂会高频读取游戏数据,用中断的方法很容易命中外挂的线程。