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.
This commit is contained in:
Jimmy Fjällid
2026-05-31 17:20:59 +02:00
parent 00ecda6111
commit e3b8c8b39e
3 changed files with 360 additions and 11 deletions
+100 -11
View File
@@ -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 {
+209
View File
@@ -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)
}
}
+51
View File
@@ -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