drsuapi: fix DsGetNCChanges V6 parser to actually extract NTDS hashes#16
Merged
drsuapi: fix DsGetNCChanges V6 parser to actually extract NTDS hashes#16
Conversation
Rewrites the hand-rolled DRS_MSG_GETCHGREPLY_V6 parser as a declarative NDR struct walker. The old parser used empirical byte offsets that were wrong in several places and drifted out of alignment within the first few REPLENTINFLIST entries, so secretsdump emitted zero NTDS hashes against any modern DC (issue #9). New files: pkg/dcerpc/drsuapi/ndr.go - NDR Decoder primitives: alignment, conformant arrays, pointer referents, UTF-16LE strings, GUIDs, with a sticky error model so partial results survive downstream faults. pkg/dcerpc/drsuapi/getncchanges_v6.go - V6/V7/V9 reply parser driven by MS-DRSR struct definitions. Correctness points that were wrong in the old parser: 1. V6 fixed header is 148 bytes, not 136. The old code omitted cNumValues + rgValues + dwDRSError and treated those 12 bytes as pNC's DSNAME. 2. DSNAME.StringName is a pure conformant array (NDRUniConformantArray), not conformant-varying. No Offset/ActualCount between NameLen and the WCHAR elements. 3. UPTODATE_VECTOR is V2_EXT on current DCs: 32-byte cursors (UUID + USN + DSTIME), not the V1 24-byte cursors the old parser hard-coded. 4. REPLENTINFLIST is serialized as a linked list via pNextEntInf. NDR uses strict DFS through the first-encountered pointer in each struct, which lays out all N fixed parts consecutively, then the non-pNext deferreds (pName, pAttr, pParent, pMeta) unwind bottom-up - the deepest node's appear first, the head's last. The parser iterates captured headers in reverse to match. 5. PROPERTY_META_DATA_EXT_VECTOR alignment: the array's MaxCount is hoisted to the front by NDR early conformance, but hoisted MaxCount uses only its primitive (4-byte) alignment. The struct's 8-byte alignment applies AFTER MaxCount, before cNumProps and the elements. Getting this wrong drifted every entry whose prior pParent UUID landed on a 4-aligned (non-8-aligned) position. Verification against live GOAD sevenkingdoms.local DC: 17/17 NTDS password hashes (Administrator, krbtgt, vagrant, KINGSLANDING\$, ESSOS\$, NORTH\$, and all domain users) match impacket-secretsdump byte-for-byte. Independent Python byte-level reference walker confirms the same 17 unicodePwd-bearing entries offline. Also removes ~950 lines of the now-unreachable legacy V6 helpers (parseGetNCChangesResponseV6, skipDSNAME, skipUpToDateVector, skipPropertyMetaDataExtVector, parsePrefixTable, parseREPLENTINFLIST, parseENTINF, findValidDSNAME, parseDSNAMEIntoObject, parseATTRBLOCK) from getncchanges.go. Closes #9.
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.
Summary
DRS_MSG_GETCHGREPLY_V6parser as a declarative NDR struct walker. The old parser drifted out of alignment within the first fewREPLENTINFLISTentries, so secretsdump emitted zero NTDS hashes against any modern DC.pkg/dcerpc/drsuapi/ndr.gowith NDR Decoder primitives (alignment, conformant arrays, pointer referents, UTF-16LE strings, GUIDs, sticky error model).pkg/dcerpc/drsuapi/getncchanges_v6.gowith a parser built from MS-DRSR struct definitions.getncchanges.go.Net: +603 / -956 lines.
Closes #9.
Correctness points the old parser got wrong
cNumValues+rgValues+dwDRSErrorand treated those 12 bytes as the start ofpNC's DSNAME.NDRUniConformantArray), not conformant-varying. NoOffset/ActualCountheader betweenNameLenand the WCHAR elements.pNextEntInf. NDR uses strict DFS through the first-encountered pointer in each struct, which lays out all N fixed parts consecutively, then the non-pNext deferreds (pName,pAttr,pParent,pMeta) unwind bottom-up (the deepest node's deferreds appear first, the head's last). The parser iterates captured headers in reverse to match.MaxCountis hoisted to the front by NDR early conformance, but hoistedMaxCountuses only its primitive (4-byte) alignment. The struct's 8-byte alignment applies afterMaxCount, beforecNumPropsand the elements. Getting this wrong drifted every entry whose priorpParentUUID landed on a 4-aligned (non-8-aligned) position. This was the final bug blocking full-response parsing.Verification
Live DC, compared to
impacket-secretsdump:17/17 real password hashes match byte-for-byte (Administrator, krbtgt, vagrant, KINGSLANDING$, ESSOS$, NORTH$, and all 11 domain users). The single line Impacket emits that we don't is
Guest:501with the empty-password placeholder hash. Guest exists in the replication stream but has nounicodePwdattribute; Impacket emits a placeholder, gopacket filters to accounts where a real hash was extracted. Cosmetic output difference, no credential recovery gap.Independent spec-driven Python reference walker (not Impacket; Impacket's own
fromStringonDRS_MSG_GETCHGREPLY_V6doesn't deeply traverse the linked list): agrees with the Go parser on exactly the 17 unicodePwd-bearing DNs.Offline parse of a 1MB captured response: 254 objects extracted from 307 replicated entries, end-to-end without drift.
Test plan
go build ./...go vet ./...go test ./...(all existing tests pass)gopacket secretsdump -just-dc-ntlmagainst GOAD sevenkingdoms.local DC, all 17 hashes recoveredimpacket-secretsdump, identical on every real hashfMoreData=1). Our test blob hasfMoreData=0; this code path is unchanged from before but worth exercising against a bigger domain before closing any follow-on issues.