diff --git a/Directory.Build.props b/Directory.Build.props index 128e21272..f94ba5755 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,7 +7,7 @@ https://github.com/Washi1337/AsmResolver git 10 - 5.0.0 + 5.1.0 diff --git a/appveyor.yml b/appveyor.yml index 7a1ff9bec..30adc28f9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,7 +4,7 @@ - master image: Visual Studio 2022 - version: 5.0.0-master-build.{build} + version: 5.1.0-master-build.{build} configuration: Release skip_commits: @@ -33,7 +33,7 @@ - development image: Visual Studio 2022 - version: 5.0.0-dev-build.{build} + version: 5.1.0-dev-build.{build} configuration: Release skip_commits: diff --git a/docs/core/segments.rst b/docs/core/segments.rst new file mode 100644 index 000000000..b58e0c50e --- /dev/null +++ b/docs/core/segments.rst @@ -0,0 +1,250 @@ +.. _segments: + +Reading and Writing File Segments +================================= + +Segments are the basis of everything in AsmResolver. +They are the fundamental building blocks that together make up a binary file (such as a PE file). +Segments are organized as a tree, where the leaves are single contiguous chunk of memory, while the nodes are segments that comprise multiple smaller sub-segments. +The aim of segments is to abstract away the complicated mess that comes with calculating offsets, sizes and updating them accordingly, allowing programmers to easily read binary files, as well as construct new ones. + +Every class that directly translates to a concrete segment in a file on the disk implements the ``ISegment`` interface. +In the following, some of the basics of ``ISegment`` as well as common examples will be introduced. + + +Basic Data Segments +------------------- + +The simplest and arguably the most commonly used form of segment is the ``DataSegment`` class. +This is a class that wraps around a ``byte[]`` into an instance of ``ISegment``, allowing it to be used in any context where a segment are expected in AsmResolver. + +.. code-block:: csharp + + byte[] data = new byte[] { 1, 2, 3, 4 }; + var segment = new DataSegment(data); + + +While the name of the ``DataSegment`` class implies it is used for defining literal data (such as a constant for a variable), it can be used to define *any* type of contiguous memory. +This also includes a raw code stream of a function body and sometimes entire program sections. + + +Reading Segment Contents +------------------------ + +Some implementations of ``ISegment`` (such as ``DataSegment``) allow for reading binary data directly. +Segments that allow for this implement ``IReadableSegment``, which defines a function ``CreateReader`` that can be used to create an instance of ``BinaryStreamReader`` that starts at the beginning of the raw contents of the segment. +This reader can then be used to read the contents of the segment. + +.. code-block:: csharp + + byte[] data = new byte[] { 1, 2, 3, 4 }; + IReadableSegment segment = new DataSegment(data); + + var reader = segment.CreateReader(); + reader.ReadByte(); // returns 1 + reader.ReadByte(); // returns 2 + reader.ReadByte(); // returns 3 + reader.ReadByte(); // returns 4 + reader.ReadByte(); // throws EndOfStreamException. + + +Alternatively, a ``IReadableSegment`` can be turned into a ``byte[]`` quickly using the ``ToArray()`` method. + +.. code-block:: csharp + + byte[] data = new byte[] { 1, 2, 3, 4 }; + IReadableSegment segment = new DataSegment(data); + + byte[] allData = segment.ToArray(); // Returns { 1, 2, 3, 4 } + + +Composing new Segments +---------------------- + +Many segments comprise multiple smaller sub-segments. +For example, PE sections often do not contain just a single data structure, but are a collection of structures concatenated together. +To facilitate more complicated structures like these, the ``SegmentBuilder`` class can be used to combine ``ISegment`` instances into one effortlessly: + +.. code-block:: csharp + + var builder = new SegmentBuilder(); + + builder.Add(new DataSegment(...)); + builder.Add(new DataSegment(...)); + + +Many segments in an executable file format require segments to be aligned to a certain byte-boundary. +The ``SegmentBuilder::Add`` method allows for specifying this alignment, and automatically adjust the offsets and sizes accordingly: + +.. code-block:: csharp + + var builder = new SegmentBuilder(); + + // Add some segment with potentially a size that is not a multiple of 4 bytes. + builder.Add(new DataSegment(...)); + + // Ensure the next segment is aligned to a 4-byte boundary in the final file. + builder.Add(new DataSegment(...), alignment: 4); + + +Since ``SegmentBuilder`` implements ``ISegment`` itself, it can also be used within another ``SegmentBuilder``, allowing for recursive constructions like the following: + +.. code-block:: csharp + + var child = new SegmentBuilder(); + child.Add(new DataSegment(...)); + child.Add(new DataSegment(...)); + + var root = new SegmentBuilder(); + root.Add(new DataSegment(...)); + root.Add(child); // Nest segment builders into each other. + + +Resizing Segments at Runtime +---------------------------- + +Most segments in an executable file retain their size at runtime. +However, some segments (such as a ``.bss`` section in a PE file) may be resized upon mapping it into memory. +AsmResolver represents these segments using the ``VirtualSegment`` class: + +.. code-block:: csharp + + var physicalContents = new DataSegment(new byte[] {1, 2, 3, 4}); + section.Contents = new VirtualSegment(physicalContents, 0x1000); // Create a new segment with a virtual size of 0x1000 bytes. + + +Patching Segments +----------------- + +Some use-cases of AsmResolver require segments to be hot-patched with new data after serialization. +This is done via the ``PatchedSegment`` class. + +Any segment can be wrapped into a ``PatchedSegment`` via its constructor: + +.. code-block:: csharp + + using AsmResolver.Patching; + + ISegment segment = ... + var patchedSegment = new PatchedSegment(segment); + + +Alternatively, you can use (the preferred) fluent syntax: + +.. code-block:: csharp + + using AsmResolver.Patching; + + ISegment segment = ... + var patchedSegment = segment.AsPatchedSegment(); + + +Applying the patches can then be done by repeatedly calling one of the ``Patch`` method overloads. +Below is an example of patching a section within a PE file: + +.. code-block:: csharp + + var peFile = PEFile.FromFile("input.exe"); + var section = peFile.Sections.First(s => s.Name == ".text"); + + var someSymbol = peImage + .Imports.First(m => m.Name == "ucrtbased.dll") + .Symbols.First(s => s.Name == "puts"); + + section.Contents = section.Contents.AsPatchedSegment() // Create patched segment. + .Patch(offset: 0x10, data: new byte[] {1, 2, 3, 4}) // Apply literal bytes patch + .Patch(offset: 0x20, AddressFixupType.Absolute64BitAddress, someSymbol); // Apply address fixup patch. + + +The patching API can be extended by implementing the ``IPatch`` yourself. + + +Calculating Offsets and Sizes +----------------------------- + +Typically, the ``ISegment`` API aims to abstract away any raw offset, relative virtual address (RVA), and/or size of a data structure within a binary file. +However, in case the final offset and/or size of a segment still need to be determined and used (e.g., when implementing new segments), it is important to understand how this is done. + +Two properties are responsible for representing the offsets: + +- ``Offset``: The starting file or memory address of the segment. +- ``Rva``: The virtual address of the segment, relative to the executable's image base at runtime. + + +Typically, these properties are read-only and managed by AsmResolver itself. +However, to update the offsets and RVAs of a segment, you can call the ``UpdateOffsets`` method. +This method traverses the entire segment recursively, and updates the offsets accordingly. + +.. code-block:: csharp + + ISegment segment = ... + + // Relocate a segment to an offsets-rva pair: + segment.UpdateOffsets(new RelocationParameters(offset: 0x200, rva: 0x2000); + + Console.WriteLine("Offset: 0x{0:X8}", segment.Offset); // Prints 0x200 + Console.WriteLine("Rva: 0x{0:X8}", segment.Rva); // Prints 0x2000 + +.. warning:: + + Try to call ``UpdateOffsets()`` as sparsely as possible. + The method does a full pass on the entire segment, and updates all offsets of all sub-segments as well. + It can thus be very inefficient to call them repeatedly. + + +The size (in bytes) of a segment can be calculated using either the ``GetPhysicalSize()`` or ``GetVirtualSize()``. +Typically, these two measurements are going to be equal, but for some segments (such as a ``VirtualSegment``) this may differ: + +.. code-block:: csharp + + ISegment segment = ... + + // Measure the size of the segment: + uint physicalSize = segment.GetPhysicalSize(); + uint virtualSize = segment.GetVirtualSize(); + + Console.WriteLine("Physical (File) Size: 0x{0:X8}", physicalSize); + Console.WriteLine("Virtual (Runtime) Size: 0x{0:X8}", virtualSize); + + +.. warning:: + + Only call ``GetPhysicalSize()`` and ``GetVirtualSize()`` whenever you know the offsets of the segment are up to date. + Due to padding requirements, many segments will have a slightly different size depending on the final file offset they are placed at. + + +.. warning:: + + Try to call ``GetPhysicalSize()`` and ``GetVirtualSize()`` as sparsely as possible. + These methods do a full pass on the entire segment, and measure the total amount of bytes required to represent it. + It can thus be very inefficient to call them repeatedly. + + +Serializing Segments +-------------------- + +Segments are serialized using the ``ISegment::Write`` method. + +.. code-block:: csharp + + ISegment segment = ... + + using var stream = new MemoryStream(); + segment.Write(new BinaryStreamWriter(stream)); + + byte[] serializedData = stream.ToArray(); + + +Alternatively, you can quickly serialize a segment to a ``byte[]`` using the ``WriteIntoArray()`` extension method: + +.. code-block:: csharp + + ISegment segment = ... + + byte[] serializedData = stream.WriteIntoArray(); + + +.. warning:: + + Only call ``Write`` whenever you know the offsets of the segment are up to date. + Many segments will contain offsets to other segments in the file, which may not be accurate until all offsets are calculated. diff --git a/docs/dotnet/advanced-module-reading.rst b/docs/dotnet/advanced-module-reading.rst index 790793dac..fa0907281 100644 --- a/docs/dotnet/advanced-module-reading.rst +++ b/docs/dotnet/advanced-module-reading.rst @@ -3,7 +3,7 @@ Advanced Module Reading ======================= -Advanced users might have the need to configure AsmResolver's module reader. For example, instead of letting the module reader throw exceptions upon reading invalid data, errors should be ignored and recovered from. Other uses might include changing the way the underlying PE or method bodies are read. These kinds of settings can be configured using the ``ModuleReaderParameters`` class. +Advanced users might need to configure AsmResolver's module reader. For example, instead of letting the module reader throw exceptions upon reading invalid data, errors should be ignored and recovered from. Other uses might include changing the way the underlying PE or method bodies are read. These kinds of settings can be configured using the ``ModuleReaderParameters`` class. .. code-block:: csharp @@ -131,7 +131,7 @@ To let the reader use this implementation of the ``IMethodBodyReader``, set the Custom Field RVA reading ------------------------ -By default, the field RVA data storing the initial binary value of a field is interpreted as raw byte blobs, and are turned into instances of the ``DataSegment`` class. To adjust this behaviour, it is possible provide a custom implementation of the ``IFieldRvaDataReader`` interface. +By default, the field RVA data storing the initial binary value of a field is interpreted as raw byte blobs, and are turned into instances of the ``DataSegment`` class. To adjust this behaviour, it is possible to provide a custom implementation of the ``IFieldRvaDataReader`` interface. .. code-block:: csharp diff --git a/docs/dotnet/advanced-pe-image-building.rst b/docs/dotnet/advanced-pe-image-building.rst index 8acc2176f..db94d483c 100644 --- a/docs/dotnet/advanced-pe-image-building.rst +++ b/docs/dotnet/advanced-pe-image-building.rst @@ -12,7 +12,7 @@ The easiest way to write a .NET module to the disk is by using the ``Write`` met This method is essentially a shortcut for invoking the ``ManagedPEImageBuilder`` and ``ManagedPEFileBuilder`` classes, and will completely reconstruct the PE image, serialize it into a PE file and write the PE file to the disk. -While this is easy, and would probably work for most .NET module processing, it does not provide much flexibility. To get more control over the construction of the new PE image, it is therefore not recommended to use a different overload of the ``Write`` method, were we pass on a custom ``IPEFileBuilder``, or a configured ``ManagedPEImageBuilder``: +While this is easy, and would probably work for most .NET module processing, it does not provide much flexibility. To get more control over the construction of the new PE image, it is therefore not recommended to use a different overload of the ``Write`` method, where we pass on a custom ``IPEFileBuilder``, or a configured ``ManagedPEImageBuilder``: .. code-block:: csharp @@ -22,7 +22,7 @@ While this is easy, and would probably work for most .NET module processing, it module.Write(@"C:\Path\To\Output\Binary.exe", imageBuilder); -Alternatively, it is possible to call the ``CreateImage`` method directly. This allows for inspecting all build artifacts, as well as post processing of the constructed PE image before it is written to the disk. +Alternatively, it is possible to call the ``CreateImage`` method directly. This allows for inspecting all build artifacts, as well as post-processing of the constructed PE image before it is written to the disk. .. code-block:: csharp @@ -72,10 +72,34 @@ Some .NET modules are carefully crafted and rely on the raw structure of all met - RIDs of rows within a metadata table. - Indices of blobs within the ``#Blob``, ``#Strings``, ``#US`` or ``#GUID`` heaps. - -The default PE image builder for .NET modules (``ManagedPEImageBuilder``) defines a property called ``DotNetDirectoryFactory``, which contains the object responsible for constructing the .NET data directory, can be configured to preserve as much of this structure as possible. With the help of the ``MetadataBuilderFlags`` enum, it is possible to indicate which structures of the metadata directory need to preserved. - -Below an example on how to configure the image builder to preserve blob data and all metadata tokens to type references: +- Unknown or unconventional metadata streams and their order. + +The default PE image builder for .NET modules (``ManagedPEImageBuilder``) defines a property called ``DotNetDirectoryFactory``, which contains the object responsible for constructing the .NET data directory, can be configured to preserve as much of this structure as possible. With the help of the ``MetadataBuilderFlags`` enum, it is possible to indicate which structures of the metadata directory need to preserved. The following table provides an overview of all preservation metadata builder flags that can be used and combined: + ++----------------------------------------+-------------------------------------------------------------------+ +| flag | Description | ++========================================+===================================================================+ +| ``PreserveXXXIndices`` | Preserves all row indices of the original ``XXX`` metadata table. | ++----------------------------------------+-------------------------------------------------------------------+ +| ``PreserveTableIndices`` | Preserves all row indices from all original metadata tables. | ++----------------------------------------+-------------------------------------------------------------------+ +| ``PreserveBlobIndices`` | Preserves all blob indices in the ``#Blob`` stream. | ++----------------------------------------+-------------------------------------------------------------------+ +| ``PreserveGuidIndices`` | Preserves all GUID indices in the ``#GUID`` stream. | ++----------------------------------------+-------------------------------------------------------------------+ +| ``PreserveStringIndices`` | Preserves all string indices in the ``#Strings`` stream. | ++----------------------------------------+-------------------------------------------------------------------+ +| ``PreserveUserStringIndices`` | Preserves all user-string indices in the ``#US`` stream. | ++----------------------------------------+-------------------------------------------------------------------+ +| ``PreserveUnknownStreams`` | Preserves any of the unknown / unconventional metadata streams. | ++----------------------------------------+-------------------------------------------------------------------+ +| ``PreserveStreamOrder`` | Preserves the original order of all metadata streams. | ++----------------------------------------+-------------------------------------------------------------------+ +| ``PreserveAll`` | Preserves as much of the original metadata as possible. | ++----------------------------------------+-------------------------------------------------------------------+ + + +Below is an example on how to configure the image builder to preserve blob data and all metadata tokens to type references: .. code-block:: csharp @@ -84,7 +108,6 @@ Below an example on how to configure the image builder to preserve blob data and | MetadataBuilderFlags.PreserveTypeReferenceIndices; imageBuilder.DotNetDirectoryFactory = factory; -If everything is supposed to be preserved as much as possible, then instead of specifying all flags defined in the ``MetadataBuilderFlags`` enum, we can also use ``MetadataBuilderFlags.PreserveAll`` as a shortcut. .. warning:: diff --git a/docs/dotnet/cloning.rst b/docs/dotnet/cloning.rst index 871f6b499..6797896ef 100644 --- a/docs/dotnet/cloning.rst +++ b/docs/dotnet/cloning.rst @@ -1,7 +1,7 @@ Member Cloning ============== -When processing a .NET module, it often involves injecting additional code. Even though all models representing .NET metadata and CIL code are mutable, it might be very time consuming and error-prone to manually import and inject metadata members and/or CIL code into the target module. +Processing a .NET module often involves injecting additional code. Even though all models representing .NET metadata and CIL code are mutable, it might be very time-consuming and error-prone to manually import and inject metadata members and/or CIL code into the target module. To help developers in injecting existing code into a module, ``AsmResolver.DotNet`` comes with a feature that involves cloning metadata members from one module and copying it over to another. All relevant classes are in the ``AsmResolver.DotNet.Cloning`` namespace: @@ -116,7 +116,7 @@ When all members are included, it is possible to call ``MemberCloner.Clone`` to var result = cloner.Clone(); -The ``MemberCloner`` will automatically resolve any cross references between types, fields and methods that are included in the cloning process. +The ``MemberCloner`` will automatically resolve any cross-references between types, fields and methods that are included in the cloning process. For instance, going with the example in the previous section, if both the ``Rectangle`` as well as the ``Vector2`` classes are included, any reference in ``Rectangle`` to ``Vector2`` will be replaced with a reference to the cloned ``Vector2``. If not all members are included, the ``MemberCloner`` will assume that these are references to external libraries, and will use the ``ReferenceImporter`` to construct references to these members instead. @@ -173,12 +173,12 @@ All references to methods defined in the ``System.Runtime.CompilerServices`` nam See :ref:`dotnet-importer-common-caveats` for more information on reference importing and its caveats. -Post processing of cloned members +Post-processing of cloned members --------------------------------- In some cases, cloned members may need to be post-processed before they are injected into the target module. The ``MemberCloner`` class can be initialized with an instance of a ``IMemberClonerListener``, that gets notified by the cloner object every time a definition was cloned. -Below an example that appends the string ``_Cloned`` to the name for every cloned type. +Below is an example that appends the string ``_Cloned`` to the name for every cloned type. .. code-block:: csharp @@ -251,7 +251,7 @@ It is important to note that the ``MemberCloner`` class itself does not inject a destinationModule.TopLevelTypes.Add(clonedType); -However, since injecting the cloned top level types is a very common use-case for the cloner, AsmResolver defines the ``InjectTypeClonerListener`` class that implements a cloner listener that injects all top level types automatically into the destination module. In such a case, the code can be reduced to the following: +However, since injecting the cloned top level types is a very common use-case for the cloner, AsmResolver defines the ``InjectTypeClonerListener`` class that implements a cloner listener that injects all top-level types automatically into the destination module. In such a case, the code can be reduced to the following: .. code-block:: csharp diff --git a/docs/dotnet/dynamic-methods.rst b/docs/dotnet/dynamic-methods.rst index 9b43317e7..645be53a0 100644 --- a/docs/dotnet/dynamic-methods.rst +++ b/docs/dotnet/dynamic-methods.rst @@ -3,7 +3,7 @@ Dynamic Methods Dynamic methods are methods that are constructed and assembled at run time. They allow for dynamically generating managed code, without having to go through the process of compiling or generating new assemblies. This is used a lot in obfuscators that implement for example reference proxies or virtual machines. -AsmResolver has support for reading dynamic methods, and transforming them into ``MethodDefinition`` objects that can be processed further. All relevant classes are present in the following namespace: +AsmResolver has support for reading dynamic methods and transforming them into ``MethodDefinition`` objects that can be processed further. All relevant classes are present in the following namespace: .. code-block:: csharp diff --git a/docs/dotnet/importing.rst b/docs/dotnet/importing.rst index 7b1bb8dd2..abf3f420c 100644 --- a/docs/dotnet/importing.rst +++ b/docs/dotnet/importing.rst @@ -3,7 +3,7 @@ Reference Importing =================== -.NET modules use entries in the TypeRef or MemberRef tables to reference types or members from external assemblies. Importing references into the current module therefore form a key role when creating new- or modifying existing .NET modules. When a member is not imported into the current module, a ``MemberNotImportedException`` will be thrown when you are trying to create a PE image or write the module to the disk. +.NET modules use entries in the TypeRef or MemberRef tables to reference types or members from external assemblies. Importing references into the current module, therefore, form a key role when creating new- or modifying existing .NET modules. When a member is not imported into the current module, a ``MemberNotImportedException`` will be thrown when you are trying to create a PE image or write the module to the disk. AsmResolver provides the ``ReferenceImporter`` class that does most of the heavy lifting. Obtaining an instance of ``ReferenceImporter`` can be done in two ways. @@ -22,7 +22,7 @@ Or obtain the default instance that comes with every ``ModuleDefinition`` object var importer = module.DefaultImporter; -The example snippets that will follow in this articule assume that there is such a ``ReferenceImporter`` object instantiated using either of these two methods, and is stored in an ``importer`` variable. +The example snippets that will follow in this article assume that there is such a ``ReferenceImporter`` object instantiated using either of these two methods, and is stored in an ``importer`` variable. Importing existing members @@ -125,7 +125,7 @@ Instantiations of generic methods are also supported. Creating new references ----------------------- -Member references can also be created and imported without having direct access to its member definition or ``System.Reflection`` instance. It is possible to create new instances of ``TypeReference`` and ``MemberReference`` using the constructors, but the preferred way is to use the factory methods that allow for a more fluent syntax. Below an example on how to create a fully imported reference to ``void System.Console.WriteLine(string)``: +Member references can also be created and imported without having direct access to its member definition or ``System.Reflection`` instance. It is possible to create new instances of ``TypeReference`` and ``MemberReference`` using the constructors, but the preferred way is to use the factory methods that allow for a more fluent syntax. Below is an example of how to create a fully imported reference to ``void System.Console.WriteLine(string)``: .. code-block:: csharp diff --git a/docs/dotnet/index.rst b/docs/dotnet/index.rst index 25405425a..13d22fbde 100644 --- a/docs/dotnet/index.rst +++ b/docs/dotnet/index.rst @@ -1,7 +1,7 @@ Overview ======== -The .NET image layer is the third layer of abstraction of the portable executable (PE) file format. It provides a high-level abstraction of the .NET metadata stored in a PE image, that is similar to APIs like ``System.Reflection``. Its root objects are ``AssemblyDefinition`` and ``ModuleDefinition``, and from there it is possible to disect the .NET assembly in a hierarchical manner. +The .NET image layer is the third layer of abstraction of the portable executable (PE) file format. It provides a high-level abstraction of the .NET metadata stored in a PE image, that is similar to APIs like ``System.Reflection``. Its root objects are ``AssemblyDefinition`` and ``ModuleDefinition``, and from there it is possible to dissect the .NET assembly hierarchically. In short, this means the following: @@ -11,4 +11,4 @@ In short, this means the following: * Methods include method bodies, * ... and so on. -The third layer of abstraction is the highest level of abstraction for a .NET assembly that AsmResolver provides. All objects exposed by this layer are completely mutable, and can be serialized back to a ``IPEImage`` from the second layer, to a ``PEFile`` from the first layer, or to the disk. +The third layer of abstraction is the highest level of abstraction for a .NET assembly that AsmResolver provides. All objects exposed by this layer are completely mutable and can be serialized back to a ``IPEImage`` from the second layer, to a ``PEFile`` from the first layer, or to the disk. diff --git a/docs/dotnet/managed-method-bodies.rst b/docs/dotnet/managed-method-bodies.rst index 3d438d3de..c3d7ce70d 100644 --- a/docs/dotnet/managed-method-bodies.rst +++ b/docs/dotnet/managed-method-bodies.rst @@ -196,6 +196,7 @@ Below an example on how to use the ``ReferenceImporter`` to emit a call to ``Con var importer = new ReferenceImporter(targetModule); var writeLine = importer.ImportMethod(typeof(Console).GetMethod("WriteLine", new[] { typeof(string) } ); + body.Instructions.Add(new CilInstruction(CilOpCodes.Ldstr, "Hello, world!")); body.Instructions.Add(new CilInstruction(CilOpCodes.Call, writeLine)); diff --git a/docs/dotnet/member-tree.rst b/docs/dotnet/member-tree.rst index 633d16808..ad6d4f6a8 100644 --- a/docs/dotnet/member-tree.rst +++ b/docs/dotnet/member-tree.rst @@ -22,7 +22,7 @@ Obtaining types in a module Types are represented by the ``TypeDefinition`` class. To get the types defined in a module, use the ``ModuleDefinition.TopLevelTypes`` property. A top level types is any non-nested type. Nested types are exposed through the ``TypeDefinition.NestedTypes``. Alternatively, to get all types, including nested types, it is possible to call the ``ModuleDefinition.GetAllTypes`` method instead. -Below, an example program that iterates through all types recursively and prints them: +Below is an example program that iterates through all types recursively and prints them: .. code-block:: csharp diff --git a/docs/dotnet/token-allocation.rst b/docs/dotnet/token-allocation.rst index 6dc6bc9e9..602c38af9 100644 --- a/docs/dotnet/token-allocation.rst +++ b/docs/dotnet/token-allocation.rst @@ -6,7 +6,7 @@ A lot of models in a .NET module are assigned a unique metadata token. This toke Custom Token Allocation ----------------------- -Some usecase of AsmResolver will depend on the knowledge of tokens of newly created members prior to serializing the module. Therefore, AsmResolver provides the ``TokenAllocator`` class, which allows for assigning new tokens to members pre-emptively. If a module is then written to the disk with the ``MetadataFlags.PreserveTableIndices`` flags set (see Advanced PE Image Building for more information on how that is done), this token will be preserved in the final image. +Some use-cases of AsmResolver will depend on the knowledge of tokens of newly created members before serializing the module. Therefore, AsmResolver provides the ``TokenAllocator`` class, which allows for assigning new tokens to members preemptively. If a module is then written to the disk with the ``MetadataFlags.PreserveTableIndices`` flags set (see Advanced PE Image Building for more information on how that is done), this token will be preserved in the final image. The token allocator for a particular module can be accessed through the ``ModuleDefinition.TokenAllocator`` property: diff --git a/docs/dotnet/type-signatures.rst b/docs/dotnet/type-signatures.rst index df16d8e31..34d5fcff1 100644 --- a/docs/dotnet/type-signatures.rst +++ b/docs/dotnet/type-signatures.rst @@ -27,6 +27,8 @@ Basic leaf type signatures: +----------------------------------+----------------------------------------------------------------------+ | ``FunctionPointerTypeSignature`` | ``method void *(int32, int64)`` | +----------------------------------+----------------------------------------------------------------------+ +| ``GenericParameterSignature`` | ``!0``, ``!!0`` | ++----------------------------------+----------------------------------------------------------------------+ | ``SentinelTypeSignature`` | (Used as a delimeter for vararg method signatures) | +----------------------------------+----------------------------------------------------------------------+ @@ -43,8 +45,6 @@ Decorator type signatures: +----------------------------------+----------------------------------------------------------------------+ | ``PointerTypeSignature`` | ``System.Int32*`` | +----------------------------------+----------------------------------------------------------------------+ -| ``GenericParameterSignature`` | ``!0``, ``!!0`` | -+----------------------------------+----------------------------------------------------------------------+ | ``CustomModifierTypeSignature`` | ``System.Int32 modreq (System.Runtime.CompilerServices.IsVolatile)`` | +----------------------------------+----------------------------------------------------------------------+ | ``BoxedTypeSignature`` | (Boxes a value type signature) | @@ -77,6 +77,7 @@ Corlib type signatures can also be looked up by their element type, by their ful If an invalid element type, name or type descriptor is passed on, these methods return ``null``. + TypeDefOrRefSignature --------------------- @@ -88,6 +89,13 @@ The ``TypeDefOrRefSignature`` class is used to reference types in either the ``T TypeSignature streamTypeSig = new TypeDefOrRefSignature(streamTypeRef); +Alternatively, ``CreateTypeReference`` can be used on any ``IResolutionScope``: + +.. code-block:: csharp + + var streamTypeSig = corlibScope.CreateTypeReference("System.IO", "Stream"); + + .. warning:: While it is technically possible to reference a basic type such as ``System.Int32`` as a ``TypeDefOrRefSignature``, it renders the .NET module invalid by most implementations of the CLR. Always use the ``CorLibTypeSignature`` to reference basic types within your blob signatures. @@ -109,6 +117,17 @@ The ``GenericInstanceTypeSignature`` class is used to instantiate generic types // listOfString now contains a reference to List. +Alternatively, a generic instance can also be generated via the ``MakeGenericType`` fluent syntax method: + +.. code-block:: csharp + + var listOfString = corlibScope + .CreateTypeReference("System.Collections.Generic", "List`1") + .MakeGenericInstanceType(module.CorLibTypeFactory.String); + + // listOfString now contains a reference to List. + + FunctionPointerTypeSignature ---------------------------- @@ -127,6 +146,21 @@ Function pointer signatures are strongly-typed pointer types used to describe ad // type now contains a reference to `method void *(int32, int32)`. +Alternatively, a function pointer signature can also be generated via the ``MakeFunctionPointerType`` fluent syntax method: + +.. code-block:: csharp + + var factory = module.CorLibTypeFactory; + var type = MethodSignature.CreateStatic( + factory.Void, + factory.Int32, + factory.Int32) + .MakeFunctionPointerType(); + + // type now contains a reference to `method void *(int32, int32)`. + + + Shortcuts --------- @@ -140,6 +174,7 @@ To quickly transform any ``ITypeDescriptor`` into a ``TypeSignature``, it is pos Likewise, a ``TypeSignature`` can also be converted back to a ``ITypeDefOrRef``, which can be referenced using a metadata token, using the ``TypeSignature.ToTypeDefOrRef()`` method. + Decorating type signatures -------------------------- diff --git a/docs/index.rst b/docs/index.rst index da9a805c7..74e1d1ad9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -15,6 +15,14 @@ Table of Contents: faq +.. toctree:: + :maxdepth: 1 + :caption: Core API + :name: sec-core + + core/segments + + .. toctree:: :maxdepth: 1 :caption: PE Files @@ -47,7 +55,7 @@ Table of Contents: .. toctree:: :maxdepth: 1 :caption: .NET assemblies and modules - :name: sec-peimage + :name: sec-dotnet dotnet/index dotnet/basics @@ -55,7 +63,6 @@ Table of Contents: dotnet/member-tree dotnet/type-signatures dotnet/importing - dotnet/methods dotnet/managed-method-bodies dotnet/unmanaged-method-bodies dotnet/dynamic-methods diff --git a/docs/pefile/basics.rst b/docs/pefile/basics.rst index 2514cf826..d893d7a0f 100644 --- a/docs/pefile/basics.rst +++ b/docs/pefile/basics.rst @@ -28,7 +28,7 @@ Opening a file can be done through one of the `FromXXX` methods: var peFile = PEFile.FromReader(reader); -By default, AsmResolver assumes the PE file is in its unmapped form. This is usually the case when files are read directly from the file system. For memory mapped PE files, use the overload of the ``FromReader`` method, which allows for specifying the memory layout of the input. +By default, AsmResolver assumes the PE file is in its unmapped form. This is usually the case when files are read directly from the file system. For memory-mapped PE files, use the overload of the ``FromReader`` method, which allows for specifying the memory layout of the input. .. code-block:: csharp @@ -36,7 +36,7 @@ By default, AsmResolver assumes the PE file is in its unmapped form. This is usu var peFile = PEFile.FromReader(reader, PEMappingMode.Mapped); -If you want to read large files (+100MB), consider using memory mapped I/O instead: +If you want to read large files (+100MB), consider using memory-mapped I/O instead: .. code-block:: csharp diff --git a/docs/pefile/sections.rst b/docs/pefile/sections.rst index 73cf12419..605425cb6 100644 --- a/docs/pefile/sections.rst +++ b/docs/pefile/sections.rst @@ -23,7 +23,7 @@ This can be used to read the data that is present in the section. If you want to .. code-block:: csharp byte[] data = section.ToArray(); - + The ``Sections`` property is mutable, which means you can add new sections and remove others from the PE. @@ -42,8 +42,8 @@ Some sections (such as `.data` or `.bss`) contain uninitialized data, and might var section = new PESection(".asmres", SectionFlags.MemoryRead | SectionFlags.ContentUninitializedData); var physicalContents = new DataSegment(new byte[] {1, 2, 3, 4}); section.Contents = new VirtualSegment(physicalContents, 0x1000); // Create a new segment with a virtual size of 0x1000 bytes. - + peFile.Sections.Add(section); -For more advanced section building, see :ref:`pe-building-sections`. \ No newline at end of file +For more advanced section building, see :ref:`pe-building-sections` and :ref:`segments`. diff --git a/docs/peimage/debug.rst b/docs/peimage/debug.rst index e3cce0738..30f092147 100644 --- a/docs/peimage/debug.rst +++ b/docs/peimage/debug.rst @@ -13,7 +13,7 @@ The relevant classes for this article are stored in the following namespace: The Debug Data Entries ---------------------- -The ``IPEImage`` exposes all debug information through the ``DebugData`` property. This is a list of ``DebugDataEntry``, providing access to the type of the debug data, as well as the version and raw contents of the data that is stored. +The ``IPEImage`` exposes all debug information through the ``DebugData`` property. This is a list of ``DebugDataEntry``, providing access to the type of debug data, as well as the version and raw contents of the data that is stored. .. code-block:: csharp @@ -24,15 +24,15 @@ The ``IPEImage`` exposes all debug information through the ``DebugData`` propert Console.WriteLine("Data start: {0:X8}", entry.Contents.Rva); } -Depending on the type of the debug data entry, the ``Contents`` property will be modelled using different implementations of ``IDebugDataSegment``. +Depending on the type of the debug data entry, the ``Contents`` property will be modeled using different implementations of ``IDebugDataSegment``. .. note:: - If a PE contains debug data using an unsuported or unrecognized format, then the contents will be modelled with a ``CustomDebugDataSegment`` instance instead, which exposes the raw contents as an ``ISegment``. + If a PE contains debug data using an unsupported or unrecognized format, then the contents will be modeled with a ``CustomDebugDataSegment`` instance instead, which exposes the raw contents as an ``ISegment``. .. note:: - Currently, AsmResolver only has rich-support for ``CodeView`` debug data. + Currently, AsmResolver only has rich support for ``CodeView`` debug data. CodeView Data diff --git a/docs/peimage/dotnet.rst b/docs/peimage/dotnet.rst index a4f41da3b..26cf9626b 100644 --- a/docs/peimage/dotnet.rst +++ b/docs/peimage/dotnet.rst @@ -68,7 +68,7 @@ AsmResolver supports parsing streams using the names in the table below. Any str | ``#US`` | ``UserStringsStream`` | +---------------------------+------------------------+ -Some streams support reading the raw contents using a ``BinaryStreamReader``. Effectively, every stream that was read from the disk is readable in this way. Below an example of a program that dumps for each readable stream the contents to a file on the disk: +Some streams support reading the raw contents using a ``BinaryStreamReader``. Effectively, every stream that was read from the disk is readable in this way. Below is an example of a program that dumps for each readable stream the contents to a file on the disk: .. code-block:: csharp @@ -101,7 +101,7 @@ The ``Streams`` property is mutable. You can add new streams, or remove existing Blob, Strings, US and GUID streams ---------------------------------- -The blob, strings, user-strings and GUID streams are all very similar in the sense that they all provide a storage for data referenced by the tables stream. Each of these streams have a very similar API in AsmResolver. +The blob, strings, user-strings and GUID streams are all very similar in the sense that they all provide a storage for data referenced by the tables stream. Each of these streams has a very similar API in AsmResolver. +------------------------+----------------------+ | Class | Method | @@ -346,7 +346,7 @@ Creating new segment references not present in the current PE image yet can be d TypeReference Hash (TRH) ------------------------ -Similar to the :ref:`pe-import-hash`, the TypeReference Hash (TRH) can be used to help identifying malware family written in a .NET language. However, unlike the Import Hash, the TRH is based on the names of all imported type references instead of the symbols specified in the imports directory of the PE. This is a more accurate representation for .NET images, as virtually every .NET image only uses one native symbol (either ``mscoree.dll!_CorExeMain`` or ``mscoree.dll!_CorDllMain``). +Similar to the :ref:`pe-import-hash`, the TypeReference Hash (TRH) can be used to help identify malware families written in a .NET language. However, unlike the Import Hash, the TRH is based on the names of all imported type references instead of the symbols specified in the imports directory of the PE. This is a more accurate representation for .NET images, as virtually every .NET image only uses one native symbol (either ``mscoree.dll!_CorExeMain`` or ``mscoree.dll!_CorDllMain``). AsmResolver includes a built-in implementation for this that is based on `the reference implementation provided by GData `_. The hash can be obtained using the ``GetTypeReferenceHash`` extension method on ``IPEImage`` or on ``IMetadata``: diff --git a/docs/peimage/pe-building.rst b/docs/peimage/pe-building.rst index 13cc88d61..51a21c479 100644 --- a/docs/peimage/pe-building.rst +++ b/docs/peimage/pe-building.rst @@ -3,11 +3,16 @@ PE File Building ================ -An ``IPEImage`` is a higher level abstraction of an actual PE file, and hides many of the actual implementation details such as raw section layout and contents of data directories. While this makes reading and interpretation of a PE very easy, it makes writing a more involved process. +An ``IPEImage`` is a higher-level abstraction of an actual PE file, and hides many of the actual implementation details such as raw section layout and contents of data directories. +While this makes reading and interpreting a PE very easy, it makes writing a more involved process. -Unfortunately, the PE file format is not well-structured. Compilers and linkers have a lot of freedom when it comes to the actual design and layout of the final PE file. For example, one compiler might place the Import Address Table (IAT) in a separate section (such as the MSVC), while others will put it next to the code in the text section (such as the C# compiler). This degree of freedom makes it virtually impossible to make a completely generic PE file builder, and as such AsmResolver does not come with one out of the box. +Unfortunately, the PE file format is not well-structured. +Compilers and linkers have a lot of freedom when it comes to the actual design and layout of the final PE file. +For example, one compiler might place the Import Address Table (IAT) in a separate section (such as the MSVC), while others will put it next to the code in the text section (such as the C# compiler). +This degree of freedom makes it virtually impossible to make a completely generic PE file builder, and as such AsmResolver does not come with one out of the box. -It is still possible to build PE files from instances of ``IPEImage``, it might just take more manual labor than you might expect at first. This article will discuss certain strategies that can be adopted when constructing new PE files from PE images. +It is still possible to build PE files from instances of ``IPEImage``, it might just take more manual labor than you might expect at first. +This article will discuss certain strategies that can be adopted when constructing new PE files from PE images. .. _pe-building-sections: @@ -15,27 +20,33 @@ It is still possible to build PE files from instances of ``IPEImage``, it might Building Sections ----------------- -As discussed in :ref:`pe-file-sections`, the ``PESection`` class represents a single section in the PE file. This class exposes a property called ``Contents`` which is of type ``ISegment``. A lot of models in AsmResolver implement this interface, and as such, these models can directly be used as the contents of a new section. +As discussed in :ref:`pe-file-sections`, the ``PESection`` class represents a single section in the PE file. +This class exposes a property called ``Contents`` which is of type ``ISegment``. +A lot of models in AsmResolver implement this interface, and as such, these models can directly be used as the contents of a new section. -.. code-block:: csharp +.. code-block:: csharp var peFile = new PEFile(); var text = new PESection(".text", SectionFlags.ContentCode | SectionFlags.MemoryRead | SectionFlags.MemoryExecute); - text.Content = new CodeSegment(new byte[] { ... } ); + text.Content = new DataSegment(new byte[] { ... } ); peFile.Sections.Add(text); -In a lot of cases, however, sections do not consist of a single data structure, but often comprise many segments concatenated together, potentially with some padding in between. Assigning a single instance of ``ISegment`` might therefore not be as convenient. To facilitate more complicated structures, the ``SegmentBuilder`` class can help a lot with building sections like these. This is a class that combines a collection of ``ISegment`` instances into one, allowing you to concatenate segments that belong together easily. Below an example that builds up a ``.text`` section that consists of a .NET data directory as well as some native code. In the example, the native code is aligned to a 4-byte boundary: +In a lot of cases, however, sections do not consist of a single data structure, but often comprise many segments concatenated together, potentially with some padding in between. +To compose new segments from a collection of smaller sub-segments structures, the ``SegmentBuilder`` class can be used. +This is a class that combines a collection of ``ISegment`` instances into one, allowing you to concatenate segments that belong together easily. +Below an example that builds up a ``.text`` section that consists of a .NET data directory as well as some native code. +In the example, the native code is aligned to a 4-byte boundary: -.. code-block:: csharp +.. code-block:: csharp var peFile = new PEFile(); var textBuilder = new SegmentBuilder(); textBuilder.Add(new DotNetDirectory { ... }); - textBuilder.Add(new CodeSegment(new byte[] { ... } ), alignment: 4); + textBuilder.Add(new DataSegment(new byte[] { ... } ), alignment: 4); var text = new PESection(".text", SectionFlags.ContentCode | SectionFlags.MemoryRead | SectionFlags.MemoryExecute); @@ -44,12 +55,38 @@ In a lot of cases, however, sections do not consist of a single data structure, peFile.Sections.Add(text); -Using complex PE models +PE files can also be patched using the ``PatchedSegment`` API. +Below is an example of patching some data in the ``.text`` section of a PE File with some literal data ``{1, 2, 3, 4}``, as well as writing an address to a symbol in the imports table: + +.. code-block:: csharp + + var peFile = PEFile.FromFile("input.exe"); + var section = peFile.Sections.First(s => s.Name == ".text"); + + var someSymbol = peImage + .Imports.First(m => m.Name == "ucrtbased.dll") + .Symbols.First(s => s.Name == "puts"); + + section.Contents = section.Contents.AsPatchedSegment() // Create patched segment. + .Patch(offset: 0x10, data: new byte[] {1, 2, 3, 4}) // Apply literal bytes patch + .Patch(offset: 0x20, AddressFixupType.Absolute64BitAddress, someSymbol); // Apply address fixup patch. + + +More detailed information on how to use segments can be found in :ref:`segments`. + + +Using complex PE models ----------------------- -The PE file format defines many complex data structures. While some these models are represented in AsmResolver using classes that derive from ``ISegment``, a lot of the classes that represent these data structures do not implement this interface, and as such cannot be used as a value for the ``Contents`` property of a ``PESection`` directly. This is due to the fact that most of these models are not required to be one single entity or chunk of continuous memory within the PE file. Instead, they are often scattered around the PE file by a compiler. For example, the Import Directory has a second component the Import Address Table which is often put in a completely different PE section (usually ``.text`` or ``.data``) than the Import Directory itself (in ``.idata`` or ``.rdata``). To make reading and interpreting these data structures more convenient for the end-user, the ``AsmResolver.PE`` package adopted some design choices to abstract these details away to make things more natural to work with. The downside of this is that writing these structures requires you to specify where AsmResolver should place these models in the final PE file. +The PE file format defines many complex data structures. +While some of these models are represented in AsmResolver using classes that derive from ``ISegment``, a lot of the classes that represent these data structures do not implement this interface, and as such cannot be used as a value for the ``Contents`` property of a ``PESection`` directly. +This is due to the fact that most of these models are not required to be one single entity or chunk of continuous memory within the PE file. Instead, they are often scattered around the PE file by a compiler. +For example, the Import Directory has a second component the Import Address Table which is often put in a completely different PE section (usually ``.text`` or ``.data``) than the Import Directory itself (in ``.idata`` or ``.rdata``). +To make reading and interpreting these data structures more convenient for the end-user, the ``AsmResolver.PE`` package adopted some design choices to abstract these details away to make things more natural to work with. +The downside of this is that writing these structures requires you to specify where AsmResolver should place these models in the final PE file. -In ``AsmResolver.PE``, most models for which is the case reside in their own namespace, and have their own set of classes dedicated to constructing new segments defined in a ``Builder`` sub namespace. For example, the Win32 resources directory models reside in ``AsmResolver.PE.Win32Resources``, but the actual builder classes are put in a sub namespace called ``AsmResolver.PE.Win32Resources.Builder``. +In ``AsmResolver.PE``, most models for which is the case reside in their own namespace, and have their own set of classes dedicated to constructing new segments defined in a ``Builder`` sub-namespace. +For example, the Win32 resources directory models reside in ``AsmResolver.PE.Win32Resources``, but the actual builder classes are put in a sub namespace called ``AsmResolver.PE.Win32Resources.Builder``. .. code-block:: csharp @@ -94,7 +131,8 @@ A more complicated structure such as the Imports Directory can be build like the Using PEFileBuilders -------------------- -As a lot of the PE file building process will be similar for many types of PE file layouts (such as the construction of the file and optional headers), AsmResolver comes with a base class called ``PEFileBuilderBase`` that abstracts many of these similarities away. Rather than defining and building up everything yourself, the ``PEFileBuilderBase`` allows you to override a couple of methods: +As a lot of the PE file-building process will be similar for many types of PE file layouts (such as the construction of the file and optional headers), AsmResolver comes with a base class called ``PEFileBuilderBase`` that abstracts many of these similarities away. +Rather than defining and building up everything yourself, the ``PEFileBuilderBase`` allows you to override a couple of methods: .. code-block:: csharp @@ -116,10 +154,10 @@ As a lot of the PE file building process will be similar for many types of PE fi protected override IEnumerable CreateDataDirectories(PEFile peFile, IPEImage image, MyBuilderContext context) { /* Create data directories here */ - } + } } - public class MyBuilderContext + public class MyBuilderContext { /* Define here additional state data to be used in your builder. */ } @@ -132,4 +170,4 @@ This can then be used like the following: IPEImage image = ... var builder = new MyPEFileBuilder(); - PEFile file = builder.CreateFile(image); \ No newline at end of file + PEFile file = builder.CreateFile(image); diff --git a/docs/peimage/tls.rst b/docs/peimage/tls.rst index ad2f97ac0..938d4dc74 100644 --- a/docs/peimage/tls.rst +++ b/docs/peimage/tls.rst @@ -55,7 +55,7 @@ Adding a new TLS directory to an image can be done using the parameterless const var tlsDirectory = new TlsDirectory(); image.TlsDirectory = tlsDirectory; -A TLS directory references all its sub segments using virtual addresses (VA) rather than relative addresses (RVA). This means that constructing a relocatable PE image with a TLS directory requires base relocation entries to be registered that let the Windows PE loader rebase all virtual addresses used in the directory when necessary. To quickly register all the required base relocations, you can call the ``GetRequiredBaseRelocations`` method and add all returned entries to the base relocation directory of the PE image: +A TLS directory references all its sub-segments using virtual addresses (VA) rather than relative addresses (RVA). This means that constructing a relocatable PE image with a TLS directory requires base relocation entries to be registered that let the Windows PE loader rebase all virtual addresses used in the directory when necessary. To quickly register all the required base relocations, you can call the ``GetRequiredBaseRelocations`` method and add all returned entries to the base relocation directory of the PE image: .. code-block:: csharp diff --git a/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.CodedIndices.cs b/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.CodedIndices.cs index 2aeee551a..52e6c3df8 100644 --- a/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.CodedIndices.cs +++ b/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.CodedIndices.cs @@ -17,6 +17,11 @@ private void AddCustomAttribute(MetadataToken ownerToken, CustomAttribute attrib { var table = Metadata.TablesStream.GetSortedTable(TableIndex.CustomAttribute); + // Ensure the signature defines the right parameters w.r.t. the constructor's parameters. + if (attribute.Constructor is not null && attribute.Signature is not null) + attribute.Signature.IsCompatibleWith(attribute.Constructor, ErrorListener); + + // Add it. var encoder = Metadata.TablesStream.GetIndexEncoder(CodedIndex.HasCustomAttribute); var row = new CustomAttributeRow( encoder.EncodeToken(ownerToken), diff --git a/src/AsmResolver.DotNet/Builder/DotNetDirectoryFactory.cs b/src/AsmResolver.DotNet/Builder/DotNetDirectoryFactory.cs index 7750a6146..41218981a 100644 --- a/src/AsmResolver.DotNet/Builder/DotNetDirectoryFactory.cs +++ b/src/AsmResolver.DotNet/Builder/DotNetDirectoryFactory.cs @@ -1,10 +1,13 @@ using System; +using System.Linq; using AsmResolver.DotNet.Builder.Discovery; using AsmResolver.DotNet.Builder.Metadata; using AsmResolver.DotNet.Code; using AsmResolver.DotNet.Code.Cil; using AsmResolver.DotNet.Code.Native; +using AsmResolver.DotNet.Serialized; using AsmResolver.PE.DotNet; +using AsmResolver.PE.DotNet.Metadata; using AsmResolver.PE.DotNet.Metadata.Blob; using AsmResolver.PE.DotNet.Metadata.Guid; using AsmResolver.PE.DotNet.Metadata.Strings; @@ -122,7 +125,16 @@ public virtual DotNetDirectoryBuildResult CreateDotNetDirectory( else if ((module.Attributes & DotNetDirectoryFlags.StrongNameSigned) != 0) buffer.StrongNameSize = 0x80; - return buffer.CreateDirectory(); + var result = buffer.CreateDirectory(); + + // If we need to preserve streams or stream order, apply the shifts accordingly. + if (module is SerializedModuleDefinition serializedModule + && (MetadataBuilderFlags & (MetadataBuilderFlags.PreserveUnknownStreams | MetadataBuilderFlags.PreserveStreamOrder)) != 0) + { + ReorderMetadataStreams(serializedModule, result.Directory.Metadata!); + } + + return result; } private MemberDiscoveryResult DiscoverMemberDefinitionsInModule(ModuleDefinition module) @@ -278,5 +290,61 @@ private static void ImportTables(ModuleDefinition module, TableIndex ta foreach (var member in module.TokenAllocator.GetAssignees(tableIndex)) importAction((TMember) member); } + + private void ReorderMetadataStreams(SerializedModuleDefinition serializedModule, IMetadata newMetadata) + { + IMetadataStream? GetStreamOrNull() + where TStream : class, IMetadataStream + { + return newMetadata.TryGetStream(out TStream? stream) + ? stream + : null; + } + + var readerContext = serializedModule.ReaderContext; + var streamIndices = new (int Index, IMetadataStream? Stream)[] + { + (readerContext.TablesStreamIndex, GetStreamOrNull()), + (readerContext.BlobStreamIndex, GetStreamOrNull()), + (readerContext.GuidStreamIndex, GetStreamOrNull()), + (readerContext.StringsStreamIndex, GetStreamOrNull()), + (readerContext.UserStringsStreamIndex, GetStreamOrNull()), + }; + + if ((MetadataBuilderFlags & MetadataBuilderFlags.PreserveUnknownStreams) != 0) + { + var originalStreams = serializedModule.DotNetDirectory.Metadata!.Streams; + + if ((MetadataBuilderFlags & MetadataBuilderFlags.PreserveStreamOrder) != 0) + { + newMetadata.Streams.Clear(); + + for (int i = 0; i < originalStreams.Count; i++) + { + var entry = streamIndices.FirstOrDefault(x => x.Index == i); + newMetadata.Streams.Insert(i, entry.Stream ?? originalStreams[i]); + } + } + else + { + for (int i = 0; i < originalStreams.Count; i++) + { + if (streamIndices.All(x => x.Index != i)) + newMetadata.Streams.Add(originalStreams[i]); + } + } + } + else if ((MetadataBuilderFlags & MetadataBuilderFlags.PreserveStreamOrder) != 0) + { + Array.Sort(streamIndices, (a, b) => a.Index.CompareTo(b.Index)); + newMetadata.Streams.Clear(); + + for (int i = 0; i < streamIndices.Length; i++) + { + if (streamIndices[i].Stream is { } stream) + newMetadata.Streams.Add(stream); + } + } + } } } diff --git a/src/AsmResolver.DotNet/Builder/MetadataBuilderFlags.cs b/src/AsmResolver.DotNet/Builder/MetadataBuilderFlags.cs index d0e4fcb7c..04672823d 100644 --- a/src/AsmResolver.DotNet/Builder/MetadataBuilderFlags.cs +++ b/src/AsmResolver.DotNet/Builder/MetadataBuilderFlags.cs @@ -126,11 +126,23 @@ public enum MetadataBuilderFlags | PreserveAssemblyReferenceIndices | PreserveMethodSpecificationIndices, /// - /// Indicates any kind of index into a blob or tables stream should be preserved whenever possible during the - /// construction of the metadata directory. + /// Indicates unconventional / spurious metadata streams present in the .NET metadata directory should be + /// preserved when possible. + /// + PreserveUnknownStreams = 0x20000, + + /// + /// Indicates unconventional metadata stream order in the .NET metadata directory should be preserved when + /// possible. + /// + PreserveStreamOrder = 0x40000, + + /// + /// Indicates any kind of index into a blob or tables stream, as well as unknown spurious metadata streams + /// should be preserved whenever possible during the construction of the metadata directory. /// PreserveAll = PreserveBlobIndices | PreserveGuidIndices | PreserveStringIndices | PreserveUserStringIndices - | PreserveTableIndices, + | PreserveTableIndices | PreserveUnknownStreams | PreserveStreamOrder, /// /// diff --git a/src/AsmResolver.DotNet/Code/Native/NativeMethodBodySerializer.cs b/src/AsmResolver.DotNet/Code/Native/NativeMethodBodySerializer.cs index b1a8d9f37..ca0b90984 100644 --- a/src/AsmResolver.DotNet/Code/Native/NativeMethodBodySerializer.cs +++ b/src/AsmResolver.DotNet/Code/Native/NativeMethodBodySerializer.cs @@ -24,17 +24,17 @@ public ISegmentReference SerializeMethodBody(MethodBodySerializationContext cont var provider = context.SymbolsProvider; // Create new raw code segment containing the native code. - var segment = new CodeSegment(nativeMethodBody.Code); + var segment = new DataSegment(nativeMethodBody.Code).AsPatchedSegment(); - // Process fixups. + // Process address fixups. for (int i = 0; i < nativeMethodBody.AddressFixups.Count; i++) { // Import symbol. var fixup = nativeMethodBody.AddressFixups[i]; var symbol = FinalizeSymbol(segment, provider, fixup.Symbol); - // Create new fixup with imported symbol. - segment.AddressFixups.Add(new AddressFixup(fixup.Offset, fixup.Type, symbol)); + // Fixup address. + segment.Patch(fixup.Offset, fixup.Type, symbol); // Add base relocation when necessary. AddBaseRelocations(segment, provider, fixup); @@ -49,7 +49,7 @@ public ISegmentReference SerializeMethodBody(MethodBodySerializationContext cont /// The code segment that is being constructed. /// The object responsible for providing symbols referenced by the native method body. /// The fixup to build base relocations for. - protected virtual void AddBaseRelocations(CodeSegment segment, INativeSymbolsProvider provider, AddressFixup fixup) + protected virtual void AddBaseRelocations(ISegment segment, INativeSymbolsProvider provider, AddressFixup fixup) { switch (fixup.Type) { @@ -74,7 +74,7 @@ protected virtual void AddBaseRelocations(CodeSegment segment, INativeSymbolsPro /// The object responsible for providing symbols referenced by the native method body. /// The symbol to reference. /// The symbol. - protected virtual ISymbol FinalizeSymbol(CodeSegment result, INativeSymbolsProvider provider, ISymbol symbol) + protected virtual ISymbol FinalizeSymbol(ISegment result, INativeSymbolsProvider provider, ISymbol symbol) { if (symbol is NativeLocalSymbol local) return new Symbol(result.ToReference((int) local.Offset)); diff --git a/src/AsmResolver.DotNet/ModuleDefinition.cs b/src/AsmResolver.DotNet/ModuleDefinition.cs index 13a5c9669..710745aa4 100644 --- a/src/AsmResolver.DotNet/ModuleDefinition.cs +++ b/src/AsmResolver.DotNet/ModuleDefinition.cs @@ -304,7 +304,7 @@ public ModuleDefinition(Utf8String? name) AssemblyReferences.Add((AssemblyReference)CorLibTypeFactory.CorLibScope); MetadataResolver = new DefaultMetadataResolver(new DotNetFrameworkAssemblyResolver()); - TopLevelTypes.Add(new TypeDefinition(null, "", 0)); + TopLevelTypes.Add(new TypeDefinition(null, TypeDefinition.ModuleTypeName, 0)); } /// @@ -325,7 +325,7 @@ public ModuleDefinition(string? name, AssemblyReference corLib) OriginalTargetRuntime = DetectTargetRuntime(); MetadataResolver = new DefaultMetadataResolver(CreateAssemblyResolver(UncachedFileService.Instance)); - TopLevelTypes.Add(new TypeDefinition(null, "", 0)); + TopLevelTypes.Add(new TypeDefinition(null, TypeDefinition.ModuleTypeName, 0)); } /// @@ -792,7 +792,7 @@ public ReferenceImporter DefaultImporter /// /// Looks up a member by its metadata token. /// - /// The token of the member to lookup. + /// The token of the member to look up. /// The member. /// /// Occurs when the module does not support looking up members by its token. @@ -803,10 +803,28 @@ public ReferenceImporter DefaultImporter public virtual IMetadataMember LookupMember(MetadataToken token) => throw new InvalidOperationException("Cannot lookup members by tokens from a non-serialized module."); + /// + /// Looks up a member by its metadata token, and casts it to the provided metadata member type. + /// + /// The token of the member to look up. + /// The type of member to look up. + /// The casted member. + /// + /// Occurs when the module does not support looking up members by its token. + /// + /// + /// Occurs when a metadata token indexes a table that cannot be converted to a metadata member. + /// + public T LookupMember(MetadataToken token) + where T : class, IMetadataMember + { + return (T) LookupMember(token); + } + /// /// Attempts to look up a member by its metadata token. /// - /// The token of the member to lookup. + /// The token of the member to look up. /// The member, or null if the lookup failed. /// true if the member was successfully looked up, false otherwise. public virtual bool TryLookupMember(MetadataToken token, [NotNullWhen(true)] out IMetadataMember? member) @@ -815,10 +833,30 @@ public virtual bool TryLookupMember(MetadataToken token, [NotNullWhen(true)] out return false; } + /// + /// Attempts to look up a member by its metadata token, and cast it to the specified metadata member type. + /// + /// The token of the member to look up. + /// The member, or null if the lookup failed. + /// The type of member to look up. + /// true if the member was successfully looked up and of the correct type, false otherwise. + public bool TryLookupMember(MetadataToken token, [NotNullWhen((true))] out T? member) + where T : class, IMetadataMember + { + if (TryLookupMember(token, out var resolved) && resolved is T casted) + { + member = casted; + return true; + } + + member = null; + return false; + } + /// /// Looks up a user string by its string token. /// - /// The token of the string to lookup. + /// The token of the string to look up. /// The member. /// /// Occurs when the module does not support looking up string by its token. @@ -915,7 +953,19 @@ public IEnumerable GetAllTypes() /// Obtains the global scope type of the .NET module. /// /// The module type. - public TypeDefinition? GetModuleType() => TopLevelTypes.Count > 0 ? TopLevelTypes[0] : null; + public TypeDefinition? GetModuleType() + { + if (TopLevelTypes.Count == 0) + return null; + + var firstType = TopLevelTypes[0]; + + // Only .NET Framework allows the module type to be renamed to something else. + if (!OriginalTargetRuntime.IsNetFramework && !firstType.IsTypeOfUtf8(null, TypeDefinition.ModuleTypeName)) + return null; + + return firstType; + } /// /// Obtains or creates the global scope type of the .NET module. @@ -923,13 +973,15 @@ public IEnumerable GetAllTypes() /// The module type. public TypeDefinition GetOrCreateModuleType() { - if (TopLevelTypes.Count == 0 || TopLevelTypes[0].Name != "") + var moduleType = GetModuleType(); + + if (moduleType is null) { - var moduleType = new TypeDefinition(null, "", 0); + moduleType = new TypeDefinition(null, TypeDefinition.ModuleTypeName, 0); TopLevelTypes.Insert(0, moduleType); } - return TopLevelTypes[0]; + return moduleType; } /// diff --git a/src/AsmResolver.DotNet/Serialized/ModuleReaderContext.cs b/src/AsmResolver.DotNet/Serialized/ModuleReaderContext.cs index e5dafc8ff..737eab418 100644 --- a/src/AsmResolver.DotNet/Serialized/ModuleReaderContext.cs +++ b/src/AsmResolver.DotNet/Serialized/ModuleReaderContext.cs @@ -46,18 +46,23 @@ public ModuleReaderContext(IPEImage image, SerializedModuleDefinition parentModu { case TablesStream tablesStream when TablesStream is null: TablesStream = tablesStream; + TablesStreamIndex = i; break; case BlobStream blobStream when BlobStream is null || !isEncMetadata: BlobStream = blobStream; + BlobStreamIndex = i; break; case GuidStream guidStream when GuidStream is null || !isEncMetadata: GuidStream = guidStream; + GuidStreamIndex = i; break; case StringsStream stringsStream when StringsStream is null || !isEncMetadata: StringsStream = stringsStream; + StringsStreamIndex = i; break; case UserStringsStream userStringsStream when UserStringsStream is null || !isEncMetadata: UserStringsStream = userStringsStream; + UserStringsStreamIndex = i; break; } } @@ -96,6 +101,14 @@ public TablesStream TablesStream get; } + /// + /// Gets the original index of the tables stream. + /// + public int TablesStreamIndex + { + get; + } = -1; + /// /// Gets the main blob stream in the metadata directory. /// @@ -104,6 +117,14 @@ public BlobStream? BlobStream get; } + /// + /// Gets the original index of the blob stream. + /// + public int BlobStreamIndex + { + get; + } = -1; + /// /// Gets the main GUID stream in the metadata directory. /// @@ -112,6 +133,14 @@ public GuidStream? GuidStream get; } + /// + /// Gets the original index of the GUID stream. + /// + public int GuidStreamIndex + { + get; + } = -1; + /// /// Gets the main strings stream in the metadata directory. /// @@ -120,6 +149,14 @@ public StringsStream? StringsStream get; } + /// + /// Gets the original index of the strings stream. + /// + public int StringsStreamIndex + { + get; + } = -1; + /// /// Gets the main user-strings stream in the metadata directory. /// @@ -128,6 +165,14 @@ public UserStringsStream? UserStringsStream get; } + /// + /// Gets the original index of the user-strings stream. + /// + public int UserStringsStreamIndex + { + get; + } = -1; + /// /// Gets the reader parameters. /// diff --git a/src/AsmResolver.DotNet/Serialized/SerializedModuleDefinition.cs b/src/AsmResolver.DotNet/Serialized/SerializedModuleDefinition.cs index 9b7866ab4..c766f2427 100644 --- a/src/AsmResolver.DotNet/Serialized/SerializedModuleDefinition.cs +++ b/src/AsmResolver.DotNet/Serialized/SerializedModuleDefinition.cs @@ -336,12 +336,11 @@ private CorLibTypeFactory CreateCorLibTypeFactory() // TODO: perhaps check public key tokens. IResolutionScope? mostRecentCorLib = null; - var mostRecentVersion = new Version(); foreach (var reference in AssemblyReferences) { if (reference.Name is not null && KnownCorLibs.KnownCorLibNames.Contains(reference.Name)) { - if (mostRecentVersion < reference.Version) + if (mostRecentCorLib is null || reference.Version > mostRecentCorLib.GetAssembly()!.Version) mostRecentCorLib = reference; } } diff --git a/src/AsmResolver.DotNet/Signatures/CustomAttributeSignature.cs b/src/AsmResolver.DotNet/Signatures/CustomAttributeSignature.cs index 1a399f616..f382851b4 100644 --- a/src/AsmResolver.DotNet/Signatures/CustomAttributeSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/CustomAttributeSignature.cs @@ -90,8 +90,9 @@ public static CustomAttributeSignature FromReader( ICustomAttributeType ctor, in BinaryStreamReader reader) { + var genericContext = GenericContext.FromMethod(ctor); var argumentTypes = ctor.Signature?.ParameterTypes ?? Array.Empty(); - return new SerializedCustomAttributeSignature(context, argumentTypes, reader); + return new SerializedCustomAttributeSignature(context, argumentTypes, genericContext, reader); } /// @@ -155,6 +156,43 @@ protected override void WriteContents(in BlobSerializationContext context) for (int i = 0; i < NamedArguments.Count; i++) NamedArguments[i].Write(context); } + + /// + /// Validates whether the signature is compatible with the provided attribute constructor. + /// + /// The constructor to validate against. + /// true if the constructor is compatible, false otherwise. + public bool IsCompatibleWith(ICustomAttributeType constructor) + { + return IsCompatibleWith(constructor, EmptyErrorListener.Instance); + } + + /// + /// Validates whether the signature is compatible with the provided attribute constructor. + /// + /// The constructor to validate against. + /// The object responsible for reporting any errors during the validation of the signature. + /// true if the constructor is compatible, false otherwise. + public virtual bool IsCompatibleWith(ICustomAttributeType constructor, IErrorListener listener) + { + var signature = constructor.Signature; + + int expectedCount = signature?.ParameterTypes.Count ?? 0; + if (expectedCount != FixedArguments.Count) + { + listener.MetadataBuilder( + $"Custom attribute constructor {constructor.SafeToString()} expects {expectedCount} arguments but the signature provided {FixedArguments.Count} arguments."); + return false; + } + + if (signature?.SentinelParameterTypes.Count > 0) + { + listener.MetadataBuilder($"Custom attribute constructor {constructor.SafeToString()} defines sentinel parameters."); + return false; + } + + return true; + } } } diff --git a/src/AsmResolver.DotNet/Signatures/Security/SecurityAttribute.cs b/src/AsmResolver.DotNet/Signatures/Security/SecurityAttribute.cs index abb55b287..8ec14c011 100644 --- a/src/AsmResolver.DotNet/Signatures/Security/SecurityAttribute.cs +++ b/src/AsmResolver.DotNet/Signatures/Security/SecurityAttribute.cs @@ -39,10 +39,7 @@ public static SecurityAttribute FromReader(in BlobReaderContext context, ref Bin } for (int i = 0; i < namedArgumentCount; i++) - { - var argument = CustomAttributeNamedArgument.FromReader(context, ref reader); - result.NamedArguments.Add(argument); - } + result.NamedArguments.Add(CustomAttributeNamedArgument.FromReader(context, ref reader)); return result; } diff --git a/src/AsmResolver.DotNet/Signatures/SerializedCustomAttributeSignature.cs b/src/AsmResolver.DotNet/Signatures/SerializedCustomAttributeSignature.cs index a7e6e6028..2b2138205 100644 --- a/src/AsmResolver.DotNet/Signatures/SerializedCustomAttributeSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/SerializedCustomAttributeSignature.cs @@ -10,22 +10,25 @@ namespace AsmResolver.DotNet.Signatures /// public class SerializedCustomAttributeSignature : CustomAttributeSignature { - private readonly BlobReaderContext _context; + private readonly BlobReaderContext _readerContext; + private readonly GenericContext _genericContext; private readonly TypeSignature[] _fixedArgTypes; private readonly BinaryStreamReader _reader; /// /// Initializes a new lazy custom attribute signature from an input blob stream reader. /// - /// The blob reading context the signature is situated in. + /// The blob reading context the signature is situated in. /// The types of all fixed arguments. + /// The generic context the arguments live in. /// The input blob reader. - public SerializedCustomAttributeSignature( - in BlobReaderContext context, + public SerializedCustomAttributeSignature(in BlobReaderContext readerContext, IEnumerable fixedArgTypes, + in GenericContext genericContext, in BinaryStreamReader reader) { - _context = context; + _readerContext = readerContext; + _genericContext = genericContext; _fixedArgTypes = fixedArgTypes.ToArray(); _reader = reader; } @@ -40,16 +43,19 @@ protected override void Initialize( // Verify magic header. ushort prologue = reader.ReadUInt16(); if (prologue != CustomAttributeSignaturePrologue) - _context.ReaderContext.BadImage("Input stream does not point to a valid custom attribute signature."); + _readerContext.ReaderContext.BadImage("Input stream does not point to a valid custom attribute signature."); // Read fixed arguments. for (int i = 0; i < _fixedArgTypes.Length; i++) - fixedArguments.Add(CustomAttributeArgument.FromReader(_context, _fixedArgTypes[i], ref reader)); + { + var instantiatedType = _fixedArgTypes[i].InstantiateGenericTypes(_genericContext); + fixedArguments.Add(CustomAttributeArgument.FromReader(_readerContext, instantiatedType, ref reader)); + } // Read named arguments. ushort namedArgumentCount = reader.ReadUInt16(); for (int i = 0; i < namedArgumentCount; i++) - namedArguments.Add(CustomAttributeNamedArgument.FromReader(_context, ref reader)); + namedArguments.Add(CustomAttributeNamedArgument.FromReader(_readerContext, ref reader)); } /// @@ -69,5 +75,11 @@ protected override void WriteContents(in BlobSerializationContext context) else _reader.Fork().WriteToOutput(context.Writer); } + + /// + public override bool IsCompatibleWith(ICustomAttributeType constructor, IErrorListener listener) + { + return !IsInitialized || base.IsCompatibleWith(constructor, listener); + } } } diff --git a/src/AsmResolver.DotNet/TypeDefinition.cs b/src/AsmResolver.DotNet/TypeDefinition.cs index ffe56e169..21821ad1c 100644 --- a/src/AsmResolver.DotNet/TypeDefinition.cs +++ b/src/AsmResolver.DotNet/TypeDefinition.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using System.Runtime.CompilerServices; using System.Threading; using AsmResolver.Collections; using AsmResolver.DotNet.Code.Cil; @@ -26,6 +25,8 @@ public class TypeDefinition : IOwnedCollectionElement, IOwnedCollectionElement { + internal static readonly Utf8String ModuleTypeName = ""; + private readonly LazyVariable _namespace; private readonly LazyVariable _name; private readonly LazyVariable _baseType; @@ -492,6 +493,22 @@ public bool IsDelegate } } + /// + /// true if this is the global (i.e., <Module>) type, otherwise false. + /// + /// + /// If the global (i.e., <Module>) type was not added or does not exist yet in the , + /// this will return false. + /// + public bool IsModuleType + { + get + { + var module = Module?.GetModuleType(); + return module != null && module == this; + } + } + /// /// Determines whether the type is marked as read-only. /// diff --git a/src/AsmResolver.PE.File/PESegmentReference.cs b/src/AsmResolver.PE.File/PESegmentReference.cs index 9eeeaea0a..f05889f7a 100644 --- a/src/AsmResolver.PE.File/PESegmentReference.cs +++ b/src/AsmResolver.PE.File/PESegmentReference.cs @@ -43,5 +43,8 @@ public uint Rva /// public ISegment? GetSegment() => throw new InvalidOperationException(); + + /// + public override string ToString() => $"0x{Rva:X8}"; } } diff --git a/src/AsmResolver.PE/Code/AddressFixup.cs b/src/AsmResolver.PE/Code/AddressFixup.cs index 32825c3b3..7ede26c26 100644 --- a/src/AsmResolver.PE/Code/AddressFixup.cs +++ b/src/AsmResolver.PE/Code/AddressFixup.cs @@ -1,11 +1,12 @@ using System; +using System.Diagnostics; namespace AsmResolver.PE.Code { /// - /// Provides information about a code or data segment referenced within a code segment for which - /// the final RVA is yet to be determined. + /// Provides information about a symbol referenced within a segment for which the final RVA is yet to be determined. /// + [DebuggerDisplay("Patch {Offset} with &{Symbol} as {Type}")] public readonly struct AddressFixup { /// @@ -20,7 +21,7 @@ public AddressFixup(uint offset, AddressFixupType type, ISymbol referencedObject Symbol = referencedObject ?? throw new ArgumentNullException(nameof(referencedObject)); Type = type; } - + /// /// Gets the offset relative to the start of the code segment pointing to the reference. /// @@ -48,4 +49,4 @@ public ISymbol Symbol /// public override string ToString() => $"+{Offset:X8} <{Symbol}> ({Type})"; } -} \ No newline at end of file +} diff --git a/src/AsmResolver.PE/Code/AddressFixupExtensions.cs b/src/AsmResolver.PE/Code/AddressFixupExtensions.cs new file mode 100644 index 000000000..39e50411c --- /dev/null +++ b/src/AsmResolver.PE/Code/AddressFixupExtensions.cs @@ -0,0 +1,54 @@ +using AsmResolver.Patching; + +namespace AsmResolver.PE.Code +{ + /// + /// Provides extensions to that adds patch overloads to quickly construct instances of + /// . + /// + public static class AddressFixupExtensions + { + /// + /// Adds an address fixup to the list of patches to apply. + /// + /// The segment to add the patch to. + /// The offset to start writing the address at, relative to the start of the segment. + /// The type of address to write. + /// The reference to write the RVA for. + /// The patched segment. + public static PatchedSegment Patch(this PatchedSegment segment, uint relativeOffset, AddressFixupType type, + ISymbol referencedObject) + { + return segment.Patch(new AddressFixup(relativeOffset, type, referencedObject)); + } + + /// + /// Adds an address fixup to the list of patches to apply. + /// + /// The segment to add the patch to. + /// The offset to start writing the address at, relative to the start of the segment. + /// The type of address to write. + /// The offset within the segment to point to, relative to the start of the segment. + /// The patched segment. + public static PatchedSegment Patch(this PatchedSegment segment, uint relativeOffset, AddressFixupType type, + uint symbolOffset) + { + return segment.Patch(new AddressFixup( + relativeOffset, + type, + new Symbol(segment.ToReference((int) symbolOffset)))); + } + + /// + /// Adds an address fixup to the list of patches to apply. + /// + /// The segment to add the patch to. + /// The fixup to apply. + /// The patched segment. + public static PatchedSegment Patch(this PatchedSegment segment, in AddressFixup fixup) + { + segment.Patches.Add(new AddressFixupPatch(fixup)); + return segment; + } + } +} diff --git a/src/AsmResolver.PE/Code/AddressFixupPatch.cs b/src/AsmResolver.PE/Code/AddressFixupPatch.cs new file mode 100644 index 000000000..a87631b18 --- /dev/null +++ b/src/AsmResolver.PE/Code/AddressFixupPatch.cs @@ -0,0 +1,56 @@ +using System; +using System.Diagnostics; +using AsmResolver.Patching; + +namespace AsmResolver.PE.Code +{ + /// + /// Implements a patch that patches a segment with an address to a symbol. + /// + [DebuggerDisplay("{Fixup}")] + public class AddressFixupPatch : IPatch + { + /// + /// Creates a new instance of the class. + /// + /// The fixup to apply. + public AddressFixupPatch(AddressFixup fixup) + { + Fixup = fixup; + } + + /// + /// Gets the fixup to apply. + /// + public AddressFixup Fixup + { + get; + } + + /// + public void Apply(in PatchContext context) + { + context.Writer.Offset = context.Segment.Offset + Fixup.Offset; + uint writerRva = context.Segment.Rva + Fixup.Offset; + uint targetRva = Fixup.Symbol.GetReference()?.Rva ?? 0; + + switch (Fixup.Type) + { + case AddressFixupType.Absolute32BitAddress: + context.Writer.WriteUInt32((uint) (context.ImageBase + targetRva)); + break; + + case AddressFixupType.Relative32BitAddress: + context.Writer.WriteInt32((int) (targetRva - (writerRva + 4))); + break; + + case AddressFixupType.Absolute64BitAddress: + context.Writer.WriteUInt64(context.ImageBase + targetRva); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + } +} diff --git a/src/AsmResolver.PE/Code/AddressFixupType.cs b/src/AsmResolver.PE/Code/AddressFixupType.cs index 4210ab7f3..b8b522057 100644 --- a/src/AsmResolver.PE/Code/AddressFixupType.cs +++ b/src/AsmResolver.PE/Code/AddressFixupType.cs @@ -1,7 +1,7 @@ namespace AsmResolver.PE.Code { /// - /// Defines all possible address fixup types that can be applied in a . + /// Defines all possible address fixup types that can be applied in an . /// public enum AddressFixupType { diff --git a/src/AsmResolver.PE/Code/CodeSegment.cs b/src/AsmResolver.PE/Code/CodeSegment.cs index b392e146e..f6dba5c0c 100644 --- a/src/AsmResolver.PE/Code/CodeSegment.cs +++ b/src/AsmResolver.PE/Code/CodeSegment.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using AsmResolver.IO; +using AsmResolver.Patching; namespace AsmResolver.PE.Code { /// /// Represents a chunk of native code in a portable executable. /// + [Obsolete("This class has been superseded by AsmResolver.Patching.PatchedSegment.")] public class CodeSegment : SegmentBase { /// @@ -72,32 +74,9 @@ public override void Write(IBinaryStreamWriter writer) writer.WriteBytes(Code); for (int i = 0; i < AddressFixups.Count; i++) - ApplyAddressFixup(writer, AddressFixups[i]); + new AddressFixupPatch(AddressFixups[i]).Apply(new PatchContext(this, ImageBase, writer)); writer.Offset = Offset + GetPhysicalSize(); } - - private void ApplyAddressFixup(IBinaryStreamWriter writer, in AddressFixup fixup) - { - writer.Offset = Offset + fixup.Offset; - uint writerRva = Rva + fixup.Offset; - uint targetRva = fixup.Symbol.GetReference()?.Rva ?? 0; - - switch (fixup.Type) - { - case AddressFixupType.Absolute32BitAddress: - writer.WriteUInt32((uint) (ImageBase + targetRva)); - break; - case AddressFixupType.Relative32BitAddress: - writer.WriteInt32((int) (targetRva - (writerRva + 4))); - break; - case AddressFixupType.Absolute64BitAddress: - writer.WriteUInt64(ImageBase + targetRva); - break; - default: - throw new ArgumentOutOfRangeException(); - } - } - } } diff --git a/src/AsmResolver.PE/Platforms/Amd64Platform.cs b/src/AsmResolver.PE/Platforms/Amd64Platform.cs index 895e39c9d..08c042736 100644 --- a/src/AsmResolver.PE/Platforms/Amd64Platform.cs +++ b/src/AsmResolver.PE/Platforms/Amd64Platform.cs @@ -27,12 +27,13 @@ public static Amd64Platform Instance /// public override RelocatableSegment CreateThunkStub(ISymbol entryPoint) { - var segment = new CodeSegment(new byte[] - { - 0x48, 0xA1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // rex.w rex.b mov rax, [&symbol] - 0xFF, 0xE0 // jmp [rax] - }); - segment.AddressFixups.Add(new AddressFixup(2, AddressFixupType.Absolute64BitAddress, entryPoint)); + var segment = new DataSegment(new byte[] + { + 0x48, 0xA1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // rex.w rex.b mov rax, [&symbol] + 0xFF, 0xE0 // jmp [rax] + }) + .AsPatchedSegment() + .Patch(2, AddressFixupType.Absolute64BitAddress, entryPoint); return new RelocatableSegment(segment, new[] { diff --git a/src/AsmResolver.PE/Platforms/I386Platform.cs b/src/AsmResolver.PE/Platforms/I386Platform.cs index 87c431ae0..26cb29d78 100644 --- a/src/AsmResolver.PE/Platforms/I386Platform.cs +++ b/src/AsmResolver.PE/Platforms/I386Platform.cs @@ -27,11 +27,12 @@ public static I386Platform Instance /// public override RelocatableSegment CreateThunkStub(ISymbol entryPoint) { - var segment = new CodeSegment(new byte[] - { - 0xFF, 0x25, 0x00, 0x00, 0x00, 0x00 // jmp [&symbol] - }); - segment.AddressFixups.Add(new AddressFixup(2, AddressFixupType.Absolute32BitAddress, entryPoint)); + var segment = new DataSegment(new byte[] + { + 0xFF, 0x25, 0x00, 0x00, 0x00, 0x00 // jmp [&symbol] + }) + .AsPatchedSegment() + .Patch(2, AddressFixupType.Absolute32BitAddress, entryPoint); return new RelocatableSegment(segment, new[] { diff --git a/src/AsmResolver/IO/BinaryStreamReader.cs b/src/AsmResolver/IO/BinaryStreamReader.cs index 9a924cda9..a5d5ae3b8 100644 --- a/src/AsmResolver/IO/BinaryStreamReader.cs +++ b/src/AsmResolver/IO/BinaryStreamReader.cs @@ -171,7 +171,7 @@ public byte ReadByte() } /// - /// Reads a single unsigned 16 bit integer from the input stream, and advances the current offset by two. + /// Reads a single unsigned 16-bit integer from the input stream, and advances the current offset by two. /// /// The consumed value. public ushort ReadUInt16() @@ -184,7 +184,7 @@ public ushort ReadUInt16() } /// - /// Reads a single unsigned 32 bit integer from the input stream, and advances the current offset by four. + /// Reads a single unsigned 32-bit integer from the input stream, and advances the current offset by four. /// /// The consumed value. public uint ReadUInt32() @@ -199,7 +199,7 @@ public uint ReadUInt32() } /// - /// Reads a single unsigned 64 bit integer from the input stream, and advances the current offset by eight. + /// Reads a single unsigned 64-bit integer from the input stream, and advances the current offset by eight. /// /// The consumed value. public ulong ReadUInt64() @@ -228,7 +228,7 @@ public sbyte ReadSByte() } /// - /// Reads a single signed 16 bit integer from the input stream, and advances the current offset by two. + /// Reads a single signed 16-bit integer from the input stream, and advances the current offset by two. /// /// The consumed value. public short ReadInt16() @@ -241,7 +241,7 @@ public short ReadInt16() } /// - /// Reads a single signed 32 bit integer from the input stream, and advances the current offset by four. + /// Reads a single signed 32-bit integer from the input stream, and advances the current offset by four. /// /// The consumed value. public int ReadInt32() @@ -256,7 +256,7 @@ public int ReadInt32() } /// - /// Reads a single signed 64 bit integer from the input stream, and advances the current offset by eight. + /// Reads a single signed 64-bit integer from the input stream, and advances the current offset by eight. /// /// The consumed value. public long ReadInt64() @@ -275,7 +275,7 @@ public long ReadInt64() } /// - /// Reads a single signed 32 bit single precision floating point number from the input stream, and advances the + /// Reads a single signed 32-bit single precision floating point number from the input stream, and advances the /// current offset by four. /// /// The consumed value. @@ -286,7 +286,7 @@ public unsafe float ReadSingle() } /// - /// Reads a single signed 64 bit double precision floating point number from the input stream, and advances the + /// Reads a single signed 64-bit double precision floating point number from the input stream, and advances the /// current offset by four. /// /// The consumed value. @@ -350,54 +350,54 @@ public byte[] ReadToEnd() } /// - /// Reads bytes from the input stream until the provided delimeter byte is reached. + /// Reads bytes from the input stream until the provided delimiter byte is reached. /// - /// The delimeter byte to stop at. - /// The read bytes, including the delimeter if it was found. - public byte[] ReadBytesUntil(byte delimeter) => ReadBytesUntil(delimeter, true); + /// The delimiter byte to stop at. + /// The read bytes, including the delimiter if it was found. + public byte[] ReadBytesUntil(byte delimiter) => ReadBytesUntil(delimiter, true); /// - /// Reads bytes from the input stream until the provided delimeter byte is reached. + /// Reads bytes from the input stream until the provided delimiter byte is reached. /// - /// The delimeter byte to stop at. - /// - /// true if the final delimeter should be included in the return value, false otherwise. + /// The delimiter byte to stop at. + /// + /// true if the final delimiter should be included in the return value, false otherwise. /// /// The read bytes. /// - /// This function always consumes the delimeter from the input stream if it is present, regardless of the value - /// of . + /// This function always consumes the delimiter from the input stream if it is present, regardless of the value + /// of . /// - public byte[] ReadBytesUntil(byte delimeter, bool includeDelimeterInReturn) + public byte[] ReadBytesUntil(byte delimiter, bool includeDelimiterInReturn) { var lookahead = Fork(); - bool hasConsumedDelimeter = lookahead.AdvanceUntil(delimeter, includeDelimeterInReturn); + bool hasConsumedDelimiter = lookahead.AdvanceUntil(delimiter, includeDelimiterInReturn); byte[] buffer = new byte[lookahead.RelativeOffset - RelativeOffset]; ReadBytes(buffer, 0, buffer.Length); - if (hasConsumedDelimeter) + if (hasConsumedDelimiter) ReadByte(); return buffer; } /// - /// Advances the reader until the provided delimeter byte is reached. + /// Advances the reader until the provided delimiter byte is reached. /// - /// The delimeter byte to stop at. - /// - /// true if the final delimeter should be consumed if available, false otherwise. + /// The delimiter byte to stop at. + /// + /// true if the final delimiter should be consumed if available, false otherwise. /// - /// true if the delimeter byte was found and consumed, false otherwise. - public bool AdvanceUntil(byte delimeter, bool consumeDelimeter) + /// true if the delimiter byte was found and consumed, false otherwise. + public bool AdvanceUntil(byte delimiter, bool consumeDelimiter) { while (RelativeOffset < Length) { byte b = ReadByte(); - if (b == delimeter) + if (b == delimiter) { - if (!consumeDelimeter) + if (!consumeDelimiter) { RelativeOffset--; return true; diff --git a/src/AsmResolver/IReadableSegment.cs b/src/AsmResolver/IReadableSegment.cs index 2d3aea65a..f07323854 100644 --- a/src/AsmResolver/IReadableSegment.cs +++ b/src/AsmResolver/IReadableSegment.cs @@ -56,6 +56,5 @@ public static byte[] ToArray(this IReadableSegment segment) { return segment.CreateReader().ReadToEnd(); } - } } diff --git a/src/AsmResolver/ISegment.cs b/src/AsmResolver/ISegment.cs index dce4aec3c..849245fd0 100644 --- a/src/AsmResolver/ISegment.cs +++ b/src/AsmResolver/ISegment.cs @@ -1,6 +1,9 @@ using System; +using System.IO; using System.Linq; using System.Text; +using AsmResolver.IO; +using AsmResolver.Patching; namespace AsmResolver { @@ -147,5 +150,63 @@ public static string CreateEscapedString(this string literal) public static ISegmentReference ToReference(this ISegment segment, int additive) => additive == 0 ? new SegmentReference(segment) : new RelativeReference(segment, additive); + + /// + /// Serializes the segment by calling and writes the result into a byte array. + /// + /// The segment to serialize to + /// The resulting byte array. + public static byte[] WriteIntoArray(this ISegment segment) + { + using var stream = new MemoryStream(); + segment.Write(new BinaryStreamWriter(stream)); + return stream.ToArray(); + } + + /// + /// Serializes the segment by calling and writes the result into a byte array. + /// + /// The segment to serialize to + /// The memory stream writer pool to rent temporary writers from. + /// The resulting byte array. + public static byte[] WriteIntoArray(this ISegment segment, MemoryStreamWriterPool pool) + { + using var rentedWriter = pool.Rent(); + segment.Write(rentedWriter.Writer); + return rentedWriter.GetData(); + } + + /// + /// Wraps the provided segment into a , making it eligible for applying + /// post-serialization patches. + /// + /// The segment to wrap. + /// + /// The wrapped segment, or if it is already an instance of + /// . + /// + public static PatchedSegment AsPatchedSegment(this ISegment segment) => segment.AsPatchedSegment(false); + + /// + /// Wraps the provided segment into a , making it eligible for applying + /// post-serialization patches. + /// + /// The segment to wrap. + /// + /// Indicates whether the segment should always be wrapped into a new instance of , + /// regardless of whether is already an instance of + /// or not. + /// + /// + /// The wrapped segment, or if it is already an instance of + /// and is set to true. + /// + public static PatchedSegment AsPatchedSegment(this ISegment segment, bool alwaysCreateNew) + { + if (alwaysCreateNew) + return new PatchedSegment(segment); + + return segment as PatchedSegment ?? new PatchedSegment(segment); + } } } diff --git a/src/AsmResolver/Patching/BytesPatch.cs b/src/AsmResolver/Patching/BytesPatch.cs new file mode 100644 index 000000000..6e1233c9b --- /dev/null +++ b/src/AsmResolver/Patching/BytesPatch.cs @@ -0,0 +1,46 @@ +using System.Diagnostics; +using AsmResolver.IO; + +namespace AsmResolver.Patching +{ + /// + /// Patches an instance of with a sequence of bytes. + /// + [DebuggerDisplay("Patch {RelativeOffset} with {NewData.Length} bytes")] + public sealed class BytesPatch : IPatch + { + /// + /// Creates a new bytes patch. + /// + /// The offset to start writing at. + /// The new data. + public BytesPatch(uint relativeOffset, byte[] newData) + { + RelativeOffset = relativeOffset; + NewData = newData; + } + + /// + /// Gets the offset relative to the start of the segment to start writing at. + /// + public uint RelativeOffset + { + get; + } + + /// + /// Gets the data to write. + /// + public byte[] NewData + { + get; + } + + /// + public void Apply(in PatchContext context) + { + context.Writer.Offset = context.Segment.Offset + RelativeOffset; + context.Writer.WriteBytes(NewData); + } + } +} diff --git a/src/AsmResolver/Patching/IPatch.cs b/src/AsmResolver/Patching/IPatch.cs new file mode 100644 index 000000000..ec917fe65 --- /dev/null +++ b/src/AsmResolver/Patching/IPatch.cs @@ -0,0 +1,15 @@ +namespace AsmResolver.Patching +{ + /// + /// Provides a mechanism for patching an instance of after it was serialized into its + /// binary representation. + /// + public interface IPatch + { + /// + /// Applies the patch. + /// + /// The context in which to + void Apply(in PatchContext context); + } +} diff --git a/src/AsmResolver/Patching/PatchContext.cs b/src/AsmResolver/Patching/PatchContext.cs new file mode 100644 index 000000000..3d8949e71 --- /dev/null +++ b/src/AsmResolver/Patching/PatchContext.cs @@ -0,0 +1,47 @@ +using AsmResolver.IO; + +namespace AsmResolver.Patching +{ + /// + /// Provides members describing the context in which a patch may be situated in. + /// + public readonly struct PatchContext + { + /// + /// Creates a new instance of the structure. + /// + /// The segment to be patched. + /// The image base to assume while patching. + /// The object responsible for writing the patches. + public PatchContext(ISegment segment, ulong imageBase, IBinaryStreamWriter writer) + { + Segment = segment; + ImageBase = imageBase; + Writer = writer; + } + + /// + /// Gets the segment to be patched. + /// + public ISegment Segment + { + get; + } + + /// + /// Gets the image base that is assumed while patching. + /// + public ulong ImageBase + { + get; + } + + /// + /// Gets the object responsible for writing the patches. + /// + public IBinaryStreamWriter Writer + { + get; + } + } +} diff --git a/src/AsmResolver/Patching/PatchedSegment.cs b/src/AsmResolver/Patching/PatchedSegment.cs new file mode 100644 index 000000000..6ed40509c --- /dev/null +++ b/src/AsmResolver/Patching/PatchedSegment.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using AsmResolver.IO; + +namespace AsmResolver.Patching +{ + /// + /// Provides a wrapper around an instance of a that patches its binary representation + /// while it is being serialized to an output stream. + /// + [DebuggerDisplay("Patched {Contents} (Count = {Patches.Count})")] + public class PatchedSegment : IReadableSegment + { + private ulong _imageBase; + + /// + /// Wraps a segment into a new instance of the class. + /// + /// The segment to patch. + public PatchedSegment(ISegment contents) + { + Contents = contents; + } + + /// + /// Gets the original segment that is being patched. + /// + public ISegment Contents + { + get; + } + + /// + /// Gets a list of patches to apply to the segment. + /// + public IList Patches + { + get; + } = new List(); + + /// + public ulong Offset => Contents.Offset; + + /// + public uint Rva => Contents.Rva; + + /// + public bool CanUpdateOffsets => Contents.CanUpdateOffsets; + + /// + public uint GetPhysicalSize() => Contents.GetPhysicalSize(); + + /// + public uint GetVirtualSize() => Contents.GetVirtualSize(); + + /// + public void UpdateOffsets(in RelocationParameters parameters) + { + Contents.UpdateOffsets(in parameters); + _imageBase = parameters.ImageBase; + } + + /// + public BinaryStreamReader CreateReader(ulong fileOffset, uint size) => Contents is IReadableSegment segment + ? segment.CreateReader(fileOffset, size) + : throw new InvalidOperationException("Segment is not readable."); + + /// + public void Write(IBinaryStreamWriter writer) + { + Contents.Write(writer); + ApplyPatches(writer); + } + + private void ApplyPatches(IBinaryStreamWriter writer) + { + ulong offset = writer.Offset; + + for (int i = 0; i < Patches.Count; i++) + Patches[i].Apply(new PatchContext(Contents, _imageBase, writer)); + + writer.Offset = offset; + } + + /// + /// Adds a patch to the list of patches to apply. + /// + /// The patch to apply. + /// The current instance. + public PatchedSegment Patch(IPatch patch) + { + Patches.Add(patch); + return this; + } + + /// + /// Adds a bytes patch to the list of patches to apply. + /// + /// The offset to start patching at, relative to the start of the segment. + /// The new data to write. + /// The current instance. + public PatchedSegment Patch(uint relativeOffset, byte[] newData) + { + Patches.Add(new BytesPatch(relativeOffset, newData)); + return this; + } + } +} diff --git a/src/AsmResolver/SegmentReference.cs b/src/AsmResolver/SegmentReference.cs index ecda7b3e2..5d939db07 100644 --- a/src/AsmResolver/SegmentReference.cs +++ b/src/AsmResolver/SegmentReference.cs @@ -56,5 +56,8 @@ public BinaryStreamReader CreateReader() } ISegment? ISegmentReference.GetSegment() => Segment; + + /// + public override string ToString() => Segment?.ToString() ?? "null"; } } diff --git a/src/AsmResolver/Symbol.cs b/src/AsmResolver/Symbol.cs index ce4759ab0..990cf4ba5 100644 --- a/src/AsmResolver/Symbol.cs +++ b/src/AsmResolver/Symbol.cs @@ -24,5 +24,8 @@ public ISegmentReference Address /// public ISegmentReference GetReference() => Address; + + /// + public override string? ToString() => Address.ToString(); } } diff --git a/src/AsmResolver/VirtualAddress.cs b/src/AsmResolver/VirtualAddress.cs index 7975ba7a6..4fd7546c7 100644 --- a/src/AsmResolver/VirtualAddress.cs +++ b/src/AsmResolver/VirtualAddress.cs @@ -33,5 +33,8 @@ public uint Rva BinaryStreamReader ISegmentReference.CreateReader() => throw new InvalidOperationException(); ISegment? ISegmentReference.GetSegment() => throw new InvalidOperationException(); + + /// + public override string ToString() => $"0x{Rva:X8}"; } } diff --git a/test/AsmResolver.Benchmarks/AsmResolver.Benchmarks.csproj b/test/AsmResolver.Benchmarks/AsmResolver.Benchmarks.csproj index 9d08fac34..c83d52de8 100644 --- a/test/AsmResolver.Benchmarks/AsmResolver.Benchmarks.csproj +++ b/test/AsmResolver.Benchmarks/AsmResolver.Benchmarks.csproj @@ -7,7 +7,7 @@ - + diff --git a/test/AsmResolver.DotNet.Dynamic.Tests/AsmResolver.DotNet.Dynamic.Tests.csproj b/test/AsmResolver.DotNet.Dynamic.Tests/AsmResolver.DotNet.Dynamic.Tests.csproj index 28574f7f7..c62d0b8c7 100644 --- a/test/AsmResolver.DotNet.Dynamic.Tests/AsmResolver.DotNet.Dynamic.Tests.csproj +++ b/test/AsmResolver.DotNet.Dynamic.Tests/AsmResolver.DotNet.Dynamic.Tests.csproj @@ -8,7 +8,7 @@ - + diff --git a/test/AsmResolver.DotNet.Tests/AsmResolver.DotNet.Tests.csproj b/test/AsmResolver.DotNet.Tests/AsmResolver.DotNet.Tests.csproj index 393b05e33..a3983706c 100644 --- a/test/AsmResolver.DotNet.Tests/AsmResolver.DotNet.Tests.csproj +++ b/test/AsmResolver.DotNet.Tests/AsmResolver.DotNet.Tests.csproj @@ -9,7 +9,7 @@ - + all diff --git a/test/AsmResolver.DotNet.Tests/Builder/ManagedPEImageBuilderTest.cs b/test/AsmResolver.DotNet.Tests/Builder/ManagedPEImageBuilderTest.cs index 44daafcce..82c2630e5 100644 --- a/test/AsmResolver.DotNet.Tests/Builder/ManagedPEImageBuilderTest.cs +++ b/test/AsmResolver.DotNet.Tests/Builder/ManagedPEImageBuilderTest.cs @@ -2,6 +2,7 @@ using System.Linq; using AsmResolver.DotNet.Builder; using AsmResolver.PE; +using AsmResolver.PE.DotNet.Metadata; using Xunit; namespace AsmResolver.DotNet.Tests.Builder @@ -79,5 +80,72 @@ public void ConstructPEImageFromExistingModuleWithPreservation() var newModule = ModuleDefinition.FromImage(result); Assert.Equal(module.Name, newModule.Name); } + + [Fact] + public void PreserveUnknownStreams() + { + // Prepare a PE image with an extra unconventional stream. + var image = PEImage.FromBytes(Properties.Resources.HelloWorld); + byte[] data = { 1, 2, 3, 4 }; + image.DotNetDirectory!.Metadata!.Streams.Add(new CustomMetadataStream("#Custom", data)); + + // Load and rebuild. + var module = ModuleDefinition.FromImage(image); + var newImage = module.ToPEImage(new ManagedPEImageBuilder(MetadataBuilderFlags.PreserveUnknownStreams)); + + // Verify unconventional stream is still present. + var newStream = Assert.IsAssignableFrom( + newImage.DotNetDirectory!.Metadata!.GetStream("#Custom")); + Assert.Equal(data, Assert.IsAssignableFrom(newStream.Contents).ToArray()); + } + + [Fact] + public void PreserveStreamOrder() + { + // Prepare a PE image with an unconventional stream order. + var image = PEImage.FromBytes(Properties.Resources.HelloWorld); + var streams = image.DotNetDirectory!.Metadata!.Streams; + for (int i = 0; i < streams.Count / 2; i++) + (streams[i], streams[streams.Count - i - 1]) = (streams[streams.Count - i - 1], streams[i]); + + // Load and rebuild. + var module = ModuleDefinition.FromImage(image); + var newImage = module.ToPEImage(new ManagedPEImageBuilder(MetadataBuilderFlags.PreserveStreamOrder)); + + // Verify order is still the same. + Assert.Equal( + streams.Select(x => x.Name), + newImage.DotNetDirectory!.Metadata!.Streams.Select(x => x.Name)); + } + + [Fact] + public void PreserveUnknownStreamsAndStreamOrder() + { + // Prepare a PE image with an unconventional stream order and custom stream. + var image = PEImage.FromBytes(Properties.Resources.HelloWorld); + var streams = image.DotNetDirectory!.Metadata!.Streams; + + for (int i = 0; i < streams.Count / 2; i++) + (streams[i], streams[streams.Count - i - 1]) = (streams[streams.Count - i - 1], streams[i]); + + byte[] data = { 1, 2, 3, 4 }; + image.DotNetDirectory!.Metadata!.Streams.Insert(streams.Count / 2, + new CustomMetadataStream("#Custom", data)); + + // Load and rebuild. + var module = ModuleDefinition.FromImage(image); + var newImage = module.ToPEImage(new ManagedPEImageBuilder( + MetadataBuilderFlags.PreserveStreamOrder | MetadataBuilderFlags.PreserveUnknownStreams)); + + // Verify order is still the same. + Assert.Equal( + streams.Select(x => x.Name), + newImage.DotNetDirectory!.Metadata!.Streams.Select(x => x.Name)); + + // Verify unconventional stream is still present. + var newStream = Assert.IsAssignableFrom( + newImage.DotNetDirectory!.Metadata!.GetStream("#Custom")); + Assert.Equal(data, Assert.IsAssignableFrom(newStream.Contents).ToArray()); + } } } diff --git a/test/AsmResolver.DotNet.Tests/Code/Native/NativeMethodBodyTest.cs b/test/AsmResolver.DotNet.Tests/Code/Native/NativeMethodBodyTest.cs index 356163a4d..e59af51e6 100644 --- a/test/AsmResolver.DotNet.Tests/Code/Native/NativeMethodBodyTest.cs +++ b/test/AsmResolver.DotNet.Tests/Code/Native/NativeMethodBodyTest.cs @@ -59,15 +59,14 @@ private static NativeMethodBody CreateDummyBody(bool isVoid, bool is32Bit) return method.NativeMethodBody = new NativeMethodBody(method); } - private static CodeSegment GetNewCodeSegment(IPEImage image) + private static IReadableSegment GetNewCodeSegment(IPEImage image) { var methodTable = image.DotNetDirectory!.Metadata! .GetStream() .GetTable(TableIndex.Method); var row = methodTable.First(r => (r.ImplAttributes & MethodImplAttributes.Native) != 0); Assert.True(row.Body.IsBounded); - var segment = Assert.IsAssignableFrom(row.Body.GetSegment()); - return segment; + return Assert.IsAssignableFrom(row.Body.GetSegment()); } [Fact] @@ -89,7 +88,7 @@ public void NativeMethodBodyShouldResultInRawCodeSegment() var segment = GetNewCodeSegment(image); // Verify code segment was created. - Assert.Equal(segment.Code, body.Code); + Assert.Equal(segment.ToArray(), body.Code); } [Fact] diff --git a/test/AsmResolver.DotNet.Tests/CustomAttributeTest.cs b/test/AsmResolver.DotNet.Tests/CustomAttributeTest.cs index b05032774..ae0e5db79 100644 --- a/test/AsmResolver.DotNet.Tests/CustomAttributeTest.cs +++ b/test/AsmResolver.DotNet.Tests/CustomAttributeTest.cs @@ -70,13 +70,22 @@ public void ReadParent() Assert.Equal(parentToken, attribute.Parent.MetadataToken); } - private static CustomAttribute GetCustomAttributeTestCase(string methodName, bool rebuild = false, bool access = false) + private static CustomAttribute GetCustomAttributeTestCase( + string methodName, + bool rebuild = false, + bool access = false, + bool generic = false) { var module = ModuleDefinition.FromFile(typeof(CustomAttributesTestClass).Assembly.Location); var type = module.TopLevelTypes.First(t => t.Name == nameof(CustomAttributesTestClass)); var method = type.Methods.First(m => m.Name == methodName); + + string attributeName = nameof(TestCaseAttribute); + if (generic) + attributeName += "`1"; + var attribute = method.CustomAttributes - .First(c => c.Constructor!.DeclaringType!.Name == nameof(TestCaseAttribute)); + .First(c => c.Constructor!.DeclaringType!.Name.Value.StartsWith(attributeName)); if (access) { @@ -286,8 +295,8 @@ public void GenericTypeArgument(bool rebuild, bool access) var nestedClass = (TypeDefinition) module.LookupMember(typeof(TestGenericType<>).MetadataToken); var expected = new GenericInstanceTypeSignature(nestedClass, false, module.CorLibTypeFactory.Object); - Assert.IsAssignableFrom(argument.Element); - Assert.Equal(expected, (TypeSignature) argument.Element, _comparer); + var element = Assert.IsAssignableFrom(argument.Element); + Assert.Equal(expected, element, _comparer); } [Theory] @@ -307,8 +316,8 @@ public void ArrayGenericTypeArgument(bool rebuild, bool access) new GenericInstanceTypeSignature(nestedClass, false, module.CorLibTypeFactory.Object) ); - Assert.IsAssignableFrom(argument.Element); - Assert.Equal(expected, (TypeSignature) argument.Element, _comparer); + var element = Assert.IsAssignableFrom(argument.Element); + Assert.Equal(expected, element, _comparer); } [Theory] @@ -322,8 +331,8 @@ public void IntPassedOnAsObject(bool rebuild, bool access) var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.Int32PassedAsObject),rebuild, access); var argument = attribute.Signature!.FixedArguments[0]; - Assert.IsAssignableFrom(argument.Element); - Assert.Equal(123, ((BoxedArgument) argument.Element).Value); + var element = Assert.IsAssignableFrom(argument.Element); + Assert.Equal(123, element.Value); } [Theory] @@ -338,8 +347,8 @@ public void TypePassedOnAsObject(bool rebuild, bool access) var argument = attribute.Signature!.FixedArguments[0]; var module = attribute.Constructor!.Module!; - Assert.IsAssignableFrom(argument.Element); - Assert.Equal(module.CorLibTypeFactory.Int32, (ITypeDescriptor) ((BoxedArgument) argument.Element).Value, _comparer); + var element = Assert.IsAssignableFrom(argument.Element); + Assert.Equal(module.CorLibTypeFactory.Int32, (ITypeDescriptor) element.Value, _comparer); } [Theory] @@ -391,8 +400,7 @@ public void FixedInt32ArrayNullAsObject(bool rebuild, bool access) var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedInt32ArrayAsObjectNullArgument),rebuild, access); var argument = attribute.Signature!.FixedArguments[0]; - Assert.IsAssignableFrom(argument.Element); - var boxedArgument = (BoxedArgument) argument.Element; + var boxedArgument = Assert.IsAssignableFrom(argument.Element); Assert.Null(boxedArgument.Value); } @@ -405,8 +413,7 @@ public void FixedInt32EmptyArrayAsObject(bool rebuild, bool access) var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedInt32ArrayAsObjectEmptyArgument),rebuild, access); var argument = attribute.Signature!.FixedArguments[0]; - Assert.IsAssignableFrom(argument.Element); - var boxedArgument = (BoxedArgument) argument.Element; + var boxedArgument =Assert.IsAssignableFrom(argument.Element); Assert.Equal(Array.Empty(), boxedArgument.Value); } @@ -419,8 +426,7 @@ public void FixedInt32ArrayAsObject(bool rebuild, bool access) var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedInt32ArrayAsObjectArgument),rebuild, access); var argument = attribute.Signature!.FixedArguments[0]; - Assert.IsAssignableFrom(argument.Element); - var boxedArgument = (BoxedArgument) argument.Element; + var boxedArgument = Assert.IsAssignableFrom(argument.Element); Assert.Equal(new[] { 1, 2, 3, 4 @@ -464,5 +470,201 @@ public void CreateNewWithFixedArgumentsViaProperty() var argument = Assert.Single(attribute.Signature.FixedArguments); Assert.Equal("My Message", argument.Element); } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void FixedGenericInt32Argument(bool rebuild, bool access) + { + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedGenericInt32Argument), + rebuild, access, true); + var argument = attribute.Signature!.FixedArguments[0]; + + int value = Assert.IsAssignableFrom(argument.Element); + Assert.Equal(1, value); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void FixedGenericStringArgument(bool rebuild, bool access) + { + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedGenericStringArgument), + rebuild, access, true); + var argument = attribute.Signature!.FixedArguments[0]; + + string value = Assert.IsAssignableFrom(argument.Element); + Assert.Equal("Fixed string generic argument", value); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void FixedGenericInt32ArrayArgument(bool rebuild, bool access) + { + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedGenericInt32ArrayArgument), + rebuild, access, true); + var argument = attribute.Signature!.FixedArguments[0]; + + Assert.Equal(new int[] {1, 2, 3, 4}, argument.Elements.Cast()); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void FixedGenericInt32ArrayAsObjectArgument(bool rebuild, bool access) + { + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedGenericInt32ArrayAsObjectArgument), + rebuild, access, true); + var argument = attribute.Signature!.FixedArguments[0]; + + var boxedArgument = Assert.IsAssignableFrom(argument.Element); + Assert.Equal(new[] + { + 1, 2, 3, 4 + }, boxedArgument.Value); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void FixedGenericTypeArgument(bool rebuild, bool access) + { + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedGenericTypeArgument), + rebuild, access, true); + var argument = attribute.Signature!.FixedArguments[0]; + + var expected = attribute.Constructor!.Module!.CorLibTypeFactory.Int32; + var element = Assert.IsAssignableFrom(argument.Element); + Assert.Equal(expected, element, _comparer); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void FixedGenericTypeNullArgument(bool rebuild, bool access) + { + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.FixedGenericTypeNullArgument), + rebuild, access, true); + var argument = attribute.Signature!.FixedArguments[0]; + + Assert.Null(argument.Element); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void NamedGenericInt32Argument(bool rebuild, bool access) + { + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.NamedGenericInt32Argument), + rebuild, access, true); + var argument = attribute.Signature!.NamedArguments[0]; + + Assert.Equal("Value", argument.MemberName); + int value = Assert.IsAssignableFrom(argument.Argument.Element); + Assert.Equal(1, value); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void NamedGenericStringArgument(bool rebuild, bool access) + { + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.NamedGenericStringArgument), + rebuild, access, true); + var argument = attribute.Signature!.NamedArguments[0]; + + Assert.Equal("Value", argument.MemberName); + string value = Assert.IsAssignableFrom(argument.Argument.Element); + Assert.Equal("Named string generic argument", value); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void NamedGenericInt32ArrayArgument(bool rebuild, bool access) + { + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.NamedGenericInt32ArrayArgument), + rebuild, access, true); + var argument = attribute.Signature!.NamedArguments[0]; + + Assert.Equal("Value", argument.MemberName); + Assert.Equal(new int[] {1,2,3,4}, argument.Argument.Elements.Cast()); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void NamedGenericInt32ArrayAsObjectArgument(bool rebuild, bool access) + { + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.NamedGenericInt32ArrayAsObjectArgument), + rebuild, access, true); + var argument = attribute.Signature!.NamedArguments[0]; + + Assert.Equal("Value", argument.MemberName); + var boxedArgument = Assert.IsAssignableFrom(argument.Argument.Element); + Assert.Equal(new[] + { + 1, 2, 3, 4 + }, boxedArgument.Value); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void NamedGenericTypeArgument(bool rebuild, bool access) + { + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.NamedGenericTypeArgument), + rebuild, access, true); + var argument = attribute.Signature!.NamedArguments[0]; + + var expected = attribute.Constructor!.Module!.CorLibTypeFactory.Int32; + var element = Assert.IsAssignableFrom(argument.Argument.Element); + Assert.Equal(expected, element, _comparer); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void NamedGenericTypeNullArgument(bool rebuild, bool access) + { + var attribute = GetCustomAttributeTestCase(nameof(CustomAttributesTestClass.NamedGenericTypeNullArgument), + rebuild, access, true); + var argument = attribute.Signature!.NamedArguments[0]; + + Assert.Null(argument.Argument.Element); + } + + [Fact] + public void TestSignatureCompatibility() + { + var module = new ModuleDefinition("Dummy"); + var factory = module.CorLibTypeFactory; + var ctor = factory.CorLibScope + .CreateTypeReference("System", "CLSCompliantAttribute") + .CreateMemberReference(".ctor", MethodSignature.CreateInstance(factory.Void, factory.Boolean)) + .ImportWith(module.DefaultImporter); + + var attribute = new CustomAttribute(ctor); + + // Empty signature is not compatible with a ctor that takes a boolean. + Assert.False(attribute.Signature!.IsCompatibleWith(attribute.Constructor!)); + + // If we add it, it should be compatible again. + attribute.Signature.FixedArguments.Add(new CustomAttributeArgument(factory.Boolean, true)); + Assert.True(attribute.Signature!.IsCompatibleWith(attribute.Constructor!)); + } } } diff --git a/test/AsmResolver.DotNet.Tests/ModuleDefinitionTest.cs b/test/AsmResolver.DotNet.Tests/ModuleDefinitionTest.cs index 7e2e1ba10..76e9b61fd 100644 --- a/test/AsmResolver.DotNet.Tests/ModuleDefinitionTest.cs +++ b/test/AsmResolver.DotNet.Tests/ModuleDefinitionTest.cs @@ -149,11 +149,30 @@ public void LookupTypeReference() { var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld); var member = module.LookupMember(new MetadataToken(TableIndex.TypeRef, 12)); - Assert.IsAssignableFrom(member); - var typeRef = (TypeReference)member; - Assert.Equal("System", typeRef.Namespace); - Assert.Equal("Object", typeRef.Name); + var reference = Assert.IsAssignableFrom(member); + Assert.Equal("System", reference.Namespace); + Assert.Equal("Object", reference.Name); + } + + [Fact] + public void LookupTypeReferenceStronglyTyped() + { + var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld); + var reference = module.LookupMember(new MetadataToken(TableIndex.TypeRef, 12)); + + Assert.Equal("System", reference.Namespace); + Assert.Equal("Object", reference.Name); + } + + [Fact] + public void TryLookupTypeReferenceStronglyTyped() + { + var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld); + + Assert.True(module.TryLookupMember(new MetadataToken(TableIndex.TypeRef, 12), out TypeReference reference)); + Assert.Equal("System", reference.Namespace); + Assert.Equal("Object", reference.Name); } [Fact] @@ -161,11 +180,20 @@ public void LookupTypeDefinition() { var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld); var member = module.LookupMember(new MetadataToken(TableIndex.TypeDef, 2)); - Assert.IsAssignableFrom(member); - var typeDef = (TypeDefinition)member; - Assert.Equal("HelloWorld", typeDef.Namespace); - Assert.Equal("Program", typeDef.Name); + var definition = Assert.IsAssignableFrom(member); + Assert.Equal("HelloWorld", definition.Namespace); + Assert.Equal("Program", definition.Name); + } + + [Fact] + public void LookupTypeDefinitionStronglyTyped() + { + var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld); + var definition = module.LookupMember(new MetadataToken(TableIndex.TypeDef, 2)); + + Assert.Equal("HelloWorld", definition.Namespace); + Assert.Equal("Program", definition.Name); } [Fact] @@ -173,11 +201,20 @@ public void LookupAssemblyReference() { var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld); var member = module.LookupMember(new MetadataToken(TableIndex.AssemblyRef, 1)); - Assert.IsAssignableFrom(member); - var assemblyRef = (AssemblyReference)member; - Assert.Equal("mscorlib", assemblyRef.Name); - Assert.Same(module.AssemblyReferences[0], assemblyRef); + var reference = Assert.IsAssignableFrom(member); + Assert.Equal("mscorlib", reference.Name); + Assert.Same(module.AssemblyReferences[0], reference); + } + + [Fact] + public void LookupAssemblyReferenceStronglyTyped() + { + var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld); + var reference = module.LookupMember(new MetadataToken(TableIndex.AssemblyRef, 1)); + + Assert.Equal("mscorlib", reference.Name); + Assert.Same(module.AssemblyReferences[0], reference); } [Fact] @@ -185,11 +222,20 @@ public void LookupModuleReference() { var module = ModuleDefinition.FromFile(Path.Combine("Resources", "Manifest.exe")); var member = module.LookupMember(new MetadataToken(TableIndex.ModuleRef, 1)); - Assert.IsAssignableFrom(member); - var moduleRef = (ModuleReference)member; - Assert.Equal("MyModel.netmodule", moduleRef.Name); - Assert.Same(module.ModuleReferences[0], moduleRef); + var reference =Assert.IsAssignableFrom(member); + Assert.Equal("MyModel.netmodule", reference.Name); + Assert.Same(module.ModuleReferences[0], reference); + } + + [Fact] + public void LookupModuleReferenceStronglyTyped() + { + var module = ModuleDefinition.FromFile(Path.Combine("Resources", "Manifest.exe")); + var reference = module.LookupMember(new MetadataToken(TableIndex.ModuleRef, 1)); + + Assert.Equal("MyModel.netmodule", reference.Name); + Assert.Same(module.ModuleReferences[0], reference); } [Fact] @@ -412,5 +458,63 @@ public void RewriteSystemPrivateXml() using var stream = new MemoryStream(); module.Write(stream); } + + [Fact] + public void GetModuleTypeNetFramework() + { + var module = ModuleDefinition.FromBytes(Properties.Resources.ModuleCctorNetFramework); + + // Module type should exist. + var type = module.GetModuleType(); + Assert.NotNull(type); + Assert.Equal("CustomModuleType", type.Name); + + // Creating module type should give us the existing type. + Assert.Same(type, module.GetOrCreateModuleType()); + } + + [Fact] + public void GetModuleTypeNet6() + { + var module = ModuleDefinition.FromBytes(Properties.Resources.ModuleCctorNet6); + + // Module type should exist. + var type = module.GetModuleType(); + Assert.NotNull(type); + Assert.Equal("", type.Name); + + // Creating module type should give us the existing type. + Assert.Same(type, module.GetOrCreateModuleType()); + } + + [Fact] + public void GetModuleTypeAbsentNet6() + { + var module = ModuleDefinition.FromBytes(Properties.Resources.ModuleCctorAbsentNet6); + + // Module type should not exist. + var type = module.GetModuleType(); + Assert.Null(type); + + // Creating should add it to the module. + type = module.GetOrCreateModuleType(); + Assert.NotNull(type); + Assert.Same(type, module.GetModuleType()); + } + + [Fact] + public void GetModuleTypeLookalikeNet6() + { + var module = ModuleDefinition.FromBytes(Properties.Resources.ModuleCctorLookalikeNet6); + + // Module type should not exist. + var type = module.GetModuleType(); + Assert.Null(type); + + // Creating should add it to the module. + type = module.GetOrCreateModuleType(); + Assert.NotNull(type); + Assert.Same(type, module.GetModuleType()); + } } } diff --git a/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs b/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs index fffb016d9..427f830ea 100644 --- a/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs +++ b/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs @@ -331,5 +331,33 @@ public static byte[] MyLibrary_X64 { return ((byte[])(obj)); } } + + public static byte[] ModuleCctorAbsentNet6 { + get { + object obj = ResourceManager.GetObject("ModuleCctorAbsentNet6", resourceCulture); + return ((byte[])(obj)); + } + } + + public static byte[] ModuleCctorLookalikeNet6 { + get { + object obj = ResourceManager.GetObject("ModuleCctorLookalikeNet6", resourceCulture); + return ((byte[])(obj)); + } + } + + public static byte[] ModuleCctorNet6 { + get { + object obj = ResourceManager.GetObject("ModuleCctorNet6", resourceCulture); + return ((byte[])(obj)); + } + } + + public static byte[] ModuleCctorNetFramework { + get { + object obj = ResourceManager.GetObject("ModuleCctorNetFramework", resourceCulture); + return ((byte[])(obj)); + } + } } } diff --git a/test/AsmResolver.DotNet.Tests/Properties/Resources.resx b/test/AsmResolver.DotNet.Tests/Properties/Resources.resx index 37af630d8..f13ac4af0 100644 --- a/test/AsmResolver.DotNet.Tests/Properties/Resources.resx +++ b/test/AsmResolver.DotNet.Tests/Properties/Resources.resx @@ -141,4 +141,16 @@ ..\Resources\MyLibrary.x64.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\Resources\ModuleCctorAbsentNet6.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\ModuleCctorLookalikeNet6.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\ModuleCctorNet6.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\ModuleCctorNetFramework.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + diff --git a/test/AsmResolver.DotNet.Tests/Resources/ModuleCctorAbsentNet6.dll b/test/AsmResolver.DotNet.Tests/Resources/ModuleCctorAbsentNet6.dll new file mode 100644 index 000000000..7dbed5038 Binary files /dev/null and b/test/AsmResolver.DotNet.Tests/Resources/ModuleCctorAbsentNet6.dll differ diff --git a/test/AsmResolver.DotNet.Tests/Resources/ModuleCctorLookalikeNet6.dll b/test/AsmResolver.DotNet.Tests/Resources/ModuleCctorLookalikeNet6.dll new file mode 100644 index 000000000..559b6cf32 Binary files /dev/null and b/test/AsmResolver.DotNet.Tests/Resources/ModuleCctorLookalikeNet6.dll differ diff --git a/test/AsmResolver.DotNet.Tests/Resources/ModuleCctorNet6.dll b/test/AsmResolver.DotNet.Tests/Resources/ModuleCctorNet6.dll new file mode 100644 index 000000000..1caa361a2 Binary files /dev/null and b/test/AsmResolver.DotNet.Tests/Resources/ModuleCctorNet6.dll differ diff --git a/test/AsmResolver.DotNet.Tests/Resources/ModuleCctorNetFramework.exe b/test/AsmResolver.DotNet.Tests/Resources/ModuleCctorNetFramework.exe new file mode 100644 index 000000000..9f67d260d Binary files /dev/null and b/test/AsmResolver.DotNet.Tests/Resources/ModuleCctorNetFramework.exe differ diff --git a/test/AsmResolver.PE.File.Tests/AsmResolver.PE.File.Tests.csproj b/test/AsmResolver.PE.File.Tests/AsmResolver.PE.File.Tests.csproj index 44c298a73..80baee93b 100644 --- a/test/AsmResolver.PE.File.Tests/AsmResolver.PE.File.Tests.csproj +++ b/test/AsmResolver.PE.File.Tests/AsmResolver.PE.File.Tests.csproj @@ -9,7 +9,7 @@ - + all diff --git a/test/AsmResolver.PE.Tests/AsmResolver.PE.Tests.csproj b/test/AsmResolver.PE.Tests/AsmResolver.PE.Tests.csproj index 2c4967fcf..2a656347a 100644 --- a/test/AsmResolver.PE.Tests/AsmResolver.PE.Tests.csproj +++ b/test/AsmResolver.PE.Tests/AsmResolver.PE.Tests.csproj @@ -16,7 +16,7 @@ - + diff --git a/test/AsmResolver.PE.Tests/Code/AddressFixupTest.cs b/test/AsmResolver.PE.Tests/Code/AddressFixupTest.cs new file mode 100644 index 000000000..605df4fd5 --- /dev/null +++ b/test/AsmResolver.PE.Tests/Code/AddressFixupTest.cs @@ -0,0 +1,98 @@ +using System; +using System.Linq; +using AsmResolver.Patching; +using AsmResolver.PE.Code; +using Xunit; + +namespace AsmResolver.PE.Tests.Code +{ + public class AddressFixupTest + { + private readonly DataSegment _input = new(Enumerable + .Range(0, 1000) + .Select(x => (byte) (x & 0xFF)) + .ToArray()); + + private readonly ISymbol _dummySymbol = new Symbol(new VirtualAddress(0x0000_2000)); + + [Fact] + public void PatchAbsolute32BitAddress() + { + var patched = new PatchedSegment(_input); + patched.Patches.Add(new AddressFixupPatch( + new AddressFixup(10, AddressFixupType.Absolute32BitAddress, _dummySymbol))); + + patched.UpdateOffsets(new RelocationParameters(0x0040_0000, 0, 0, true)); + + byte[] expected = _input.ToArray(); + Buffer.BlockCopy(BitConverter.GetBytes(0x0040_2000), 0, expected, 10, sizeof(int)); + + byte[] actual = patched.WriteIntoArray(); + Assert.Equal(expected, actual); + } + + [Fact] + public void PatchAbsolute32BitAddressFluent() + { + var patched = _input + .AsPatchedSegment() + .Patch(10, AddressFixupType.Absolute32BitAddress, _dummySymbol); + + patched.UpdateOffsets(new RelocationParameters(0x0040_0000, 0, 0, true)); + + byte[] expected = _input.ToArray(); + Buffer.BlockCopy(BitConverter.GetBytes(0x0040_2000), 0, expected, 10, sizeof(int)); + + byte[] actual = patched.WriteIntoArray(); + Assert.Equal(expected, actual); + } + + [Fact] + public void PatchAbsolute64BitAddress() + { + var patched = new PatchedSegment(_input); + patched.Patches.Add(new AddressFixupPatch( + new AddressFixup(10, AddressFixupType.Absolute64BitAddress, _dummySymbol))); + + patched.UpdateOffsets(new RelocationParameters(0x0000_0001_0000_0000, 0, 0, true)); + + byte[] expected = _input.ToArray(); + Buffer.BlockCopy(BitConverter.GetBytes(0x0000_0001_0000_2000), 0, expected, 10, sizeof(ulong)); + + byte[] actual = patched.WriteIntoArray(); + Assert.Equal(expected, actual); + } + + [Fact] + public void PatchAbsolute64BitAddressFluent() + { + var patched = _input + .AsPatchedSegment() + .Patch(10, AddressFixupType.Absolute64BitAddress, _dummySymbol); + + patched.UpdateOffsets(new RelocationParameters(0x0000_0001_0000_0000, 0, 0, true)); + + byte[] expected = _input.ToArray(); + Buffer.BlockCopy(BitConverter.GetBytes(0x0000_0001_0000_2000), 0, expected, 10, sizeof(ulong)); + + byte[] actual = patched.WriteIntoArray(); + Assert.Equal(expected, actual); + } + + [Fact] + public void PatchRelative32BitAddress() + { + var patched = new PatchedSegment(_input); + patched.Patches.Add(new AddressFixupPatch( + new AddressFixup(10, AddressFixupType.Relative32BitAddress, _dummySymbol))); + + patched.UpdateOffsets(new RelocationParameters(0x0040_0000, 0, 0, true)); + + byte[] expected = _input.ToArray(); + Buffer.BlockCopy(BitConverter.GetBytes(0x2000 - 10 - 4), 0, expected, 10, sizeof(int)); + + byte[] actual = patched.WriteIntoArray(); + Assert.Equal(expected, actual); + } + } +} diff --git a/test/AsmResolver.PE.Tests/DotNet/Builder/MixedModeAssemblyTest.cs b/test/AsmResolver.PE.Tests/DotNet/Builder/MixedModeAssemblyTest.cs index b5e6988d6..678015d14 100644 --- a/test/AsmResolver.PE.Tests/DotNet/Builder/MixedModeAssemblyTest.cs +++ b/test/AsmResolver.PE.Tests/DotNet/Builder/MixedModeAssemblyTest.cs @@ -26,7 +26,7 @@ public MixedModeAssemblyTest(TemporaryDirectoryFixture fixture) _fixture = fixture; } - private static void ReplaceBodyWithNativeCode(IPEImage image, CodeSegment body, bool is32bit) + private static void ReplaceBodyWithNativeCode(IPEImage image, ISegment body, bool is32bit) { // Adjust image flags appropriately. image.DotNetDirectory!.Flags &= ~DotNetDirectoryFlags.ILOnly; @@ -89,7 +89,7 @@ public void NativeBodyWithNoCalls() // Read image var image = PEImage.FromBytes(Properties.Resources.TheAnswer_NetFx); - ReplaceBodyWithNativeCode(image, new CodeSegment(new byte[] + ReplaceBodyWithNativeCode(image, new DataSegment(new byte[] { 0xb8, 0x39, 0x05, 0x00, 0x00, // mov rax, 1337 0xc3 // ret @@ -120,27 +120,24 @@ public void NativeBodyWithCall() var function = new ImportedSymbol(0x4fc, "puts"); module.Symbols.Add(function); - var body = new CodeSegment(new byte[] - { - /* 00: */ 0x48, 0x83, 0xEC, 0x28, // sub rsp, 0x28 - /* 04: */ 0x48, 0x8D, 0x0D, 0x10, 0x00, 0x00, 0x00, // lea rcx, qword [rel str] - /* 0B: */ 0xFF, 0x15, 0x00, 0x00, 0x00, 0x00, // call qword [rel puts] - /* 11: */ 0xB8, 0x37, 0x13, 0x00, 0x00, // mov eax, 0x1337 - /* 16: */ 0x48, 0x83, 0xC4, 0x28, // add rsp, 0x28 - /* 1A: */ 0xC3, // ret - - // str: - 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x66, // "Hello f" - 0x72, 0x6f, 0x6d, 0x20, 0x74, 0x68, 0x65, // "rom the" - 0x20, 0x75, 0x6e, 0x6d, 0x61, 0x6e, 0x61, // " unmana" - 0x67, 0x65, 0x64, 0x20, 0x77, 0x6f, 0x72, // "ged wor" - 0x6c, 0x64, 0x21, 0x00 // "ld!" - }); - - // Fixup puts call. - body.AddressFixups.Add(new AddressFixup( - 0xD, AddressFixupType.Relative32BitAddress, function - )); + var body = new DataSegment(new byte[] + { + /* 00: */ 0x48, 0x83, 0xEC, 0x28, // sub rsp, 0x28 + /* 04: */ 0x48, 0x8D, 0x0D, 0x10, 0x00, 0x00, 0x00, // lea rcx, qword [rel str] + /* 0B: */ 0xFF, 0x15, 0x00, 0x00, 0x00, 0x00, // call qword [rel puts] + /* 11: */ 0xB8, 0x37, 0x13, 0x00, 0x00, // mov eax, 0x1337 + /* 16: */ 0x48, 0x83, 0xC4, 0x28, // add rsp, 0x28 + /* 1A: */ 0xC3, // ret + + // str: + 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x66, // "Hello f" + 0x72, 0x6f, 0x6d, 0x20, 0x74, 0x68, 0x65, // "rom the" + 0x20, 0x75, 0x6e, 0x6d, 0x61, 0x6e, 0x61, // " unmana" + 0x67, 0x65, 0x64, 0x20, 0x77, 0x6f, 0x72, // "ged wor" + 0x6c, 0x64, 0x21, 0x00 // "ld!" + }) + .AsPatchedSegment() + .Patch(0xD, AddressFixupType.Relative32BitAddress, function); // Replace body. ReplaceBodyWithNativeCode(image, body, false); @@ -170,24 +167,22 @@ public void NativeBodyWithCallX86() var function = new ImportedSymbol(0x4fc, "puts"); module.Symbols.Add(function); - var body = new CodeSegment(new byte[] - { - /* 00: */ 0x55, // push ebp - /* 01: */ 0x89, 0xE5, // mov ebp,esp - /* 03: */ 0x6A, 0x6F, // push byte +0x6f ; H - /* 05: */ 0x68, 0x48, 0x65, 0x6C, 0x6C, // push dword 0x6c6c6548 ; ello - /* 0A: */ 0x54, // push esp - /* 0B: */ 0xFF, 0x15, 0x00, 0x00, 0x00, 0x00, // call [dword puts] - /* 11: */ 0x83, 0xC4, 0x0C, // add esp,byte +0xc - /* 14: */ 0xB8, 0x37, 0x13, 0x00, 0x00, // mov eax,0x1337 - /* 19: */ 0x5D, // pop ebp - /* 1A: */ 0xC3, // ret - }); - - // Fix up puts call. - body.AddressFixups.Add(new AddressFixup( - 0xD, AddressFixupType.Absolute32BitAddress, function - )); + var body = new DataSegment(new byte[] + { + /* 00: */ 0x55, // push ebp + /* 01: */ 0x89, 0xE5, // mov ebp,esp + /* 03: */ 0x6A, 0x6F, // push byte +0x6f ; H + /* 05: */ 0x68, 0x48, 0x65, 0x6C, 0x6C, // push dword 0x6c6c6548 ; ello + /* 0A: */ 0x54, // push esp + /* 0B: */ 0xFF, 0x15, 0x00, 0x00, 0x00, 0x00, // call [dword puts] + /* 11: */ 0x83, 0xC4, 0x0C, // add esp,byte +0xc + /* 14: */ 0xB8, 0x37, 0x13, 0x00, 0x00, // mov eax,0x1337 + /* 19: */ 0x5D, // pop ebp + /* 1A: */ 0xC3, // ret + }) + .AsPatchedSegment() + .Patch(0xD, AddressFixupType.Absolute32BitAddress, function); + image.Relocations.Clear(); image.Relocations.Add(new BaseRelocation(RelocationType.HighLow, body.ToReference(0xD))); diff --git a/test/AsmResolver.PE.Win32Resources.Tests/AsmResolver.PE.Win32Resources.Tests.csproj b/test/AsmResolver.PE.Win32Resources.Tests/AsmResolver.PE.Win32Resources.Tests.csproj index 66003d51f..3cdf923ca 100644 --- a/test/AsmResolver.PE.Win32Resources.Tests/AsmResolver.PE.Win32Resources.Tests.csproj +++ b/test/AsmResolver.PE.Win32Resources.Tests/AsmResolver.PE.Win32Resources.Tests.csproj @@ -8,7 +8,7 @@ - + all diff --git a/test/AsmResolver.Symbols.Pdb.Tests/AsmResolver.Symbols.Pdb.Tests.csproj b/test/AsmResolver.Symbols.Pdb.Tests/AsmResolver.Symbols.Pdb.Tests.csproj index 495bbf831..942be7824 100644 --- a/test/AsmResolver.Symbols.Pdb.Tests/AsmResolver.Symbols.Pdb.Tests.csproj +++ b/test/AsmResolver.Symbols.Pdb.Tests/AsmResolver.Symbols.Pdb.Tests.csproj @@ -8,7 +8,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/AsmResolver.Tests/AsmResolver.Tests.csproj b/test/AsmResolver.Tests/AsmResolver.Tests.csproj index 2bac4b411..b3874df72 100644 --- a/test/AsmResolver.Tests/AsmResolver.Tests.csproj +++ b/test/AsmResolver.Tests/AsmResolver.Tests.csproj @@ -8,7 +8,7 @@ - + all diff --git a/test/AsmResolver.Tests/Patching/PatchedSegmentTest.cs b/test/AsmResolver.Tests/Patching/PatchedSegmentTest.cs new file mode 100644 index 000000000..9f68452ec --- /dev/null +++ b/test/AsmResolver.Tests/Patching/PatchedSegmentTest.cs @@ -0,0 +1,56 @@ +using System; +using System.Linq; +using AsmResolver.Patching; +using Xunit; + +namespace AsmResolver.Tests.Patching +{ + public class PatchedSegmentTest + { + private readonly DataSegment _input = new(Enumerable + .Range(0, 1000) + .Select(x => (byte) (x & 0xFF)) + .ToArray()); + + [Fact] + public void SimpleBytesPatch() + { + var patched = new PatchedSegment(_input); + + uint relativeOffset = 10; + byte[] newData = {0xFF, 0xFE, 0xFD, 0xFC}; + patched.Patches.Add(new BytesPatch(relativeOffset, newData)); + + byte[] expected = _input.ToArray(); + Buffer.BlockCopy(newData, 0, expected, (int) relativeOffset, newData.Length); + + byte[] result = patched.WriteIntoArray(); + Assert.Equal(expected, result); + } + + [Fact] + public void DoublePatchedSegmentShouldReturnSameInstance() + { + var x = _input.AsPatchedSegment(); + var y = x.AsPatchedSegment(); + Assert.Same(x, y); + } + + [Fact] + public void SimpleBytesPatchFluent() + { + uint relativeOffset = 10; + byte[] newData = {0xFF, 0xFE, 0xFD, 0xFC}; + + var patched = _input + .AsPatchedSegment() + .Patch(10, newData); + + byte[] expected = _input.ToArray(); + Buffer.BlockCopy(newData, 0, expected, (int) relativeOffset, newData.Length); + + byte[] result = patched.WriteIntoArray(); + Assert.Equal(expected, result); + } + } +} diff --git a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/AsmResolver.DotNet.TestCases.CustomAttributes.csproj b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/AsmResolver.DotNet.TestCases.CustomAttributes.csproj index 27560206d..a2f2e3d95 100644 --- a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/AsmResolver.DotNet.TestCases.CustomAttributes.csproj +++ b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/AsmResolver.DotNet.TestCases.CustomAttributes.csproj @@ -2,6 +2,7 @@ netstandard2.0 + 11 diff --git a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/CustomAttributesTestClass.cs b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/CustomAttributesTestClass.cs index 852f2d44f..f4444fa45 100644 --- a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/CustomAttributesTestClass.cs +++ b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/CustomAttributesTestClass.cs @@ -134,5 +134,64 @@ public void FixedNullTypeArgument() { } + [TestCase(1)] + public void FixedGenericInt32Argument() + { + } + + [TestCase("Fixed string generic argument")] + public void FixedGenericStringArgument() + { + } + + [TestCase(new int[] {1,2,3,4})] + public void FixedGenericInt32ArrayArgument() + { + } + + [TestCase(new int[] {1,2,3,4})] + public void FixedGenericInt32ArrayAsObjectArgument() + { + } + + [TestCase(typeof(int))] + public void FixedGenericTypeArgument() + { + } + + [TestCase(null)] + public void FixedGenericTypeNullArgument() + { + } + + [TestCase(Value = 1)] + public void NamedGenericInt32Argument() + { + } + + [TestCase(Value = "Named string generic argument")] + public void NamedGenericStringArgument() + { + } + + [TestCase(Value = new int[] {1,2,3,4})] + public void NamedGenericInt32ArrayArgument() + { + } + + [TestCase(Value = new int[] {1,2,3,4})] + public void NamedGenericInt32ArrayAsObjectArgument() + { + } + + [TestCase(Value = typeof(int))] + public void NamedGenericTypeArgument() + { + } + + [TestCase(Value = null)] + public void NamedGenericTypeNullArgument() + { + } } } diff --git a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/TestCaseAttribute.Generic.cs b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/TestCaseAttribute.Generic.cs new file mode 100644 index 000000000..096766fa0 --- /dev/null +++ b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/TestCaseAttribute.Generic.cs @@ -0,0 +1,24 @@ +using System; + +namespace AsmResolver.DotNet.TestCases.CustomAttributes +{ + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] + public class TestCaseAttribute : Attribute + { + public TestCaseAttribute() + { + Value = default; + } + + public TestCaseAttribute(T value) + { + Value = value; + } + + public T Value + { + get; + set; + } + } +}