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 address (that is, KUSER_SHARED_DATA.SystemCall but in kernel) and see if we have a match.
There's actually only 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 0x2E 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 that it is a global kernel variable named KeLoaderBlock:
PAGELK:0000000140597154 mov rcx, cs:KeLoaderBlock_0
PAGELK:000000014059715B call KiInitializeBootStructures
Its type is known to be _LOADER_PARAMETER_BLOCK and its definition 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 INT 0x2E.
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 known as "truslets" (see IUM for a longer explanation).
At that point I can only guess; my first thought is that calling INT 0x2E 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.