Suspicious named pipe events 
FalconFriday — Suspicious named pipe events — 0xFF1B
January 14, 2022

Olaf Hartong

TL;DR for blue teams: Attackers use named pipes to conveniently move laterally and mostly bypass detection. This blog post shows a method for detecting anomalous named pipes using Microsoft Defender for Endpoint. This same logic can be applied to Sysmon telemetry.

TL;DR for red teams: Named pipes are and will remain a great way to move laterally. Proper operational security will decide whether you’ll be easily detected or not. Spend time on understanding your Malleable Profiles and start properly masquerading with custom code.

Some time ago I was excited to see that the Microsoft Defender for Endpoint (MDE) team added visibility into the NamedPipe events. Upon introduction, this new event type was not being generated everywhere yet, since somewhere Q3/Q4 of 2021 it should have become available in all tenants and queryable to you.

Named pipes

A named pipe is meant for communication between two or more unrelated processes and can also have bi-directional communication. A named pipe can be accessed much like a file. Win32 SDK functions CreateFile, ReadFile, WriteFile and CloseHandle open, read from, write to, and close a pipe, respectively. A good basic description van be found here.

Microsoft seems to be slowly steering away from the use of named pipes by adding more sandboxing. For their Windows Store apps (which have a more predominant place in Windows 11) they started this behavior in Windows 10, version 1709. Pipes are only supported within an app-container, i.e., from one UWP process to another UWP process that’s part of the same app. Also, named pipes must use the syntax “\\.\pipe\LOCAL\” for the pipe name. However, for non-Windows Store applications there is no such directive yet, so use of named pipes will remain for quite some time.

Named pipes are a very commonly-used way of inter-process communication — and also work great for machine-machine communication. As a result, they are also used by a lot of threat actors, due to the level of stealth for their lateral movement and command and control communication. There is a significant amount of research done on how CobaltStrike operates; most of the references in this article will focus on that. Keep in mind that this is quite a common technique, used by a plethora of tools.

Frequently abused named pipes

One of the valuable sources used during the creation of specific detection rules is a long list of Malleable Profiles created by Michael Haag that contain a set of pipe names that are commonly (re)used.

Malleable Profiles are essentially configuration files that are used, in a domain-specific language, to redefine indicators in the beacon’s communication. These profiles allow, for instance, to configure the sleep time interval and jitter of the agent, but also things like communication headers, user agent, certificates, default spawning sub-process and the pipe name for SMB communication.

There are a lot of repetitive names that will make a nice reference list for detections. A few that particularly stood out to me were the following ones:


The # signs in the Malleable Profiles tell CobaltStrike that it should replace those characters with a random HEX character. However, the rest remains static, which potentially provides several detection opportunities. The fact that each # in the pipe name is replaced with a valid HEX character is already an interesting possible indicator.

Named pipe structures

These above-mentioned mojo pipe names are part of the Chromium framework, which is used by Google Chrome, Microsoft Edge, Microsoft Teams and several other browsers. As you can imagine they are extremely prevalent so they are ideal for an attacker to hide amongst them; since looking at all of them does not make sense at all for a defender. I needed to get a deeper understanding of the structure of these pipe names to see whether it was possible to build some anomaly detections for it, instead of just matching on the IOCs above, which can be easily altered.

The build-up of these long strings can be found in the source code of the Chromium project and is following a standard structure.

Snippet from the Chromium source containing the named pipe creation code.

These pipes start with the mojo name, followed by the ProcessId, then the ThreadId and a random integer string. We can also see the DACL it will set when it creates the pipe — this is outside of the scope of this blog post.

As we’ve learnt earlier, CobaltStrike uses the names as highlighted above for every pipe it creates and only replaces the #-marked characters with HEX characters. So the ProcessId and ThreadId parts of this name will be static and not match the real Ids. Additionally, the random string can never contain the letters A-F since it should be an integer, not a HEX value.

Having learnt this, I started looking at other (very) noisy pipe names that followed a similar structure which could be interesting hiding places for malicious actors. I came up with some interesting candidates:


All of these pipe names have a pattern in common: they start with a name, then a divider dot or dash and they have the ProcessId or ThreadId in there – in some cases both. These are therefore eligible to be added to a detection rule.

During our testing, these events also proved to be sampled like a lot of other events, as we describe in this blog post. Therefore, using the FileOperation field, which states whether the pipe (or file) was created or opened (aka connected), is not advisable in the detection.

The rule

The rule uses two sets of lists: one based on commonly-seen named pipes in combination with CobaltStrike reports, and a second one that has been observed in the earlier-mentioned list of Malleable Profiles and based on our own observations in analyzing malware sandbox telemetry (where there is a set of hex characters placed behind the string by CobaltStrike for randomization). The rest of the rule focuses on the patterns described above, where either the expected ProcessId or ThreadId does not exist, or where there are unexpected characters in the random integer.

Keep in mind there are several best practices to the Kusto language. Especially at larger data volumes this can have a significant impact. For instance, when you’re searching for a symbol or alphanumeric word that is bound by non-alphanumeric characters, or the start or end of a field, use has or in. Using has is significantly faster than contains, startwith or endswith.

Obviously, this rule can be bypassed in several ways. However, it will catch a lot of the commonly-used pipe names and some unobserved ones used by a threat actor trying to blend in. Additionally, a rule can be added that looks for anomalous patterns based on a larger lookup dataset and another option obviously is the process/pipe relationship. This first option is somewhat resource-intensive, so in larger environments this is less preferable, the second one is quite simple to build.

A good strategy in general is to layer detections and look for additional out-of-the- ordinary behavior to catch the more refined actors.

As a red teamer, spend some time investigating other avenues of using named pipes. For instance, improving the spawning logic to mimic the real patterns might make it a lot more realistic. Additional to mimicking the pattern, also injecting into the right processes will help by blending in. For instance, dllhost.exe connecting to a mojo named pipe, which is mostly used by browsers and Teams, stands out way too much.

You can find the rule in the FalconForce GitHub repository.

let timeframe=1h;
let CobaltStrikeDefaults= dynamic([@"msagent_", @"MSSE-", @"postex_", @"status_", @"mypipe-f", @"mypipe-h",@"ntsvcs_",@"scerpc_", @"mojo.5688.8052."]);
let CobaltStrikeMallable= dynamic([@"win_svc", @"ntsvcs", @"scerpc", @"status_", @"SearchTextHarvester", @"DserNamePipe",@"wkssvc_",@"scerpc_", @"spoolss_",@"CatalogChangeListener",@"fullduplex_",@"demoagent_",@"PGMessagePipe",@"MsFteWds",@"postex_ssh_",@"windows.update.manager",@"\f4c3",@"\f53f",@"halfduplex_"]);
| where Timestamp >= ago(timeframe)
| where ActionType == "NamedPipeEvent"
| extend AdditionalFields=parse_json(AdditionalFields)
| extend ThreadId=tostring(AdditionalFields.ThreadId)
| extend PipeName=tostring(AdditionalFields.PipeName)
// creating string based variants of the processIDs for matching several times later
| extend InitiatingPID=tostring(InitiatingProcessId)
| extend InitiatingParentPID=tostring(InitiatingProcessParentId)
// Customer specific whitelist
// End customer specific whitelist
| where PipeName has_any (CobaltStrikeDefaults) or
// mojo is generated by Chrome(ium) browsers and teams and have distinct pattern including the (parent)ProcessId and ThreadId plus a random character string, CobaltStrike generates hex
        (PipeName matches regex @"\\mojo\.\d+\.\d+\." and not(PipeName matches regex @"\\mojo\.\d+\.\d+\.\d+$" or PipeName has InitiatingPID or PipeName has InitiatingParentPID or PipeName has ThreadId)) or
// chrome(ium) browsers sync processes have distinct pattern including the (parent)ProcessId and ThreadId plus a random character string, CobaltStrike generates hex
        (PipeName matches regex @"\\(edge|chrome)\.sync\.\d+\.\d+\." and not(PipeName matches regex @"\\(edge|chrome|edge\.sync|chrome\.sync)\.\d+\.\d+\.\d+$" or PipeName has InitiatingPID or PipeName has InitiatingParentPID or PipeName has ThreadId)) or
// PSHost is generated by PowerShell and has a distinct pattern including the (parent)ProcessId
        (PipeName matches regex @"\\PSHost\.\d+\." and not(PipeName matches regex @"\\PSHost\.\d+\.\d+\." or PipeName has InitiatingPID or PipeName has InitiatingParentPID)) or
// crashpad pipes have a distinct pattern including the ProcessId and a string of upper case characters
        (PipeName matches regex @"\\crashpad_" and not(PipeName matches regex @"\\crashpad_\d+_[A-Z]+" or PipeName has InitiatingPID or PipeName has InitiatingParentPID)) or
// firefox pipes have a distinct pattern including the ProcessId and 1-3 digits which are sequential for each new pipe
        (PipeName matches regex @"\\cubeb-pipe-" and not(PipeName matches regex @"\\cubeb-pipe-\d+_[0-9]{1-3}+" or PipeName has InitiatingPID)) or
// based on a list of public mallable profiles and a suffix that is a random HEX string
        (PipeName has_any (CobaltStrikeMallable) and PipeName matches regex @"[a-fA-F0-9]{2,10}$") or
        (PipeName matches regex @"\\pipe\\[0-9a-f]{7,10}" or PipeName matches regex @"\\pipe\\[0-9a-f]{8}")
view hosted with ❤ by GitHub

Knowledge center

Other articles

FalconHound, attack path management for blue teams

FalconHound, attack path management for blue teams

[dsm_breadcrumbs show_home_icon="off" separator_icon="K||divi||400" admin_label="Supreme Breadcrumbs" _builder_version="4.18.0" _module_preset="default" items_font="||||||||" items_text_color="rgba(255,255,255,0.6)" custom_css_main_element="color:...

Together. Secure. Today.

Stay in the loop and sign up to our newsletter

FalconForce realizes ambitions by working closely with its customers in a methodical manner, improving their security in the digital domain.

Energieweg 3
3542 DZ Utrecht
The Netherlands

FalconForce B.V.
[email protected]
(+31) 85 044 93 34

KVK 76682307
BTW NL860745314B01