Why does the CREATOR_OWNER SID sometimes reset itself to the object’s current owner rather than its original owner?

I noted some time ago that the CREATOR_OWNER security identifier (SID) is not a shorthand that refers to the object's current owner. Rather, the CREATOR_OWNER SID is a template that is applied when the object is created. When the template is applied, all occurrences of CREATOR_OWNER are replaced with the object's current owner, and it is the replaced SID that controls access. Changing the object's owner doesn't cause these access control entries to be recalculated;¹ they continue to refer to the captured value.

A customer observed this phenomenon when they created a folder with an inheritable access control entry (ACE) for CREATOR_OWNER. They observed that those access control entries were indeed propagated to child objects, with the CREATOR_OWNER changed to the object's actual owner. Furthermore, if they went to the Security properties and changed the child object's owner, the ACE was not recalculated to update the ACE's SID from the old owner to the new owner.

However (and this is the weird part), if they use the Security properties to make some unrelated change to the object's access control list (ACL), then this has a side effect of recalculating the ACEs and updating the CREATOR_OWNER-sourced ACEs to refer to the new owner.

This recalculation is not being done by the security infrastructure. It's being done by the ACL editor.

When you change the access control list for an item, the ACL editor calls Tree­Set­Named­Security­Info and passes an ACL that consists only of the non-inherited ACEs, and it sets the UNPROTECTED_DACL_SECURITY_INFORMATION flag, which means "Oh, and also inherit ACEs from my parent, as if I were newly-created."

In other words, the ACL edit deletes all the ACEs that were obtained by inherance, and then creates new ACEs based on the current parent's inheritable ACEs.

The ACL editor is trying to be helpful, but it ends up being confusing.

¹ What would this recalculation even mean if the object was moved to a new folder after being created, or if the containing folder's access control list were modified in the interim? I guess you could have a bit somewhere in the ACE that says, "This was originally created from a template that used CREATOR_OWNER." The closest thing to that is the INHERITED_ACE bit, which says "This ACE was autogenerated via inheritance," but it doesn't give any information as to what the original ACE was. Suppose the object's current owner is Bob. If an ACE applies to Bob and has the INHERITED_ACE bit set, it could mean that the original template ACE's SID was CREATOR_OWNER that was changed to Bob during template application because Bob was the original owner, or it could mean that the original template ACE's SID was CREATOR_GROUP that was changed to Bob during template application because Bob was the original group, or it could mean that the original template ACE's SID was Bob all along.

Comments (12)

  1. Stefan Kanthak says:

    The callback function of Tree­Set­Named­Security­Info() https://msdn.microsoft.com/en-us/library/aa965849.aspx is declared without calling convention; with compiler switches /Gr or /Gz this will generate WRONG code.
    Most (if not all) other callback functions used in the Win32 API are but declared CALLBACK alias __stdcall
    Is there a deeper reason for this bug, or is this just sloppy coding?

    1. Darran Rowe says:

      To be a bit picky, the code generated is correct for the definitions, but this is an ODR violation.

      1. Stefan Kanthak says:

        No ODR here!
        The first compilation unit, the DLL, is compiled with /Gz, so the callback function gets __stdcall calling convention.
        The second compilation unit, the user program, is typically compiled without /Gz, so the callback function gets __cdecl calling convention.

        1. Darran Rowe says:

          Well, if you want to be picky to my picky, then fine, I’ll be picky back.
          The exact rule that governs this is, in the C++17 standard 6.5 [basic.link] paragraph 10.
          “After all adjustments of types (…), the types specified by all declarations referring to a given variable or function shall be identical, except that declarations for an array object can specify array types that differ by the presence or absence of a major array bound. A violation of this rule on type identity does not require a diagnostic.”
          Since the type of TreeSetNamedSecurityInfo has external linkage and the types are different then we have a function with external linkage declared with two different types in two different translation units.
          Either way, the code being generated isn’t wrong for the definitions.

          1. Stefan Kanthak says:

            1. who said something about C++?
            https://msdn.microsoft.com/en-us/library/aa965849.aspx is the pure ANSI C interface
            2. the difference between theory and practice is the practice.
            Unless compiled with /Gz the callee doesn’t match the caller’s expectation and will lead to a crash or vulnerability.
            Do I REALLY need to come up with an exploit?

          2. Darran Rowe says:

            Stefan Kanthak:
            Who said anything about the C++ standard’s rules only apply to C++. The standard itself is quite compatible with C, and in fact the rules are usually the same, just worded differently. The C11 standard, 6.7 paragraph 4, states that all declaration in the same scope that refer to the same object must have compatible types. So even for a C interface the rule is the same.
            Also, I don’t think you quite get what I mean. If the compiler is given incorrect information on a function type, you can’t call the code it generates wrong, since for the declarations the compiler is given, the code generated is correct. But note, this is NOT the same as me saying this isn’t a problem, this is a problem. It is just that this is caused by the declarations violating the rules of the C standard.

    2. I filed a bug against the team that owns that header file. Just sloppiness. Forgetting the calling convention is a common error. Since the unit test builds with the same compiler switches as the OS code (namely /Gz – stdcall as default), the lack of an explicit convention specifier is not detected.

      1. Stefan Kanthak says:

        So it’s a double happy^Wsloppiness when such simple to spot bugs go undetected.
        Insane question: why doesn’t Microsoft let customers file such bugs directly?
        Thanks for picking this up.

        1. In practice, what happens is that most of the bugs are low quality, and it’s hard to find the needle of good bugs in the haystack of bad ones.

          1. Stefan Kanthak says:

            I consider every bug low quality.-)

  2. Stefan Kanthak says:

    Are you able to find the MSVC keywords __cdecl, __stdcall or __fastcall (or anything like them which other compilers might support) in the standards you reference?
    Or the words “calling convention”?

    1. Darran Rowe says:

      Why does that matter? This isn’t as if this kind of problem stems from just the calling convention. Any time the declaration between two translation units is different, be it a compiler extension or from something in the language itself (32 bit type vs 64 bit type as one of the parameters) then this type of problem can still surface.

      But the C++17 standard does actually use the term calling convention in relation to linkage specification. You can find this in 10.5 paragraph 1.

      The C11 itself is a completely different thing. It implies that the description in the standard is for an abstract machine, and any implementation will have to fill in the gaps when porting to a real machine. So it does refer to abstract semantics and actual semantics. For example, paragraph 9. So this acknowledges that the standard itself isn’t enough for a real machine and extra semantics are needed on real processors. There is also a note in 6.2.5 paragraph 14, note 44 that says an implementation is allowed to add keywords to provide alternative ways to designate a type. Function pointers are types, function prototypes are types, so this does allow for keywords like __cdecl and __stdcall to be used

Skip to main content