[英]How can I read the content of the User-Parameters attribute of an Active Directory Object?
我需要从Active Directory对象中提取一些信息,例如配置文件路径,或者用户是否被锁定。
我可以看到这些信息保存在Active Directory对象的User-Parameters属性中,但是该属性的值是一个难以理解的字符的错误字符串:
我可以看到另一个用户遇到了同样的问题,但是关于如何解析该属性还没有明确的解决方案。
如何在保持理智的同时提取我需要的数据?
User-Parameters属性值非常特别 。
虽然属性定义说,该值是Unicode字符串 ,但现实是有点复杂:该值是使用一对夫妇的“算法”,然后浇铸为Unicode字符串编码的混合文本和二进制数据的二进制有效载荷-这是为什么你看到垃圾字符,这些字符实际上是纯二进制数据显示为字符串。
互联网上有很多关于如何解码这个值的文章,但是大多数都是错误的或者设法使用错误的过程解码该值(虽然在他们的例子中产生正确的数据,但随时可能会中断)。
这类文章的例子是:
如何编码数据在终端服务终端服务器运行时接口协议文档中定义。
特别是你正在寻找:
您可以选择查看编码/解码示例 ,了解如何解码属性值的(真正)简洁示例。
如果你还在我身边,没有失去你的理智,召唤克苏鲁或者激怒了,微软里面的任何人都认为做这样的事情是个好主意,让我们继续写代码来解析和提取数据。
Per me si va nelacittàdolente,
根据我的意见,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];
}
现在我们已经拥有了属性的值,我们需要一些东西来保存它包含的值。
首先,我们需要一些枚举。
第一个是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
}
Nota bene: TSProperty中定义的值缺少几个条目,名为
Undefined*
; 这些值没有在定义中列出但是已经在野外观察,如果你没有定义它们,你的标志枚举将会中断,当你通过调试器或.ToString()
查看时,它将不会很好地显示。
第二个是CtxShadow
枚举:
public enum CtxShadow : uint
{
Disable = 0x00000000,
EnableInputNotify = 0x00000001,
EnableInputNoNotify = 0x00000002,
EnableNoInputNotify = 0x00000003,
EnableNoInputNoNotify = 0x00000004
}
现在我们可以定义类来保存属性:
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; }
}
这里没什么好看的 ,这些只是TSProperty文档中定义的属性。
现在是时候解码属性值中包含的有效负载了。
userParameters和TSProperty文档定义了有效负载的结构。
有效载荷分为两个主要部分:“标题”部分和“数据”部分。
“标题”部分包含
“data”部分是一个未分隔的连续属性列表,每个属性都包含
必须分别从“NameLength”和“ValueLength”字段获取“PropName”和“PropValue”字段的长度。
等待。 “双ASCII编码”? 那是什么?
每个属性都可以包含byte,uint或ASCII字符串作为值。 保存时,属性的值将转换为其二进制表示形式,然后将二进制表示的字节数组转换为十六进制字符串表示形式,然后将十六进制字符串表示形式的每个字符转换为其二进制表示形式,然后进行存储。
对于后代,这是微软解释编码算法的方式:
要为PropValue字段创建编码二进制BLOB,请为输入的每个字节创建其ASCII编码的十六进制表示,并将此表示放在输出缓冲区的2个连续字节中,最重要的十六进制数字首先跟随最不重要的十六进制数字。
例如,如果输入字节包含字符“A”的ASCII表示,则结果输出将是两个ASCII字符的序列:字符“4”后跟字符“1”,因为包含ASCII的字节的十六进制表示形式字符'A'是41。
因此,对应于包含字符“A”的输入缓冲器字节的输出缓冲器将是2个字节的序列,其十六进制表示为34和31。
作为另一个例子,包含ASCII字符串“ABCDE \\ 0”的输入缓冲区将被编码为ASCII字符串“414243444500”(没有终止0),这与12个字节的序列相同,其十六进制表示为34,31 ,34,32,34,33,34,34,34,35,30和30。
很简单,是吗?
让我们一步一步地分解这个过程。
首先,我们需要为属性准备一个容器,并将数据转换为更易于管理的形式:
var userParameters = new UserParameters();
var bytes = Encoding.Unicode.GetBytes(attributeValue);
var memoryStream = new MemoryStream(bytes);
var binaryReader = new BinaryReader(memoryStream, Encoding.Unicode, true);
我们为什么要使用MemoryStream
和BinaryReader
? 因为它真的, 非常容易使用:我们可以简单地调用正在消耗并且正在路上的.ReadBytes(int)
,而不是必须跟踪我们应该开始读取的偏移量。
然后我们解析有效载荷的“标题”部分:
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);
我们不关心reservedData
所以我们可以安全地忽略它。
然而,我们应该关心signature
:当转换为Unicode字符串时,它应该总是等于字符串“P”,如果它没有数据的错误。 如果signatureValue
不等于“P”,我真的鼓励你抛出一个很好的InvalidDataException。
tsPropertyCount
告诉我们要读取多少属性,因此我们将其转换为ushort。
然后我们需要读取与tsPropertyCountValue
告诉我们一样多的属性:
for (var i = 0; i < tsPropertyCountValue; i++)
我们并不真正关心i
,我们只需要根据需要多次执行循环的内容。
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);
如前所述,我们需要获取nameLengthValue
和valueLengthValue
来了解属性名称的长度和属性值的长度。
类似于signature
, typeValue
应始终等于0x01
,即使文档不是很清楚。 就个人而言,如果它不等于0x01
我会抛出一个InvalidDataException。
我们将propName
的值转换回Unicode字符串以获取属性的名称,然后发生魔术。
byte[] propValueValue = GetPropValueValue(propValue);
是神奇发生的地方: GetPropValueValue
将双重ASCII编码的值解码回其原生形式:
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;
}
要将十六进制字节转换回其值,有一个简单的辅助函数:
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.");
}
为什么在重建原始字节时我们使用int而不是byte?
因为在HexToInt
内部执行的减法输出产生一个int,并且HexToInt
和bitmask操作输出是一个int,所以将两个半字节转换为byte是浪费资源,它们将在下一条指令中转换回int。
感谢CodesInChaos的十六进制到字节转换和黑魔法。
现在我们只需要将值转换为正确的类型并将其分配给我们类的正确属性,我们可以使用简单的if-else链来实现:
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.");
}
我们完成了!
Nota bene:
CtxCfgPresent
属性是特殊的,它应该总是存在,它的值应该总是等于0xB00B1E55
(是的,我知道,非常有趣)。 如果它丢失或其值不等于0xB00B1E55
那么该属性的值已损坏且不应使用,我个人建议在这种情况下抛出友好的InvalidDataException。
对于那些设法和我一直待到最后的人,我在GitHub上发布了一个Minimal,Complete和Verifiable示例 (由于字符数限制,我不能在此处包含完整代码)。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.