VirtualBox: Windows Process DLL Signature Bypass EoP
Platform: VirtualBox v5.1.22 r115126 x64 (Tested on Windows 10)
Class: Elevation of Privilege
The process hardening implemented by the VirtualBox driver can be circumvented to load arbitrary code inside a VirtualBox process giving access to the VBoxDrv driver which can allow routes to EoP from a normal user.
NOTE: I don’t know if you consider this an issue or not, however you fixed the last bypass I sent so it’s possible you still consider it a security boundary.
The ring 3 process hardening in VirtualBox adds three hooks to module loading to try and prevent untrusted code being loaded into the process, LdrLoadDll, NtCreateSection and a LDR DLL notification. Each will try and verify a DLL load and either reject the load with an error or kill the process is it’s not possible to prevent it from occurring. Looking at the hooks there a couple of issues which when combined together allow a user to inject an arbitrary DLL into a protected process.
The location checks are not very rigorous. As far as I can tell arbitrary files need to be owned by an admin/trustedinstaller but this check is waived if the file is in system32/WinSxS. However this doesn’t take into account that there are some directories which can be written to inside system32 such as Tasks.
The code to enforce specific certificates doesn’t seem to be enabled so at the very least combined with 1, you can load any validly signed file.
It might be considered that 2 isn’t an issue as getting a signing cert could be a sufficient burden for a “malicious” attacker, so instead it’s worth considering what else the weak path checking allows you to do. The handling of DLL paths has some interesting behaviours, most interestingly there’s the behaviour where if no file extension is added to the path then the loader will automatically append .DLL to it. This is actually implemented inside LdrLoadDll, this leads to our third problem:
3. If the path passed to LdrLoadDll doesn’t have an extension then the protection code will signature check the extension less file but the loader will load the file with a .DLL extension. E.g. if trying to load \path\abc then \path\abc is signature checked but \path\abc.dll is loaded.
When combined with the ability to bypass the owner check we can drop an arbitrary valid signed file alongside our untrusted DLL and exploit this TOCTOU to load an arbitrary unsigned DLL. The following will show inside the VboxHardening.log when loading the file testdll.
2064.492c: \Device\HarddiskVolume4\Windows\System32\Tasks\dummy\testdll: Owner is not trusted installer
2064.492c: \Device\HarddiskVolume4\Windows\System32\Tasks\dummy\testdll: Relaxing the TrustedInstaller requirement for this DLL (it's in system32).
2064.492c: supHardenedWinVerifyImageByHandle: -> 0 (\Device\HarddiskVolume4\Windows\System32\Tasks\dummy\testdll) WinVerifyTrust
2064.492c: supR3HardenedWinVerifyCacheInsert: \Device\HarddiskVolume4\Windows\System32\Tasks\dummy\testdll
2064.492c: supR3HardenedMonitor_LdrLoadDll: pName=c:\windows\system32\tasks\dummy\testdll (rcNtResolve=0xc0150008) *pfFlags=0x0 pwszSearchPath=0000000000002009:<flags> [calling]
This shows that it successfully passed the signature check inside the LdrLoadDll hook, however one of the other hooks should try and recheck the real testdll.dll file when it gets loaded instead. As the name of this file won’t match the cached signature check it should still fail to complete loading. This is where the fourth issue comes in:
4. When doing the check inside supHardenedWinVerifyImageByHandle with WinVerifyTrust disabled (i.e. when in the DLL load notification hook) and the target file has no signature information an incorrect error code is returned which looks like success leading to the DLL being allowed to load and execute.
Specifically when supR3HardenedDllNotificationCallback is called it passes true to the fAvoidWinVerifyTrust parameter of supR3HardenedScreenImage. This first uses the RT code to check if the file is signed or not, if we use an unsigned file then this will return the error VERR_LDRVI_NOT_SIGNED (-22900). Later in supHardenedWinVerifyImageByLdrMod this error is checked and the function supHardNtViCheckIfNotSignedOk is called. This seems to result in the error coding changing from an error to VINF_LDRVI_NOT_SIGNED (22900) which is actually a success code. Normally this would be overridden again by the call to WinVerifyTrust but because that’s disabled the final result of this process is the DLL notification callback thinks the signature check was successful even though it wasn’t. This results in the DLL being allowed to complete loading.
For example the following is a snippet of the output when the bypass occurs.
2064.492c: \Device\HarddiskVolume4\Windows\System32\Tasks\dummy\TestDll.dll: Owner is not trusted installer
2064.492c: \Device\HarddiskVolume4\Windows\System32\Tasks\dummy\TestDll.dll: Relaxing the TrustedInstaller requirement for this DLL (it's in system32).
2064.492c: supR3HardenedWinVerifyCacheScheduleImports: Import todo: #1 'user32.dll'.
2064.492c: supR3HardenedWinVerifyCacheScheduleImports: Import todo: #2 'advapi32.dll'.
2064.492c: supHardenedWinVerifyImageByHandle: -> 22900 (\Device\HarddiskVolume4\Windows\System32\Tasks\dummy\TestDll.dll)
2064.492c: supR3HardenedWinVerifyCacheInsert: \Device\HarddiskVolume4\Windows\System32\Tasks\dummy\TestDll.dll
2064.492c: supR3HardenedDllNotificationCallback: load 00007ff8a8600000 LB 0x00027000 c:\windows\system32\tasks\dummy\testdll.DLL [fFlags=0x0]
2064.492c: supR3HardenedScreenImage/LdrLoadDll: cache hit (Unknown Status 22900 (0x5974)) on \Device\HarddiskVolume4\Windows\System32\Tasks\dummy\TestDll.dll [avoiding WinVerifyTrust]
2064.492c: Detected loader lock ownership: rc=Unknown Status 22900 (0x5974) '\Device\HarddiskVolume4\Windows\System32\Tasks\dummy\TestDll.dll'.
2064.492c: supR3HardenedWinVerifyCacheProcessWvtTodos: 22900 (was 22900) fWinVerifyTrust=0 for '\Device\HarddiskVolume4\Windows\System32\Tasks\dummy\TestDll.dll' [rescheduled]
This combination of issues results in being able to inject arbitrary executable code into a VirtualBox protected process and access the resources such as the kernel driver that this would provide.
Proof of Concept:
I’ve provided a PoC DLL which will be loaded through abusing the VBox COM Client loading process in source form. I’ve also provided a registry file which will need to be imported.
The DLL must be compiled in release mode for the architecture you’re going to run VirtualBox on. Then follow these steps:
1) Create the directory c:\windows\system32\tasks\dummy on the command line using ‘mkdir c:\windows\system32\tasks\dummy’
2) Import the provided .reg file to setup the COM hijack using the command line ‘reg import keys.reg’
3) Copy a valid signed file (such as VirtualBox.exe) to the file c:\windows\system32\tasks\dummy\testdll.
4) Copy the compiled PoC dll to c:\windows\system32\tasks\dummy\testdll.dll.
5) Start a VM. Each process the DLL is injected into will show a message box. This will include the protected VirtualBox.exe process.
Untrusted DLL loading should fail inside a protected process.
DLL is loaded into the protected process.
This bug is subject to a 90 day disclosure deadline. After 90 days elapse or a patch has been made broadly available, the bug report will become visible to the public.