This question is related to this one but it didn't fill some of the gaps I had, So I decided to ask it again with a few more details, and maybe put a bounty on this.
Anyway, usually if you look up Nt/Zw functions on ntdll, you see something like:
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
Now I know that this is comparing the offset from KUSER_SHARED_DATA and deciding whether or not to perform a syscall vs INT 2E. At first I thought that INT 2E would be executed if the running program is a 32 bit application, but after reading a bit about WOW64 it seems that those applications would use the 32 bit version of ntdll which doesn't execute int 2e but rather goes through the heaven's gate to get to the kernel:
public ZwClose
ZwClose proc near
mov eax, 3000Fh ; NtClose
mov edx, offset j_Wow64Transition
call edx ; j_Wow64Transition
retn 4
ZwClose endp
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? I've also been told that one of the reasons for INT 2E is for CET compatibility so I'm bit confused about INT 2E.
CodePudding user response:
So as far as I understand Wow64Transition will eventually jump to the 64-bit version of ntdll which I listed first, right?
Yes.
If that's so, is that when INT 2E is executed instead of syscall?
No.
First, let get the obvious out of the way: you can still call INT 0x2E on a modern Windows system without any problem, the interrupt vector is still here and points to the system call dispatcher:
0: kd> !idt 0x2e
Dumping IDT: fffff8010a900000
2e: fffff8010ca11ec0 nt!KiSystemServiceShadow
What does make it call int 0x2E?
As you saw, the piece of code that does the ring3 / ring0 transition checks a bit in the KUSER_SHARED_DATA structure.
At offset 0x308 we have a field called SystemCall
:
0: kd> dt _kuser_shared_data
nt!_KUSER_SHARED_DATA
...
0x308 SystemCall : Uint4B
...
KUSER_SHARED_DATA is mapped at two different addresses: one in user land (0x7FFE0000) and one in kernel land (0xFFFFF78000000000). Both of these adresses are backed by the same physical page (the user land one is obviously read only).
Note that these adresses are constant and not subjected to ASLR. Thus in the kernel we can search for the 0xFFFFF78000000308
(KUSER_SHARED_DATA.SystemCall
but in kernel) address and see if we have a match.
There's actually one match in a function named 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
So if KiSystemCallSelector
is 1 and HvlEnlightenments
has bit 19 set, then KUSER_SHARED_DATA.SystemCall
is set.
HvlEnlightenments
is a bitfield that is set when a virtualized OS knows that it is actually virtualized (those OS are called "enlightened OS"). This means that in fine, the feature (calling int 2E instead of SYSCALL) is related to virtualized OS.
We are left with KiSystemCallSelector
; this variable is set in a function called 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
We can see the first parameter to this function is important; it happens it is a global kernel variable named KeLoaderBlock
:
PAGELK:0000000140597154 mov rcx, cs:KeLoaderBlock_0
PAGELK:000000014059715B call KiInitializeBootStructures
It's type is known to be LOADER_PARAMETER_BLOCK
and is publicly available in the kernel symbols, so the previous bit of code looks like this with symbolic information:
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
At offset 0x74 of the _LOADER_PARAMETER_EXTENSION
structure we have a bitfield:
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
};
The bt eax, 8
instruction is testing for bit 8, thus the VsmConfigured
bit.
Thus if we are virtualized and VsmConfigured
is 1, then we use INT2E.
Why?
VSM stands for 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 know as truslets (see IUM
for a longer explanation).
At that point I can only guess; my first thought is that it (calling INT2E) is only applied to a specific kernel (not the "normal" kernel in VTL0, but which one, I still don't know).
It is actually easier for a VMM (the hypervisor) to catch VM-exits for interruption than syscalls; A VM-exit occurs when some events (e.g. specific instructions like INT, RDMSR, WMSR) happen that make the code transition from its normal execution flow back into the hypervisor, so the hypervisor can actually look at what triggered the VM-exit and act accordingly (e.g. redirecting the code flow or "lying" to the OS).
After writing this answer I saw that someone actually chased the same path in a more thoroughly explained blog post: The Windows 10 TH2 INT 2E mystery. They weren't sure in which exact case the kernel will use INT2E though. We can only guess at that point.
CodePudding user response:
can a 64-bit application on windows execute INT 2E instead of syscall?
This depends on what you exactly want to know:
If you want to ask if your application can safely call
int 2e
, the answer is: No!According to a table found in the internet,
EAX=0Fh
isZwClose
in Windows 10 butZwOpenKey
in Windows 7.And as you can see in the table, the meaning of some values (e.g.
EAX=06Dh
) even changed during a Windows 10 update!If you directly use
INT 2E
, it is possible that your application works fine now but it does not work after the next update!The same is true for
syscall
- so your application can use neither of them in your own application.(A Microsoft DLL can only use
INT 2E
orsyscall
if the DLL is replaced whenever the kernel is updated.)If you want to know what happens if you use
INT 2E
instead ofsyscall
in a 64-bit application:I can only speculate - especially because Microsoft may also change the behaviour (what happens if
INT 2E
is used from a 64-bit application) after the next update.However, the behaviour may be similar to Linux:
In Linux,
INT 80
interprets all addresses (pointers) as 32-bit values;syscall
will interprets them as 64-bit values. For this reason, it would be possible to useINT 80
from a 64-bit application if no pointers (addresses) are passed to the kernel (under Windows,ZwClose
would be an example). However, it would not be possible to pass pointers to the kernel (e.g.ZwOpenFile
).