Koh is a C# and Beacon Object File (BOF) toolset that allows for the capture of user credential material via purposeful token/logon session leakage.
Some code was inspired by Elad Shamir’s Internal-Monologue project (no license), as well as KB180548. For why this is possible and Koh’s approeach, see the Technical Background section of this README.
For a deeper explanation of the motivation behind Koh and its approach, see the Koh: The Token Stealer post.
@harmj0y is the primary author of this code base. @tifkin_ helped with the approach, BOF implementation, and some token mechanics.
The Koh “server” captures tokens and uses named pipes for control/communication. This can be wrapped in Donut and injected into any high-integrity SYSTEM process (see The Inline Shenanigans Bug).
We are not planning on releasing binaries for Koh, so you will have to compile yourself 🙂
Koh has been built against .NET 4.7.2 and is compatible with Visual Studio 2019 Community Edition. Simply open up the project .sln, choose “Release”, and build. The Koh.exe
assembly and Koh.bin
Donut-built PIC will be output to the main directory. The Donut blob is both x86/x64 compatible, and is built with the following options using v0.9.3 of Donut at ./Misc/Donut.exe
:
[ Instance type : Embedded
[ Entropy : Random names + Encryption
[ Compressed : Xpress Huffman
[ File type : .NET EXE
[ Parameters : capture
[ Target CPU : x86+amd64
[ AMSI/WDLP : abort
Koh.exe Koh.exe <list | monitor | capture> [GroupSID... GroupSID2 ...]
Group SIDs can be supplied command line as well, causing Koh to monitor/capture only logon sessions that contain the specified group SIDs in their negotiated token information.
The current usable client is a Beacon Object File at .\Clients\BOF\
. Load the .\Clients\BOF\KohClient.cna
aggressor script in your Cobalt Strike client to enable BOF control of the Koh server. The only requirement for using captured tokens is SeImpersonatePrivilege. The communication named pipe has an “Everyone” DACL but uses a basic shared password (super securez).
To compile fresh on Linux using Mingw, see the .\Clients\BOF\build.sh
script. The only requirement (on Debian at least) should be apt-get install gcc-mingw-w64
beacon> help koh
koh list – lists captured tokens
koh groups LUID – lists the group SIDs for a captured token
koh filter list – lists the group SIDs used for capture filtering
koh filter add SID – adds a group SID for capture filtering
koh filter remove SID – removes a group SID from capture filtering
koh filter reset – resets the SID group capture filter
koh impersonate LUID – impersonates the captured token with the give LUID
koh release all – releases all captured tokens
koh release LUID – releases the captured token for the specified LUID
koh exit – signals the Koh server to exit
The koh filter add S-1-5-21-<DOMAIN>-<RID>
command will only capture tokens that contain the supplied group SID. This command can be run multiple times to add additional SIDs for capture. This can help prevent possible stability issues due to a large number of token leaks.
When a new logon session is estabslished on a system, a new token for the logon session is created by LSASS using the NtCreateToken() API call and returned by the caller of LsaLogonUser(). This increases the ReferenceCount field of the logon session kernel structure. When this ReferenceCount reaches 0, the logon session is destroyed. Because of the information described in the Why This Is Possible section, Windows systems will NOT release a logon session if a token handle still exists to it (and therefore the reference count != 0).
So if we can get a handle to a newly created logon session via a token, we can keep that logon session open and later impersonate that token to utilize any cached credentials it contains.
Why This Is Possible
MS16-111 was applied back to Windows 7/Server 2008, so this approach should be effective for everything except Server 2003 systems.
Enumerating logon sessions is easy (from an elevated context) through the use of the LsaEnumerateLogonSessions() Win32 API. What is more difficult is taking a specific logon session identifier (LUID) and somehow getting a usable token linked to that session.
We brainstormed a few ways to a) hold open logon sessions and b) abuse this for token impersonation/use of cached credentials.
The SSPI AcquireCredentialsHandle() call has a pvLogonID field which states:
A pointer to a locally unique identifier (LUID) that identifies the user. This parameter is provided for file-system processes such as network redirectors.
Note: In order to utilize a logon session LUID with AcquireCredentialsHandle() you need SeTcbPrivilege, however this is usually easier to get than SeCreateTokenPrivilege.
Using this call while specifying a logon session ID/LUID appears to increase the ReferenceCount for the logon session structure, preventing it from being released. However, we’re not presented with another problem: given a “leaked”/held open logon session, how do we get a usable token from it? WTSQueryUserToken() only works with desktop sessions, and there’s no userland API that we could find that lets you map a LUID to a usable token.
However we can use two additional SSPI functions, InitializeSecurityContext() and AcceptSecurityContext() to act as client and server to ourselves, negotiating a new security context that we can then use with QuerySecurityContextToken() to get a usable token. This was documented in KB180548 (mirrored by PKISolutions here) for the purposes of credential validation. This is a similar approach to Internal-Monologue, except we are completing the entire handshake process, producing a token, and then holding that for later use.
Filtering can then be done on the token itself, via CheckTokenMembership() or GetTokenInformation(). For example, we could release any tokens except for ones belonging to domain admins, or specific groups we want to target.
I’ve been coding for a decent amount of time. This is one of the weirder and frustrating-to-track-down bugs I’ve hit in a while – please help me with this lol.
execute-assembly
from an elevated (but non-SYSTEM) context, everything works properly.SEC_E_NO_CREDENTIALS
and everything fails ¯\_(ツ)_/¯We have tried (with no success):
For all intents and purposes, the thread context right before the call to AcquireCredentialsHandle works in this context, but the result errors out. And we have no idea why.
If you have an idea of what this might be, please let us know! And if you want to try playing around with a simpler assembly, check out the AcquireCredentialsHandle repo on my GitHub for troubleshooting.
To quote @tifkin_ “Everything is stealthy until someone is looking for it.” While Koh’s approach is slightly different than others, there are still IOCs that can be used to detect it.
The unique TypeLib GUID for the C# Koh collector is 4d5350c8-7f8c-47cf-8cde-c752018af17e
as detailed in the Koh.yar Yara rule in this repo. If this is not changed on compilation, it should be a very high fidelity indicator of the Koh server.
When the Koh server starts is opens up a named pipe called \\.\pipe\imposecost
that stays open as long as Koh is running. The default password used for Koh communication is password
, so sending password list
to any \\.\pipe\imposecost
pipe will let you confirm if Koh is indeed running. The default impersonation pipe used is \\.pipe\imposingcost
.
If Koh starts in an elevated context but not as SYSTEM, a handle/token clone of winlogon
is performed to perform a getsystem
type elevation.
I’m sure that no attackers will change the indicators mentioned above.
There are likely some RPC artifacts for the token capture that we’re hoping to investigate. We will update this section of the README if we find any additional detection artifacts along these lines. Hooking of some of the possibly-uncommon APIs used by Koh (LsaEnumerateLogonSessions or the specific AcquireCredentialsHandle/InitializeSecurityContext/AcceptSecurityContext, specifically using a LUID in AcquireCredentialsHandle) could be explored for effectiveness, but alas, I am not an EDR.
After publishing the Koh: The Token Stealer post, I had a great exchange between @cnotin and @SteveSyfuhs about what ended up being a partial mitigation for this approach.
The KB2871997 patch introduced a TokenLeakDetectDelaySecs
setting, which triggers the “…clearing of any credentials of logged off users…“. By default in fact, members of the “Protected Users Security Group” have this behavior enforced regardless of the registry setting. However, setting this to a non-zero value will clear ALL credentials out of memory when a user logs off. Specifically, as Steve mentions: If set, it'll start a timer on a sessions *interactive* logoff event, and on fire will purge anything still tied to it. Off by default. Protected Users always on, with a default of 30s.
There are two important things to note in the above paragraph: “logoff event” and “interactive”. This can result in some situations where a user’s credential is NOT cleared:
runas
or runas /netonly
type spawn or something similar, there is no logoff event when the process stops and the credential/token can still be captured.(I need to test other logon situations like NetworkClearText.)
However, if the user is in the “Protected Users Security Group” or TokenLeakDetectDelaySecs
is non-zero, and the user actively logs off of an interactive or remote interactive (RDP) session, the credentials will be cleared. I need to program Koh to better deal with these specific types of situations.
TL;DR you should really be using the “Protected Users Security Group” for sensitive users, and see if setting TokenLeakDetectDelaySecs
to a value like 30 is doable in your environment.
Kali Linux 2024.4, the final release of 2024, brings a wide range of updates and…
This Go program applies a lifetime patch to PowerShell to disable ETW (Event Tracing for…
GPOHunter is a comprehensive tool designed to analyze and identify security misconfigurations in Active Directory…
Across small-to-medium enterprises (SMEs) and managed service providers (MSPs), the top priority for cybersecurity leaders…
The free and open-source security platform SecHub, provides a central API to test software with…
Don't worry if there are any bugs in the tool, we will try to fix…