drsuapi: fix V6 parser drift on child-domain UPTODATE_VECTOR#31
Merged
Conversation
skipUpToDateVectorV2 was missing the Align(8) pad between the hoisted MaxCount conformance and the struct's fixed fields. UPTODATE_VECTOR_V1/V2 cursors contain LONGLONG fields (USN, DSTIME), so the struct's alignment is 8; NDR demands the struct alignment is applied AFTER MaxCount (which uses its own primitive 4-byte alignment), before the first struct field. This is the same correctness pattern PR #16 (commit 8127aec) enumerated for PROPERTY_META_DATA_EXT_VECTOR -- it was missed for UPTODATE_VECTOR. PR #16's verification ran against sevenkingdoms.local (forest root) where the response's DSNAME ends at pos 260 (%8 == 4), so the post- MaxCount position landed at 264 -- already 8-aligned. Align(8) was a no-op and the miss was invisible. north.sevenkingdoms.local's longer DN puts post-MaxCount at 284 (%8 == 4), where the missing pad drops 4 bytes; the parser then misreads cNumCursors as 0, skips zero cursor bytes instead of the real 32-byte cursor, and walks into garbage in the prefix-table deferred data. Result: zero accounts emitted, no error surfaced (V6 parse errors are swallowed unless -debug is on). Verified against a live GOAD lab: 18/18 NTDS hashes on north.sevenkingdoms.local now match impacket-secretsdump byte-for-byte, 17/17 on sevenkingdoms.local still match PR #16's original verification. Bundles a sweep of related correctness and hardening issues surfaced during the fix: - skipUpToDateVectorV2 and readPrefixTableV2 now use the wire MaxCount (the authoritative NDR field for array layout) instead of the in- struct cNumCursors/cNumPrefixes. The two agree on any well-formed reply, but the wire value dictates stream position; trusting it keeps the cursor aligned on malformed input. - readREPLENTINFLISTArrayV2 now early-terminates when pNextEntInf is 0. REPLENTINFLIST is structurally a linked list per MS-DRSR; the pointer chain terminator is the structural truth, not cNumObjects. - Deferred-data reads are now gated only on the pointer referent being non-zero, never on a sibling inline count. NDR serializes a 4-byte MaxCount==0 for a non-null pointer to an empty array; the old "pointer AND count > 0" gating dropped that read and drifted. - Every helper that bails on a maxReasonable cap now calls a new Decoder.Fail() helper so d.err is set and downstream Read*/Skip calls become no-ops. The previous silent return left the cursor unadvanced and downstream helpers read into the wrong data. - SeekTo now respects the first-error-wins invariant by short- circuiting when d.err is already set, so descriptive Fail() errors aren't clobbered by a generic seek-range error in a later Skip. - DSNAME and PROPERTY_META_DATA_EXT_VECTOR helpers now also enforce the maxReasonable cap on count*size products that could overflow int on 32-bit Go builds. - readDSNAMEv2's RID extraction and processAttribute's RID extraction for ATTID_objectSid now require sidLen >= 12 (rev + subAuthCount + idAuth + at least 1 SubAuthority). At sidLen == 8 the "last 4 bytes" is the tail of IdentifierAuthority, not a RID, and would produce bogus DES keys for password decryption. - DN-to-SAMAccountName fallback now uses a backslash-aware RDN split and a case-insensitive "CN=" prefix check. Closes #28.
This was referenced May 12, 2026
pull Bot
pushed a commit
to rvrsh3ll/gopacket
that referenced
this pull request
May 13, 2026
…elper Three of the four follow-ups in mandiant#32 from the PR mandiant#31 review pass: 1. Replace the loose maxReasonable (10M element) ceiling with a precise per-call-site bounds check: every wire-driven make/Skip in the V6 parser now validates count * elemSize against d.Remaining() before allocating, so a malformed reply with a tiny payload and a huge embedded count can no longer trigger a multi-MB speculative make for bytes that don't actually exist on the wire. Product math is uint64 so it stays safe on 32-bit builds. CheckBounds lives on Decoder so other parsers can reach for the same primitive. 2. firstRDNValue now actually unescapes RFC 4514 backslash sequences via strings.Builder, so a DN like "CN=Smith\, John,OU=Foo" extracts to "Smith, John" rather than "Smith\, John". Trailing-backslash (malformed) drops silently. Table-driven tests cover the common cases. 3. Remove the dead writePartialAttrSet helper. writeGetNCChangesRequestV8 has always written a NULL referent for pPartialAttrSet, so the function was never reached. Item #3 of the issue (writeDSNAME structLen += 2 cargo-cult) is deferred: it's request-side, so there's no parser-side regression detection, and needs positive lab confirmation across DsBind/DsCrackNames/DsGetNCChanges/ DsGetDomainControllerInfo on both forest root and child domains before flipping. Filed for a separate PR. Verified end-to-end against the GOAD lab (sevenkingdoms.local forest root + north.sevenkingdoms.local child domain): full --just-dc-ntlm and --just-dc --history runs both return all accounts with correct NTLM hashes, hash histories, and parsed Kerberos keys.
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
Fixes #28:
secretsdumpDCSync silently returning zero accounts against child domains. Root cause is a missingAlign(8)inskipUpToDateVectorV2.UPTODATE_VECTOR_V{1,2}_EXTcursors contain LONGLONG (USN, DSTIME), giving the struct 8-byte alignment. NDR hoists the array's MaxCount to the front using its primitive 4-byte alignment; the struct's 8-byte alignment then applies AFTER MaxCount before the first field. This is the exact same correctness pattern PR #16 (8127aec) enumerated forPROPERTY_META_DATA_EXT_VECTOR, just missed forUPTODATE_VECTOR.PR #16's verification ran against
sevenkingdoms.local(forest root), where the response's DSNAME ends at pos 260, so post-MaxCount lands at 264 (already 8-aligned). Align(8) was a no-op and the miss was invisible.north.sevenkingdoms.local's longer DN puts post-MaxCount at 284 (%8 == 4); the missing pad drops 4 bytes, the parser misreadscNumCursorsas 0, skips zero cursor bytes instead of the real 32-byte cursor, and walks into garbage in the prefix-table deferred data. Zero accounts come back, no error surfaces (V6 parse errors are swallowed unless-debugis on).Verified against the live GOAD lab:
north.sevenkingdoms.local: 18/18 NTDS hashes match impacket-secretsdump byte-for-byte (was 0 before the fix).sevenkingdoms.local: 17/17 still match PR drsuapi: fix DsGetNCChanges V6 parser to actually extract NTDS hashes #16's original verification.Hardening sweep
While in the parser, fixed a set of related correctness and DoS-hardening issues surfaced by a focused audit:
MaxCount wire authority.
skipUpToDateVectorV2andreadPrefixTableV2now drive their loops from the wire MaxCount (the authoritative NDR field) rather than the sibling in-structcNumCursors/cNumPrefixes. They agree on well-formed replies; on malformed ones, trusting the wire keeps the cursor aligned.Linked-list early termination.
readREPLENTINFLISTArrayV2now breaks onpNextEntInf == 0.REPLENTINFLISTis structurally a linked list per MS-DRSR; the chain terminator is the structural truth, notcNumObjects.Pointer-only deferred gating. Three call sites previously gated deferred reads on
pointer != 0 && count > 0. NDR serializes a 4-byteMaxCount==0even for a non-null pointer to an empty array; the old gating dropped that read and drifted. Gate solely on the pointer.Decoder.Fail()helper. New method that setsd.erronly on first failure, so descriptive failure context survives downstreamSkip/Aligncalls. EverymaxReasonablebail-out now uses it. Previously the silentreturnleft the cursor unadvanced and the next helper read into the wrong data.SeekTofirst-error-wins.SeekTo(and by extensionSkipandAlign) now short-circuit whend.erris already set, so a generic seek-range error in a laterSkipdoesn't clobber a more descriptiveFail()error.32-bit overflow caps.
DSNAMEandPROPERTY_META_DATA_EXT_VECTORhelpers now apply themaxReasonablecap oncount * sizeproducts that could wrapinton 32-bit Go builds. The cap is promoted to a package-level constant so all helpers share it.SID minimum length. RID extraction in
readDSNAMEv2andprocessAttribute(ATTID_objectSid) now requiressidLen >= 12(rev + subAuthCount + idAuth + at least one SubAuthority). At length 8 the "last 4 bytes" is the tail of IdentifierAuthority, which would have produced bogus DES keys for password decryption on a malformed SID.DN parsing. Case-insensitive
CN=prefix check and backslash-aware RDN split, so an account with an escaped comma in its CN (e.g.CN=Smith\, John,OU=...) extracts the rightSAMAccountName.Test plan
secretsdump -k -no-pass -just-dcagainstwinterfell.north.sevenkingdoms.localreturns 18 NTDS hashes matching impacket byte-for-byte (was 0).secretsdump -k -no-pass -just-dcagainstkingslanding.sevenkingdoms.localstill returns the original PR drsuapi: fix DsGetNCChanges V6 parser to actually extract NTDS hashes #16 baseline 17 hashes.go build ./...clean.go vet ./...clean.