x86_64 - Windows 上的 64 位应用程序可以执行 INT 2E 而不是系统调用吗?

Tre*_*rey 3 x86 assembly winapi x86-64 system-calls

这个问题与这个问题相关,但它没有填补我的一些空白,所以我决定再次询问它并提供更多细节,也许可以对此给予赏金。

无论如何,通常如果您在 ntdll 上查找 Nt/Zw 函数,您会看到类似以下内容的内容:

ZwClose         proc near
mov     r10, rcx
mov     eax, 0Fh
test    byte ptr ds:7FFE0308h, 1
jnz     short loc_a
syscall
retn

loc_a:
int     2Eh
retn
NtClose         endp
Run Code Online (Sandbox Code Playgroud)

现在我知道这是比较KUSER_SHARED_DATA的偏移量并决定是否执行系统调用INT 2E。起初我认为如果正在运行的程序是 32 位应用程序,则会执行 INT 2E,但在阅读了一些有关 WOW64 的内容后,这些应用程序似乎会使用 32 位版本的 ntdll,它不执行 int 2e 而是执行穿过天堂之门到达内核:

    public ZwClose
     ZwClose proc near
     mov     eax, 3000Fh     ; NtClose
     mov     edx, offset j_Wow64Transition
     call    edx ; j_Wow64Transition
     retn    4
     ZwClose endp
Run Code Online (Sandbox Code Playgroud)

据我了解,Wow64Transition最终会跳转到我首先列出的 ntdll 的 64 位版本,对吗?如果是这样,是不是执行 INT 2E 而不是系统调用时?我还被告知 INT 2E 的原因之一是CET兼容性,所以我对 INT 2E 有点困惑。

Nei*_*tsa 7

据我了解,Wow64Transition 最终会跳转到我首先列出的 ntdll 的 64 位版本,对吗?

是的。

如果是这样,是不是执行 INT 2E 而不是系统调用时?

不。

首先,让我们先弄清楚这一点:您仍然可以在现代 Windows 系统上调用 INT 0x2E,没有任何问题,中断向量仍然在这里并指向系统调用调度程序:

0: kd> !idt 0x2e

Dumping IDT: fffff8010a900000

2e:     fffff8010ca11ec0 nt!KiSystemServiceShadow
Run Code Online (Sandbox Code Playgroud)

是什么让它调用 int 0x2E?

正如您所看到的,执行ring3/ring0转换的代码片段检查KUSER_SHARED_DATA结构中的一位。

在偏移量 0x308 处,我们有一个名为 的字段SystemCall

0: kd> dt _kuser_shared_data
nt!_KUSER_SHARED_DATA
...
   +0x308 SystemCall       : Uint4B
...
Run Code Online (Sandbox Code Playgroud)

KUSER_SHARED_DATA 映射到两个不同的地址:一个位于用户空间 (0x7FFE0000),另一个位于内核空间 (0xFFFFF78000000000)。这两个地址都由同一物理页支持(用户空间显然是只读的)。

请注意,这些地址是恒定的并且不受 ASLR 的影响。因此,我们可以在内核中搜索该0xFFFFF78000000308地址(即,KUSER_SHARED_DATA.SystemCall但在内核中)并查看是否有匹配项。

实际上,名为的函数中只有一个匹配项KiInitializeKernel

PAGELK:00000001405A36A2                 mov     r14d, 1
PAGELK:00000001405A36A8                 cmp     cs:KiSystemCallSelector, r14d
PAGELK:00000001405A36AF                 jnz     loc_1405A3161
;...
PAGELK:00000001405A9236                 test    cs:HvlEnlightenments, 80000h
PAGELK:00000001405A9240                 jz      loc_1405A3161
PAGELK:00000001405A9246                 mov     eax, r14d
PAGELK:00000001405A9249                 mov     ds:0FFFFF78000000308h, eax
Run Code Online (Sandbox Code Playgroud)

因此,如果KiSystemCallSelector为 1 并且HvlEnlightenments设置了位 19,则KUSER_SHARED_DATA.SystemCall设置为 1。

HvlEnlightenments是当虚拟化操作系统知道它实际上是虚拟化时设置的位字段(这些操作系统称为“enlightened OS”)。这意味着该功能(调用 INT 0x2E 而不是 SYSCALL)与虚拟化操作系统相关。

我们只剩下KiSystemCallSelector;该变量在一个名为的函数中设置KiInitializeBootStructures

PAGELK:00000001405A1E48                 mov     rsi, rcx ; rsi = rcx (1st function param)
; ...
PAGELK:00000001405A2052                 mov     rdx, [rsi+0F0h]
PAGELK:00000001405A2059                 mov     eax, [rdx+74h]
; ...
PAGELK:00000001405A206A loc_1405A206A:
PAGELK:00000001405A206A                 bt      eax, 8
PAGELK:00000001405A206E                 jnb     short loc_1405A2077
PAGELK:00000001405A2070                 mov     cs:KiSystemCallSelector, r13d ; r13d = 1
Run Code Online (Sandbox Code Playgroud)

我们可以看到这个函数的第一个参数很重要;它恰好是一个名为的全局内核变量KeLoaderBlock

PAGELK:0000000140597154                 mov     rcx, cs:KeLoaderBlock_0
PAGELK:000000014059715B                 call    KiInitializeBootStructures
Run Code Online (Sandbox Code Playgroud)

它的类型是已知的,_LOADER_PARAMETER_BLOCK并且它的定义在内核符号中是公开的,因此前面的代码看起来像这样,带有符号信息:

PAGELK:00000001405A2052                 mov     rdx, [rsi+_LOADER_PARAMETER_BLOCK.Extension] ; _LOADER_PARAMETER_EXTENSION*
PAGELK:00000001405A2059                 mov     eax, [rdx+_LOADER_PARAMETER_EXTENSION._bf_74] ; bit field
; ...
PAGELK:00000001405A206A loc_1405A206A:
PAGELK:00000001405A206A                 bt      eax, 8
PAGELK:00000001405A206E                 jnb     short loc_1405A2077
PAGELK:00000001405A2070                 mov     cs:KiSystemCallSelector, r13d ; r13d = 1
Run Code Online (Sandbox Code Playgroud)

在结构的偏移 0x74 处,_LOADER_PARAMETER_EXTENSION我们有一个位域:

              struct                                                                               // 22 elements, 0x4 bytes (sizeof)    
              {                                                                                                                          
/*0x074*/         ULONG32      LastBootSucceeded : 1;                                              // 0 BitPosition                      
/*0x074*/         ULONG32      LastBootShutdown : 1;                                               // 1 BitPosition                      
/*0x074*/         ULONG32      IoPortAccessSupported : 1;                                          // 2 BitPosition                      
/*0x074*/         ULONG32      BootDebuggerActive : 1;                                             // 3 BitPosition                      
/*0x074*/         ULONG32      StrongCodeGuarantees : 1;                                           // 4 BitPosition                      
/*0x074*/         ULONG32      HardStrongCodeGuarantees : 1;                                       // 5 BitPosition                      
/*0x074*/         ULONG32      SidSharingDisabled : 1;                                             // 6 BitPosition                      
/*0x074*/         ULONG32      TpmInitialized : 1;                                                 // 7 BitPosition                      
/*0x074*/         ULONG32      VsmConfigured : 1;                                                  // 8 BitPosition                      
/*0x074*/         ULONG32      IumEnabled : 1;                                                     // 9 BitPosition                      
/*0x074*/         ULONG32      IsSmbboot : 1;                                                      // 10 BitPosition                     
/*0x074*/         ULONG32      BootLogEnabled : 1;                                                 // 11 BitPosition                     
/*0x074*/         ULONG32      DriverVerifierEnabled : 1;                                          // 12 BitPosition                     
/*0x074*/         ULONG32      SuppressMonitorX : 1;                                               // 13 BitPosition                     
/*0x074*/         ULONG32      SuppressSmap : 1;                                                   // 14 BitPosition                     
/*0x074*/         ULONG32      Unused : 6;                                                         // 15 BitPosition                     
/*0x074*/         ULONG32      FeatureSimulations : 6;                                             // 21 BitPosition                     
/*0x074*/         ULONG32      MicrocodeSelfHosting : 1;                                           // 27 BitPosition                     
/*0x074*/         ULONG32      XhciLegacyHandoffSkip : 1;                                          // 28 BitPosition                     
/*0x074*/         ULONG32      DisableInsiderOptInHVCI : 1;                                        // 29 BitPosition                     
/*0x074*/         ULONG32      MicrocodeMinVerSupported : 1;                                       // 30 BitPosition                     
/*0x074*/         ULONG32      GpuIommuEnabled : 1;                                                // 31 BitPosition                     
              }; 
Run Code Online (Sandbox Code Playgroud)

bt eax, 8指令正在测试位 8,即该VsmConfigured位。

因此,如果我们是虚拟化的且VsmConfigured为 1,则我们使用 INT 0x2E。

为什么?

VSM 代表Virtual Secure Mode引入了 VTL(虚拟信任级别),用于隔离操作系统本身的各个部分:例如,VTL0 是所谓的“正常世界”,操作系统的“常用”部分(包括内核及其虚拟空间),而 VTL1 包含安全内核和称为“truslet”的非常具体的进程(请参阅IUM参考资料以获得更长的解释)。

那时我只能猜测;我的第一个想法是调用 INT 0x2E 仅适用于特定内核(不是 VTL0 中的“正常”内核,但我仍然不知道是哪一个)。

实际上,VMM(虚拟机管理程序)比系统调用更容易捕获 VM 退出以进行中断;当某些事件(例如 INT、RDMSR、WMSR 等特定指令)发生时,会发生 VM 退出,这些事件使代码从正常执行流转换回虚拟机管理程序,因此虚拟机管理程序实际上可以查看触发 VM 退出的原因并采取行动相应地(例如重定向代码流或“撒谎”到操作系统)。


写完这个答案后,我看到有人实际上在一篇更彻底解释的博客文章中追寻了同样的道路: Windows 10 TH2 INT 2E 之谜。但他们不确定内核在哪种具体情况下将使用 INT2E。到那时我们只能猜测。