Jan 7 2025 | Jared Atkinson

Part 15: Function Type Categories

Share

On Detection: Tactical to Functional

Seven Ways to View API Functions

Introduction

Welcome back to Part 15 of the On Detection: Tactical to Functional blog series. I wrote this article to serve as a resource for those attempting to create tool graphs to describe the capabilities of the attacker tools or malware samples they encounter. Throughout this series, I’ve analyzed many API functions and summarized my analysis through function call stacks. As I expanded the range of functions I’ve analyzed, I noticed that there is not a one-size-fits-all solution when creating function call stacks.

Overall, I’ve found that there are, at least, seven different types of functions:

  1. Standard Functions (Syscalls)
  2. Sub-operations (NtQueryInformation* or NtSetInformation*)
  3. Remote Procedure Calls (NdrClientCall*)
  4. Local Security Authority Functions (LsaCallAuthenticationPackage)
  5. Driver IOCTLs (DeviceIoControl or NtFsControlFile)
  6. Compound Functions (Multiple operations)
  7. Local Functions

In this article, I work to categorize functions based on their function type. To do so, I will introduce the category and what makes its functions unique, share a sample whose critical function exemplifies the category, provide the sample’s tool graph and explain how it works, and finally break down the specific function call stack that demonstrates the category in question. Ultimately, you should have the toolset to identify the category to which a given function belongs based on a few heuristics I will share along the journey. This knowledge will empower you to better understand and categorize the attacker tools or malware samples you encounter.

This article is broken down into sections that describe and demonstrate the categories. I subdivide each section further into a “category introduction,” where I will explain the category at a high level; the “sample,” where I will introduce the tool or malware sample that we will analyze to learn about the category; the “tool graph,” where I will explain how the sample implements its functionality; and the “function call stack,” where I will focus on the sample’s critical function which exemplifies the category of focus.

While this article follows several relevant articles that examine individual function types in depth, I consider it more of a preamble to such deep dives. Here, I will introduce two or three new function-type categories I have not previously discussed, but I plan to write a subsequent deep dive on each eventually.

1. Standard Function (syscall)

The first and most common category is the so-called standard functions. These functions are standard because they represent the most common construction and serve as excellent starting points for anyone interested in gaining experience building function call stacks. Standard functions typically involve multiple layers of redirection between DLLs but will inevitably pass the caller-supplied arguments to a system call (syscall) for execution in the kernel. If you’ve followed this series, you will be highly familiar with standard functions and know that we use the syscall as an abstraction layer, which I call an operation. As we progress into other categories, we will see that the operation will remain, but we will derive it differently.

Sample

Our standard function sample comes from DGRonpa’s excellent Process_Injection repository. I enjoy this repo because they have created simple proof-of-concept implementations for the Process Injection technique’s diverse set of procedures. If you are just getting started with implementing the analytical process I present in this series, I recommend you check out this project. It demonstrates how slight implementation changes can have a massive impact on the mutual detectability of malware samples. This article will focus on the Shellcode_Injection.cpp sample, which implements the canonical process injection procedure. For those unfamiliar with process injection, it is primarily a defense evacuation technique used by attackers to inject or migrate their malicious code into an otherwise legitimate process, thus making them less susceptible to detection.

Process_Injection/Classical_Injeciton/Shellcode_Injection.cpp at main · DGRonpa/Process_Injection

Tool Graph

Tool Graph: Shellcode_Injection.cpp

We can construct the tool graph by analyzing the source code at the link in the previous section. In doing so, we see that the sample begins by opening a handle to the target process, the process that the attacker intends to inject their code into, by calling the kernel32!OpenProcess function. Next, the code passes the process handle to kernel32!VirtualAllocEx. VirtualAllocEx is a function that allows an application to allocate a buffer in the memory of a different or remote process. The memory buffer will hold the attacker’s shellcode. Next, the sample writes shellcode to the newly allocated buffer via the kernel32!WriteProcessMemory function. Finally, the sample calls kernel32!CreateRemoteThread to execute the shellcode in the target process, thus finalizing the injection routine.

Function Call Stack

Function Call Stack: kernel32!CreateRemoteThread

Based on my analysis of this sample, it appears that kernel32!CreateRemoteThread is the sample’s critical function. I attribute this choice to the fact that CreateRemoteThread is the function that causes the injection. When the sample calls CreateRemoteThread, we can say that process injection has occurred. Let’s take a look at the function call stack for kernel32!CreateRemoteThread. It is worth understanding the general flow we expect in standard functions.

By analyzing the function call stack, we find that the final function, before the Operation, is a system call (syscall) named NtCreateThreadEx. Syscalls, like NtCreateThreadEx, are responsible for transitioning execution from user-mode to kernel-mode. This transition serves as a boundary whereby analysts can treat the kernel-side functionality as opaque, as it is not standard for a user-mode application to interact with kernel code except through very specific windows like syscalls.

2. Sub-Operations

Now that we have built the foundation, we can look at functions that offer a slight variation. Like Standard functions, this next function-type category is also based around a syscall but provides a bit more variation that must be accounted for by analysts. I call this group of functions Sub-Operations. These functions are often difficult to discern at the Win32 API layer but become evident at the Native API or Syscall layers due to a unique naming convention where the Native API and Syscall are referred to by Nt(Query/Set)Information* where the * represents a specific object type such as Process, File, etc.

For more detailed information on functions in the sub-operation category, I recommend reading Part 14 of this series.

Sample

To demonstrate the Sub-Operation category, we will investigate a sample that Jonathan Johnson wrote for our Malware Morphology workshop. This sample, called Sample 1, implements a simple version of Token Impersonation, and we will find that its critical function is a Sub-Operation.

MalwareMorphology/Sample 1/src/Source.cpp at main · jaredcatkinson/MalwareMorphology

I encourage you to check out the full workshop to see how attackers can change their approach when facing different constraints or circumstances. The slides and labs are available in the linked GitHub repository, and a video recording of the workshop lecture is available on NorthSec’s YouTube channel.

Tool Graph

Tool Graph: Malware Morphology Sample 1

Let’s analyze Sample 1’s source code to build the tool graph. This sample starts by calling kernel32!OpenProcess to open a handle to the target process. In this case, the target process is the process from whom the attacker wants to impersonate. Next, it passes the process handle to advapi32!OpenProcessToken and opens a handle to the target process’s primary token. The primary token represents the user context the attacker is after. While the attacker now has access to their target token, Windows does not allow a process to impersonate the primary token of a different process, so Jonny calls advapi32!DuplicateToken to make a fresh copy of the token. Finally, he passes the duplicated token to advapi32!SetThreadToken to execute the impersonation. Finally, Jonny follows good programming practice by cleaning up the handles he opened by calling kernel32!CloseHandle three times. It is important to note that these calls to CloseHandle are purely optional, so it is plausible that a malicious implementation may exclude them.

Function Call Stack

Function Call Stack: advapi32!SetThreadToken

When it comes to Token Impersonation, the moment of impersonation represents the critical point in the function chain. As a result, we can identify advapi32!SetThreadToken as the crucial function in the function chain.

The function call stack, shown below, appears to follow a similar pattern to the Standard Function. However, the implementation diverges when the code reaches kernelbase.dll. In this case, the high-level function, advapi32!SetThreadToken is a wrapper around a more generic low-level function called ntdll!NtSetInformationThread. In Part 14 of this series, I discussed a class of functions called Nt(Set/Query)Information* that facilitate the setting (writing) or querying (reading) of metadata properties for the target object specified by the *, which in this case happens to be a Thread. The caller specifies which specific metadata property to query or set via an information class.” Each object type has a unique set of information classes. You can see in the screenshot below that SetThreadToken specifies information class 5 via the second parameter, which corresponds to ThreadImpersonationToken.

For this category of functions, the information class is critical for properly assigning the operation. If we treated advapi32!SetThreadToken as if it were a Standard Function, we would derive the operation from the syscall, NtSetInformationThread, meaning the operation would be Thread Set. The problem is that the Thread Set operation covers ALL of the Thread’s metadata properties, including properties irrelevant to token impersonation, such as ThreadIoPriority. While it may be interesting to know whether someone changed a thread’s IO priority, that change does not indicate Token Impersonation. All said, there are 41 Thread information classes (metadata properties), and only one is relevant to this particular procedure. Therefore, we require a way to differentiate between Thread Set operations, and the most natural option for doing so is using the information class as an additional indicator. Thus, the resulting operation is Thread Set: ThreadImpersonationToken.

3. Remote Procedure Calls (NdrClientCall4)

Now, we’ve reached the categories I have not yet covered in this series. The first is related to remote procedure call (RPC) procedures. Functions built on RPC Procedures do not result in syscalls, at least not directly. Instead, they implement the client component of the RPC client/server relationship. In the RPC model, the client and server can exist within different processes or even on different systems. Previously, I mentioned that syscalls are responsible for transferring execution from user mode to kernel mode. Similarly, an RPC procedure transfers execution from the client application, the Win32 API function, to the server application. This process is commonly referred to as Interprocess Communication (IPC). Especially in cases where the client and server exist on separate systems, this boundary is at least as robust as the user mode/kernel mode boundary. A key feature of RPC is that the server component generally constrains the set of sub-routines a client can execute on the server. This constraint creates an opportunity to establish our operation abstraction layer.

Sample

For this third class of functions I decided to use a BOF called sc_create that is included in the TrustedSec CS-Remote-OPs-BOF project. Based on the commit history, it appears that it was originally written by Christopher Paschen. The BOF’s name indicates that Christopher reimplemented the functionality of the sc.exe create command. I’ve found that BOFs provide a great starting point for analysts that are trying to get their feet wet in this typeof analysis. Their simplicity can primarily be attributed to the fact that BOF often implement very discrete capabilities which limits the amount of digging that one must do.

CS-Remote-OPs-BOF/src/Remote/sc_create/entry.c at main · trustedsec/CS-Remote-OPs-BOF

Tool Graph

Tool Graph: sc_create BOF

Our analysis of the sc_create BOF found that it first opened a handle to the Service Control Manager (SCM) via the advapi32!OpenSCManagerW function. Next, the BOF creates the target service via the advapi32!CreateServiceW function. While the operator can specify specific service details such as its name, binary path, and start type during creation, some other features must be added after the fact. To make these additional changes, the tool calls the advapi32!ChangeServiceConfig2W function. Finally, we see the tool close the handles to the newly created service and the SCM by calling the advapi32!CloseServiceHandle function twice.

Function Call Stack

Function Call Stack: advapi32!CreateServiceW

Since the technique in this example is Service Creation, it makes sense to choose advapi32!CreateServiceW as the sample’s critical function for investigation.

CreateServiceW is the first function category that does not make a syscall. Technically, it does EVENTUALLY, but there is an important caveat. You see, advapi32!CreateServiceW is an API function wrapper around an RPC Procedure. In the image below, we see the kernelbase!CreateServiceW function call the rpcrt4!NdrClientCall2 function. NdrClientCall2 is one of a set of functions, represented by NdrClientCall*, that RPC clients can use to make requests. Analysis of the NdrClientCall2’s first and second parameters, which is outside the scope of this article, allows us to identify that kernelbase!CreateServiceW invokes the RCreateServiceW procedure from the Microsoft Service Control Manager Remote Protocol (MS-SCMR).

The Service Control Manager (services.exe) implements its API via RPC, allowing other applications to interact with it. Applications can query, create (as we see here), delete, or modify services through the MS-SCMR RPC interface. Now, we need to understand that the relationship between the calling application and the Service Control Manager is a client/server relationship where clients can make local AND remote requests. The remote aspect is why service creation is a powerful lateral movement technique. Given sufficient permissions, attackers can create malicious services on remote systems. The client/server relationship is essential in our analysis because it acts like a boundary where interaction is constrained to a finite set of RPC procedures like RCreateServiceW. This constraint allows for the application of the operation abstraction layer, which is labeled Service Create in this specific case.

Note: This is not to say that it is not valuable to understand what happens on the server side of the interaction because, in some cases, like this one, an understanding of the server-side component can reveal an alternative procedure to achieve the same outcome (lateral movement via service creation). That said, the ability to implement the server-side implementation, especially remotely, has shown in my analysis to be the exception, not the rule. I will write more about this in a future post.

4. LSA Functions (LsaCallAuthenticationPackage)

Next, we encounter a function type category that took me a while to understand, the LSA Function. Eventually, I discovered that LSA Functions are to RPC Procedures as Sub-Operations are to Standard Functions. That is, LSA Functions rely on a specific RPC Procedure called SspirCallRpc, which the Microsoft Security Support Provider Interface (MS-SSPI) implements. MS-SSPI offers an extensible framework for authentication whereby Microsoft and third-party vendors can extend how users authenticate to the system. As a result, all interactions with “Authentication Packages” flow through the same RPC Procedure. However, the Local Security Authority (LSA) determines how to handle each request based on the specified Authentication Package and AP function. These additional variables indicate that it will not be enough to observe the invocation of SspirCallRpc. Instead, more precise monitoring is necessary, but unfortunately, I am not aware of any vendor that supports this level of monitoring as of this writing.

I highly recommend checking out Evan McBroom’s excellent LSA Whisper project to learn more about the Authentication Packages and LSA.

Sample

This category of functions is the one that inspired me to write this post. I struggled to integrate functions that interact with the Local Security Authority (LSA) into my model for an extended period. When I finally understood how they fit, I realized I had incorrectly conceptualized other categories, like RPC functions. This category is essential when considering modern tradecraft, as shown by my colleagues Will Schroeder in his work on Rubeus and Evan McBroom in his work on LSA Whisperer. Many modern workflows manipulate an agent’s identity context through interactions with LSA, so modern detection approaches must understand these underlying mechanisms. With that in mind, we will look at the inner workings of the pass-the-ticket (ptt) argument in many of Rubeus’ commands.

Rubeus/Rubeus/lib/LSA.cs at master · GhostPack/Rubeus

Tool Graph

Tool Graph: Rubeus ptt

At this point, each sample we have investigated supported one use case. Rubeus is a full-featured tool suite that supports many Kerberos-related attacks. The pass-the-ticket feature assumes the operator has a Kerberos ticket and facilitates adding that ticket to the ticket cache of the specified logon session.

The tool graph below shows that Rubeus begins by connecting to the Local Security Authority (LSA) server using the secur32!LsaConnectUntrusted function. This results in an LSA handle that the tool uses in subsequent calls. Next, the application finds the Kerberos authentication package (AP) by calling the secur32!LsaLookupAuthenticationPackage function. Rubeus then calls secur32!LsaCallAuthenticationPackage specifying the Kerberos authentication package’s KerbSubmitTicket function. The KerbSubmitTicket AP function adds the supplied ticket to the specified logon session’s ticket cache. Finally, Rubeus releases the LSA handle by calling secur32!LsaDeregisterLogonProcess.

Function Call Stack

Function Call Stack: secur32!LsaCallAuthenticationPackage (Kerberos KerbSubmitTicket)

The secur32!LsaCallAuthenticationPackage function is responsible for “passing-the-ticket” in the sense that it is the function that adds the ticket to the ticket cache. For that reason, let’s dig a bit deeper into it to understand how it works.

When we open secur32!LsaCallAuthenticationPackage in our disassembler, we see that it eventually makes an RPC Procedure call to the SspirCallRpc procedure implemented by the Microsoft Security Support Provider Interface (MS-SSPI). You might immediately think that I’m crazy because we just covered RPC Procedures in the previous section, but it turns out that this particular case is more complicated than that. That’s because there is an additional layer or two of abstraction. The SspirCallRpc procedure acts as a gateway for applications to interact with LSA regardless of the authentication package and function that the caller specifies. Behind the scenes, MS-SSPI (loaded in lsass.exe) handles these calls and passes execution to the appropriate authentication package, such as kerberos.dll. As a result, observing a call to the SspirCallRpc procedure only indicates that some authentication-related operation has occurred. However, it does not provide the necessary level of detail to determine whether the operation is the one you are interested in, such as adding a Kerberos ticket to the ticket cache, in this case via KerbSubmitTicket. Like Sub-Operations, this category requires that we operate at a more granular level if we can hope to use this signal in any meaningful detection strategy.

It is also worth noting that these LSA-related operations function based on the same client/server relationship as RPC but are often constrained to the local machine only. The lsass.exe application runs as a protected process on modern operating systems, so LsaCallAuthenticationPackage or SspirCallRpc, more specifically, acts as an interface for applications to interact with LSA. This constraint allows us, again, to treat the LSA side of this interaction opaquely. I’ve found it helpful to label the resulting operation using the AP function, which in this example is KerbSubmitTicket.

5. Driver IOCTLs

Another important category I have not extensively written about is Driver IOCTLs. These functions rely on making specific Input/Output Control calls to kernel drivers via API functions like kernel32!DeviceIOControl and ntdll!NtfsControlFile. IOCTLs facilitate direct interaction with a kernel driver and represent something between a Syscall and an RPC Procedure. In this case, we treat the transfer of execution from the client application to the kernel driver as the boundary for abstraction.

Sample

To demonstrate Driver IOCTLs in practice, I will use Matt Graeber’s Get-System implementation from the PowerSploit project. Get-System provides two approaches to Token Theft. The first follows the traditional Token Impersonation approach, while the second relies on a trick with named pipes. For this analysis, I will focus on the named pipe approach.

PowerSploit/Privesc/Get-System.ps1 at d943001a7defb5e0d1657085a77a0e78609be58f · PowerShellMafia/PowerSploit

Thanks to Jonathan Johnson for helping to identify a function that embodies this category. He pointed me to one of his blog posts focused on advapi32!ImpersonateNamedPipeClient, which inspired me to find a sample that leverages that function. If you want to understand how I constructed the function call stack, I recommend you read his post.

Exploring Impersonation through the Named Pipe Filesystem Driver

Tool Graph

Tool Graph: PowerSploit Get-System

After consulting the source code for our sample, I found that the named pipe impersonation trick is a bit complicated. It starts with a call to advapi32!CreatePipe, which generates a new pipe with a fairly permissive DACL. Next, we interact with the Service Control Manager (SCM) via advapi32!OpenSCManagerW. This call results in a handle to the local SCM. Matt passes the SCM handle to advapi32!CreateServiceW to create a new service that the attack will eventually use to connect to the named pipe. Services can run as the NT AUTHORITYSYSTEM user, resulting in the “named pipe client” being SYSTEM. We then see a call to advapi32!CloseServiceHandle to close the service handle. Next, Get-System calls advapi32!OpenServiceW to open a new handle to the service. The service handle is passed to advapi32!StartServiceW to execute the service, thus connecting to the named pipe. After the service is executed, the sample is cleaned up via advapi32!DeleteService and two calls to advapi32!CloseServiceHandle. Finally, advapi32!ImpersonateNamedPipeClient is called to steal the SYSTEM account token.

Function Call Stack

Function Call Stack: advapi32!ImpersonateNamedPipeClient

As mentioned earlier, I selected this sample to highlight the advapi32!ImpersonateNamedPipeClient function.

Like many other functions, ImpersonateNamedPipeClient flows to kernelbase.dll, where it makes a call to a Native API function called ntdll!NtFsControlFile. The Microsoft documentation for the NtFsControlFile function states that “[it] sends a control code directly to a specified file system or file system filter driver, causing the corresponding driver to perform the specified action.” We can see from the arguments passed to NtFsControlFile that the handle is to the specified named pipe, which, in the case of Matt’s Get-System implementation, is the dummy named pipe created via the CreatePipe function call. We also see that kernelbase!ImpersonateNamePipeClient hardcodes the sixth parameter, representing the FsControlCode, to 0x11001C.

Implementation of kernelbase!ImpersonateNamedPipeClient

The FsControlCode tells the driver which sub-routine to execute. In his blog post analyzing the function, Jonny explains that this control code is known as FSCTL_PIPE_IMPERSONATE and that the components of the control code indicate that this is function number 7 for named pipes (FILE_DEVICE_NAMED_PIPE). We can, therefore, use the control code as the abstraction layer for our operation, which we will call Pipe Impersonate.

6. Compound Functions

A special category is the Compound Function. A Compound Function is a function that, at the Win32 API level, appears as a single function but, lower in the function call stack, breaks down into many more specific functions and, thus, operations. We typically find that Compound Functions implement logic to execute an everyday routine that would normally require multiple individual function calls. A great example of a compound function is dbghelp!MiniDumpWriteDump, which creates process crash dumps. It is built on top of kernel32!ReadProcessMemory. However, a crash dump requires numerous specific memory reads to occur. MiniDumpWriteDump implements this logic, so the application developer is not required to understand the details of the crash dump or process memory structures.

Sample

We will look at Justin Bui’s Primary Token Theft sample for this category. Justin wrote this program to evaluate which processes would be the best targets for System Token Theft. It is a relatively simple program, but it allows us to see a third implementation of Token Impersonation. I challenge you to analyze this sample’s operation chain and compare it to the sample we used to dig into Sub-Operations.

PrimaryTokenTheft/main.cpp at master · slyd0g/PrimaryTokenTheft

Tool Graph

Tool Graph: PrimaryTokenTheft

The tool graph for the PrimaryTokenTheft sample is relatively straightforward compared to some of our other examples. In all, we see that there were three function calls kernel32!OpenProcess, advapi32!OpenProcessToken, and advapi32!ImpersonateLoggedOnUser. The first function, kernel32!OpenProcess is used to open a handle to the target process. This process typically runs in the context of a user with desirable access, such as the NT AUTHORITY/SYSTEM account or a Domain Administrator account. The resulting handle is then passed to advapi32!OpenProcessToken, which opens a handle to the desired access token. Finally, Justin passes the token handle to advapi32!ImpersonateLoggedOnUser, where all the magic happens.

Function Call Stack

Function Call Stack: advapi32!ImpersonateLoggedOnUser

Digging into the advapi32!ImpersonateLoggedOnUser function’s call stack, everything progresses normally, like a standard function, until we reach kernelbase.dll. As shown in the image below, we see calls to ntdll!NtQueryInformationToken, ntdll!NtDuplicateToken, ntdll!NtSetInformationThread, and ntdll!NtClose.

Implementation of kernelbase!ImpersonateLoggedOnUser

As a result, the function call stack splits at kernelbase!ImpersonateLoggedOnUser. The end effect is that rather than a one-to-one relationship between the function and the operation, we find that advapi32!ImpersonateLoggedOnUser has a one-to-four relationship, which has massive implications for our detection engineering efforts. Each underlying function will fit within one of the categories covered in this article. For example, NtQueryInformationThread and NtSetInformationThread belong to the Sub-Operation category, while NtDuplicateToken and NtClose belong to the Standard Function category.

You will notice that the resulting operation chain for this particular sample is not much different from that of the sample used in the Sub-Operation section. This similarity means that these two samples are likely to be mutually detectable, or, put another way, they can both be detected using the same analytic.

7. Local Functions (memcpy or GetCurrentProcess)

The final category is probably the least important from the detection engineering perspective. However, understanding how to spot functions from this set will empower you to separate the wheat from the chaff. Local Functions is the name I’ve given to the functions that never cross a relevant boundary. They often, if not always, are resolved within the initial implementation DLL and perform local tasks. You will find that as you begin to analyze complicated functions, you will encounter many local functions, so you must understand how to spot these functions so you can be sure that you can safely ignore them.

Sample

In this example, we will rely on the Get-ProcessTokenPrivilege PowerShell function from Will Schroeder’s PowerUp module. This function is a helper function that implements something similar to whoami /priv.

PowerSploit/Privesc/PowerUp.ps1 at master · PowerShellMafia/PowerSploit

Tool Graph

Tool Graph: Get-ProcessTokenPrivilege

The tool graph shows that this PowerShell function is meant to check which privileges are enabled for the current user context. We see kernel32!GetCurrentProcess emits a handle to the calling process. That handle is then passed to advapi32!OpenProcessToken, which returns a handle to the calling process’s primary token. Finally, the advapi32!GetTokenInformation function is called to return a list of the token’s enabled privileges.

Note: Can you identify which categories advapi32!OpenProcessToken and advapi32!GetTokenInformation fit into?

Function Call Stack

Function Call Stack: kernel32!GetCurrentProcess

In this sample, the kernel32!GetCurrentProcess function represents the Local Function category. After opening kernel32.dll in IDA, we can browse to the implementation of the GetCurrentProcess function. We see that this function does not do much. It returns -1, which functions as a pseudo-handle for the calling process.

Implementation of kernel32!GetCurrentProcess

As a result, the function call stack is essentially non-existent, as shown below. Since kernel32!GetCurrentProcess does not cross a client/server boundary like that of user-mode/kernel-mode, RPC client/server, or LSA client/auth package it does not have a corresponding operation.

Conclusion

For as long as I can remember, we have used API functions to describe a particular malware sample’s functionality. These function chains have proven helpful to Detection Engineers as they provide a blueprint for observation. However, not all functions are created equally. In our analysis, we’ve identified seven function categories, each requiring a different analytical approach. I hope the descriptions in this article help you determine which type of function you have encountered so you can ensure you are collecting data with an adequate level of granularity.


Part 15: Function Type Categories was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.