Windows: Running Object Table Register ROTFLAGS_ALLOWANYCLIENT EoP
Platform: Windows 10 10586/14393 not tested 8.1 Update 2 or Windows 7
Class: Elevation of Privilege
By setting an appropriate AppID it’s possible for a normal user process to set a global ROT entry. This can be abused to elevate privileges.
NOTE: I’m not sure which part of this chain to really report. As far as I can tell it’s pretty much all by design and fixing the initial vector seems difficult. Perhaps this is only a bug which can be fixed to prevent sandbox escapes?
When registering an object in the ROT the default is to only expose that registration to the same user identity on the same desktop/window station. This includes preventing the same user at different ILs (such as between sandbox and normal user) from seeing the same registration. However it could be imagined that you might want to register an entry for all users/contexts so IRunningObjectTable::Register takes a grfFlags parameter with the value ROTFLAGS_ALLOWANYCLIENT which allows the ROT entry to be exposed to all users.
The description of this flag indicates it can only be used if the COM process is a Local Service or a RunAs application. In fact there’s an explicit ROTFlags value for the AppID which would grant the privilege to a normal application. Quick testing proves this to be correct, a “normal” application cannot expose the ROT entry to any client as RPCSS does a check that the calling process is allowed to expose the entry. However there are two clear problems with the check. Creating a RunAs COM object in the current session would typically run at the same privilege level as the caller, therefore an application which wanted to abuse this feature could inject code into that process. Secondly while it’s not possible to register a per-user COM object which specifies a RunAs AppID it’s possible to explicitly set the AppID when calling CoInitializeSecurity (either via the GUID or by naming your program to match one which maps to the correct AppID).
Therefore in the current implementation effectively any process, including sandboxed ones should be able to register a global ROT entry. What can we do with this? The ROT is mainly used for OLE duties, for example Word and Visual Studio register entries for each document/project open. It would be nice not to rely on this, so instead I’ll abuse another OLE component, which we’ve seen before, the fact that LoadTypeLib will fall back to a moniker if it can’t find the type library file specified.
If the file loading fails then LoadTypeLib will effectively call MkParseDisplayName on the passed in string. One of the things MPDN does is try and create a file moniker with the string passed in as an argument. File Monikers have an interesting feature, the COM libraries will check if there’s a registered ROT entry for this file moniker already present, if it is instead of creating a new object it will call IRunningObjectTable::GetObject instead when binding. So as we can register a ROT entry for any user in any context we can provide our own implementation of ITypeLib running inside our process, by registering it against the path to the type library any other process which tries to open that library would instead get our spoofed one, assuming we can force the file open to fail.
This is the next key part, looking at the LoadTypeLib implementation the code calls FindTypeLib if this function fails the code will fall back to the moniker route. There’s two opportunities here, firstly CreateFile is called on the path, we could cause this to fail by opening the file with no sharing mode, in theory it should fail. However in practice it doesn’t most type libraries are in system location, if you don’t have the possibility of write permission on the file the OS automatically applies FILE_SHARE_READ which makes it impossible to lock the file in its entirety. Also some TLBs are stored inside a DLL which is then used so this route is out. Instead the other route is more promising, VerifyIsExeOrTlb is called once the file is open to check the type of file to parse. This function tries to load the first 64 bytes and checks for magic signatures. We can cause the read to fail by using the LockFile API to put an exclusive lock on that part of the file. This also has the advantage that it doesn’t affect file mappings so will also work with loaded DLLs.
We now can cause any user of a type library to get redirected to our “fake” one without abusing impersonation/symbolic link tricks. How can we use this to our advantage? The final trick is to abuse again the auto-generation of Stubs/Proxies from automation compatible interfaces. If we can get a more privileged process to use our type library when creating a COM stub we can cause a number of memory safety issues such as type confusion, arbitrary memory read/writes and extending the vtable to call arbitrary functions. This is an extremely powerful primitive, as long as you can find a more privileged process which uses a dual automation interface. For example the FlashBroker which is installed on every Win8+ machine is intentionally allowed to be created by sandboxed IE/Edge and uses dual interfaces with auto-generated Stubs. We could abuse for example the BrokerPrefSetExceptionDialogSize and BrokerPrefGetExceptionDialogSize to do arbitrary memory writes. This all works because the stub creation has no was of ensuring that the actual server implementation matches the generated stub (at least without full symbols) so it will blindly marshal pointers or call outside of the object's vtable.
Proof of Concept:
I’ve provided a PoC as a C# project. You need to compile it first. It fakes out the Windows Search Service’s type library to modify the IGatherManagerAdmin2::GetBackoffReason method so that instead of marshaling a pointer to an integer for returning the caller can specify an arbitrary pointer value. When the method on the server side completes it will try and write a value to this address which will cause a Write AV. The Windows Search service would be ideal for abuse but many of the functions seem to require Administrator access to call. That’s not to say you couldn’t convert this into a full working exploit but I didn’t.
1) Compile the C# project. It should be compiled as a 64 bit executable.
2) Restart the Windows Search service just to ensure it hasn’t cached the stub previously. This probably isn’t necessary but just to be certain.
3) Attach a debugger to SearchIndexer.exe to catch the crash.
4) Execute the PoC as a normal user (do not run under the VSHOST as the CoInitializeSecurity call will fail). You need to pass the path to the provided mssitlb.tlb file which has been modified appropriately.
5) The service should crash trying to write a value to address 0x12345678
rax=0000015ee04665a0 rbx=0000015ee0466658 rcx=0000015ee0466658
rdx=0000000000000000 rsi=0000000000000004 rdi=0000000000000000
rip=00007fff80e3a75d rsp=00000036541fdae0 rbp=00000036541fdb20
r8=00000036541fd868 r9=0000015ee3bb50b0 r10=0000000000000000
r11=0000000000000246 r12=0000015ee3c02988 r13=00000036541fe1c0
iopl=0 nv up ei pl zr na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246
00007fff`80e3a75d 418936 mov dword ptr [r14],esi ds:00000000`12345678=????????
# Child-SP RetAddr Call Site
00 00000036`541fdae0 00007fff`b416d533 MSSRCH!CGatheringManager::GetBackoffReason+0x8d
01 00000036`541fdb10 00007fff`b413b0d0 RPCRT4!Invoke+0x73
02 00000036`541fdb60 00007fff`b2fa479a RPCRT4!NdrStubCall2+0x430
03 00000036`541fe180 00007fff`b3853c93 combase!CStdStubBuffer_Invoke+0x9a [d:\th\com\combase\ndr\ndrole\stub.cxx @ 1446]
04 00000036`541fe1c0 00007fff`b305ccf2 OLEAUT32!CUnivStubWrapper::Invoke+0x53
05 (Inline Function) --------`-------- combase!InvokeStubWithExceptionPolicyAndTracing::__l7::<lambda_b8ffcec6d47a5635f374132234a8dd15>::operator()+0x42 [d:\th\com\combase\dcomrem\channelb.cxx @ 1805]
06 00000036`541fe210 00007fff`b3001885 combase!ObjectMethodExceptionHandlingAction<<lambda_b8ffcec6d47a5635f374132234a8dd15> >+0x72 [d:\th\com\combase\dcomrem\excepn.hxx @ 91]
07 (Inline Function) --------`-------- combase!InvokeStubWithExceptionPolicyAndTracing+0x9e [d:\th\com\combase\dcomrem\channelb.cxx @ 1808]
08 00000036`541fe280 00007fff`b3006194 combase!DefaultStubInvoke+0x275 [d:\th\com\combase\dcomrem\channelb.cxx @ 1880]
09 (Inline Function) --------`-------- combase!SyncStubCall::Invoke+0x1b [d:\th\com\combase\dcomrem\channelb.cxx @ 1934]
0a (Inline Function) --------`-------- combase!SyncServerCall::StubInvoke+0x1b [d:\th\com\combase\dcomrem\servercall.hpp @ 736]
0b (Inline Function) --------`-------- combase!StubInvoke+0x297 [d:\th\com\combase\dcomrem\channelb.cxx @ 2154]
0c 00000036`541fe4a0 00007fff`b3008b47 combase!ServerCall::ContextInvoke+0x464 [d:\th\com\combase\dcomrem\ctxchnl.cxx @ 1568]
0d (Inline Function) --------`-------- combase!CServerChannel::ContextInvoke+0x83 [d:\th\com\combase\dcomrem\ctxchnl.cxx @ 1458]
0e (Inline Function) --------`-------- combase!DefaultInvokeInApartment+0x9e [d:\th\com\combase\dcomrem\callctrl.cxx @ 3438]
0f 00000036`541fe770 00007fff`b3007ccd combase!AppInvoke+0x8a7 [d:\th\com\combase\dcomrem\channelb.cxx @ 1618]
10 00000036`541fe8a0 00007fff`b300b654 combase!ComInvokeWithLockAndIPID+0xb2d [d:\th\com\combase\dcomrem\channelb.cxx @ 2686]
11 00000036`541feb30 00007fff`b40fd433 combase!ThreadInvoke+0x1724 [d:\th\com\combase\dcomrem\channelb.cxx @ 6954]
12 00000036`541fedc0 00007fff`b40fbed8 RPCRT4!DispatchToStubInCNoAvrf+0x33
13 00000036`541fee10 00007fff`b40fcf04 RPCRT4!RPC_INTERFACE::DispatchToStubWorker+0x288
14 00000036`541fef10 00007fff`b40f922d RPCRT4!RPC_INTERFACE::DispatchToStubWithObject+0x404
15 00000036`541fefb0 00007fff`b40f9da9 RPCRT4!LRPC_SCALL::DispatchRequest+0x35d
16 00000036`541ff090 00007fff`b40f64dc RPCRT4!LRPC_SCALL::HandleRequest+0x829
17 00000036`541ff180 00007fff`b40f48c9 RPCRT4!LRPC_SASSOCIATION::HandleRequest+0x45c
18 00000036`541ff200 00007fff`b411eaca RPCRT4!LRPC_ADDRESS::ProcessIO+0xb29
19 00000036`541ff350 00007fff`b422e490 RPCRT4!LrpcIoComplete+0x10a
1a 00000036`541ff3f0 00007fff`b422bc66 ntdll!TppAlpcpExecuteCallback+0x360
1b 00000036`541ff4a0 00007fff`b34b8102 ntdll!TppWorkerThread+0x916
1c 00000036`541ff8b0 00007fff`b425c5b4 KERNEL32!BaseThreadInitThunk+0x22
1d 00000036`541ff8e0 00000000`00000000 ntdll!RtlUserThreadStart+0x34
Not doing what ever it did.
It did it!
Proof of Concept: