At FourCore, we are building a security control validation platform, FourCore ATTACK. We have developed real-world cyber attack simulations, accurately mimicking an attacker's behaviour. This is enabled by our agents for Windows and Linux developed in Go. These are lightweight agents which connect to our SaaS platform to deliver attack simulations.
This article discusses the challenges we faced while developing the Windows agent around accessing, manipulating, and utilizing the different types of Windows Tokens via Go.
We have also open-sourced wintoken, A Go package to steal Windows tokens, enable/disable token privileges and grab interactive and linked tokens.
When a threat actor compromises a machine, the permissions he gets significantly impact how successful the attack would be. For our specific use-case at FourCore, running as a single user chosen during install time will limit us, and we will have a very tunneled view of security gaps. Therefore, it is essential to run attack simulations as different users, to understand how the impact of an attacker can vary between users.
The FourCore ATTACK Windows agent is available as a pre-configured MSI to the customer and installed as a service on the customer's endpoint to be targeted for attack simulations via the platform. We run as a Windows service which allows performing actions as different users depending on the simulation's requirements.
Users are tricky; they can be local admins or standard users and be logged in or signed out. We cannot and do not ask for or store user account passwords and don't know when they might turn their system off. We cannot depend on the user for our simulations.
Go supports Windows API with the x/sys/windows sub-repository but not all Windows APIs are directly available. Microsoft is slowly changing this with their win32metadata project, but there still is a long way to go. As we will discuss in the next few sections, we use various Win32 APIs via Go to perform attack simulations as different users on Windows depending on the simulation's requirements.
Let's take a little first to introduce the concept of Windows access tokens and how they dictate what we can or cannot do on the system.
Tokens on Windows are immutable structures stored in the Kernel, containing identity and privilege information. An all-powerful system token is created when the computer starts, but for user activities, you get an access token after you log in. Also, whenever any local admin logs in, there are two tokens, one for unelevated uses, aka filtered admin token and the other for elevated tasks, aka admin token. These tokens are bound to one another and are known as Linked Tokens.
There are two types of access tokens, Primary Token and Impersonation Token. Primary Tokens are called process tokens, as primary tokens can only be attached to processes. Impersonation tokens are called thread tokens, as impersonation tokens can only be attached to threads. Although these tokens can be freely converted to each other using the DuplicateTokenEx API.
Any program started by the user has the access token associated with it and is known as the Primary Access Token. By default, child processes inherit their Parent's token and privileges. When you log in with an appropriate password, winlogon.exe, with some magic, starts explorer.exe with a proper token, and you finally land on your desktop with the privileges provided to you by your organisation.
Even though our agent runs as a Windows service, and we have system-level access, running with the correct user privileges becomes a difficult task. For example, do we steal the elevated or filtered token if the interactive user is a local admin? Should we even steal the token or create a new one using an API such as LogonUser? Would token stealing be considered a malicious activity?
Before you decide to steal a token from explorer.exe
, let me tell you that as a Service on Windows, you have access to this great set of APIs available to fetch the interactive token of the user currently logged into the system.
You can enumerate (WTSEnumerateSessionsA) to fetch the active session (WTSGetActiveConsoleSessionId) on the system, query the token (WTSQueryUserToken) associated with that session, and finally duplicate that token so that you can start a new process (CreateProcessAsUserA) as that interactive user. If the user is a local admin and you want to run in an elevated manner, you can also get the linked token (TOKEN_LINKED_TOKEN) using the Windows API.
You will be amused to know that some APIs have a bit of weird behaviour, such as OpenProcessToken's API docs mention that you need PROCESS_QUERY_INFORMATION in desired access, but it also works with PROCESS_QUERY_LIMITED_INFORMATION.
Each token has some associated privileges, which dictate what actions the user of the token can perform. Since a token is an immutable kernel object, you cannot add or remove privileges, only enable or disable them using the AdjustTokenPrivileges API. All the privileges associated with a token also have a minimum integrity level needed to enable those privileges. For example, without running with High Integrity, you cannot enable SeDebugPrivilege. This is an additional security measure taken by UAC designers to prevent access to securable objects.
Tokens have integrity levels associated with them. Integrity Levels are a way of controlling access to securable objects and are evaluated before other access policies (DACLs). For example, a process running with an integrity level set to Low cannot interact with objects that have a medium integrity level. You can think of the integrity Level as a broad classification of securable objects. Some groups of objects(placed in high integrity) need to be more secure than other groups of objects(placed in medium integrity). Windows defines four integrity levels: Low, Medium, High, and System. A standard user has a medium integrity level, and elevated users have a high integrity level. Windows takes steps that an object with low integrity cannot access objects with high integrity.
There are some Privileges known as Super Privileges, which, if present in a token, would mean that the token is an elevated token. Some examples of these Super Privileges are SeCreateTokenPrivilege
, SeTcbPrivilege
, SeTakeOwnershipPrivilege
, SeLoadDriverPrivilege
, SeDebugPrivilege
, SeBackupPrivilege
, etc.
Let's join all of these together for a quick recap in a little step-by-step.
Manipulating Windows APIs to simulate advanced real-world threats is core to our platform. Stealing tokens, impersonating users, enabling privileges, etc is core to how our Windows agent and attack simulations work. We built the wintoken package for our own use-cases to simplify working with tokens.
The package should help you abstract away many pain points we faced with Windows Tokens during development. The package exposes functions to steal tokens, enable/disable privileges, and grab interactive and linked tokens. It's licensed under the permissive MIT License and you can freely use it in your own projects.
Here are a few examples on how to use wintoken:
1package main 2 3import ( 4 "os/exec" 5 "syscall" 6 7 "github.com/fourcorelabs/wintoken" 8) 9 10func main() { 11 token, err := wintoken.OpenProcessToken(1234, wintoken.TokenPrimary) //pass 0 for own process 12 if err != nil { 13 panic(err) 14 } 15 defer token.Close() 16 17 //Now you can use the token anywhere you would like 18 cmd := exec.Command("/path/to/binary") 19 cmd.SysProcAttr = &syscall.SysProcAttr{Token: syscall.Token(token.Token())} 20}
1package main 2 3import ( 4 "os/exec" 5 "syscall" 6 7 "github.com/fourcorelabs/wintoken" 8) 9 10func main() { 11 //You can get an interactive token(if you are running as a service) 12 //and specify that you want the linked token(elevated) in the same line 13 token, err := wintoken.GetInteractiveToken(wintoken.TokenLinked) 14 if err != nil { 15 panic(err) 16 } 17 defer token.Close() 18 19 //Now you can use the token anywhere you would like 20 cmd := exec.Command("/path/to/binary") 21 cmd.SysProcAttr = &syscall.SysProcAttr{Token: syscall.Token(token.Token())} 22}
1package main 2 3import ( 4 "fmt" 5 6 "github.com/fourcorelabs/wintoken" 7) 8 9func main() { 10 token, err := wintoken.OpenProcessToken(1234, wintoken.TokenPrimary) 11 if err != nil { 12 panic(err) 13 } 14 defer token.Close() 15 16 fmt.Println(token.GetPrivileges()) 17 fmt.Println(token.GetIntegrityLevel()) 18 fmt.Println(token.UserDetails()) 19}
1package main 2 3import( 4 "github.com/fourcorelabs/wintoken" 5) 6 7func main(){ 8 token, err := wintoken.OpenProcessToken(1234, wintoken.TokenPrimary) 9 if err != nil { 10 panic(err) 11 } 12 //Enable, Disable, or Remove privileges in one line 13 token.EnableAllPrivileges() 14 token.DisableTokenPrivileges([]string{"SeShutdownPrivilege", "SeTimeZonePrivilege"}) 15 token.RemoveTokenPrivilege("SeUndockPrivilege") 16}
Understanding Windows Tokens is essential whether you are from a Red Team or Blue Team or just developing software that would run on windows. It is not a small topic, and this blog introduced little concepts and a library to simplify some token manipulation tasks. I have linked references from which you can gain a deeper understanding of Windows tokens and their critical nature.