返回
顶部

参考链接:

本文所有压缩包的密码都是1,使用VS2017进行编译

项目文件

reactos_rpcrt4_test_code

使用reactos的rpcrt4.dll作为运行时来调试rpc的内部实现原理

下图中的aaarpcrt4.dll就是reactos项目的rpcrt4.dll,advapi32_vista.dll和kernel32_vista.dll是我从reactos操作系统中拷贝出来的,他俩是reactos的rpcrt4的依赖

image-20250922161413977

reactos的源码无法直接使用,需要对多个文件进行修改,修改后可以正常使用的reactos源码项目

另外,编译环境需要RosBE,把这个下载解压,然后bin目录添加到环境变量重启电脑即可编译reactos项目

服务端实现

RpcServerUseProtseqEp

RPCRT4_get_or_create_serverprotseq

构造一个Protseq字符串,然后调用RPCRT4_get_or_create_serverprotseq,该函数会构造传出参数RpcServerProtseq结构体,maxcall指定最大连接数

image-20250922170231373

进入该函数后,他会尝试遍历一个存储了所有已经存在的RpcServerProtseq列表,如果发现已经存在,就直接使用已经存在的

否则就调用alloc_serverprotoseq创建一个新的

该函数首先通过调用rpcrt4_get_protseq_ops来查询我们指定的protocol sequence是否在他的支持范围内,reactos的rpc运行时只支持下面这3种

protcol sequence name: ncacn_np
protcol sequence name: ncalrpc
protcol sequence name: ncacn_ip_tcp

我们匹配到ncalrpc协议,从protseq_ops结构体列表中返回对应的节点

image-20250922171340374

可以看到该结构体除了第一个字段是协议名称,其他的全部都是成员函数

紧接着调用成员函数alloc来分配一个RpcServerProtseq_np结构体,注意这里的后缀_np,因为我们使用的是ncalrpc协议,这里使用的也是对应的RpcServerProtseq结构体,不过它里面会内嵌一个RpcServerProtseq结构体,字段名为common

ncalrpc对应的alloca函数为

rpcrt4_protseq_np_alloc

然后填充该结构体的各个字段,该结构体的两个list会在此处初始化,分别用于记录listener和connection

还有一个list_entry作为protseqs全局列表的索引被添加进去,这个就是前面RPCRT4_get_or_create_serverprotseq尝试查询是否存在已经创建的协议序列的那个列表

并且还初始化了一个critical section,后面会用到

image-20250922172756478

RPCRT4_use_protseq

首先使用RPCRT4_protseq_is_endpoint_registered检测该段点是否已经被注册过

他查询的是我们前面创建出来的RpcServerProtseq_np的listener列表,也就是查询自己是否已经注册过这个端点

如果发现该段点还未被创建,就调用ops对象的成员函数open_endpoint来注册一个端点

status = ps->ops->open_endpoint(ps, endpoint);

对于ncalrpc端点,对应的函数是rpcrt4_protseq_ncalrpc_open_endpoint,该函数会调用RPCRT4_CreateConnection创建一个RpcConnection对象

根据协议序列,从一个全局数组conn_protseq_list中返回一个预设的connection_ops对象

image-20250922212924299

之后就是对该对象进行初始化,给各个字段进行赋值,其中初始化了两个list_entry,推测为后续提供给某个全局链表使用

返回之后调用ncalrpc_pipe_name函数,传入的参数为端点名称

reactos的ncalrpc并未真正使用windows的lpc机制,而是使用命名管道模拟的

image-20250922215632826

这个函数就是拼接出来一个命名管道路径,返回之后调用rpcrt4_conn_create_pipe创建命名管道

connection->pipe = CreateNamedPipeA(connection->listen_pipe, PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
                                        PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE,
                                        PIPE_UNLIMITED_INSTANCES,
                                        RPC_MAX_PACKET_SIZE, RPC_MAX_PACKET_SIZE, 5000, NULL);

之后connnection对象被插入到protseq对象的listeners列表中

list_add_head(&protseq->listeners, &Connection->protseq_entry);

RpcServerRegisterAuthInfo

注册认证信息

我们是本地通信,这一部分忽略

其实就是在全局链表中新增了一个rpc_server_registered_auth_info节点

RpcServerRegisterIfEx

就是创建了一个RpcServerInterface对象,然后添加到全局链表server_interfaces中

image-20250922225518055

RpcServerListen

该函数会调用RPCRT4_start_listen

该函数会创建一个事件叫做listen_done_event

然后全局变量listen_count自增,如果是第一次listen,自增之后将等于1,此时将全局变量std_listen设置为1

之后在一个循环中调用RPCRT4_start_listen_protseq,遍历所有的协议序列,逐个监听

if (std_listen)
  {
    EnterCriticalSection(&server_cs);
    LIST_FOR_EACH_ENTRY(cps, &protseqs, RpcServerProtseq, entry)
    {
      status = RPCRT4_start_listen_protseq(cps, TRUE);
      if (status != RPC_S_OK)
        break;

      /* make sure server is actually listening on the interface before
       * returning */
      RPCRT4_sync_with_server_thread(cps);
    }
    LeaveCriticalSection(&server_cs);
  }

传入的参数就是RpcServerProtseq对象

image-20250923095058627

RPCRT4_start_listen_protseq函数会初始化该对象的一些字段包括mgr_mutex和server_ready_event,然后启动一个线程,线程routine是RPCRT4_server_thread

RPCRT4_server_thread

首先是一个死循环

objs = cps->ops->get_wait_array(cps, objs, &count);

这里是一个动态注册的函数,ncalrpc对应的函数就是rpcrt4_protseq_np_get_wait_array

rpcrt4_protseq_np_get_wait_array

在该线程函数中,根据传入的RpcServerProtseq对象,可以定位到RpcServerProtseq_np对象,因为RpcServerProtseq对象是RpcServerProtseq_np的common字段,使用下面的代码即可定位到RpcServerProtseq_np对象的地址

CONTAINING_RECORD(protseq, RpcServerProtseq_np, common);

然后他会遍历protseq的listeners链表

LIST_FOR_EACH_ENTRY(conn, &protseq->listeners, RpcConnection_np, common.protseq_entry)

上面这个宏的含义如下

遍历protseq->listeners链表的每一个节点,每一个节点就是一个list_entry,该list_entry同时位于RpcConnection_np结构体的common.protseq_entry字段,这样只要我们拿到list_entry,减去common.protseq_entry字段在RpcConnection_np结构体中的偏移量,即可得到RpcConnection_np对象的地址,这样我们就可以获取到所有的conn节点

此时connection对象的listen_event字段为空,会尝试执行get_np_event函数

如果有event_cache就返回缓存并清空缓存,没有的话就创建一个

static HANDLE get_np_event(RpcConnection_np *connection)
{
    HANDLE event = InterlockedExchangePointer(&connection->event_cache, NULL);
    return event ? event : CreateEventW(NULL, TRUE, FALSE, NULL);
}

返回后调用NtFsControlFile函数

status = NtFsControlFile(conn->pipe, event, NULL, NULL, &conn->io_status, FSCTL_PIPE_LISTEN, NULL, 0, NULL, 0);

通知指定的pipe等待客户端连接,由于我们指定了event参数,这个调用会立即返回并将status设置为STATUS_PENDING,此时conn->io_status中并未包含任何有意义的值,等待操作真正完成之后,里面才会包含有意义的值

之后将count传出参数自增,需要注意的是count的初始值为1,因为他要预留一个位置用于存储mgr_event,表明当前处于等待连接状态的连接数量

后面会分配内存,大小为count*sizeof(HNADLE)

objs = HeapAlloc(GetProcessHeap(), 0, *count*sizeof(HANDLE));

其中objs[0]将存储RpcServerProtseq_np对象的mgr_event,之后使用跟上面一样的遍历方法对objs依次赋值conn对象的listen_event字段

这样我们就获取到了一个wait_array,用于等待来自客户端的连接

wait_for_new_connection

在上面调用rpcrt4_protseq_np_get_wait_array函数返回之后,我们得到一个wait_array

res = cps->ops->wait_for_new_connection(cps, count, objs);

之后传入RpcServerProtseq对象和wait_array以及array长度,此处为动态注册函数,ncalrpc对应的为rpcrt4_protseq_np_wait_for_new_connection

在该函数中,他会使用WaitForMultipleObjectsEx一次性等待所有的wait_array中的事件,这个函数会阻塞我们当前线程,直到有新的管道连接进来,或者mgr_event处于signaled状态,不要忘了objs数组的第一个成员就是mgr_event,而这个event会在我们的主线程中被signal

那么被主线程signal之后,我们的WaitForMultipleObjectsEx会返回WAIT_OBJECT_0,也就是说等待数组中的第一个事件被signal了,之后我们将返回值设为0并返回

然后将set_ready_event变量设置为TRUE

前面我们提到,我们是在一个死循环里面,此时我们会继续回到循环头部

if (set_ready_event)
    {
        /* signal to function that changed state that we are now sync'ed */
        SetEvent(cps->server_ready_event);
        set_ready_event = FALSE;
    }

那么此时这个条件就满足了,server_ready_event事件就会被signal,此时主线程的wait操作就可以解除阻塞状态,这样主线程就知道RPCRT4_server_thread线程已经准备好接收连接了

此时我们会再次调用到rpcrt4_protseq_np_wait_for_new_connection函数,这次就没有人会signal我们的mgr_event了,只有在pipe有新的连接进来时才会接触server_thread的阻塞

image-20250923112749731

这里有一个需要注意的点,mgr_event是自动重置事件,在signal之后,之前处于wait状态的线程解除阻塞,系统会自动重置mgr_event为non-signaled状态,再次调用wait依然还会阻塞

ps->mgr_event = CreateEventW(NULL, FALSE, FALSE, NULL);

CreateEventW的第二个参数为bManualReset,指定为FALSE即代表自动重置

在客户端调用rpcrt4_conn_open_pipe打开pipe之后,服务端收到连接,WaitForMultipleObjectsEx解除阻塞状态,进入连接处理的逻辑代码

b_handle = objs[res - WAIT_OBJECT_0];

使用上面的代码可以计算出来是哪一个正在listen的pipe handle接收到了连接,res是WaitForMultipleObjectsEx函数的返回值

        LIST_FOR_EACH_ENTRY(conn, &protseq->listeners, RpcConnection_np, common.protseq_entry)
        {
            if (b_handle == conn->listen_event)
            {
                release_np_event(conn, conn->listen_event);
                conn->listen_event = NULL;
                if (conn->io_status.u.Status == STATUS_SUCCESS || conn->io_status.u.Status == STATUS_PIPE_CONNECTED)
                    cconn = rpcrt4_spawn_connection(&conn->common);
                else
                    ERR("listen failed %x\n", conn->io_status.u.Status);
                break;
            }
        }

哪一个连接的event和解除阻塞状态的conn对象的listen_event对得上号,那么就说明这个连接有新的客户端进来了

release_np_event会获取cache_event字段,如果不为空,就关闭eventhandle,返回后将listen_event置空,但是这里直接置空的话,如果后面你需要关闭这个event的句柄,你怎么办?

哦我懂了,在release_np_event函数中,listen_event被交换到了event_cache字段中

然后调用rpcrt4_spawn_connection函数,传入的参数为收到连接的老的连接对象,该函数会使用RPCRT4_CreateConnection根据老的连接对象的属性创建一个新的连接对象,之后调用rpcrt4_conn_handoff函数

rpcrt4_conn_handoff(old_connection, connection);
static inline RPC_STATUS rpcrt4_conn_handoff(RpcConnection *old_conn, RpcConnection *new_conn)
{
  return old_conn->ops->handoff(old_conn, new_conn);
}

动态注册,ncalrpc对应的函数为rpcrt4_ncalrpc_handoff

其实他这个逻辑就是新的连接进来之后就创建出一个新的连接对象来承载新的连接,而老的连接对象的pipehandle字段将用存储一个新创建的pipe实例,相关的事件字段也会被重置,从而可以继续他的监听工作

而每次创建新连接的关键就是调用下面的代码

connection->pipe = CreateNamedPipeA(connection->listen_pipe, PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
                                        PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE,
                                        PIPE_UNLIMITED_INSTANCES,
                                        RPC_MAX_PACKET_SIZE, RPC_MAX_PACKET_SIZE, 5000, NULL);

你可能会问,我们之前不是已经创建过这个命名管道了吗,为什么这里还可以重复创建,原因就是管道的特殊之处就在于它可以拥有多个实例(instance)

我不知道他这里为什么要记录本地计算机名称

if (!GetComputerNameA(new_conn->NetworkAddr, &len))

最后使用新创建出来的连接对象作为参数,调用RPCRT4_new_client函数,该函数会创建一个线程,线程函数为RPCRT4_io_thread,参数为新连接,此时新连接对象中保存着收到连接的pipe实例的句柄

RPCRT4_receive_fragment

在服务端专门用来处理客户端连接的线程函数RPCRT4_io_thread会接收用户发送过来的数据

通过connect中的connection_ops对象动态注册的receive_fragment成员函数来接收数据包,不过我们当前调试的实例显示connection_ops对象的receive_fragment成员是空的

在这种情况下,RPCRT4_default_receive_fragment函数将会被调用

首先他会尝试读取出一个头部RpcPktCommonHdr

dwRead = rpcrt4_conn_read(Connection, &common_hdr, sizeof(common_hdr));

通过rpcrt4_conn_np_read进行namedpipe的读取操作

核心就是使用NtReadFile对管道进行读取

NtReadFile(connection->pipe, event, NULL, NULL, &connection->io_status, buffer, count, NULL, NULL);

而且这里传入了event参数,是一个异步读操作,会立即返回,实际调试的时候这里返回了一个0x80000005,STATUS_BUFFER_OVERFLOW,我懂了,他提供的buffer长度只是一个header的长度,而实际上客户端写入的数据要远大于一个header的长度,即使返回了bufferoverflow,他依然可以正常读取出一个header的数据,最终返回的io_status.information中存储的就是成功读取出的数据长度

读出的header数据如下

image-20250924173406379

header中的type字段可以用于确定header的类型

 sizeof(Header->request), 0, sizeof(Header->response),
    sizeof(Header->fault), 0, 0, 0, 0, 0, 0, 0, sizeof(Header->bind),
    sizeof(Header->bind_ack), sizeof(Header->bind_nack),
    0, 0, sizeof(Header->auth3), 0, 0, 0, sizeof(Header->http)

0xb对应的就是Header->bind,也就是说客户端发送过来的是bind请求

可以看到,一开始我们读取的是common_hdr,在获取到common_hdr之后,我们得到header类型,根据header类型获取到真正的header长度,然后我们按照这个长度分配buffer,将buffer向前移动sizeof(comon_hdr),因为前面以前将common_hdr读出来了,再从管道读的话就是之后的内容了

dwRead = rpcrt4_conn_read(Connection, &(*Header)->common + 1, hdr_length - sizeof(common_hdr));

&(*Header)->common + 1其实就是从header开始向前移动一个common_hdr的长度

在之前读取出来的common_hdr中,我们通过frag_len可以知道整个数据包的长度,那么减去现在的头部的长度,如果不等于0,说明后面还有payload

最终该函数读取出了header和payload

process_bind_packet

在确定当前请求为bind请求之后,就通过该函数来处理bind数据包

RPCRT4_find_interface

这个是我很感兴趣的一个函数,他会使用接口的GUID来寻找接口,我倒要看看他究竟是怎么实现的

sif = RPCRT4_find_interface(NULL, &ctxt_elem->abstract_syntax,
                                      &ctxt_elem->transfer_syntaxes[j], FALSE);

他传入了我们接口自定的GUID和传输语法GUID作为参数

查找的方法就是遍历全局链表server_interfaces,根据我们传入的interface GUID进行比对

  LIST_FOR_EACH_ENTRY(cif, &server_interfaces, RpcServerInterface, entry) {
    if (!memcmp(if_id, &cif->If->InterfaceId, sizeof(RPC_SYNTAX_IDENTIFIER)) &&
        (!transfer_syntax || !memcmp(transfer_syntax, &cif->If->TransferSyntax, sizeof(RPC_SYNTAX_IDENTIFIER))) &&
        (check_object == FALSE || UuidEqual(MgrType, &cif->MgrTypeUuid, &status)) &&
        std_listen) {
      InterlockedIncrement(&cif->CurrentCalls);
      break;
    }
  }

最后调用RPCRT4_MakeBinding函数

该函数就是初始化一个RpcBinding结构体

0:005> dt RpcBinding 00000242915AF1C0
aaarpcrt4!RpcBinding
   +0x000 refs             : 0n1
   +0x008 Next             : (null) 
   +0x010 server           : 0n1
   +0x014 ObjectUuid       : _GUID {00000000-0000-0000-0000-000000000000}
   +0x028 Protseq          : 0x00000242`915aefb0  "ncalrpc"
   +0x030 NetworkAddr      : 0x00000242`915af270  "DESKTOP-7PE7PN4"
   +0x038 Endpoint         : 0x00000242`915af2b0  "NCALRPCServer12138"
   +0x040 NetworkOptions   : (null) 
   +0x048 BlockingFn       : (null) 
   +0x050 ServerTid        : 0
   +0x058 FromConn         : 0x00000242`915acb40 _RpcConnection
   +0x060 Assoc            : (null) 
   +0x068 AuthInfo         : (null) 
   +0x070 QOS              : (null) 
   +0x078 CookieAuth       : (null) 

在这一切都处理完成之后,服务端需要发回一个ack确认包给客户端

RPCRT4_BuildBindAckHeader

构建完ack包之后,使用RPCRT4_SendWithAuth函数发送给client

发送函数会调用connection_ops对象的write方法,ncalrpc对应的是rpcrt4_conn_np_write函数,该函数的核心调用就是NtWriteFile,现在我们回去调试客户端

QueueUserWorkItem

客户端发送了RPCCALL到服务端,此时服务端的RPCRT4_io_thread线程函数的死循环开始处理PKT_REQUEST类型的数据包

 if (!QueueUserWorkItem(RPCRT4_worker_thread, packet, WT_EXECUTELONGFUNCTION)) {
        ERR("couldn't queue work item for worker thread, error was %d\n", GetLastError());
        HeapFree(GetProcessHeap(), 0, packet);
        status = RPC_S_OUT_OF_RESOURCES;
      } else {
        continue;
      }

他这里使用了用户模式的WorkItem,和windows内核模式的work item一样,用户模式下的workitem同样也运行在当前进程的arbitrary thread context下

将客户端发送过来的数据包packet作为workitem函数RPCRT4_worker_thread的参数,并且指定了WT_EXECUTELONGFUNCTION标志以便于windows thread pool决定是否创建新的线程

而此线程函数只是RPCRT4_process_packet函数的wrapper,该函数会根据packet类型调用对应的函数,此时应该调用PKT_REQUEST数据包的处理函数process_request_packet

其实我现在最想知道的就是他如何处理那个传出参数

根据客户端传过来的procnum从dispatchtable中定位对应的函数

image-20250925162553133

从上图中可以看到,其实派遣表中的6个函数全部都是NdrServerCall2,下面让我们来看看这个函数里面到底暗藏什么玄机

NdrServerCall2是NdrStubCall2的wrapper,根据函数的注释可以知道这个函数的功能是unmarshal客户端传送过来的数据并调用指定的函数

从RpcMsg中可以获取到服务端的派遣函数表

image-20250925163354488

RpcMsg之所以能够索引到这个信息,并不是说客户端发送的数据中包含这些东西,而是在服务端的代码中根据conn对象的guid定位到接口描述信息,然后把这个信息放到了rpcmsg对象的RpcInterfaceInformation字段中

关键点就在此处,服务端为我们分配了contexthandle并将其写入了传出参数的地址中

image-20250925170456736

在NdrContextHandleInitialize函数中,新分配的context handle结构体会被添加到一个全局链表中

list_add_tail(&assoc->context_handle_list, &context_handle->entry);

另外就是他通过RPCRT4_PushThreadContextHandle函数,在当前线程的tdata对象中也存了一个context_handle_list并将我们新创建的context_handle添加了进去

我现在不理解的的点事,服务端和客户端的context handl的值是互不相干的,那么他们是如何对这个context handle进行跟踪的呢?

server context handle定位代码调用栈

0:006> k
 # Child-SP          RetAddr               Call Site
00 0000004b`eeefe770 00007ffc`85255e0d     aaarpcrt4!RpcServerAssoc_FindContextHandle+0x63 [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\rpc_assoc.c @ 495] 
01 0000004b`eeefe7c0 00007ffc`8526ac7f     aaarpcrt4!NDRSContextUnmarshall2+0x1ad [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_contexthandle.c @ 349] 
02 0000004b`eeefe820 00007ffc`8525a2fb     aaarpcrt4!NdrServerContextNewUnmarshall+0x1ef [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 7238] 
03 0000004b`eeefe880 00007ffc`852755b0     aaarpcrt4!NdrContextHandleUnmarshall+0x1cb [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 7044] 
04 0000004b`eeefe8e0 00007ffc`85279080     aaarpcrt4!call_unmarshaller+0xf0 [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_stubless.c @ 126] 
05 0000004b`eeefe940 00007ffc`8527426f     aaarpcrt4!stub_do_args+0x4b0 [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_stubless.c @ 1283] 
06 0000004b`eeefe9d0 00007ffc`8527375c     aaarpcrt4!NdrStubCall2+0xa0f [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_stubless.c @ 1537] 
07 0000004b`eeeff460 00007ffc`8529148c     aaarpcrt4!NdrServerCall2+0x1c [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_stubless.c @ 1575] 
08 0000004b`eeeff4a0 00007ffc`8528d84d     aaarpcrt4!process_request_packet+0x46c [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\rpc_server.c @ 436] 
09 0000004b`eeeff540 00007ffc`8528e1f7     aaarpcrt4!RPCRT4_process_packet+0xad [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\rpc_server.c @ 515] 
0a 0000004b`eeeff590 00007ffc`986a0cd3     aaarpcrt4!RPCRT4_worker_thread+0x47 [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\rpc_server.c @ 538] 
0b 0000004b`eeeff5e0 00007ffc`9867d79a     ntdll!TpSetPoolStackInformation+0x1a3
0c 0000004b`eeeff6c0 00007ffc`96b47374     ntdll!TpReleaseCleanupGroupMembers+0xada
0d 0000004b`eeeff9c0 00007ffc`9867cc91     KERNEL32!BaseThreadInitThunk+0x14
0e 0000004b`eeeff9f0 00000000`00000000     ntdll!RtlUserThreadStart+0x21

说白了就是将context handle强转为NDR_SCONTEXT对象,然后和const_handle_list中的所有节点进行对比,比对的根据就是NDR_SCONTEXT->uuid

现在的问题是我不知道他是如果将客户端的contexthandle转换为server端可识别的一个合法地址的,需要再回溯一下

从实际调试结果来看,在服务端的RPCRT4_io_thread线程中,RPCRT4_receive_fragment函数接收过来的payload中就已经有了用于索引context handle的GUID了

我在RPCRT4_ReceiveWithAuth函数中增加了下面的代码

image-20250926102952780

这样在payload收到之后我们就可以看到他里面的GUID信息,从debug信息中也可以看到,此时dump出来的guid和一开始给context handle分配的GUID是一致的

image-20250926103108376

所以从本质上来讲,客户端的context handle的值对服务端并没有意义,有意义的是这个GUID,我现在可以大胆推测,客户端那边肯定对context handle和GUID进行了关联,现在我就去看一下他是怎么关联的

RPCRT4_start_listen

上面提到RPCRT4_start_listen通过调用RPCRT4_start_listen_protseq启动了一个新的线程,线程函数为RPCRT4_server_thread

该函数返回后,会调用RPCRT4_sync_with_server_thread

static void RPCRT4_sync_with_server_thread(RpcServerProtseq *ps)
{
  /* make sure we are the only thread sync'ing the server state, otherwise
   * there is a race with the server thread setting an older state and setting
   * the server_ready_event when the new state hasn't yet been applied */
  WaitForSingleObject(ps->mgr_mutex, INFINITE);

  ps->ops->signal_state_changed(ps);

  /* wait for server thread to make the requested changes before returning */
  WaitForSingleObject(ps->server_ready_event, INFINITE);

  ReleaseMutex(ps->mgr_mutex);
}

其中ps->ops->signal_state_changedrpcrt4_protseq_np_signal_state_changed,动态注册,ncalrpc对应的就是这个函数,这个函数会将上面RPCRT4_server_thread中的wait_array的第一个事件,即mgr_event设置为signaled状态

这里在signal完mgr_event事件之后,会阻塞等待server_ready_event事件,该事件会在RPCRT4_server_thread线程准备好接受连接之后被signal,这也是为什么这个函数叫做RPCRT4_sync_with_server_thread的原因,它可以确保在该函数返回的时候,服务线程已经开始监听连接了

之后主线程会调用RpcMgmtWaitServerListen来等待在RPCRT4_start_listen函数中创建的listen_done_event事件,从而进入阻塞状态

客户端实现

RpcStringBindingComposeA

就是字符串拼接

根据protocol sequence和endpoint拼接出来下面这样的字符串

ncalrpc:[NCALRPCServer12138]

RpcBindingFromStringBinding

传入上面拼接出来的字符串,获取到一个binding handle

在该函数中,会通过RPCRT4_CreateBindingA创建一个RpcBinding对象,并对其进行初始化

image-20250923140733900

最后会创建一个RpcAssoc对象关联到我们的Assoc字段上

image-20250923141340315

然后这个RpcAssoc对象被添加到了全局链表client_assoc_list中

RpcBindingSetAuthInfoExA

我们是本地通信,忽略这一块

NdrClientCall2

可以看到一开始是一堆汇编代码

PUBLIC NdrClientCall2
.PROC NdrClientCall2
    mov [rsp + 18h], r8
    .SAVEREG r8, 18h
    mov [rsp + 20h], r9
    .SAVEREG r9, 20h
    sub rsp, 28h
    .ALLOCSTACK 28h
    .ENDPROLOG

    lea r8, [rsp + 28h + 18h]
    xor r9, r9
    call ndr_client_call

    add rsp, 28h
    ret
.ENDP

对应的调试器中的汇编代码如下:

  589 00007ffe`5b7294d4 4c89442418      mov     qword ptr [rsp+18h],r8
  592 00007ffe`5b7294d9 4c894c2420      mov     qword ptr [rsp+20h],r9
  594 00007ffe`5b7294de 4883ec28        sub     rsp,28h
  598 00007ffe`5b7294e2 4c8d442440      lea     r8,[rsp+40h]
  599 00007ffe`5b7294e7 4d33c9          xor     r9,r9
  600 00007ffe`5b7294ea e8a1edfcff      call    aaarpcrt4!ndr_client_call (00007ffe`5b6f8290)
  602 00007ffe`5b7294ef 4883c428        add     rsp,28h
  603 00007ffe`5b7294f3 c3              ret

我们实际的C代码如下:

    g_pNdrClientCall2(
        (PMIDL_STUB_DESC)&NCALRPCInterface_StubDesc,
        (PFORMAT_STRING)&NCALRPCInterface__MIDL_ProcFormatString.Format[114],
        hBinding,
        szString,
        phContext);

可以看到我们实际上传了5个参数,现在r8\r9都被修改了,我很好奇他怎么访问到我们传入的参数

虽然r8\r9都被修改了,但是他们俩在此之前先存入了栈里面,最后r8还保存了一个栈地址作为第三个参数传入进去了,根据汇编代码我们可以知道

poi(r8)就是原始的a3,poi(r8+8)就是原始的a4,poi(r8+10)就是原始的a5

image-20250923144433597

image-20250923144347976

我们给NdrClientCall2传入的第二个参数其实是一个NDR_PROC_HEADER结构体

image-20250923145612389

如果flag中存在Oi_HAS_RPCFLAGS标志位,则需要重新解释为NDR_PROC_HEADER_RPC结构体

#define Oi_HAS_RPCFLAGS             0x08

image-20250923145835069

处理完成后pFormat指针行前移动sizeof(NDR_PROC_HEADER_RPC),header已经处理完了,该处理下面的内容了

    if (pProcHeader->Oi_flags & Oi_HAS_RPCFLAGS)
    {
        const NDR_PROC_HEADER_RPC *header_rpc = (const NDR_PROC_HEADER_RPC *)&pFormat[0];
        stack_size = header_rpc->stack_size;
        procedure_number = header_rpc->proc_num;
        pFormat += sizeof(NDR_PROC_HEADER_RPC);
    }

如果不存在Oi_OBJECT_PROC标志位,就调用get_handle_desc_size

#define Oi_OBJECT_PROC              0x04

其实这一部分代码就是在解析NCALRPCInterface__MIDL_ProcFormatString的format字段的144偏移

pFormat += get_handle_desc_size(pProcHeader, pFormat);

这一部分已经属于NDR引擎的范畴了

我们使用的是explicit_handle,handle_type为FC_BIND_PRIMITIVE,对应的结构体如下,这个和微软的文档是完全对的上的

0:000> dt NDR_EHD_PRIMITIVE
aaarpcrt4!NDR_EHD_PRIMITIVE
   +0x000 handle_type      : UChar
   +0x001 flag             : UChar
   +0x002 offset           : Uint2B

根据文档,offset字段描述了handle相对于stack开始位置的偏移

pFormat指针继续向前

我们确实是有两个参数,一个是helloworld字符串,另一个是传出参数context handle

0:000> dt NDR_PROC_PARTIAL_OIF_HEADER poi(pOIFHeader)
aaarpcrt4!NDR_PROC_PARTIAL_OIF_HEADER
   +0x000 constant_client_buffer_size : 0
   +0x002 constant_server_buffer_size : 0x38
   +0x004 Oi2Flags         : INTERPRETER_OPT_FLAGS
   +0x005 number_of_params : 0x2 ''

最后pformat前进到如下位置,然后调用do_ndr_client_call

image-20250923160204852

image-20250923160215484

根据format string的描述,我们的szString参数位于stack的0x8偏移

        RetVal = do_ndr_client_call(pStubDesc, pFormat, pHandleFormat,
                stack_top, fpu_stack, &stubMsg, procedure_number, stack_size,
                number_of_params, Oif_flags, ext_flags, pProcHeader);

image-20250923160417615

这跟我们在刚进入NdrClientCall2的汇编代码时做的验证是一致的

do_ndr_client_call

该函数会调用NdrClientInitializeNew初始化MIDL_STUB_MESSAGE结构体

0:000> dt   MIDL_STUB_MESSAGE 0049`009bedd0
aaarpcrt4!MIDL_STUB_MESSAGE
   +0x000 RpcMsg           : 0x00000049`009be490 _RPC_MESSAGE
   +0x008 Buffer           : 0x00000049`009bede0  ""
   +0x010 BufferStart      : (null) 
   +0x018 BufferEnd        : (null) 
   +0x020 BufferMark       : 0x00000049`009bf410  ""
   +0x028 BufferLength     : 0
   +0x02c MemorySize       : 0x7ffe
   +0x030 Memory           : 0x00000000`00000004  "--- memory read error at address 0x00000000`00000004 ---"
   +0x038 IsClient         : 0x1 ''
   +0x039 Pad              : 0xf4 ''
   +0x03a uFlags2          : 0x9b
   +0x03c ReuseBuffer      : 0n0
   +0x040 pAllocAllNodesContext : (null) 
   +0x048 pPointerQueueState : (null) 
   +0x050 IgnoreEmbeddedPointers : 0n0
   +0x058 PointerBufferMark : (null) 
   +0x060 CorrDespIncrement : 0 ''
   +0x061 uFlags           : 0 ''
   +0x062 UniquePtrCount   : 0
   +0x068 MaxCount         : 0x00000049`42630000
   +0x070 Offset           : 0x9bf040
   +0x074 ActualCount      : 0x49
   +0x078 pfnAllocate      : 0x00007ff6`54ae1069     void*  ncalrpcClt!ILT+100(MIDL_user_allocate)+0
   +0x080 pfnFree          : 0x00007ff6`54ae1096     void  ncalrpcClt!ILT+145(MIDL_user_free)+0
   +0x088 StackTop         : (null) 
   +0x090 pPresentedType   : (null) 
   +0x098 pTransmitType    : 0x00000000`00000008  "--- memory read error at address 0x00000000`00000008 ---"
   +0x0a0 SavedHandle      : 0x00000049`009bf204 Void
   +0x0a8 StubDesc         : 0x00007ff6`54aeb490 _MIDL_STUB_DESC
   +0x0b0 FullPtrXlatTables : (null) 
   +0x0b8 FullPtrRefId     : 0
   +0x0bc PointerLength    : 0
   +0x0c0 fInDontFree      : 0y0
   +0x0c0 fDontCallFreeInst : 0y0
   +0x0c0 fInOnlyParam     : 0y0
   +0x0c0 fHasReturn       : 0y0
   +0x0c0 fHasExtensions   : 0y0
   +0x0c0 fHasNewCorrDesc  : 0y0
   +0x0c0 fIsIn            : 0y0
   +0x0c0 fIsOut           : 0y0
   +0x0c0 fIsOicf          : 0y0
   +0x0c0 fBufferValid     : 0y0
   +0x0c0 fHasMemoryValidateCallback : 0y0
   +0x0c0 fInFree          : 0y0
   +0x0c0 fNeedMCCP        : 0y0
   +0x0c0 fUnused          : 0y000
   +0x0c0 fUnused2         : 0y1010101100010110 (0xab16)
   +0x0c4 dwDestContext    : 2
   +0x0c8 pvDestContext    : (null) 
   +0x0d0 SavedContextHandles : 0x00000000`00000005  -> ???? 
   +0x0d8 ParamNumber      : 0n-1424648832
   +0x0e0 pRpcChannelBuffer : (null) 
   +0x0e8 pArrayInfo       : (null) 
   +0x0f0 SizePtrCountArray : 0x00000000`00000038  -> ??
   +0x0f8 SizePtrOffsetArray : 0x00000001`0000007f  -> ??
   +0x100 SizePtrLengthArray : 0x000001ad`ab160700  -> 0
   +0x108 pArgQueue        : 0x00000000`00000001 Void
   +0x110 dwStubPhase      : 0
   +0x118 LowStackMark     : 0x00000000`00000005 Void
   +0x120 pAsyncMsg        : (null) 
   +0x128 pCorrInfo        : (null) 
   +0x130 pCorrMemory      : (null) 
   +0x138 pMemoryList      : (null) 
   +0x140 pCSInfo          : 0x000001ad`ab160240 CS_STUB_INFO
   +0x148 ConformanceMark  : 0x00000000`00000047  "--- memory read error at address 0x00000000`00000047 ---"
   +0x150 VarianceMark     : 0x000001ad`ab140320  "???"
   +0x158 Unused           : 0n1845411184976
   +0x160 pContext         : 0x00000000`14040010 _NDR_PROC_CONTEXT
   +0x168 ContextHandleHash : 0x00000000`0000000d Void
   +0x170 pUserMarshalList : 0x00000000`50000063 Void
   +0x178 Reserved51_3     : 0n176
   +0x180 Reserved51_4     : 0n1845411185384
   +0x188 Reserved51_5     : 0n0

之后调用client_get_handle获取到handle值

 switch (pProcHeader->handle_type)
    {
    /* explicit binding: parse additional section */
    case 0:
        switch (*pFormat) /* handle_type */
        {
        case FC_BIND_PRIMITIVE: /* explicit primitive */
            {
                const NDR_EHD_PRIMITIVE *pDesc = (const NDR_EHD_PRIMITIVE *)pFormat;

                TRACE("Explicit primitive handle @ %d\n", pDesc->offset);

                if (pDesc->flag) /* pointer to binding */
                    return **(handle_t **)ARG_FROM_OFFSET(pStubMsg->StackTop, pDesc->offset);
                else
                    return *(handle_t *)ARG_FROM_OFFSET(pStubMsg->StackTop, pDesc->offset);
            }
#define ARG_FROM_OFFSET(args, offset) ((args) + (offset))

stack起始位置+偏移获取到参数地址,然后取值得到真正的参数

由于我们启用了HAS_EXTENSIONS标志位,说明我们有一个NDR_PROC_HEADER_EXTS结构体,而该结构体的ext_flag又启用了HasNewCorrDesc标志位,因此我们会进入到这段代码

       if (ext_flags.HasNewCorrDesc)
        {
            /* initialize extra correlation package */
            NdrCorrelationInitialize(stub_msg, NdrCorrCache, sizeof(NdrCorrCache), 0);
            if (ext_flags.Unused & 0x2) /* has range on conformance */
                stub_msg->CorrDespIncrement = 12;
        }

这个东西是微软对NDR标准的扩展,使得它能够支持更多的特性

但是reactos似乎并没有完全实现这个东西,他在这留了一个fixme

client_do_args

client_do_args(stub_msg, format, STUBLESS_CALCSIZE, fpu_stack,
                       number_of_params, (unsigned char *)&retval);

format结构体有好几个,当需要确定参数类型的时候,就需要用到NCALRPCInterface__MIDL_TypeFormatString

重点,TypeFormatString

 PFORMAT_STRING pTypeFormat = (PFORMAT_STRING)&pStubMsg->StubDesc->pFormatTypes[params[i].u.type_offset];

pStubMsg->StubDesc就是NdrClientCall2的第一个参数,在我们的实例中就是NCALRPCInterface_StubDesc,他是一个MIDL_STUB_DESC结构体,pFormatTypes是一个UCAHR指针,所以我们可以直接以数组的形式来对其进行索引,从而获得目标参数的类型信息

image-20250923171506711

然后根据传入的phase(阶段)参数决定下一步的动作为STUBLESS_CALCSIZE,计算size,调用call_buffer_sizer

在该函数中,会根据参数类型调用不同的buffer_sizer函数

m = NdrBufferSizer[pFormat[0] & NDR_TABLE_MASK];
    if (m) m(pStubMsg, pMemory, pFormat);
0:000> dqs NdrBufferSizer l50
00007ffe`5b72efc0  00000000`00000000
00007ffe`5b72efc8  00007ffe`5b6d95d0 aaarpcrt4!NdrBaseTypeBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 6815]
00007ffe`5b72efd0  00007ffe`5b6d95d0 aaarpcrt4!NdrBaseTypeBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 6815]
00007ffe`5b72efd8  00007ffe`5b6d95d0 aaarpcrt4!NdrBaseTypeBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 6815]
00007ffe`5b72efe0  00007ffe`5b6d95d0 aaarpcrt4!NdrBaseTypeBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 6815]
00007ffe`5b72efe8  00007ffe`5b6d95d0 aaarpcrt4!NdrBaseTypeBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 6815]
00007ffe`5b72eff0  00007ffe`5b6d95d0 aaarpcrt4!NdrBaseTypeBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 6815]
00007ffe`5b72eff8  00007ffe`5b6d95d0 aaarpcrt4!NdrBaseTypeBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 6815]
00007ffe`5b72f000  00007ffe`5b6d95d0 aaarpcrt4!NdrBaseTypeBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 6815]
00007ffe`5b72f008  00007ffe`5b6d95d0 aaarpcrt4!NdrBaseTypeBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 6815]
00007ffe`5b72f010  00007ffe`5b6d95d0 aaarpcrt4!NdrBaseTypeBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 6815]
00007ffe`5b72f018  00007ffe`5b6d95d0 aaarpcrt4!NdrBaseTypeBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 6815]
00007ffe`5b72f020  00007ffe`5b6d95d0 aaarpcrt4!NdrBaseTypeBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 6815]
00007ffe`5b72f028  00007ffe`5b6d95d0 aaarpcrt4!NdrBaseTypeBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 6815]
00007ffe`5b72f030  00007ffe`5b6d95d0 aaarpcrt4!NdrBaseTypeBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 6815]
00007ffe`5b72f038  00007ffe`5b6d95d0 aaarpcrt4!NdrBaseTypeBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 6815]
00007ffe`5b72f040  00007ffe`5b6d95d0 aaarpcrt4!NdrBaseTypeBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 6815]
00007ffe`5b72f048  00007ffe`5b6dc1d0 aaarpcrt4!NdrPointerBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 1607]
00007ffe`5b72f050  00007ffe`5b6dc1d0 aaarpcrt4!NdrPointerBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 1607]
00007ffe`5b72f058  00007ffe`5b6dc1d0 aaarpcrt4!NdrPointerBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 1607]
00007ffe`5b72f060  00007ffe`5b6dc1d0 aaarpcrt4!NdrPointerBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 1607]
00007ffe`5b72f068  00007ffe`5b6dc580 aaarpcrt4!NdrSimpleStructBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 1802]
00007ffe`5b72f070  00007ffe`5b6dc580 aaarpcrt4!NdrSimpleStructBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 1802]
00007ffe`5b72f078  00007ffe`5b6dcfd0 aaarpcrt4!NdrConformantStructBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 4832]
00007ffe`5b72f080  00007ffe`5b6dcfd0 aaarpcrt4!NdrConformantStructBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 4832]
00007ffe`5b72f088  00007ffe`5b6ddbb0 aaarpcrt4!NdrConformantVaryingStructBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 5050]
00007ffe`5b72f090  00007ffe`5b6de9c0 aaarpcrt4!NdrComplexStructBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 3779]
00007ffe`5b72f098  00007ffe`5b6dfbe0 aaarpcrt4!NdrConformantArrayBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 3983]
00007ffe`5b72f0a0  00007ffe`5b6e0210 aaarpcrt4!NdrConformantVaryingArrayBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 4112]
00007ffe`5b72f0a8  00007ffe`5b6df490 aaarpcrt4!NdrFixedArrayBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 5272]
00007ffe`5b72f0b0  00007ffe`5b6df490 aaarpcrt4!NdrFixedArrayBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 5272]
00007ffe`5b72f0b8  00007ffe`5b6e0bd0 aaarpcrt4!NdrVaryingArrayBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 5510]
00007ffe`5b72f0c0  00007ffe`5b6e0bd0 aaarpcrt4!NdrVaryingArrayBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 5510]
00007ffe`5b72f0c8  00007ffe`5b6e18d0 aaarpcrt4!NdrComplexArrayBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 4274]
00007ffe`5b72f0d0  00007ffe`5b6e3bc0 aaarpcrt4!NdrConformantStringBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 2491]
00007ffe`5b72f0d8  00000000`00000000
00007ffe`5b72f0e0  00000000`00000000
00007ffe`5b72f0e8  00007ffe`5b6e3bc0 aaarpcrt4!NdrConformantStringBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 2491]
00007ffe`5b72f0f0  00007ffe`5b6e4430 aaarpcrt4!NdrNonConformantStringBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 2679]
00007ffe`5b72f0f8  00000000`00000000
00007ffe`5b72f100  00000000`00000000
00007ffe`5b72f108  00000000`00000000
00007ffe`5b72f110  00007ffe`5b6e20c0 aaarpcrt4!NdrEncapsulatedUnionBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 6094]
00007ffe`5b72f118  00007ffe`5b6e2570 aaarpcrt4!NdrNonEncapsulatedUnionBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 6279]
00007ffe`5b72f120  00007ffe`5b6da8c0 aaarpcrt4!NdrByteCountPointerBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 6358]
00007ffe`5b72f128  00007ffe`5b6e28e0 aaarpcrt4!NdrXmitOrRepAsBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 6411]
00007ffe`5b72f130  00007ffe`5b6e28e0 aaarpcrt4!NdrXmitOrRepAsBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 6411]
00007ffe`5b72f138  00007ffe`5b6f2490 aaarpcrt4!NdrInterfacePointerBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_ole.c @ 376]
00007ffe`5b72f140  00007ffe`5b6da010 aaarpcrt4!NdrContextHandleBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 6964]
00007ffe`5b72f148  00000000`00000000
00007ffe`5b72f150  00000000`00000000
00007ffe`5b72f158  00000000`00000000
00007ffe`5b72f160  00007ffe`5b6e3370 aaarpcrt4!NdrUserMarshalBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 4505]
00007ffe`5b72f168  00000000`00000000
00007ffe`5b72f170  00000000`00000000
00007ffe`5b72f178  00007ffe`5b6da480 aaarpcrt4!NdrRangeBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 6565]
00007ffe`5b72f180  00007ffe`5b6d95d0 aaarpcrt4!NdrBaseTypeBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 6815]
00007ffe`5b72f188  00007ffe`5b6d95d0 aaarpcrt4!NdrBaseTypeBufferSize [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 6815]

pMemory此时就是我们的第一个参数,hello字符串,我们此时的类型值为0x22(FC_C_CSTRING),对应的sizer函数是NdrConformantStringBufferSize

计算方式其实就是strlen+1,结果存储到pStubMsg->ActualCount

  case FC_C_CSTRING:
  case FC_C_WSTRING:
    if (fc == FC_C_CSTRING)
    {
      TRACE("string=%s\n", debugstr_a((const char *)pMemory));
      pStubMsg->ActualCount = strlen((const char *)pMemory)+1;
    }

但是这个并不是最终的buffer_len,因为还要考虑对齐的问题,SizeConformance函数会进行对齐处理

static inline void SizeConformance(MIDL_STUB_MESSAGE *pStubMsg)
{
    align_length(&pStubMsg->BufferLength, 4);
    if (pStubMsg->BufferLength + 4 < pStubMsg->BufferLength)
        RpcRaiseException(RPC_X_BAD_STUB_DATA);
    pStubMsg->BufferLength += 4;
}

align_length:

*len = (*len + align - 1) & ~(align - 1);

人家这代码写的就是简洁高效,他等效于下面这个函数

if(*len%align) *len=(*len+align)-(*len+align)%align

但是明显第二种写法要进行更多次运算,而第一种写法采用位运算,要高效的多

通过位运算进行对齐操作是非常常规的写法

但是他这个对齐并不是针对ActualCount字段进行的对齐,有点奇怪哈,bufferlength字段本来就是0,对齐之后还是0,最后加了个4变成4了

后面有对bufferlen进行了一些操作,最后和actualcount加在了一起,然后这个结果也没对齐,不知道为啥,最后的长度值是0x21

然后就是第二个参数,第二个参数是传出参数,不需要计算size

NdrGetBuffer

NdrGetBuffer(stub_msg, stub_msg->BufferLength, hbinding);       

hbinding被赋值给stubmsg->RpcMsg->Handle,根据前面计算出来的bufferlen分配内存,然后调用I_RpcNegotiateTransferSyntax

但是他这个buffer分配完内存之后并没有初始化

之后就是调用I_RpcNegotiateTransferSyntax协商通信语法

该函数会调用RPCRT4_OpenBinding,该函数又会调用RpcAssoc_GetClientConnection

return RpcAssoc_GetClientConnection(Binding->Assoc, InterfaceId,
         TransferSyntax, Binding->AuthInfo, Binding->QOS, Binding->CookieAuth, Connection, from_cache);

第一个参数是RpcAssoc结构体,里面存有endpoint信息

0:000> dt RpcAssoc 000001c68bf21560
aaarpcrt4!RpcAssoc
   +0x000 entry            : list
   +0x010 refs             : 0n1
   +0x018 Protseq          : 0x000001c6`8bf21630  "ncalrpc"
   +0x020 NetworkAddr      : 0x000001c6`8bf22700  ""
   +0x028 Endpoint         : 0x000001c6`8bf20fb0  "NCALRPCServer12138"
   +0x030 NetworkOptions   : (null) 
   +0x038 assoc_group_id   : 0
   +0x03c http_uuid        : _GUID {a1df7372-ff69-43ef-94f0-fa2e73cf954c}
   +0x050 cs               : _RTL_CRITICAL_SECTION
   +0x078 free_connection_pool : list
   +0x088 connection_cnt   : 0n0
   +0x090 context_handle_list : list

RpcConnection对象应该就是用来存储即将被创建出来的连接信息的,目前他里面保存的信息是没有什么意义的

最后调用RPCRT4_CreateConnection来创建连接

首先调用rpcrt4_get_conn_protseq_ops返回对应的connection_ops对象,用于对连接进行各种操作

0:000> dt connection_ops 00007ffe5b7356f8
aaarpcrt4!connection_ops
   +0x000 name             : 0x00007ffe`5b733468  "ncalrpc"
   +0x008 epm_protocols    : [2]  "???"
   +0x010 alloc            : 0x00007ffe`5b711790     _RpcConnection*  aaarpcrt4!rpcrt4_conn_np_alloc+0
   +0x018 open_connection_client : 0x00007ffe`5b7117d0     long  aaarpcrt4!rpcrt4_ncalrpc_open+0
   +0x020 handoff          : 0x00007ffe`5b711d60     long  aaarpcrt4!rpcrt4_ncalrpc_handoff+0
   +0x028 read             : 0x00007ffe`5b711ea0     int  aaarpcrt4!rpcrt4_conn_np_read+0
   +0x030 write            : 0x00007ffe`5b712000     int  aaarpcrt4!rpcrt4_conn_np_write+0
   +0x038 close            : 0x00007ffe`5b712120     int  aaarpcrt4!rpcrt4_conn_np_close+0
   +0x040 close_read       : 0x00007ffe`5b7121f0     void  aaarpcrt4!rpcrt4_conn_np_close_read+0
   +0x048 cancel_call      : 0x00007ffe`5b712240     void  aaarpcrt4!rpcrt4_conn_np_cancel_call+0
   +0x050 is_server_listening : 0x00007ffe`5b711d20     long  aaarpcrt4!rpcrt4_ncalrpc_np_is_server_listening+0
   +0x058 wait_for_incoming_data : 0x00007ffe`5b712280     int  aaarpcrt4!rpcrt4_conn_np_wait_for_incoming_data+0
   +0x060 get_top_of_tower : 0x00007ffe`5b7131e0     unsigned int64  aaarpcrt4!rpcrt4_ncalrpc_get_top_of_tower+0
   +0x068 parse_top_of_tower : 0x00007ffe`5b7132a0     long  aaarpcrt4!rpcrt4_ncalrpc_parse_top_of_tower+0
   +0x070 receive_fragment : (null) 
   +0x078 is_authorized    : 0x00007ffe`5b7133e0     int  aaarpcrt4!rpcrt4_ncalrpc_is_authorized+0
   +0x080 authorize        : 0x00007ffe`5b7133f0     long  aaarpcrt4!rpcrt4_ncalrpc_authorize+0
   +0x088 secure_packet    : 0x00007ffe`5b713420     long  aaarpcrt4!rpcrt4_ncalrpc_secure_packet+0
   +0x090 impersonate_client : 0x00007ffe`5b7126d0     long  aaarpcrt4!rpcrt4_conn_np_impersonate_client+0
   +0x098 revert_to_self   : 0x00007ffe`5b712810     long  aaarpcrt4!rpcrt4_conn_np_revert_to_self+0
   +0x0a0 inquire_auth_client : 0x00007ffe`5b713440     long  aaarpcrt4!rpcrt4_ncalrpc_inquire_auth_client+0

这个函数就是创建一个初始化的连接对象,此时还没有真正发起连接

然后作为唯一的一个参数调用RPCRT4_OpenClientConnection函数

在该函数中会调用上面根据协议序列所引到的connection_ops对象的成员函数open_connection_client来建立连接,该函数为动态注册,实际调用的是rpcrt4_ncalrpc_open

首先根据endpoint拼接出pipename

\\.\pipe\lrpc\NCALRPCServer12138

然后调用rpcrt4_conn_open_pipe打开管道

核心其实就是

pipe = CreateFileA(pname, GENERIC_READ|GENERIC_WRITE, 0, NULL,
                       OPEN_EXISTING, dwFlags | FILE_FLAG_OVERLAPPED, 0);

启用了overlapped,表明后续的读写操作都将以异步的形式进行

dwMode = PIPE_READMODE_MESSAGE;
  SetNamedPipeHandleState(pipe, &dwMode, NULL, NULL);

这段代码至关重要,他告诉windows的npdriver管道中的数据将以消息流的形式进行传递,而非字节流,一般用于基于命名管道的RPC实现中

然后就是调用RPCRT4_BuildBindHeader构建一个bind请求头,然后使用RPCRT4_Send发送数据包

之后使用RPCRT4_ReceiveWithAuth接收来自服务端的响应数据包,实际调用的函数是RPCRT4_default_receive_fragment,这个过程和服务端是一样的,在这个函数中我们会从管道中读取数据,解析头部,解析数据

call_marshaller

参数的序列化,和前面的buffersizer一样,marshaller也是一个全局数组,根据pformat选定不懂的marshaller函数

在我们一开始的NdrClientCall2的第二个参数中指定了参数类型相对于pFormat指针的偏移量,使用这个偏移量计算出一个地址,取出来bytes值作为marshaller数组的索引来得到对应的marshaller函数

image-20250925113143799

image-20250925113057774

我们szString对应的marshaller就是NdrConformantStringMarshall

那我们来看一下他是如何序列化我们的字符串"Hello Context World!"

有点像服务端的sizer函数

array_compute_and_write_conformance(FC_C_CSTRING, pStubMsg, pszMessage,
                                      pFormat, TRUE /* fHasPointers */);

array_compute_and_write_conformance函数上来就是switch-case,根据a1确定类型,对于FC_C_CSTRING类型

pStubMsg->ActualCount = strlen((const char *)pMemory)+1;

使用字符串长度+1来填充stubMsg的actualCount字段

我们上面得到的类型指针,紧跟着的下一个字节也是有特定的意义的

image-20250925133042788

此处我们是0x5C,FC_PAD,无需做特殊处理,stubMsg的MaxCount字段直接就设置为ActualCount字段的值

之后调用WriteConformance(pStubMsg);

static inline void WriteConformance(MIDL_STUB_MESSAGE *pStubMsg)
{
    align_pointer_clear(&pStubMsg->Buffer, 4);
    if (pStubMsg->Buffer + 4 > (unsigned char *)pStubMsg->RpcMsg->Buffer + pStubMsg->BufferLength)
        RpcRaiseException(RPC_X_BAD_STUB_DATA);
    NDR_LOCAL_UINT32_WRITE(pStubMsg->Buffer, pStubMsg->MaxCount);
    pStubMsg->Buffer += 4;
}

对该函数的代码进行理解

地址对齐

static inline void align_pointer_clear( unsigned char **ptr, unsigned int align )
{
    ULONG_PTR mask = align - 1;
    memset( *ptr, 0, (align - (ULONG_PTR)*ptr) & mask );
    *ptr = (unsigned char *)(((ULONG_PTR)*ptr + mask) & ~mask);
}

我直接让chatgpt解释了一下这段代码的作用,说白了这个函数就是把指针向前移动到alignment的倍数的位置,被跳过的地址将会被清0

比如说现在buffer的地址是0x1002,对齐为4,那么经过这个函数的处理,buffer地址会变成0x1004,0x1002\0x1003这两个地址会被清空为0

pStubMsg->BufferpStubMsg->RpcMsg->Buffer是同一个东西

NDR_LOCAL_UINT32_WRITE就是以小端序列写入一个UINT32数据,这里是把MaxCount字段写入了buffer中

所谓小端就是低位在前

image-20250925142304623

写入了UINT32,即一个DWORD,buffer要向前移动4

然后是array_write_variance_and_marshall,上面的array_write_variance_and_marshall只写入了一个MaxCount,并未写入我们的hello字符串

首先调用WriteVariance(pStubMsg);

这个函数中的单词Variance并非其字面意义(统计学上的方差),而是NDR标准中的一个术语,特指一个数组或者字符串中的可变部分,即偏移量或者长度这种字段,因此从函数名中我们就可以知道,这个函数大概率是用来写入我的hello字符串长度的

这时再来看这个函数的内容

static inline void WriteVariance(MIDL_STUB_MESSAGE *pStubMsg)
{
    align_pointer_clear(&pStubMsg->Buffer, 4);
    if (pStubMsg->Buffer + 8 > (unsigned char *)pStubMsg->RpcMsg->Buffer + pStubMsg->BufferLength)
        RpcRaiseException(RPC_X_BAD_STUB_DATA);
    NDR_LOCAL_UINT32_WRITE(pStubMsg->Buffer, pStubMsg->Offset);
    pStubMsg->Buffer += 4;
    NDR_LOCAL_UINT32_WRITE(pStubMsg->Buffer, pStubMsg->ActualCount);
    pStubMsg->Buffer += 4;
}

他写入了hello字符串的在buffer中的offset和字符串的长度+1,这两个字段共占用8bytes

然后调用safe_multiply(esize, pStubMsg->ActualCount);,其中esize变量根据我们的参数类型决定,宽字节(FC_C_WSTRING)是2,ascii字符串(FC_C_CSTRING)就是1

之后调用safe_copy_to_buffer进行hello字符串到stubMsg的拷贝操作

memcpy(pStubMsg->Buffer, p, size);

这就是到目前为止pstubmsg的buffer的内容

0:000> db 0001d4`586beac1-15-8-4
000001d4`586beaa0  15 00 00 00 00 00 00 00-15 00 00 00 48 65 6c 6c  ............Hell
000001d4`586beab0  6f 20 43 6f 6e 74 65 78-74 20 57 6f 72 6c 64 21  o Context World!

字符串长度+1(DWORD)

字符串相body部分的偏移(DWORD)

字符串长度+1(DWORD)

字符串内容

处理context handle参数

这个参数和上面的字符串不一样,在我们调用NdrClientCall的时候我们传进来的是一个地址,用于接收服务端给我们分配的contexthandle,也就是说这个参数本质上是一个传出参数

从实际调试结果来看,并未作任何处理

NdrSendReceive

好,现在客户端把Open函数转换成数据包并通过命名管道发送给了服务端,那么我们现在就回去调试服务端的代码

现在来看一下客户端的context_handle和guid是如何关联的,在客户端使用contexthandle时,最终会到达NDRCContextMarshall函数,该函数的第一个参数就是我们在一开始的open函数中获取到的handle_context传出参数,他其实是一个context_handle_entry类型的结构体,这一点在函数

get_context_entry可以得到验证

image-20250926112906176

现在我要看一下这个GUID是如何存储到我们的context handle里面去的,context handle写入调用栈

0:000> k
 # Child-SP          RetAddr               Call Site
00 000000e9`bbafddc0 00007ffc`5a795939     aaarpcrt4!ndr_update_context_handle+0x14e [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_contexthandle.c @ 200] 
01 000000e9`bbafde10 00007ffc`5a7a9e1b     aaarpcrt4!NDRCContextUnmarshall+0x69 [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_contexthandle.c @ 216] 
02 000000e9`bbafde60 00007ffc`5a79a2ea     aaarpcrt4!NdrClientContextUnmarshall+0x8b [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 7098] 
03 000000e9`bbafde90 00007ffc`5a7b55b0     aaarpcrt4!NdrContextHandleUnmarshall+0x1ba [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_marshall.c @ 7040] 
04 000000e9`bbafdef0 00007ffc`5a7b5a2c     aaarpcrt4!call_unmarshaller+0xf0 [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_stubless.c @ 126] 
05 000000e9`bbafdf50 00007ffc`5a7b800d     aaarpcrt4!client_do_args+0x3ac [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_stubless.c @ 531] 
06 000000e9`bbafdfc0 00007ffc`5a7b89ae     aaarpcrt4!do_ndr_client_call+0x7ad [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_stubless.c @ 842] 
07 000000e9`bbafe8d0 00007ffc`5a7e94ff     aaarpcrt4!ndr_client_call+0x71e [c:\users\x\downloads\reactos-master\dll\win32\rpcrt4\ndr_stubless.c @ 994] 
08 000000e9`bbaff150 00007ff7`e9c125f0     aaarpcrt4!NdrClientCall2+0x1b [C:\Users\x\Downloads\reactos-master\output-VS-amd64-sln\dll\win32\rpcrt4\asm\msvc_rpcrt4_asm.asm @ 602] 
09 000000e9`bbaff180 00007ff7`e9c121bb     ncalrpcClt!Open+0x80 [c:\users\x\downloads\reactos_rpcrt4_test_code\ncalrpcclt\ncalrpcinterface_clntstub.c @ 164] 
0a 000000e9`bbaff290 00007ff7`e9c131a4     ncalrpcClt!wmain+0x29b [c:\users\x\downloads\reactos_rpcrt4_test_code\ncalrpcclt\ncalrpcclt.c @ 159] 
0b 000000e9`bbaff850 00007ff7`e9c1304e     ncalrpcClt!invoke_main+0x34 [d:\agent\_work\2\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 91] 
0c 000000e9`bbaff890 00007ff7`e9c12f0e     ncalrpcClt!__scrt_common_main_seh+0x12e [d:\agent\_work\2\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288] 
0d 000000e9`bbaff900 00007ff7`e9c13239     ncalrpcClt!__scrt_common_main+0xe [d:\agent\_work\2\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 331] 
0e 000000e9`bbaff930 00007ffc`96b47374     ncalrpcClt!wmainCRTStartup+0x9 [d:\agent\_work\2\s\src\vctools\crt\vcstartup\src\startup\exe_wmain.cpp @ 17] 
0f 000000e9`bbaff960 00007ffc`9867cc91     KERNEL32!BaseThreadInitThunk+0x14
10 000000e9`bbaff990 00000000`00000000     ntdll!RtlUserThreadStart+0x21

根据调用栈回溯可知,context handle里面的wire data的guid来自于prcmsg->buffer,就是server端分配好之后给我们发送过来的

逆向tips

procnum

客户端发起调用的时候最终肯定会调用NdrClientCall,该函数的第二个参数是一个地址,这个地址+6取word就是procnum

其实这个参数可以解释为NDR_PROC_HEADER结构体

aaarpcrt4!_NDR_PROC_HEADER_RPC
   +0x000 handle_type      : UChar
   +0x001 Oi_flags         : UChar
   +0x002 rpc_flags        : Uint4B
   +0x006 proc_num         : Uint2B
   +0x008 stack_size       : Uint2B

image-20250926144826803

总结

看rpcrt4的源码,我就只想弄清楚一件事,就是服务端是如何识别context handle的,得出的结论如下:

客户端发送context handle创建请求,服务端分配一个用于标识context handle的guid返回给客户端,客户端将这个guid存储到context handle中
下一次使用context handle发起请求时,会将context handle中存储的guid发送过去,服务端根据guid定位到相应客户端的context handle

另外一点需要提的就是,使用explicit binding handle是没有办法在server端标识client的,他不像context handle一样,客户端和服务端共享一个guid标识符,binding handle对服务端来说只是用于标识一个连接,至于这个连接是哪一个客户端在使用,他是不清楚的,因为server端的hbinding和客户端的hbinding之间不存在映射关系

非要说的话那也只能是在RPCRT4_MakeBinding函数中通过对比客户端发送过来的数据包来确认当前产生的binding是否为目标客户端的,但是这个数据包对比实战的时候不太现实