Speaking DCERPC to Windows: Creating Services Without CreateService
About the author. I’m Colin Xiao — an independent reverse engineer, mostly working solo on contract jobs. I dig into compiled code of every kind, including but not limited to: Android and iOS apps, x86/x64/ARM native binaries, packers and obfuscators, DRM and license checks, network protocols and APIs, crypto and signature algorithms, malware, game binaries, and embedded/IoT firmware. I also handle large-scale data collection and vulnerability research. If you have any project in mind, I’d be glad to take it on — whatever it is, as long as it’s within my wheelhouse. Just reach out. You can reach me on X at @colinxiao_lab or on Telegram at https://t.me/ColinXiao666.
This research is published for educational and defensive purposes. The target sample is anonymized throughout.
Speaking DCERPC to Windows: Creating Services Without CreateService
This technique comes from a SilverFox-family sample. By hand-crafting RPC packets and sending them to the corresponding RPC service, the sample bypasses the hooks that most EDR/AV products place on ring-3 functions like CreateService and NdrClientCall3, evading service-creation monitoring and restrictions. The result is a fully “white” process chain: the process looks trusted and is unlinked from its parent, which lowers its detection score.
A normal RPC call builds the required data and finally talks over the NdrClientCall* family of functions, whose lower layer is really just the packet handling for RPC bind and RPC request — ultimately carried by NtAlpcSendWaitReceivePort and friends. Because CreateService and NdrClientCall get hooked by AV, this method avoids those sensitive APIs entirely and instead builds well-formed RPC packets by hand. That defeats security products which hook the RPC/service functions but don’t parse the RPC protocol itself.
A full RPC exchange is roughly four steps: RPC bind (connect), bind response, request, and request response. The structures and packets can be recovered by reversing services.exe, capturing local traffic, and reading the docs.
Packet structure and parsing
Before communication, an RPC client must bind — a bit like the TCP handshake. Each side keeps a local structure describing the connection state, which I’ll call RpcConnectionStruct. It tracks the connection state and the input/output buffers. The actual RPC packets are built from a few other key structures and data buffers:
- Bind packet:
RpcBaseHeaderStruct,RpcBindRequestHeaderStruct,RpcBindResponseHeaderStruct, data - Request packet:
RpcBaseHeaderStruct,RpcRequestHeaderStruct,RpcResponseHeaderStruct, data

Parsing, roughly:
- Send (write): write the
RpcBaseheader, thenRpcRequestHeader, thenRpcConnectionStruct->bProcedureInputData(the packet payload). - Receive (read): read the
RpcBaseheader, verify the RPC version, packet type, packet flags, and that the frame length matches what was actually read. ThenRpcResponseHeader, then the return data, which holds the response from the target RPC service.
RpcConnectionStruct
This structure holds the RPC connection state and required data. Like TCP, each side keeps its own local connection-state block; it’s one of the important pieces when building RPC traffic by hand. The most important field is hFile — a handle obtained by opening the named pipe the RPC service created, through which you reach the service. The request and response bodies also live in this structure, and every send/receive touches it:
struct RpcConnectionStruct {
HANDLE hFile; // file handle, the pipe used for RPC communication
DWORD dwCallIndex; // call index, identifies the RPC request
DWORD dwInputError; // input error flag
DWORD dwRequestInitialised; // request-initialised flag
BYTE bProcedureInputData[MAX_PROCEDURE_DATA_LENGTH]; // procedure input data; later sends are laid out here
DWORD dwProcedureInputDataLength; // procedure input data length
BYTE bProcedureOutputData[MAX_PROCEDURE_DATA_LENGTH]; // procedure output data
DWORD dwProcedureOutputDataLength; // procedure output data length
};
Taking the SVCCTL service used by the SilverFox sample as the example: services.exe registers the SVCCTL interface in SvcctrlMain, defining the named pipe \pipe\ntsvcs and two RPC transport protocol sequences — ncacn_np (named pipes) and ncalrpc (local inter-process communication).

An RPC service can bind several protocol sequences, or just one — it’s implementation-defined. Each bound sequence is another path to that interface. Windows’ DCE/MS RPC framework uses many transports:
+-- ncacn_ip_tcp (dynamic TCP port)
|
DCE/MS RPC--+-- ncadg_ip_udp (dynamic UDP port)
|
+-- ncacn_np (fixed 139/445 TCP ports)
|
+-- ... ... (other protocol sequences)
For named pipes (ncacn_np), you typically call CreateFile on the pipe path and stuff the returned handle into RpcConnectionStruct.hFile:
HANDLE hFile = NULL;
char szPipePath[512];
RpcConnectionStruct RpcConnection;
// set pipe path
memset(szPipePath, 0, sizeof(szPipePath));
_snprintf(szPipePath, sizeof(szPipePath) - 1, "\\\\.\\pipe\\%s", pPipeName);
// open rpc pipe
hFile = CreateFileA(szPipePath, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
memset((void*)&RpcConnection, 0, sizeof(RpcConnection));
RpcConnection.hFile = hFile;
For an RPC service, the actual functionality lives in the methods under its various interfaces. So to remotely invoke a specific service function you need not just the pipe handle, but also the interface to bind, the serialization/deserialization interface, and the procedure number. Some of these are in Microsoft’s docs, some come from reversing the RPC server, and some from captured traffic. Let’s walk through the remaining key structures.
RpcBaseHeaderStruct
The base header carries the RPC version, packet type, and packet length. During the initial bind, the packet length equals the base header plus the request header; a Bind packet is normally 72 bytes and a Bind_ack 60 bytes.
struct RpcBaseHeaderStruct
{
WORD wVersion; // version 5
BYTE bPacketType; // packet type: Bind=11, Bind_ack=12, 13=failure; 0 for other requests, 2=success in responses
BYTE bPacketFlags; // packet flags 3
DWORD dwDataRepresentation; // data representation 0x10
WORD wFragLength; // fragment length (Base header + BindRequest header + InputDataLength in the connection)
WORD wAuthLength; // auth length 0
DWORD dwCallIndex; // call index, increments from 0 per packet, maintained by the connection struct and re-stamped into the base header
};
wVersion is essentially always v5 in the DCERPC packets seen today (v4 shows up on very old Windows 2000 systems), so treat it as a constant 5.
bPacketType should be 11 (bind) on the initial connect — the RPC analogue of a TCP SYN — and when reading the bind response you must check it equals 12 (bind_ack, the ACK analogue); otherwise the connection failed, and the value is usually 13.
The rest is annotated above; consult the docs for full detail.
RpcBindRequestHeaderStruct
The bind request header carries the interface GUID to bind, the interface version, the transport, and its version.
For SVCCTL, services.exe registers the interface with RpcServerRegisterIf3, described by an RPC_SERVER_INTERFACE structure (shown in the appendix). Two fields matter here: InterfaceId and TransferSyntax — the service-interface GUID and the serialization-interface GUID — which we fill into the bind request header.
struct RpcBindRequestContextEntryStruct
{
WORD wContextID; // context ID identifying a specific interface/object. 0
WORD wTransItemCount; // transfer item count, usually the number of transfer syntaxes. 1
BYTE bInterfaceUUID[16]; // interface UUID, e.g. 367abb81-9844-35f1-ad32-98f038001003
DWORD dwInterfaceVersion; // interface version. 2
BYTE bTransferSyntaxUUID[16]; // transfer syntax UUID, e.g. 8a885d04-1ceb-11c9-9fe8-08002b104860
DWORD dwTransferSyntaxVersion; // transfer syntax version. 2
};
struct RpcBindRequestHeaderStruct
{
WORD wMaxSendFrag; // max send fragment size, default 4096
WORD wMaxRecvFrag; // max recv fragment size, default 4096
DWORD dwAssocGroup; // association group; 0 for bind, 0x00012bee in response
BYTE bContextCount; // context count
BYTE bAlign[3]; // alignment padding
RpcBindRequestContextEntryStruct Context; // context entry
};
The bind request header is two structures; the context-entry structure is 4-byte aligned so its size can shift. Everything except wContextID (maintained by both sides and stored in RpcConnectionStruct) must be analyzed per-service.
Reversing services.exe yields the interface GUID 367abb81-9844-35f1-ad32-98f038001003 and the serialization-protocol GUID 8a885d04-1ceb-11c9-9fe8-08002b104860, filled directly into RpcBindRequestContextEntryStruct.
RpcBindResponseHeaderStruct
This one is three segments: RpcBindResponseHeader1Struct + SecondaryAddrHeaderBlock + RpcBindResponseHeader2Struct.
struct RpcBindResponseHeader1Struct
{
WORD wMaxSendFrag;
WORD wMaxRecvFrag;
DWORD dwAssocGroup;
};
struct SecondaryAddrHeaderBlock
{
WORD ScndryAddrLen;
BYTE ScndryAddr;
BYTE bAlign[3];// 4-byte alignment padding
};
struct RpcBindResponseContextEntryStruct
{
WORD wResult;// non-zero means bind failed; must be checked when parsing
WORD wAlign; // Ack reason
BYTE bTransferSyntax[16];// when wResult is non-zero, the serialization protocol
DWORD dwTransferSyntaxVersion;
};
struct RpcBindResponseHeader2Struct
{
DWORD dwContextResultCount;// 1 means a context exists
RpcBindResponseContextEntryStruct Context;
};
The Bind_ack packet:

ScndryAddr has a variable length — anywhere from 2 to 6 bytes — and needs 4-byte alignment.
wResult is 0 on success; other values:
- Acceptance (0) — success; the server accepted the call and ran the operation.
- User rejection (1) — the user rejected the call, possibly due to permissions.
- Provider rejection (2) — the provider rejected the bind, usually an unsupported interface.
- Reject (3) — a generic rejection for unspecified reasons.
- Other rejection (4) — rejected for some other reason not listed above.
When wResult is non-zero, the other three fields become meaningful: wAlign gives a more specific failure reason (see the docs), and bTransferSyntax carries the server’s supported transfer-syntax UUID when wResult == 2 and wAlign == 1.
That’s enough to build a simple RPC bind packet. Once bound, you send the target interface’s procedure number to invoke the function you want — open the service control manager, create a service, start it, and so on.
RpcRequestHeaderStruct
The field to watch here is wProcedureNumber, which selects the remote method.
struct RpcRequestHeaderStruct
{
DWORD dwAllocHint; // allocation hint for the server to size its response
WORD wContextID; // context ID identifying a specific interface/object
WORD wProcedureNumber; // procedure number selecting the remote procedure, e.g. 24 for service creation
};
The Request packet:

For the SVCCTL interface, IDA on services.exe reveals the vtable mapping procedure numbers to functions. For example, wProcedureNumber = 2 calls RDeleteService to delete a service, and the rest increment through the vtable:

RpcResponseHeaderStruct
struct RpcResponseHeaderStruct
{
DWORD dwAllocHint; // allocation hint for the server to size its response
WORD wContextID; // context ID identifying a specific interface/object
BYTE bCancelCount; // cancel count, whether the request was cancelled
BYTE bAlign[1]; // alignment padding
};
The Response packet:

Putting it to work
With the structures and their packet layout understood, you can send the request data. To reproduce service creation and start: after obtaining the service-manager object, the data section carries that object plus the procedure number to execute the call. Creating a service requires more data — service name, display name, start type, binary path, and so on — but most functions in the vtable above just need the service-manager object plus a procedure number. Here’s the C code that simulates sending a service-creation packet (adapted from Cmdexec):



The program pointed to by the service path must implement ServiceMain (both EXEs and DLLs; a DLL must also export it).
The result below: testservice created, and Service-loader.exe runs as SYSTEM. This loader is analogous to the first-stage loader SilverFox uses — remote download, file drop, persistence, loading the payload into memory, decryption, thread injection, COM registration, DLL side-loading (“white-black”), and so on all follow later. This article focuses on the front half: the service-creation/start used for stealth and process-chain unlinking. The rest is standard RAT behavior, covered in other vendors’ analyses of SilverFox and similar families.

Appendix
Some notes from reproduction and other related structures. Skip if you have the background.
Service-related registry keys
Creating a service auto-generates a key named after it under HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services, plus values like ImagePath.
For a service started through svchost, the service name must exist under HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Svchost. Ways to achieve that:
- Add a new service group and put the service name in it;
- Add the service name to an existing group;
- Reuse a service name from an existing group that isn’t installed on this machine;
- Modify an existing service in an existing group to point its
ServiceDllat your own DLL.
The first two are legitimate. With (1), starting the service spawns a new svchost process. With (2), if that group’s svchost is already running, you can’t start the service immediately — svchost already cached the group’s info in memory and registered dispatch handlers via StartServiceCtrlDispatcher(), so a newly-added service can’t register again until the machine or that group’s svchost restarts. The last two are backdoor-friendly, especially (4): no new service is added, only one registry value changes, and nothing shows in the services console. For instance, the EventSystem service points at es.dll by default; repointing its ServiceDll to EventSystem.dll is very hard to spot. Beyond calling CreateService(), installing a service also requires setting its ServiceDll — and for (1)/(2), the svchost registry entries; clean those up on uninstall.
NdrClientCall3
The lowest ring-3 function you can see on the client side of an RPC call; the important structures are all in its arguments. The server side has a corresponding NdrServerCall*:
CLIENT_CALL_RETURN RPC_VAR_ENTRY NdrClientCall3(
MIDL_STUBLESS_PROXY_INFO *pProxyInfo,// pointer to the proxy info for this RPC call
unsigned long nProcNum,// the procedure number / operand to invoke
void *pReturnValue,
...
);
Newer IDL-generated code uses NdrClientCall2 instead, with slightly different arguments and an extra IDL_handle:
NdrClientCall2(
( PMIDL_STUB_DESC )&HelloCrispr_StubDesc,
(PFORMAT_STRING) &rpcDemo__MIDL_ProcFormatString.Format[0],
( unsigned char * )&IDL_handle);
The HelloCrispr_StubDesc argument is the first element of the _MIDL_STUB_DESC structure that pProxyInfo points to — one less level of indirection.
RpcServerRegisterIf
To build your own RPC server/client you start with an IDL description, generate the client/server stubs with the MIDL tool, then compile the resulting *_s.c, *_c.c, *_h.c files on the server and client sides. The server registers its interface with RpcServerRegisterIf, passing an RPC_IF_HANDLE:
RPCRTAPI
RPC_STATUS
RPC_ENTRY
RpcServerRegisterIf (
_In_ RPC_IF_HANDLE IfSpec,
_In_opt_ UUID __RPC_FAR * MgrTypeUuid,
_In_opt_ RPC_MGR_EPV __RPC_FAR * MgrEpv
);
typedef void __RPC_FAR * RPC_IF_HANDLE;
RPC_IF_HANDLE
The server registers its interface with the RpcServerRegisterIf family; the runtime maintains the registered info. The key structure is RPC_IF_HANDLE, which on the server side is RPC_SERVER_INTERFACE:
typedef struct _RPC_SERVER_INTERFACE
{
unsigned int Length;
RPC_SYNTAX_IDENTIFIER InterfaceId;
RPC_SYNTAX_IDENTIFIER TransferSyntax;
PRPC_DISPATCH_TABLE DispatchTable;
unsigned int RpcProtseqEndpointCount;
PRPC_PROTSEQ_ENDPOINT RpcProtseqEndpoint;// RpcProtocolSequence + Endpoint, the protocol sequence and port (string pointers)
RPC_MGR_EPV __RPC_FAR *DefaultManagerEpv;
void const __RPC_FAR *InterpreterInfo;// points to _MIDL_SERVER_INFO_
unsigned int Flags ;
} RPC_SERVER_INTERFACE, __RPC_FAR * PRPC_SERVER_INTERFACE;
On the server, 80 bytes into RPC_IF_HANDLE is the InterpreterInfo member, a pointer to _MIDL_SERVER_INFO_; 8 bytes into that (64-bit) is its second element, DispatchTable — the vtable holding pointers to the service functions (create, delete, start, etc.).
_MIDL_SERVER_INFO_
typedef struct _MIDL_SERVER_INFO_
{
PMIDL_STUB_DESC pStubDesc;// points to its own grandparent
const SERVER_ROUTINE * DispatchTable;// the functions provided
PFORMAT_STRING ProcString;
const unsigned short * FmtStringOffset;
const STUB_THUNK * ThunkTable;
PRPC_SYNTAX_IDENTIFIER pTransferSyntax;
ULONG_PTR nCount;
PMIDL_SYNTAX_INFO pSyntaxInfo;
} MIDL_SERVER_INFO, *PMIDL_SERVER_INFO;
The client side has the same RPC_IF_HANDLE, mirrored as RPC_CLIENT_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 *InterpreterInfo;
unsigned int Flags;
} RPC_CLIENT_INTERFACE, *PRPC_CLIENT_INTERFACE;
_MIDL_STUB_DESC
The first element of _MIDL_STUB_DESC is RpcInterfaceInformation — which is the RPC_IF_HANDLE pointer.
typedef struct _MIDL_STUB_DESC {
void *RpcInterfaceInformation;// RPC_IF_HANDLE / RPC_SERVER_INTERFACE structure
void * )(size_t) *(pfnAllocate;
void()(void *) * pfnFree;
union {
handle_t *pAutoHandle;
handle_t *pPrimitiveHandle;
PGENERIC_BINDING_INFO pGenericBindingInfo;
} IMPLICIT_HANDLE_INFO;// RPC_BINDING_HANDLE
const NDR_RUNDOWN *apfnNdrRundownRoutines;
const GENERIC_BINDING_ROUTINE_PAIR *aGenericBindingRoutinePairs;
const EXPR_EVAL *apfnExprEval;
const XMIT_ROUTINE_QUINTUPLE *aXmitQuintuple;
const unsigned char *pFormatTypes;
int fCheckBounds;
unsigned long Version;
MALLOC_FREE_STRUCT *pMallocFreeStruct;
long MIDLVersion;
const COMM_FAULT_OFFSETS *CommFaultOffsets;
const USER_MARSHAL_ROUTINE_QUADRUPLE *aUserMarshalQuadruple;
const NDR_NOTIFY_ROUTINE *NotifyRoutineTable;
ULONG_PTR mFlags;
const NDR_CS_ROUTINES *CsRoutineTables;
void *ProxyServerInfo;
_MIDL_STUBLESS_PROXY_INFO
typedef struct _MIDL_STUBLESS_PROXY_INFO {
PRPC_STUB_DESC pStubDesc;// points to MIDL_STUB_DESC
unsigned char *ProcFormatString;
USHORT *FormatStringOffset;
PRPC_SYNTAX_IDENTIFIER pTransferSyntax;
ULONG nCount;
struct _MIDL_SYNTAX_INFO *pSyntaxInfo;
} MIDL_STUBLESS_PROXY_INFO, *PMIDL_STUBLESS_PROXY_INFO;
This article is mostly compiled notes, so it may read a little rough in places.
References
- https://scz.617.cn/network/200602141259.txt
- https://xz.aliyun.com/t/11313
- https://www.freesion.com/article/9057903392/
- https://blog.csdn.net/qq_43332010/article/details/122841622
- https://www.huorong.cn/document/tech/vir_report/1784
Colin Xiao — I take on contract reverse-engineering work, and writeups like this are how I show what I can do. If you need help with anything in this space — including but not limited to the areas I cover above — I’m available. No project is too big or too small — if it’s reverse engineering and I can do it, I’ll take it. DM me on X (@colinxiao_lab) or Telegram (@ColinXiao666).
Get in touch