Showing posts with label MSRPC. Show all posts
Showing posts with label MSRPC. Show all posts

Saturday, 21 August 2021

How the Windows Firewall RPC Filter Works

I did promise that I'd put out a blog post on how the Windows RPC filter works. Now that I released my more general blog post on the Windows firewall I thought I'd come back to a shorter post about the RPC filter itself. If you don't know the context, the Windows firewall has the ability to restrict access to RPC interfaces. This is interesting due to the renewed interest in all things RPC, especially the PetitPotam trick. For example you can block any access to the EFSRPC interfaces using the following script which you run with the netsh command.

rpc
filter
add rule layer=um actiontype=block
add condition field=if_uuid matchtype=equal data=c681d488-d850-11d0-8c52-00c04fd90f7e
add filter
add rule layer=um actiontype=block
add condition field=if_uuid matchtype=equal data=df1941c5-fe89-4e79-bf10-463657acf44d
add filter
quit

This script adds two rules which will block any calls on the RPC interfaces with UUIDs of c681d488-d850-11d0-8c52-00c04fd90f7e and df1941c5-fe89-4e79-bf10-463657acf44d. These correspond to the two EFSRPC interfaces.

How does this work within the context of the firewall? Does the kernel components of the Windows Filtering Platform have a builtin RPC protocol parser to block the connection? That'd be far too complex, instead everything is done in user-mode by some special layers. If you use NtObjectManager's firewall Get-FwLayer command you can check for layers registered to run in user-mode by filtering on the IsUser property.

PS> Get-FwLayer | Where-Object IsUser
KeyName                      Name
-------                      ----
FWPM_LAYER_RPC_PROXY_CONN    RPC Proxy Connect Layer
FWPM_LAYER_IPSEC_KM_DEMUX_V4 IPsec KM Demux v4 Layer
FWPM_LAYER_RPC_EP_ADD        RPC EP ADD Layer
FWPM_LAYER_KM_AUTHORIZATION  Keying Module Authorization Layer
FWPM_LAYER_IKEEXT_V4         IKE v4 Layer
FWPM_LAYER_IPSEC_V6          IPsec v6 Layer
FWPM_LAYER_IPSEC_V4          IPsec v4 Layer
FWPM_LAYER_IKEEXT_V6         IKE v6 Layer
FWPM_LAYER_RPC_UM            RPC UM Layer
FWPM_LAYER_RPC_PROXY_IF      RPC Proxy Interface Layer
FWPM_LAYER_RPC_EPMAP         RPC EPMAP Layer
FWPM_LAYER_IPSEC_KM_DEMUX_V6 IPsec KM Demux v6 Layer

In the output we can see 5 layers with RPC in the name of the layer. 
  • FWPM_LAYER_RPC_EP_ADD - Filter new endpoints created by a process.
  • FWPM_LAYER_RPC_EPMAP - Filter access to endpoint mapper information.
  • FWPM_LAYER_RPC_PROXY_CONN - Filter connections to the RPC proxy.
  • FWPM_LAYER_RPC_PROXY_IF - Filter interface calls through an RPC proxy.
  • FWPM_LAYER_RPC_UM - Filter interface calls to an RPC server
Each of these layers is potentially interesting, and you can add rules through netsh for all of them. But we'll just focus on how the FWPM_LAYER_RPC_UM layer works as that's the one the script introduced at the start works with. If you run the following command after adding the RPC filter rules you can view the newly created rules:

PS> Get-FwFilter -LayerKey FWPM_LAYER_RPC_UM -Sorted | Format-FwFilter
Name       : RPCFilter
Action Type: Block
Key        : d4354417-02fa-11ec-95da-00155d010a06
Id         : 78253
Description: RPC Filter
Layer      : FWPM_LAYER_RPC_UM
Sub Layer  : FWPM_SUBLAYER_UNIVERSAL
Flags      : Persistent
Weight     : 567453553048682496
Conditions :
FieldKeyName               MatchType Value
------------               --------- -----
FWPM_CONDITION_RPC_IF_UUID Equal     df1941c5-fe89-4e79-bf10-463657acf44d


Name       : RPCFilter
Action Type: Block
Key        : d4354416-02fa-11ec-95da-00155d010a06
Id         : 78252
Description: RPC Filter
Layer      : FWPM_LAYER_RPC_UM
Sub Layer  : FWPM_SUBLAYER_UNIVERSAL
Flags      : Persistent
Weight     : 567453553048682496
Conditions :
FieldKeyName               MatchType Value
------------               --------- -----
FWPM_CONDITION_RPC_IF_UUID Equal     c681d488-d850-11d0-8c52-00c04fd90f7e

If you're read my general blog post the output should made some sense. The FWPM_CONDITION_RPC_IF_UUID condition key is used to specify the UUID for the interface to match on. The FWPM_LAYER_RPC_UM has many possible fields to filter on, which you can query by inspecting the layer object's Fields property.

PS> (Get-FwLayer -Key FWPM_LAYER_RPC_UM).Fields

KeyName                              Type      DataType
-------                              ----      --------
FWPM_CONDITION_REMOTE_USER_TOKEN     RawData   TokenInformation
FWPM_CONDITION_RPC_IF_UUID           RawData   ByteArray16
FWPM_CONDITION_RPC_IF_VERSION        RawData   UInt16
FWPM_CONDITION_RPC_IF_FLAG           RawData   UInt32
FWPM_CONDITION_DCOM_APP_ID           RawData   ByteArray16
FWPM_CONDITION_IMAGE_NAME            RawData   ByteBlob
FWPM_CONDITION_RPC_PROTOCOL          RawData   UInt8
FWPM_CONDITION_RPC_AUTH_TYPE         RawData   UInt8
FWPM_CONDITION_RPC_AUTH_LEVEL        RawData   UInt8
FWPM_CONDITION_SEC_ENCRYPT_ALGORITHM RawData   UInt32
FWPM_CONDITION_SEC_KEY_SIZE          RawData   UInt32
FWPM_CONDITION_IP_LOCAL_ADDRESS_V4   IPAddress UInt32
FWPM_CONDITION_IP_LOCAL_ADDRESS_V6   IPAddress ByteArray16
FWPM_CONDITION_IP_LOCAL_PORT         RawData   UInt16
FWPM_CONDITION_PIPE                  RawData   ByteBlob
FWPM_CONDITION_IP_REMOTE_ADDRESS_V4  IPAddress UInt32
FWPM_CONDITION_IP_REMOTE_ADDRESS_V6  IPAddress ByteArray16

There's quite a few potential configuration options for the filter. You can filter based on the remote user token that's authenticated to the interface. Or you can filters based on the authentication level and type. This could allow you to protect an RPC interface so that all callers have to use Kerberos with at RPC_C_AUTHN_LEVEL_PKT_PRIVACY level. 

Anyway, configuring it is less important to us, you probably want to know how it works, as the first step to trying to find a way to bypass it is to know where this filter layer is processed (note, I've not found a bypass, but you never know). 

Perhaps unsurprisingly due to the complexity of the RPC protocol the filtering is implemented within the RPC server process through the RpcRtRemote extension DLL. Except for RPCSS this DLL isn't loaded by default. Instead it's only loaded if there exists a value for the WNF_RPCF_FWMAN_RUNNING WNF state. The following shows the state after adding the two RPC filter rules with netsh.

PS> $wnf = Get-NtWnf -Name 'WNF_RPCF_FWMAN_RUNNING'
PS> $wnf.QueryStateData()

Data ChangeStamp
---- -----------
{}             2

The RPC runtime sets up a subscription to load the DLL if the WNF value is ever changed. Once loaded the RPC runtime will register all current interfaces to check the firewall. The filter rules are checked when a call is made to the interface during the normal processing of the security callback. The runtime will invoke the FwFilter function inside RpcRtRemote, passing all the details about the firewall interface call. The filter call is only made for DCE/RPC protocols, so not ALPC. It also will only be called if the caller is remote. This is always the case if the call comes via TCP, but for named pipes it will only be called if the pipe was opened via SMB.

Here's where we can finally determine how the RPC filter is processed. The FwFilter function builds a list of firewall values corresponding to the list of fields for the FWPM_LAYER_RPC_UM layer and passes them to the FwpsClassifyUser0 API along with the numeric ID of the layer. This API will enumerate all filters for the layer and apply the condition checks returning the classification, e.g. block or permit. Based on this classification the RPC runtime can permit or refuse the call. 

In order for a filter to be accessible for classification the RPC server must have FWPM_ACTRL_OPEN access to the engine and FWPM_ACTRL_CLASSIFY access to the filter. By default the Everyone group has these access rights, however AppContainers and potentially other sandboxes do not. However, in general AppContainer processes don't tend to create privileged RPC servers, at least any which a remote attacker would find useful. You can check the access on various firewall objects using the Get-AccessibleFwObject command.

PS> $token = Get-NtToken -Filtered -Flags LuaToken
PS> Get-AccessibleFwObject -Token $token | Where-Object Name -eq RPCFilter

TokenId Access             Name
------- ------             ----
4ECF80  Classify|Open RPCFilter
4ECF80  Classify|Open RPCFilter

I hope this gives enough information for someone to dig into it further to see if there's any obvious bypass I missed. I'm sure there's probably some fun trick you could do to circumvent restrictions if you look hard enough :-)

Saturday, 14 August 2021

How to secure a Windows RPC Server, and how not to.

The PetitPotam technique is still fresh in people's minds. While it's not directly an exploit it's a useful step to get unauthenticated NTLM from a privileged account to forward to something like the AD CS Web Enrollment service to compromise a Windows domain. Interestingly after Microsoft initially shrugged about fixing any of this they went and released a fix, although it seems to be insufficient at the time of writing.

While there's plenty of details about how to abuse the EFSRPC interface, there's little on why it's exploitable to begin with. I thought it'd be good to have a quick overview of how Windows RPC interfaces are secured and then by extension why it's possible to use the EFSRPC interface unauthenticated. 

Caveat: No doubt I might be missing other security checks in RPC, these are the main ones I know about :-)

RPC Server Security

The server security of RPC is one which has seemingly built up over time. Therefore there's various ways of doing it, and some ways are better than others. There are basically three approaches, which can be mixed and matched:
  1. Securing the endpoint
  2. Securing the interface
  3. Ad-hoc security
Let's take each one in turn to determine how each one secures the RPC server.

Securing the Endpoint

You register the endpoint that the RPC server will listen on using the RpcServerUseProtseqEp API. This API takes the type of endpoint, such as ncalrpc (ALPC), ncacn_np (named pipe) or ncacn_ip_tcp (TCP socket) and creates the listening endpoint. For example the following would create a named pipe endpoint called DEMO.

RpcServerUseProtseqEp(
    L"ncacn_np",
    RPC_C_PROTSEQ_MAX_REQS_DEFAULT,
    L"\\pipe\\DEMO",
    nullptr);

The final parameter is optional but represents a security descriptor (SD) you assign to the endpoint to limit who has access. This can only be enforced on ALPC and named pipes as something like a TCP socket doesn't (technically) have an access check when it's connected to. If you don't specify an SD then a default is assigned. For a named pipe the default DACL grants the following uses write access:
  • Everyone
  • NT AUTHORITY\ANONYMOUS LOGON
  • SELF
Where SELF is the creating user's SID. This is a pretty permissive SD. One interesting thing about RPC endpoints is they are multiplexed. You don't explicit associate an endpoint with the RPC interface you want to access. Instead you can connect to any endpoint that the process has created. The end result is that if there's a less secure endpoint in the same process it might be possible to access an interface using the least secure one. In general this makes relying on endpoint security risky, especially in processes which run multiple services, such as LSASS. In any case if you want to use a TCP endpoint you can't rely on the endpoint security as it doesn't exist.

Securing the Interface

The next way of securing the RPC server is to secure the interface itself. You register the interface structure that was generated by MIDL using one of the following APIs:
Each has a varying number of parameters some of which determine the security of the interface. The latest APIs are RpcServerRegisterIf3 and RpcServerInterfaceGroupCreate which were introduced in Windows 8. The latter is just a way of registering multiple interfaces in one call so we'll just focus on the former. The RpcServerRegisterIf3 has three parameters which affect security, SecurityDescriptor, IfCallback and Flags. 

The SecurityDescriptor parameter is easiest to explain. It assigns an SD to the interface, when a call is made on that interface then the caller's token is checked against the SD and access is only granted if the check passes. If no SD is specified a default is used which grants the following SIDs access (assuming a non-AppContainer process)
  • NT AUTHORITY\ANONYMOUS LOGON
  • Everyone
  • NT AUTHORITY\RESTRICTED
  • BUILTIN\Administrators
  • SELF
The token to use for the access check is based either on the client's authentication (we'll discuss this later) or the authentication for the endpoint. ALPC and named pipe are authenticated transports, where as TCP is not. When using an unauthenticated transport the access check will be against the anonymous token. This means if the SD does not contain an allow ACE for ANONYMOUS LOGON it will be blocked.

Note, due to a quirk of the access check process the RPC runtime grants access if the caller has any access granted, not a specific access right. What this means is that if the caller is considered the owner, which is normally set to the creating user SID they might only be granted READ_CONTROL but that's sufficient to bypass the check. This could also be useful if the caller has SeTakeOwnershipPrivilege or similar as it'd be possible to generically bypass the interface SD check (though of course that privilege is dangerous in its own right).

The second parameter, IfCallback, takes an RPC_IF_CALLBACK function pointer. This callback function will be invoked when a call is made to the interface, although it will be called after the SD is checked. If the callback function returns RPC_S_OK then the call will be allowed, anything else will deny the call. The callback gets a pointer to the interface and the binding handle and can do various checks to determine if the caller is allowed to access the interface.

A common check is for the client's authentication level. The client can specify the level to use when connecting to the server using the RpcBindingSetAuthInfo API however the server can't directly specify the minimum authentication level it accepts. Instead the callback can use the RpcBindingInqAuthClient API to determine what the client used and grant or deny access based on that. The authentication levels we typically care about are as follows:
  • RPC_C_AUTHN_LEVEL_NONE - No authentication
  • RPC_C_AUTHN_LEVEL_CONNECT - Authentication at connect time, but not per-call.
  • RPC_C_AUTHN_LEVEL_PKT_INTEGRITY - Authentication at connect time, each call has integrity protection.
  • RPC_C_AUTHN_LEVEL_PKT_PRIVACY - Authentication at connect time, each call is encrypted and has integrity protection.
The authentication is implemented using a defined authentication service, such as NTLM or Kerberos, though that doesn't really matter for our purposes. Also note that this is only used for RPC services available over remote protocols such as named pipes or TCP. If the RPC server listens on ALPC then it's assumed to always be RPC_C_AUTHN_LEVEL_PKT_PRIVACY. Other checks the server could do would be the protocol sequence the client used, this would allow rejecting access via TCP but permit named pipes.

The final parameter is the flags. The flag most obviously related to security is RPC_IF_ALLOW_SECURE_ONLY (0x8). This blocks access to the interface if the current authentication level is RPC_C_AUTHN_LEVEL_NONE. This means the caller must be able to authenticate to the server using one of the permitted authentication services. It's not sufficient to use a NULL session, at least on any modern version of Windows. Of course this doesn't say much about who has authenticated, a server might still want to check the caller's identity.

The other important flag is RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH (0x10). If the server specifies a security callback and this flag is not set then any unauthenticated client will be automatically rejected. 

If this wasn't complex enough there's at least one other related setting which applies system wide which will determine what type of clients can access what RPC server. The Restrict Unauthenticated RPC Clients group policy. By default this is set to None if the RPC server is running on a server SKU of Windows and Authenticated on a client SKU. 

In general what this policy does is limit whether a client can use an unauthenticated transport such as TCP when they haven't also separately authenticated to an valid authentication level. When set to None RPC servers can be accessed via an unauthenticated transport subject to any other restrictions the interface is registered with. If set to Authenticated then calls over unauthenticated transports are rejected, unless the RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH flag is set for the interface or the client has authenticated separately. There's a third option, Authenticated without exceptions, which will block the call in all circumstances if the caller isn't using an authenticated transport. 

Ad-hoc Security

The final types of checks are basically anything else the server does to verify the caller. A common approach would be to perform a check within a specific function on the interface. For example, a server could generally allow unauthenticated clients, except when calling a method to read a important secret value. At that point is could insert an authentication level check to ensure the client has authenticated at RPC_C_AUTHN_LEVEL_PKT_PRIVACY so that the secret will be encrypted when returned to the client. 

Ultimately you'll have to check each function you're interested in to determine what, if any, security checks are in place. As with all ad-hoc checks it's possible that there's a logic bug in there which can be exploited to bypass the security restrictions.

Digging into EFSRPC

Okay, that covers the basics of how an RPC server is secured. Let's look at the specific example of the EFSRPC server abused by PetitPotam. Oddly there's two implementation of the RPC server, one in efslsaext.dll which the interface UUID of c681d488-d850-11d0-8c52-00c04fd90f7e and one in efssvc.dll with the interface UUID of df1941c5-fe89-4e79-bf10-463657acf44d. The one in efslsaext.dll is the one which is accessible unauthenticated, so let's start there. We'll go through the three approaches to securing the server to determine what it's doing.

First, the server does not register any of its own protocol sequences, with SDs or not. What this means is who can call the RPC server is dependent on what other endpoints have been registered by the hosting process, which in this case is LSASS.

Second, checking the for calls to one of the RPC server interface registration functions there's a single call to RpcServerRegisterIfEx in InitializeLsaExtension. This allows the caller to specify the security callback but not an SD. However in this case it doesn't specify any security callback. The InitializeLsaExtension function also does not specify either of the two security flags (it sets RPC_IF_AUTOLISTEN which doesn't have any security impact). This means that in general any authenticated caller is permitted.

Finally, from an ad-hoc security perspective all the main functions such as EfsRpcOpenFileRaw call the function EfsRpcpValidateClientCall which looks something like the following (error check removed).

void EfsRpcpValidateClientCall(RPC_BINDING_HANDLE Binding, 
                               PBOOL ValidClient) {
  unsigned int ClientLocalFlag;
  I_RpcBindingIsClientLocal(NULL, &ClientLocalFlag);
  if (!ClientLocalFlag) {
    RPC_WSTR StringBinding;
    RpcBindingToStringBindingW(Binding, &StringBinding);
    RpcStringBindingParseW(StringBinding, NULL, &Protseq, 
                           NULL, NULL, NULL);
    if (CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, 
        Protseq, -1, L"ncacn_np", -1) == CSTR_EQUAL)
        *ValidClient = TRUE;
    }
  }
}

Basically the ValidClient parameter will only be set to TRUE if the caller used the named pipe transport and the pipe wasn't opened locally, i.e. the named pipe was opened over SMB. This is basically all the security that's being checked for. Therefore the only security that could be enforced is limited by who's allowed to connect to a suitable named pipe endpoint.

At a minimum LSASS registers the \pipe\lsass named pipe endpoint. When it's setup in lsasrv.dll a SD is defined for the named pipe that grants the following users access:
  • Everyone
  • NT AUTHORITY\ANONYMOUS LOGON
  • BUILTIN\Administrators
Therefore in theory the anonymous user has access to the pipe, and as there are no other security checks in place in the interface definition. Now typically anonymous access isn't granted by default to named pipes via a NULL session, however domain controllers have an exception to this policy through the configured Network access: Named Pipes that can be accessed anonymously security option. For DCs this allows lsarpc, samr and netlogon pipes, which are all aliases for the lsass pipe, to be accessed anonymously.

You can now understand why the EFS RPC server is accessible anonymously on DCs. How does the other EFS RPC server block access? In that case it specifies an interface SD to limit access to only the Everyone group and BUILTIN\Administrators. By default the anonymous user isn't a member of Everyone (although it can be configured as such) therefore this blocks access even if you connected via the lsass pipe.

The Fix is In

What did Microsoft do to fix PetitPotam? One thing they definitely didn't do is change the interface registration or the named pipe endpoint security. Instead they added an additional ad-hoc check to EfsRpcOpenFileRaw. Specifically they added the following code:

DWORD AllowOpenRawDL = 0;
RegGetValueW(
  HKEY_LOCAL_MACHINE,
  L"SYSTEM\\CurrentControlSet\\Services\\EFS",
  L"AllowOpenRawDL",
  RRF_RT_REG_DWORD | RRF_ZEROONFAILURE,
  NULL,
  &AllowOpenRawDL);
if (AllowOpenRawDL == 1 && 
    !EfsRpcpValidateClientCall(hBinding, &ValidClient) && ValidClient) {
  // Call allowed.
}

Basically unless the AllowOpenRawDL registry value is set to one then the call is blocked entirely regardless of the authenticating client. This seems to be a perfectly valid fix, except that EfsRpcOpenFileRaw isn't the only function usable to start an NTLM authentication session. As pointed out by Lee Christensen you can also do it via EfsRpcEncryptFileSrv or EfsRpcQueryUsersOnFile or others. Therefore as no other changes were put in place these other functions are accessible just as unauthenticated as the original.

It's really unclear how Microsoft didn't see this, but I guess they might have been blinded by them actually fixing something which they were adamant was a configuration issue that sysadmins had to deal with. 

UPDATE 2021/08/17: It's worth noting that while you can access the other functions unauthenticated it seems any network access is done using the "authenticated" caller, i.e. the ANONYMOUS user so it's probably not that useful. The point of this blog is not about abusing EFSRPC but why it's abusable :-)

Anyway I hope that explains why PetitPotam works unauthenticated (props to topotam77 for the find) and might give you some insight into how you can determine what RPC servers might be accessible going forward. 

Wednesday, 1 July 2020

Generating NDR Type Serializers for C#

As part of updating NtApiDotNet to v1.1.28 I added support for Kerberos authentication tokens. To support this I needed to write the parsing code for Tickets. The majority of the Kerberos protocol uses ASN.1 encoding, however some Microsoft specific parts such as the Privileged Attribute Certificate (PAC) uses Network Data Representation (NDR). This is due to these parts of the protocol being derived from the older NetLogon protocol which uses MSRPC, which in turn uses NDR.

I needed to implement code to parse the NDR stream and return the structured information. As I already had a class to handle NDR I could manually write the C# parser but that'd take some time and it'd have to be carefully written to handle all use cases. It'd be much easier if I could just use my existing NDR byte code parser to extract the structure information from the KERBEROS DLL. I'd fortunately already written the feature, but it can be non-obvious how to use it. Therefore this blog post gives you an overview of how to extract NDR structure data from existing DLLs and create standalone C# type serializer.

First up, how does KERBEROS parse the NDR structure? It could have manual implementations, but it turns out that one of the lesser known features of the MSRPC runtime on Windows is its ability to generate standalone structure and procedure serializers without needing to use an RPC channel. In the documentation this is referred to as Serialization Services.

To implement a Type Serializer you need to do the following in a C/C++ project. First, add the types to serialize inside an IDL file. For example the following defines a simple type to serialize.

interface TypeEncoders
{
    typedef struct _TEST_TYPE
    {
        [unique, string] wchar_t* Name;
        DWORD Value;
    } TEST_TYPE;
}

You then need to create a separate ACF file with the same name as the IDL file (i.e. if you have TYPES.IDL create a file TYPES.ACF) and add the encode and decode attributes.

interface TypeEncoders
{
    typedef [encode, decode] TEST_TYPE;
}

Compiling the IDL file using MIDL you'll get the client source code (such as TYPES_c.c), and you should find a few functions, the most important being TEST_TYPE_Encode and TEST_TYPE_Decode which serialize (encode) and deserialize (decode) a type from a byte stream. How you use these functions is not really important. We're more interested in understanding how the NDR byte code is configured to perform the serialization so that we can parse it and generate our own serializers. 

If you look at the Decode function when compiled for a X64 target it should look like the following:

void
TEST_TYPE_Decode(
    handle_t _MidlEsHandle,
    TEST_TYPE * _pType)
{
    NdrMesTypeDecode3(
         _MidlEsHandle,
         ( PMIDL_TYPE_PICKLING_INFO  )&__MIDL_TypePicklingInfo,
         &TypeEncoders_ProxyInfo,
         TypePicklingOffsetTable,
         0,
         _pType);
}

The NdrMesTypeDecode3 is an API implemented in the RPC runtime DLL. You might be shocked to hear this, but this function and its corresponding NdrMesTypeEncode3 are not documented in MSDN. However, the SDK headers contain enough information to understand how it works.

The API takes 6 parameters:
  1. The serialization handle, used to maintain state such as the current stream position and can be used multiple times to encode or decode more that one structure in a stream.
  2. The MIDL_TYPE_PICKLING_INFO structure. This structure provides some basic information such as the NDR engine flags.
  3. The MIDL_STUBLESS_PROXY_INFO structure. This contains the format strings and transfer types for both DCE and NDR64 syntax encodings.
  4. A list of type offset arrays, these contains the byte offset into the format string (from the Proxy Info structure) for all type serializers.
  5. The index of the type offset in the 4th parameter.
  6. A pointer to the structure to serialize or deserialize.

Only parameters 2 through 5 are needed to parse the NDR byte code correctly. Note that the NdrMesType*3 APIs are used for dual DCE and NDR64 serializers. If you compile as 32 bit it will instead use NdrMesType*2 APIs which only support DCE. I'll mention what you need to parse the DCE only APIs later, but for now most things you'll want to extract are going to have a 64 bit build which will almost always use NdrMesType*3 even though my tooling only parses the DCE NDR byte code.

To parse the type serializers you need to load the DLL you want to extract from into memory using LoadLibrary (to ensure any relocations are processed) then use either the Get-NdrComplexType PS command or the NdrParser::ReadPicklingComplexType method and pass the addresses of the 4 parameters.

Let's look at an example in KERBEROS.DLL. We'll pick the PAC_DEVICE_INFO structure as it's pretty complex and would require a lot of work to manually write a parser. If you disassemble the PAC_DecodeDeviceInfo function you'll see the call to NdrMesTypeDecode3 as follows (from the DLL in Windows 10 2004 SHA1:173767EDD6027F2E1C2BF5CFB97261D2C6A95969).

mov     [rsp+28h], r14  ; pObject
mov     dword ptr [rsp+20h], 5 ; nTypeIndex
lea     r9, off_1800F3138 ; ArrTypeOffset
lea     r8, stru_1800D5EA0 ; pProxyInfo
lea     rdx, stru_1800DEAF0 ; pPicklingInfo
mov     rcx, [rsp+68h]  ; Handle
call    NdrMesTypeDecode3

From this we can extract the following values:

MIDL_TYPE_PICKLING_INFO = 0x1800DEAF0
MIDL_STUBLESS_PROXY_INFO = 0x1800D5EA0
Type Offset Array = 0x1800F3138
Type Offset Index = 5

These addresses are using the default load address of the library which is unlikely to be the same as where the DLL is loaded in memory. Get-NdrComplexType supports specifying relative addresses from a base module, so subtract the base address of 0x180000000 before using them. The following script will extract the type information.

PS> $lib = Import-Win32Module KERBEROS.DLL
PS> $types = Get-NdrComplexType -PicklingInfo 0xDEAF0 -StublessProxy 0xD5EA0 `
     -OffsetTable 0xF3138 -TypeIndex 5 -Module $lib

As long as there was no error from this command the $types variable will now contain the parsed complex types, in this case there'll be more than one. Now you can format them to a C# source code file to use in your application using Format-RpcComplexType.

PS> Format-RpcComplexType $types -Pointer

This will generate a C# file which looks like this. The code contains Encoder and Decoder classes with static methods for each structure. We also passed the Pointer parameter to Format-RpcComplexType. This is so that the structured are wrapped inside a Unique Pointers. This is the default when using the real RPC runtime, although except for Conformant Structures isn't strictly necessary. If you don't do this then the decode will typically fail, certainly in this case.

You might notice a serious issue with the generated code, there are no proper structure names. This is unavoidable, the MIDL compiler doesn't keep any name information with the NDR byte code, only the structure information. However, the basic Visual Studio refactoring tool can make short work of renaming things if you know what the names are supposed to be. You could also manually rename everything in the parsed structure information before using Format-RpcComplexType.

In this case there is an alternative to all that. We can use the fact that the official MS documentation contains a full IDL for PAC_DEVICE_INFO and its related structures and build our own executable with the NDR byte code to extract. How does this help? If you reference the PAC_DEVICE_INFO structure as part of an RPC interface no only can you avoid having to work out the offsets as Get-RpcServer will automatically find the location you can also use an additional feature to extract the type information from your private symbols to fixup the type information.

Create a C++ project and in an IDL file copy the PAC_DEVICE_INFO structures from the protocol documentation. Then add the following RPC server.

[
    uuid(4870536E-23FA-4CD5-9637-3F1A1699D3DC),
    version(1.0),
]
interface RpcServer
{
    int Test([in] handle_t hBinding, 
             [unique] PPAC_DEVICE_INFO device_info);
}

Add the generated server C code to the project and add the following code somewhere to provide a basic implementation:

#pragma comment(lib, "rpcrt4.lib")

extern "C" void* __RPC_USER MIDL_user_allocate(size_t size) {
    return new char[size];
}

extern "C" void __RPC_USER MIDL_user_free(void* p) {
    delete[] p;
}

int Test(
    handle_t hBinding,
    PPAC_DEVICE_INFO device_info) {
    printf("Test %p\n", device_info);
    return 0;
}

Now compile the executable as a 64-bit release build if you're using 64-bit PS. The release build ensures there's no weird debug stub in front of your function which could confuse the type information. The implementation of Test needs to be unique, otherwise the linker will fold a duplicate function and the type information will be lost, we just printf a unique string.

Now parse the RPC server using Get-RpcServer and format the complex types.

PS> $rpc = Get-RpcServer RpcServer.exe -ResolveStructureNames
PS> Format-RpcComplexType $rpc.ComplexTypes -Pointer

If everything has worked you'll now find the output to be much more useful. Admittedly I also did a bit of further cleanup in my version in NtApiDotNet as I didn't need the encoders and I added some helper functions.

Before leaving this topic I should point out how to handle called to NdrMesType*2 in case you need to extract data from a library which uses that API. The parameters are slightly different to NdrMesType*3.

void
TEST_TYPE_Decode(
    handle_t _MidlEsHandle,
    TEST_TYPE * _pType)
{
    NdrMesTypeDecode2(
         _MidlEsHandle,
         ( PMIDL_TYPE_PICKLING_INFO  )&__MIDL_TypePicklingInfo,
         &TypeEncoders_StubDesc,
         ( PFORMAT_STRING  )&types__MIDL_TypeFormatString.Format[2],
         _pType);
}
  1. The serialization handle.
  2. The MIDL_TYPE_PICKLING_INFO structure.
  3. The MIDL_STUB_DESC structure. This only contains DCE NDR byte code.
  4. A pointer into the format string for the start of the type.
  5. A pointer to the structure to serialize or deserialize.
Again we can discard the first and last parameters. You can then get the addresses of the middle three and pass them to Get-NdrComplexType.

PS> Get-NdrComplexType -PicklingInfo 0x1234 `
    -StubDesc 0x2345 -TypeFormat 0x3456 -Module $lib

You'll notice that there's a offset in the format string (2 in this case) which you can pass instead of the address in memory. It depends what information your disassembler shows:

PS> Get-NdrComplexType -PicklingInfo 0x1234 `
    -StubDesc 0x2345 -TypeOffset 2 -Module $lib

Hopefully this is useful for implementing these NDR serializers in C#. As they don't rely on any native code (or the RPC runtime) you should be able to use them on other platforms in .NET Core even if you can't use the ALPC RPC code.

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