Home

Faking Remote Procedure Calls

As I continue to prepare myself for the Offensive Security Exploitation Expert exam I am continuing to work my way through a Microsoft Edge Type Confusion bug. I got tangled up in Remote Procedure Calls. The OffSec syllabus is publicly available on their website.

What has RPC got to do with Microsoft Edge? It is known technique for bypassing certain mitigations. CVE-2021-26411 was the result of an exploitation of Internet Explorer found in the wild, and RPC was used to bypass Control Flow Guard (CFG).

Whenever I am trying to understand a complex topic I try to implement it in C so I can write my own code and debug it in WinDbg to get a better understanding of what is going on in memory and what is being passed to the associated Win32 APIs.

Previous Research

As with many of my blog topics there has been research done by much cleverer people than me. I have attended the Advanced Windows Exploitation course offered by Offsec and a lot of research on this topic was done by the authors. If you want to fully understand how RPC can be reverse engineered I highly recommend you attend the course.

However, others have carried out research in this area and have documented the structures:

The code is my own but some of the field contents have been taken from other peoples research. I will point those out as we go along.

Note: I did find it helpful to map out RPC structures from other implementations (which I will not disclose as I need to be very careful about what I write withought violating any academic policies), it’s a bit messy, but hopefully you get the idea:

Screenshot 2025-01-21 at 16 24 25

RPC Stubs

RPC has been around since the 1960s. Microsoft adopted it in Windows NT 3.1 in 1993, using it for inter-process and network communication via its MSRPC implementation. RPC is a feature to allow code in one process to call a procedure in a different process. RPC also manages transportation of the call, this enables RPCs to be carried out accross networks on processes running on different hosts.

The protocol uses client and server stubs to send and receive calls:

Screenshot 2025-01-21 at 16 24 25

The idea behind injecting fake RPC calls is that we can use the server runtime to get the server stub to call Win32 APIs out of context, completely bypassing the client stub and the transport layer:

Screenshot 2025-01-21 at 16 24 25

So, how can we do this. Let’s try to understand how RPC calls are processed using a basic RPC implementation.

NdrServerCall2

I decided to do some basic dynamic anlaysis on an RPC client/server call. I followed Building a Simple RPC Client and Server: A Step-by-Step Guide by Pavel Yosifovich which shows how to implement a very basic RPC server and client.

I ran the server in WinDbg and set a breakpoint on the Add function. I ran the client application, which calls the Add function using RPC. When the breakpoint was hit I looked at the call stack:

0:003> k L2
 # Child-SP          RetAddr               Call Site
00 000000ff`2d0feac8 00007ffa`be3c7863     Server!Add
01 000000ff`2d0fead0 00007ffa`be42b4a6     RPCRT4!Invoke+0x73
0:003> ?RPCRT4!Invoke+0x73-RPCRT4
Evaluate expression: 489571 = 00000000`00077863

Using Binary Ninja I discovered that the call to Invoke is made in the NdrStubCall2 function:

Screenshot 2025-01-21 at 16 24 25

Working backwards I could ascertain that the NdrServerCall2 function called NdrStubCall2 function, this looked like a basic wrapper function:

Screenshot 2025-01-21 at 16 24 25

Microsoft Documentation shows that NdrServerCall2 takes a single argument, which is a PRPC_MESSAGE pointer. We can create our fake RPC_MESSAGE and call this API:

Screenshot 2025-01-21 at 16 24 25

Next we can look at the RPC structs.

RPC Structures

Going through various research sources, dynamic analysis in WinDbg, and pulling my hair out I came up with the following diagram:

Screenshot 2025-01-21 at 16 24 25

I will try to explain the important fields for each structure.

RPC_MESSAGE

RPC_SERVER_INTERFACE

MIDL_SERVER_INFO

MIDL_STUB_DESC

Implementing Fake RPCs in C

I started by setting the scene. I created a large buffer where I would store my fake RPC structs, zeroed out the buffer, resolved the address of LoadLibraryA (this was the Win32 API I was going to call with the fake RPC), and get the base address of the RPC library to locate a vftable address:

printf("Faking RPC Calls\n----------------\n\n");

// allocate a large buffer to fake the RPC strucs in
LPVOID buffer = VirtualAlloc(NULL, 0x10000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (buffer == NULL)
{
	printf("[!] Unable to allocate buffer!\n");
	return 1;
}

// zero out the buffer
memset(buffer, 0x0, 0x10000);
printf("[+] Buffer allocation: 0x%p\n", buffer);

LPVOID loadLib = &LoadLibraryA;
HMODULE rpcLib = LoadLibraryA("RPCRT4.DLL");

Next, I started to build the RPC_MESSAGE structure in the buffer:

// index
DWORD index = 0;
PLONGLONG rpcStructs = (PLONGLONG)(buffer);

// RPC_MESSAGE - offset 0x00
rpcStructs[index] = (LONGLONG)buffer + 0x860; index += 1;								// 0x00 - vptr address;
rpcStructs[index] = (LONGLONG)0x10; index += 1;											// 0x08 - DataRepresentation
rpcStructs[index] = (LONGLONG)buffer + 0x800; index += 1;								// 0x10 - Arguments Buffer
rpcStructs[index] = 0x30; index += 2;													// 0x18 - BufferLength and ProcNum - both are DWORDS
rpcStructs[index] = (LONGLONG)buffer + 0x100; index += 4;								// 0x28 - RpcInterfaceInfo - ptr to RPC_SERVER_INTERFACE
rpcStructs[index] = 0x1000;																// 0x48 - RpcFlags - 0x1000 = RPC_BUFFER_COMPLETE

The RPC_MESSAGE structure points to a RPC_SERVER_INTERFACE:

// RPC_SERVER_INTERFACE - offset 0x100
index = 0x20;
rpcStructs[index] = 0x60;	 index += 10; // was 3										// 0x00 - Length - 0x60
rpcStructs[index] = (LONGLONG)buffer + 0x200; index += 1;								// 0x50 - InterpreterInfo - ptr to MIDL_SERVER_INFO
rpcStructs[index] = 0x4000000;															// 0x58 - Flags - 0x4000000

And this points to a MIDL_SERVER_INFO structure:

// MIDL_SERVER_INFO - offset 0x200
index = 0x40;
rpcStructs[index] = (LONGLONG)buffer + 0x300; index += 1;								// 0x00 - pStubDesc - ptr to MIDL_STUB_DESC
rpcStructs[index] = (LONGLONG)&loadLib; index += 1;										// 0x08 - DispatchTable - ptr to function to call
rpcStructs[index] = (LONGLONG)buffer + 0x900; index += 1;								// 0x10 - ProcString - ptr to offset +0x900
rpcStructs[index] = (LONGLONG)buffer + 0x960;											// 0x18 - FmtStringOffset

Within this code, there is a DispatchTable field which points to the LoadLibraryA API. There is also a ProcString field that points to a really complicated buffer which I will show last. This sturct also points to a MIDL_STUB_DESC structure:

// MIDL_STUB_DESC - offset 0x300
index = 0x61;
rpcStructs[index] = (LONGLONG)&malloc; index += 1;										// 0x08 - Allocator - ptr to malloc()
rpcStructs[index] = (LONGLONG)&free; index += 6;										// 0x10 - Deallocator - ptr to free()
rpcStructs[index] = (LONGLONG)buffer + 0x9a0; index += 1;								// 0x40 - pFormatTypes - offset 0x9a0
rpcStructs[index] = (LONGLONG)0x0005000200000000;										// 0x4c - Version - 0x50002

The arguments for the API call are pointed to by the RPC_MESSAGE struct:

// arguments buffer -  offset 0x800
char arg[] = "ws2_32.dll";

index = 0x100;
rpcStructs[index] = (LONGLONG)&arg; index += 1;											// argument 1
rpcStructs[index] = 0x2222222222222222; index += 1;										// argument 2
rpcStructs[index] = 0x3333333333333333; index += 1;										// argument 3
rpcStructs[index] = 0x4444444444444444; index += 1;										// argument 4
rpcStructs[index] = 0x5555555555555555; index += 1;										// argument 5
rpcStructs[index] = 0x6666666666666666; index += 1;										// argument 6

We can provide up to six arguments, but in this PoC only the first one is relevant. The RPC_MESSAGE also points to a vptr:

// vptr - offset 0x860
index = 0x10c;
rpcStructs[index] = (LONGLONG)rpcLib + 0xe2208;	index += 1;								// 0x00 - vftable address;
rpcStructs[index] = 0x0000004089abcdef;													// stops the exception after the call

The two DWORD fields directly after the vftable pointer is essential to stop the application crashing following the RPC. This was discovered by OffSec and will not be discussed here.

The ProcString buffer is also discussed in the AWE course and will not be discussed here. Here is the code:

// format (ProcString) string - offset 0x900
index = 0x120;
rpcStructs[index] = (LONGLONG)0x0000000000004832; index += 1;
rpcStructs[index] = (LONGLONG)0x0744001000600083; index += 1;
rpcStructs[index] = (LONGLONG)0x000000000000010a; index += 1;
rpcStructs[index] = (LONGLONG)0x000b000000480000; index += 1;
rpcStructs[index] = (LONGLONG)0x0048000b00080048; index += 1;
rpcStructs[index] = (LONGLONG)0x00180048000b0010; index += 1;
rpcStructs[index] = (LONGLONG)0x000b00200048000b; index += 1;
rpcStructs[index] = (LONGLONG)0x0070000b00280048; index += 1;
rpcStructs[index] = (LONGLONG)0x00001000000b0078;

Bringing it all together we make the NdrServerCall2 call and display the returned result:

NdrServerCall2((PRPC_MESSAGE)buffer);
printf("[+] Call completed!\n");

// in an exploit an arbitrary read would be used
printf("[+] Return value: 0x%p\n", *(LONGLONG*)rpcStructs[2]);

Notice that the return value is written to the ArgumentsBuffer field in the RPC_MESSAGE structure.

Fail

When running my code I found that it failed me!

This threw me for quite a while so I decided to go into Binary Ninja and WinDbg to find out why it was crashing. Within HeapAlloc there was an access violation:

(4dc.2bdc): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
ntdll!RtlAllocateHeap+0x20:
00007ffa bec508e0 817b10eeddeedd  cmp     dword ptr [rbx+10h],0DDEEDDEEh ds:00000000'®0000010=????????

Without going in to to much detail, by tracing the calls and registry assignments in WinDbg I identified the source of the crash was that rcx was NULL when HeapAlloc was called from within AllocWrapper in the RPC module. Using Binary Ninja we can see that rcx is populated from a global variable:

Screenshot 2025-01-21 at 16 25 57

When analysing this memory location in WinDbg we can clearly see that it is zeroed out:

0:000> dq RPCRT4+0x10cee8
00007ffa be45cee8  00000000`00000000  00000000`00000000
00007ffa be45cf08  00000000`00000000  00000000`00000000
00007ffa be45cf18  00000000`00000000  00000000`00000000
00007ffa be45cf28  00000000`00000000  00000000`00000000
00007ffa be45cf38  00000000`00000000  00000000`00000000
00007ffa be45cf48  00000000`00000000  00000000`00000000
00007ffa be45cf58  00000000`00000000  00000000`00000000

The prototype for HeapAlloc is shown below:

Screenshot 2025-01-21 at 16 25 57

So, rcx should contain a pointer to the heap used for allocation. I assumed, incorrectly, that the Allocator field in MIDL_STUB_DESC ensured that this variable was populated. When I realised this was not the case I suspected that I had to ensure that RPC was initialised for the application.

Initialising RPC

To ensure my application was initialised for RPC I used Building a Simple RPC Client and Server: A Step-by-Step Guide. If you are interested then take a look at this simple tutorial. The important part is that we need to implement two functions to inform RPC which allocator and deallocator we wish to use:

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

void midl_user_free(void* p)
{
	free(p);
}

Once I had set up RPC correctly I had no further issues.

For the Win

Running the code again I found that the fake RPC calls the LoadLibraryA Win32 API and loads the ws2_32.dll module:

Screenshot 2025-01-21 at 16 25 57

The return value is also returned to the application properly, without any crashes:

Screenshot 2025-01-21 at 16 25 57

Phew!

Conclusion

The purpose of this exercise was to understand how to fake RPCs and where to send them. It in no way bypasses CFG, but gives me the foundational knowledge of how I might craft fake RPC structures in an exploitation scenario. I learned a lot about RPC and it’s internals. I hope this is useful to at least one other person.

We are done here!

References

A Clever but Tedious CFG Bypass

Building a Simple RPC Client and Server: A Step-by-Step Guide

Demystifying Remote Procedure Calls (RPC) for Beginners: A Comprehensive Guide

Exploiting Windows RPC to bypass CFG mitigation: analysis of CVE-2021-26411 in-the-wild sample

Internet Explorer Memory Corruption Vulnerability

NdrServerCall2 function (rpcndr.h)

Network Data Representation (NDR)

Home

Comments

Feel free to leave comments or questions for this blog post. Please be respectful, I will moderate comments and reserve the right to remove them.