Microsoft has always been pretty terrible at documenting new and interesting features for their System Integrity Policy used to enable security features like UMCI, Device Guard/Windows Defender Application Control etc. This short blog post is about another feature which seems to be totally undocumented*, but is available in Windows 10 since 1803, Adminless mode.
* No doubt Alex Ionescu will correct me on this point if I'm wrong.
TL;DR; Windows 10 SMode has an Adminless mode which fails any access check which relies on the BUILTIN\Administrators group. This is somewhat similar to macOS's System Integrity Protection in that the Administrator user cannot easily modify system resources. You can enable it by setting the DWORD value SeAdminlessEnforcementModeEnabled in HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Kernel to 1 on Windows 10 1809 SMode. I'd not recommend setting this value on a working SMode system as you might lock yourself out of the computer.
If you look at the kernel 1803 and above at the API SeAccessCheck (and similar) you'll see it now calls the method SeAccessCheckWithHintWithAdminlessChecks. The Adminless part is new, but what is Adminless and how is it enabled? Let's see some code, this is derived from 1809 [complexity reduced for clarity]:
BOOLEAN SeAccessCheck(PSECURITY_DESCRIPTOR SecurityDescriptor,
PSECURITY_SUBJECT_CONTEXT SubjectSecurityContext,
BOOLEAN SubjectContextLocked,
ACCESS_MASK DesiredAccess,
ACCESS_MASK PreviouslyGrantedAccess,
PPRIVILEGE_SET *Privileges,
PGENERIC_MAPPING GenericMapping,
KPROCESSOR_MODE AccessMode,
PACCESS_MASK GrantedAccess,
PNTSTATUS AccessStatus) {
BOOLEAN AdminlessCheck = FALSE;
PTOKEN Token = SeQuerySubjectContextToken(SubjectSecurityContext);
DWORD Flags;
BOOLEAN Result
SeCodeIntegrityQueryPolicyInformation(205, &Flags, sizeof(Flags));
if (Flags & 0xA0000000) {
AdminlessCheck = SeTokenIsAdmin(Token) &&
!RtlEqualSid(SeLocalSystemSid, Token->UserAndGroups->Sid);
}
if (AdminlessCheck) {
Result = SeAccessCheckWithHintWithAdminlessChecks(
...,
GrantedAccess,
AccessStatus,
TRUE);
if (Result) {
return TRUE;
}
if (SepAccessStatusHasAccessDenied(GrantedAccess, AccessStatus)
&& SeAdminlessEnforcementModeEnabled) {
SepLogAdminlessAccessFailure(...);
return FALSE;
}
}
return SeAccessCheckWithHintWithAdminlessChecks(
..., FALSE);
}
The code has three main parts. First a call is made to SeCodeIntegrityQueryPolicyInformation to look up system information class 205 from the CI module. Normally these information classes are also accessible through NtQuerySystemInformation, however 205 is not actually wired up in 1809 therefore you can't query the flags from user-mode directly. If the flags returned have the bits 31 or 29 set, then the code tries to determine if the token being used for the access check is an admin (it the token a member of the the BUILTIN\Administrators group) and it's not a SYSTEM token based on the user SID.
If this token is not an admin, or it's a SYSTEM token then the second block is skipped. The SeAccessCheckWithHintWithAdminlessChecks method is called with the access check arguments and a final argument of FALSE and the result returned. This is the normal control flow for the access check. If the second block is instead entered SeAccessCheckWithHintWithAdminlessChecks is called with the final argument set to TRUE. This final argument is what determines whether Adminless checks are enabled or not, but not whether the checks are enforced. We'll see what the checks are are in a minute, but first let's continue here. Finally in this block SepAccessStatusHasAccessDenied is called which takes the granted access and the NTSTATUS code from the check and determines whether the access check failed with access denied. If the global variable SeAdminlessEnforcementModeEnabled is also TRUE then the code will log an optional ETW event and return FALSE indicating the check has failed. If Adminless mode is not enabled the normal non Adminless check is made.
There's two immediate questions you might ask, first where do the CI flags get set and how do you set SeAdminlessEnforcementModeEnabled to TRUE? The latter is easy, by creating a DWORD registry value set to 1 in "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Kernel" with the name AdminlessEnforcementModeEnabled the kernel will set that global variable to TRUE. The CI flags is slightly more complicated, the call to SeCodeIntegrityQueryPolicyInformation drills down to SIPolicyQueryWindowsLockdownMode inside the CI module. Which looks like the following:
void SIPolicyQueryWindowsLockdownMode(PULONG LockdownMode) {
SIPolicyHandle Policy;
if (SIPolicyIsPolicyActive(7, &Policy)) {
ULONG Options;
SIPolicyGetOptions(Policy, &Options, NULL);
if ((Options >> 6) & 1)
*LockdownMode |= 0x80000000;
else
*LockdownMode |= 0x20000000;
}
else {
*LockdownMode |= 0x40000000;
}
}
The code queries whether policy 7 is active. Policy 7 corresponds to the system integrity policy file loaded from WinSIPolicy.p7b (see g_SiPolicyTypeInfo in the CI module) which is the policy file used by SMode (what used to be Windows 10S). If 7 is active then the depending on an additional option flag either bit 31 or bit 29 is set in the LockdownMode parameter. If policy 7 is not active then bit 30 is set. Therefore what the call in SeAccessCheck is checking for is basically whether the current system is running Windows in SMode. We can see this more clearly by looking at 1803 which has slightly different code:
if (!g_sModeChecked) {
SYSTEM_CODE_INTEGRITY_POLICY Policy = {};
ZwQuerySystemInformation(SystemCodeIntegrityPolicyInformation,
&Policy, sizeof(Policy));
g_inSMode = Policy.Options & 0xA0000000;
g_sModeChecked = TRUE;
}
The code in 1803 makes it clear that if bit 29 or 31 is set then it's consider to be SMode. This code also uses ZwQuerySystemInformation instead of SeCodeIntegrityQueryPolicyInformation to extract the flags via the SystemCodeIntegrityPolicyInformation information class. We can call this instead of information class 205 using NtObjectManager. We can see in the screenshot below that on a non-SMode system calling NtSystemInfo::CodeIntegrityPolicy has Flag40000000 set which would not be considered SMode.
In contrast on an SMode installation we can see Flag20000000 is set instead. This means it's ready to enable Adminless mode.
We now know how to enable Adminless mode, but what is the mode enforcing? The final parameter to SeAccessCheckWithHintWithAdminlessChecks is forwarded to other methods. For example the method SepSidInTokenSidHash has been changed. This method checks whether a specific SID is in the list of a token's group SIDs. This is used for various purposes. For example when checking the DACL each ACE is enumerated and SepSidInTokenSidHash is called with the SID from the ACE and the token's group list. If the SID is in the group list the access check handles the ACE according to type and updates the current granted access. The change for Adminless looks like the following:
BOOLEAN SepSidInTokenSidHash(PSID_AND_ATTRIBUTES_HASH SidAndHash,
PSID Sid,
BOOLEAN AdminlessCheck) {
if (AdminlessCheck && RtlEqualSid(SeAliasAdminsSid, Sid) )
return FALSE;
// ...
return TRUE;
}
Basically if the AdminlessCheck argument is TRUE and the SID to check is BUILTIN\Administrators then fail immediately. This checks in repeated in a number of other places as well. The net result is Administrators (except for SYSTEM which is needed for system operation) can no longer access a resource based on being a member of the Administrators group. As far as I can tell it doesn't block privilege checks, so if you were able to run under a token with "GOD" privileges such as SeDebugPrivilege you could still circumvent the OS security. However you need to be running with High Integrity to use the most dangerous privileges which you won't get as a normal user.
I don't really know what the use case for this mode is, at least it's not currently on by default on SMode. As it's not documented anywhere I could find then I assume it's also not something Microsoft are expecting users/admins to enable. The only thoughts I had were kiosk style systems or in Hyper-V containers to block all administrators access. If you were managing a fleet of SMode devices you could also enable this to make it harder for a user to run code as admin, however it wouldn't do much if you had a privilege escalation to SYSTEM.
This sounds similar in some ways to System Integrity Protection/SIP/rootless on macOS in that it limits the ability for a user modify the system except rather than a flag which indicates a resource can be modified like on macOS and administrator could still modify a resource as long as they have another group to use. Perhaps eventually Microsoft might document this feature, considering the deep changes to access checking it required. Then again, knowing Microsoft, probably not.
Sunday, 13 January 2019
Thursday, 27 December 2018
Abusing Mount Points over the SMB Protocol
This blog post is a quick writeup on an interesting feature of SMBv2 which might have uses for lateral movement and red-teamers. When I last spent significant time looking at symbolic link attacks on Windows I took a close look at the SMB server. Since version 2 the SMB protocol has support for symbolic links, specifically the NTFS Reparse Point format. If the SMB server encounters an NTFS symbolic link within a share it'll extract the REPARSE_DATA_BUFFER and return it to the client based on the SMBv2 protocol specification §2.2.2.2.1.
The client OS is responsible for parsing the REPARSE_DATA_BUFFER and following it locally. This means that only files the client can already access can be referenced by symbolic links. In fact even resolving symbolic links locally isn't enabled by default, although I did find a bypass which allowed a malicious server to bypass the client policy and allowing resolving symbolic links locally. Microsoft declined to fix the bypass at the time, it's issue 138 if you're interested.
What I found interesting is while IO_REPARSE_TAG_SYMLINK is handled specially on the client, if the server encounters the IO_REPARSE_TAG_MOUNT_POINT reparse point it would follow it on the server. Therefore, if you could introduce a mount point within a share you could access any fixed disk on the server, even if it's not shared directly. That could have many uses for lateral movement, but the question becomes how could we add a mount point without already having local access to the disk?
First thing to try is to just create a mount point via a UNC path and see what happens. Using the MKLINK CMD built-in you get the following:
The error would indicate that setting mount points on remote servers isn't supported. This would make some sense, setting a mount point on a remote drive would result in unexpected consequences. You'd assume the protocol either doesn't support setting reparse points at all, or at least restricts them to only allowing symbolic links. We can get a rough idea what the protocol expects by looking up the details in the protocol specification. Setting a reparse point requires sending the FSCTL_SET_REPARSE_POINT IO control code to a file, therefore we can look up the section on the SMB2 IOCTL command to see if any there's any information about the control code.
After a bit of digging you'll find that FSCTL_SET_REPARSE_POINT is indeed supported and there's a note in §3.3.5.15.13 which I've reproduced below.
CreateMountPoint doesn't show an error about only supporting local NTFS volumes, but it does return an access denied error. This ties in with the description §3.3.5.15.13, if the implied check fails the code should return access denied. Of course the protocol specification doesn't actually say what check should be performed, I guess it's time to break out the disassembler and look at the implementation in the SMBv2 driver, srv2.sys.
I used IDA to look for immediate values for IO_REPARSE_TAG_SYMLINK which is 0xA000000C. It seems likely that any check would first look for that value along with any other checking for the other tags. In the driver from Windows 10 1809 there was only one hit in Smb2ValidateIoctl. The code is roughly as follows:
NTSTATUS Smb2ValidateIoctl(SmbIoctlRequest* request) { // ... switch(request->IoControlCode) { case FSCTL_SET_REPARSE_POINT: REPARSE_DATA_BUFFER* reparse = (REPARSE_DATA_BUFFER*)request->Buffer;
// Validate length etc. if (reparse->ReparseTag != IO_REPARSE_TAG_SYMLINK &&
!request->SomeOffset->SomeByteValue) {
return STATUS_ACCESS_DENIED; } // Complete FSCTL_SET_REPARSE_POINT request. } }
The code extracts the data from the IOCTL request, it fails with STATUS_ACCESS_DENIED if the tag is not IO_REPARSE_TAG_SYMLINK and some byte value is 0 which is referenced from the request data. Tracking down who sets this value can be tricky sometimes, however I usually have good results by just searching for the variables offset as an immediate value in IDA, in this case 0x200 and just go through the results looking for likely MOV instructions. I found an instruction "MOV [RCX+0x200], AL" inside Smb2ExecuteSessionSetupReal which looked to be the one. The variable is being set with the result of the call to Smb2IsAdmin which just checks if the caller has the BUILTIN\Administrators group in their token. It seems that we can set arbitrary reparse points on a remote share, as long as we're an administrator on the machine. We should still test that's really the case:
Testing from an administrator account allows us to create the mount point, and when listing the directory from a UNC path the Windows folder is shown. While I've demonstrated this on local admin shares this will work on any share and the mount point is followed on the remote server.
Is this trick useful? Requiring administrator access does mean it's not something you could abuse for local privilege escalation and if you have administrator access remotely there's almost certainly nastier things you could do. Still it could be useful if the target machine has the admin shares disabled, or there's monitoring in place which would detect the use of ADMIN$ or C$ in lateral movement as if there's any other writable share you could add a new directory which would give full control over any other fixed drive.
I can't find anyone documenting this before, but I could have missed it as the search results are heavily biased towards SAMBA configurations when you search for SMB and mount points (for obvious reasons). This trick is another example of ensuring you test any assumptions about the security behavior of a system as it's probably not documented what the actual behavior is. Even though a tool such as MKLINK claims a lack of a support for setting remote mount points by digging into available specification and looking at the code itself you can find some interesting stuff.
The client OS is responsible for parsing the REPARSE_DATA_BUFFER and following it locally. This means that only files the client can already access can be referenced by symbolic links. In fact even resolving symbolic links locally isn't enabled by default, although I did find a bypass which allowed a malicious server to bypass the client policy and allowing resolving symbolic links locally. Microsoft declined to fix the bypass at the time, it's issue 138 if you're interested.
What I found interesting is while IO_REPARSE_TAG_SYMLINK is handled specially on the client, if the server encounters the IO_REPARSE_TAG_MOUNT_POINT reparse point it would follow it on the server. Therefore, if you could introduce a mount point within a share you could access any fixed disk on the server, even if it's not shared directly. That could have many uses for lateral movement, but the question becomes how could we add a mount point without already having local access to the disk?
First thing to try is to just create a mount point via a UNC path and see what happens. Using the MKLINK CMD built-in you get the following:
The error would indicate that setting mount points on remote servers isn't supported. This would make some sense, setting a mount point on a remote drive would result in unexpected consequences. You'd assume the protocol either doesn't support setting reparse points at all, or at least restricts them to only allowing symbolic links. We can get a rough idea what the protocol expects by looking up the details in the protocol specification. Setting a reparse point requires sending the FSCTL_SET_REPARSE_POINT IO control code to a file, therefore we can look up the section on the SMB2 IOCTL command to see if any there's any information about the control code.
After a bit of digging you'll find that FSCTL_SET_REPARSE_POINT is indeed supported and there's a note in §3.3.5.15.13 which I've reproduced below.
"When the server receives a request that contains an SMB2 header with a Command value equal to SMB2 IOCTL and a CtlCode of FSCTL_SET_REPARSE_POINT, message handling proceeds as follows:
If the ReparseTag field in FSCTL_SET_REPARSE_POINT, as specified in [MS-FSCC] section 2.3.65, is not IO_REPARSE_TAG_SYMLINK, the server SHOULD verify that the caller has the required permissions to execute this FSCTL.<330> If the caller does not have the required permissions, the server MUST fail the call with an error code of STATUS_ACCESS_DENIED."330>
The text in the specification seems to imply the server only needs to check explicitly for IO_REPARSE_TAG_SYMLINK, and if the tag is something different it should do some sort of check to see if it's allowed, but it doesn't say anything about setting a different tag to be explicitly banned. Perhaps it's just the MKLINK built-in which doesn't handle this scenario? Let's try the CreateMountPoint tool from my symboliclink-testing-tools project and see if that helps.CreateMountPoint doesn't show an error about only supporting local NTFS volumes, but it does return an access denied error. This ties in with the description §3.3.5.15.13, if the implied check fails the code should return access denied. Of course the protocol specification doesn't actually say what check should be performed, I guess it's time to break out the disassembler and look at the implementation in the SMBv2 driver, srv2.sys.
I used IDA to look for immediate values for IO_REPARSE_TAG_SYMLINK which is 0xA000000C. It seems likely that any check would first look for that value along with any other checking for the other tags. In the driver from Windows 10 1809 there was only one hit in Smb2ValidateIoctl. The code is roughly as follows:
NTSTATUS Smb2ValidateIoctl(SmbIoctlRequest* request) { // ... switch(request->IoControlCode) { case FSCTL_SET_REPARSE_POINT: REPARSE_DATA_BUFFER* reparse = (REPARSE_DATA_BUFFER*)request->Buffer;
// Validate length etc. if (reparse->ReparseTag != IO_REPARSE_TAG_SYMLINK &&
!request->SomeOffset->SomeByteValue) {
return STATUS_ACCESS_DENIED; } // Complete FSCTL_SET_REPARSE_POINT request. } }
The code extracts the data from the IOCTL request, it fails with STATUS_ACCESS_DENIED if the tag is not IO_REPARSE_TAG_SYMLINK and some byte value is 0 which is referenced from the request data. Tracking down who sets this value can be tricky sometimes, however I usually have good results by just searching for the variables offset as an immediate value in IDA, in this case 0x200 and just go through the results looking for likely MOV instructions. I found an instruction "MOV [RCX+0x200], AL" inside Smb2ExecuteSessionSetupReal which looked to be the one. The variable is being set with the result of the call to Smb2IsAdmin which just checks if the caller has the BUILTIN\Administrators group in their token. It seems that we can set arbitrary reparse points on a remote share, as long as we're an administrator on the machine. We should still test that's really the case:
Testing from an administrator account allows us to create the mount point, and when listing the directory from a UNC path the Windows folder is shown. While I've demonstrated this on local admin shares this will work on any share and the mount point is followed on the remote server.
Is this trick useful? Requiring administrator access does mean it's not something you could abuse for local privilege escalation and if you have administrator access remotely there's almost certainly nastier things you could do. Still it could be useful if the target machine has the admin shares disabled, or there's monitoring in place which would detect the use of ADMIN$ or C$ in lateral movement as if there's any other writable share you could add a new directory which would give full control over any other fixed drive.
I can't find anyone documenting this before, but I could have missed it as the search results are heavily biased towards SAMBA configurations when you search for SMB and mount points (for obvious reasons). This trick is another example of ensuring you test any assumptions about the security behavior of a system as it's probably not documented what the actual behavior is. Even though a tool such as MKLINK claims a lack of a support for setting remote mount points by digging into available specification and looking at the code itself you can find some interesting stuff.
Sunday, 18 November 2018
Finding Windows RPC Client Implementations Through Brute Force
Recently, @SandboxEscaper wrote a detailed blog post (link seems she's locked down the blog, here's a link to an archive) about reverse engineering local RPC servers for the purposes of discovering sandbox escapes and privilege escalation vulnerabilities. After reading I thought I should put together a sort-of companion piece on RPC client implementation for PoC writing, specifically not implementing one unless you really need to.
If you go and read the blog post it goes through finding an RPC service to investigate using RpcView, then using the tool to decompile the RPC interface to an IDL file which can be added to a C++ project. This has a few problems when you're dealing with an unknown RPC interface:
If you go and read the blog post it goes through finding an RPC service to investigate using RpcView, then using the tool to decompile the RPC interface to an IDL file which can be added to a C++ project. This has a few problems when you're dealing with an unknown RPC interface:
- Even if the decompiler was perfect (and RpcView or my own in my NtObjectManager PowerShell module are definitely not) the original IDL to NDR compilation process is lossy. Reversing this process with a decompiler doesn't always produce a 100% correct IDL file and thus the regenerated NDR might not be 100% compatible.
- The NDR engine is terrible at giving useful diagnostic information for why the IDL is incorrect, usually just returning error code 1783 "The stub received bad data". This is made even more painful when dealing with complex structures or unions which must be exactly correct otherwise it all goes to hell.
- It's hard to use the IDL from any language but C/C++, as that's really the only supported output format for RPC interfaces.
While all three of these problems are annoying when trying to produce a working PoC, the last one annoys me especially. I have a thing about writing my PoCs in C#, about the only exception to using C# is when I need to interact with an RPC server. There's plenty of ways around this, for example I could build the client into a native DLL and export methods to call from C#, but this feel unsatisfactory.
At least in some cases, Microsoft have already done most of the work for me. If there's a native RPC server on a default installation of Windows there must be some sort of client component. In some cases this client might be embedded completely inside a binary and not directly callable, COM is a good example. However in other cases the developers also provide a general purpose library to interact with the server. If you can find the client library, it'll bring a number of advantages:
- If it's a truly general purpose the library will export methods which can be easily interacted with from C# using P/Invoke (or any other language which can invoke native exports).
- The majority of these libraries will deal with setting up the RPC client connection, dealing with asynchronous calls and custom serialization requirements.
- The NDR client code is going to be 100% compatible with the server, which should eliminate error code 1783 as well as dealing with changes to parameters, method layout and interface IDs which can happen between major versions of the OS.
- You only have to deal with calling a C style method (or sometimes a COM interface, but that's still a C calling convention) which gives a bit more flexibility which it comes to getting structure definitions correct.
- As it's a library there's a chance that useful type information might be disclosed in the client code, or it will allow to to track down callers of these APIs in other binaries that you can RE to get a better idea of how to call the methods correctly.
There's sadly some disadvantages to this approach:
- Not all clients will actually be in a general purpose library with easy entry points, or at least the entry points don't cleanly map to the underlying RPC methods. That's not to say it's useless as you could load the DLL then use a relative pointer to the RPC client structures and manually reconstruct the call but that removes many of the advantages.
- The library might be general purpose but the developers added a significant amount of client side parameter verification or don't expose some parameters at all. Some bugs are only going to present themselves by calling the RPC method with parameters the developers didn't expect to receive, perhaps because they verify in the client.
To prevent this blog post getting even longer let's look how I could identify the client library for the Data Sharing Service which SandboxEscaper dropped a bug in that was recently fixed as CVE-2018-8584. The bug SandboxEscaper discovered was in the method PolicyChecker::CheckFilePermission implemented in dssvc.dll. By calling one of the RPC methods, such as RpcDSSMoveFromSharedFile an arbitrary file can be deleted by the SYSTEM user. Looking at dssvc.dll it doesn't contain any client code, so we have to go hunting for the client. For this we'll use my NtObjectManager PowerShell module as it contains code to do just this. Any lines which start with PS> are to be executed in PowerShell.
Step 1: Install the NtObjectManager module from the PowerShell gallery.
PS> Install-Module NtObjectManager -Scope CurrentUser
PS> Import-Module NtObjectManager
You might need to also disable the script execution policy for this to work successfully.
Step 2: Parse RPC interfaces in all system DLLs using Get-RpcServer cmdlet.
PS> $rpc = ls c:\windows\system32\*.dll | Get-RpcServer -ParseClients
This call passes the list of all DLLs in system32 to the Get-RpcServer command and specifies that it should also parse all clients. This command does a heuristic search in a DLL's data sections for RPC servers and clients and parses the NDR structures. You can use this to generate RPC server definitions similar to RpcView (but in my own weird C# pseudo-code syntax) but for this scenario we only care about the clients. My code does have some advantages, for example the parsed NDR data is stored as a .NET object so you can do better analysis of the interface, but that's something for another day.
Step 3: Filter out the client based on IID and Client status.
PS> $rpc | ? {$_.Client -and $_.InterfaceId -eq 'bf4dc912-e52f-4904-8ebe-9317c1bdd497'} | Select FilePath
The server's IID is bf4dc912-e52f-4904-8ebe-9317c1bdd497 which you can easily get from the IDL server definition in the uuid attribute. We also need to filter only client implementations using the Client property.
If you've followed these procedures you'll find that the client implementation is in the DLL dsclient.dll. Admittedly we might have been able to guess this based on the similarity of names, but it's not always so simple.
Step 4: Disassemble/RE the library to find out how to call the methods.
It doesn't mean the DLL contains a general purpose library, we'll still need to open it in a disassembler and take a look. In this case we're lucky, if we look at the exports for the dsclient.dll library we find the names match up with the server. For example there's a DSMoveFromSharedFile which would presumably match up with RpcDSSMoveFromSharedFile.
If you follow this code you'll find it's just a simple wrapper around a call to the method DSCMoveFromSharedFile which binds to the RPC endpoint and calls the server. There's no parameter verification taking place so we can just determine how we can call this method from C# using the server IDL we generated earlier.
And that's it, I was able to implement a PoC for CVE-2018-8584 by defining the following C# P/Invoke method:
[DllImport("dsclient.dll", CharSet = CharSet.Unicode)]
public static extern int DSMoveFromSharedFile(string token, string source_file);
public static extern int DSMoveFromSharedFile(string token, string source_file);
Of course your mileage may vary depending on your RPC server. But what I've described here is a quick and easy way to determine if there's a quick and easy way to avoid writing C++ code :-)
Tuesday, 9 October 2018
Farewell to the Token Stealing UAC Bypass
With the release of Windows 10 RS5 the generic UAC bypass I documented in "Reading Your Way Around UAC" (parts 1, 2 and 3) has been fixed. This quick blog post will describe the relatively simple change MS made to the kernel to fix the UAC bypass and some musing on how it still might be possible to bypass.
As a quick recap, the UAC bypass I documented allowed any normal user on the same desktop to open a privileged UAC admin process and get a handle to the process' access token. The only requirement was there was an existing elevated process running on the desktop, but that's a very common behavior. That in itself didn't allow you to do much directly. However by duplicating the token which, made it writable, it was possible to selectively downgrade the token so that it could be impersonated.
Prior to Windows 10 all you needed to do was downgrade the token's integrity level to Medium. This left the token still containing the Administrators group, but it passed the kernel's checks for impersonation. This allows you to directly modify administrator only resources. For Windows 10 an elevation check was introduced which prevented a process in a non-elevated session from impersonating an elevated token. This was indicated by a flag in the limited token's logon session structure. If the flag was set, but you were impersonating an elevated token it'd fail. This didn't stop you from impersonating the token as long as it was considered non-elevated then abusing WMI to spawn a process in that session or the Secondary Logon Service to get back administrator privileges.
Let's look now at how it was fixed. The changed code is in the SeTokenCanImpersonate method which determines whether a token is allowed to impersonated or not.
TOKEN* process_token = ...; TOKEN* imp_token = ...; #define LIMITED_LOGON_SESSION 0x4 if (SeTokenIsElevated(imp_token)) { if (!SeTokenIsElevated(process_token) && (process_token->LogonSession->Flags & LIMITED_LOGON_SESSION)) { return STATUS_PRIVILEGE_NOT_HELD; } } if (process_token->LogonSession->Flags & LIMITED_LOGON_SESSION && !(imp_token->LogonSession->Flags & LIMITED_LOGON_SESSION)) { SepLogUnmatchedSessionFlagImpersonationAttempt(); return STATUS_PRIVILEGE_NOT_HELD; }
The first part of the code is the same as was introduced in Windows 10. If you try and impersonate an elevated token and your process is running in the limited logon session it'll be rejected. The new check introduced ensures that if you're in the limited logon session you're not trying to impersonate a token in a non-limited logon session. And there goes the UAC bypass, using any variation of the attack you need to impersonate the token to elevate your privileges.
The fix is pretty simple, although I can't help think there must be some edge case which this would trip up. The only case which comes to mind in tokens returned from the LogonUser APIs, however those are special cases earlier in the function so I could imagine this would only be a problem when there might be a more significant security bug.
It's worth bearing in mind that due to the way Microsoft fixes bugs in UAC this will not be ported to versions prior to RS5. So if you're on a Windows Vista through Windows 10 RS4 machine you can still abuse this to bypass UAC, in most cases silently. And there's hardly a lack of other UAC bypasses, you just have to look at UACME. Though I'll admit none of the bypasses are as interesting to me as a fundamental design flaw in the whole technology. The only thing I can say is Microsoft seems committed to fixing these bugs eventually, even if they seem to introduce more UAC bypasses in each release.
Can this fix be bypassed? It's predicated on the user not having control over a process running outside of the limited logon session. A potential counter example would be processes spawned from an elevated process where the token is intentionally restricted, such as in sandboxed applications such as Adobe Reader or Chrome. However in order for that to be exploitable you'd need to convince the user to elevate those applications which doesn't make for a general technique. There's of course potential impersonation bugs, such as my Constrained Impersonation attack which could be used to bypass Over-The-Shoulder elevation but also could be used to impersonate SYSTEM tokens. Bugs like that tend to be something Microsoft want to fix (the Constrained Impersonation one was fixed as CVE-2018-0821) so again not a general technique.
I did have a quick think about other ways of bypassing this, then I realized I don't actually care ;-)
As a quick recap, the UAC bypass I documented allowed any normal user on the same desktop to open a privileged UAC admin process and get a handle to the process' access token. The only requirement was there was an existing elevated process running on the desktop, but that's a very common behavior. That in itself didn't allow you to do much directly. However by duplicating the token which, made it writable, it was possible to selectively downgrade the token so that it could be impersonated.
Prior to Windows 10 all you needed to do was downgrade the token's integrity level to Medium. This left the token still containing the Administrators group, but it passed the kernel's checks for impersonation. This allows you to directly modify administrator only resources. For Windows 10 an elevation check was introduced which prevented a process in a non-elevated session from impersonating an elevated token. This was indicated by a flag in the limited token's logon session structure. If the flag was set, but you were impersonating an elevated token it'd fail. This didn't stop you from impersonating the token as long as it was considered non-elevated then abusing WMI to spawn a process in that session or the Secondary Logon Service to get back administrator privileges.
Let's look now at how it was fixed. The changed code is in the SeTokenCanImpersonate method which determines whether a token is allowed to impersonated or not.
TOKEN* process_token = ...; TOKEN* imp_token = ...; #define LIMITED_LOGON_SESSION 0x4 if (SeTokenIsElevated(imp_token)) { if (!SeTokenIsElevated(process_token) && (process_token->LogonSession->Flags & LIMITED_LOGON_SESSION)) { return STATUS_PRIVILEGE_NOT_HELD; } } if (process_token->LogonSession->Flags & LIMITED_LOGON_SESSION && !(imp_token->LogonSession->Flags & LIMITED_LOGON_SESSION)) { SepLogUnmatchedSessionFlagImpersonationAttempt(); return STATUS_PRIVILEGE_NOT_HELD; }
The first part of the code is the same as was introduced in Windows 10. If you try and impersonate an elevated token and your process is running in the limited logon session it'll be rejected. The new check introduced ensures that if you're in the limited logon session you're not trying to impersonate a token in a non-limited logon session. And there goes the UAC bypass, using any variation of the attack you need to impersonate the token to elevate your privileges.
The fix is pretty simple, although I can't help think there must be some edge case which this would trip up. The only case which comes to mind in tokens returned from the LogonUser APIs, however those are special cases earlier in the function so I could imagine this would only be a problem when there might be a more significant security bug.
It's worth bearing in mind that due to the way Microsoft fixes bugs in UAC this will not be ported to versions prior to RS5. So if you're on a Windows Vista through Windows 10 RS4 machine you can still abuse this to bypass UAC, in most cases silently. And there's hardly a lack of other UAC bypasses, you just have to look at UACME. Though I'll admit none of the bypasses are as interesting to me as a fundamental design flaw in the whole technology. The only thing I can say is Microsoft seems committed to fixing these bugs eventually, even if they seem to introduce more UAC bypasses in each release.
Can this fix be bypassed? It's predicated on the user not having control over a process running outside of the limited logon session. A potential counter example would be processes spawned from an elevated process where the token is intentionally restricted, such as in sandboxed applications such as Adobe Reader or Chrome. However in order for that to be exploitable you'd need to convince the user to elevate those applications which doesn't make for a general technique. There's of course potential impersonation bugs, such as my Constrained Impersonation attack which could be used to bypass Over-The-Shoulder elevation but also could be used to impersonate SYSTEM tokens. Bugs like that tend to be something Microsoft want to fix (the Constrained Impersonation one was fixed as CVE-2018-0821) so again not a general technique.
I did have a quick think about other ways of bypassing this, then I realized I don't actually care ;-)
Subscribe to:
Posts (Atom)