准备好了吗?如果是这样,那么拿起你的 IDA 或 Ghidra 和一杯咖啡,让我们开始吧!
根据 Microsoft MSDN 官方文档,LogonUser 函数尝试将用户登录到本地计算机,并返回表示登录用户的令牌的句柄。函数声明是(注意匈牙利语表示法):
BOOL LogonUserA(
[in] LPCSTR lpszUsername,
[in, optional] LPCSTR lpszDomain,
[in, optional] LPCSTR lpszPassword,
[in] DWORD dwLogonType,
[in] DWORD dwLogonProvider,
[out] PHANDLE phToken
);
从参数中,我们可以假设,如果我们提供有效的凭据,我们将收到一个有效的令牌句柄作为回报。这就是 LogonUserA 的全部目的,红队成员可以使用令牌句柄来模拟指定的用户。
相关视频教程
恶意软件开发(更新到了125节)
LogonUserA MSDN 文档中的“要求”部分指出,动态链接库 (DLL) 导出该函数。首先,让我们在IDA中打开它。步骤如下:Advapi32.dll
打开 IDA,单击“新建”,然后双击位于 下的 DLL。Advapi32.dll
C:\windows\system32\Advapi32.dll
选择 后,IDA 将要求为 DLL 提供其他加载选项。Advapi32.dll
它有一个 IDA 反编译器窗口和图形模式窗口并排,如下所示:
另一个工具提示是将反编译器窗口与图形窗口“同步”。为此,您可以右键单击图形窗口,将鼠标悬停在“同步”按钮上,然后单击表示反编译器窗口的名称(在本例中为 )。现在,当您单击代码的任何一行时,两个窗口都将跳转到相应的行。Pseudocode-A
好。让我们回到 !当您在将 LogonUserA 加载到 IDA 时单击“是”时,IDA 将尝试从 Microsoft 服务器下载公共符号。在这篇文章中,我已经下载了一个文件,这就是为什么 IDA 为我填充了函数的名称及其参数。LogonUserA
从一开始,我们可以看到该函数充当 的“包装器函数”。值得注意的是,IDA 还在堆栈上添加了四个“零”,因为其参数指示可能需要比 .LogonUserA
LogonUserCommonA
LogonUserCommonA
LogonUserCommonA
LogonUserA
双击该函数后,IDA 将在我们的两个窗口中显示反编译代码。(IDA双击)LogonUserCommonA
LogonUserCommonA
双击后,将首先调用并提供的参数,例如“用户名、域和密码”。这些调用只是将美国国家标准协会 (ANSI) 编码的字符串参数转换为UNICODE_STRING类型,这是 Windows 接受的字符串参数。Microsoft很好地记录了这背后的原因。LogonUserCommonA
RtlInitAnsiString
RtlAnsiStringToUnicodeString
将我们提供的用户名、域和密码转换为UNICODE_STRINGs后,将继续调用LogonUserCommonA
LogonUserCommonW
在函数内部,我们可以看到它对 进行了另一个函数调用。LogonUserCommonW
LogonUserEXEXW
双击 ,图形窗口将跳转到 (tl;dr, .idata 部分存储有关二进制文件的可移植可执行文件的导入目录信息,我们可以向上滚动以查看 advapi32 从哪个 DLL 导入。LogonUserEXEXW
.idata
LogonUserEXEXW
SspiCli.dll
在这种情况下,我们需要为 IDA 打开一个新的 IDA 实例,如果我们允许 IDA 下载公共符号,IDA 将很乐意填充函数名称和参数。SspiCli.dll
要使用搜索功能,请按并输入功能名称窗口。Ctrl + F
LogonUserEXEXW
接下来,单击 .然后,IDA将跳转到我们两个窗口中的功能(您可以在下面看到设置)。LogonUserEXEXW
浏览函数体,函数的第一部分似乎用于验证 and 参数。IDA 很感激地将代码反编译为一个开关盒,以便我们可以更好地看到它。logonType
logonProvider
这里的问题是,“Microsoft创造了什么?”以及“它们之间有什么区别?”logonType
logOnProviders
如果我们回过头来重新访问 的函数原型,我们可以在页面底部看到 和 的描述。然而,我们在IDA中看到的数字所代表的每个名称并不是很简单。LogonUserA
dwLogonType
dwLogonProvider
我的解决方案是使用 Visual Studio 或更确切地说是 Windows SDK 头文件找到它。我们可以找到 (e.g.) 下指定的名称之一,并将其复制粘贴到包含头文件 () 的测试 Visual Studio 项目中。dwLogonType
LOGON32_LOGON_BATCH
Windows.h
要关注它,请按住 Ctrl 并左键单击突出显示的名称。执行此操作后,您将看到数字与反编译的 switch 语句 IDA 匹配。
A 定义为“要执行的登录操作的类型”。此参数可以是 中定义的下列值之一。类似地,我们可以看到 中指定的,并且根据 Microsoft 的说法,a 指定了身份验证提供程序,默认提供程序是“协商”提供程序。此参数可以是以下值之一。
注意:我们将在以后的博客文章中介绍登录提供程序的含义logOnType
Winbase.h
logOnProvider
Winbase.h
logOnProvider
IDA 反编译函数体的第二部分(如下所示)注意到 和 都在该部分中定义,它们是初始化的静态变量。logOn32MsvAuthPkgID
LogOn32NegoAuthPkgId
.data
对于不知道的人,the 被解释为有符号整数,因此我们正在输入“if”语句,暂时可以忽略 。在嵌套的“if”语句中,有一个函数调用(例如,),当双击函数名称时,该函数调用将显示其反编译代码。0FFFFFFFFh
-1
RtlEnterCriticalSection
L32pInitiLsa
IDA 将为我们反编译代码,结果将类似于下面的示例图像。
你会注意到,两个常量字符串使用了该函数,稍后,该字符串用于对函数的函数调用。幸运的是,这里有很好的记录。从函数原型中,我们可以找出字符串,并且只是身份验证包的名称。成功调用后,该函数返回包标识符 () 并将其保存到前面提到的部分。RtlInitString
LsaLookupAuthenticationPackage
LsaLookupAuthenticationPackage
MICROSOFT_AUTHENTICATION_PACKAGE_V1_0
Negotiate
pkgID
.data
返回 ,我们继续讨论函数的第三部分。在我们调用的部分中,我们在末尾传递了四个额外的零,如下所示:L32pInitLsa
Addvapi32!LogonUserCommonA
这些零被保存在堆栈中,并且从未在 和 中使用过,IDA 认为它们在这里被 使用。四个零是参数,正在检查参数是否为 NULL,如果是,它将为我们启动它。
注意:我没有花太多时间试图理解这部分代码,因为它在 RPC 上下文中无关紧要,所以如果您知道这些“零”有不同的用途,请给我发 dm!Advapi32!LogonUserCommonA
Advapi32!LogonUserCommonW
SspiCli!LogonUserEXEXW
a8, a9, a10, and a11
该函数的第 4 部分很有趣,因为这里调用了几个新函数,并且 .L32GetDefaultDomainName
L32pLogonUser
第一个“if”语句检查我们提供的域参数的第一个字节是否等于十进制 46,如果您查找 ASCII 表,它会指示“.”字符。根据 Microsoft 文档,如果此参数为“.”,则该函数仅使用本地帐户数据库验证帐户。
双击 IDA 中的函数名称将显示 的反编译代码。此代码在我们的例子中不是很重要,但它的作用是调用以获取本地计算机名称,并将本地计算机名称放在全局变量处,然后将用户提供的域设置为 。L32GetDefaultDomainName
LsaLookupGetDomainInfo
Logon32DomainName
goto LABEL_6
Logon32DomainName
返回自 ,该函数将我们提供的密码(已键入)再次转换为(不确定为什么),第二个“if”语句根据我们提供的参数确定身份验证包 ID。如果大于 4,请使用 ;如果没有,请使用从上一个 .L32GetDefaultDomainName
UNICODE_STRING
UNICODE_STRING
logOnProvider
MSV1_0 package ID
Negotiate package ID
lsalookupAuthenticationPackage
最后我们到达函数调用,双击函数名会指向反编译的函数代码。L32pLogonUser
是前面提到的全局变量。有一个新字符串正在初始化“LogonUser API”,还有一个新变量,它看起来像包含我们提供的参数的缓冲区的长度。LogonLsaHandle
SspiCli!L32pInitLsa
代码的下一部分为缓冲区的长度分配堆内存;该函数首先初始化 to 。输入 “if” 语句,如果 不是 4 (即 ),则变量将被赋值 2。代码的其余部分使用用户提供的参数和填充缓冲区。这里需要注意的是,IDA 将 AuthInformation 缓冲区标识为 ,这意味着 的每次取消引用都指向一个字(两个字节)。_MSV1_0_LOGON_SUBMIT_TYPE
82
LogonProvider
LOGON32_PROVIDER_VIRTUAL
_MSV1_0_LOGON_SUBMIT_TYPE
_WORD *AuthInformation
AuthInformation
最终缓冲区类似于下图,其中前四个字节存储0x52(缓冲区的长度),缓冲区的其余部分包含我们提供的用户名、密码和域。请注意,由于显而易见的原因,此处的密码已被编辑。AuthInformation
UNICODE_STRING
初始化缓冲区后,调用将对 进行另一个函数调用。从函数原型中,IDA 在伪代码窗口中很好地显示了参数,我们采用之前通过 分配的全局变量,该整数MSV1_0身份验证包 ID 或协商包 ID 全局变量,该全局变量之前通过 、我们新分配的身份验证缓冲区、身份验证缓冲区的长度和其他参数。SspiLogonUser
LsaHandle
L32pInitLsa
L32pInitLsa
单步进入该函数,我们可以看到它几乎是一个带有“if else”语句的包装函数。检查名为的全局变量,以查看它是否被赋值;这是用于查看函数是否在进程内部调用的检查。在此方案中,它会将执行流重定向到 .SspipLogonUser
SSPISRV_SecpLsaInprocDispatch
lsass.exe
NdrClientCall3
NdrClientCall3
是一个强大的函数,它允许开发人员调用一个 RPC 服务器,而不必担心幕后打包的所有参数,但是这个函数试图调用哪个 RPC 服务器/接口呢?从 NdrClientCall3 的函数原型中,我们知道第一个参数是 。快速搜索将我们引导至有关此类型的 Microsoft Rust 文档(您也可以在 Windows SDK 附带的文件中找到它)。MIDL_STUBLESS_PROXY_INFO
RPCNDR.h
的第一个字段称为 。我们可以通过右键单击字段名称来进一步拆解它。MIDL_STUBLESS_PROXY_INFO
MIDL_STUB_DESC
正如官方Microsoft文档中对MIDL_STUB_DESC的描述中所说的那样:“对于服务器端的非对象 RPC 接口,它指向 RPC 服务器接口结构。在客户端,它指向 RPC 客户端接口结构。对于对象接口,它是 null。由于我们不在 RPC 服务器二进制文件中,而是在调用 RPC 服务器的 RPC 客户端中,因此 RpcInterfaceInformation 指针包含指向 RPC 客户端接口结构的指针。请注意,在描述中,它提到:“数据结构在头文件 Rpcdcep.h 中定义。有关语法块和成员定义,请参阅头文件。这将在下一步中派上用场。RpcInterfaceInformation
双击 IDA 中的 进行验证。sspirpc_Proxyinfo
我们将看到另一个窗口跳转到该部分。.rdata
接下来,单击 ,因为它是指向结构的指针。sspirpc_StubDesc@@3U_MIDL_STUB_DESC@@B
MIDL_STUB_DESC
到达结构后,单击 ,因为它是指向 的指针。MIDL_STUB_DESC
unk_7FF86CF454B0
RpcInterfaceInformation
现在,还记得Microsoft之前关于 ?如果我们想找到其他有用的信息,例如,、等结构中的确切字段以及指针指向什么,该怎么办?回想一下,在文档中,它提到我们可以在 中找到详细信息,这在 Windows SDK 工具包中提供。RpcInterfaceInformation
RpcInterfaceInformation
RPC_SYNTAX_IDENTIFIER
PRPC_DISPATCH_TABLE
PRPC_PROTSEQ_ENDPOINT
InterpreterInfo
Rpcdcep.h
我在我最喜欢的文本编辑器 (Visual Studio) 代码中打开了它,并检查了 .我们可以搜索 RpcInterfaceInformation 并查看对名称的一些引用。Rpcdcep.h
在这种情况下,RpcInterfaceInformation 似乎是一种类型。要跳转到定义,请按住 Crtl 并左键单击名称。RPC_SERVER_INTERFACE
RPC_SERVER_INTERFACE
回想一下 Microsoft 文档中的那些说:“对于服务器端的非对象 RPC 接口,它指向 RPC 服务器接口结构。在客户端,它指向 RPC 客户端接口结构“?这就解释了。
如果我们返回到 Visual Studio 代码并检查结构,我们可以看到它不仅包含全局唯一标识符 (GUID),还包含 和 ._RPC_SYNTAX_IDENTIFIER
RPC_VERSION
MajorVersion
MinorVersion
如果我们在 上做同样的事情,指针将指向一个名为 的结构。有关示例,请参见下文。PRPC_DISPATCH_TABLE
RPC_DISPATCH_TABLE
怎么样 ?啊哈!contains 和 ,这很有意义,因为 RPC 客户端/服务器运行时库都必须知道用于连接的协议序列和端口。PRPC_PROTSEQ_ENDPOINT
PRPC_PROTSEQ_ENDPOINT
RpcProtocolSequence
Endpoint
现在,结构中只有两个未知字段:和 。我们可以使用 IDA 的帮助在 DLL 中查看它。DefaultManagerEpv
InterpreterInfo
回到 IDA,我们可以根据之前获得的知识手动布局内存结构。我们可以单击一个地址(在本例中为 ),然后点击将本地类型应用于 IDA 已知的结构。00000001800295E4
Alt + Q
使用已识别的字段,继续签出和 。RPC_CLIENT_INTERFACE
DefaultManagerEpv
InterpreterInfo
由于我们仍处于 的 IDA 视图中,因此仅初始化了 InterpreterInfo 字段。我们可以双击跳转到地址偏移量。SspiCli.dll
?sspirpc_ServerInfo@@3U_MIDL_SERVER_INFO_@@B
从 IDA 首次启动时为我们下载的 Microsoft 公共符号中,IDA 已将此结构标识为 . 快速搜索显示,此结构是在另一个名为 的文件中定义的,该文件也在 Windows SDK 中提供。_MIDL_SERVER_INFO_
rpcndr.h
我们可以在 IDA 中再次根据结构定义手动映射内存布局。
如果我们单击 ,我们将看到内存包含一个函数偏移量。我不会描述该函数的作用,因为它与这篇博文无关。off_180029740
到目前为止,我们只是从(即 RPC 客户端)查看了 RPC 使用情况......但是服务器端呢?请记住:我们最初的目标是确定在 RPC 服务器端调用了哪个函数!SspiCli.dll
要使 RPC 运行时库调用 RPC 服务器,客户端和服务器端的调用必须相同。我们可以将字节拉到一起,并将它们放入 PowerShell 中。RPC_CLIENT_INTERFACE.InterfaceId.SyntaxGUID
RPC_CLIENT_INTERFACE.InterfaceId.SyntaxGUID
快速搜索显示 RPC 接口属于 托管的 RPC 服务器。4f32adc8–6052–4a04–8701–293ccf2096f0
SspiSrv.dll
回想一下,在函数原型中,第二个参数是 opNum,我们可以将其视为 RPC 服务器存储在该部分中的函数表中函数的索引。RPC 运行时库将处理所有参数打包并查找承载 及其接口的进程,并将参数与接口信息一起传递给进程 RPC 运行时库。RPC 服务器端的 RPC 运行时库解压缩所有参数,并调用索引指向的函数 RPC 服务器函数表中的函数。NdrClientCall3
.rdata
SspiSrv.dll
我们现在知道对 和 is 12 的 in 调用。要在 SspiSrv.dll 中搜索接口 GUID,请打开 IDA 的新实例,加载 ,按 打开二进制搜索窗口。NdrClientCall3
SspipLogonUser
SspiSrv.dll
opNum
SspiSrv.dll
Alt + B
接下来,我们可以将 的前四个字节放入框中,然后点击“确定”。InterfaceId
String
在此示例中,IDA 在 .SspiSrv.dll
双击它将向我们显示我们已经看到的内容。
这是 中的指针,它看起来与我们之前在 中看到的指针相同。现在我们在RPC服务器中,我们怎样才能找到接口的函数表?答案就在结构中。RpcInterfaceInformation
SspiSrv.dll
SspiCli.dll
_RPC_SERVER_INTERFACE
在 IDA 中,我们可以通过将鼠标悬停在上面并按 来进行交叉引用。从那里,我们可以看到引用此特定内存地址的代码。unk_7ff86CF454B0
x
在此示例中,该地址在代码的两个不同部分中被引用两次:一次在函数中的部分中,一次在 .rdata 中。我们记得 是结构的一个字段,因此这里的第二个交叉引用很可能指向 .双击交叉引用应该会将我们带到窗口中的新视图,如下所示:.text
SspiSrvInitialize
RpcInterfaceInformation
MIDL_STUB_DESC
MIDL_STUB_DESC
.rdata
并透露它确实是我们的结构。MIDL_user_allocate
MIDL_user_free
MIDL_STUB_DESC
很酷,现在让我们按照前面的步骤再次演练它。
双击 o 进入结构unk_7FF86CF454B0
RpcInterfaceInformation
双击查找(注意:该结构现在将代替off_7FF86CF443D0
RPC_SERVER_INTERFACE.InterpreterInfo
RPC_SERVER_INTERFACE
RPC_CLIENT_INTERFACE
)
3.到达后,我们可以通过双击找到RPC_SERVER_INTERFACE.InterpreterInfo
MIDL_SERER_INFO.Dispatchable
off_7FF86CF44180
与客户端 DLL 上的方法相比,我们现在应该看到一堆不同的方法。
现在我们找到了调度表,最后一个问题仍然存在:我们从哪个调用?好吧,答案很容易找到!还记得通话中使用的吗?这是 RPC 运行时库用于确定 RPC 客户端尝试调用的函数的索引。我们可以简单地数到函数表中的第 13 个函数(注意:索引从 0 开始)函数,即 。正如函数名称所暗示的那样,我们认为它与 有关。如果你不相信我,你可以用调试器设置一个断点,然后自己找出答案。LogonUserA
OpNum
NdrClientCall3
SspiLogonUser
LogonUserA
我们已经到了这篇博文的末尾。如果您发现任何错误,请在 X 上私信我,我会做出相应的调整!这篇博文是为像我这样对逆向工程感兴趣的人准备的,他们正在寻找一篇涵盖一些基本的 IDA 用法和逆向工程方法的手把手文章。
https://specterops.io/wp-content/uploads/sites/3/2022/06/RPC_for_Detection_Engineers.pdfhttps://posts.specterops.io/wmi-internals-part-3-38e5dad016be
https://csandker.io/2021/02/21/Offensive-Windows-IPC-2-RPC.html
https://www.fortinet.com/blog/threat-research/the-case-studies-of-microsoft-windows-remote-procedure-call-serv