Aug 4 2022 | Jared Atkinson

On Detection: Tactical to Functional

Share

Part 2: Operations

Introduction

Welcome back to my On Detection: Tactical to Functional series. In the first post in this series, we explored the source code for Mimikatz’s sekurlsa::logonPasswords command. We discovered that Mimikatz relies on three Windows APIs to read credentials from the memory of the LSASS process. First, it calls NtQuerySystemInformation to enumerate processes to find the Process Identifier (PID) of LSASS. Next, it calls OpenProcess to open a read handle to LSASS. It finishes by calling ReadProcessMemory to read the contents of the LSASS process’s memory which contains credential information. As a result, we determined a functional path for the sekurlsa::logonPasswords command of ntdll!NtQuerySystemInformation -> kernel32!OpenProcess -> kernel32!ReadProcessMemory.

Function Calls made by Mimikatz’s sekurlsa::logonPasswords command

It is quite common for malware analysts to become familiar with the chain of function calls used by malware to achieve some behavior. For instance, when I first started in Infosec, I was told that if you ever see OpenProcess -> VirtualAllocEx -> WriteProcessMemory -> CreateRemoteThread, you are dealing with Process Injection. At the time, I had no idea what these (API functions) were or how I’d see them, but that sequence has stuck with me.

In this blog post, we will explore how we can take this simple sequence of functions and expand it. You see, NtQuerySystemInformation -> OpenProcess -> ReadProcessMemory is not the ONLY way to dump credentials from LSASS. In doing so, I will introduce a new layer of abstraction that focuses on what operation(s) a function is performing. The operation is based on the outcome of the function call, for instance, WriteProcessMemory might be called a Process Write operation. This abstraction layer provides a more accurate way to describe the threat and prevents us from being too myopically focused on certain API functions. It also serves as the beginning of a coherent taxonomy that will reach all the way up to existing Tactics and Techniques codified in MITRE ATT&CK. Let’s dig in!

Function Call Stacks

In my Understanding the Function Call Stack post, I demonstrated how documented Windows API functions often serve as wrappers for more profound functionality. In that post, we explored how CreateFileW ultimately calls a Native API function called ntdll!NtCreateFile, which in turn makes a syscall to a function in the kernel also called NtCreateFile. I’ve reshared the Function Call Graph from the previous post below for reference:

Initial CreateFileW Function Call Graph

The point of that previous post was not to describe how CreateFileW works but to describe how all Windows operating system functions work. Therefore, we can use this mapping methodology to explore the function call stack for NtQuerySystemInformation, OpenProcess, and ReadProcessMemory, respectively, which we will do below.

NtQuerySystemInformation

The first function called by Mimikatz sekurlsa::logonPasswords is NtQuerySystemInformation. This is a Native API function that resides in ntdll.dll, so it is already relatively low in the function stack. Upon investigating its implementation, it’s found to simply call a syscall by the same name NtQuerySystemInformation.

It’s worth noting that when a syscall is made, it isn’t actually made to a named function. Instead, they are represented by a service index (a number) that is a then resolved into a function via the System Service Descriptor Table (SSDT). Unfortunately, these values change depending on the version of the Operating System or even based on patch state, so it is difficult to refer to them by their syscall number. For simplicity, I will refer to them by their function name. For more information on the details of syscall check out j00ru’s windows-syscalls project.

Additionally, it is worth noting that the same code segment that is pointed to by NtQuerySystemInformation is also pointed to by ZwQuerySystemInformation and RtlGetNativeSystemInformation as shown below (notice the exported entry comments at the top of the image):

NtQuerySystemInformation Implementation

Therefore, it is possible for an attacker, based on our current understanding, to achieve the same outcome using ntdll!NtQuerySystemInformation, ntdll!ZwQuerySystemInformation, ntdll!RtlGetNativeSystemInformation, and the syscall NtQuerySystemInformation. As a result, this graph can be produced to show the possible function call paths.

Initial Process Enumerate Function Graph

OpenProcess

The second function is OpenProcess which is a properly documented Windows API function. Using the Function Call Stack methodology, we can map out the calls made by kernel32!OpenProcess. In doing so, we see that kernel32!OpenProcess follows a relatively similar path to kernel32!CreateFileW, at least in principle. kernel32!OpenProcess calls a version of itself in the api-me-win-core-processthreads-l1–1–2 API Set, which redirects to kernelbase!OpenProcess. Next, kernelbase!OpenProcess calls ntdll!NtOpenProcess, which finishes by making a syscall to a function in the kernel called NtOpenProcess. Similar to what was observed with NtQuerySystemInformation, the code for ntdll!NtOpenProcess can also be reached through a call to ntdll!ZwOpenProcess.

Initial Process Access Function Graph

ReadProcessMemory

The last step for mimikatz is to read the contents of LSASS’s process memory. After all, this is where the credentials actually reside. It might even be said that ReadProcessMemory is the primary function call and that the preceding calls, NtQuerySystemInformation and OpenProcess, were simply used to obtain its prerequisites (a process handle to LSASS).

In this case, we see that kernel32!ReadProcessMemory points to an API Set called api-ms-win-core-memory-l1–1–0, redirecting to kernelbase!ReadProcessMemory. Then we see kernelbase!ReadProcessMemory calls ntdll!NtReadVirtualMemory then makes a syscall to the NtReadVirtualMemory function in the kernel. As is becoming typical, we also see that the code for ntdll!NtReadVirtualMemory is also pointed to by a different exported function called ntdll!ZwReadVirtualMemory.

Initial Process Read Function Graph

Dumpert by Outflank

You might be wondering why this type of analysis is important. John Lambert famously said, “Defenders think in lists. Attackers think in graphs. As long as this is true, attackers win.” One relevant example that proves this point is the tool Dumpert by the team at Outflank. It is prevalent for defenders, including security vendors, to view attack techniques through the functional lens. This means they might observe that Mimikatz follows the functional sequence of ntdll!NtQuerySystemInformation, kernel32!OpenProcess, and kernel32!ReadProcessMemory and build their visibility, detection rules, or preventative controls around this EXACT pattern. This is a problem because they aren’t approaching the problem from the perspective of the graph of possibilities. The team from Outflank did the exact opposite. They understood that kernel32!OpenProcess existed within a function call stack, resulting in a syscall to NtOpenProcess. They also understood that many EDR tools had visibility of kernel32!OpenProcess but not the associated syscall. As a result, they replicated Mimikatz functionality but replaced the kernel32!OpenProcess call with the NtOpenProcess syscall. Additionally, they realized that while kernel32!ReadProcessMemory might be the “orthodox” way to read the contents of a remote process’s memory, there may be less common ways to achieve this same outcome.

With this in mind, they replaced the kernel32!ReadProcessMemory call with a call to dbghelp!MiniDumpWriteDump. These two changes resulted, at least in the short term, in an implementation that achieved the same exact outcome as Mimikatz sekurlsa::logonPasswords while decreasing the likelihood of detection. The resulting function call sequence was syscall!NtQuerySystemInformation -> syscall!NtOpenProcess -> dbghelp!MiniDumpWriteDump as shown below:

Function Calls made by Dumpert

At this point, our function call graphs recognize syscall!NtQuerySystemInformation and syscall!NtOpenProcess, but dbghelp!MiniDumpWriteDump isn’t integrated into our map quite yet. We can use the function call stack analysis approach to understand the calls that dbghelp!MiniDumpWriteDump makes. In doing so, we see that dbghelp!MiniDumpWriteDump calls dbgcore!MiniDumpWriteDump, which calls a couple internal functions before calling kernelbase!ReadProcessMemory. This means that the function call path of dbgcore!MiniDumpWriteDump converges with the function call path of kernel32!ReadProcessMemory, so instead of being two independent paths, they can be combined into one coherent graph as shown below:

Updated Process Read Function Graph

Dumpert shows us that there are numerous ways to execute a given technique and that the variables involved can lead to unforeseen outcomes such as missed detection opportunities or bypasses of preventative controls.

Operations

If we take John Lambert’s advice and begin viewing the problem from a graph perspective, we can see that in almost all cases, numerous functions can be used to achieve any outcome. This realization allows us to create a model that will enable these outcomes to be discussed even though they are abstract concepts. Functions are the tangible layer of the model in the sense that we can interact with functions directly. In some sense, they are concrete ideas that exist, are documented, and can be touched in the context of code. However, it can be useful to view the problem more abstractly in the sense that any combination of functions in the ntdll!NtQuerySystemInformation, kernel32!OpenProcess, and kernel32!ReadProcessMemory function call graphs can be combined to achieve the OS Credential Dumping: LSASS Memory technique. This means that the set of functions that make up each function call graph can be rolled up into one abstract category, which I call the operational layer. With this in mind, we can convert the two function paths for mimikatz sekurlsa::logonPasswords and Dumpert into one operational path that covers both. The NtQuerySystemInformation functional graph is abstracted as Process Enumeration, the OpenProcess functional graph is abstracted as Process Access, and the ReadProcessMemory functional graph is abstracted as Process Read. Therefore, we can say that both tools follow the Process Enumerate -> Process Access -> Process Read Operational Path as shown below:

Initial Operation Graph for OS Credential Dumping: LSASS Memory

The key is that each operation has a corresponding set of functions that can be called to perform the operation. For instance, based on our current understanding, there are 4 Process Enumerate functions, 6 Process Access functions, and 8 Process Read functions. These numbers can be used to calculate the total number of possible functional permutations for the operational path. At the operational level, we know of one path, Process Enumerate -> Process Access -> Process Read, but by multiplying the number of functions within each operation in the sequence (4 x 6 x 8), we find that there are 192 total functional permutations. This is the power of abstraction. We can convert 192 individual sequences of function calls into 1 sequence of operations.

I’ve created an interactive notebook that should help to visualize how these function calls are related. The idea is that for each operation, there are several valid choices of functions to perform the operation.

https://medium.com/media/20c3d81a1bc128aefcda95ffc790ea88/href

Conclusion

We must understand that the sequence isn’t ntdll!NtQuerySystemInformation -> kernel32!OpenProcess -> kernel32!ReadProcessMemory it is Process Enumerate -> Process Access -> Process Read. Just like how the old sequence of kernel32!OpenProcess -> kernel32!VirtualAllocEx -> kernel32!WriteProcessMemory -> kernel32!CreateRemoteThread was never THE sequence for process injection. It was only ONE possible sequence for process injection.

The power of abstraction is that it allows us to summarize the world, but it is essential to not lose sight of the detail. We must maintain the ability to zoom in and out of the layers, increasing and decreasing resolution depending on our task. So far, we’ve only explored two layers of the abstraction, the functional and operational layers, but in time we will explore more. Stay tuned. In the next post, we will explore whether the Process Enumerate -> Process Access -> Process Read operational path is the ONLY operational path possible to achieve the OS Credential Dumping: LSASS Memory technique. If not, we will work to discover what other operational paths exist and demonstrate how we might find them.


On Detection: Tactical to Functional was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.