Files
SabreTools.Serialization/SabreTools.Wrappers/PortableExecutable.Extraction.cs
gmipf dc18951d0b Close the Wise script and the multipart file after extraction (#104)
* Look for the Wise overlay header before opening the file again

ExtractWiseOverlay (and ExtractWise for NE) opened a second stream on
the executable for multi-part Wise installers before checking for a
Wise overlay header at all. For any other executable that stream was
never closed, so the file stayed open until the stream was finalized.
On Windows such a file cannot be deleted, so an extraction folder that
contains it is left behind in TEMP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Close the Wise script and the multipart file after extraction

ExtractWiseOverlay (and ExtractWise for NE) never closed WiseScript.bin,
which it opens from the output directory, nor the stream with the
installer and its parts, so these files stayed open until their streams
were finalized.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep the original block that opens the multipart file

Close the multipart stream with a separate using declaration after the
block instead of rewriting it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:30:32 -04:00

762 lines
32 KiB
C#

using System;
using System.IO;
using Nanook.GrindCore;
using Nanook.GrindCore.BZip2;
using Nanook.GrindCore.ZLib;
using SabreTools.IO.Extensions;
using SabreTools.Matching;
using SabreTools.Numerics.Extensions;
namespace SabreTools.Wrappers
{
public partial class PortableExecutable : IExtractable
{
/// <inheritdoc/>
/// <remarks>
/// This extracts the following data:
/// - Archives and executables in the overlay
/// - Archives and executables in resource data
/// - CExe-compressed resource data
/// - SecuROM Matroschka package sections
/// - SFX archives
/// + 7z
/// + Advanced Installer
/// + InstallShield Executables
/// + MS-CAB
/// + PKZIP
/// + RAR
/// + Spoon Installer
/// - Wise installers
/// </remarks>
public bool Extract(string outputDirectory, bool includeDebug)
{
bool cai = ExtractAdvancedInstaller(outputDirectory, includeDebug);
bool cexe = ExtractCExe(outputDirectory, includeDebug);
bool issexe = ExtractInstallShieldExecutable(outputDirectory, includeDebug);
bool matroschka = ExtractMatroschka(outputDirectory, includeDebug);
bool resources = ExtractFromResources(outputDirectory, includeDebug);
bool spoon = ExtractSpoonInstaller(outputDirectory, includeDebug);
// Skip Wise section extraction if the overlay succeeded
bool wiseOverlay = ExtractWiseOverlay(outputDirectory, includeDebug);
bool wiseSection = wiseOverlay || ExtractWiseSection(outputDirectory, includeDebug);
// Overlay can be skipped in some situations
bool overlay = cai || issexe || spoon || wiseOverlay
|| ExtractFromOverlay(outputDirectory, includeDebug);
return cai || cexe || issexe || matroschka || overlay || resources
|| spoon || wiseOverlay || wiseSection;
}
/// <summary>
/// Extract a Caphyon Advanced Installer SFX overlay
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractAdvancedInstaller(string outputDirectory, bool includeDebug)
{
try
{
// Ensure the stream is starting at the beginning
_dataSource.SeekIfPossible(0, SeekOrigin.Begin);
// Try to deserialize the source data
var deserializer = new Serialization.Readers.AdvancedInstaller();
var sfx = deserializer.Deserialize(_dataSource);
if (sfx is null || sfx.Entries.Length == 0)
return false;
// Loop through the entries and extract
for (int i = 0; i < sfx.Entries.Length; i++)
{
var entry = sfx.Entries[i];
// Get the offset and size
long offset = entry.FileOffset;
int size = (int)entry.FileSize;
// Try to read the file data
byte[] data = ReadRangeFromSource(offset, size);
if (data.Length == 0)
continue;
// Ensure directory separators are consistent
string filename = entry.Name.Length == 0 ? $"FILE_{i}" : entry.Name;
filename = filename.TrimStart(['\\', '/']);
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName is not null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Write the output file
var fs = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
fs.Write(data);
fs.Flush();
}
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Extract a CExe-compressed executable
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractCExe(string outputDirectory, bool includeDebug)
{
try
{
// Get all resources of type 99 with index 2
var resources = FindResourceByNamedType("99, 2");
if (resources is null || resources.Count == 0)
return false;
// Get the first resource of type 99 with index 2
var resource = resources[0];
if (resource is null || resource.Length == 0)
return false;
// Create the output data buffer
byte[]? data = [];
// If we had the decompression DLL included, it's zlib
if (FindResourceByNamedType("99, 1").Count > 0)
data = DecompressCExeZlib(resource);
else
data = DecompressCExeLZ(resource);
// If we have no data
if (data is null)
return false;
// Create the temp filename
string tempFile = string.IsNullOrEmpty(Filename) ? "temp.sxe" : $"{Path.GetFileNameWithoutExtension(Filename)}.sxe";
tempFile = Path.Combine(outputDirectory, tempFile);
var directoryName = Path.GetDirectoryName(tempFile);
if (directoryName is not null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Write the file data to a temp file
var tempStream = File.Open(tempFile, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
tempStream.Write(data);
tempStream.Flush();
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Extract data from the overlay
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractFromOverlay(string outputDirectory, bool includeDebug)
{
try
{
// Cache the overlay data for easier reading
var overlayData = OverlayData;
if (overlayData.Length == 0)
return false;
// Set the output variables
int overlayOffset = 0;
string extension = string.Empty;
// Only process the overlay if it is recognized
for (; overlayOffset < 0x400 && overlayOffset < overlayData.Length - 0x10; overlayOffset++)
{
int temp = overlayOffset;
byte[] overlaySample = overlayData.ReadBytes(ref temp, 0x10);
if (overlaySample.StartsWith(Data.Models.SevenZip.Constants.SignatureBytes))
{
extension = "7z";
break;
}
else if (overlaySample.StartsWith([0x3B, 0x21, 0x40, 0x49, 0x6E, 0x73, 0x74, 0x61, 0x6C, 0x6C]))
{
// 7-zip SFX script -- ";!@Install" to ";!@InstallEnd@!"
overlayOffset = overlayData.FirstPosition([0x3B, 0x21, 0x40, 0x49, 0x6E, 0x73, 0x74, 0x61, 0x6C, 0x6C, 0x45, 0x6E, 0x64, 0x40, 0x21]);
if (overlayOffset == -1)
return false;
overlayOffset += 15;
extension = "7z";
break;
}
else if (overlaySample.StartsWith(Data.Models.BZip2.Constants.SignatureBytes))
{
extension = "bz2";
break;
}
else if (overlaySample.StartsWith(Data.Models.CFB.Constants.SignatureBytes))
{
// Assume embedded CFB files are MSI
extension = "msi";
break;
}
else if (overlaySample.StartsWith([0x1F, 0x8B]))
{
extension = "gz";
break;
}
else if (overlaySample.StartsWith(Data.Models.MicrosoftCabinet.Constants.SignatureBytes))
{
extension = "cab";
break;
}
else if (overlaySample.StartsWith(Data.Models.PKZIP.Constants.LocalFileHeaderSignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith(Data.Models.PKZIP.Constants.EndOfCentralDirectoryRecordSignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith(Data.Models.PKZIP.Constants.EndOfCentralDirectoryRecord64SignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith(Data.Models.PKZIP.Constants.DataDescriptorSignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith(Data.Models.RAR.Constants.OldSignatureBytes))
{
extension = "rar";
break;
}
else if (overlaySample.StartsWith(Data.Models.RAR.Constants.NewSignatureBytes))
{
extension = "rar";
break;
}
else if (overlaySample.StartsWith([0x55, 0x48, 0x41, 0x06]))
{
extension = "uha";
break;
}
else if (overlaySample.StartsWith([0x3C, 0x3F, 0x78, 0x6D, 0x6C]))
{
extension = "xml";
break;
}
else if (overlaySample.StartsWith([0x3C, 0x00, 0x3F, 0x00, 0x78, 0x00, 0x6D, 0x00, 0x6C, 0x00]))
{
extension = "xml";
break;
}
else if (overlaySample.StartsWith([0xFF, 0xFE, 0x3C, 0x00, 0x3F, 0x00, 0x78, 0x00, 0x6D, 0x00, 0x6C, 0x00]))
{
extension = "xml";
break;
}
else if (overlaySample.StartsWith(Data.Models.XZ.Constants.HeaderSignatureBytes))
{
extension = "xz";
break;
}
else if (overlaySample.StartsWith(Data.Models.MSDOS.Constants.SignatureBytes))
{
extension = "bin"; // exe/dll
break;
}
}
// If the extension is unset
if (extension.Length == 0)
return false;
// Create the temp filename
string tempFile = $"embedded_overlay.{extension}";
if (Filename is not null)
tempFile = $"{Path.GetFileName(Filename)}-{tempFile}";
tempFile = Path.Combine(outputDirectory, tempFile);
var directoryName = Path.GetDirectoryName(tempFile);
if (directoryName is not null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Write the resource data to a temp file
using var tempStream = File.Open(tempFile, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
// If the overlay is partially cached, read it from the source in blocks
if (OverlaySize > overlayData.Length)
{
long currentOffset = OverlayAddress + overlayOffset;
long bytesLeft = OverlaySize - overlayOffset;
while (bytesLeft > 0)
{
int bytesToRead = (int)Math.Min(0x4000, bytesLeft);
byte[] buffer = ReadRangeFromSource(currentOffset, bytesToRead);
if (buffer.Length == 0)
break;
tempStream.Write(buffer);
tempStream.Flush();
currentOffset += bytesToRead;
bytesLeft -= bytesToRead;
}
}
// Otherwise, read from the cached data
else
{
tempStream.Write(overlayData, overlayOffset, overlayData.Length - overlayOffset);
tempStream.Flush();
}
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Extract data from the resources
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractFromResources(string outputDirectory, bool includeDebug)
{
try
{
// Cache the resource data for easier reading
var resourceData = ResourceData;
if (resourceData.Count == 0)
return false;
// Get the resources that have an archive signature
int i = 0;
foreach (var kvp in resourceData)
{
// Get the key and value
string resourceKey = kvp.Key;
var value = kvp.Value;
if (value is null || value is not Data.Models.PortableExecutable.Resource.Entries.GenericResourceEntry ba || ba.Data.Length == 0)
continue;
// Set the output variables
int resourceOffset = 0;
string extension = string.Empty;
// Only process the resource if it a recognized signature
for (; resourceOffset < 0x400 && resourceOffset < ba.Data.Length - 0x10; resourceOffset++)
{
int temp = resourceOffset;
byte[] resourceSample = ba.Data.ReadBytes(ref temp, 0x10);
if (resourceSample.StartsWith(Data.Models.SevenZip.Constants.SignatureBytes))
{
extension = "7z";
break;
}
else if (resourceSample.StartsWith([0x42, 0x4D]))
{
extension = "bmp";
break;
}
else if (resourceSample.StartsWith(Data.Models.BZip2.Constants.SignatureBytes))
{
extension = "bz2";
break;
}
else if (resourceSample.StartsWith(Data.Models.CFB.Constants.SignatureBytes))
{
// Assume embedded CFB files are MSI
extension = "msi";
break;
}
else if (resourceSample.StartsWith([0x47, 0x49, 0x46, 0x38]))
{
extension = "gif";
break;
}
else if (resourceSample.StartsWith([0x1F, 0x8B]))
{
extension = "gz";
break;
}
else if (resourceSample.StartsWith([0xFF, 0xD8, 0xFF, 0xE0]))
{
extension = "jpg";
break;
}
else if (resourceSample.StartsWith([0x3C, 0x68, 0x74, 0x6D, 0x6C]))
{
extension = "html";
break;
}
else if (resourceSample.StartsWith(Data.Models.MicrosoftCabinet.Constants.SignatureBytes))
{
extension = "cab";
break;
}
else if (resourceSample.StartsWith(Data.Models.PKZIP.Constants.LocalFileHeaderSignatureBytes))
{
extension = "zip";
break;
}
else if (resourceSample.StartsWith(Data.Models.PKZIP.Constants.EndOfCentralDirectoryRecordSignatureBytes))
{
extension = "zip";
break;
}
else if (resourceSample.StartsWith(Data.Models.PKZIP.Constants.EndOfCentralDirectoryRecord64SignatureBytes))
{
extension = "zip";
break;
}
else if (resourceSample.StartsWith(Data.Models.PKZIP.Constants.DataDescriptorSignatureBytes))
{
extension = "zip";
break;
}
else if (resourceSample.StartsWith([0x89, 0x50, 0x4E, 0x47]))
{
extension = "png";
break;
}
else if (resourceSample.StartsWith(Data.Models.RAR.Constants.OldSignatureBytes))
{
extension = "rar";
break;
}
else if (resourceSample.StartsWith(Data.Models.RAR.Constants.NewSignatureBytes))
{
extension = "rar";
break;
}
else if (resourceSample.StartsWith([0x55, 0x48, 0x41, 0x06]))
{
extension = "uha";
break;
}
else if (resourceSample.StartsWith([0x3C, 0x3F, 0x78, 0x6D, 0x6C]))
{
extension = "xml";
break;
}
else if (resourceSample.StartsWith([0x3C, 0x00, 0x3F, 0x00, 0x78, 0x00, 0x6D, 0x00, 0x6C, 0x00]))
{
extension = "xml";
break;
}
else if (resourceSample.StartsWith([0xFF, 0xFE, 0x3C, 0x00, 0x3F, 0x00, 0x78, 0x00, 0x6D, 0x00, 0x6C, 0x00]))
{
extension = "xml";
break;
}
else if (resourceSample.StartsWith(Data.Models.XZ.Constants.HeaderSignatureBytes))
{
extension = "xz";
break;
}
else if (resourceSample.StartsWith(Data.Models.MSDOS.Constants.SignatureBytes))
{
extension = "bin"; // exe/dll
break;
}
}
// If the extension is unset
if (extension.Length == 0)
continue;
try
{
// Create the temp filename
string tempFile = $"embedded_resource_{i++} ({resourceKey}).{extension}";
if (Filename is not null)
tempFile = $"{Path.GetFileName(Filename)}-{tempFile}";
tempFile = Path.Combine(outputDirectory, tempFile);
var directoryName = Path.GetDirectoryName(tempFile);
if (directoryName is not null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Write the resource data to a temp file
using var tempStream = File.Open(tempFile, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
tempStream.Write(ba.Data, resourceOffset, ba.Data.Length - resourceOffset);
tempStream.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
}
}
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Extract data from a SecuROM Matroschka Package
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractMatroschka(string outputDirectory, bool includeDebug)
{
// Check if executable contains Matroschka package or not
if (MatroschkaPackage is null)
return false;
// Attempt to extract package
return MatroschkaPackage.Extract(outputDirectory, includeDebug);
}
/// <summary>
/// Extract data from an Installshield Executable
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractInstallShieldExecutable(string outputDirectory, bool includeDebug)
{
// Check if executable contains an InstallShield Executable or not
if (ISEXE is null)
return false;
// Attempt to extract package
return ISEXE.Extract(outputDirectory, includeDebug);
}
/// <summary>
/// Extract a Spoon Installer SFX overlay
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractSpoonInstaller(string outputDirectory, bool includeDebug)
{
try
{
// Ensure the stream is starting at the beginning
_dataSource.SeekIfPossible(0, SeekOrigin.Begin);
// Try to deserialize the source data
var deserializer = new Serialization.Readers.SpoonInstaller();
var sfx = deserializer.Deserialize(_dataSource);
if (sfx?.Entries is null)
return false;
// Loop through the entries and extract
for (int i = 0; i < sfx.Entries.Length; i++)
{
var entry = sfx.Entries[i];
// Get the offset and compressed size
long offset = entry.FileOffset;
int compressed = (int)entry.CompressedSize;
int extracted = (int)entry.UncompressedSize;
// Try to read the file data
byte[] bz2Data = ReadRangeFromSource(offset, compressed);
if (bz2Data.Length == 0)
continue;
// Try opening the stream
using var ms = new MemoryStream(bz2Data);
using var bz2File = new BZip2Stream(ms, new CompressionOptions { LeaveOpen = false, Type = CompressionType.Decompress});
// Try to read the decompressed data
byte[] data = bz2File.ReadBytes(extracted);
// Ensure directory separators are consistent
string filename = entry.Filename?.TrimEnd('\0') ?? $"FILE_{i}";
filename = filename.TrimStart(['\\', '/']);
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName is not null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Write the output file
var fs = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
fs.Write(data);
fs.Flush();
}
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Extract data from a Wise installer
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractWiseOverlay(string outputDirectory, bool includeDebug)
{
// Try to find the overlay header
long offset = FindWiseOverlayHeader();
if (offset <= 0 || offset > Length)
return false;
// Get the source data for reading
Stream source = _dataSource;
if (Filename is not null)
{
// Try to open a multipart file
if (WiseOverlayHeader.OpenFile(Filename, includeDebug, out var temp) && temp is not null)
source = temp;
}
// Close the multipart file again on return
using var multipart = source == _dataSource ? null : source;
// Seek to the overlay and parse
source.SeekIfPossible(offset, SeekOrigin.Begin);
var header = WiseOverlayHeader.Create(source);
if (header is null)
{
if (includeDebug) Console.Error.WriteLine("Could not parse a Wise overlay header");
return false;
}
// Extract the header-defined files
bool extracted = header.ExtractHeaderDefinedFiles(outputDirectory, includeDebug);
if (!extracted)
{
if (includeDebug) Console.Error.WriteLine("Could not extract Wise overlay header-defined files");
return false;
}
// Open the script file from the output directory
using var scriptStream = File.OpenRead(Path.Combine(outputDirectory, "WiseScript.bin"));
var script = WiseScript.Create(scriptStream);
if (script is null)
{
if (includeDebug) Console.Error.WriteLine("Could not parse WiseScript.bin");
return false;
}
// Get the source directory
string? sourceDirectory = null;
if (Filename is not null)
sourceDirectory = Path.GetDirectoryName(Path.GetFullPath(Filename));
// Process the state machine
return script.ProcessStateMachine(header, sourceDirectory, outputDirectory, includeDebug);
}
/// <summary>
/// Extract using Wise section
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractWiseSection(string outputDirectory, bool includeDebug)
{
// Get the section header
var header = WiseSection;
if (header is null)
{
if (includeDebug) Console.Error.WriteLine("Could not parse a Wise section header");
return false;
}
// Attempt to extract section
return header.Extract(outputDirectory, includeDebug);
}
/// <summary>
/// Decompress CExe data compressed with LZ
/// </summary>
/// <param name="resource">Resource data to inflate</param>
/// <returns>Inflated data on success, null otherwise</returns>
private static byte[]? DecompressCExeLZ(byte[] resource)
{
try
{
var decompressor = IO.Compression.SZDD.Decompressor.CreateSZDD(resource);
using var dataStream = new MemoryStream();
decompressor.CopyTo(dataStream);
return dataStream.ToArray();
}
catch
{
// Reset the data
return null;
}
}
/// <summary>
/// Decompress CExe data compressed with zlib
/// </summary>
/// <param name="resource">Resource data to inflate</param>
/// <returns>Inflated data on success, null otherwise</returns>
private static byte[]? DecompressCExeZlib(byte[] resource)
{
try
{
// Open the input stream as ZLib-compressed
using var inputStream = new MemoryStream(resource);
var zlibStream = new ZLibStream(inputStream, new CompressionOptions { Type = CompressionType.Decompress });
// Inflate the data into the buffer
using var outputStream = new MemoryStream();
zlibStream.BlockCopy(outputStream);
byte[] data = outputStream.ToArray();
return data;
}
catch
{
// Reset the data
return null;
}
}
}
}