Monday, 4 February 2019

A Brief History of BaseNamedObjects on Windows NT

Recently I RE'd some new undocumented feature in Windows which I thought I should put it in a short blog post. To expand it out slightly this blog will be a brief history of BaseNamedObjects (BNO from now on) from Windows NT 3.1 to modern Windows 10.

TL;DR; New versions of Windows 10 have a BaseNamedObjects isolation feature when creating a non-sandbox process which allows an application to redirect named objects transparently to a non-shared location. I've added support for the feature in NtObjectManager v1.1.19.

Just to get you up to speed, what is BNO? The majority of Windows NT kernel objects can be assigned names. The named objects are added to the Object Manager Namespace, a hierarchical object based file system. When calling a native system call such as NtCreateEvent you can specify an OBJECT_ATTRIBUTES structure which can include a full path to the object location. However, if you're calling a Win32 API it's typical to only be provided a simple name as demonstrated by the CreateEvent lpName parameter.

Screenshot showing CreateEventW prototype with an lpName parameter which specifies the object name.
As you can't provide a full path the question might be, where does the object get created? In NT 3.1 the kernel sets up a special \BaseNamedObjects directory when it starts. When you call CreateEvent the KERNEL32 library appends the name to the object directory to get the full path*. The end result is you create your named event at the location \BaseNamedObjects\{lpName}. We can use my NtObjectManager PowerShell module to list the BaseNamedObjects directory on a modern system by listing the drive NtObject:\BaseNamedObjects, as shown below.

* This isn't strictly how the library handles redirecting the name, but it's good enough for this post.

PowerShell console showing listing of ntobject:\BaseNamedObjects.

The BNO directory was shared by all users on the system, which for NT 3.1 meant system services and the user logged into the physical console. While there's some security implications with sharing this global location for all named events this wasn't a big concern back in 1993.

This global approach hit a problem with the introduction of Terminal Services in Windows 2000 (it was available in NT4 but as an extension). Specifically now you could have multiple "normal" users logged on at the same time on a single system you run the risk of name collisions, making one app impossible to run if another user had already started it and grabbed the name of the event or similar. Not discounting the increased security risk of sharing these resources. To remedy this problem when a new user logs into a Terminal Server a new instance of CSRSS is started and creates a new directory \Session\{ID}\BaseNamedObjects where {ID} maps to the Session identifier, just an integer value.

Due to the way the original Win32 APIs were designed adding a new directory could be made transparent. Instead of mapping the name parameter to the global BNO the KERNEL32 library could look up the session ID and create the session specific name. If the session ID is 0, indicating the physical console and service session, the name is still mapped to the global BNO, anything else is mapped to the per-session directory. It would still be useful for an application to create or open entries to the global BNO, so CSRSS also creates a symbolic link, Global, which maps to the global location. Therefore if you pass the name Global\NAME it will actually create the named object inside the global BNO. There's also a corresponding Local symbolic link, which just maps back to itself. In NtObjectManager you can list the per-session BNO through the SessionNtObject: drive as shown below:

Listing SessionNtObject:\ directory in PowerShell and selecting out symbolic links with the filter "? IsSymbolicLink".

Not much changed in Windows XP, other than Terminal Services being made available in consumer facing versions of the OS. It was used to implement Fast User Switching for example. The next evolution of BNO was in Vista. First Session 0 now became the preserve of system services, instead of being shared with the physical console. All user login sessions placed their named objects in a per-session BNO whether the user connected locally or remotely. The more interesting change was the introduction of private namespaces, exposed through the CreatePrivateNamespace API.

The API allows an application to create their own private BNO. Through the use of a "Boundary Descriptor" it is also possible to share it securely with other application if it has the correct parameters. This private BNO doesn't override the application's BNO, instead the APIs provide a lpAliasPrefix parameter, which you can use to prefix your object names. For example if you create a namespace with the "Flubber" prefix, then you can create or open objects by specify "Flubber\{NAME}" and KERNEL32 will automatically resolve it to the correct location. NtObjectManager exposes private namespaces through the Get-NtDirectory and New-NtDirectory commands with the PrivateNamespaceDescriptor parameter (read the help for more information on its structure). You can also map the private namespace as a drive using New-PSDrive command and specifying a root name of "ntpriv:{BOUNDARY}" where {BOUNDARY} is the boundary descriptor string as shown below:

Creating a new Private Namespace with "New-NtDirectory -PrivateNamespaceDescriptor FLUBBER". Then mapping it as a drive with "New-PSDrive -Name flubber -PSProvider NtObjectManager -Root ntpriv:FLUBBER"

Private namespaces are used in a few locations, such as IE/Edge but on the whole they're not that popular, perhaps because they require explicit changes to code to add the new prefix.

The next step in BNO history was introduced in Windows 8 to support the AppContainer (AC) sandbox. Supporting named objects in a sandbox using built-in functionality is difficult to get right, first because you don't really want a sandboxed application manipulating more privileged application's named objects. Second you also don't want other AC applications manipulating other AC named objects as some sandboxes have more access than others. Avoiding both of these problems made a global BNO location pretty much a non-starter. Instead, MS added code to automatically detect if an application is in an AC sandbox and redirect the named objects transparently to \Sessions\{ID}\AppContainerNamedObjects\{SID}, where SID is the SDDL form of the AC's package SID. The non-sandbox process creates the directory before starting the AC process and is ACL'ed so only that package can modify it. This solves the problem neatly, and again is a testament to the original design of hiding the real underlying object naming from the Win32 API layer.

Listing the AppContainerNamedObjects directory with "ls ntobject:\Sessions\9\AppContainerNamedObjects".

Finally we get to the last part, the area I was RE'ing. Windows 10 RS3 introduced a new undocumented feature, BNO Isolation. When I say it's undocumented I can't find any public reference to it, and the expected definitions don't appear in the Windows SDK headers. Of course there seems to be some structures in the Process Hacker source code for the native format but I try and avoid asking where exactly that's come from ;-)

Anyway, I think the name of the feature pretty much gives away its purpose. It allows a process to create an isolated BNO directory without being in an AC or requiring you to use a private namespace and the accompanying prefix. It's setup by specifying the name of the isolation directory in the  ProcThreadAttributeBnoIsolation Process/Thread attribute when creating a new process. From the Win32 level you only need to specify the name. Internally CreateProcess creates the appropriate BNO directory and supporting symbolic links. At the native level the prefix name and a list handles to capture is passed to NtCreateUserProcess and this information is stored in the process token. KERNEL32 can then query for the isolation prefix using the TokenBnoIsolation information class (which is documented, sort of) when setting up its BNO directory and all named objects are redirected to this new location. I've exposed the BNO prefix in NtObjectManager with the BnoIsolationPrefix property on the token object. You can setup a new process with the BNO isolation by setting the BnoIsolationPrefix property on the Win32Config object. For example:

Creating a new process with a BnoIsolationPrefix value in Win32ProcessConfig. Then listing the new directory under Sessions\9\BaseNamedObjects\Flubber.

The isolated name gets created under the per-session BNO. If you want true isolation you probably want to name the directory with a unique random GUID. Note that the isolation prefix is not inherited across process creation, which is a bit of a shame as that makes it less useful. Still I could see it being a useful feature for running arbitrary applications in a slightly more isolated fashion, shame it's not really documented.

There ends the brief history of BNO in Windows NT. While BNO isolation doesn't immediately look interesting from a security perspective I can imagine it could find some use process isolation and containment.

Sunday, 13 January 2019

Enabling Adminless Mode on Windows 10 SMode

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.

Calling NtSystemInfo::CodeIntegrityPolicy in Powershell on a non-SMode system showing Flag40000000

In contrast on an SMode installation we can see Flag20000000 is set instead. This means it's ready to enable Adminless mode.

Calling NtSystemInfo::CodeIntegrityPolicy in Powershell on a SMode system showing Flag20000000

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.










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.

Screenshot of symbolic link error response from SMB specifications.

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:

Using mklink on \\localhost\c$\abc returns the error "Local NTFS volumes are required to complete the operation."

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."
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.

Using CreateMountPoint on \\localhost\c$\abc gives access denied.

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:

Using CreateMountPoint on \\localhost\c$\abc is successful and listing the directory showing the windows folder.


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:

  • 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.


Decompilation of DSMoveFromSharedFile


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);

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 :-)