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

Time:11-20

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 is ZwClose in Windows 10 but ZwOpenKey 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 or syscall if the DLL is replaced whenever the kernel is updated.)

  • If you want to know what happens if you use INT 2E instead of syscall 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 use INT 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).

  • Related