4 min read

Windows Inter Process Communication A Deep Dive Beyond the Surface - Part 11

Windows Inter Process Communication A Deep Dive Beyond the Surface - Part 11

Welcome to the new part of inter-process communication, where we are still discussing RPC. In the last part, we started analyzing the server stub and called it a static analysis. In today’s part, we will discuss the client side, which is simpler than the server side.

This part strongly depends on the previous part (Part 10). I will not repeat structures that we already discussed there, so I really recommend reading the previous part before this one.

We will start by revisiting the meaning of the RPC handle that we discussed earlier, and after that we will see what actually happens when the client makes a remote function call.

So let’s jump in.

Testbed

We will use the same testbed we introduced in Part 10, and the client code can be found here.

Again, the client is simple and tries to call two functions as shown below:

  result = PrintINT(5);
  printf("Received result from server: %d\n", result);
  char* outputString = NULL;
  result = PrintString("hello");
  printf("Received result from server: %d\n", result);

It calls PrintINT and passes an integer equal to 5, then prints the received value from the server. After that, it calls PrintString, passes the string "hello", and prints the received value as well.

Client Stub Generation

For the client stub, it is the same as the server side that we mentioned in the previous part. The stub is generated by MIDL and, in our case, is presented as example_c.c, where c stands for client.

RPC Handle

In the first parts, when we discussed RPC, we explained the meaning of the RPC handle and the different handle types (you can check it here).

The handle, by definition, is a logical link between the client and the server. In practice, it is an internal structure that describes the connection between the client and the server and contains information such as protocol, endpoint, and security settings.

In the client code, the handle is just a pointer to an internal RPC runtime structure. This is the only access the client has to the handle. (It is more complex than a normal programming structure, we will discuss that later during dynamic analysis.)

The handle is defined inside the client stub as:

handle_t ImplicitHandle;

The handle_t type is just a void*.

This handle is created inside the RPC runtime when the function RpcBindingFromStringBinding is called in the client code:

    status = RpcBindingFromStringBinding(
        (RPC_CSTR)"ncalrpc:[ExampleEndpoint]",
        &ImplicitHandle);

This call takes the string binding as input and returns a pointer to the handle, which will be used by the client later (for example, to set security attributes). As you can see, the client only holds a pointer and has limited control over the handle, which is fully managed by the RPC runtime.

Functions Call

The next and most important part is the function calls. In our case, we first call:

result = PrintINT(5);

We can see the declaration of this function in the header file example.h, which is generated by MIDL:

int PrintINT( 
    /* [in] */ int x);

The definition of this function can be found in the client-generated stub:

int PrintINT( 
    /* [in] */ int x)
{

    CLIENT_CALL_RETURN _RetVal;

    _RetVal = NdrClientCall2(
                  ( PMIDL_STUB_DESC  )&Example_StubDesc,
                  (PFORMAT_STRING) &example__MIDL_ProcFormatString.Format[0],
                  ( unsigned char * )&x);
    return ( int  )_RetVal.Simple;
    

Inside this function, we first define a variable of type CLIENT_CALL_RETURN, which will contain the return value from the remote server.

If we look at its definition:

typedef union _CLIENT_CALL_RETURN
    {
    void  *         Pointer;
    LONG_PTR        Simple;
    } CLIENT_CALL_RETURN;

This union allows handling both pointer and non-pointer return types:

  • Pointer: used when the RPC function returns a pointer (e.g., a structure or buffer)
  • Simple: used for simple values like integers or status codes

NdrClientCall2

Next, we have the call to NdrClientCall2.

This is similar in concept to NdrServerCall2 on the server side. It is one of the main entry points to the NDR engine on the client side.

Its main role is:

  • marshal the function parameters into a byte stream
  • send the request to the server
  • receive the response
  • unmarshal the return value

The function is defined as:

CLIENT_CALL_RETURN RPC_VAR_ENTRY NdrClientCall2(
  [in] PMIDL_STUB_DESC pStubDescriptor,
  [in] PFORMAT_STRING  pFormat,
       ...             
);

The first argument is a pointer to MIDL_STUB_DESC, which we already saw on the server side:

static const MIDL_STUB_DESC Example_StubDesc = 
    {
    (void *)& Example___RpcClientInterface,
    MIDL_user_allocate,
    MIDL_user_free,
    &ImplicitHandle,
    0,
    0,
    0,
    0,
    example__MIDL_TypeFormatString.Format,
    1, /* -error bounds_check flag */
    0x50002, /* Ndr library version */
    0,
    0x8010274, /* MIDL Version 8.1.628 */
    0,
    0,
    0,  /* notify & notify_flag routine table */
    0x1, /* MIDL flag */
    0, /* cs routines */
    0,   /* proxy/server info */
    0
    };

This structure provides:

  • memory allocation routines
  • a pointer to the type format string
  • a reference to the RPC interface description

The first field points to RPC_CLIENT_INTERFACE, which describes the client-side RPC interface.

typedef struct _RPC_CLIENT_INTERFACE
{
    unsigned int Length;
    RPC_SYNTAX_IDENTIFIER   InterfaceId;
    RPC_SYNTAX_IDENTIFIER   TransferSyntax;
    PRPC_DISPATCH_TABLE     DispatchTable;
    unsigned int            RpcProtseqEndpointCount;
    PRPC_PROTSEQ_ENDPOINT   RpcProtseqEndpoint;
    ULONG_PTR               Reserved;
    void const __RPC_FAR *  InterpreterInfo;
    unsigned int Flags ;
} RPC_CLIENT_INTERFACE, __RPC_FAR * PRPC_CLIENT_INTERFACE;

This structure contains:

  • the interface UUID
  • the transfer syntax (NDR)

The MIDL_STUB_DESC also contains:

example__MIDL_TypeFormatString.Format

This describes how data types are marshalled and unmarshalled which we saw in the server side too.

Returning back to the NdrClientCall2 , the second argument:

example__MIDL_ProcFormatString.Format[0]

which is a pointer into the procedure format string.

It describes how the specific function parameters should be marshalled and unmarshalled.

For PrintINT the offset is 0 however for PrintString it's 36 as code below:

    _RetVal = NdrClientCall2(
                  ( PMIDL_STUB_DESC  )&Example_StubDesc,
                  (PFORMAT_STRING) &example__MIDL_ProcFormatString.Format[36],
                  ( unsigned char * )&y);

The last parameter passed to NdrClientCall2 is a pointer to the function argument stack layout (we will discuss what does that meaning later).

In our case:

(unsigned char *)&x

This is a pointer to the integer we want to send to the server.

The function supports multiple parameters, which is why in the Microsoft definition you see ... (variadic arguments). These arguments are laid out in memory according to the procedure format, and the NDR engine uses that layout to marshal them correctly.

With the end of this part, we are finishing the static analysis. I know some parts may have felt confusing, especially things related to marshaling and the NDR engine. However, everything will become clearer when we dive into dynamic analysis.