返回
顶部

rpc_demo,密码是1

搜索rpcserver注册位置,我们拿赛门铁克epp来做例子

主服务进程ccsvchst.exe

根据我们的rpc实例程序我们可以知道,rpc服务段肯定要调用RpcServerRegisterIf2函数进行注册,注册的时候要传入一个字符串来表示rpc服务的类型,示例程序是ncacn_ip_tcp

image-20250725093754290

使用rpcview可以看到主服务进程ccsvchst的rpc类型是ncalrpc

image-20250725093940695

那么我们搜索ccsvchst进程的所有二进制文件,去找这个字符串即可

我们使用windbg内核调试器获取到该进程的所有dll路径,然后全部拷贝到一个目录中,之后使用下面这个python脚本去搜索这个字符串

import os

def utf16le_search_in_file(file_path, utf16le_bytes):
    try:
        with open(file_path, 'rb') as f:
            data = f.read()
            return utf16le_bytes in data
    except Exception as e:
        # Skip files we can't read
        return False

def search_folder_for_utf16le_string(folder_path, target_string):
    utf16le_bytes = target_string.encode('utf-16le')
    for root, dirs, files in os.walk(folder_path):
        for filename in files:
            file_path = os.path.join(root, filename)
            if utf16le_search_in_file(file_path, utf16le_bytes):
                print(f"[FOUND] {file_path}")

if __name__ == "__main__":
    folder = r"C:\users\x\downloads\sep"  # 🔧 Change this
    search_string = "ncalrpc"     # 🔧 Change this
    search_folder_for_utf16le_string(folder, search_string)

搜索出来下面这3个不在system32下的dll包含这个字符串

CsrssDrvLoadDll.dll
ccIPC.dll
symamsi.dll

IPC很可能是进程间通信的意思,那我们就先来看这个dll

查看导入表,果不其然,存在RpcServerRegisterIfEx函数,查看其引用,找到注册代码

image-20250725102234794

对照我们的示例代码,那么第一个参数就是RemotePrivilegeCall_v1_0_s_ifspec

    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
    );

这个参数的值实际上就是RemotePrivilegeCall___RpcServerInterface的地址

RPC_IF_HANDLE RemotePrivilegeCall_v1_0_s_ifspec = (RPC_IF_HANDLE)& RemotePrivilegeCall___RpcServerInterface;

那么我们就知道unk_180031F80就是RemotePrivilegeCall___RpcServerInterface,他的结构如下:

static const RPC_SERVER_INTERFACE RemotePrivilegeCall___RpcServerInterface =
{
    sizeof(RPC_SERVER_INTERFACE),
    { { 0xAB4ED934,0x1293,0x10DE,{ 0xBC,0x12,0xAE,0x18,0xC4,0x8D,0xEF,0x33 } },{ 1,0 } },
    { { 0x8A885D04,0x1CEB,0x11C9,{ 0x9F,0xE8,0x08,0x00,0x2B,0x10,0x48,0x60 } },{ 2,0 } },
    (RPC_DISPATCH_TABLE*)&RemotePrivilegeCall_v1_0_DispatchTable,
    0,
    0,
    0,
    &RemotePrivilegeCall_ServerInfo,
    0x04000000
};

RemotePrivilegeCall_ServerInfo里面包含有rpc服务端函数的信息,现在我们去windbg里面看一下

RemotePrivilegeCall_ServerInfo是RPC_SERVER_INTERFACE结构体的InterpreterInfo字段,他的偏移量是80

image-20250725103451436

image-20250725103437553

那这个地方存的是RemotePrivilegeCall_ServerInfo的地址

static const MIDL_SERVER_INFO RemotePrivilegeCall_ServerInfo =
{
    &RemotePrivilegeCall_StubDesc,
    RemotePrivilegeCall_ServerRoutineTable,
    RemotePrivilegeCall__MIDL_ProcFormatString.Format,
    RemotePrivilegeCall_FormatStringOffsetTable,
    0,
    0,
    0,
    0 };

我们在8偏移处即可得到rpc服务端函数的表

dqs poi(ccipc!getfactory+2eb84+8)

image-20250725103757702

然后我们要找到client,看看他是怎么调用rpc接口的,首先我们要获取到服务端rpc的接口标识符,就是一个GUID

这个GUID就在RemotePrivilegeCall___RpcServerInterface的4偏移量,长度为0x10

image-20250725105207976

这一串字节序列同样也会出现在client的二进制文件中

image-20250725105234200

那么使用这种方法,我们就可以在赛门铁克的安装包中找到client

我靠赛门铁克他这个二进制里面的guid和实际跑起来的时候的guid竟然是不一样的,太逆天了,我自己的示例程序就是一样的,不知道他这个是怎么回事,但是好在第二个语法的GUID是正确的,我们可以先搜索这个来确定大致的范围

image-20250725111244462

那我们就是用下面的脚本搜索语法GUID字节序列04 5d 88 8a eb 1c c9 11-9f e8 08 00 2b 10 48 60

import os


def search_bytes_in_file(file_path, target_bytes):
    try:
        with open(file_path, 'rb') as f:
            data = f.read()
            return target_bytes in data
    except Exception:
        return False


def search_folder_for_bytes(folder_path, target_hex_string):
    # Convert hex string like "DEADBEEF" to bytes
    target_bytes = bytes.fromhex(target_hex_string)

    for root, dirs, files in os.walk(folder_path):
        for filename in files:
            file_path = os.path.join(root, filename)
            if search_bytes_in_file(file_path, target_bytes):
                print(f"[FOUND] {file_path}")


if __name__ == "__main__":
    folder = r"C:\Program Files\Symantec\Symantec Endpoint Protection"  # 🔧 Change this
    hex_string = "045d888aeb1cc9119fe808002b104860"  # 🔧 Change this to your target bytes (no 0x, no spaces)
    search_folder_for_bytes(folder, hex_string)

关键问题是这样找出来的也不一定是客户端,因为服务端也会有这个语法GUID

那这样的话,我们就只能用rpcview来逆向这个接口,获取到定义文件了

[
uuid(a3e7e0e5-615c-4355-964f-a58e97cfc695),
version(1.0),
]
interface DefaultIfName
{

long Proc0(
    [in]long arg_1, 
    [in][size_is(arg_1)]byte *arg_2, 
    [out]long *arg_3, 
    [out][ref][size_is(, *arg_3)]byte **arg_4, 
    [out][context_handle] void** arg_5);

long Proc1(
    [in][out][context_handle] void** arg_1);

long Proc2(
    [in][context_handle] void* arg_1, 
    [in]long arg_2, 
    [in][size_is(arg_2)]byte *arg_3);

long Proc3(
    [in][context_handle] void* arg_1, 
    [in]long arg_2, 
    [in][size_is(arg_2)]byte *arg_3);

long Proc4(
    [in][context_handle] void* arg_1, 
    [in]long arg_2, 
    [in][size_is(arg_2)]byte *arg_3, 
    [out]long *arg_4, 
    [out][ref][size_is(, *arg_4)]byte **arg_5);

long Proc5(
    [in][context_handle] void* arg_1, 
    [in]long arg_2, 
    [in][size_is(arg_2)]byte *arg_3);

long Proc6(
    [in][context_handle] void* arg_1, 
    [in]long arg_2, 
    [in][size_is(arg_2)]byte *arg_3, 
    [in]long arg_4, 
    [in][size_is(arg_4)]byte *arg_5, 
    [out]long *arg_6, 
    [out][ref][size_is(, *arg_6)]byte **arg_7);
} 

正好7个函数,和我们前面分析出来的函数数组也是能够对应得上的

但是为什么这里逆向出来的没有binding_handle啊,这对吗这?

感觉还是要找到客户端才行

recatos源码

从网上把recatos的源码下载下来然后把rpcrt4.dll编译出来,我们的示例程序使用动态加载的方式调用我们自己编译出来的rpcrt4.dll,就可以在调试的时候查看源代码了

RpcServerUseProtseqEpW

对于tcp_ip形式的RPC,rpcrt其实就是监听了一个指定端口的socket而已

image-20250728101457496

>   rpcrt4.dll!rpcrt4_protseq_ncacn_ip_tcp_open_endpoint(_RpcServerProtseq * protseq, const char * endpoint) Line 1647  C
    rpcrt4.dll!RPCRT4_use_protseq(_RpcServerProtseq * ps, const char * endpoint) Line 841   C
    rpcrt4.dll!RpcServerUseProtseqEpExW(wchar_t * Protseq, unsigned int MaxCalls, wchar_t * Endpoint, void * SecurityDescriptor, _RPC_POLICY * lpPolicy) Line 1053  C
    rpcrt4.dll!RpcServerUseProtseqEpW(wchar_t * Protseq, unsigned int MaxCalls, wchar_t * Endpoint, void * SecurityDescriptor) Line 939 C
    RPCServer.exe!main() Line 70    C++
    [External Code] 

也就是说在调用RpcServerUseProtseqEpW之后,tcp端口就已经开始监听了

RpcServerRegisterIf2 注册接口

所谓的注册,就是把我们添加到一个全局的list中

image-20250728102807264

RpcEpRegisterW 注册接口到end point mapper

这个其实是和\\pipe\\epmapper RPC接口进行通信来进行注册的,最后会看到他调用ndrclientcall2和这个接口进行通信

image-20250728104908493

ncalrpc internal

这个通信机制的内部就是使用命名管道

image-20250728160151411

管道名称就是RpcServerUseProtseqEpW的第三个参数,即endpoint

\\.\pipe\lrpc\MyLocalRpcEndpoint

赛门铁克

000001e4`2778dab8  "{3334ED3B-03FC-43B2-A709-D20C266"
000001e4`2778daf8  "5E1D7}"

他这个nclrpc的管道名称好像是动态生成的,就是一个GUID