From 89eefd225da8fb1ed156ba37092c020f39c86687 Mon Sep 17 00:00:00 2001 From: Washi Date: Sat, 18 Mar 2023 13:01:35 +0100 Subject: [PATCH 1/4] Add BundlerParameters::FromTemplate and FromExistingFile, obsolete constructors. --- .../Bundles/BundleManifest.cs | 25 ++- .../Bundles/BundlerParameters.cs | 201 ++++++++++++++++++ .../Bundles/BundleManifestTest.cs | 51 ++++- 3 files changed, 260 insertions(+), 17 deletions(-) diff --git a/src/AsmResolver.DotNet/Bundles/BundleManifest.cs b/src/AsmResolver.DotNet/Bundles/BundleManifest.cs index c3a6025cf..58e82defb 100644 --- a/src/AsmResolver.DotNet/Bundles/BundleManifest.cs +++ b/src/AsmResolver.DotNet/Bundles/BundleManifest.cs @@ -9,6 +9,7 @@ using AsmResolver.IO; using AsmResolver.PE.File; using AsmResolver.PE.File.Headers; +using AsmResolver.PE.Win32Resources; using AsmResolver.PE.Win32Resources.Builder; namespace AsmResolver.DotNet.Bundles @@ -28,9 +29,6 @@ public class BundleManifest 0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae }; - private static readonly byte[] AppBinaryPathPlaceholder = - Encoding.UTF8.GetBytes("c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2"); - private IList? _files; /// @@ -194,7 +192,7 @@ public static BundleManifest FromDataSource(IDataSource source, ulong offset) /// The read manifest. public static BundleManifest FromReader(BinaryStreamReader reader) => new SerializedBundleManifest(reader); - private static long FindInFile(IDataSource source, byte[] data) + internal static long FindInFile(IDataSource source, byte[] needle) { // Note: For performance reasons, we read data from the data source in blocks, such that we avoid // virtual-dispatch calls and do the searching directly on a byte array instead. @@ -206,12 +204,12 @@ private static long FindInFile(IDataSource source, byte[] data) { int read = source.ReadBytes(start, buffer, 0, buffer.Length); - for (int i = sizeof(ulong); i < read - data.Length; i++) + for (int i = sizeof(ulong); i < read - needle.Length; i++) { bool fullMatch = true; - for (int j = 0; fullMatch && j < data.Length; j++) + for (int j = 0; fullMatch && j < needle.Length; j++) { - if (buffer[i + j] != data[j]) + if (buffer[i + j] != needle[j]) fullMatch = false; } @@ -317,6 +315,7 @@ public void WriteUsingTemplate(Stream outputStream, in BundlerParameters paramet /// The parameters to use for bundling all files into a single executable. public void WriteUsingTemplate(IBinaryStreamWriter writer, BundlerParameters parameters) { + // Verify entry point assembly exists within the bundle and is a correct length. var appBinaryEntry = Files.FirstOrDefault(f => f.RelativePath == parameters.ApplicationBinaryPath); if (appBinaryEntry is null) throw new ArgumentException($"Application {parameters.ApplicationBinaryPath} does not exist within the bundle."); @@ -325,6 +324,7 @@ public void WriteUsingTemplate(IBinaryStreamWriter writer, BundlerParameters par if (appBinaryPathBytes.Length > 1024) throw new ArgumentException("Application binary path cannot exceed 1024 bytes."); + // Patch headers when necessary. if (!parameters.IsArm64Linux) EnsureAppHostPEHeadersAreUpToDate(ref parameters); @@ -333,21 +333,26 @@ public void WriteUsingTemplate(IBinaryStreamWriter writer, BundlerParameters par if (signatureAddress == -1) throw new ArgumentException("AppHost template does not contain the bundle signature."); - long appBinaryPathAddress = FindInFile(appHostTemplateSource, AppBinaryPathPlaceholder); + long appBinaryPathAddress = FindInFile(appHostTemplateSource, parameters.PathPlaceholder); if (appBinaryPathAddress == -1) throw new ArgumentException("AppHost template does not contain the application binary path placeholder."); + // Write template. writer.WriteBytes(parameters.ApplicationHostTemplate); + + // Append manifest. writer.Offset = writer.Length; ulong headerAddress = WriteManifest(writer, parameters.IsArm64Linux); + // Update header address in apphost template. writer.Offset = (ulong) signatureAddress - sizeof(ulong); writer.WriteUInt64(headerAddress); + // Replace binary path placeholder with actual path. writer.Offset = (ulong) appBinaryPathAddress; writer.WriteBytes(appBinaryPathBytes); - if (AppBinaryPathPlaceholder.Length > appBinaryPathBytes.Length) - writer.WriteZeroes(AppBinaryPathPlaceholder.Length - appBinaryPathBytes.Length); + if (parameters.PathPlaceholder.Length > appBinaryPathBytes.Length) + writer.WriteZeroes(parameters.PathPlaceholder.Length - appBinaryPathBytes.Length); } private static void EnsureAppHostPEHeadersAreUpToDate(ref BundlerParameters parameters) diff --git a/src/AsmResolver.DotNet/Bundles/BundlerParameters.cs b/src/AsmResolver.DotNet/Bundles/BundlerParameters.cs index 2e83c3c16..a834d7fc2 100644 --- a/src/AsmResolver.DotNet/Bundles/BundlerParameters.cs +++ b/src/AsmResolver.DotNet/Bundles/BundlerParameters.cs @@ -1,6 +1,9 @@ +using System; using System.IO; +using System.Text; using AsmResolver.IO; using AsmResolver.PE; +using AsmResolver.PE.File; using AsmResolver.PE.File.Headers; using AsmResolver.PE.Win32Resources; @@ -11,6 +14,8 @@ namespace AsmResolver.DotNet.Bundles /// public struct BundlerParameters { + private const string DefaultPathPlaceholder = "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2"; + /// /// Initializes new bundler parameters. /// @@ -22,6 +27,7 @@ public struct BundlerParameters /// /// The name of the file in the bundle that contains the entry point of the application. /// + [Obsolete("Use BundlerParameters::FromTemplate instead.")] public BundlerParameters(string appHostTemplatePath, string appBinaryPath) : this(File.ReadAllBytes(appHostTemplatePath), appBinaryPath) { @@ -34,6 +40,7 @@ public BundlerParameters(string appHostTemplatePath, string appBinaryPath) /// /// The name of the file in the bundle that contains the entry point of the application. /// + [Obsolete("Use BundlerParameters::FromTemplate instead.")] public BundlerParameters(byte[] appHostTemplate, string appBinaryPath) { ApplicationHostTemplate = appHostTemplate; @@ -41,6 +48,7 @@ public BundlerParameters(byte[] appHostTemplate, string appBinaryPath) IsArm64Linux = false; Resources = null; SubSystem = SubSystem.WindowsCui; + PathPlaceholder = Encoding.UTF8.GetBytes(DefaultPathPlaceholder); } /// @@ -58,6 +66,7 @@ public BundlerParameters(byte[] appHostTemplate, string appBinaryPath) /// The path to copy the PE headers and Win32 resources from. This is typically the original native executable /// file that hosts the CLR, or the original AppHost file the bundle was extracted from. /// + [Obsolete("Use BundlerParameters::FromTemplate instead.")] public BundlerParameters(string appHostTemplatePath, string appBinaryPath, string? imagePathToCopyHeadersFrom) : this( File.ReadAllBytes(appHostTemplatePath), @@ -80,6 +89,7 @@ public BundlerParameters(string appHostTemplatePath, string appBinaryPath, strin /// The binary to copy the PE headers and Win32 resources from. This is typically the original native executable /// file that hosts the CLR, or the original AppHost file the bundle was extracted from. /// + [Obsolete("Use BundlerParameters::FromTemplate instead.")] public BundlerParameters(byte[] appHostTemplate, string appBinaryPath, byte[]? imageToCopyHeadersFrom) : this( appHostTemplate, @@ -102,6 +112,7 @@ imageToCopyHeadersFrom is not null /// The binary to copy the PE headers and Win32 resources from. This is typically the original native executable /// file that hosts the CLR, or the original AppHost file the bundle was extracted from. /// + [Obsolete("Use BundlerParameters::FromTemplate instead.")] public BundlerParameters(byte[] appHostTemplate, string appBinaryPath, IDataSource? imageToCopyHeadersFrom) : this( appHostTemplate, @@ -124,6 +135,7 @@ imageToCopyHeadersFrom is not null /// The PE image to copy the headers and Win32 resources from. This is typically the original native executable /// file that hosts the CLR, or the original AppHost file the bundle was extracted from. /// + [Obsolete("Use BundlerParameters::FromTemplate instead.")] public BundlerParameters(byte[] appHostTemplate, string appBinaryPath, IPEImage? imageToCopyHeadersFrom) : this( appHostTemplate, @@ -143,6 +155,7 @@ public BundlerParameters(byte[] appHostTemplate, string appBinaryPath, IPEImage? /// /// The subsystem to use in the final Windows PE binary. /// The resources to copy into the final Windows PE binary. + [Obsolete("Use BundlerParameters::FromTemplate instead.")] public BundlerParameters( byte[] appHostTemplate, string appBinaryPath, @@ -154,6 +167,7 @@ public BundlerParameters( IsArm64Linux = false; SubSystem = subSystem; Resources = resources; + PathPlaceholder = Encoding.UTF8.GetBytes(DefaultPathPlaceholder); } /// @@ -183,6 +197,16 @@ public string ApplicationBinaryPath set; } + /// + /// Gets or sets the path placeholder in that will be replaced with the + /// contents of . + /// + public byte[] PathPlaceholder + { + get; + set; + } + /// /// Gets a value indicating whether the bundled executable targets the Linux operating system on ARM64. /// @@ -217,5 +241,182 @@ public SubSystem SubSystem get; set; } + + /// + /// Initializes new bundler parameters from an apphost template. + /// + /// + /// The path to the application host file template to use. By default this is stored in + /// <DOTNET-INSTALLATION-PATH>/sdk/<version>/AppHostTemplate or + /// <DOTNET-INSTALLATION-PATH>/packs/Microsoft.NETCore.App.Host.<runtime-identifier>/<version>/runtimes/<runtime-identifier>/native. + /// + /// + /// The name of the file in the bundle that contains the entry point of the application. + /// + public static BundlerParameters FromTemplate(string appHostTemplatePath, string appBinaryPath) + { + return FromTemplate(appHostTemplatePath, appBinaryPath, null); + } + + /// + /// Initializes new bundler parameters from an apphost template. + /// + /// + /// The path to the application host file template to use. By default this is stored in + /// <DOTNET-INSTALLATION-PATH>/sdk/<version>/AppHostTemplate or + /// <DOTNET-INSTALLATION-PATH>/packs/Microsoft.NETCore.App.Host.<runtime-identifier>/<version>/runtimes/<runtime-identifier>/native. + /// + /// + /// The name of the file in the bundle that contains the entry point of the application. + /// + /// + /// The path to the binary to copy the PE headers and Win32 resources from. This is typically the original + /// native executable file that hosts the CLR, or the original AppHost file the bundle was extracted from. + /// + public static BundlerParameters FromTemplate(string appHostTemplatePath, string appBinaryPath, string? imagePathToCopyHeadersFrom) + { + return FromTemplate( + File.ReadAllBytes(appHostTemplatePath), + appBinaryPath, + imagePathToCopyHeadersFrom is not null + ? File.ReadAllBytes(imagePathToCopyHeadersFrom) + : null); + } + + /// + /// Initializes new bundler parameters from an apphost template. + /// + /// The application host template file to use. + /// + /// The name of the file in the bundle that contains the entry point of the application. + /// + public static BundlerParameters FromTemplate(byte[] appHostTemplate, string appBinaryPath) + { + return FromTemplate(appHostTemplate, appBinaryPath, default(PEImage?)); + } + + /// + /// Initializes new bundler parameters from an apphost template. + /// + /// The application host template file to use. + /// + /// The name of the file in the bundle that contains the entry point of the application. + /// + /// + /// The binary to copy the PE headers and Win32 resources from. This is typically the original native executable + /// file that hosts the CLR, or the original AppHost file the bundle was extracted from. + /// + public static BundlerParameters FromTemplate(byte[] appHostTemplate, string appBinaryPath, byte[]? imageToCopyHeadersFrom) + { + var image = imageToCopyHeadersFrom is not null + ? PEImage.FromBytes(imageToCopyHeadersFrom) + : null; + + return FromTemplate(appHostTemplate, appBinaryPath, image); + } + + /// + /// Initializes new bundler parameters from an apphost template. + /// + /// The application host template file to use. + /// + /// The name of the file in the bundle that contains the entry point of the application. + /// + /// + /// The image to copy the PE headers and Win32 resources from. This is typically the original native executable + /// file that hosts the CLR, or the original AppHost file the bundle was extracted from. + /// + public static BundlerParameters FromTemplate(byte[] appHostTemplate, string appBinaryPath, IPEImage? imageToCopyHeadersFrom) + { + return new BundlerParameters + { + ApplicationHostTemplate = appHostTemplate, + ApplicationBinaryPath = appBinaryPath, + PathPlaceholder = Encoding.UTF8.GetBytes(DefaultPathPlaceholder), + IsArm64Linux = false, + Resources = imageToCopyHeadersFrom?.Resources, + SubSystem = imageToCopyHeadersFrom?.SubSystem ?? SubSystem.WindowsCui, + }; + } + + /// + /// Extracts bundler parameters from an existing packaged bundled PE file. + /// + /// The path to the original PE file. + /// + /// The name of the file in the bundle that contains the entry point of the application. + /// + /// + /// This method uses heuristics to determine the right offsets within the existing apphost bundle file, and is + /// not guaranteed to always produce the right bundler parameters. + /// + public static BundlerParameters FromExistingFile(string originalFile, string appBinaryPath) + { + return FromExistingFile(File.ReadAllBytes(originalFile), appBinaryPath, appBinaryPath); + } + + /// + /// Extracts bundler parameters from an existing packaged bundled PE file. + /// + /// The raw contents of the original PE file. + /// + /// The name of the file in the bundle that contains the entry point of the application. + /// + /// + /// This method uses heuristics to determine the right offsets within the existing apphost bundle file, and is + /// not guaranteed to always produce the right bundler parameters. + /// + public static BundlerParameters FromExistingFile(byte[] originalFile, string appBinaryPath) + { + return FromExistingFile(originalFile, appBinaryPath, appBinaryPath); + } + + /// + /// Extracts bundler parameters from an existing packaged bundled PE file. + /// + /// The raw contents of the original PE file. + /// + /// The original name of the file in the bundle that contains the entry point of the application. + /// + /// + /// The new name of the file in the bundle that contains the entry point of the application. + /// + /// + /// This method uses heuristics to determine the right offsets within the existing apphost bundle file, and is + /// not guaranteed to always produce the right bundler parameters. + /// + public static BundlerParameters FromExistingFile(byte[] originalFile, string originalAppBinaryPath, string newAppBinaryPath) + { + PEFile file; + try + { + file = PEFile.FromBytes(originalFile); + } + catch (Exception ex) + { + throw new NotSupportedException("Only valid PE files are currently supported for repackaging.", ex); + } + + // Strip original bundle and reserialize PE file to use as template. + file.EofData = null; + using var stream = new MemoryStream(); + file.Write(stream); + + // Construct a template path to search for in the PE. + byte[] pathPlaceholder = new byte[32]; + Encoding.UTF8.GetBytes( + originalAppBinaryPath, 0, originalAppBinaryPath.Length, + pathPlaceholder, 0); + + return new BundlerParameters + { + ApplicationHostTemplate = stream.ToArray(), + ApplicationBinaryPath = newAppBinaryPath, + PathPlaceholder = pathPlaceholder, + IsArm64Linux = false, + Resources = PEImage.FromFile(file).Resources, + SubSystem = file.OptionalHeader.SubSystem, + }; + } } } diff --git a/test/AsmResolver.DotNet.Tests/Bundles/BundleManifestTest.cs b/test/AsmResolver.DotNet.Tests/Bundles/BundleManifestTest.cs index ffc339182..dbbc76ab4 100644 --- a/test/AsmResolver.DotNet.Tests/Bundles/BundleManifestTest.cs +++ b/test/AsmResolver.DotNet.Tests/Bundles/BundleManifestTest.cs @@ -7,6 +7,7 @@ using AsmResolver.DotNet.Bundles; using AsmResolver.IO; using AsmResolver.PE; +using AsmResolver.PE.DotNet.Cil; using AsmResolver.PE.File; using AsmResolver.PE.File.Headers; using AsmResolver.PE.Win32Resources.Version; @@ -120,10 +121,9 @@ public void WriteWithSubSystem(SubSystem subSystem) string appHostTemplatePath = FindAppHostTemplate("6.0"); using var stream = new MemoryStream(); - manifest.WriteUsingTemplate(stream, new BundlerParameters(appHostTemplatePath, "HelloWorld.dll") - { - SubSystem = subSystem - }); + var parameters = BundlerParameters.FromTemplate(appHostTemplatePath, "HelloWorld.dll"); + parameters.SubSystem = subSystem; + manifest.WriteUsingTemplate(stream, parameters); var newFile = PEFile.FromBytes(stream.ToArray()); Assert.Equal(subSystem, newFile.OptionalHeader.SubSystem); @@ -143,7 +143,7 @@ public void WriteWithWin32Resources() // Bundle with PE image as template for PE headers and resources. using var stream = new MemoryStream(); - manifest.WriteUsingTemplate(stream, new BundlerParameters( + manifest.WriteUsingTemplate(stream, BundlerParameters.FromTemplate( File.ReadAllBytes(appHostTemplatePath), "HelloWorld.dll", oldImage)); @@ -186,7 +186,7 @@ public void NewManifestShouldGenerateBundleIdIfUnset() Assert.Null(manifest.BundleID); using var stream = new MemoryStream(); - manifest.WriteUsingTemplate(stream, new BundlerParameters( + manifest.WriteUsingTemplate(stream, BundlerParameters.FromTemplate( FindAppHostTemplate("6.0"), "HelloWorld.dll")); @@ -205,6 +205,43 @@ public void SameManifestContentsShouldResultInSameBundleID() Assert.Equal(manifest.BundleID, newManifest.GenerateDeterministicBundleID()); } + [Fact] + public void PatchAndRepackageExistingBundleV6() + { + // Read manifest and locate main entry point file. + var manifest = BundleManifest.FromBytes(Properties.Resources.HelloWorld_SingleFile_V6); + var mainFile = manifest.Files.First(f => f.RelativePath.Contains("HelloWorld.dll")); + + // Patch entry point file. + var module = ModuleDefinition.FromBytes(mainFile.GetData()); + module.ManagedEntryPointMethod!.CilMethodBody! + .Instructions.First(i => i.OpCode.Code == CilCode.Ldstr) + .Operand = "Hello, Mars!"; + + using var moduleStream = new MemoryStream(); + module.Write(moduleStream); + + mainFile.Contents = new DataSegment(moduleStream.ToArray()); + mainFile.IsCompressed = false; + + // Repackage bundle using existing bundle as template. + using var bundleStream = new MemoryStream(); + manifest.WriteUsingTemplate(bundleStream, BundlerParameters.FromExistingFile( + Properties.Resources.HelloWorld_SingleFile_V6, + mainFile.RelativePath)); + + // Verify application runs as expected. + string output = _fixture + .GetRunner() + .RunAndCaptureOutput( + "HelloWorld.exe", + bundleStream.ToArray(), + null, + 5000); + + Assert.Equal($"Hello, Mars!{Environment.NewLine}", output); + } + private void AssertWriteManifestWindowsPreservesOutput( BundleManifest manifest, string sdkVersion, @@ -216,7 +253,7 @@ private void AssertWriteManifestWindowsPreservesOutput( string appHostTemplatePath = FindAppHostTemplate(sdkVersion); using var stream = new MemoryStream(); - manifest.WriteUsingTemplate(stream, new BundlerParameters(appHostTemplatePath, fileName)); + manifest.WriteUsingTemplate(stream, BundlerParameters.FromTemplate(appHostTemplatePath, fileName)); var newManifest = BundleManifest.FromBytes(stream.ToArray()); AssertBundlesAreEqual(manifest, newManifest); From 31c9200235fb4137b7733dfed078b5fbcc6fb9d8 Mon Sep 17 00:00:00 2001 From: Washi Date: Sat, 18 Mar 2023 13:16:04 +0100 Subject: [PATCH 2/4] Update documentation on new bundle repacking feature. --- docs/dotnet/bundles.rst | 45 ++++++++++++++++--- .../Bundles/BundlerParameters.cs | 8 ++-- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/docs/dotnet/bundles.rst b/docs/dotnet/bundles.rst index 670b9eb3d..ea2a770c0 100644 --- a/docs/dotnet/bundles.rst +++ b/docs/dotnet/bundles.rst @@ -3,7 +3,7 @@ AppHost / SingleFileHost Bundles Since the release of .NET Core 3.1, it is possible to deploy .NET assemblies as a single binary. These files are executables that do not contain a traditional .NET metadata header, and run natively on the underlying operating system via a platform-specific application host bootstrapper. -AsmResolver supports extracting the embedded files from these types of binaries. Additionally, given an application host template provided by the .NET SDK, AsmResolver also supports constructing new bundles as well. All relevant code is found in the following namespace: +AsmResolver supports extracting the embedded files from these types of binaries. Additionally, given the original file or an application host template provided by the .NET SDK, AsmResolver also supports constructing new bundles as well. All relevant code is found in the following namespace: .. code-block:: csharp @@ -96,14 +96,14 @@ Constructing new bundled executable files requires a template file that AsmResol - ``/sdk//AppHostTemplate`` - ``/packs/Microsoft.NETCore.App.Host.//runtimes//native`` -Using this template file, it is then possible to write a new bundled executable file using ``WriteUsingTemplate``: +Using this template file, it is then possible to write a new bundled executable file using ``WriteUsingTemplate`` and the ``BundlerParameters::FromTemplate`` method: .. code-block:: csharp BundleManifest manifest = ... manifest.WriteUsingTemplate( @"C:\Path\To\Output\File.exe", - new BundlerParameters( + BundlerParameters.FromTemplate( appHostTemplatePath: @"C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Host.win-x64\6.0.0\runtimes\win-x64\native\apphost.exe", appBinaryPath: @"HelloWorld.dll")); @@ -117,12 +117,47 @@ For bundle executable files targeting Windows, it may be required to copy over s BundleManifest manifest = ... manifest.WriteUsingTemplate( @"C:\Path\To\Output\File.exe", - new BundlerParameters( + BundlerParameters.FromTemplate( appHostTemplatePath: @"C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Host.win-x64\6.0.0\runtimes\win-x64\native\apphost.exe", appBinaryPath: @"HelloWorld.dll", imagePathToCopyHeadersFrom: @"C:\Path\To\Original\HelloWorld.exe")); -``BundleManifest`` also defines other ```WriteUsingTemplate`` overloads taking ``byte[]``, ``IDataSource`` or ``IPEImage`` instances instead of paths. + +If you do not have access to a template file (e.g., if the SDK is not installed) but have another existing PE file that was packaged in a similar fashion, it is then possible to use this file as a template instead by extracting the bundler parameters using the ``BudnlerParameters::FromExistingFile`` method. This is in particularly useful when trying to patch existing AppHost bundles. Below is a full example for patching a bundled Hello World application to let it print ``Hello, Mars!`` instead: + +.. code-block:: csharp + + string inputPath = @"C:\Path\To\Bundled\HelloWorld.exe"; + string outputPath = Path.ChangeExtension(inputPath, ".patched.exe"); + + // Read manifest and locate main embedded file. + var manifest = BundleManifest.FromFile(inputPath); + var mainFile = manifest.Files.First(f => f.RelativePath == "HelloWorld.dll"); + + // Read the file as a module, and patch the "Hello, World!" string with "Hello, Mars!". + var module = ModuleDefinition.FromBytes(mainFile.GetData()); + module.ManagedEntryPointMethod!.CilMethodBody! + .Instructions.First(i => i.OpCode.Code == CilCode.Ldstr) + .Operand = "Hello, Mars!"; + + // Replace the contents of the embedded HelloWorld.dll with the new version: + using var moduleStream = new MemoryStream(); + module.Write(moduleStream); + mainFile.Contents = new DataSegment(moduleStream.ToArray()); + mainFile.IsCompressed = false; + + // Repackage bundle using existing bundle as template. + manifest.WriteUsingTemplate(outputPath, BundlerParameters.FromExistingFile( + inputPath, + mainFile.RelativePath)); + + +.. warning:: + + The ``BundlerParameters.FromExistingFile`` method applies heuristics on the input file to determine the parameters for patching the input file. As heuristics are not perfect, this is not guaranteed to always work. + + +``BundleManifest`` and ``BundlerParameters`` also define overloads of the ``WriteUsingTemplate`` and ``FromTemplate`` / ``FromExistingFile`` respectively, taking ``byte[]``, ``IDataSource`` or ``IPEImage`` instances instead of file paths. Managing Files diff --git a/src/AsmResolver.DotNet/Bundles/BundlerParameters.cs b/src/AsmResolver.DotNet/Bundles/BundlerParameters.cs index a834d7fc2..34557219a 100644 --- a/src/AsmResolver.DotNet/Bundles/BundlerParameters.cs +++ b/src/AsmResolver.DotNet/Bundles/BundlerParameters.cs @@ -350,9 +350,9 @@ public static BundlerParameters FromTemplate(byte[] appHostTemplate, string appB /// This method uses heuristics to determine the right offsets within the existing apphost bundle file, and is /// not guaranteed to always produce the right bundler parameters. /// - public static BundlerParameters FromExistingFile(string originalFile, string appBinaryPath) + public static BundlerParameters FromExistingBundle(string originalFile, string appBinaryPath) { - return FromExistingFile(File.ReadAllBytes(originalFile), appBinaryPath, appBinaryPath); + return FromExistingBundle(File.ReadAllBytes(originalFile), appBinaryPath, appBinaryPath); } /// @@ -368,7 +368,7 @@ public static BundlerParameters FromExistingFile(string originalFile, string app /// public static BundlerParameters FromExistingFile(byte[] originalFile, string appBinaryPath) { - return FromExistingFile(originalFile, appBinaryPath, appBinaryPath); + return FromExistingBundle(originalFile, appBinaryPath, appBinaryPath); } /// @@ -385,7 +385,7 @@ public static BundlerParameters FromExistingFile(byte[] originalFile, string app /// This method uses heuristics to determine the right offsets within the existing apphost bundle file, and is /// not guaranteed to always produce the right bundler parameters. /// - public static BundlerParameters FromExistingFile(byte[] originalFile, string originalAppBinaryPath, string newAppBinaryPath) + public static BundlerParameters FromExistingBundle(byte[] originalFile, string originalAppBinaryPath, string newAppBinaryPath) { PEFile file; try From f6db4f855cecd7a81d926d7e6c6fb449081dff3e Mon Sep 17 00:00:00 2001 From: Washi Date: Sat, 18 Mar 2023 17:31:06 +0100 Subject: [PATCH 3/4] Rename to FromExistingBundle, simplify docs. --- docs/dotnet/bundles.rst | 29 +++++++------------ .../Bundles/BundlerParameters.cs | 2 +- .../Bundles/BundleManifestTest.cs | 2 +- 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/docs/dotnet/bundles.rst b/docs/dotnet/bundles.rst index ea2a770c0..35dd13ee8 100644 --- a/docs/dotnet/bundles.rst +++ b/docs/dotnet/bundles.rst @@ -123,7 +123,7 @@ For bundle executable files targeting Windows, it may be required to copy over s imagePathToCopyHeadersFrom: @"C:\Path\To\Original\HelloWorld.exe")); -If you do not have access to a template file (e.g., if the SDK is not installed) but have another existing PE file that was packaged in a similar fashion, it is then possible to use this file as a template instead by extracting the bundler parameters using the ``BudnlerParameters::FromExistingFile`` method. This is in particularly useful when trying to patch existing AppHost bundles. Below is a full example for patching a bundled Hello World application to let it print ``Hello, Mars!`` instead: +If you do not have access to a template file (e.g., if the SDK is not installed) but have another existing PE file that was packaged in a similar fashion, it is then possible to use this file as a template instead by extracting the bundler parameters using the ``BudnlerParameters::FromExistingBundle`` method. This is in particularly useful when trying to patch existing AppHost bundles. Below is a full example for patching a bundled Hello World application to let it print ``Hello, Mars!`` instead: .. code-block:: csharp @@ -132,32 +132,23 @@ If you do not have access to a template file (e.g., if the SDK is not installed) // Read manifest and locate main embedded file. var manifest = BundleManifest.FromFile(inputPath); - var mainFile = manifest.Files.First(f => f.RelativePath == "HelloWorld.dll"); - - // Read the file as a module, and patch the "Hello, World!" string with "Hello, Mars!". - var module = ModuleDefinition.FromBytes(mainFile.GetData()); - module.ManagedEntryPointMethod!.CilMethodBody! - .Instructions.First(i => i.OpCode.Code == CilCode.Ldstr) - .Operand = "Hello, Mars!"; - - // Replace the contents of the embedded HelloWorld.dll with the new version: - using var moduleStream = new MemoryStream(); - module.Write(moduleStream); - mainFile.Contents = new DataSegment(moduleStream.ToArray()); - mainFile.IsCompressed = false; + /* ... Make changes to manifest and its files ... */ + // Repackage bundle using existing bundle as template. - manifest.WriteUsingTemplate(outputPath, BundlerParameters.FromExistingFile( - inputPath, - mainFile.RelativePath)); + manifest.WriteUsingTemplate( + outputPath, + BundlerParameters.FromExistingBundle( + originalFile: inputPath, + appBinaryPath: mainFile.RelativePath)); .. warning:: - The ``BundlerParameters.FromExistingFile`` method applies heuristics on the input file to determine the parameters for patching the input file. As heuristics are not perfect, this is not guaranteed to always work. + The ``BundlerParameters.FromExistingBundle`` method applies heuristics on the input file to determine the parameters for patching the input file. As heuristics are not perfect, this is not guaranteed to always work. -``BundleManifest`` and ``BundlerParameters`` also define overloads of the ``WriteUsingTemplate`` and ``FromTemplate`` / ``FromExistingFile`` respectively, taking ``byte[]``, ``IDataSource`` or ``IPEImage`` instances instead of file paths. +``BundleManifest`` and ``BundlerParameters`` also define overloads of the ``WriteUsingTemplate`` and ``FromTemplate`` / ``FromExistingBundle`` respectively, taking ``byte[]``, ``IDataSource`` or ``IPEImage`` instances instead of file paths. Managing Files diff --git a/src/AsmResolver.DotNet/Bundles/BundlerParameters.cs b/src/AsmResolver.DotNet/Bundles/BundlerParameters.cs index 34557219a..2dee2e335 100644 --- a/src/AsmResolver.DotNet/Bundles/BundlerParameters.cs +++ b/src/AsmResolver.DotNet/Bundles/BundlerParameters.cs @@ -366,7 +366,7 @@ public static BundlerParameters FromExistingBundle(string originalFile, string a /// This method uses heuristics to determine the right offsets within the existing apphost bundle file, and is /// not guaranteed to always produce the right bundler parameters. /// - public static BundlerParameters FromExistingFile(byte[] originalFile, string appBinaryPath) + public static BundlerParameters FromExistingBundle(byte[] originalFile, string appBinaryPath) { return FromExistingBundle(originalFile, appBinaryPath, appBinaryPath); } diff --git a/test/AsmResolver.DotNet.Tests/Bundles/BundleManifestTest.cs b/test/AsmResolver.DotNet.Tests/Bundles/BundleManifestTest.cs index dbbc76ab4..49af6fbb5 100644 --- a/test/AsmResolver.DotNet.Tests/Bundles/BundleManifestTest.cs +++ b/test/AsmResolver.DotNet.Tests/Bundles/BundleManifestTest.cs @@ -226,7 +226,7 @@ public void PatchAndRepackageExistingBundleV6() // Repackage bundle using existing bundle as template. using var bundleStream = new MemoryStream(); - manifest.WriteUsingTemplate(bundleStream, BundlerParameters.FromExistingFile( + manifest.WriteUsingTemplate(bundleStream, BundlerParameters.FromExistingBundle( Properties.Resources.HelloWorld_SingleFile_V6, mainFile.RelativePath)); From 2190f8977910da19fd397a896b425602e7616c28 Mon Sep 17 00:00:00 2001 From: Washi Date: Sun, 19 Mar 2023 13:57:08 +0100 Subject: [PATCH 4/4] Add V1 and V2 repackage tests. Ensure temp dirs are not present on disk when running tests. --- docs/dotnet/bundles.rst | 4 +- .../Bundles/BundleManifestTest.cs | 44 +++++++++++++++++-- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/docs/dotnet/bundles.rst b/docs/dotnet/bundles.rst index 35dd13ee8..81ef259e5 100644 --- a/docs/dotnet/bundles.rst +++ b/docs/dotnet/bundles.rst @@ -123,14 +123,14 @@ For bundle executable files targeting Windows, it may be required to copy over s imagePathToCopyHeadersFrom: @"C:\Path\To\Original\HelloWorld.exe")); -If you do not have access to a template file (e.g., if the SDK is not installed) but have another existing PE file that was packaged in a similar fashion, it is then possible to use this file as a template instead by extracting the bundler parameters using the ``BudnlerParameters::FromExistingBundle`` method. This is in particularly useful when trying to patch existing AppHost bundles. Below is a full example for patching a bundled Hello World application to let it print ``Hello, Mars!`` instead: +If you do not have access to a template file (e.g., if the SDK is not installed) but have another existing PE file that was packaged in a similar fashion, it is then possible to use this file as a template instead by extracting the bundler parameters using the ``BundlerParameters::FromExistingBundle`` method. This is in particularly useful when trying to patch existing AppHost bundles: .. code-block:: csharp string inputPath = @"C:\Path\To\Bundled\HelloWorld.exe"; string outputPath = Path.ChangeExtension(inputPath, ".patched.exe"); - // Read manifest and locate main embedded file. + // Read manifest. var manifest = BundleManifest.FromFile(inputPath); /* ... Make changes to manifest and its files ... */ diff --git a/test/AsmResolver.DotNet.Tests/Bundles/BundleManifestTest.cs b/test/AsmResolver.DotNet.Tests/Bundles/BundleManifestTest.cs index 49af6fbb5..1f84d6daa 100644 --- a/test/AsmResolver.DotNet.Tests/Bundles/BundleManifestTest.cs +++ b/test/AsmResolver.DotNet.Tests/Bundles/BundleManifestTest.cs @@ -205,11 +205,34 @@ public void SameManifestContentsShouldResultInSameBundleID() Assert.Equal(manifest.BundleID, newManifest.GenerateDeterministicBundleID()); } + [Fact] + public void PatchAndRepackageExistingBundleV1() + { + Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + AssertPatchAndRepackageChangesOutput(Properties.Resources.HelloWorld_SingleFile_V1); + } + + [Fact] + public void PatchAndRepackageExistingBundleV2() + { + Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + AssertPatchAndRepackageChangesOutput(Properties.Resources.HelloWorld_SingleFile_V2); + } + [Fact] public void PatchAndRepackageExistingBundleV6() + { + Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + AssertPatchAndRepackageChangesOutput(Properties.Resources.HelloWorld_SingleFile_V6); + } + + private void AssertPatchAndRepackageChangesOutput( + byte[] original, + [CallerFilePath] string className = "File", + [CallerMemberName] string methodName = "Method") { // Read manifest and locate main entry point file. - var manifest = BundleManifest.FromBytes(Properties.Resources.HelloWorld_SingleFile_V6); + var manifest = BundleManifest.FromBytes(original); var mainFile = manifest.Files.First(f => f.RelativePath.Contains("HelloWorld.dll")); // Patch entry point file. @@ -224,20 +247,25 @@ public void PatchAndRepackageExistingBundleV6() mainFile.Contents = new DataSegment(moduleStream.ToArray()); mainFile.IsCompressed = false; + manifest.BundleID = manifest.GenerateDeterministicBundleID(); + // Repackage bundle using existing bundle as template. using var bundleStream = new MemoryStream(); manifest.WriteUsingTemplate(bundleStream, BundlerParameters.FromExistingBundle( - Properties.Resources.HelloWorld_SingleFile_V6, + original, mainFile.RelativePath)); // Verify application runs as expected. + DeleteTempExtractionDirectory(manifest, "HelloWorld.dll"); string output = _fixture .GetRunner() .RunAndCaptureOutput( "HelloWorld.exe", bundleStream.ToArray(), null, - 5000); + 5000, + className, + methodName); Assert.Equal($"Hello, Mars!{Environment.NewLine}", output); } @@ -251,6 +279,7 @@ private void AssertWriteManifestWindowsPreservesOutput( [CallerMemberName] string methodName = "Method") { string appHostTemplatePath = FindAppHostTemplate(sdkVersion); + DeleteTempExtractionDirectory(manifest, fileName); using var stream = new MemoryStream(); manifest.WriteUsingTemplate(stream, BundlerParameters.FromTemplate(appHostTemplatePath, fileName)); @@ -270,6 +299,15 @@ private void AssertWriteManifestWindowsPreservesOutput( Assert.Equal(expectedOutput, output); } + private static void DeleteTempExtractionDirectory(BundleManifest manifest, string fileName) + { + if (manifest.MajorVersion != 1 || manifest.BundleID is null || !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return; + + string tempPath = Path.Combine(Path.GetTempPath(), ".net", Path.GetFileNameWithoutExtension(fileName), manifest.BundleID); + if (Directory.Exists(tempPath)) + Directory.Delete(tempPath, true); + } private static string FindAppHostTemplate(string sdkVersion) {