May 21 2024 | Jared Atkinson

Behavior vs. Execution Modality


On Detection: Tactical to Functional

Part 12


At Shmoocon 2015, Will Schroeder (Harmj0y) gave a talk titled “I Hunt Sys Admins,” describing how attackers can hunt (or find the location of) system administrators throughout the network. The talk is only 15 minutes long, so I highly recommend you watch it to understand the motivations and implementations of attackers concerning “user hunting.” As you can see in the screenshot below, Harmj0y describes the motivation behind this attack as “determining where high-value users are logged in” to steal their credentials. At the time of his talk, Mimikatz was the primary method of stealing credentials; still, user hunting continues to be necessary for identity snowball attacks regardless of the credential access technique used.

Later, “user hunting” became the foundation of the popular attack path mapping project BloodHound. Initially, BloodHound would provide a map of the environment by understanding how users, computers, groups, logon sessions, and other factors impact access control. Specifically, users’ location within the network is a crucial component because the location is both ephemeral and admin access to a machine implies the ability to take over the identity of any users logged on to that system. If an attacker can find where high value users are located in the network, they can steal their credentials. As described in his talk, account takeover is not limited to Mimikatz. Attackers can take over accounts via many techniques within the Credential Access Tactic category, such as LSASS Credential Dumping, Token Impersonation (not sure why this is not considered a Credential Access Technique), etc.

In his March 2018 blog post, Rohan Vazarkar (CptJesus) wrote about some of the mechanisms SharpHound uses, the data collection component of the BloodHound project, to collect the necessary data for projecting attack paths. In the Session Collection section of the post, he describes how attackers use the NetSessionEnum function to collect this information. In this post, we will analyze the NetSessionEnum function, understand the operation chain associated with the user hunting technique, and examine five tools that implement this behavior. Ultimately, we will explore how changes to how a behavior is executed (execution modality) can impact detection as much as the operation chain (behavior) itself.


In their descriptions of User Hunting, Harmj0y and Rohan mentioned one Windows API function that was particularly interesting: NetSessionEnum. As a part of the Win32 API, you can find the NetSessionEnum documented online. Below is an image of the function’s overview, which describes it as “[it] provides information about sessions established on a server.” It also shows the syntax for how developers can call the function from their application. In his post, Rohan details how SharpHound calls this function and why specific parameters must be specified in certain ways to guarantee the function returns the information necessary for mapping attack paths.

In one of my previous blog posts, “Understanding the Function Call Stack,” I described how the Windows API is an abstraction that sits atop the functionality. Eventually, this post will require a clear understanding of the NetSessionEnum function call stack, so we should check it out now.

This section will walk through the NetSessionEnum implementation to demonstrate how the function call stack was derived. Feel free to skip to the Behavior section if you trust me. 🙂

Finding the Implementing Library

The first step is identifying which dynamic link library (DLL) implements the NetSessionEnum function. The Requirements section of the Microsoft API documentation page provides information about Netapi32.dll, the DLL in the screenshot below.

So when Harmj0y and CptJesus, or frankly most other developers, mention NetSessionEnum, they specifically mean the version of that function implemented within netapi32.dll or written differently netapi32!NetSessionEnum.

The netapi32!NetSessionEnum syntax is a common way to differentiate between functions with the same name implemented in different DLLs. This additional precision provides clarity in discussions when the difference between, say, kernel32!OpenProcess and kernelbase!OpenProcess might matter. The way to interpret it is DLL!Function, where DLL corresponds to the implementing DLL and Function refers to the function name. We will see later in the post that it is necessary to be specific about the function we refer to.

While it is true that the majority of legitimate applications will use this “orthodox” version of the API, we’ve seen a trend among malware to use lower-level undocumented API functions or system calls (syscalls). With that in mind, it is worth understanding the entire function call stack to understand what exactly happens after netapi32!NetSessionEnum is called. To figure this out, we can open netapi32.dll in a disassembler like IDA Pro.


After loading netapi32.dll into our disassembler, we find that the netapi32!NetSessionEnum function is implemented as a “forwarded export” to srvcli.dll.

netapi32!NetSessionEnum implements a “forwarded export” which transparently points execution to srvcli!NetSessionEnum

This means that when an application calls netapi32!NetSessionEnum the call is transparently forwarded to srvcli!NetSessionEnum, so in order to continue our exploration of the function call stack we will need to load srvcli.dll into our disassembler next.


Upon decompiling srvcli!NetSessionEnum, we immediately see a call to the NdrClientCall3 function, which is responsible for facilitating RPC procedure calls. This particular instance calls the NetrSessionEnum (Opnum 12) procedure implemented by the [MS-SRVS] Interface (otherwise known as the Server Service Remote Protocol).

For a detailed description of how to analyze calls to the NdrClientCall3 function, I highly recommend checking out Kai Huang’s Uncovering RPC Servers through Windows API Analysis post. Search for the section titled “A sneak peek into SspiCli!SspipLogonUser and NdrClientCall3,” where he explains how to break down this function’s different parameters and related structures.

srvcli!NetSessionEnum calls NdrClientCall3 which executes the MS-SRVS!NetrSessionEnum RPC Procedure

Using this analysis, we can construct a function call stack that demonstrates the relationship between the three functions (netapi32!NetSessionEnum, srvcli!NetSessionEnum, and ms-srvs!NetrSessionEnum) that our samples called. The nested nature of this relationship allows us to treat all three functions as “functionally equivalent,” meaning that we can ignore the differences between tools implementing one instead of the other since they are minimal.

As a result of this analysis, we have a new function call stack starting with netapi32!NetSessionEnum, flowing through srvcli!NetSessionEnum, and finally, calling the NetrSessionEnum RPC Procedure implemented by MS-SRVS (ms-srvs!NetrSessionEnum). This function call stack implements the Session Enumeration operation as shown below:

Note: I’ve found that the NdrClientCallX functions are best represented as the RPC Procedure that it is executing. In this case, I’ve used ms-srvs!NetrSessionEnum in its place.

In previous examples, we’ve seen that the function call stack ends at the syscall because it represents the transfer of execution from user mode to kernel mode. For this model, we treat what happens in the kernel as a black box because developers cannot simply call functions that reside in the kernel in the same way they call lower-level API functions like those that reside in ntdll.dll. This same concept is applied to RPC procedures because they transfer execution from the RPC client (in this case, srvcli.dll) to the RPC server (srvsvc.dll). Generally speaking, especially regarding user hunting, the use case for Session Enumeration is to do so remotely. As such, the server component can be treated as a black box because it is not possible, as far as I know, for the client to call the same functions as the server component on a remote system.


At this point, we are familiar with the concept of user hunting and its application for red teamers and threat actors. We’ve also become familiar with the Session Enumeration operation that facilitates this behavior. If you’ve reached this point in the series, you may wonder why I’m only talking about a single operation. After all, I thought procedures were chains of operations? Well, in this case, NetSessionEnum is a function that has no dependency on other functions. As a result, Session Enumeration can manifest as a stand-alone operation in a chain, as shown below:

Operation “chain” for User Hunting

Session Enumeration’s stand-alone nature lends itself as a great teaching aid. Since there is only one operation, we can skip any questions regarding which operation is optimal for detecting the procedure. After all, we should be able to figure this out since there is only one option. I think of this much like the approach to teaching mathematics. In algebra class, students learn how to solve single-variate equations (solve for X) before they learn to tackle multivariate equations (solve for X and Y). The authors of the algebra curriculum understand that many of the real-world applications of mathematics are multivariate; however, they also understand that if you cannot solve for X, you likely cannot solve for both X and Y! In our case, a single-operation procedure is equivalent to solving for X. It only gets more difficult as you add more operations to your chain or multiple chains to your detection.

The critical takeaway is understanding that the operation chain represents the behavior we discuss when we say “behavior-based detection.” In this case, the behavior is a single operation. Still, in other cases, the behavior may be a sequence of operations in a chain, as we’ve seen with LSASS Memory Dumping or Process Injection (shown below for reference):

Execution Modalities

At this point, we’ve identified the behavior (i.e., operation chain) that attackers want to execute as part of user hunting. Still, the attacker must weaponize the behavior before they can use it in their operation. Behaviors are conceptual; tools are concrete. Developers implement these ideas in code, which makes them concrete and usable. During this weaponization process, developers have many implementation options. Despite the relative simplicity of this user hunting procedure, there is a relatively wide range of ways to implement the behavior. This section will analyze five tools that implement “user hunting” (the Session Enumeration operation), but each does so uniquely. The fact that developers can implement the same behavior in many ways brings up a distinction I believe is essential for detection engineers to understand. The differences between behavior and weaponization. I describe these differences as “execution modalities.”

Example: Heart Rate Monitoring

An example that I like to use to differentiate behavior from modality is heart rate (HR) monitoring. In today’s society, it is common to encounter several devices that monitor our HR for one reason or another. How often do we stop and think about how this diagnostic is measured? It turns out there are three primary diagnostic modalities used to measure heart rate.


The first mode is the most rudimentary but readily available to all of us. It is an old-fashioned pulse measurement. The measurer takes the pulse by pressing their fingers against an artery of the patient. Generally, the pulse is taken on the wrist (radial pulse) or the neck (carotid pulse). Luke Paine told me EMTs humorously call this a “digital” measurement.


Photoplethysmography (PPG) is the second modality. We typically encounter this modality in consumer electronic devices such as Apple Watches and Oura Rings, as well as the pulse oximeter that hospitals use. This mode uses an LED to illuminate the skin so an optical sensor can measure blood flow changes, which the device interprets as heart rate. Due to the optical sensor, we call this modality “optical.”


The third mode, electrocardiography (ECG/EKG), uses electrodes attached to the skin in different locations to measure the electrical changes caused by each cardiac cycle. We will call this modality “electrical.”

Each approach has its pros and cons but measures the same phenomenon. Numerous considerations, such as necessary precision, activity levels, equipment availability, etc., influence the practitioner’s modality choice. This section will show how to apply the same conceptual approach to attacker tradecraft. However, detection engineers must differentiate between the behavior and the modality used to implement said behavior. We currently conflate these two concepts, leading to numerous detection efficacy mistakes.

Session Enumeration Tools

Now that we’ve established the difference between behavior and modality, I want to look at tools that implement the same behavior but use different modalities. In this section, we will analyze five tools that implement the Session Enumeration operation and thus can be used for user hunting, as Harmj0y described in his talk. Some tools are built into the operating system, some are meant to be administrative tools, and some were constructed explicitly for attackers. Interestingly, while they all ostensibly do the same thing, they are weaponized differently. These differences in weaponization affect execution, detectability, extensibility, interoperability, etc. An important point is that our high-order objective in detection is behavior-based, not modality-based. Therefore, we should be interested in detecting Session Enumeration (behavior-focused), not Session Enumeration in PowerShell (modality-focused). I labeled each tool’s subsection using the tool name [function] (modality) syntax to simplify things. Let’s jump in!

net.exe session [srvcli!NetSessionEnum] (Built-in Console Application)

It is natural to start our analysis with a tool built into the Windows operating system: net.exe. Many of you are familiar with net.exe and its many modules. For instance, you may have used net user /add to add a local user account to a computer. However, one module, net session, allows for the enumeration of user sessions, making it relevant to our analysis.

We can learn how this tool works by opening it in our disassembler. Once our disassembler has loaded and analyzed it, we see that when a user executes net.exe, it calls the CreateProcessW API function to create a new process called net1.exe.

For those unfamiliar, there is a fun story about the creation of net1.exe involving Y2K.

After we open net1.exe, we find a subroutine called session_display that eventually calls the NetSessionEnum function. A glance at the imports table shows that net1.exe calls srvcli!NetSessionEnum. Calling the function in srvcli.dll is interesting because it skips a level in our function call graph (netapi32!NetSessionEnum). However, it is essential to note that the first parameter, servername, which represents the computer on which the function will enumerate sessions, is set to 0. The documentation indicates that when the servername parameter is NULL, shown as 0, it will enumerate sessions on the local computer. Since the NULL value is hardcoded, net(1).exe is not a tool that can facilitate “user hunting.”

NetSess.exe [netapi32!NetSessionEnum] (Third-party Console Application)

The second tool is a third-party binary released by Joeware called NetSess.exe. This tool has been around for a long time and is often referred to by later implementations.

Like net.exe, I threw NetSess.exe into a disassembler to peek under the hood. I eventually found a call to NetSessionEnum, which is the basis of the tool’s functionality. The difference is that NetSess.exe calls the netapi32 version of the function and takes the servername parameter from the command-line. User control of the servername parameter means the tool facilitates the enumeration of sessions on remote systems, meaning that NetSess.exe is the first tool we can use for user hunting!

One drawback to NetSess.exe, however, is that it is a console application. As a result, an attacker must bring this tool with them or download it to the target system before using it. As we progress through our tour of user hunting tools, we will encounter other tool variations that do not require dropping a binary file to disk. This in-memory execution is, of course, valuable to an attacker to avoid standard disk-based AV scanning.

PowerView Get-NetSession [netapi32!NetSessionEnum] (PowerShell)

Our third tool is Harmj0y’s PowerView Get-NetSession function. PowerView was the precursor to BloodHound as we know it. Will wrote the original BloodHound collector as a wrapper around various PowerView functions, including Get-NetSession. As the screenshot below shows, Get-NetSession relies on a call to netapi32!NetSessionEnum. Like NetSess.exe, Get-NetSession can enumerate sessions on remote computers, which means it supports “user hunting.”

When Harmj0y wrote PowerView, PowerShell’s value proposition was that it could execute scripts in memory (without being dropped to disk and thus subject to AV scans). Also, PowerView targeted PowerShell version 2, which did not include many security features we’ve all come to know and love. These features made PowerShell an exciting modality for user hunting and attacker tradecraft in general.

get-netsession [netapi32!NetSessionEnum] (BOF)

TrustedSec provides a Beacon Object File (BOF) called get-netsession as part of their CS-Situational-Awareness-BOF project. I imagine this BOF’s name is a tribute to the PowerView function discussed earlier, but I have not confirmed this. Since we can access the get-netsession source, we can review the code to understand its implementation. Very quickly, we see a call to the netapi32!NetSessionEnum function, so we know that its behavior will be similar to the previous tools we’ve analyzed.

An advantage of BOFs is that they integrate the functionality directly into the agent itself, removing many more superficial detection opportunities. For instance, when executed, it does not create a process (like NetSess.exe or powershell.exe). Detections focused on process names, command-lines, PowerShell ScriptBlocks, etc., will not be relevant for this modality. [ms-srvs!NetrSessionEnum] (Direct RPC Request)

The final tool we will analyze is part of the popular Impacket suite. For those unfamiliar with Impacket, it is a Python framework primarily intended for working with network protocols (e.g., RPC). While the framework supports interaction with many RPC Interfaces, it also includes a set of example scripts to demonstrate its functionality. One such example is, which we will analyze. seems to be a reimplementation of Rob Fuller’s netview.exe, which Harmj0y discussed in his Shmoocon talk. The original tool did much more than simple Session Enumeration; we see the same in Impacket’s implementation. However, we can focus on the Session Enumeration component of the script, shown below in the getSessions function. Here, we see a call to the srvs.hNetrSessionEnum function, Impacket’s implementation of the [MS-SRVS] NetrSessionEnum RPC procedure (ms-srvs!NetrSessionEnum).

According to the NetSessionEnum function call stack we built earlier, the netapi32!NetSessionEnum function eventually calls the ms-srvs!NetrSessionEnum RPC procedure as part of its execution. Additionally, our analysis of the function call stack showed that nothing happens between netapi32!NetSessionEnum and ms-srvs!NetrSessionEnum, so any application that calls the RPC procedure directly, like Impacket, will not skip any observable behavior. However, it tremendously reduces the observable footprint of execution due to a modal change.

An exciting feature Impacket facilitates is the ability to proxy these requests to the target. Proxying allows the implementation of the behavior without the need to run an application on target or call the relevant Win32 API Function (e.g., netapi32!NetSessionEnum). This approach challenges many assumptions in contemporary detection engineering strategies, such as the Implicit Process Create.


When the analytical dust settles, plotting the samples we analyzed on the function call stack is useful. In doing so, we find three samples (NetSess.exe, PowerView Get-NetSession, and BOF get-netsession) called the high-level netapi32!NetSessionEnum function, one sample (net session) called the undocumented srvcli!NetSessionEnum function, and one sample (Impacket called the ms-srvs!NetrSessionEnum RPC procedure directly. The updated graph is below:

Detection engineers, malware analysts, and thread intel analysts must learn to differentiate between behavioral and modal changes. Behavioral changes result in a new or different operation chain. Modal changes focus on how an operation is executed, not how it is changed. Generally speaking, a behavioral change is more substantial than a modal change. Modern endpoint detection and response (EDR) sensors generate events that focus on operations. As a result, changes that do not affect the operation chain (the behavior) should not generally impact behavioral detections. It is, however, critical to consider whether your detection rule is resilient to modal changes. If not, it likely falls in the “signature” category. There’s nothing wrong with using signatures as a part of a comprehensive detection strategy, but we often conflate signatures with behavioral detections.

Modality Masquerading as a Technique

Another thing I want to point out is what I perceive to be an error in the way that ATT&CK categorizes some techniques. All tools analyzed in this post implement T1033 System Owner/User Discovery. We know the behavior because we can observe the operation chain (in this case, just a single operation) they are executing. However, for PowerView’s Get-NetSession, an additional technique also applies, specifically T1059.001 Command and Scripting Interpreter: PowerShell. PowerShell is not a technique; instead, it is an execution modality. The distinction between technique (behavior) and modality is important because a modality, like PowerShell, can be applied to any technique. It also applies to some of the other Command and Scripting Interpreter sub-techniques. For instance, Windows Command Shell. Does NetSess.exe implement T11059.003 Command and Scripting Interpreter Windows Command Shell? I guess so, but that is the least exciting aspect of that sample.

I’m not trying to say that you shouldn’t detect abnormal PowerShell command-lines and ScriptBlock executions like encoded commands and Unmanaged PowerShell. What I’m saying is that this technique is disproportionately common because it is not a behavior like most of the other techniques. It is an execution modality that attackers can apply to most techniques. The problem is that this causes detection rules that over indexes on the PowerShell aspect of, for instance, PowerView’s Get-NetSession function, to miss all of the other equally devastating implementations out there.


Threat actors and red teamers are spoilt for choice while pursuing their objectives. These choices are not limited to which technique they will use to steal credentials or laterally move, but they also include which tool they will use to implement that technique. Depending on their modality, these tools afford the attacker different capabilities. As defenders, we must understand these differences and their implications towards our ability to detect and defend against them.

During our analysis, we saw one tool that is built into the operating system (Built-in Console Application), a tool that had to be dropped to disk (Third-party Console Application), a tool that could run in PowerShell’s memory (PowerShell Script), a tool that runs in the memory of an arbitrary process (Beacon Object File), and a tool that can run via a proxy without ever touching the subject endpoint (Direct RPC Request). It is self-evident that if we want to detect user hunting or Session Enumeration more specifically, we must detect all implementations regardless of their modality.

The truth is that most of us only detect some of the five. We may detect PowerView’s Get-NetSession because we are skeptical of PowerShell code. We may detect net.exe (even though this is not particularly threatening) because it is built-in and has predictable command-line arguments. Do we detect the BOF or the Impacket variation? How about some variation that only calls netapi32!NetSessionEnum, but we have no other known indicators? Have you tested it? In a later post, we will continue building on this knowledge to demonstrate why a single test case does not provide sufficient evidence to answer these questions confidently.

On Detection: Tactical to Functional Series

Behavior vs. Execution Modality was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.