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 internet,EAX=0Fh
在 Windows 10 中是 ZwClose
,但在 Windows 7 中是 ZwOpenKey
。
正如您在 table 中看到的,某些值(例如 EAX=06Dh
)的含义甚至在 Windows 10 更新期间发生了变化!
如果您直接使用INT 2E
,有可能您的应用现在可以正常使用,但在下次更新后就无法使用了!
syscall
也是如此 - 因此您不能在自己的应用程序中使用它们。
(Microsoft DLL 只能使用 INT 2E
或 syscall
,如果在更新内核时替换 DLL。)
如果您想知道如果您在 64 位应用程序中使用 INT 2E
而不是 syscall
会发生什么:
我只能推测 - 特别是因为 Microsoft 也可能会在下一次更新后更改行为(如果从 64 位应用程序使用 INT 2E
会发生什么)。
但是,行为 可能 类似于 Linux:
在Linux中,INT 80
将所有地址(指针)解释为32位值; syscall
会将它们解释为 64 位值。出于这个原因,如果没有指针(地址)被传递给内核,则可以从 64 位应用程序使用 INT 80
(在 Windows 下,ZwClose
就是一个例子) .但是,不可能将指针传递给内核(例如 ZwOpenFile
)。
这个问题与 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 internet,
EAX=0Fh
在 Windows 10 中是ZwClose
,但在 Windows 7 中是ZwOpenKey
。正如您在 table 中看到的,某些值(例如
EAX=06Dh
)的含义甚至在 Windows 10 更新期间发生了变化!如果您直接使用
INT 2E
,有可能您的应用现在可以正常使用,但在下次更新后就无法使用了!syscall
也是如此 - 因此您不能在自己的应用程序中使用它们。(Microsoft DLL 只能使用
INT 2E
或syscall
,如果在更新内核时替换 DLL。)如果您想知道如果您在 64 位应用程序中使用
INT 2E
而不是syscall
会发生什么:我只能推测 - 特别是因为 Microsoft 也可能会在下一次更新后更改行为(如果从 64 位应用程序使用
INT 2E
会发生什么)。但是,行为 可能 类似于 Linux:
在Linux中,
INT 80
将所有地址(指针)解释为32位值;syscall
会将它们解释为 64 位值。出于这个原因,如果没有指针(地址)被传递给内核,则可以从 64 位应用程序使用INT 80
(在 Windows 下,ZwClose
就是一个例子) .但是,不可能将指针传递给内核(例如ZwOpenFile
)。