利用 LINQ to XML 查询混淆图





5.00/5 (4投票s)
LINQ to XML 技术实践应用。
引言
在我过去的工作经历中,我曾处理过一些关于我们公司某个产品的用户错误报告。这些报告包含了调用堆栈信息,旨在帮助我们检测错误的根源。
我们在生产代码上使用了一个混淆工具,因此错误报告提供的调用堆栈信息需要结合混淆映射进行一些“查找”,以及手动文本搜索。这种“查找”并不总是容易的事情——混淆映射是一个巨大的 XML 文件,大小超过 25 MB,而大多数文本编辑器都无法很好地处理如此大的信息量。这些编辑器之所以有这样的偏好是合理的,因为一个普通的人类创建的文件很少会超过 1 MB 的边界。
当您需要语法高亮,甚至更进一步——XML 树解析/导航时,情况会变得更糟。另一个大问题是存在大量的 XML 元素具有相同的混淆名称,要识别它们的类型,您需要手动分析父 XML 元素。
任务定义
面对这些问题,我决定通过自动化名称解析过程来帮助我们的支持团队。自动化任务的要求如下:
- 应该有一个工具,允许根据混淆名称查找原始的类或类成员名称。
- 用户界面应尽可能简单——我认为简单的任务不应该需要复杂的用户操作。
- 使用 LINQ to XML——这是一种方便易用的处理 XML 数据的方式。但也许最主要的因素是,我终于有机会在实践中使用这项技术了。
让我们进入具体的步骤。首先,我们定义输入数据。
这是调用堆栈内容示例
Type: System.ArgumentException
Stack:
at System.ThrowHelper.ThrowArgumentException(ExceptionResource resource)
at System.Collections.Generic.Dictionary.Insert(TKey key, TValue value, Boolean add)
at ne.c(IError A_0)
at ne.c(ErrorListEventArgs A_0)
at ne.c.a()
at ne.c(Object A_0, EventArgs A_1)
at System.Windows.Forms.Timer.OnTick(EventArgs e)
at System.Windows.Forms.Timer.TimerNativeWindow.WndProc(Message& m)
at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg,
IntPtr wparam, IntPtr lparam)
我们公司使用 Dotfuscator 工具,其社区版随 Visual Studio 一起提供。所有混淆器的混淆映射格式都相同,因此任何人都可以使用自己的代码测试此名称解析工具。混淆映射是一个 XML 文件,其结构如下
<dotfuscatorMap version="1.1">
<header /> <!--Provides timestamp and version information.-->
<mapping> <!--Mapping information.-->
<module> <!--Module mapping include module’s types.-->
<name>ModuleName.dll</name> <!--Original module name.-->
<type /> <!--Type name and members mapping.-->
...
<type/> <!--Other type name and members mapping.-->
</module>
</mapping>
<statistics /> <!--Some obfuscation statistics.-->
</dotfuscatorMap>
<type>
元素的结构如下
<type>
<name>type_name</name> <!--Original type name.-->
<newname>obfuscated_name</newname> <!--Obfusctaed type name (Optional).-->
<methodlist> <!--Type’s method list.-->
<method>
<signature>void(object, System.EventArgs)</signature> <!--Method signature.-->
<name>method_name</name> <!--Original method name.-->
<newname>obfuscated_name</newname> <!--Obfusctaed method name (Optional).-->
</method>
...
</methodlist>
<fieldlist> <!--Types filed list.-->
<field>
<signature>System.Windows.Forms.Button</signature> <!--Field signature.-->
<name>field_name</name> <!--Original field name.-->
<newname>obfuscated_name</newname> <!--Obfuscated field name (Optional).-->
</field>
...
</fieldlist>
</type>
您会注意到,混淆名称始终放在可选的 <newname>
元素中。如果省略此元素,则对象使用其原始名称。
接下来,我们应该定义用户输入。例如,我们需要查找一个混淆名称为“a”的类型。通常,我们搜索“<newname>a</newname>”字符串——这将找到所有混淆名称为“a”的类型、方法和字段。在复杂的项目中,大约有数千个结果。为了实现我们的搜索目标,我们需要分析父元素并检测它是否是 <type>
元素。
因此,用户通常使用两个参数:第一个参数是混淆映射文件的路径,第二个参数是混淆名称。还有一个(隐式)参数——搜索结果类型(类型/方法/字段),但我们将尝试从第二个参数推断此参数。根据 UI 简洁性的要求,我认为这足够了。
类型名称搜索
第一个任务是使用混淆名称搜索原始类型名称。让我们来做。首先,我们需要列出映射文件中的所有类型。这很简单
// Construct the XElement to access map file.
XElement map = XElement.Load("sample.xml");
var types = map.Descendants("type");
这里的主要操作是 map.Descendants("type")
调用,它会返回 XElement
内容中的所有 <type>
元素。
Descendants()
方法返回一个普通的后代 XML 元素集合。这个集合包括子元素、孙子元素等。因此,如果我们写 map.Descendants()
,我们将获得映射文档中所有 XML 元素的枚举。这个方法有一个重载,允许通过指定匹配元素名称过滤器来过滤输出集合。我使用了这个重载来过滤掉除 <type>
之外的所有元素。
注意:过滤器名称应该是完全限定名称;这意味着,如果被过滤的元素有命名空间,过滤器名称也必须带有命名空间。
注意:请记住,Descendants
使用延迟执行,这意味着对底层 XML 的实际访问将在您首次访问 Descendants
结果时执行,而不是在调用此函数时执行。
map.Descendants("type")
将扫描整个 XML 树来查找指定的元素类型;这不是最有效的解决方案,但它是最简单的。使用直接元素导航可以减少整个 XML 扫描,从而更有效率。例如,我们可以使用这样的表达式
var types = map.Elements("mapping").Elements("module").Elements("type");
根据 XML 内容,此表达式可以提供比 Descendants
调用快十倍的性能。但对于这个应用程序,我更喜欢 Descendants
函数的简洁性。
现在我们有了所有的 <type> 元素,需要找到与混淆名称匹配的项。我使用 LINQ 查询来实现这一点:
string obfuscatedName = "a"; // Define the search criteria.
var found = from type in types
// Filter type elements by obfuscatedName matching.
where type.Element("newname").Value == obfuscatedName
select type;
types
集合通过匹配类型的子元素 <newname>
的内容与传入的混淆名称来过滤。这也可以使用带有 lambda 表达式的 Where
扩展方法来完成:
var found = types.Where(t => t.Element("newname").Value == obfuscatedName);
如前所述,<newname>
元素是可选的,因此当类型未混淆时,Element("newname")
返回 null
。为了避免可能的 NRE,我将 LINQ 查询修改为以下内容:
var found = from type in map.Descendants("type")
// Declare name element variable.
let name = type.Element("newname") ?? type.Element("name")
// Filter type elements by obfuscatedName matching.
where name.Value == obfuscatedName
select type;
此代码将搜索混淆名称或原始名称与 obfuscatedName
匹配的类型。
let
关键字引入了一个新变量 name
,它保存 <newname>
元素或在没有 <newname>
元素时保存 <name>
元素。这个新变量是一个匿名类型,它由当前的 <type>
元素和一个 <name>
/<newname>
元素组成。类似于这样:
new { Type = type, Name = type.Element("newname") ?? type.Element("name") };
整个查询可以用 C# 表示为:
IEnumerable<xelement> found = map.Descendants("type").
Select(type => new { Type = type, Name = type.Element("newname") }).
Where(tn => tn.Name.Value == obfuscatedName).
Select(tn => tn.Type);
正如我们所见, there is the second Select
function call, which (in conjunction with the anonymous type projection) will give us some performance penalty, so I rewrite the query to the following
var found = from type in map.Descendants("type")
// Filter type elements by obfuscatedName matching.
where (type.Element("newname") ?? type.Element("name")).Value ==
obfuscatedName
select type;
接下来要做的是处理复杂的类型名称。在 XML 中,这些名称用“/”分隔而不是“.”;例如,“MyClass.MyInternalClass” 名称由 “MyClass/MyInternalClass” 字符串值表示。我们只需要在 obfuscatedName
变量中将“.”替换为“/”即可匹配:
obfuscatedName = obfuscatedName.Replace('.', '/');
最后,我们提供匿名类型投影,这将帮助我们在 C# 中处理搜索结果:
var types = from type in found
select new {
ModuleName = type.Parent.Element("name").Value,
TypeName = type.Element("name").Value
};
之后,您可以按需处理搜索结果,例如将其输出到控制台:
foreach (var type in types) {
Console.WriteLine("Module: {0}", type.ModuleName);
Console.WriteLine("Type: {0}", type.TypeName);
Console.WriteLine();
}
摘要
就是这样。我们已经找到了提供混淆名称的类型。在下一部分中,我将深入探讨 LINQ 查询,提供字段和方法名称解析解决方案。
感谢您的时间,欢迎提出任何问题或建议。