Security Identifiers
SID structure, well-known SIDs, relative versus domain SIDs, and how SID analysis exposes token abuse and lateral movement
A Windows event log entry shows process creation with a creator SID of S-1-5-18. That's the SYSTEM account SID. A process running as SYSTEM spawned a child with a different token — maybe impersonation is in play, or something created a process using a stolen token. SID recognition is the first step: knowing that S-1-5-18 is SYSTEM, S-1-5-19 is Local Service, and S-1-5-20 is Network Service lets you instantly characterize any logged event without looking up the account name.
SID Structure
A Security Identifier (SID) uniquely identifies a security principal: a user account, group, computer account, or built-in entity. It's a variable-length binary structure with a textual form: S-R-I-SA₁-SA₂-...-SAₙ.
SID binary structure:
Byte 0: Revision (always 1)
Byte 1: SubAuthorityCount (N, up to 15)
Bytes 2-7: IdentifierAuthority (6 bytes, big-endian)
Bytes 8+: SubAuthority[0..N-1] (each 4 bytes, LE DWORD)
Text form: S-{Revision}-{IdentifierAuthority}-{SA0}-{SA1}-...
Example:
S-1-5-21-1234567890-987654321-1122334455-1001
| | | domain sub-authorities | RID
| | NT Authority (IdentifierAuthority = 5)
| Revision = 1
S = SID prefix
| Field | Size | Meaning |
|---|---|---|
| Revision | 1 byte | Always 1 |
| SubAuthorityCount | 1 byte | Number of SubAuthority DWORDs that follow (0–15) |
| IdentifierAuthority | 6 bytes | Top-level authority issuing the SID (most common: 5 = NT Authority) |
| SubAuthority[i] | 4 bytes each | Progressive narrowing of the principal; last one is the RID (Relative Identifier) |
Well-Known SIDs
| SID | Name | Notes |
|---|---|---|
| S-1-0-0 | Null SID | No authority |
| S-1-1-0 | Everyone | All users including anonymous |
| S-1-2-0 | Local | Users logged on locally |
| S-1-3-0 | Creator Owner | Placeholder in inheritable ACEs |
| S-1-5-1 | Dialup | |
| S-1-5-2 | Network | Users logged on over network |
| S-1-5-4 | Interactive | Users at keyboard/interactive session |
| S-1-5-6 | Service | Logged on as service |
| S-1-5-7 | Anonymous | Anonymous logon |
| S-1-5-11 | Authenticated Users | Any authenticated user |
| S-1-5-12 | Restricted Code | Software restriction policies |
| S-1-5-18 | SYSTEM | Local System account |
| S-1-5-19 | Local Service | Network access as anonymous |
| S-1-5-20 | Network Service | Network access as computer account |
| S-1-5-32-544 | BUILTIN\Administrators | Local Admins group |
| S-1-5-32-545 | BUILTIN\Users | Local Users group |
| S-1-5-32-547 | BUILTIN\Power Users | Legacy, limited privileges |
| S-1-5-21-...-500 | Local Administrator | Built-in administrator; last SA = RID 500 |
| S-1-5-21-...-501 | Guest | RID 501 |
| S-1-16-0 | Untrusted Mandatory Level | Integrity level SID |
| S-1-16-4096 | Low Mandatory Level | 0x1000 |
| S-1-16-8192 | Medium Mandatory Level | 0x2000 — standard user |
| S-1-16-8448 | Medium Plus | 0x2100 |
| S-1-16-12288 | High Mandatory Level | 0x3000 — admin UAC elevated |
| S-1-16-16384 | System Mandatory Level | 0x4000 — SYSTEM |
Domain vs Local SIDs
A domain account SID has the form S-1-5-21-{DA1}-{DA2}-{DA3}-{RID} where DA1/DA2/DA3 are the three sub-authorities that identify the domain (set when the domain was created; the same for all accounts in that domain). The RID is the unique per-account identifier within the domain.
To identify whether a SID belongs to a domain: extract the first three sub-authorities and compare to the domain's own SID. The built-in SYSTEM account (S-1-5-18) has only one sub-authority (18) — there's no domain component. BUILTIN group SIDs use S-1-5-32 as the authority with the group RID as the single sub-authority.
SID APIs
// Look up the account name for any SID
void PrintSidInfo(PSID pSid)
{
char name[256] = {};
char domain[256] = {};
DWORD nameLen = 256;
DWORD domainLen = 256;
SID_NAME_USE use;
char *strSid = NULL;
ConvertSidToStringSidA(pSid, &strSid);
printf("SID: %s\n", strSid);
LocalFree(strSid);
if (LookupAccountSidA(NULL, pSid, name, &nameLen,
domain, &domainLen, &use)) {
const char *useStr[] = {
"", "User", "Group", "Domain", "Alias",
"WellKnownGroup", "DeletedAccount", "Invalid",
"Unknown", "Computer", "Label"
};
printf("Name: %s\\%s (%s)\n", domain, name, useStr[use]);
}
}
// Build a SID from scratch (e.g., S-1-5-18 SYSTEM)
PSID BuildSystemSid()
{
SID_IDENTIFIER_AUTHORITY ntAuth = SECURITY_NT_AUTHORITY;
PSID pSid;
AllocateAndInitializeSid(&ntAuth, 1, SECURITY_LOCAL_SYSTEM_RID,
0,0,0,0,0,0,0, &pSid);
return pSid; // caller FreeSid()
}
SID Analysis in Detection
# Python: parse SID from a hex string and classify it
import struct, re
def parse_sid(sid_bytes: bytes) -> str:
rev = sid_bytes[0]
count = sid_bytes[1]
ia = int.from_bytes(sid_bytes[2:8], 'big')
subs = [struct.unpack_from('<I', sid_bytes, 8 + i*4)[0]
for i in range(count)]
return "S-{}-{}-{}".format(rev, ia, "-".join(str(s) for s in subs))
WELL_KNOWN = {
"S-1-5-18": "SYSTEM",
"S-1-5-19": "Local Service",
"S-1-5-20": "Network Service",
"S-1-1-0": "Everyone",
"S-1-5-32-544": "BUILTIN\\Administrators",
"S-1-16-12288": "High IL",
"S-1-16-8192": "Medium IL",
"S-1-16-4096": "Low IL",
}
def classify_sid(sid_str: str) -> str:
if sid_str in WELL_KNOWN:
return WELL_KNOWN[sid_str]
parts = sid_str.split("-")
if len(parts) == 8 and parts[2] == "5" and parts[3] == "21":
rid = int(parts[-1])
if rid == 500: return "Built-in Administrator"
elif rid == 501: return "Guest"
elif rid >= 1000: return f"Domain User (RID={rid})"
return "Unknown"
Q & A
Two processes on the same machine run as "Administrator" — do they have the same SID?
It depends which "Administrator" account. The built-in local Administrator account (RID 500) always has the SID S-1-5-21-{machine-domain}-500. Any two processes running as the built-in local Administrator on the same machine have the same user SID — the machine's domain sub-authorities plus RID 500. However, a domain Administrator account has a different SID: S-1-5-21-{AD-domain}-500, where the AD-domain sub-authorities are the domain's three random sub-authorities generated at domain creation time. So: local Admin on machine A ≠ local Admin on machine B (different machine sub-authorities), but domain Admin on any domain machine = same domain SID. This is important for lateral movement detection: pass-the-hash reusing a local Admin credential works on machines where the local Admin has the same password, but the SID will differ per machine. Kerberos tickets for domain accounts carry the domain SID which is the same everywhere in the domain — that's what attackers want when they steal a domain account token or ticket.
What is a SID history attribute and why is it a common persistence mechanism in AD attacks?
SID history (sIDHistory attribute in Active Directory) is a migration feature: when a user account is migrated from one domain to another, their old domain SID is added to sIDHistory. When they authenticate in the new domain, the Kerberos ticket includes both their new SID and their old SID from sIDHistory. Windows access checks treat all SIDs in the token as authoritative — so the user's old domain SID grants access to resources that still have ACEs for it. The attack (SID History injection) works as follows: an attacker with Domain Admin (or equivalent) privileges adds a high-privilege SID (such as the Enterprise Admins SID, S-1-5-21-...-519, or even the SYSTEM SID) to the sIDHistory attribute of a low-privilege account. When that low-privilege account authenticates, the Kerberos PAC includes the injected SID. Domain controllers add PAC SIDs to the token. The account now silently has enterprise admin access via the token, even though it looks like a regular user. Detection: look for anomalous values in the sIDHistory attribute (Windows Event ID 4765 logs SID history additions); compare the sIDHistory of all accounts against expected values in your environment. Any non-migration SID history addition is suspicious. Many orgs disable SID filtering across domain trusts and forget about this vector.