Oct 18 2023 | kai huang

Uncovering RPC Servers through Windows API Analysis

Share

Intro

Have you ever tried to reverse a simple Win32 API? If not, let’s look at one together today! This article serves as a hand-holding walkthrough and documents in detail how I analyzed a simple Win32 API: LogonUserA. Throughout the article, we’ll go over how to use some of IDA’s most common features and look for some “poorly-documented” Microsoft structures.

Are you ready? If so, then grab your IDA or Ghidra and a cup of coffee, and let’s get started!

Advapi32!LogonUser

Per the official Microsoft MSDN documentation, The LogonUser function attempts to log a user on to the local computer and returns a handle to a token that represents the logged-on user.” The function declaration is (note the Hungarian notation):

BOOL LogonUserA(
[in] LPCSTR lpszUsername,
[in, optional] LPCSTR lpszDomain,
[in, optional] LPCSTR lpszPassword,
[in] DWORD dwLogonType,
[in] DWORD dwLogonProvider,
[out] PHANDLE phToken
);

From the parameters, we can assume that if we supply valid credentials, we will receive a valid token handle in return. That is the whole purpose of LogonUserA and red teamers can use the token handle to impersonate the specified user.

Advapi32!LogonUserA

The requirements section in the LogonUserA MSDN documentation states that Advapi32.dll dynamic-link library (DLL) exports the function. First, let’s open it up in IDA. The steps are listed as follows:

Open IDA, click “New”, and double-click on the Advapi32.dll DLL located under C:windowssystem32Advapi32.dll.

After selecting Advapi32.dll, IDA will ask for additional loading options for the DLL.

I like the tip given out by @herrcore which is having an IDA decompiler window and the graph mode window side by side like this:

Another tool tip is “Synchronizing” the decompiler window to with the graph window. You can do this by right-clicking on the graph window, hovering over the “Synchronize with” button, and clicking on the name representing the decompiler window (in this case, Pseudocode-A). Now when you click on any line of the code, both windows will jump to the corresponding line.

Alright. Let’s get back to LogonUserA! When you click “yes” upon loading LogonUserA into IDA, IDA will attempt to download the public symbol from the Microsoft server. For this article, I have already downloaded a file and that is why IDA has populated the name of the function and its parameters for me.

Right off the bat, we can see the function LogonUserA acts as a “Wrapper function” to LogonUserCommonA. Something worth noting is that IDA also adds four “Zeroes” onto the stack as the arguments for LogonUserCommonA which indicates LogonUserCommonA might take additional arguments than LogonUserA.

Addvapi32!LogonUserCommonA

After double-clicking on the LogonUserCommonA function, IDA will show the decompiled code for LogonUserCommonA in both of our windows. (IDA double click)

After double-clicking, LogonUserCommonA will first call RtlInitAnsiString and RtlAnsiStringToUnicodeString with our supplied arguments such as “username, domain, and password”. Those calls simply turn our American National Standards Institute (ANSI) encoded string arguments into the UNICODE_STRING type — which is what Windows accepts as string arguments. Microsoft has well documented the reasoning behind this.

Advapi32!LogonUserCommonW

After converting our supplied username, domain, and password to UNICODE_STRINGs, the LogonUserCommonA will proceed to call LogonUserCommonW

Inside the LogonUserCommonW function, we can see that it makes another function call to LogonUserEXEXW.

Double-clicking on LogonUserEXEXW, the graph window will jump to the .idata section (tl;dr, .idata section stores the import directory information about a portable executable) of the binary and we can scroll up to see which DLL the advapi32 is importing LogonUserEXEXW from SspiCli.dll.

SspiCli!LogonUserEXEXW

In this scenario, we need to open a new IDA instance for the SspiCli.dll and, if we allow IDA to download public symbols, IDA will happily populate the function names and parameters.

To use the search function, press Ctrl + F and type LogonUserEXEXW into the function name window.

Next, click on the LogonUserEXEXW. IDA will then jump to the function in both of our windows (you can see the setup below).

Skimming through the function body, the first part of the function appears to verify the logonType and logonProvider arguments. IDA gratefully decompiled the code into a switch case for us so that we can see it better.

The questions here are, “What are some of the logonType and logOnProviders Microsoft has created?” and, “What are the differences between them?”

If we go back and revisit the function prototype for LogonUserA, we can see the dwLogonType and dwLogonProvider’s descriptions at the bottom of the page. However, it is not very straightforward as to what each of the names is represented by numbers we have seen in IDA.

My solution is to find it with Visual Studio or rather Windows SDK header files. We can find one of the names specified under dwLogonType (e.g., LOGON32_LOGON_BATCH) and copy-paste it to our test Visual Studio project with header files (Windows.h) included.

To follow it, hold Ctrl and left-click on the highlighted name. Once this action is performed, you will see the numbers match with the switch statement IDA decompiled.

A logOnType is defined as, “The type of logon operation to perform” . This parameter can be one of the following values, defined in Winbase.h.” Similarly, we can see the logOnProvider specified in Winbase.h, and according to Microsoft, a logOnProvider specifies the authentication provider, the default provider is the “negotiate” provider. This parameter can be one of the following values.”
NOTE: We will cover what a logon provider means in future blog posts

The second part of the IDA-decompiled function body (shown below) notices that the logOn32MsvAuthPkgID and LogOn32NegoAuthPkgId are all defined in the .data section, which are initialized static variables.

For people who don’t know, the 0FFFFFFFFh is interpreted as-1 as a signed integer, hence we are entering the “if” statement and can ignore the RtlEnterCriticalSection for now. Within the nested “if” statement, there is a function call (e.g., L32pInitiLsa) that will reveal its decompiled code when double-clicking on the function name.

From SspiCli!LogonUserEXEXW to SspiCli!L32pInitLsa & SspiCli!LsaLookupAuthenticationPackage

IDA will decompile the code for us and the result will resemble the example image below.

You’ll note that two constant strings used the RtlInitString function and later, that string is used with a function call to the LsaLookupAuthenticationPackage function. Luckily for us, the LsaLookupAuthenticationPackage is well documented here. From the function prototype, we can figure out the strings MICROSOFT_AUTHENTICATION_PACKAGE_V1_0 and Negotiate are simply the name of authentication packages. Upon successful call, the function returns the package identifier (pkgID) and saves them to the previously mentioned .data section.

Back to SspiCli!LogonUserEXEXW

Returning from L32pInitLsa, we continue to the third part of the function. In the section where we called Addvapi32!LogonUserCommonA, we passed four additional zeroes at the end, like so:

These zeros are saved onto the stack and never used within Advapi32!LogonUserCommonA and Advapi32!LogonUserCommonW, IDA thinks that they are used here by SspiCli!LogonUserEXEXW . The four zeros are arguments a8, a9, a10, and a11 and it’s being checked to see if the argument is NULL, if so, it will initiate it for us.
Note: I did not spend too much time trying to understand this part of the code as it was irrelevant in the RPC context, so if you know those “Zeros” serves a different purpose, please send me a dm!

From SspiCli!LogonUserEXEXW to SspiCli!L32GetDefaultDomainName

The 4th part of the function is interesting because there are a couple of new functions called here, L32GetDefaultDomainName and L32pLogonUser.

The first “if” statement checks to see if the first byte of our supplied domain argument is equal to decimal 46 which if you look up the ASCII table, it indicates the “.” character. According to Microsoft documents, If this parameter is “.”, the function validates the account by using only the local account database.

Double clicking on the function name in IDA will show the decompiled code for L32GetDefaultDomainName. This code is not very important in our case, but what it does is to calls LsaLookupGetDomainInfo to get the local computer name, and put the local computer nameit at a global variable Logon32DomainName, and then goto LABEL_6 sets the user-supplied domain to Logon32DomainName.

Returning from L32GetDefaultDomainName, the function casts our supplied password (already UNICODE_STRING type) to UNICODE_STRING again (not exactly sure why), and the second “if” statement determines the authentication package ID based on our supplied logOnProvider argument. If it’s larger than 4, use MSV1_0 package ID; if not, use Negotiate package ID obtained from the previous lsalookupAuthenticationPackage.

From SspiCli!LogonUserExExW to SspiCli!L32pLogonUser

Finally, we will arrive at the function call L32pLogonUser, double-clicking on the function name will point us to the decompiled function code.

The LogonLsaHandle is a global variable that was populated via SspiCli!L32pInitLsa mentioned previously. There is a new string being initialized “LogonUser API” and a new variable which looks like the length of a buffer containing our supplied arguments.

The next portion of the code allocates a heap memory for the length of the buffer; the function first initializes the _MSV1_0_LOGON_SUBMIT_TYPE to 82. Entering the “if” statement, if the LogonProvider is not 4 (which is LOGON32_PROVIDER_VIRTUAL), then the _MSV1_0_LOGON_SUBMIT_TYPE variable will be assigned with the value 2. The rest of the code fills the buffer with our user-supplied arguments and padding. Another note here is that IDA identified the AuthInformation buffer as _WORD *AuthInformation, which means each dereference of the AuthInformation would point to a word (two bytes).

The final buffer AuthInformation looks similar to the image below where the first four bytes store the 0x52 (the length of the buffer) and the rest of the buffer contains our supplied username, password, and domain in UNICODE_STRING. Please note that the password here has been redacted for obvious reasons.

From SspiCli!LogonUserEXEXW to SspiCli!L32pLogonUser to SspiCli!SspipLogonUser

After the buffer is initialized, the call will make another function call to SspiLogonUser. From the function prototype, IDA has nicely displayed the arguments in the pseudocode window, we are taking the LsaHandle global variable assigned previously via L32pInitLsa, an integer which is either MSV1_0 authentication package ID or Negotiate package ID global variable that was assigned previously via L32pInitLsa, our newly allocated authentication buffer, Authentication buffer’s length, and other arguments.

A sneak peek into SspiCli!SspipLogonUser and NdrClientCall3

Stepping into the SspipLogonUser function, we can see it’s almost a wrapper function with an “if else” statement. A global variable named SSPISRV_SecpLsaInprocDispatch is checked to see if it has been assigned a value; this is a check to see if the function is called inside of the lsass.exe process. In this scenario, it will redirect the execution flow to the NdrClientCall3.

NdrClientCall3 is a powerful function that allows the developer to make a call to an RPC server without worrying about all the parameters packed behind the scene, But which RPC server/interface is this function trying to call? From the function prototype of NdrClientCall3, we know the first argument is MIDL_STUBLESS_PROXY_INFO. A quick search lead us to Microsoft Rust documentation on this type (you can also find it in the RPCNDR.h file shipped with Windows SDK).

The first field of MIDL_STUBLESS_PROXY_INFO is called MIDL_STUB_DESC. We can take it apart further by right-clicking on the field name.

As the description of MIDL_STUB_DESC from official Microsoft documentation says about the first field RpcInterfaceInformation: “For a nonobject RPC interface on the server-side, it points to an RPC server interface structure. On the client side, it points to an RPC client interface structure. It is null for an object interface.” Since we are not inside of an RPC server binary but rather an RPC client that’s making a call to an RPC server, the RpcInterfaceInformation pointer contains a pointer to the RPC client interface structure. Note that in the description, it mentioned: “The data structure is defined in the header file Rpcdcep.h. See the header file for syntax block and member definitions.” This will come in handy for the next step.

Double-click the sspirpc_Proxyinfo in IDA to verify.

We will see the other window jump to the .rdata section.

Next, click on sspirpc_StubDesc@@3U_MIDL_STUB_DESC@@B, as it is the pointer to the MIDL_STUB_DESC struct.

Once we arrive at the MIDL_STUB_DESC struct, click on unk_7FF86CF454B0, as it is the pointer pointing to RpcInterfaceInformation.

Pointer to RpcInterfaceInformation

Now, remember the previous Microsoft documentation on fields within the RpcInterfaceInformation? What if we want to find additional useful information about RpcInterfaceInformation such as what exact fields are in structures like RPC_SYNTAX_IDENTIFIER, PRPC_DISPATCH_TABLE, PRPC_PROTSEQ_ENDPOINT, and what the pointer InterpreterInfo points toward? Recall in the documentation, it mentioned that we can find details in Rpcdcep.h, which ships within the Windows SDK kit.

I opened it in my favorite text editor (Visual Studio) code and inspected the content of Rpcdcep.h. We can search RpcInterfaceInformation and see some references to the name.

The RpcInterfaceInformation appears to be a type of RPC_SERVER_INTERFACE in this case. To jump to the definition, hold Crtl and left-click on the RPC_SERVER_INTERFACE name.

Recall the ones from Microsoft documentation says: “For a nonobject RPC interface on the server-side, it points to an RPC server interface structure. On the client side, it points to an RPC client interface structure”? This explains it.

If we go back to the Visual Studio code and inspect the _RPC_SYNTAX_IDENTIFIER structure, we can see that it not only contains a globally unique identifier (GUID) but also an RPC_VERSION which contains MajorVersion and MinorVersion.

If we do the same thing on PRPC_DISPATCH_TABLE, the pointer will point to a structure named RPC_DISPATCH_TABLE. See below for an example.

How about PRPC_PROTSEQ_ENDPOINT? Aha! PRPC_PROTSEQ_ENDPOINT contains RpcProtocolSequence and Endpoint, which makes sense because both the RPC client/server runtime library have to know which protocol sequence and port to use for connections.

Now there are only two unknown fields within the structure: DefaultManagerEpv and InterpreterInfo. We can use the help from IDA to look at it within the DLL.

With the help of IDA…

Going back to IDA, we can manually lay out the memory structures from the knowledge obtained previously. We can click on an address (in this case, it’s 00000001800295E4) and hit Alt + Q to apply a local type to the structure known to IDA.

With the field of RPC_CLIENT_INTERFACE identified, proceed to check out DefaultManagerEpv and InterpreterInfo.

Since we are still in SspiCli.dll’s IDA view, only the InterpreterInfo field was initialized. We can double-click on ?sspirpc_ServerInfo@@3U_MIDL_SERVER_INFO_@@B to jump to address offset.

From Microsoft public symbol that IDA has downloaded for us when first started, IDA has identified this struct as _MIDL_SERVER_INFO_. A quick search revealed that this struct is defined within another file named rpcndr.h, which is also shipped within Windows SDK.

We can manually map the memory layout according to the structure definition again in IDA.

If we click on the off_180029740, we will see that the memory contains one function offset. I will not describe what the function does, as it is not relevant to this blog post.

RPC Server Side

Up to this point, we simply looked at the RPC usage from the SspiCli.dll (i.e., the RPC client)…but what about the server side? Remember: our original goal is to identify which function is invoked on the RPC server side!

For the RPC runtime library to make a call to the RPC server, the RPC_CLIENT_INTERFACE.InterfaceId.SyntaxGUID has to be the same both on the client side and the server side. We can pull the bytes together from RPC_CLIENT_INTERFACE.InterfaceId.SyntaxGUIDand put them into PowerShell.

A quick search on 4f32adc8–6052–4a04–8701–293ccf2096f0 reveals the RPC interface belongs to an RPC server hosted from SspiSrv.dll.

Recall in the NdrClientCall3 function prototype, the second argument is opNum, which we can think of as the index for a function within a function table that the RPC server stored within the .rdata section. The RPC runtime library, which will handle all the parameters packing and find the process hosting the SspiSrv.dll and its interface(s), passes the arguments to the process RPC runtime library along with the interface information. The RPC runtime library on the RPC server side unpacks all arguments and invokes the function the index points to within the RPC server function table.

We now know the NdrClientCall3 in SspipLogonUser calls to SspiSrv.dll and the opNum is 12. To search for the interface GUID within SspiSrv.dll, open a new instance of IDA, load SspiSrv.dll, press Alt + B to open up the binary search window.

Next, we can just put the first four bytes of the InterfaceId into the String box and hit “OK”.

In this example, IDA found one instance of the byte sequence within the SspiSrv.dll.

Double-clicking on it will show us something we are already seen.

This is the RpcInterfaceInformation pointer within SspiSrv.dll, which looks the same as the one we saw previously in SspiCli.dll. How can we find the function table for the interface, now that we are inside the RPC server? The answer lies within the _RPC_SERVER_INTERFACE structure.

In IDA, we can do a cross-reference by hovering over unk_7ff86CF454B0 and pressing x. From there, we can see the code where this particular memory address was referenced.

In this example, the address was referenced twice in two different sections of the code: once in the .text section within the function SspiSrvInitialize and once within .rdata. We recall that the RpcInterfaceInformation is a field of the MIDL_STUB_DESC structure, so it is likely the second cross-reference here points to the MIDL_STUB_DESC. Double-clicking on the .rdata cross-reference should bring us to a new view in the window, like so:

The MIDL_user_allocate and MIDL_user_free gives it away that it is indeed our MIDL_STUB_DESC structure.

Cool, now let’s walk through it again following the previous steps.

  1. Double-click on unk_7FF86CF454B0 o into the RpcInterfaceInformation structure
  2. Double-click on off_7FF86CF443D0 to find RPC_SERVER_INTERFACE.InterpreterInfo (Note: the structure will now be RPC_SERVER_INTERFACE instead of RPC_CLIENT_INTERFACE)

3. Once we arrived at RPC_SERVER_INTERFACE.InterpreterInfo we can find MIDL_SERER_INFO.Dispatchable by double-clicking off_7FF86CF44180

We should now see a bunch of different methods compared to what was on the client side DLL.

Now we find the dispatch table and the last question remains: which one are we calling from LogonUserA? Well, the answer is easy to find! Remember the OpNum used in the call NdrClientCall3? This is the index used by the RPC runtime library to determine which function the RPC client is trying to call. We can simply count to the 13th function (Note: the index starts at 0) function in the function table which is SspiLogonUser. As the function name suggests, we believe it has something to do with LogonUserA. If you don’t believe me, you can set up a breakpoint with a debugger and find out for yourself.

Finally

We have arrived at the end of this blog post. If you find any mistakes, please DM me on X and I will adjust accordingly! This blog post is meant for people like me who are interested in reverse engineering and are looking for a hand-holding article that covers some of the basic IDA usage and reverse engineering methodologies.

Credits

I want to give my thanks to all the people and blog posts who helped me better understand RPC! Additionally, I want to give some shoutouts to many of my colleagues such as Evan, and Lee at SpecterOps, and friends who helped me to understand the topic.

https://specterops.io/wp-content/uploads/sites/3/2022/06/RPC_for_Detection_Engineers.pdf
https://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


Uncovering RPC Servers through Windows API Analysis was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.