ADSI corrupt ADAM SID in ACE

ADAM Sid in newly or modified ACEs are corrupted by ADSI during the transfert of these ACEs to the ADAM server. The buggy component is probably the ActiveDs.dll of Windows XP SP2: Windows XP Professional - 5.1.2600.2180 (xpsp_sp2_rtm.040803-2158).

This behaviour is reproductible and happen on all ACLs containing ADAM Sid manipulated throught ADSI with Windows XP. All windows XP users that manipulate ACLs containing ADAM sid are affected.

When the problem occurs, the server respond with a:
System.Runtime.InteropServices.COMException (0x80072035): The server is unwilling to process the request.
when the security descriptor is transmitted for update to the server.

A hotfix is now available, the knowledge base article regarding this issue is KB 896354.

//#define MakeBinaryRoundTrip  // Uncomment to activate a roundtrip from IID => RAW => IID
//#define CorruptSid           // Uncomment to corrupt the initial Sid and shows nothing change in RAW format
//#define PatchSid             // Uncomment to fix the raw format during the roundtrip that should also be enabled

using System;
using System.Runtime.InteropServices;
using System.DirectoryServices;
using ActiveDs;


namespace Test
{
   // <summary>
   // Test case for KB 896354
   // </summary>
   public class SdTest
    {
       public static void Main()
        {
           // Retrieve an DirectoryEntry to modify its ACLs
           DirectoryEntry entry =
               new DirectoryEntry("LDAP://localhost/ou=People,dc=softec,dc=st",
                   "MYDOMAIN\\UserName","mypassword",AuthenticationTypes.Secure);

           // Limit ntSecurityDescriptor transfert to DACL
           entry.Invoke("SetOption",
                (int) ActiveDs.ADS_OPTION_ENUM.ADS_OPTION_SECURITY_MASK,
                (int) ActiveDs.ADS_SECURITY_INFO_ENUM.ADS_SECURITY_INFO_DACL);

           // Retrieve DACL of the entry
           IADsSecurityDescriptor sd = (IADsSecurityDescriptor)
                entry.Properties["nTSecurityDescriptor"].Value;
           // Get the ACL
           IADsAccessControlList cl = (IADsAccessControlList) sd.DiscretionaryAcl;
           // Create a new ACE
           IADsAccessControlEntry ce = new AccessControlEntryClass();
            ce.AccessMask = (int) ADS_RIGHTS_ENUM.ADS_RIGHT_DS_LIST_OBJECT;
            ce.AceFlags = (int) ADS_ACEFLAG_ENUM.ADS_ACEFLAG_INHERIT_ACE;
            ce.AceType = (int) ADS_ACETYPE_ENUM.ADS_ACETYPE_ACCESS_ALLOWED;
            ce.Flags = 0;

#if !CorruptSid
           // This is the SID of an ADAM user retrieved and converted from its objectSid attribute
           ce.Trustee = "S-1-297827799-2113850819-3783973228-1289696667-1737617808-1218521112";
           //                      ^^^ this the important part to look at, based on 8th byte equal D7
           //  When the 4 most significant bit of 8th byte became 07 in place of D7, the same
           //  SID shows S-1-297827591-2113850819-3783973228-1289696667-1737617808-1218521112
#else
           // This is the same SID, but the 4 most significant bits of its 8th byte has been corrupted
           ce.Trustee = "S-1-297827815-2113850819-3783973228-1289696667-1737617808-1218521112";
           //                      ^^^ here is the change, 16 more, which means D7 became E7
#endif

           // Add the ACE at the end of the security descriptor
           cl.AddAce(ce);

#if MakeBinaryRoundTrip
           // Convert IID security descriptor to a Self-relative
           // security descriptor structure
           IADsSecurityUtility su = new ADsSecurityUtilityClass();
           byte [] bsd = (byte []) su.ConvertSecurityDescriptor(sd,
                (int) ADS_SD_FORMAT_ENUM.ADS_SD_FORMAT_IID,
                (int) ADS_SD_FORMAT_ENUM.ADS_SD_FORMAT_RAW);
           
           // Display the raw security descriptor obtained using SDDL
           // The raw ACL has been sorted so the newly added ACE is now first, dunno why ?
           // Note that the newly added ACE has now the wrong SID shown above
           Console.WriteLine( "Raw SD: {0}",
                ConvertSDToStringSD( bsd, ADS_SECURITY_INFO_ENUM.ADS_SECURITY_INFO_DACL ));

#if PatchSid
           // Retrieve a pointer to the DACL from the raw SD
           IntPtr pDacl;
           bool daclPresent;
           bool daclDefaulted;
           if( !GetSecurityDescriptorDacl(bsd,out daclPresent,out pDacl,out daclDefaulted) )
                ThrowLastError();

           // Retrieve a pointer to the first ACE, since the new one is now first
           IntPtr pAce;
           if( !GetAce(pDacl, 0, out pAce) )
                ThrowLastError();

           // Patch the 8th byte of the Sid from 07 to D7
           Console.WriteLine("Corrupted byte patch from {0:X} to D7",Marshal.ReadByte(pAce,15));
            Marshal.WriteByte(pAce,15,0xD7);

           // Redisplay the SD patched to confirm the patch
           Console.WriteLine( "Patched SD: {0}", ConvertSDToStringSD( bsd, ADS_SECURITY_INFO_ENUM.ADS_SECURITY_INFO_DACL ));
#endif

           // Display the initial SD trustees, the newly Added ACE is first
           Console.WriteLine( "Original Trustees:" );
           foreach( IADsAccessControlEntry ceorigin in cl )
                Console.WriteLine( "   {0}", ceorigin.Trustee );

           // Convert the raw SD back to IID format
           IADsSecurityDescriptor sdclone = (IADsSecurityDescriptor) su.ConvertSecurityDescriptor(bsd,
                (int) ActiveDs.ADS_SD_FORMAT_ENUM.ADS_SD_FORMAT_RAW,
                (int) ActiveDs.ADS_SD_FORMAT_ENUM.ADS_SD_FORMAT_IID);
           // Get its ACL
           IADsAccessControlList clclone = (IADsAccessControlList) sdclone.DiscretionaryAcl;
           // Display the cloned SD trustees, if unpatched, it differ from the original one
           Console.WriteLine( "Cloned Trustees:" );
           foreach( IADsAccessControlEntry ceclone in clclone )
                Console.WriteLine( "   {0}", ceclone.Trustee );

           // Store the cloned SD to the directory cache
           entry.Properties["nTSecurityDescriptor"].Value = sdclone;
#else
           // Store the original SD to the directory cache
           entry.Properties["nTSecurityDescriptor"].Value = sd;
#endif
           // commit change to the cache, an exception occurs here if the sd is not patched
           entry.CommitChanges();
        }


// Utility functions and interop wrappers

#if MakeBinaryRoundTrip

        [DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true,
             EntryPoint="ConvertSecurityDescriptorToStringSecurityDescriptor",
             CallingConvention=CallingConvention.Winapi)]

        [return: MarshalAs(UnmanagedType.Bool)]
       private static extern bool ConvertSecurityDescriptorToStringSecurityDescriptor(
            [In()] byte[] pSelfRelativeSD,
           int RequestedStringSDRevision,
           int SecurityInformation,
           out IntPtr StringSecurityDescriptor,
           out int StringSecurityDescriptorLen
            );

       public static string ConvertSDToStringSD(byte[] securityDescriptor, ADS_SECURITY_INFO_ENUM securityInfo)
        {
            IntPtr pStringSD;
           int stringSDLen;
           if( !ConvertSecurityDescriptorToStringSecurityDescriptor(
                securityDescriptor, 1, (int) securityInfo, out pStringSD, out stringSDLen
                ) )
                ThrowLastError();

           try
            {
               return Marshal.PtrToStringAuto(pStringSD, stringSDLen);
            }
           finally
            {
                LocalFree( pStringSD );
            }
        }

        [DllImport("Kernel32", EntryPoint="LocalFree",
             CallingConvention=CallingConvention.Winapi,
             SetLastError=true, CharSet=CharSet.Auto)]

       private static extern IntPtr _LocalFree(IntPtr hMem);
       public static void LocalFree(IntPtr hMem)
        {
           if( _LocalFree(hMem) != IntPtr.Zero )
                ThrowLastError();
        }

       public static void ThrowLastError()
        {
            Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
        }


#if PatchSid

        [DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true,
             EntryPoint="GetSecurityDescriptorDacl",
             CallingConvention=CallingConvention.Winapi)]

        [return: MarshalAs(UnmanagedType.Bool)]
       private static extern bool GetSecurityDescriptorDacl([In()]byte [] pSd,
            [MarshalAs(UnmanagedType.Bool)] out bool daclPresent,
           out IntPtr pDacl,
            [MarshalAs(UnmanagedType.Bool)] out bool daclDefaulted);
           
        [DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true,
             EntryPoint="GetAce",
             CallingConvention=CallingConvention.Winapi)]

        [return: MarshalAs(UnmanagedType.Bool)]
       private static extern bool GetAce(IntPtr pDacl,
           int index,
           out IntPtr pAce);
#endif

#endif

    }
}

This sample code try to update an ACLs in an ADAM repository. A new ACE is created for an ADAM user and added to the ACL of an existing entry. The user SID as been retrieved from the objectSID attribute of the user entry:
01 05 00 00 11 C0 7D D7 C3 CD ....
and converted to string using Win32 API:
S-1-297827799-211...
It is hardcoded in the sample code for simplicity.

Unhandled Exception: System.Runtime.InteropServices.COMException (0x80072035): T
he server is unwilling to process the request.
   at System.DirectoryServices.Interop.IAds.SetInfo()
   at System.DirectoryServices.DirectoryEntry.CommitChanges()
   at Test.SdTest.Main() in c:\test\src\sdtest.cs:line 115

When committing the change of ACLs to the ADAM instance the server reply with a:
System.Runtime.InteropServices.COMException (0x80072035): The server is unwilling to process the request.

After many hours of research, I have added some code (#define MakeBinaryRoundTrip in the sample) to convert the SecurityDescriptor into an SDDL string, which require first to convert the IID SD into a RAW structure self-relative descriptor using an IADsSecurityUtility object.

Raw SD: D:AI(A;CI;LO;;;S-1-297827591-2113850819-3783973228-1289696667-1737617808
-1218521112)(A;CIID;LCRPLORC;;;S-1-297827799-2113850819-514)(A;CIID;CCDCLCSWRPWP
DTLOCRSDRCWDWO;;;S-1-297827799-2113850819-512)
Original Trustees:

   S-1-297827799-2113850819-514
   S-1-297827799-2113850819-512
   S-1-297827799-2113850819-3783973228-1289696667-1737617808-1218521112
Cloned Trustees:
   S-1-297827591-2113850819-3783973228-1289696667-1737617808-1218521112
   S-1-297827799-2113850819-514
   S-1-297827799-2113850819-512

Unhandled Exception: System.Runtime.InteropServices.COMException (0x80072035): T
he server is unwilling to process the request.
   at System.DirectoryServices.Interop.IAds.SetInfo()
   at System.DirectoryServices.DirectoryEntry.CommitChanges()
   at Test.SdTest.Main() in c:\test\src\sdtest.cs:line 115

As the sample shows, when making the conversion of the IID SD into a RAW SD and back into a IID SD, the SID of the newly created ACL change ! It became:
S-1-297827591-211...
notice the 591 in place of 799. Note that the corruption is already present in the RAW form as shown by the convertion of the RAW SD to SDDL string:
01 05 00 00 11 C0 7D 07 C3 CD ....
notice the 4 most significant bit of the 8th byte has been cleared (07 in place of D7). Which means that either the SD converted is wrong or that the conversion from IID to RAW does not perform correctly.

Moreover, if I corrupt these 4bits in the user SID (#define CorruptSid to have E7 (...815...) in place of D7 (...799...)), the SID received back from the IID => RAW => IID convertion is the same again. 

Raw SD: D:AI(A;CI;LO;;;S-1-297827591-2113850819-3783973228-1289696667-1737617808
-1218521112)(A;CIID;LCRPLORC;;;S-1-297827799-2113850819-514)(A;CIID;CCDCLCSWRPWP
DTLOCRSDRCWDWO;;;S-1-297827799-2113850819-512)

Original Trustees:
   S-1-297827799-2113850819-514
   S-1-297827799-2113850819-512
   S-1-297827815-2113850819-3783973228-1289696667-1737617808-1218521112
Cloned Trustees:
   S-1-297827591-2113850819-3783973228-1289696667-1737617808-1218521112
   S-1-297827799-2113850819-514
   S-1-297827799-2113850819-512

Unhandled Exception: System.Runtime.InteropServices.COMException (0x80072035): T
he server is unwilling to process the request.
   at System.DirectoryServices.Interop.IAds.SetInfo()
   at System.DirectoryServices.DirectoryEntry.CommitChanges()
   at Test.SdTest.Main() in c:\test\src\sdtest.cs:line 115

This confirm that these bits are effectively ignored and dropped during the conversion. More tests has shown that this only happen when the SID has been created or change using put_Trustee from the IADsAccessControlEntry interface. If I keep the same SID in an ACE created with the dsacl tool, and just change the AccessMask for example, the ADAM update the ACE correctly.

My final test was to patch the SID in its RAW form (#define PatchSid and undefine #CorruptSid no more needed). So before converting back the RAW SD into IID, I replace the 8th byte of the SID in the ACE by D7, fixing the wrong 07.

Raw SD: D:AI(A;CI;LO;;;S-1-297827591-2113850819-3783973228-1289696667-1737617808
-1218521112)(A;CIID;LCRPLORC;;;S-1-297827799-2113850819-514)(A;CIID;CCDCLCSWRPWP
DTLOCRSDRCWDWO;;;S-1-297827799-2113850819-512)

Corrupted byte patch from 7 to D7
Patched SD: D:AI(A;CI;LO;;;S-1-297827i799-2113850819-3783973228-1289696667-173761
7808-1218521112)(A;CIID;LCRPLORC;;;S-1-297827799-2113850819-514)(A;CIID;CCDCLCSW
RPWPDTLOCRSDRCWDWO;;;S-1-297827799-2113850819-512)
Original Trustees:
   S-1-297827799-2113850819-514
   S-1-297827799-2113850819-512
   S-1-297827799-2113850819-3783973228-1289696667-1737617808-1218521112

Cloned Trustees:
   S-1-297827799-2113850819-3783973228-1289696667-1737617808-1218521112
   S-1-297827799-2113850819-514
   S-1-297827799-2113850819-512

The converted back IID SD now correctly report the SID 799, and is the same than the original SD. Using this patched SD, ADAM accept the change and update the ACL correctly.

My conclusion is that during the transfert of the SD to the ADAM directory, a convertion to RAW form is made and that conversion fails the same way mine fail. The ADAM server obviously refuse to update its ACLs with an unknown SID and is therefore unwilling to process.

A hotfix is now available, the knowledge base article regarding this issue is KB 896354.