14 min to read
RpcView 原理详解

本文将详细介绍 RpcView 的原理,RpcView 是一个可以查看 Windows 环境下 remote procedure call(RPC)接口和参数的工具,个人使用之后感觉很神奇,就感觉能知道 windows 底层的很多接口信息,但是不知道怎么实现的,网上搜了一大波也没看到相应的原理介绍,反而,很多题目是讲 RpcView 原理和使用的文章却是一眼 AI = =,于是决定自己分析和写一下 ~
基本情况
RpcView 原始仓库在 这里,配置可以参考 这篇博客,但是因为我配环境的机子的网不是自己可控的,然后遇到了 Qt 连接不上服务器的问题,所以就找到了 这个改进后的仓库,它的 artifacts 直接可用(我安装的系统版本为 windows server 2022,可供参考)。以及为啥这个仓库才 3 stars,可惜了(就像为啥我的博客仓库才 2 stars,也可惜了,呜呜)
Microsoft 官方文档讲 Windows 上 rpc 实现和一些 coding 规范见这里
简单介绍一下 RPC(remote procedure call)是一种进程间通信的方法,允许本地进程和比如说共享计算机网络的另一台机子上的进程通信,既然是通过网络通信,就肯定和网络协议相关,RPC 的协议是 request-response 模式的,客户端发送请求到服务器,服务器处理请求并返回响应
放一个运行截图,具体使用方法很多博客有讲,我们还是推荐 itm4n 师傅的这篇,不仅介绍了各个部分怎么使用,而且还说了怎么使用它来写 Rpc client,感觉非常实用!
源码分析
进程
首先我们看到 UI 上的被红色框住的这些部分,RpcView 具有能力来找出使用 Rpc 接口的进程,并且给出对应的进程信息,比如程序路径,运行使用的命令等,我们先看这些进程是怎么被 RpcView 找到的
主要的函数是 RpcCoreGetProcessInfo
,因为函数很短,所以还是粘一下源码
// https://github.com/silverf0x/RpcView/blob/14d5e1a3b6cc02196dabdcf668ea341129b36be0/RpcCore/RpcCore.c#L387
RpcProcessInfo_T* __fastcall RpcCoreGetProcessInfo(void* pRpcCoreCtxt,DWORD Pid,DWORD Ppid,ULONG ProcessInfoMask)
{
SHFILEINFOW ShFileInfo;
HANDLE hProcess;
VOID PTR_T pRpcServer;
RPC_SERVER_T RpcServer;
RpcProcessInfo_T* pRpcProcessInfo=NULL;
RpcCoreInternalCtxt_T* pRpcCoreInternalCtxt=(RpcCoreInternalCtxt_T*)pRpcCoreCtxt;
pRpcProcessInfo=(RpcProcessInfo_T*)OS_ALLOC(sizeof(RpcProcessInfo_T));
if (pRpcProcessInfo==NULL) return (NULL);
//
// Process minimal info
//
pRpcProcessInfo->Pid = Pid;
pRpcProcessInfo->ParentPid = Ppid;
pRpcProcessInfo->RpcProcessType = RpcProcessType_UNKNOWN;
hProcess=ProcexpOpenProcess(PROCESS_VM_READ|PROCESS_QUERY_INFORMATION,FALSE,Pid);
if (hProcess!=NULL)
{
#ifdef _WIN64
IsWow64Process(hProcess, &pRpcProcessInfo->bIsWow64);
#endif
}
//
// Process general information
//
if (ProcessInfoMask & RPC_PROCESS_INFO_MISC)
{
GetProcessNameFromPid(Pid,pRpcProcessInfo->Name,sizeof(pRpcProcessInfo->Name));
GetProcessPath(Pid,pRpcProcessInfo->Path,sizeof(pRpcProcessInfo->Path));
pRpcProcessInfo->Version=GetModuleVersion(pRpcProcessInfo->Path);
GetModuleDescription(pRpcProcessInfo->Path,pRpcProcessInfo->Description,sizeof(pRpcProcessInfo->Description));
GetUserAndDomainName(pRpcProcessInfo->Pid,pRpcProcessInfo->User,sizeof(pRpcProcessInfo->User));
GetProcessPebInfo(hProcess,pRpcProcessInfo->CmdLine,sizeof(pRpcProcessInfo->CmdLine),pRpcProcessInfo->Desktop,sizeof(pRpcProcessInfo->Desktop));
// Get icon
if (hProcess!=NULL)
{
ZeroMemory(&ShFileInfo,sizeof(ShFileInfo));
if (SHGetFileInfoW(pRpcProcessInfo->Path,0,&ShFileInfo,sizeof(ShFileInfo),SHGFI_ICON|SHGFI_LARGEICON))
{
pRpcProcessInfo->hIcon=ShFileInfo.hIcon;
}
}
}
//
// Process RPC information
//
if (ProcessInfoMask & RPC_PROCESS_INFO_RPC)
{
if (pRpcCoreInternalCtxt->pGlobalRpcServer==NULL)
{
GetRpcServerAddressInProcess(pRpcProcessInfo->Pid,pRpcCoreInternalCtxt);
if (pRpcCoreInternalCtxt->pGlobalRpcServer==NULL) goto End;
}
if (!ReadProcessMemory(hProcess,pRpcCoreInternalCtxt->pGlobalRpcServer,&pRpcServer,sizeof(VOID PTR_T),NULL)) goto End;
if (!ReadProcessMemory(hProcess,pRpcServer,&RpcServer,sizeof(RpcServer),NULL)) goto End;
//If the number of endpoints is correct we have a RPC server
if (RpcServer.AddressDict.NumberOfEntries!=0)
{
pRpcProcessInfo->RpcProcessType = GetProcessType(hProcess,&RpcServer);
pRpcProcessInfo->bIsServer = TRUE;
pRpcProcessInfo->EndpointsCount = RpcServer.AddressDict.NumberOfEntries;
pRpcProcessInfo->SspCount = RpcServer.AuthenInfoDict.NumberOfEntries;
pRpcProcessInfo->InterfacesCount = RpcServer.InterfaceDict.NumberOfEntries;
pRpcProcessInfo->InCalls = RpcServer.InCalls;
pRpcProcessInfo->OutCalls = RpcServer.OutCalls;
pRpcProcessInfo->InPackets = RpcServer.InPackets;
pRpcProcessInfo->OutPackets = RpcServer.OutPackets;
pRpcProcessInfo->bIsListening = RpcServer.bIsListening;
pRpcProcessInfo->MaxCalls = RpcServer.MaxCalls;
}
}
End:
if (hProcess!=NULL) CloseHandle(hProcess);
return (pRpcProcessInfo);
}
提取进程
提取进程名:先调用 CreateToolhelp32Snapshot 函数来对当前进程使用的堆、模块、线程等信息进行快照,然后使用 Process32First 和 Process32Next 函数来遍历快照中的进程信息,获取每个进程的 PID、名称和路径等信息,如果进程的 PID 匹配,则提取该进程对应的 szExeFile
提取进程路径:先调用 ProcexpOpenProcess
拿进程句柄,然后调用 QueryFullProcessImageNameW 函数获取进程的完整路径
提取用户名:先获取进程句柄和 token,然后调用 GetTokenInformation 函数获取用户名(同时可以拿导用户的 sid),然后用 LookupAccountSidW 函数将 SID 转换为用户名和域名
提取命令行:使用 NtQueryInformationProcess
函数来拿到对应 Peb 的基地址,Peb 是进程环境块信息,包含了进程运行时的详细参数等,然后用 ReadProcessMemory
读取内存,读出 Peb,最后从 Peb 中提取命令行参数
提取进程 RPC 使用情况
判断进程是否使用 RPC 接口:还是贴一个 GetRpcServerAddressInProcess
函数源码,整体思路大概就是:我们先遍历所有模块,看 rpcrt4.dll
模块
// https://github.com/silverf0x/RpcView/blob/master/RpcCore/RpcCore.c#L210
BOOL WINAPI GetRpcServerAddressInProcess(DWORD Pid,RpcCoreInternalCtxt_T* pRpcCoreInternalCtxt)
{
VOID PTR_T PTR_T pCandidate;
VOID PTR_T pRpcServer;
RPC_SERVER_T RpcServer;
ModuleSectionInfo_T ModuleSectionInfo;
HANDLE hProcess = NULL;
GetRpcServerAddressCallbackCtxt_T GetRpcServerAddressCallbackCtxt;
DWORD cbSize;
HMODULE* pHmodule = NULL;
CHAR ModuleFileName[MAX_PATH];
BOOL bResult=FALSE;
hProcess = ProcexpOpenProcess(PROCESS_ALL_ACCESS, FALSE, Pid);
if (hProcess == NULL) goto End;
if (!EnumProcessModulesEx(hProcess, NULL, 0, &cbSize, LIST_MODULES_ALL)) goto End;
pHmodule = (HMODULE*)malloc(cbSize);
if (pHmodule == NULL) goto End;
if (!EnumProcessModulesEx(hProcess, pHmodule, cbSize, &cbSize, LIST_MODULES_ALL)) goto End;
for(ULONG i=0;i<cbSize/sizeof(*pHmodule);i++)
{
ModuleFileName[0] = 0;
GetModuleFileNameExA(hProcess, pHmodule[i], ModuleFileName, sizeof(ModuleFileName));
if (strstr(ModuleFileName,"RPCRT4.dll")==NULL) goto NextModule;
if (!GetModuleDataSection(hProcess, pHmodule[i], &ModuleSectionInfo)) goto End;
#pragma warning(push)
#pragma warning(disable:4305)
pCandidate=(VOID PTR_T PTR_T)ModuleSectionInfo.pBase;
#pragma warning(pop)
GetRpcServerAddressCallbackCtxt.bFound=FALSE;
for (i=0; i<ModuleSectionInfo.Size; i+=sizeof(VOID PTR_T))
{
if (!ReadProcessMemory(hProcess,pCandidate,&pRpcServer,sizeof(VOID PTR_T),NULL)) goto NextCandidate;
if (!ReadProcessMemory(hProcess,pRpcServer,&RpcServer,sizeof(RpcServer),NULL)) goto NextCandidate;
GetRpcServerAddressCallbackCtxt.pRpcServer = pRpcServer;
if (!EnumSimpleDict(hProcess,&RpcServer.InterfaceDict,&GetRpcServerAddressCallback,&GetRpcServerAddressCallbackCtxt)) goto End;
if (GetRpcServerAddressCallbackCtxt.bFound==TRUE)
{
_cprintf("gRpcServer localized at address %p in process %u\n",pCandidate,Pid);
pRpcCoreInternalCtxt->pGlobalRpcServer=pCandidate;
bResult=TRUE;
break;
}
NextCandidate:
pCandidate++;
}
CloseHandle(hProcess);
hProcess=NULL;
goto End;
NextModule: ;
}
End:
if (hProcess!=NULL) CloseHandle(hProcess);
if (pHmodule !=NULL) free(pHmodule);
return (bResult);
}
这个函数的主要思路是:先拿到进程句柄,然后枚举所有模块,找到 rpcrt4.dll
模块,接着获取该模块的 sections,读取名为 gRpcCoreDataSectionName
的 section 处的 base address 和 size, 然后从 base address 开始,逐个读取 8 字节内存当成一个指向 gRPCServer 的地址,如果满足 接口 UUID 和 pRpcCoreInternalCtxt
的 UUID 一致,且 RpcServer 地址和 pRpcCoreInternalCtxt
的 RpcServer 地址一致,则说明该进程使用了 RPC 接口就返回 true,否则返回 false
更上一层,不出意料的,该项目在枚举进程/Endpoints 的时候都采用了 Visitor 模式,在多个 Visitor 组件中和 EnumProc
函数中,都会调用 GetRpcServerAddressInProcess
函数来获取进程的 RPC 使用情况
Endpoints
Rpc Endpoints 是程序将其 RPC 请求发送到以访问服务器数据的网络位置,还是看 itm4n 师傅的这篇,感觉就是一个可以发送请求到的网络设备,以 ioctl routine 类比的话,我们发 IOCTL 的流程是先打开一个设备路径获得句柄,再向该句柄发送 IOCTL 请求,RPC 的流程也是向该 Endpoint 发送请求
对每个进程枚举终端的函数为 RpcCoreEnumProcessEndpoints
,还是贴一个代码
// https://github.com/silverf0x/RpcView/blob/14d5e1a3b6cc02196dabdcf668ea341129b36be0/RpcView/RpcCoreManager.c#L254
BOOL __fastcall RpcCoreEnumProcessEndpoints(void* pRpcCoreCtxt,DWORD Pid,RpcCoreEnumProcessEndpointsCallbackFn_T RpcCoreEnumProcessEndpointsCallbackFn,void* pCallbackCtxt)
{
HANDLE hProcess;
BOOL bResult=FALSE;
RPC_SERVER_T RpcServer;
UINT i;
UINT Size;
VOID PTR_T * pTable=NULL;
VOID PTR_T pRpcServer;
RPC_ADDRESS_T RpcAddress;
WCHAR ProtocoleW[RPC_MAX_ENDPOINT_PROTOCOL_SIZE];
WCHAR NameW[RPC_MAX_ENDPOINT_NAME_SIZE];
RpcEndpointInfo_T RpcEndpointInfo;
BOOL bContinue=TRUE;
RpcCoreInternalCtxt_T* pRpcCoreInternalCtxt=(RpcCoreInternalCtxt_T*)pRpcCoreCtxt;
hProcess=ProcexpOpenProcess(PROCESS_VM_READ,FALSE,Pid);
if (hProcess==NULL) goto End;
if (!ReadProcessMemory(hProcess,pRpcCoreInternalCtxt->pGlobalRpcServer,&pRpcServer,sizeof(VOID PTR_T),NULL)) goto End;
if (!ReadProcessMemory(hProcess,pRpcServer,&RpcServer,sizeof(RpcServer),NULL)) goto End;
if (RpcServer.AddressDict.NumberOfEntries > MAX_SIMPLE_DICT_ENTRIES)
{
goto End;
}
Size=RpcServer.AddressDict.NumberOfEntries*sizeof(VOID PTR_T);
pTable=(VOID PTR_T *)OS_ALLOC(Size);
if (pTable==NULL) goto End;
if (!ReadProcessMemory(hProcess,RpcServer.AddressDict.pArray,pTable,Size,NULL)) goto End;
for (i=0; i<RpcServer.AddressDict.NumberOfEntries; i++)
{
if (!ReadProcessMemory(hProcess,pTable[i],&RpcAddress,sizeof(RpcAddress),NULL)) goto End;
if (!ReadProcessMemory(hProcess,RpcAddress.Protocole,ProtocoleW,sizeof(ProtocoleW),NULL)) goto End;
if (!ReadProcessMemory(hProcess,RpcAddress.Name,NameW,sizeof(NameW),NULL)) goto End;
RpcEndpointInfo.pName = NameW;
RpcEndpointInfo.pProtocole = ProtocoleW;
bResult=RpcCoreEnumProcessEndpointsCallbackFn(Pid,&RpcEndpointInfo,pCallbackCtxt,&bContinue);
if (!bResult) goto End;
if (!bContinue) break;
}
bResult=TRUE;
End:
if (hProcess!=NULL) CloseHandle(hProcess);
if (pTable!=NULL) OS_FREE(pTable);
return (bResult);
}
嗯 首先那个 RpcCoreEnumProcessEndpointsCallbackFn
,看了一下该函数所有的调用点,发现都是调用的 EnumEndpoints
函数,该函数比较有意思,名字和它实际功能不符,本质还是调用 AddEndpoint 函数,把该 Endpoint 添加到需要显示的 Endpoints 列表中
所以该函数作用是,枚举所有的 AddressDict
中的 Entry, 这个 AddressDict
看之前 RpcCoreGetProcessInfo
函数,感觉作用是存所有 Endpoint 的信息,比如协议和名字之类的,然后对于每一个 Endpoint,调用 RpcCoreEnumProcessEndpointsCallbackFn
回调函数,用于在 UI 上显示
Interfaces
感觉就是一个 Interface 对应于一种通过 Rpc 可以提供的服务,它是一类可以被远程调用的方法和函数的集合,每个接口有独立的 Uuid 作为标识
我们接下来来看这些 interfaces 的信息是怎么被 RpcView 获取的,相关的函数比较长,就先不在这里贴了
主要的函数是 RpcCoreEnumProcessInterfaces
和 RpcCoreGetInterfaceInfo
我们先看 RpcCoreGetInterfaceInfo
函数,该函数签名如下
RpcInterfaceInfo_T* __fastcall RpcCoreGetInterfaceInfo(void* pRpcCoreCtxt, DWORD Pid, RPC_IF_ID* pIf, ULONG InterfaceInfoMask);
第一步是根据 Interface 的 Uuid 获取对应信息,见 GetProcessInterface
函数,这个和之前 RpcCoreEnumProcessEndpoints
函数比较像,都是在进程的 RpcServer
结构体中获取 InterfaceDict
, 依次遍历该字典中的每个 Entry,并且比较每个 Entry 的 Uuid 是否和我们的传参相同,如果相同,则读取该 Entry 的信息,以指向它的指针的形式返回
在图中显示的信息中,Type
, Syntax
, EpMapper
这几个是我们可以直接从该 RpcInterfaceInfo_T
结构体读出来的
对于 Interface 的 name
,则是以下逻辑:先在注册表 HKEY_CLASSES_ROOT\Interface{UUID} 下查找默认值,如果可以找到,则说明这是一个已注册的COM接口,直接返回注册表中的名称,如果查找失败,则去本地配置文件中查询
而对于 Base
和 Location
,则需要我们从 DispatchTable
中获取模块信息,模块是指的实现该接口功能的二进制文件,如动态链接库之类的,首先我们尝试对模块打快照,然后用 Module32FirstW
函数找到该模块的大小和基地址,如果打快照失败,就从 pRpcInterfaceInfo->pLocationBase
往前每 4k 字节读取一次,看看是否是 PE header 的魔数,如果是的话,就读取PE头获取模块大小
拿到具体模块的地址之后,我们也就不难通过 GetMappedFileNameW
和 QueryDosDeviceW
来获取模块的名称和路径了
对于 Version
和 Flags
,我们从目标进程读取 MIDL_SERVER_INFO
结构,然后从中读取 MIDL_STUB_DESC,再从中读取 Version
和 Flags
字段
这个 MIDL_STUB_DESC
个人感觉是 “MIDL stub description” 的缩写,还是先解释一下 Stub 是啥吧,Stub 是一个中间件,它在客户端和服务器之间起到桥梁的作用,负责将客户端的请求转换为服务器可以理解的格式,并将服务器的响应转换为客户端可以理解的格式,通俗来说,就是负责解包和反序列化之类的一个组件
最后就是 Description
了,这个是通过 GetFileVersionInfoSizeW
函数先是判断能否获取到 FileVersion,然后通过 GetFileVersionInfoW
函数获取到版本信息,再通过以下这段代码,对于每个语言,查询 “FileDescription” 的 version-information value,查到了就记录下来然后返回,因为界面上的 Description 和系统使用的语言是一致的,所以猜测这种写法是为了保证该 Description 是对应于当前系统语言的 common practice
所以为什么要调用 GetFileVersionInfoSizeW
和 GetFileVersionInfoW
,也是 common practice 笑死,在 VerQueryValueW 微软文档 中,说了 “To retrieve the appropriate resource, before you call VerQueryValue, you must first call the GetFileVersionInfoSize function, and then the GetFileVersionInfo function”
if (!VerQueryValueW(pData, L"\\VarFileInfo\\Translation", (LPVOID*)&lpTranslate,&cbTranslate)) goto End;
//
// Read the file description for each language and code page.
//
for(i=0; i < (cbTranslate/sizeof(LanguageCodePage_T)); i++)
{
StringCbPrintfW(SubBlock,sizeof(SubBlock),L"\\StringFileInfo\\%04x%04x\\FileDescription",lpTranslate[i].wLanguage,lpTranslate[i].wCodePage);
//
// Retrieve file description for language and code page "i".
//
if (VerQueryValueW(pData,SubBlock,(LPVOID*)&lpBuffer,&Size))
{
StringCbPrintfW(pDescription,Bytes,L"%s",lpBuffer);
break;
}
}
Decompilation
最后一部分,是我们对于每个 active 的 interface,可以右键反编译其中的接口,大概输出格式如下
[
uuid(0497b57d-2e66-424f-a0c6-157cd5d41700),
version(1.0),
]
interface DefaultIfName
{
typedef struct Struct_14_t
{
long StructMember0;
long StructMember1;
}Struct_14_t;
// ....
long Proc0(
[in][unique][string] wchar_t* arg_2,
[in][unique][string] wchar_t* arg_3,
[in]long arg_4,
[in]long arg_5,
[in][string] wchar_t* arg_6,
[in][string] wchar_t* arg_7,
[in]struct Struct_22_t* arg_8,
[in][string] wchar_t* arg_9,
[in][string] wchar_t* arg_10,
[in]unsigned __int3264 arg_11,
[in]long arg_12,
[in]long arg_13,
[in]/* FC_SYSTEM_HANDLE */ hyper arg_14,
[in]hyper arg_15,
[in][unique]hyper *arg_16,
[in][unique]struct Struct_84_t* arg_17,
[in]hyper arg_18,
[out]struct Struct_56_t* arg_19);
long Proc1(
[in][string] wchar_t* arg_2,
[in]/* FC_SYSTEM_HANDLE */ hyper arg_3,
[out]struct Struct_124_t* arg_4);
long Proc2(
[in][string] wchar_t* arg_2,
[in]/* FC_SYSTEM_HANDLE */ hyper arg_3,
[out]struct Struct_124_t* arg_4);
// ....
}
总体来说,可以拆分为3个 level: interface, function, type(也就是结构体), 下面会针对每个 level 进行分析
Interface
对应于上面输出的以下形式
interface DefaultIfName{
// structure types
// function declarations
}
主要函数是 IdlInterface::decode
,它会针对每个函数,分别调用它的 decode
方法来获取函数签名,然后再对结构体进行 Decode
Function
主要函数是 IdlFunction::decode
,它是分为3个部分:找函数名,从 proc header 里面拿信息,和从参数中拿信息
函数名的寻找,一路跟踪下来其实是在分析 windows binary 中很常见的方法:从模块的 pdb 中拿信息,这里也是一样的
然后是 proc header,它在这一块发挥的作用主要是拿函数的参数个数和第一个参数的位置,Windows 文档上有写 Oif header
里面有 NumberOfParams
字段,表示函数的参数个数和第一个参数位置,然后对每个参数,依次调用 IdlType
的 decode
方法来获取每个参数的类型信息就可以了,这也并不复杂
Type
主要函数是 IdlType::decode
就像是 rpcrt4.dll 做的那样,它也是先 decode 简单类型,再依次将复杂的结构体,如指针,数组,结构体等,按照 FCType
来 switch-case 然后对于每一个类型,分别进行处理
总结
首先,感觉就是 RpcView 整体的实现看得出来对 Windows 用户态开发的很多接口都相当熟悉,而且很多都是 common practice,看起来还是很震撼的
然后这个项目也让我想到了 Device Tree,这个是用来看驱动和设备的对应关系,以及设备的一些信息,比如路径之类的,感觉作者都是对于 Windows 底层的一些实现非常了解,读来尤为震撼
以及受限于篇幅和作者的时间和精力,源码中一些部分未被详细分析,但是感觉都是读代码,搜一搜可以找得到的原理,如果有兴趣,可以在评论区给我 assign 任务,或者一起分析
Comments