0:000> u ntdll!KiFastSystemCall ntdll!KiFastSystemCall: 7c92eb8b 8bd4 mov edx,esp 7c92eb8d 0f34 sysenter ntdll!KiFastSystemCallRet: 7c92eb8f 90 nop 7c92eb90 90 nop 7c92eb91 90 nop 7c92eb92 90 nop 7c92eb93 90 nop ntdll!KiFastSystemCallRet: 7c92eb94 c3 ret
而我们通过dump disasm得到的结果
0x77d1a146 : FF 15 24 14 D1 77 call dword ptr [0x77d11424] 【ntdll.dll.[.text].NtCallbackReturn】 0x7c92d51d : B8 14 00 00 00 mov eax, 0x14 0x7c92d522 : BA 00 03 FE 7F mov edx, 0x7ffe0300 0x7c92d527 : FF 12 call dword ptr [edx]
【ntdll.dll.[.text].KiFastSystemCall】 0x7c92eb8b : 8B D4 mov edx, esp 0x7c92eb8d : 0F 34 sysenter 0x7c92eb94 : C3 ret
0x77ef67d4 : C3 ret
根据上面的结果显示,KiFastSystemCall在执行结束本来应该返回到0x7c92d527的下一条指令即0x7c92d529处执行,但是为什么会返回到0x77ef67d4处呢?
通过查看调用栈
call [0x0012f43c] : 0x7c92d527 --> 0x7c92eb8b
return [0x0012f458] : 0x77ef67d4 <-- 0x7c92eb94
Thread 38:
[# 0] [0x0012fb20]:[0x7c941739 - 0x7c94173e] ==> [0x7c93c9e4] ntdll.dll.[.text].LdrFindResourceDirectory_U_0x000000af [# 1] [0x0012fa18]:[0x7c93cba6 - 0x7c93cbab] ==> [0x7c921193] ntdll.dll.[.text].LdrInitializeThunk_0x00000015 [# 2] [0x0012f9f8]:[0x7c9211a4 - 0x7c9211a7] ==> [0x77d1f518] USER32.dll.[.text].UserClientDllInitialize [# 3] [0x0012f46c]:[0x77d1f76f - 0x77d1f774] ==> [0x77d1f791] USER32.dll.[.text].UserClientDllInitialize_0x00000279 [# 4] [0x0012f460]:[0x77ef655e - 0x77ef6563] ==> [0x77ef67c8] GDI32.dll.[.text].GdiProcessSetup_0x000001ac [# 5] [0x0012f45c]:[0x77ef67d2 - 0x77ef67d4] ==> [0x7c92eb8b] ntdll.dll.[.text].KiFastSystemCall [# 6] [0x0012f450]:[0x7c92eae0 - 0x7c92eae3] ==> [0x77d1a12e] USER32.dll.[.text].ClientThreadSetup_0x00000134 [# 7] [0x0012f440]:[0x77d1a146 - 0x77d1a14c] ==> [0x7c92d51d] ntdll.dll.[.text].NtCallbackReturn --> [# 8] [0x0012f43c]:[0x7c92d527 - 0x7c92d529] ==> [0x7c92eb8b] ntdll.dll.[.text].KiFastSystemCall
可见,0x77ef67d4是在第一次调用KiFastSystemCall的时候,保存在栈上的返回地址,当时的栈的位置是0x0012f45c(还没有将返回地址压到栈上时);
而在返回时,栈的位置是0x0012f458,因此正好是返回到了第一次调用KiFastSystemCall的位置。
如果是这样,那么我们的影子调用栈出现了什么问题呢?
我们知道,当用户态程序进入内核态执行后(通过sysenter),内核可能会调度到用户态的APC等等回调函数执行,这些回调函数的执行发起者是内核态代码,因此它们需要将执行流程返回给内核代码;
Windows提供了一种机制,当用户态的回调函数没有提供显式的返回到内核的代码时,Windows会自动执行默认的返回到内核态的代码,而这一段代码恰好也是通过KiFastSystemCall机制完成的(因为KiFastSystemCall是用户态通向内核态的唯一入口),这一机制被称为NtCallbackReturn。
因此,我们怀疑,NtCallbackReturn机制会使用户态的代码孤立起来看,是不符合栈平衡的,原因就是NtCallbackReturn的内核服务程序中会对用户态的栈做调整,以掩盖异步回调函数被执行过的痕迹。
因此,我们相信,NtCallbackReturn的存在,使得我们单纯地依据用户态的call/ret指令建立起来的影子调用栈出现了不平衡的现象。
解决方法,就是在调用过NtCallbackReturn之后,添加三次等效ret的效果,可以解决影子栈的不平衡问题。
事情到了这里,还没有结束,因为还不知道有哪些系统调用会破坏用户态栈的平衡。
比如NtContinue就很可疑(根据实现结果),那么用户态的影子调用栈要通过什么方式来保证其正确性呢?
更普遍的做法,可以根据KiFastSystemCallRet来判断,它可以与KiFastSystemCall配对,从而消除中间的多余项目。