Add support for the undocumented @TOKEN. (0xfc) attribute token (Part of #135) - #138
Merged
Merged
Conversation
… of #135) Windows implements a fifth conditional-expression attribute token, 0xfc, with the SDDL prefix "@token.". MS-DTYP 2.4.4.17.8 documents only 0xf8-0xfb and its 2.5.1.1 ABNF admits only @user./@device./@resource., so a codec written to the specification rejects it: Unmarshal failed with "unknown conditional-expression token 0xfc" and the parser with "unknown attribute prefix". The token is implemented in both directions by sechost.dll and advapi32.dll, which parse "@token." into 0xfc and render 0xfc back to "@token.", and it is evaluated by the kernel: ntoskrnl.exe's evaluator assigns it its own internal attribute source class and reads it from the access token, not from the user-claims collection that 0xf9 uses. It is therefore not a synonym for tokenUserAttr, and is deliberately kept distinct. Its wire encoding is identical to the other attribute tokens - token byte, DWORD byte length, UTF-16 name - so the change is four small additions: the constant, the prefix arm in parseAttribute, the serializer arm, and the token in the decoder's attribute case. encode.go needed no change because it writes Attribute.Token generically. Serialization emits "@token." to match this package's existing capitalisation of @user./@device./@resource. rather than Windows' all-caps rendering; parsing folds case, so either form is accepted on input.
p0dalirius
force-pushed
the
enhancement-token-attribute-0xfc
branch
from
July 29, 2026 10:41
c5e9666 to
107d5af
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linked Issue
Part of #135Deliberately not a closing keyword. #135 covers two undocumented tokens —
0xfc(@TOKEN.) and0xa3(&). This PR implements only0xfc.0xa3is left open there: its semantics are now known (a bitwise AND flag test on two 64-bit integers, resolved in the issue's comments) but choosing how to represent it needs a call on precedence that is better made separately — see Notes.Root Cause
Windows implements a fifth conditional-expression attribute token,
0xfc, carrying the SDDL prefix@TOKEN.. MS-DTYP 2.4.4.17.8 documents only0xf8–0xfb, and its 2.5.1.1 ABNF admits only@user./@device./@resource.. This package was written to the specification, so it rejected the token in both directions:Unmarshalfailed withunknown conditional-expression token 0xfc, and the parser withunknown attribute prefix in "@TOKEN.foo".The token is not an obscure corner. It is implemented in both directions by
sechost.dllandadvapi32.dll, which parse@TOKEN.into0xfcand render0xfcback to@TOKEN., and it is evaluated by the kernel:ntoskrnl.exe's conditional-expression evaluator dispatches on the attribute token and gives0xfcits own internal source class (6), reading its value from the access token rather than from the user-claims collection that0xf9uses. Evidence for all of that is in #135.Fix Description
Four small additions, since the wire encoding of
0xfcis identical to the other attribute tokens — token byte,DWORDbyte length, UTF-16 name:ace/condition/condition.gotokenTokenAttr byte = 0xfc, with a comment recording that it is undocumented and not a synonym fortokenUserAttr;Attribute.Tokendoc updatedace/condition/parser.go@token.arm inparseAttribute()'s prefix ladder, placed with the other three so it inherits the existing case foldingace/condition/serialize.gotokenTokenAttr→"@Token."inattributeText()ace/condition/decode.gotokenTokenAttradded to the attribute case indecodeToken()encode.goneeded no change: it writesAttribute.Tokengenerically.Two deliberate choices:
tokenUserAttr. Mapping@TOKEN.onto0xf9would have been a one-line shortcut and is wrong — the two differ at every layer in Windows (parse ladder, render string, theLocalGetReferencedTokenTypesForConditionbitmask, and the evaluator's source class). A test pins this so a future simplification cannot collapse them.@Token., matching this package's existing capitalisation of@User./@Device./@Resource.. Windows renders all four in all-caps (@TOKEN.,@USER., …) and this package already normalises the documented three, so following the local convention keeps output internally consistent. Parsing folds case, so@TOKEN.,@token.and@ToKeN.are all accepted.How Verified
Runtime, before the fix:
Runtime, after the fix — text → binary → text, with the token byte inspected:
Decoding a hand-assembled payload of the shape Windows emits:
Full security-descriptor round-trip, so the token works inside a real conditional ACE and not just in the condition codec:
Regression check — with the new tests present but the four source files reverted to their pre-fix state,
go test ./ace/condition/...reports 19 failures. With the fix,go test ./...is green across the repository,gofmt -l ace/is clean, andgo vetreports nothing.Live sanity check against AD DS (Server 2025 lab): a
0xfccondition written to a real object is stored intact by the DC and read back unchanged, so descriptors containing this token do occur in a form this package must be able to read. Details in #135.Test Coverage
Added — in
ace/condition/condition_test.go:TestTokenAttribute_0xfc— asserts the encoded token byte is0xfcand the text form round-trips to@Token.foo == 1.TestTokenAttribute_PrefixIsCaseInsensitive— all four casings of the prefix yield0xfc.TestTokenAttribute_DecodeWindowsBlob— decodes a hand-assembled Windows-shaped payload; this is the case that previously failed withunknown conditional-expression token 0xfc.TestTokenAttribute_RoundTripStable—Marshal → Unmarshal → Marshalis byte-stable across seven expressions covering both operand positions, string/int/hex literals, composites,&&mixing with@User., andExists.TestTokenAttribute_NotAliasOfUserAttr— asserts@Token.and@User.encode to different tokens and that@User.is still0xf9.Scope of Change
ace/condition/condition.go,ace/condition/parser.go,ace/condition/serialize.go,ace/condition/decode.go,ace/condition/condition_test.go@token.somethingpreviously producedunknown attribute prefixand now parses as a token attribute. That is the intent, and@-prefixed names were already reserved — the existingcase strings.HasPrefix(name, "@")arm rejected every unrecognised@prefix, so no previously-valid expression changes meaning.Risk and Rollout
Additive. The change widens the set of accepted input and adds one serializer arm reachable only for a token that previously could not be constructed. No existing token's encoding, decoding or text form is altered. Safe to merge without staged rollout.
Notes
Why
0xa3is not in this PR. Its semantics are settled — bitwise AND of two 64-bit integers, non-zero meaning TRUE, resolved by decompilingntoskrnl.exe(see #135) — but supporting it means committing to a precedence of 10, which is the lowest in Windows' operator table, below||(11) and&&(12). That makesa & b || cparse asa & (b || c). Reusing&&'s precedence would silently reassociate expressions, so the value of encoding it at all, versus decoding it only so such an ACE round-trips instead of erroring, is a judgement worth making on its own rather than bundling here.Two observations noted while testing, both out of scope and neither addressed:
Exists/Not_Existsin Windows reject an operand of type0xf9,0xfbor0xfcwhile permitting0xf8and0xfa— the rejected three being exactly the sources drawn from the security context. This package acceptsExistson any attribute, including@Token., and this PR does not change that; adding the restriction would be a new rejection of input that currently parses.1supplied as anint64(0x04) comes back as anint8(0x01). Self-consistent round-trips are byte-stable, so this is not a defect on its own, but it may be an interop difference against Windows' encoder and would want its own investigation before anyone relies on byte-identical re-encoding of externally-produced blobs.