Alb*_*reo 2 c# active-directory
我需要从Active Directory对象中提取一些信息,例如配置文件路径,或者用户是否被锁定.
我可以看到这些信息保存在Active Directory对象的User-Parameters属性中,但是该属性的值是一个难以理解的字符的错误字符串:

我可以看到另一个用户遇到了同样的问题,但是关于如何解析该属性还没有明确的解决方案.
如何在保持理智的同时提取我需要的数据?
该用户参数属性值是相当... ... 特别.
虽然属性定义说,该值是Unicode字符串,但现实是有点复杂:该值是使用一对夫妇的"算法",然后编码的混合文本和二进制数据的二进制有效载荷铸造 Unicode字符串-这是为什么你看到垃圾字符,这些字符实际上是纯二进制数据显示为字符串.
互联网上有很多关于如何解码这个值的文章,但是大多数文章都是错误的,或者设法使用错误的程序解码该值(虽然在他们的例子中产生正确的数据,但在任何时候都可能会破坏).
这类文章的例子是:
如何编码数据在终端服务终端服务器运行时接口协议文档中定义.
特别是你正在寻找:
您可以选择查看编码/解码示例,了解如何解码属性值的(真正)简洁示例.
如果你还在我身边,没有失去你的理智,召唤克苏鲁或者自焚,微软里面的任何人都认为做这样的事情是个好主意,让我们继续写代码来解析和提取数据.
Per me si va nelacittà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
(对于那些不耐烦地阅读所有内容的人来说,在答案的最后有一个最小的,完整的,可验证的例子.)
您可以通过System.DirectoryServices(通常为calles"S.DS")或System.DirectoryServices.Protocols(通常称为"S.DS.P")库来获取值.如果您需要有关这些库如何工作的帮助,可以阅读使用System.DirectoryServices搜索Active Directory和System.DirectoryServices.Protocols文章简介.
如果您是.NET Framework用户,这些是您通常的GAC程序集,因此您必须像往常一样添加它们.
如果您是.NET Core用户,请高兴!这两个库已经在2017年11月15日在NuGet上发布 - 尽管是预发行版 - 所以你现在也可以查询Active Directory!去获取它们:System.DirectoryServices,System.DirectoryServices.Protocols.
Nota bene:本答案中的代码是针对.NET Core 2.0目标编写的,如果您不使用.NET Core,则可能需要稍微调整代码; 然而,这些更改应该小而简单,因为库的核心和非核心版本非常相似.
这里我们使用System.DirectoryServices.Protocols读取属性:
var ldapDirectoryIdentifier = new LdapDirectoryIdentifier(
"domain-controller.example.com",
389,
true,
false);
var networkCredential = new NetworkCredential(
"alice@example.com",
"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];
}
Run Code Online (Sandbox Code Playgroud)
现在我们已经拥有了属性的值,我们需要一些东西来保存它包含的值.
首先,我们需要一些枚举.
第一个是CtxCfgFlags1枚举:
[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
}
Run Code Online (Sandbox Code Playgroud)
Nota bene:TSProperty中定义的值缺少几个条目,名称为
Undefined*; 这些值没有在定义中列出但是已经在野外观察,如果你没有定义它们,你的标志枚举将会中断,当你通过调试器查看它时,它将不会很好地显示.ToString().
第二个是CtxShadow枚举:
public enum CtxShadow : uint
{
Disable = 0x00000000,
EnableInputNotify = 0x00000001,
EnableInputNoNotify = 0x00000002,
EnableNoInputNotify = 0x00000003,
EnableNoInputNoNotify = 0x00000004
}
Run Code Online (Sandbox Code Playgroud)
现在我们可以定义类来保存属性:
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; }
}
Run Code Online (Sandbox Code Playgroud)
这里没什么好看的,这些只是TSProperty文档中定义的属性.
现在是时候解码属性值中包含的有效负载了.
该userParameters和TSProperty文件定义了有效载荷的结构.
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);
Run Code Online (Sandbox Code Playgroud)
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);
Run Code Online (Sandbox Code Playgroud)
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++)
Run Code Online (Sandbox Code Playgroud)
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);
Run Code Online (Sandbox Code Playgroud)
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;
}
Run Code Online (Sandbox Code Playgroud)
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.");
}
Run Code Online (Sandbox Code Playgroud)
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.");
}
Run Code Online (Sandbox Code Playgroud)
And we're done!
Nota bene: The
CtxCfgPresentproperty 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 to0xB00B1E55then 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).