From e3b8c8b39e23dd4f41164f2bc034ed0f8b3d699b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Fj=C3=A4llid?= Date: Sun, 31 May 2026 17:20:59 +0200 Subject: [PATCH] Support ACCESS_*_OBJECT_ACE and add GUID helpers Parse and marshal the object-ACE family (MS-DTYP 2.4.4.3-2.4.4.6): read the Flags field and the optional ObjectType / InheritedObjectType GUIDs (gated by IsObjectAceType) before the SID, leaving plain ACEs untouched so existing Mask/Sid callers are unaffected. Expose the GUIDs through AcePermissions and add GuidToString/GuidFromString for the little-endian object-ACE wire layout, with round-trip tests. --- msdtyp/structures.go | 111 ++++++++++++++++++-- msdtyp/structures_test.go | 209 ++++++++++++++++++++++++++++++++++++++ msdtyp/utils.go | 51 ++++++++++ 3 files changed, 360 insertions(+), 11 deletions(-) create mode 100644 msdtyp/structures_test.go diff --git a/msdtyp/structures.go b/msdtyp/structures.go index d2543b2..1e0b5e6 100644 --- a/msdtyp/structures.go +++ b/msdtyp/structures.go @@ -33,7 +33,7 @@ import ( var ( le = binary.LittleEndian be = binary.BigEndian - log = golog.Get("github.com/jfjallid/go-smb/msdtyp") + log = golog.Get("github.com/jfjallid/go-smb/msdtyp").SetDisplayName("msdtyp") ) // MS-DTYP Section 2.4.6 Security_Descriptor Control Flag @@ -126,6 +126,32 @@ var aceFlagsMap = map[byte]string{ FailedAccessAceFlag: "FailedAccessAce", } +// MS-DTYP Section 2.4.4.3 ACCESS_ALLOWED_OBJECT_ACE - Flags field bits that +// indicate which of the optional ObjectType / InheritedObjectType GUIDs are +// present in the ACE body. +const ( + AceObjectTypePresent uint32 = 0x00000001 + AceInheritedObjectTypePresent uint32 = 0x00000002 +) + +var objectAceTypes = map[byte]bool{ + AccessAllowedObjectAceType: true, + AccessDeniedObjectAceType: true, + SystemAuditObjectAceType: true, + SystemAlarmObjectAceType: true, + AccessAllowedCallbackObjectAceType: true, + AccessDeniedCallbackObjectAceType: true, + SystemAuditCallbackObjectAceType: true, + SystemAlarmCallbackObjectAceType: true, +} + +// IsObjectAceType reports whether an ACE type carries the optional Flags + +// ObjectType + InheritedObjectType GUID fields (the ACCESS_*_OBJECT_ACE family, +// MS-DTYP 2.4.4.3-2.4.4.6) rather than the plain Mask|SID body. +func IsObjectAceType(t byte) bool { + return objectAceTypes[t] +} + const ( AccessMaskGenericRead = "GENERIC_READ" AccessMaskGenericWrite = "GENERIC_WRITE" @@ -185,11 +211,17 @@ type ACEHeader struct { Size uint16 //Includes header size? } -// MS-DTYP Section 2.4.4.2 ACCESS_ALLOWED_ACE +// MS-DTYP Section 2.4.4.2 ACCESS_ALLOWED_ACE and 2.4.4.3 ACCESS_ALLOWED_OBJECT_ACE. +// ObjectFlags/ObjectType/InheritedObjectType are only populated (and only +// (un)marshalled) for the object-ACE family, see IsObjectAceType. They are zero +// for basic ACEs, so existing callers reading Mask/Sid are unaffected. type ACE struct { - Header ACEHeader - Mask uint32 - Sid SID //Must be multiple of 4 + Header ACEHeader + Mask uint32 + ObjectFlags uint32 // object ACEs only; which GUIDs follow + ObjectType [16]byte // present iff ObjectFlags&AceObjectTypePresent + InheritedObjectType [16]byte // present iff ObjectFlags&AceInheritedObjectTypePresent + Sid SID //Must be multiple of 4 } // MS-DTYP Section 2.4.2.3 RPC_SID @@ -215,11 +247,13 @@ type SecurityDescriptor struct { } type AcePermissions struct { - AceType string - AceFlags byte - AceFlagStrings string - Permissions []string - Sid string + AceType string + AceFlags byte + AceFlagStrings string + Permissions []string + Sid string + ObjectType string // GUID string; empty unless an object ACE with ObjectType present + InheritedObjectType string // GUID string; empty unless an object ACE with InheritedObjectType present } type PaclPermissions struct { @@ -517,6 +551,29 @@ func (s *ACE) MarshalBinary() (ret []byte, err error) { return } + // Object ACEs carry a Flags field followed by 0-2 GUIDs before the SID. + if IsObjectAceType(s.Header.Type) { + err = binary.Write(w, le, s.ObjectFlags) + if err != nil { + log.Errorln(err) + return + } + if s.ObjectFlags&AceObjectTypePresent != 0 { + err = binary.Write(w, le, s.ObjectType) + if err != nil { + log.Errorln(err) + return + } + } + if s.ObjectFlags&AceInheritedObjectTypePresent != 0 { + err = binary.Write(w, le, s.InheritedObjectType) + if err != nil { + log.Errorln(err) + return + } + } + } + // Encode ACE SID sidBuf, err := s.Sid.MarshalBinary() if err != nil { @@ -557,6 +614,29 @@ func readACE(r *bytes.Reader) (a *ACE, err error) { return } + // Object ACEs carry a Flags field followed by 0-2 GUIDs before the SID. + if IsObjectAceType(a.Header.Type) { + err = binary.Read(r, le, &a.ObjectFlags) + if err != nil { + log.Errorln(err) + return + } + if a.ObjectFlags&AceObjectTypePresent != 0 { + err = binary.Read(r, le, &a.ObjectType) + if err != nil { + log.Errorln(err) + return + } + } + if a.ObjectFlags&AceInheritedObjectTypePresent != 0 { + err = binary.Read(r, le, &a.InheritedObjectType) + if err != nil { + log.Errorln(err) + return + } + } + } + sid, err := ReadSID(r) if err != nil { log.Errorln(err) @@ -665,13 +745,22 @@ func (s *PACL) UnmarshalBinary(buf []byte) (err error) { func (a ACE) Permissions() AcePermissions { perms := ParseAccessMask(a.Mask) sidStr := a.Sid.ToString() - return AcePermissions{ + p := AcePermissions{ Sid: sidStr, Permissions: perms, AceType: AceTypeMap[a.Header.Type], AceFlags: a.Header.Flags, AceFlagStrings: ParseAceFlags(a.Header.Flags), } + if IsObjectAceType(a.Header.Type) { + if a.ObjectFlags&AceObjectTypePresent != 0 { + p.ObjectType = GuidToString(a.ObjectType) + } + if a.ObjectFlags&AceInheritedObjectTypePresent != 0 { + p.InheritedObjectType = GuidToString(a.InheritedObjectType) + } + } + return p } func (s *PACL) Permissions() PaclPermissions { diff --git a/msdtyp/structures_test.go b/msdtyp/structures_test.go new file mode 100644 index 0000000..6e990dd --- /dev/null +++ b/msdtyp/structures_test.go @@ -0,0 +1,209 @@ +// MIT License +// +// # Copyright (c) 2025 Jimmy Fjällid +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +package msdtyp + +import ( + "bytes" + "testing" +) + +// resetPasswordGUID is the extended-right GUID for User-Force-Change-Password. +const resetPasswordGUID = "00299570-246d-11d0-a768-00aa006e0529" + +func mustSID(t *testing.T, s string) SID { + t.Helper() + sid, err := ConvertStrToSID(s) + if err != nil { + t.Fatalf("ConvertStrToSID(%q): %v", s, err) + } + return *sid +} + +func TestGuidStringRoundTrip(t *testing.T) { + b, err := GuidFromString(resetPasswordGUID) + if err != nil { + t.Fatalf("GuidFromString: %v", err) + } + // Verify the documented little-endian wire layout for the first 3 groups. + want := []byte{0x70, 0x95, 0x29, 0x00, 0x6d, 0x24, 0xd0, 0x11, + 0xa7, 0x68, 0x00, 0xaa, 0x00, 0x6e, 0x05, 0x29} + if !bytes.Equal(b[:], want) { + t.Fatalf("GuidFromString wire bytes = %x, want %x", b[:], want) + } + if got := GuidToString(b); got != resetPasswordGUID { + t.Fatalf("GuidToString = %q, want %q", got, resetPasswordGUID) + } + // Braces and uppercase should be tolerated and normalised. + if _, err := GuidFromString("{" + resetPasswordGUID + "}"); err != nil { + t.Fatalf("GuidFromString with braces: %v", err) + } + if _, err := GuidFromString("not-a-guid"); err == nil { + t.Fatalf("GuidFromString accepted an invalid GUID") + } +} + +func aceRoundTrip(t *testing.T, in ACE) ACE { + t.Helper() + buf, err := in.MarshalBinary() + if err != nil { + t.Fatalf("ACE.MarshalBinary: %v", err) + } + if int(in.Header.Size) != len(buf) { + t.Fatalf("Header.Size %d != marshalled length %d", in.Header.Size, len(buf)) + } + var out ACE + if err := out.UnmarshalBinary(buf); err != nil { + t.Fatalf("ACE.UnmarshalBinary: %v", err) + } + return out +} + +func TestBasicACERoundTrip(t *testing.T) { + sid := mustSID(t, "S-1-5-21-1004336348-1177238915-682003330-512") + size := 4 + 4 + 8 + 4*len(sid.SubAuthorities) // header+mask+sid + in := ACE{ + Header: ACEHeader{Type: AccessAllowedAceType, Flags: ContainerInheritAce, Size: uint16(size)}, + Mask: 0x000F01FF, + Sid: sid, + } + out := aceRoundTrip(t, in) + if out.Header.Type != AccessAllowedAceType || out.Mask != 0x000F01FF { + t.Fatalf("basic ACE header/mask mismatch: %+v", out) + } + if out.Sid.ToString() != sid.ToString() { + t.Fatalf("basic ACE SID = %s, want %s", out.Sid.ToString(), sid.ToString()) + } + // A basic ACE must not consume/emit any object fields. + if out.ObjectFlags != 0 || out.ObjectType != ([16]byte{}) { + t.Fatalf("basic ACE leaked object fields: %+v", out) + } +} + +func TestObjectACERoundTripObjectTypeOnly(t *testing.T) { + sid := mustSID(t, "S-1-5-21-1004336348-1177238915-682003330-1104") + guid, _ := GuidFromString(resetPasswordGUID) + sidLen := 8 + 4*len(sid.SubAuthorities) + size := 4 + 4 + 4 + 16 + sidLen // header+mask+objflags+1guid+sid + in := ACE{ + Header: ACEHeader{Type: AccessAllowedObjectAceType, Size: uint16(size)}, + Mask: 0x00000100, // ADS_RIGHT_DS_CONTROL_ACCESS + ObjectFlags: AceObjectTypePresent, + ObjectType: guid, + Sid: sid, + } + out := aceRoundTrip(t, in) + if out.ObjectFlags != AceObjectTypePresent { + t.Fatalf("ObjectFlags = %#x, want %#x", out.ObjectFlags, AceObjectTypePresent) + } + if out.ObjectType != guid { + t.Fatalf("ObjectType = %x, want %x", out.ObjectType, guid) + } + if out.InheritedObjectType != ([16]byte{}) { + t.Fatalf("InheritedObjectType should be zero, got %x", out.InheritedObjectType) + } + if out.Sid.ToString() != sid.ToString() { + t.Fatalf("object ACE SID = %s, want %s", out.Sid.ToString(), sid.ToString()) + } + p := out.Permissions() + if p.ObjectType != resetPasswordGUID { + t.Fatalf("Permissions().ObjectType = %q, want %q", p.ObjectType, resetPasswordGUID) + } +} + +func TestObjectACERoundTripBothGUIDs(t *testing.T) { + sid := mustSID(t, "S-1-5-11") + ot, _ := GuidFromString(resetPasswordGUID) + iot, _ := GuidFromString("bf967aba-0de6-11d0-a285-00aa003049e2") // user class + sidLen := 8 + 4*len(sid.SubAuthorities) + size := 4 + 4 + 4 + 16 + 16 + sidLen + in := ACE{ + Header: ACEHeader{Type: AccessAllowedObjectAceType, Flags: ContainerInheritAce, Size: uint16(size)}, + Mask: 0x00000010, + ObjectFlags: AceObjectTypePresent | AceInheritedObjectTypePresent, + ObjectType: ot, + InheritedObjectType: iot, + Sid: sid, + } + out := aceRoundTrip(t, in) + if out.ObjectType != ot || out.InheritedObjectType != iot { + t.Fatalf("GUID mismatch: ot=%x iot=%x", out.ObjectType, out.InheritedObjectType) + } +} + +// TestSecurityDescriptorWithObjectACE exercises the full SD marshal/unmarshal +// path with a DACL containing both a basic and an object ACE. +func TestSecurityDescriptorWithObjectACE(t *testing.T) { + owner := mustSID(t, "S-1-5-32-544") + basicSID := mustSID(t, "S-1-5-18") + objSID := mustSID(t, "S-1-5-21-1004336348-1177238915-682003330-1104") + guid, _ := GuidFromString(resetPasswordGUID) + + basic := ACE{ + Header: ACEHeader{Type: AccessAllowedAceType, Size: uint16(8 + 8 + 4*len(basicSID.SubAuthorities))}, + Mask: 0x000F01FF, + Sid: basicSID, + } + obj := ACE{ + Header: ACEHeader{Type: AccessAllowedObjectAceType, Size: uint16(4 + 4 + 4 + 16 + 8 + 4*len(objSID.SubAuthorities))}, + Mask: 0x00000100, + ObjectFlags: AceObjectTypePresent, + ObjectType: guid, + Sid: objSID, + } + basicBuf, _ := basic.MarshalBinary() + objBuf, _ := obj.MarshalBinary() + aclSize := 8 + len(basicBuf) + len(objBuf) + dacl := &PACL{ + AclRevision: 4, // ACL_REVISION_DS + AclSize: uint16(aclSize), + ACLS: []ACE{basic, obj}, + } + sd := &SecurityDescriptor{ + Revision: 1, + Control: SecurityDescriptorFlagSR, + OwnerSid: &owner, + Dacl: dacl, + } + buf, err := sd.MarshalBinary() + if err != nil { + t.Fatalf("SecurityDescriptor.MarshalBinary: %v", err) + } + var out SecurityDescriptor + if err := out.UnmarshalBinary(buf); err != nil { + t.Fatalf("SecurityDescriptor.UnmarshalBinary: %v", err) + } + if out.OwnerSid == nil || out.OwnerSid.ToString() != owner.ToString() { + t.Fatalf("owner mismatch: %+v", out.OwnerSid) + } + if out.Dacl == nil || len(out.Dacl.ACLS) != 2 { + t.Fatalf("expected 2 DACL ACEs, got %+v", out.Dacl) + } + if out.Dacl.ACLS[0].Sid.ToString() != basicSID.ToString() { + t.Fatalf("basic ACE SID mismatch: %s", out.Dacl.ACLS[0].Sid.ToString()) + } + got := out.Dacl.ACLS[1] + if got.Header.Type != AccessAllowedObjectAceType || + got.ObjectType != guid || + got.Sid.ToString() != objSID.ToString() { + t.Fatalf("object ACE mismatch: %+v", got) + } +} diff --git a/msdtyp/utils.go b/msdtyp/utils.go index 3dfef19..0e6fe0b 100644 --- a/msdtyp/utils.go +++ b/msdtyp/utils.go @@ -24,6 +24,7 @@ package msdtyp import ( "bytes" "encoding/binary" + "encoding/hex" "fmt" "io" "slices" @@ -473,6 +474,56 @@ func ConvertStrToSID(s string) (sid *SID, err error) { return } +// GuidToString renders a GUID stored in the standard little-endian wire layout +// (as used inside object ACEs: Data1 uint32 LE, Data2/Data3 uint16 LE, Data4 8 +// bytes as-is) to the canonical "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" form. +func GuidToString(b [16]byte) string { + return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", + le.Uint32(b[0:4]), + le.Uint16(b[4:6]), + le.Uint16(b[6:8]), + be.Uint16(b[8:10]), + b[10:16], + ) +} + +// GuidFromString parses a canonical GUID string into the little-endian wire +// layout expected inside object ACEs. Surrounding braces are tolerated. +func GuidFromString(s string) (b [16]byte, err error) { + s = strings.TrimSuffix(strings.TrimPrefix(s, "{"), "}") + parts := strings.Split(s, "-") + if len(parts) != 5 { + err = fmt.Errorf("invalid GUID %q", s) + return + } + d1, err := strconv.ParseUint(parts[0], 16, 32) + if err != nil { + return b, fmt.Errorf("invalid GUID %q: %w", s, err) + } + d2, err := strconv.ParseUint(parts[1], 16, 16) + if err != nil { + return b, fmt.Errorf("invalid GUID %q: %w", s, err) + } + d3, err := strconv.ParseUint(parts[2], 16, 16) + if err != nil { + return b, fmt.Errorf("invalid GUID %q: %w", s, err) + } + g4, err := hex.DecodeString(parts[3]) + if err != nil || len(g4) != 2 { + return b, fmt.Errorf("invalid GUID %q: bad group 4", s) + } + g5, err := hex.DecodeString(parts[4]) + if err != nil || len(g5) != 6 { + return b, fmt.Errorf("invalid GUID %q: bad group 5", s) + } + le.PutUint32(b[0:4], uint32(d1)) + le.PutUint16(b[4:6], uint16(d2)) + le.PutUint16(b[6:8], uint16(d3)) + copy(b[8:10], g4) + copy(b[10:16], g5) + return b, nil +} + func ConvertToFiletime(t time.Time) uint64 { // Credit to https://github.com/Azure/go-ntlmssp/blob/master/unicode.go for logic ft := uint64(t.UnixNano()) / 100