65.9K
CodeProject 正在变化。 阅读更多。
Home

使用未文档化的内部 .NET 类方法读取 PE 映像清单资源

starIconstarIconstarIconstarIconstarIcon

5.00/5 (7投票s)

2013年1月20日

CPOL

7分钟阅读

viewsIcon

33069

这篇简短的文章展示了如何使用 System.Deployment.Application.Win32InterOp 命名空间中未文档化的内部类方法来获取 PE 映像的清单资源。

引言

这是一篇简短的文章,它向您展示了如何通过访问内部命名空间中未文档化的内部 .NET Framework 类来读取 PE 映像的清单资源,方法是将包含该类的程序集加载到执行的程序集中,然后导入并实例化该内部类,最后通过反射和动态调用来调用该方法。此外,它还展示了如何将代码封装在一个可重用的 static 方法中,以及如何正确地转换返回的 datastream。最终结果将是一个 XmlDocument 对象类实例,该实例提供了一种便捷的方式,通过遍历 .NET Framework 的 XML/DOM 函数来探索清单的内部结构。 

背景  

我一直在寻找一种快速简便的方法来读取 PE 映像中的清单文件,就像资源编辑器、IDE 或调试器/反汇编器可以做的那样,但不幸的是,.NET 运行时并没有提供一种简单的方法来做到这一点,至少到目前为止我不知道有。标准的“最简单”方法是使用 P/Invoke,通过使用 Windows API 资源管理函数来访问编译到 PE 映像资源节中的清单数据。获取所需信息的步骤如下:

  1. LoadLibrary(Ex)
  2. FindResource 
  3. SizeOfResource  
  4. LoadResource
  5. LockResource
  6. CopyMemory 或类似合适的内存复制函数,如 Marshal.Copy(...)
  7. FreeLibrary


另一种方法是读取原始 PE 映像的二进制数据,并一步一步地解析其内容,直到获得所需的信息,但这非常困难,因为您必须访问 PE 映像的字节和位,任何一个错误的偏移量计算或数据误解都可能导致完全无用的信息或损坏的结果数据。另一种方法是使用 Debug Help Library API,但这又是大量的 P/invoke,我们不想这样做。还有另一种完全“托管”的方法可以做到这一点。

查看 .NET Framework 的源代码,我在一个未文档化的命名空间中发现了一个未文档化的类/方法,它恰好为我完成了上述所有步骤,而且我不需要导入/声明任何额外的、不必要的 P/invoke 签名。我们使用的方法名为 GetManifestFromPEResources(...),而实现该方法的类是位于 System.Deployment.Application.Win32InterOpt 命名空间中的内部(internal class)声明的 SystemUtils 类。实现该类的二进制程序集 DLL 文件在我的 32 位 .NET 安装目录的“\Windows\Microsoft.NET\Framework\v2.0.50727\”中命名为 System.Deployment.dll。在 64 位 .NET 目录中也可以找到相同的库。该方法内部使用上述完全相同的 Windows API 函数,通过 P/Invoke 访问它们(就像任何 .NET 版本中的大多数运行时类一样,它们通过访问 Windows API 或 Windows Runtime 来完成某些任务),但好消息是,所有这些都可以通过对这个未文档化函数的一次调用来完成,而我们不必详细了解它内部的细节(但我们知道它正是我们想要做的)。一旦我们获取了清单数据,我们会适当地转换它,并从中创建一个 XmlDocument 对象,以便于访问 XML 对象,因为我们知道清单是纯文本 XML,但这都已在代码中进行了解释。

使用方法

代码本身非常直观,并包含一些基本的注释来解释步骤。代码中使用的类已使用其完整的命名空间限定符进行声明,以明确其在框架运行时中的来源: 

所需函数的签名如下: 

public static byte[] GetManifestFromPEResources(string filePath) 

完整的代码如下:

C# 代码

//Overloaded simplified static method returning
//only the XmlDocument or null on failure
public static System.Xml.XmlDocument GetPEFileManifest(
    System.String fileName)
{
    System.Xml.XmlDocument xmld;
    System.Exception err;

    GetPEFileManifest(
        fileName,
        out xmld,
        out err);

    return xmld;
}

//Full static method returning error
//information on failure
public static System.Boolean GetPEFileManifest(
    System.String fileName,
    out System.Xml.XmlDocument applicationXmlManifest,
    out System.Exception error) 
{
    try
    {
        //First check valid input parameters
        if(System.String.IsNullOrEmpty(fileName) == true)
            throw new System.NullReferenceException("Parameter \"fileName\" cant be null or empty");

        //First check if file is valid
        if(System.IO.File.Exists(fileName) == false)
            throw new System.IO.FileNotFoundException
                ("Parameter \"fileName\" does not point to a existing file");

        //Load System.Deployment.dll
        System.Reflection.Assembly SystemDeploymentAssembly = System.Reflection.Assembly.Load(
            "System.Deployment, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");

        //Get SystemUtils class from assembly
        System.Type SystemUtilsClass = SystemDeploymentAssembly.GetType(
          "System.Deployment.Application.Win32InterOp.SystemUtils");

        //Create instance of SystemUtils class 
        System.Object SystemUtils = System.Activator.CreateInstance(SystemUtilsClass);

        //Invoke the internal method named "GetManifestFromPEResources" via reflection
        System.Byte[] ManifestBytes = SystemUtils.GetType().InvokeMember(
            "GetManifestFromPEResources",
            System.Reflection.BindingFlags.InvokeMethod |
            System.Reflection.BindingFlags.Public |
            System.Reflection.BindingFlags.Static,
            null,
            SystemUtils, 
            new System.Object[] { fileName }) as System.Byte[];

        //The string holding the final xml text
        string ManifestXmlString = string.Empty;

        //Read bytes with memory stream and stream reader to make sure
        //to get the right encoded data, because some of the resources do have a BOM (Byte Order Mark)
        //Read this for more information: http://en.wikipedia.org/wiki/Byte_Order_Mark
        using (System.IO.MemoryStream ManifestBytesMemoryStream = 
                            new System.IO.MemoryStream(ManifestBytes))
        using (System.IO.StreamReader ManifestBytesStreamReader = _
                new System.IO.StreamReader(ManifestBytesMemoryStream, true))
        {
            ManifestXmlString = ManifestBytesStreamReader.ReadToEnd().Trim();
        }

        //Create a xml document and load the xml string
        System.Xml.XmlDocument ManifestXmlDocument = new System.Xml.XmlDocument();
        
        //Load the xml string 
        ManifestXmlDocument.LoadXml(ManifestXmlString);

        //Return the loaded xml document
        applicationXmlManifest = ManifestXmlDocument;

        error = null;
        return true;
    }
    catch(System.Exception err)
    {
        //Something went wrong for some reason
        error = err;
        applicationXmlManifest = null;
        return false;
    }
}

VB.NET 代码

'Overloaded simplified static method returning
'only the XmlDocument or null on failure
Public Shared Function GetPEFileManifest(ByVal fileName As System.String) As System.Xml.XmlDocument
    Dim xmld As System.Xml.XmlDocument
    Dim err As System.Exception

    GetPEFileManifest(fileName, xmld, err)

    Return xmld
End Function

'Full static method returning error
'information on failure
Public Shared Function GetPEFileManifest(ByVal fileName As System.String, _
       ByRef applicationXmlManifest As System.Xml.XmlDocument, _
       ByRef [error] As System.Exception) As System.Boolean
    Try
        'First check valid input parameters
        If System.[String].IsNullOrEmpty(fileName) = True Then
            Throw New System.NullReferenceException("Parameter ""fileName"" cant be null or empty")
        End If

        'First check if file is valid
        If System.IO.File.Exists(fileName) = False Then
            Throw New System.IO.FileNotFoundException_
              ("Parameter ""fileName"" does not point to a existing file")
        End If

        'Load System.Deployment.dll
        Dim SystemDeploymentAssembly As System.Reflection.Assembly = _
          System.Reflection.Assembly.Load("System.Deployment, _
             Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")

        'Get SystemUtils class from assembly
        Dim SystemUtilsClass As System.Type = _
          SystemDeploymentAssembly.[GetType]_
             ("System.Deployment.Application.Win32InterOp.SystemUtils")

        'Create instance of SystemUtils class 
        Dim SystemUtils As System.Object = System.Activator.CreateInstance(SystemUtilsClass)

        'Invoke the internal method named "GetManifestFromPEResources" via reflection
        Dim ManifestBytes As System.Byte() = TryCast(SystemUtils.[GetType]().InvokeMember(_
          "GetManifestFromPEResources", System.Reflection.BindingFlags.InvokeMethod Or _
          System.Reflection.BindingFlags.[Public] Or System.Reflection.BindingFlags.[Static], _
          Nothing, SystemUtils, New System.Object() {fileName}), System.Byte())

        'The string holding the final xml text
        Dim ManifestXmlString As String = String.Empty

        'Read bytes with memory stream and stream reader to make sure
        'to get the right encoded data, 
        'because some of the resources do have a BOM (Byte Order Mark)
        'Read this for more information: http://en.wikipedia.org/wiki/Byte_Order_Mark
        Using ManifestBytesMemoryStream As New System.IO.MemoryStream(ManifestBytes)
            Using ManifestBytesStreamReader As _
                    New System.IO.StreamReader(ManifestBytesMemoryStream, True)
                ManifestXmlString = ManifestBytesStreamReader.ReadToEnd().Trim()
            End Using
        End Using

        'Create a xml document and load the xml string
        Dim ManifestXmlDocument As System.Xml.XmlDocument = New System.Xml.XmlDocument()

        'Load the xml string 
        ManifestXmlDocument.LoadXml(ManifestXmlString)

        'Return the loaded xml document
        applicationXmlManifest = ManifestXmlDocument

        [error] = Nothing
        Return True
    Catch err As System.Exception
        'Something went wrong for some reason
        [error] = err
        applicationXmlManifest = Nothing
        Return False
    End Try
End Function

代码详解 (C#)

首先,我们检查输入参数,以确保我们有一个有效的文件名可以访问。然后,我们通过使用

System.Reflection.Assembly.Load()  

方法加载包含我们想要的方法的程序集。该方法有几个重载,您可能想使用其中的另一个变体。请参阅 MSDN 库中的文档。在本例中,我们要加载的程序集是“System.Deployment”,我们使用程序集的完整名称来引用和加载它: 

"System.Deployment, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" 

该程序集可能还有其他版本/区域性,但我尚未对此进行研究。请记住,一旦程序集被加载到应用程序的 AppDomain 中,出于我不想在此讨论的几个原因,它就无法被卸载。这里有很多文章展示了如何将外部程序集加载到单独的 AppDomain 中,并在使用后将其卸载,以防您想卸载它。将其加载到执行的程序集后,我们使用反射来访问加载的程序集中的所需类和方法。

//Get SystemUtils class from assembly
System.Type SystemUtilsClass = SystemDeploymentAssembly.GetType
      ("System.Deployment.Application.Win32InterOp.SystemUtils");
 
//Create instance of SystemUtils class 
System.Object SystemUtils = System.Activator.CreateInstance(SystemUtilsClass);
 
//Invoke the internal method named "GetManifestFromPEResources" via reflection
System.Byte[] ManifestBytes = SystemUtils.GetType().InvokeMember(
    "GetManifestFromPEResources",
    System.Reflection.BindingFlags.InvokeMethod |
    System.Reflection.BindingFlags.Public |
    System.Reflection.BindingFlags.Static,
    null,
    SystemUtils, 
    new System.Object[] { fileName }) as System.Byte[];

首先,我们通过查询已加载的程序集来获取 SystemUtils 类类型,该程序集获取指定名称的 Type 对象,并进行区分大小写的搜索以供进一步使用。在此处使用程序集中类的 FQN(完全限定名)非常重要。成功获取 SystemUtils 类类型后,我们使用 Activator 类的 CreateInstance(...) 方法创建它的一个实例。最后,我们通过调用已实例化类的成员方法来调用隐藏的 GetManifestFromPEResources(...) 方法。为了成功调用,您必须传递方法的名称的确切大小写以及上面使用的正确绑定标志。有关更多信息,请参阅 MSDN 文档。获取包含 PE 清单的 byte[] 数组后,我们需要将其转换为 string。做法如下:

//The string holding the final xml text
string ManifestXmlString = string.Empty;

//Read bytes with memory stream and stream reader to make sure
//to get the right encoded data, because some of the resources do have a BOM (Byte Order Mark)
//Read this for more information: http://en.wikipedia.org/wiki/Byte_Order_Mark
using (System.IO.MemoryStream ManifestBytesMemoryStream = new System.IO.MemoryStream(ManifestBytes))
using (System.IO.StreamReader ManifestBytesStreamReader = 
        new System.IO.StreamReader(ManifestBytesMemoryStream, true))
{
    ManifestXmlString = ManifestBytesStreamReader.ReadToEnd().Trim();
} 

我们使用 StreamReader 类是因为它能够识别清单 byte[] 数组前几个字节中的 BOM(字节顺序标记),而我们不必再处理它。如果数组中有 BOM(资源中并非总有 BOM),StreamReader 会为我们处理;如果没有,它将简单地将字节读取到流中。在十六进制编辑器中查看 BOM,它看起来是这样的:

最后,我们从获取的 XML string 创建一个 XmlDocument 并将其返回给调用者。

                //Create a xml document and load the xml string
System.Xml.XmlDocument ManifestXmlDocument = new System.Xml.XmlDocument();

//Load the xml string 
ManifestXmlDocument.LoadXml(ManifestXmlString);

//Return the loaded xml document
applicationXmlManifest = ManifestXmlDocument; 

我决定将 XML 文本打包到 XmlDocument 类中的原因很简单。一旦清单映射到 XmlDocument,它就可以完全被 .NET Framework 的 XML/DOM 函数访问,并且也可以像您想要的那样进行操作(读取、写入、扩展、拆分等)来修改它。

摘要 

尽管所有这些都可以通过使用单个 Windows API 调用 P/Invoke 来完成,但我个人认为使用这个未文档化的函数更“干净”,因为它将所有调用封装在一个函数中,并且所有操作都从外部以纯托管代码完成,而且这个未文档化的类/方法从 .NET 2.0 到最新的 .NET 运行时库版本都可用。请始终牢记:未文档化的函数始终是品味问题,并且可能会在将来的版本/更新中更改或完全不可用,因此必须决定是否要使用此函数,或者完全使用 Windows API 函数重写它,以便拥有自己的代码库以供将来使用。

关注点

还有另一种方法:更新 PE 的清单资源,但我在这里不讨论它,也不是本文的宗旨。我只想指出,如果有人确实感兴趣,可以使用 Windows API 中的 Resource 函数按照以下顺序进行操作:

  1. BeginUpdateResource
  2. UpdateResource
  3. EndUpdateResource

历史      

  • 2013 年 1 月 19 日 - 文章初稿发布 
  • 2013 年 1 月 22 日 - 添加了 VB.NET 代码并更正了一些拼写错误

外部链接 

© . All rights reserved.