Search code examples
c#winapiacldacl

C# WINAPI - Create a new DACL


I am trying to launch a process in a context of a user that I log on using LogonUserExW.

In order to do that, I need to modify DACL of Winstation "Winsta0" and Desktop "Default". What I do is get the DACL and Security Descriptor (which I don't actually need nor use) using GetSecurityInfo. Then I call GetAclInformation twice to get information about the size and revision of the ACL.

ACL_SIZE_INFORMATION aclInfoSizeOut = new ACL_SIZE_INFORMATION();
GetAclInformation(daclWinsta, out aclInfoSizeOut, Marshal.SizeOf(aclInfoSizeOut), ACL_INFORMATION_CLASS.AclSizeInformation);
ACL_REVISION_INFORMATION aclInfoRevisionOut = new ACL_REVISION_INFORMATION();
GetAclInformation(daclWinsta, out aclInfoRevisionOut, Marshal.SizeOf(aclInfoRevisionOut), ACL_INFORMATION_CLASS.AclRevisionInformation);

Once I have the data, I calculate the required size of the new ACL:

int cbNewACL = Convert.ToInt32(aclInfoSizeOut.AclBytesInUse + Marshal.SizeOf(allowedAce) + GetLengthSid(userpSid) - sizeof(UInt32));

Then as per https://web.archive.org/web/20121231022835/http://support.microsoft.com/kb/102102 I allocate memory for the new ACL using

IntPtr pNewAcl = LocalAlloc(LocalAllocFlags.LPTR, cbNewACL);

This is where it starts getting confusing for me BIG time. I fail to understand how this actually works. Using the LocalAlloc I now have a pointer to a memory of the size I specified. Then I initialize the new ACL which is supposed to be in that memory block, BUT how? Because the pointer is actually rewritten once I use the function below. Interestingly enough, it always has the same value every time I call the function. Whereas the pointer returned by Locallloc is always different.

InitializeAcl(out pNewAcl, cbNewACL, aclInfoRevisionOut.AclRevision);

The pAcl in InitializeAcl function is defnied as "[out] pAcl A pointer to an ACL structure to be initialized by this function. Allocate memory for pAcl before calling this function." I would understand if it was passed as REF for the function to know where to actually initialize the ACL, but REF doesn't change it at all.

Another thing is when I call the InitializeAcl, the cbNewAcl gets set to 0. This goes absolutely beyond my head. How and why does it change the value?

Then when I call AddAccessAllowedAce it completely messes up the structures I have set before I get the data from the existing ACL- aclInfoSizeOut and aclInfoRevisionOut which get set to nonsense values. Such as count 0, bytes in use 78548557 (some high number). And cbNewAcl gets changed to 1. I have no idea why it does that.

AddAccessAllowedAce(ref pNewAcl, aclInfoRevisionOut.AclRevision, ACCESS_MASK.READ_CONTROL | ACCESS_MASK.WINSTA_ALL_ACCESS, userpSid);

Below is my full code which "works" - I get the user's token and psid, I query the existing DACL, I query the members of the DACL and get their string SIDs, but that is about it. I can't create a new DACL. Posting it with my comments as well. I hope I did not forget any signatures as the code is a bit longer in VS than what I am actually posting.

[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool CloseHandle(IntPtr hObject);
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool LogonUserExW(
            string lpszUsername,
            string lpszDomain,
            string lpszPassword,
            int dwLogonType,
            int dwLogonProvider,
            out IntPtr phToken,
            out IntPtr ppLogonSid,
            out IntPtr ppProfileBuffer,
            out IntPtr pdwProfileLength,
            out QUOTA_LIMITS pQuotaLimits
            );
[DllImport("Advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern int ConvertSidToStringSidW(
                IntPtr SidPtr,
                out IntPtr SidString
            );          
[DllImport("Advapi32.dll", SetLastError = true)]
public static extern int GetSecurityInfo
            (
                IntPtr handle,
                SE_OBJECT_TYPE ObjectType,
                SECURITY_INFORMATION SecurityInfo,
                out IntPtr ppsidOwner,
                out IntPtr ppsidGroup,
                out IntPtr ppDacl,
                out IntPtr ppSacl,
                out IntPtr ppSecurityDescriptor
            );          
[DllImport("User32.dll", SetLastError = true)]
public static extern IntPtr GetProcessWindowStation();          
[DllImport("Advapi32.dll", SetLastError = true)]
public static extern bool GetAclInformation(
                IntPtr pAcl,
                out ACL_SIZE_INFORMATION pAclInformation,
                int nAclInformationLength,
                ACL_INFORMATION_CLASS aclInformationClass
            );
public struct ACL_SIZE_INFORMATION
{
    public uint AceCount;
    public uint AclBytesInUse;
    public uint AclBytesFree;
}
public struct ACL_REVISION_INFORMATION
{
    public int AclRevision;
}
public enum  ACL_INFORMATION_CLASS
{
            AclRevisionInformation = 1,
            AclSizeInformation
}

public struct QUOTA_LIMITS
{
            public int PagedPoolLimit;
            public int NonPagedPoolLimit;
            public int MinimumWorkingSetSize;
            public int MaximumWorkingSetSize;
            public int PagefileLimit;
            public Int64 TimeLimit;
}
ublic struct ACCESS_ALLOWED_ACE
{
    public ACE_HEADER Header;
    public ACCESS_MASK Mask;
    public UInt16 SidStart;
}
ublic enum LocalAllocFlags : uint
{
    LHND = 0x0042,
    LMEM_FIXED = 0x0000,
    LMEM_MOVEABLE = 0x0002,
    LMEM_ZEROINIT = 0x0040,
    LPTR = 0x0040
}

[DllImport("Kernel32.dll", SetLastError = true)]
public static extern IntPtr LocalAlloc(LocalAllocFlags flags, int bytes);
DllImport("Advapi32.dll", SetLastError = true)]
public static extern bool InitializeAcl(
    out IntPtr pAcl,
    int nAclLength,
    int dwAclRevision
    );
public static extern int AddAccessAllowedAce
    (
        ref IntPtr pAcl,
        /*  ACL_REVISION    2
            ACL_REVISION_DS 4
         */
        int dwAceRevision,
        ACCESS_MASK AccessMask,
        IntPtr pSid
    );
    
    
    
LogonUserExW("XXXXX", "FFFFF", "BBBBB", 2, 0, out IntPtr userToken, out IntPtr userpSid, out IntPtr profileBuffer, out IntPtr profileLength, out QUOTA_LIMITS profQuotaLimits);

ConvertSidToStringSidW(userpSid, out IntPtr ptrSid);

string stringSid = Marshal.PtrToStringUni(ptrSid);

IntPtr winstaHandle = GetProcessWindowStation();

//get security descriptor for the station
int getDescriptorResult = GetSecurityInfo(winstaHandle, SE_OBJECT_TYPE.SE_WINDOW_OBJECT, SECURITY_INFORMATION.DACL_SECURITY_INFORMATION | SECURITY_INFORMATION.PROTECTED_DACL_SECURITY_INFORMATION, out IntPtr owner, out IntPtr group, out IntPtr daclWinsta, out IntPtr Sacl, out IntPtr secDescriptor);

//this actually works but shows different values than ACL_SIZE_INFORMATION and ACL_REVISION_INFORMATION
//maybe DACL is in different structure than ACL struct? could not find any other struct in the documentation
ACL aclStruct = (ACL)Marshal.PtrToStructure(daclWinsta, typeof(ACL));

ACL_SIZE_INFORMATION aclInfoSizeOut = new ACL_SIZE_INFORMATION();
GetAclInformation(daclWinsta, out aclInfoSizeOut, Marshal.SizeOf(aclInfoSizeOut), ACL_INFORMATION_CLASS.AclSizeInformation);
ACL_REVISION_INFORMATION aclInfoRevisionOut = new ACL_REVISION_INFORMATION();
GetAclInformation(daclWinsta, out aclInfoRevisionOut, Marshal.SizeOf(aclInfoRevisionOut), ACL_INFORMATION_CLASS.AclRevisionInformation);
//count:17 bytes:436

//create a new ACL just to get its size for the cbNewACL value
ACCESS_ALLOWED_ACE allowedAce = new ACCESS_ALLOWED_ACE();

int cbNewACL = Convert.ToInt32(aclInfoSizeOut.AclBytesInUse + Marshal.SizeOf(allowedAce) + GetLengthSid(userpSid) - sizeof(UInt32));
//??? for some reason cbNewACL gets cleared after calling InitializeAcl
IntPtr pNewAcl = LocalAlloc(LocalAllocFlags.LPTR, cbNewACL);


InitializeAcl(out pNewAcl, cbNewACL, aclInfoRevisionOut.AclRevision);
//just for test try to add it right away
//AddAccessAllowedAce(ref pNewAcl, aclInfoRevisionOut.AclRevision, ACCESS_MASK.READ_CONTROL | ACCESS_MASK.WINSTA_ALL_ACCESS, userpSid);
//after calling AddAccessAllowedAce the information is structs aclInfoSizeOut and aclInfoRevisionOut gets destroyed
//cbNewAcl gets set to 1
//???




bool newAceAdded = false;
bool userSidAlreadyExists = false;
//copy old ACEs into the new ACL
if (aclInfoSizeOut.AceCount != 0)
{
    int indexInNewAcl = 0;
    for (int i=0;i<aclInfoSizeOut.AceCount;i++)
    {
        //get ace and add it to the new ACL
        GetAce(daclWinsta, i, out IntPtr existingAce);
        if (existingAce != IntPtr.Zero)
        {
            //get ace header from the ACE pointer to identify the ACE type
            //get first 4 bytes from the pointer starting at 0 since ACE_HEADER struct is of size 4
            //once we have it in the byte array, create a GC handle with the bytes - this allocates them in a managed memory
            //then read it to the structure using Marshal.PtrToStructure

            ACE_HEADER aceHeader = new ACE_HEADER();
            byte[] aceHeaderBytes = new byte[Marshal.SizeOf(aceHeader)];
            Marshal.Copy(existingAce, aceHeaderBytes, 0, aceHeaderBytes.Length);
            GCHandle aceHeaderBytesHandle = GCHandle.Alloc(aceHeaderBytes, GCHandleType.Pinned);
            try
            {
                aceHeader = (ACE_HEADER)Marshal.PtrToStructure(aceHeaderBytesHandle.AddrOfPinnedObject(), typeof(ACE_HEADER));
                switch (aceHeader.AceType)
                {
                    case 0:
                    case 1:
                        //0 allowed - ACCESS_ALLOWED_ACE, 1 denied - ACCESS_DENIED_ACE 
                        //the two have the same members definied in their structure.
                        //the declaration below is not explicitly needed in our case, but might be useful for some other things
                        //ACCESS_ALLOWED_ACE existingDeniedAllowedACE = (ACCESS_ALLOWED_ACE)Marshal.PtrToStructure(existingAce, typeof(ACCESS_ALLOWED_ACE));
                        //0 and 1 in this context are together in one clause since they are definied by the same members in the same order, thus allowing us
                        //to get SidStart from the same offset from the ACE IntPtr
                        //GUESSING: SidStart starts at 8th byte in the struct, therefore an offset of 8 bytes - size of ACE_HEADER and ACCESS_MASK (Uint16)
                        //if for example the ACE type would be definied by more members with SidStart at the end, we would have to calculate the offset by
                        //the sum of all members except for SidStart
                        IntPtr sidPtrInAce = IntPtr.Zero;
                        
                        try
                        {
                            sidPtrInAce = IntPtr.Add(existingAce, 8);
                            ConvertSidToStringSidW(sidPtrInAce, out IntPtr pSidStringInAce);
                            if (pSidStringInAce == IntPtr.Zero) { throw new Exception("failed to get pSidString"); }
                            string sidStringInAce = Marshal.PtrToStringUni(pSidStringInAce);
                            Console.WriteLine(sidStringInAce);
                            if (sidStringInAce == stringSid) { userSidAlreadyExists = true; }
                        } catch {  }

                        break;
                }
                if (userSidAlreadyExists) { break; }

                //if the ACE was not added AND the processing ACE is not NON-INHERITED DENIED and is not DENIED for Object AND is not Enable for Object
                //then add our ADE to ensure it is in the correct order
                //Windows 2000 and later ACE ordering in ACL:
                //Non-Inherited -> Inherited
                //withing each of the two groups the ACEs are also in the following order
                //Disable for the object
                //Disable for the subject of the object
                //Enable for the object
                //Enable for the subject of the object
                //So basically if we hit anything but NON-Inherited disable for the object, NON-Inherited Disable for the subject of the object and NON-Inherited Enable for the object
                //then we should add our ACE - this will ensure correct order
                //^ could not figure out how to distinguish between Enable for the object and Enable for the subject of the object

                //instead, add the ACE the moment we find an inherited ACE

                if (!newAceAdded && isAceInherited(aceHeader.AceFlags))
                {
                    //AddAccessAllowedAce(ref pNewAcl, aclInfoRevisionOut.AclRevision, ACCESS_MASK.READ_CONTROL | ACCESS_MASK.WINSTA_ALL_ACCESS, userpSid);
                    newAceAdded = true;
                    //indexInNewAcl++;
                }

                //AddAce(ref pNewAcl, aclInfoRevisionOut.AclRevision, indexInNewAcl, existingAce, aceHeader.AceSize);
                indexInNewAcl++;
            }
            catch (Exception ex)
            {
                aceHeaderBytesHandle.Free();
            }
        } else
        {
            Console.WriteLine("failed obtaining existing ACE in ACL. quitting"); Console.Read(); return;
        }
    }
}
if (userSidAlreadyExists) { Console.WriteLine("user already exists in the ACL. quit"); Console.Read(); return; }
if (!newAceAdded && !userSidAlreadyExists)
{
    //we did not hit an inherited ACE - probably none exists in the ACL OR AceCount was 0 - no going through ACEs
    //therefore user was not added
    //AddAccessAllowedAce(ref pNewAcl, aclInfoRevisionOut.AclRevision, ACCESS_MASK.READ_CONTROL | ACCESS_MASK.WINSTA_ALL_ACCESS, userpSid);
    newAceAdded = true;
}
if (!newAceAdded)
{
    Console.WriteLine("failed to add new ACE. quit"); Console.Read(); return;
}

CloseHandle(userToken);
LocalFree(userpSid);
Console.WriteLine("ready to quit");
Console.Read();

Solution

  • As per Selvin's comment:

    The problem was with InitializeAcl signature. It is not out, but in and the function initialized the acl in the address pointed by the IntPtr. In addition to that, I had to change AddAce and AddAccessAllowedAce signatures as well - ref IntPtr pAcl to just in IntPtr.