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

x86_64 - can a 64-bit application on windows execute INT 2E instead of syscall?

这个问题与 this one 有关,但它并没有填补我的一些空白,所以我决定再问一遍,提供更多细节,也许可以悬赏一下。

无论如何,通常如果你在 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

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

    public ZwClose
     ZwClose proc near
     mov     eax, 3000Fh     ; NtClose
     mov     edx, offset j_Wow64Transition
     call    edx ; j_Wow64Transition
     retn    4
     ZwClose endp

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

So as far as I understand Wow64Transition will eventually jump to the 64-bit version of ntdll which I listed first, right?

是的。

If that's so, is that when INT 2E is executed instead of syscall?

没有

首先,让我们明白一点:您仍然可以在现代 Windows 系统上毫无问题地调用 INT 0x2E,中断向量仍然存在并指向系统调用调度程序:

0: kd> !idt 0x2e

Dumping IDT: fffff8010a900000

2e:     fffff8010ca11ec0 nt!KiSystemServiceShadow

是什么让它调用 int 0x2E?

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

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

的字段
0: kd> dt _kuser_shared_data
nt!_KUSER_SHARED_DATA
...
   +0x308 SystemCall       : Uint4B
...

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

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

HvlEnlightenments 是一个位域,当虚拟化的 OS 知道它实际上被虚拟化时设置(那些 OS 被称为“开明的 OS”)。这意味着很好,该功能(调用 INT 0x2E 而不是 SYSCALL)与虚拟化相关 OS.

我们还剩下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

我们可以看出这个函数的第一个参数很重要;恰好是一个名为KeLoaderBlock:

的全局内核变量
PAGELK:0000000140597154                 mov     rcx, cs:KeLoaderBlock_0
PAGELK:000000014059715B                 call    KiInitializeBootStructures

它的类型已知为 _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

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

              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                     
              }; 

bt eax, 8 指令正在测试位 8,因此是 VsmConfigured 位。

因此如果我们被虚拟化并且VsmConfigured是1,那么我们使用INT 0x2E。

为什么?

VSM 代表 Virtual Secure Mode which introduces VTLs (Virtual Trust Level) which are used to segregate parts of the OS itself: for example VTL0 is the so-called "normal world" where the "usual" part of the OS resides (including the kernel and its virtual space) while VTL1 harbors the secure kernel and very specific processes known as "truslets" (see IUM 更长的解释)。

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

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


写完这个答案后,我看到有人在更详尽解释的博客 post:The Windows 10 TH2 INT 2E mystery 中实际上追逐了相同的路径。他们不确定内核将在哪种情况下使用 INT2E。我们只能猜测这一点。

can a 64-bit application on windows execute INT 2E instead of syscall?

这取决于你到底想知道什么:

  • 如果你想问你的应用程序是否可以安全调用int 2e,答案是:不!

    根据 a table found in the internetEAX=0Fh 在 Windows 10 中是 ZwClose,但在 Windows 7 中是 ZwOpenKey

    正如您在 table 中看到的,某些值(例如 EAX=06Dh)的含义甚至在 Windows 10 更新期间发生了变化!

    如果您直接使用INT 2E,有可能您的应用现在可以正常使用,但在下次更新后就无法使用了!

    syscall 也是如此 - 因此您不能在自己的应用程序中使用它们。

    (Microsoft DLL 只能使用 INT 2Esyscall,如果在更新内核时替换 DLL。)

  • 如果您想知道如果您在 64 位应用程序中使用 INT 2E 而不是 syscall 会发生什么:

    我只能推测 - 特别是因为 Microsoft 也可能会在下一次更新后更改行为(如果从 64 位应用程序使用 INT 2E 会发生什么)。

    但是,行为 可能 类似于 Linux:

    在Linux中,INT 80将所有地址(指针)解释为32位值; syscall 会将它们解释为 64 位值。出于这个原因,如果没有指针(地址)被传递给内核,则可以从 64 位应用程序使用 INT 80(在 Windows 下,ZwClose 就是一个例子) .但是,不可能将指针传递给内核(例如 ZwOpenFile)。