Friday, January 22, 2010

Calling LsaAddAccountRights from managed code: C# wrapper for Win32 securty API that is missed from .NET library

OK, so today we will learn about dealing with these nasty native code wrappers that you have to deal with eventualy, when you can not find your "favorite" win32 function and you can not think about alternatives. While .NET have System.Securty namespace, some how I was not able to google out the way of Adding Account Rights a "managed way". But no worries, it can be done. We just need a wrapper!

First we have to define the Win32 function so C# will understand how to call it:


[DllImport("advapi32", CharSet = CharSet.Unicode, SetLastError = true),
SuppressUnmanagedCodeSecurityAttribute]
internal static extern uint LsaAddAccountRights(
LSA_HANDLE PolicyHandle,
IntPtr pSID,
LSA_UNICODE_STRING[] UserRights,
int CountOfRights
);


This loooks awfuly fugly and this is not all. You need to define all structures we use as well as few others functions: Win32 functions like to come in groups.

Here is example:

///
/// LSA_UNICODE_STRING structure
///

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct LSA_UNICODE_STRING
{
internal ushort Length;
internal ushort MaximumLength;
[MarshalAs(UnmanagedType.LPWStr)]
internal string Buffer;
}

Now, if you think that calling would be easy after that, you wrong! You have to initalize everthing ourself, allocate memory and copy buffers. It looks at least twice as ugly as you would do it in old good C/C++.

Byte[] buffer = new Byte[sid.BinaryLength];
sid.GetBinaryForm(buffer, 0);

IntPtr pSid = Marshal.AllocHGlobal(sid.BinaryLength);
Marshal.Copy(buffer, 0, pSid, sid.BinaryLength);

LSA_UNICODE_STRING[] privileges = new LSA_UNICODE_STRING[1];

LSA_UNICODE_STRING lsaRights = new LSA_UNICODE_STRING();
lsaRights.Buffer = rights;
lsaRights.Length = (ushort)(rights.Length * sizeof(char));
lsaRights.MaximumLength = (ushort)(lsaRights.Length + sizeof(char));

privileges[0] = lsaRights;

ret = LsaAddAccountRights(lsaHandle, pSid, privileges, 1);

This is a evil mix of "good" and "evil". Managed and unmanged(native) code come together in ugly embrace. Yaaaack...

Now, the code below is just a wraper on one function, you would most likly do it better with proper constractor and Dsipose() and more throws to handle all error conditions ... and add least LsaRemoveAccountRights e.t.c. But this simple exersize left to the reader :-)

Here is a stating point bellow (it works for me, btw) :


using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Principal;
using LSA_HANDLE = System.IntPtr;

/// 
/// LSA_OBJECT_ATTRIBUTES structure
/// 
[StructLayout(LayoutKind.Sequential)]
struct LSA_OBJECT_ATTRIBUTES
{
    internal int Length;
    internal IntPtr RootDirectory;
    internal IntPtr ObjectName;
    internal int Attributes;
    internal IntPtr SecurityDescriptor;
    internal IntPtr SecurityQualityOfService;
}

/// 
/// LSA_UNICODE_STRING structure
/// 
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct LSA_UNICODE_STRING
{
    internal ushort Length;
    internal ushort MaximumLength; 
    [MarshalAs(UnmanagedType.LPWStr)]
    internal string Buffer;
}

/// 
/// Wraps LsaAddAccountRights call.
/// 
public sealed class LsaSecurityWrapper
{
    [DllImport("advapi32", CharSet = CharSet.Unicode, SetLastError = true),
    SuppressUnmanagedCodeSecurityAttribute]
    internal static extern uint LsaOpenPolicy( 
    LSA_UNICODE_STRING[] SystemName, 
    ref LSA_OBJECT_ATTRIBUTES ObjectAttributes,
    int AccessMask,
    out IntPtr PolicyHandle
);

[DllImport("advapi32", CharSet = CharSet.Unicode, SetLastError = true),
    SuppressUnmanagedCodeSecurityAttribute]
    internal static extern uint LsaAddAccountRights(
    LSA_HANDLE PolicyHandle,
    IntPtr pSID,
    LSA_UNICODE_STRING[] UserRights,
    int CountOfRights
);

[DllImport("advapi32", CharSet = CharSet.Unicode, SetLastError = true),
    SuppressUnmanagedCodeSecurityAttribute]
    internal static extern uint LsaRemoveAccountRights(
    LSA_HANDLE PolicyHandle,
    IntPtr AccountSid,
    bool AllRights,
    LSA_UNICODE_STRING[] UserRights,
    int CountOfRights
);

[DllImport("advapi32")]
internal static extern int LsaClose(IntPtr PolicyHandle);

enum Access : int
{ 
    POLICY_READ = 0x20006,
    POLICY_ALL_ACCESS = 0x00F0FFF,
    POLICY_EXECUTE = 0X20801,
    POLICY_WRITE = 0X207F8
}

// rights: (http://msdn.microsoft.com/en-us/library/bb545671(VS.85).aspx)
public static void AddAccountRights(SecurityIdentifier sid, string rights)
{
    IntPtr lsaHandle;

    LSA_UNICODE_STRING[] system = null;
    LSA_OBJECT_ATTRIBUTES lsaAttr;
    lsaAttr.RootDirectory = IntPtr.Zero;
    lsaAttr.ObjectName = IntPtr.Zero;
    lsaAttr.Attributes = 0;
    lsaAttr.SecurityDescriptor = IntPtr.Zero;
    lsaAttr.SecurityQualityOfService = IntPtr.Zero;
    lsaAttr.Length = Marshal.SizeOf(typeof(LSA_OBJECT_ATTRIBUTES));
    lsaHandle = IntPtr.Zero;

    uint ret = LsaOpenPolicy(system, ref lsaAttr, (int)Access.POLICY_ALL_ACCESS,   out lsaHandle);
    if (ret == 0)
    {
        Byte[] buffer = new Byte[sid.BinaryLength];
        sid.GetBinaryForm(buffer, 0);

        IntPtr pSid = Marshal.AllocHGlobal(sid.BinaryLength);
        Marshal.Copy(buffer, 0, pSid, sid.BinaryLength);

        LSA_UNICODE_STRING[] privileges = new LSA_UNICODE_STRING[1];

        LSA_UNICODE_STRING lsaRights = new LSA_UNICODE_STRING();
        lsaRights.Buffer = rights;
        lsaRights.Length = (ushort)(rights.Length * sizeof(char));
        lsaRights.MaximumLength = (ushort)(lsaRights.Length + sizeof(char));

        privileges[0] = lsaRights;

        ret = LsaAddAccountRights(lsaHandle, pSid, privileges, 1);

        LsaClose(lsaHandle);

        Marshal.FreeHGlobal(pSid);

        if (ret != 0)
        {
             throw new Win32Exception("LsaAddAccountRights failed with error code: " + ret);
        }
    }
    else
    {
        throw new Win32Exception("LsaOpenPolicy failed with error code: " + ret);
    }
  }
}