Many of us in the cybersecurity industry have a natural inclination to delve into technical concepts and understand what's going on behind the scenes. Or, if you're like me, over time you develop it and realize the many complexities and dependencies you missed, creating a desire to dig deeper and figure out what's going on. I find this challenges assumptions and forces a breakdown of what I think is a good way to think analytically.
Lately, that has meant an interest in digging deeper below the "program" level. For many of us, the MITER ATT&CK Matrix presents a conceptual model of how adversary trading techniques work. You have Tactics, Techniques, and Procedures (TTPs), which describe the motivations, actions, and methods an adversary uses to achieve its malicious goals.
If you're like me, you've probably stuck at that "process" level and haven't spent all of your time digging through all the intricacies. In my own fixed mindset, I mistakenly thought this was already the point where I needed to go deep. I've come to appreciate the obvious fallacy of this mentality, as it can drive the development of detection and defense methods that focus on more static indicators of compromise (IOCs). However, once we start looking under the hood of The Program, we realize that there is a whole host of activity going on. In this case, it'sfunction call stackJared Atkinson writes on his blog,Understanding the function call stack.
In my case, it seemed a little daunting at first. I've never delved into these concepts and I fear I'll have to learn a lot of other stuff before I can do so. This is also a wrong assumption. I've recently had the opportunity to start analyzing a couple of different tools using the methods he lays out in this blog, and my goal here is to give an overview of how I'm approaching this problem and the method call stacks that can be used for basic analysis. Once you're able to do this, I'm sure you'll discover a whole new layer of concepts worth digging into and understanding in order to achieve your goal of demystifying your adversary's trade techniques.
As an administrative note, this blog is written so that you can follow the steps I've taken you through. In the end, I'll use a step-by-step approach as a quick reference. I've also compiled a requirements section at the bottom of this blog for your convenience should you choose to follow along.
I initially struggled with the idea that I would have to have a ton of deep prerequisite knowledge to start digging into function call stacks. While this is not on the level I originally envisioned, I believe you should at least understand a few things. First, familiarizing yourself with the function call stack discussed in the previous blog link is a good starting point (you don't have to know every detail, but you should be able to look at it and understand what's going on conceptually). Second, you don't have to be an expert programmer, but you should be familiar with reading some code - it's important to understand flow, variables, different statements (if-then, else, etc.). Of course, you have to be willing to learn along the way. Things might not make sense at first, but as you dig deeper, read documentation, look at code, analyze connections between functions, things start to connect and make more sense.
For this, I decided to use a fairly simple tool for analysis. withintrickeryThere is a PowerUp script written in the privilege escalation moduleWill Schroeder, which includes a namedGet ProcessTokenPrivilege.The function itself isn't necessarily malicious, but it has what I need to better understand a way to analyze a function's call stack.
For context, Get-ProcessTokenPrivilege is a PowerShell function that returns the current or specified process ID's privileges. As such, an attacker might use this to determine the privileges of a process they are using or a process they might want to turn to. It does this by opening a process handle, opening a process token, querying the token information, and then closing the token handle. It's slightly different if you're querying the process you're in (pseudo handle vs. regular handle), but otherwise the same.
For this analysis, we'll look at one of these steps and understand what's going on behind the scenes. This analysis will be straight forward and should give you a good idea of what the function call stack looks like when you drill down into individual functions. Note that I don't repeat these steps for each function as that would be redundant for this tool.
Above I depicted apartPower-up code. Now, if you follow along, you'll notice that I started with a later function in Get-ProcessTokenPrivilege and chose to start with OpenProcessToken instead of OpenProcess. You can follow here if you like, or start from scratch (OpenProcess) and go from there - I'll provide guidance for doing both, as the differences between the two are minimal. Anyway, what you see above is the block of code in the PowerUp script that references the OpenProcessToken. To provide some context, this is the point at which a process handle is retrieved (note the "IF" statement at the top that checks the validity of the process handle) and passed to the function on the line annotated with a green square ($Advapi32::OpenProcessToken). Now, to dig into the function call stack, we won't worry about everything you see here. It's the specific OpenProcessToken function we're interested in. Also useful context here is that OpenProcessToken is an explicit declaration of a dynamic link library (DLL) exported from -Advapi32. I'll note that you won't always see references to DLLs, so if you only have functions, it's best to check the API docs, which will list a table of requirements. In this case, ieOpenProcessTokenThis confirms Advapi32 as the export library.
With that in mind, we need to look at this referenced library and find out what's actually going on behind the scenes. For this we need a software disassembler - I usedevelopment association(although you can also use Ghidra). A few things to note, though: make sure you open IDA as an administrator, or you'll have problems loading the DLLs; Advapi32 is located in C:\Windows\System32\, and you'll have to go there when prompted by IDA.
After opening IDA, you should verify that you can see the function you are looking for. This is done in the bottom right corner of IDA (A), where you can look at the Export tab (B), press CTRL+F and enter the function name©, which should appear at the top (D). Of course, this The function's documentation states that its module is advapi32.dll, so it should also appear in the function window on the left (E). If you look, you'll see that it's not (F, G). Instead, you have OpenProcessTokenStub. This is okay and will not affect your ability to continue your analysis. Just double click on OpenProcessToken in the Export tab and it will take you to the function in the disassembly output.
Selecting it should lead you to an IDA View-A window showing:
What's interesting here is that the process we see is actually not OpenProcessToken, but OpenProcessTokenStub (A, similar to what we saw in the last step of the Functions window). Below is the only reference to OpenProcessToken which refers to __imp_OpenProcessToken (B).
This is interesting because it further suggests that OpenProcessToken is not native to advapi32.dll. It's not uncommon though, and you'll see it a lot when looking at functions. In this case, the `__imp_` part indicates that OpenProcessToken is an imported function. This ultimately means...
From here, since it's imported, you have to go to the "Import" tab and look for OpenProcessToken (another CTRL+F). When you do this, you should get this somewhat confusing-looking output:
Here you will see the library from which OpenProcessToken is imported (A), listed as `api-ms-win-core-processthreads-l1–1–0` (B). This is calledCollection of APIs, unfortunately it won't appear in your System32 folder as a DLL that can be disassembled in IDA.
That's good though, because there are still ways around this. In James Forshaw's NtObjectManager tool, there is a cmdlet called Get-NtApiSet. Using the following command structure "Get-NtApiSet -Name
In this case, it is kernel32.dll. This means we should actually be looking for OpenProcessToken in Kernel32. In this case, you can follow the exact same steps as mentioned earlier to see what happens to the OpenProcessToken . I won't go into details for you, so if you want to open Kernel32 in IDA and do the same thing, feel free to use extrarep.
If you do, you'll find it againyesis exported, but doesn't show up in the functions listed in the left window I referenced above. Also, you'll notice that when you select it from the "Export" tab, you get the following:
So, again, we find that OpenProcessToken is imported from elsewhere. If you go to the Imports tab and look for OpenProcessToken, you'll see something familiar:
It is imported from another API set. If you look closely, it's the exact same API set as before. Now, from the previous step we know that api-ms-win-core-processthreads-l1–1–0 redirects to kernel32.dll as the host module. So what now? We have a function that is imported, but the reference library is the same DLL that says it was imported. To fix this, we must be able to access the objects in the output of the Get-NtApiSet we used earlier. Specifically, the `Hosts` object. Therefore, our command changes slightly: `Get-NtApiSet -name
From here, it's a similar process as before. Open IDA, drop kernelbase.dll in, now you will see two things: OpenProcessToken will appear as an exported function (first picture) and a base function (second picture):
You can choose one of these (since they will take you to the same line in the disassembly output). You may see a text view at this point, but you can also left click and select "Graph View" which will take you to a more visual output of the function (not pictured).
In either case, when we look at the function, we can see that it calls a separate function. In this case, pink text highlights this call: `__imp_NtOpenProcessToken`. Again, `__imp_` means it's an imported function, so we need to go back to the Imports tab and search for it. In this case, however, we don't have to worry about the API set because NtOpenProcessToken (A) is imported from NTDLL (B). As a side note, you may notice that when we look up NtOpenProcessToken we find a similar function (NtOpenProcessTokenEx) that seems to be relevant, although not in our function call stack. If you're interested, I suggest you dig a little further into this function to see why it's different, and under what circumstances it might be called instead of NtOpenProcessToken.
From here, we can open ntdll.dll in IDA and do a final search. Again, as before, we'll see NtOpenProcess in the "Exports" (A) and Functions list (B):
Selecting it and opening it (text or graphical view is fine), we see the function was last called as a "Syscall" or system call (green square):
This system call is the last step in the function call stack and represents a basic level of operation. In this case, we can refer to the operation as "token opening". At this point, we don't have to keep digging into more DLLs - that's the depth of our analysis. Graphically depicted, this function call stack would look like this:
Now, if you're anything like me, you might think that the whole process seems like it's just about figuring out what's going on behind the scenes. It's easy to imagine this when the original call to OpenProcessToken is made and the underlying system calls indicate that essentially the same thing happened. However, this is not the case for all Win32 APIs (morehere), and when you take that knowledge and dig into other tools, you'll find that there's more than one simple process to what's going on. Or a tool has different IF-ELSE statements that try multiple sets of actions to achieve a specific goal. Of course, these are factors that can differentiate a particular tool when it comes to understanding what a tool is doing behind the scenes. While I haven't gone into all of these variants, keep in mind that this walkthrough is just to give you enough knowledge to do it yourself. I know that when I have the tools to do this, I start to ask more questions, I start to do more digging, and I start to understand more about the intricacies of function call stacks.
If you recall, of course, there are more Win32 APIs referenced in the function. As I said, I won't cover these explicitly, as the instructions here will just repeat multiple times for roughly the same process. That said, if you decide to go through all of this, you'll find that your final product looks similar to this:
If yours looks slightly different, here's my chance to challenge you to go back and look at the PowerUp script to see why there is a deviation. If you get stuck, I'll describe what I've learned, which I hope you can use in further iterations of analyzing function call stacks.
For clarity, I've shown the general approach I've followed in this blog in list format with minimal description. I'm assuming you've read through the previous section, so if you have questions about the steps here, I'll detail them above.
- Start with the tools you are interested in.
- Static analysis of the code Did you find any references to Win32 APIs? If not, that's fine, but you'll have to dig into the functions to see what's going on. An example of where you might run into this kind of problem is a tool like Impacket, where the functions are written in Python, but end up referencing a function that you can drill into for further analysis.
- Review the documentation for the function and locate the DLL associated with the identified function. Although Microsoft did noteverythingFor the record, these advanced features should be documented and include the DLL in the requirements section.
- Pull the DLL in IDA and note where your target function can be found. There are three places you can and should look in order: (1) Exports (tab, far right) — functions shouldalwaysIn this list; (2) Functions (window, leftmost) - functionpossibleIn this list; (3) Imports (tab, rightmost) — functionpossiblein this list, but you should verify what happens under "Exports" and "Function" first.
- When you find functions that call imported functions ("__imp_"), look at the libraries they reference. This could be a single DLL, but it could also be an API set with a naming convention like "api-ms-win-security-base-l1–1–0" or similar. If it is another DLL, repeat steps 4-5.
- If the referenced library is an API set, remember to use Get-NtApiSet from the NtObjectManager module. This will help you find the host module from which the function was exported, and help eliminate confusion pointing to the API set of the DLL you just came from. Remember to enumerate the "hosts" to verify which host module is the ultimate home for a particular function.
- Once you reach NTDLL, start looking for basic system calls. This is the last call made in the function call stack, and the last point that determines the underlying operations that occurred in user mode. Note that you may find functions that reference multiple functions at the NTDLL level (or higher). This will require additional analysis, but the steps outlined here are sufficient to continue digging.
- Once you've identified the full function call stack, move on to the next function and make sure to string them together in a meaningful way. Sometimes functions are written consecutively; sometimes there are many IF-ELSE type statements that can bog down your analysis.
- Share your experiences - If you learn something new, I encourage you to write about your own experiences and share what you learned.
Going through the process was a little daunting for me at first, but I aspired to get better at it because I wanted to go beyond the "program" level where we tend to stop for one reason or another. For me, the mental model of "tactics, techniques, and procedures" ends at the "procedure" level, and I don't consider what might be below that level. My fault.
But through the process, I realized it wasn't as daunting as it first seemed. In some ways, it can be pretty straightforward and simple to start understanding what's going on under the hood. I've also learned more about what's going on behind the scenes of a particular tool, and now that I've looked at a few tools, I'm starting to see similarities between what they do. Between opening or creating handles, access tokens, query tokens, and closing handles, these operations exhibit commonalities across tools that are not immediately apparent without digging deeper.
Of course, it should be noted that my own walkthrough is fairly basic - that's the intent. I mean to help other people get started, so this blog may not be enough for deeper technical analysis, you may need to dig into more complex tools. This is especially true if you don't have a codebase to look at, but that's a whole other story. The same is true for tools utilizing Remote Procedure Calls (RPC) -- there is a similar approach, but with some minor changes that I haven't covered here.
For those who may have any other questions feel free to reach out here,LinkedIn, orTwitter.I look forward to helping others with their own learning process. From here, I also recommend turning to Jared Atkinson's blog series, "On Detection: From Tactics to Functions,” as he proceeded to address issues such asHow different tools are starting to take advantage of similar capabilities;How to handle RPCscompared to the standard Win32 API;Composite function, which includes multiple functions nested within a single function; andfunction changeturn outthousandsVariations on how an attacker executes a particular process. For more reading or those references I mentioned, see the resources below. Otherwise, good luck everyone as you've been digging in to better understand the technologies we use.
Special thanks to
Thank you so muchjonny johnsonAnswering a whole host of questions through my own learning experiences,Jared Atkinsonbreak it out and let it hit the first time, thenEvan McBloomAll the way down to the patent level, providing an answer to one of my many "but why" questions.
- Windows 10 (results may vary on other versions of Windows)
- IDA free download:https://hex-rays.com/ida-free/
- Nt object manager:https://www.powershellgallery.com/packages/NtObjectManager/1.1.33
- Home page:https://github.com/PowerShellMafia/PowerSploit
- Get ProcessTokenPrivilege (inside PowerUp):https://github.com/PowerShellMafia/PowerSploit/blob/d943001a7defb5e0d1657085a77a0e78609be58f/Privesc/PowerUp.ps1#L1236
Arrows Drawing Website
- Processes and threads:https://learn.microsoft.com/en-us/windows/win32/procthread/processes-and-threads
- Win32 API：https://learn.microsoft.com/en-us/windows/win32/apiindex/api-index-portal
- Collection of APIs:https://learn.microsoft.com/en-us/windows/win32/apiindex/windows-apisets
- Dynamic link library —https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-libraries
- T1057 - Process Discovery (Get-ProcessTokenPrivilege):https://attack.mitre.org/techniques/T1057/
- Powersploit entrance:https://attack.mitre.org/software/S0194/
Further reading on Function CallStack:
- Function call stack:https://medium.com/specter-ops-posts/understanding-the-function-call-stack-f08b5341efa4
- On Detection: From Tactics to Functionality (#1):https://medium.com/specter-ops-posts/on-detection-tactical-to-functional-d71da6505720
Beyond Procedure: Delving into the Function Call StackOriginally published onPosts by SpecterOps team membersOn Medium, people continued the conversation by highlighting and responding to the story.
*** This is a syndicated blog from Security Bloggers NetworkPosts by SpecterOps team members - MediumauthorNathan.Read the original text:https://posts.specterops.io/beyond-procedures-digging-into-the-function-call-stack-88c082aeb573?source=rss----f05f8696e3cc---4