Reputation: 11095
I need to extract some information from an Active Directory object, such as the profile path or if the user is locked out or not.
I can see these information are saved inside the User-Parameters attribute of the Active Directory object, but the value of this attribute is a mangled string of incomprehensible characters:
I can see that another user had the same problem, but there's not clear solution on how to parse that attribute.
How can I extract the data I need while maintaining my sanity?
Upvotes: 4
Views: 6195
Reputation: 11095
The User-Parameters attribute value is quite… particular.
While the attribute definition says that the value is a Unicode string, the reality is a little more complex: the value is a binary payload of mixed text and binary data encoded using a couple of "algorithms" and then casted to a Unicode string — that's why you see junk characters, these character are actually pure binary data displayed as a string.
There are many articles on the internet on how to decode this value, but most of them are either wrong or manage to decode the value using a wrong procedure (which, while yielding correct data in their examples, could break at any time).
Examples of such articles are:
How the data is encoded is defined in the Terminal Services Terminal Server Runtime Interface Protocol documentation.
In particular you're looking for:
Optionally you can have a look to the Encoding/Decoding Example for a (really) terse example on how to decode the properties values.
If you're still with me, didn't lose your sanity, summoned Cthulhu or set on fire whoever inside Microsoft ever thought that doing something like this was a good idea, let's proceed to write the code to parse and extract the data.
Per me si va ne la città dolente,
per me si va ne l'etterno dolore,
per me si va tra la perduta gente.
[…]
Lasciate ogni speranza, voi ch'entrate.— Dante Alighieri, Divina Commedia, Inferno, Canto III
(There's a Minimal, Complete, and Verifiable example linked at the end of the answer for those who are too impatient to read everything.)
You can get the value through either the System.DirectoryServices (usually calles "S.DS") or System.DirectoryServices.Protocols (usually called "S.DS.P") libraries. If you need some help on how these libraries work you can read the Using System.DirectoryServices to Search the Active Directory and Introduction to System.DirectoryServices.Protocols articles.
If you are a .NET Framework user, these are your usual GAC assemblies so you'll have to add them as you always do.
If you are a .NET Core user, rejoice! Both libraries have been released on NuGet — albeit as a pre-release — on 2017-11-15, so you too can query Active Directory now! Go fetch them: System.DirectoryServices, System.DirectoryServices.Protocols.
Nota bene: The code in this answer has been written for a .NET Core 2.0 target, if you're not using .NET Core you probably have to adjust the code slightly; these changes however should be small and easy as the Core and non-Core version of the libraries are very similar.
Here we're reading the attribute using System.DirectoryServices.Protocols:
var ldapDirectoryIdentifier = new LdapDirectoryIdentifier(
"domain-controller.example.com",
389,
true,
false);
var networkCredential = new NetworkCredential(
"[email protected]",
"p@sSw0rd",
"example.com");
var ldapConnection = new LdapConnection(
ldapDirectoryIdentifier,
networkCredential,
AuthType.Kerberos);
var searchRequest = new SearchRequest(
"DC=example,DC=com",
"(objectClass=user)",
SearchScope.Subtree,
"userParameters");
// WARNING
// If the parameters of either LdapDirectoryIdentifier or NetworkCredential are wrong
// (e.g. invalid credentials) you'll get an exception here.
var searchResponse = (SearchResponse) ldapConnection.SendRequest(searchRequest);
foreach (SearchResultEntry searchResultEntry in searchResponse.Entries)
{
// WARNING
// This WILL throw an exception when used on an object where the attribute is missing.
// You should really check that the attribute exists and has exactly one value.
// I skipped that for brevity, you should not.
var directoryAttribute = searchResultEntry.Attributes["userParameters"];
var attributeValue = (string) directoryAttribute[0];
}
Now that we have the value of the attribute, we need something to hold the values it contains.
First of all, we need some enums.
The first one is the CtxCfgFlags1
enum:
[Flags]
public enum CtxCfgFlags1 : uint
{
Undefined1 = 0x00000000,
Undefined2 = 0x00000001,
Undefined3 = 0x00000002,
DisableCam = 0x00000004,
WallpaperDisabled = 0x00000008,
DisableExe = 0x00000010,
DisableClip = 0x00000020,
DisableLpt = 0x00000040,
DisableCcm = 0x00000080,
DisableCdm = 0x00000100,
DisableCpm = 0x00000200,
UseDefaultGina = 0x00000400,
HomeDirectoryMapRoot = 0x00000800,
DisableEncryption = 0x00001000,
ForceClientLptDef = 0x00002000,
AutoClientLpts = 0x00004000,
AutoClientDrives = 0x00008000,
LogonDisabled = 0x00010000,
ReconnectSame = 0x00020000,
ResetBroken = 0x00040000,
PromptForPassword = 0x00080000,
InheritSecurity = 0x00100000,
InheritAutoClient = 0x00200000,
InheritMaxIdleTime = 0x00400000,
InheritMaxdisconnectionTime = 0x00800000,
InheritMaxsessionTime = 0x01000000,
InheritShadow = 0x02000000,
InheritCallbackNumber = 0x04000000,
InheritCallback = 0x08000000,
Undefined4 = 0x10000000,
Undefined5 = 0x20000000,
Undefined6 = 0x40000000,
Undefined7 = 0x80000000
}
Nota bene: The values defined in TSProperty are missing a couple of entries, the ones named
Undefined*
; these values are not listed in the definition but have been observed in the wild, if you don't define them your flag enum will break and won't be displayed nicely when you look at it through the debugger or.ToString()
.
The second one is the CtxShadow
enum:
public enum CtxShadow : uint
{
Disable = 0x00000000,
EnableInputNotify = 0x00000001,
EnableInputNoNotify = 0x00000002,
EnableNoInputNotify = 0x00000003,
EnableNoInputNoNotify = 0x00000004
}
Now we can define the class to hold the properties:
public class UserParameters
{
public uint? CtxCfgPresent { get; set; }
public CtxCfgFlags1? CtxCfgFlags1 { get; set; }
public uint? CtxCallBack { get; set; }
public uint? CtxKeyboardLayout { get; set; }
public byte? CtxMinEncryptionLevel { get; set; }
public uint? CtxNwLogonServer { get; set; }
public string CtxWfHomeDir { get; set; }
public string CtxWfHomeDirDrive { get; set; }
public string CtxInitialProgram { get; set; }
public uint? CtxMaxConnectionTime { get; set; }
public uint? CtxMaxDisconnectionTime { get; set; }
public uint? CtxMaxIdleTime { get; set; }
public string CtxWfProfilePath { get; set; }
public CtxShadow? CtxShadow { get; set; }
public string CtxWorkDirectory { get; set; }
public string CtxCallbackNumber { get; set; }
}
Nothing fancy here, these are simply the properties defined in the TSProperty document.
Now it's time to decode the payload contained inside the value of the attribute.
The userParameters and TSProperty documents define the structure of the payload.
The payload is divided in two main sections: a "header" section and a "data" section.
The "header" section contains
The "data" section is an unseparated contiguous list of properties, each property contains
The length of the "PropName" and "PropValue" fields must be obtained from the "NameLength" and "ValueLength" fields respectively.
Wait. "double-ASCII-encoded"? What's that?
Each property can have as a value either a byte, a uint, or an ASCII string. When saved the value of the property is converted to its binary representation, then the byte-array of the binary representation is converted to it hexadecimal string representation, then each character of the hexadecimal string representation is converted to its binary representation and then stored.
For posterity, this is how the encoding algorithm is explained by Microsoft:
To create the encoded binary BLOB for the PropValue field, for each byte of the input create its ASCII-encoded hexadecimal representation and place this representation in 2 consecutive bytes of the output buffer, the most significant hexadecimal digit first followed by the least significant hexadecimal digit.
For example, if the input byte contains the ASCII representation of character 'A', the resulting output will be a sequence of two ASCII characters: character '4' followed by character '1' because the hexadecimal representation of a byte that contains the ASCII character 'A' is 41.
Hence, the output buffer corresponding to the input buffer byte containing character 'A' will be a sequence of 2 bytes whose hexadecimal representations are 34 and 31.
As another example, the input buffer containing the ASCII string "ABCDE\0" would be encoded into the ASCII string "414243444500" (without the terminating 0), which is the same as a sequence of 12 bytes whose hexadecimal representations are 34, 31, 34, 32, 34, 33, 34, 34, 34, 35, 30, and 30.
Simple, yes?
Let's break down the process step-by-step.
First of all, we need to prepare a container for the properties and the convert the data to a more manageable form:
var userParameters = new UserParameters();
var bytes = Encoding.Unicode.GetBytes(attributeValue);
var memoryStream = new MemoryStream(bytes);
var binaryReader = new BinaryReader(memoryStream, Encoding.Unicode, true);
Why are we using MemoryStream
and BinaryReader
? Because it's really, really easier to use: instead of having to keep track of the offset where we should start to read from as we gradually proceed, we can simply call .ReadBytes(int)
which is consuming and be on our way.
Then we parse the "header" section of the payload:
byte[] reservedData = binaryReader.ReadBytes(96);
byte[] signature = binaryReader.ReadBytes(2);
byte[] tsPropertyCount = binaryReader.ReadBytes(2);
string signatureValue = Encoding.Unicode.GetString(signature);
ushort tsPropertyCountValue = BitConverter.ToUInt16(tsPropertyCount, 0);
We don't care about reservedData
so we can safely ignore it.
We however should care about signature
: when converted to a Unicode string it should always be equal to the string "P", if it's not there's something wrong with the data. I really encourage you to throw a nice InvalidDataException if signatureValue
is not equal to "P".
tsPropertyCount
tells us how many properties we have to read, so we convert it to a ushort.
Then we need to read as many property as tsPropertyCountValue
tells us:
for (var i = 0; i < tsPropertyCountValue; i++)
We don't really care for i
, we only need to execute the content of the loop as many times as required.
byte[] nameLength = binaryReader.ReadBytes(2);
byte[] valueLength = binaryReader.ReadBytes(2);
byte[] type = binaryReader.ReadBytes(2);
ushort nameLengthValue = BitConverter.ToUInt16(nameLength, 0);
ushort valueLengthValue = BitConverter.ToUInt16(valueLength, 0);
ushort typeValue = BitConverter.ToUInt16(type, 0);
byte[] propName = binaryReader.ReadBytes(nameLengthValue);
byte[] propValue = binaryReader.ReadBytes(valueLengthValue);
string propNameValue = Encoding.Unicode.GetString(propName);
byte[] propValueValue = GetPropValueValue(propValue);
As previously stated we need to obtain nameLengthValue
and valueLengthValue
to know the length of the name of the property and the length of the value of the property.
typeValue
, similarly to signature
, should always be equal to 0x01
, even if the documentation is not really clear about it. Personally I'd throw an InvalidDataException if it's not equal to 0x01
.
We convert the value of propName
back to a Unicode string to get the name of the property, and then the magic happens.
byte[] propValueValue = GetPropValueValue(propValue);
is where the magic happens: GetPropValueValue
decodes the doubly-ASCII-encoded value back to its native form:
private static byte[] GetPropValueValue(byte[] propValue)
{
// Since the encoding algorithm doubles the space used, we halve it.
var propValueValue = new byte[propValue.Length / 2];
// Parse the encoded bytes two-by-two, since the encoding algorithm transforms
// one bytes in two bytes we need to read two of them to obtain the original one.
for (var j = 0; j < propValue.Length; j = j + 2)
{
// Compute the two halves (nibbles) of the original byte from the values of the
// two encoded bytes. Each encoded bytes is actually an hexadecimal character,
// so each encoded byte can only have a value between 48 and 57 ('0' to '9')
// or between 97 and 102 ('a' to 'f'). Yes, it's an utter waste of space.
var highNibble = HexToInt(propValue[j]);
var lowNibble = HexToInt(propValue[j + 1]);
// Recreate the original byte from the two nibbles.
propValueValue[j / 2] = (byte) (highNibble << 4 | lowNibble);
}
return propValueValue;
}
To convert the hexadecimal byte back to its value there's a simple helper function:
private static int HexToInt(byte value)
{
if ('0' <= value && value <= '9')
{
return value - '0';
}
if ('a' <= value && value <= 'f')
{
return value - 'a' + 10;
}
if ('A' <= value && value <= 'F')
{
return value - 'A' + 10;
}
throw new Exception("Invalid character.");
}
Why are we using int instead of byte when rebuilding the original byte?
Because the output of the subtraction performed inside HexToInt
produces an int, and the bitshift and bitmask operations output is an int, so converting the two nibbles to byte is a waste of resources, they would be converted back to int in the next instruction.
Thanks to CodesInChaos for his hex-to-byte conversion and black magic.
Now we only have to convert the value to the correct type and assign it to the correct property of our class, we can do that with a simple if-else chain:
if (string.Equals(propNameValue, nameof(UserParameters.CtxCfgPresent), StringComparison.OrdinalIgnoreCase))
{
userParameters.CtxCfgPresent = BitConverter.ToUInt32(propValueValue, 0);
}
else if (string.Equals(propNameValue, nameof(UserParameters.CtxCfgFlags1), StringComparison.OrdinalIgnoreCase))
{
userParameters.CtxCfgFlags1 = (CtxCfgFlags1) BitConverter.ToUInt32(propValueValue, 0);
}
else if (string.Equals(propNameValue, nameof(UserParameters.CtxCallBack), StringComparison.OrdinalIgnoreCase))
{
userParameters.CtxCallBack = BitConverter.ToUInt32(propValueValue, 0);
}
else if (string.Equals(propNameValue, nameof(UserParameters.CtxKeyboardLayout), StringComparison.OrdinalIgnoreCase))
{
userParameters.CtxKeyboardLayout = BitConverter.ToUInt32(propValueValue, 0);
}
else if (string.Equals(propNameValue, nameof(UserParameters.CtxNwLogonServer), StringComparison.OrdinalIgnoreCase))
{
userParameters.CtxNwLogonServer = BitConverter.ToUInt32(propValueValue, 0);
}
else if (string.Equals(propNameValue, nameof(UserParameters.CtxMaxConnectionTime), StringComparison.OrdinalIgnoreCase))
{
userParameters.CtxMaxConnectionTime = BitConverter.ToUInt32(propValueValue, 0);
}
else if (string.Equals(propNameValue, nameof(UserParameters.CtxMaxDisconnectionTime), StringComparison.OrdinalIgnoreCase))
{
userParameters.CtxMaxDisconnectionTime = BitConverter.ToUInt32(propValueValue, 0);
}
else if (string.Equals(propNameValue, nameof(UserParameters.CtxMaxIdleTime), StringComparison.OrdinalIgnoreCase))
{
userParameters.CtxMaxIdleTime = BitConverter.ToUInt32(propValueValue, 0);
}
else if (string.Equals(propNameValue, nameof(UserParameters.CtxShadow), StringComparison.OrdinalIgnoreCase))
{
userParameters.CtxShadow = (CtxShadow) BitConverter.ToUInt32(propValueValue, 0);
}
else if (string.Equals(propNameValue, nameof(UserParameters.CtxMinEncryptionLevel), StringComparison.OrdinalIgnoreCase))
{
userParameters.CtxMinEncryptionLevel = propValueValue[0];
}
else if (string.Equals(propNameValue, nameof(UserParameters.CtxWfHomeDir), StringComparison.OrdinalIgnoreCase))
{
userParameters.CtxWfHomeDir = Encoding.ASCII.GetString(propValueValue, 0, propValueValue.Length - 1);
}
else if (string.Equals(propNameValue, nameof(UserParameters.CtxWfHomeDirDrive), StringComparison.OrdinalIgnoreCase))
{
userParameters.CtxWfHomeDirDrive = Encoding.ASCII.GetString(propValueValue, 0, propValueValue.Length - 1);
}
else if (string.Equals(propNameValue, nameof(UserParameters.CtxInitialProgram), StringComparison.OrdinalIgnoreCase))
{
userParameters.CtxInitialProgram = Encoding.ASCII.GetString(propValueValue, 0, propValueValue.Length - 1);
}
else if (string.Equals(propNameValue, nameof(UserParameters.CtxWfProfilePath), StringComparison.OrdinalIgnoreCase))
{
userParameters.CtxWfProfilePath = Encoding.ASCII.GetString(propValueValue, 0, propValueValue.Length - 1);
}
else if (string.Equals(propNameValue, nameof(UserParameters.CtxWorkDirectory), StringComparison.OrdinalIgnoreCase))
{
userParameters.CtxWorkDirectory = Encoding.ASCII.GetString(propValueValue, 0, propValueValue.Length - 1);
}
else if (string.Equals(propNameValue, nameof(UserParameters.CtxCallbackNumber), StringComparison.OrdinalIgnoreCase))
{
userParameters.CtxCallbackNumber = Encoding.ASCII.GetString(propValueValue, 0, propValueValue.Length - 1);
}
else
{
throw new Exception("Unsupported property.");
}
And we're done!
Nota bene: The
CtxCfgPresent
property is special, it should always be present and its value should always be equal to0xB00B1E55
(yes, I know, very funny). If it's missing or its value is not equal to0xB00B1E55
then the value of the attribute is corrupted and should not be used, personally I suggest throwing our friendly InvalidDataException in such cases.
For those who managed to stay with me until the end, I've published a Minimal, Complete, and Verifiable example on GitHub (I can't include the full code here due to character count limitations).
Upvotes: 15