PE: Add LoadLibrary DLL unpacker load strategy
This commit is contained in:
@@ -8,6 +8,9 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using NoisyCowStudios.Bin2Object;
|
||||
|
||||
namespace Il2CppInspector
|
||||
{
|
||||
@@ -16,10 +19,24 @@ namespace Il2CppInspector
|
||||
// PE format specification: https://docs.microsoft.com/en-us/windows/win32/debug/pe-format?redirectedfrom=MSDN
|
||||
internal class PEReader : FileFormatReader<PEReader>
|
||||
{
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||
private extern static IntPtr LoadLibrary(string lpLibFileName);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||
private extern static bool FreeLibrary(IntPtr hLibModule);
|
||||
|
||||
private COFFHeader coff;
|
||||
private IPEOptHeader pe;
|
||||
private PESection[] sections;
|
||||
private uint pFuncTable;
|
||||
private bool mightBePacked;
|
||||
|
||||
// Section types we need to rename in obfuscated binaries
|
||||
private Dictionary<PE, string> wantedSectionTypes = new Dictionary<PE, string> {
|
||||
[PE.IMAGE_SCN_MEM_READ | PE.IMAGE_SCN_MEM_EXECUTE | PE.IMAGE_SCN_CNT_CODE] = ".text",
|
||||
[PE.IMAGE_SCN_MEM_READ | PE.IMAGE_SCN_CNT_INITIALIZED_DATA] = ".rdata",
|
||||
[PE.IMAGE_SCN_MEM_READ | PE.IMAGE_SCN_MEM_WRITE | PE.IMAGE_SCN_CNT_INITIALIZED_DATA] = ".data"
|
||||
};
|
||||
|
||||
public PEReader(Stream stream) : base(stream) {}
|
||||
|
||||
@@ -84,21 +101,26 @@ namespace Il2CppInspector
|
||||
// Get sections table
|
||||
sections = ReadArray<PESection>(coff.NumberOfSections);
|
||||
|
||||
// Packed with Themida?
|
||||
// TODO: Deal with Themida packing (issues #56, #95, #101)
|
||||
if (sections.FirstOrDefault(x => x.Name == ".themida") is PESection _) {
|
||||
throw new InvalidOperationException("This IL2CPP binary is packed with Themida and cannot be loaded directly. Unpack the binary first and try again. NOTE: Automatic unpacking of PE files will be included in a future update coming soon!");
|
||||
}
|
||||
// Unpacking must be done starting here, one byte after the end of the headers
|
||||
// Packed or previously packed with Themida? This is purely for information
|
||||
if (sections.FirstOrDefault(x => x.Name == ".themida") is PESection _)
|
||||
Console.WriteLine("Themida protection detected");
|
||||
|
||||
// Packed with something else?
|
||||
if (sections.FirstOrDefault(x => x.Name == ".rdata") is null) {
|
||||
throw new InvalidOperationException("This IL2CPP binary is packed in a way not currently supported by Il2CppInspector and cannot be loaded. NOTE: Automatic unpacking of PE files will be included in a future update coming soon!");
|
||||
}
|
||||
// Packed with anything (including Themida)?
|
||||
mightBePacked = sections.FirstOrDefault(x => x.Name == ".rdata") is null;
|
||||
|
||||
// Rename sections if needed (before potentially searching them or rewriting them to the stream)
|
||||
foreach (var section in sections.Where(s => wantedSectionTypes.Keys.Contains(s.Characteristics)))
|
||||
// Replace section name if blank or all whitespace
|
||||
if (Regex.IsMatch(section.Name, @"^\s*$"))
|
||||
section.Name = wantedSectionTypes[section.Characteristics];
|
||||
|
||||
// Get base of code
|
||||
GlobalOffset = pe.ImageBase + pe.BaseOfCode - sections.First(x => x.Name == ".text").PointerToRawData;
|
||||
|
||||
// Confirm that .rdata section begins at same place as IAT
|
||||
var rData = sections.First(x => x.Name == ".rdata");
|
||||
if (rData.VirtualAddress != IATStart)
|
||||
return false;
|
||||
mightBePacked |= rData.VirtualAddress != IATStart;
|
||||
|
||||
// Calculate start of function pointer table
|
||||
pFuncTable = rData.PointerToRawData + IATSize;
|
||||
@@ -116,17 +138,95 @@ namespace Il2CppInspector
|
||||
pFuncTable += 8;
|
||||
}
|
||||
|
||||
// Get base of code
|
||||
GlobalOffset = pe.ImageBase + pe.BaseOfCode - sections.First(x => x.Name == ".text").PointerToRawData;
|
||||
// In the fist go round, we signal that this is at least a valid PE file; we don't try to unpack yet
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load DLL into memory and save it as a new PE stream
|
||||
private void load() {
|
||||
// Check that the process is running in the same word size as the DLL
|
||||
// One way round this in future would be to spawn a new process of the correct word size
|
||||
if ((Environment.Is64BitProcess && Bits == 32) || (!Environment.Is64BitProcess && Bits == 64))
|
||||
throw new InvalidOperationException($"Cannot unpack a {Bits}-bit DLL from within a {(Environment.Is64BitProcess ? 64 : 32)}-bit process. Use the {Bits}-version of Il2CppInspector to unpack this DLL.");
|
||||
|
||||
// Get file path
|
||||
// This error should never occur with the bundled CLI and GUI; only when used as a library by a 3rd party tool
|
||||
if (!(LoadOptions.BinaryFilePath is string dllPath))
|
||||
throw new InvalidOperationException("To load a packed PE file, you must specify the DLL file path in LoadOptions");
|
||||
|
||||
// Attempt to load DLL and run startup functions
|
||||
// NOTE: This can cause a CSE (AccessViolation) for certain types of protection
|
||||
// so only try to unpack as the final load strategy
|
||||
IntPtr hModule = LoadLibrary(dllPath);
|
||||
if (hModule == IntPtr.Zero) {
|
||||
var lastErrorCode = Marshal.GetLastWin32Error();
|
||||
throw new InvalidOperationException($"Unable to load the DLL for unpacking: error code {lastErrorCode}");
|
||||
}
|
||||
|
||||
// Maximum image size
|
||||
var size = sections.Last().VirtualAddress + sections.Last().VirtualSize;
|
||||
|
||||
// Allocate memory for unpacked image
|
||||
var peBytes = new byte[size];
|
||||
|
||||
// Copy relevant sections from unmanaged memory
|
||||
foreach (var section in sections.Where(s => wantedSectionTypes.Keys.Contains(s.Characteristics)))
|
||||
Marshal.Copy(IntPtr.Add(hModule, (int) section.VirtualAddress), peBytes, (int) section.VirtualAddress, (int) section.VirtualSize);
|
||||
|
||||
// Decrease reference count for unload
|
||||
FreeLibrary(hModule);
|
||||
|
||||
// Rebase
|
||||
pe.ImageBase = (ulong) hModule.ToInt64();
|
||||
|
||||
// Rewrite sections to match memory layout
|
||||
foreach (var section in sections) {
|
||||
section.PointerToRawData = section.VirtualAddress;
|
||||
section.SizeOfRawData = section.VirtualSize;
|
||||
}
|
||||
|
||||
// Truncate memory stream at start of COFF header
|
||||
var endOfSignature = ReadUInt32(0x3C) + 4; // DOS header + 4-byte PE signature
|
||||
BaseStream.SetLength(endOfSignature);
|
||||
|
||||
// Re-write the stream (the headers are only necessary in case the user wants to save)
|
||||
using var writer = new BinaryObjectWriter(BaseStream, Endianness, true);
|
||||
writer.Position = endOfSignature;
|
||||
writer.WriteObject(coff);
|
||||
if (Bits == 32) writer.WriteObject((PEOptHeader32) pe);
|
||||
else writer.WriteObject((PEOptHeader64) pe);
|
||||
writer.WriteArray(sections);
|
||||
writer.Write(peBytes, (int) Position, peBytes.Length - (int) Position);
|
||||
|
||||
IsModified = true;
|
||||
}
|
||||
|
||||
// Raw file / unpacked file load strategies
|
||||
public override IEnumerable<IFileFormatReader> TryNextLoadStrategy() {
|
||||
// First load strategy: the regular file
|
||||
yield return this;
|
||||
|
||||
// Second load strategy: load the DLL into memory to unpack it
|
||||
if (mightBePacked) {
|
||||
Console.WriteLine("IL2CPP binary appears to be packed - attempting to unpack and retrying");
|
||||
StatusUpdate("Unpacking binary");
|
||||
load();
|
||||
yield return this;
|
||||
}
|
||||
}
|
||||
|
||||
public override uint[] GetFunctionTable() {
|
||||
if (pFuncTable == 0)
|
||||
return Array.Empty<uint>();
|
||||
|
||||
Position = pFuncTable;
|
||||
var addrs = new List<uint>();
|
||||
ulong addr;
|
||||
while ((addr = pe is PEOptHeader32? ReadUInt32() : ReadUInt64()) != 0)
|
||||
addrs.Add(MapVATR(addr) & 0xfffffffc);
|
||||
|
||||
// Use TryMapVATR to avoid crash if function table is stripped or corrupted
|
||||
// Can happen with packed or previously packed files
|
||||
while ((addr = pe is PEOptHeader32? ReadUInt32() : ReadUInt64()) != 0 && TryMapVATR(addr, out uint fileOffset))
|
||||
addrs.Add(fileOffset & 0xfffffffc);
|
||||
return addrs.ToArray();
|
||||
}
|
||||
|
||||
@@ -155,7 +255,7 @@ namespace Il2CppInspector
|
||||
|
||||
return exports.Values;
|
||||
}
|
||||
|
||||
|
||||
public override uint MapVATR(ulong uiAddr) {
|
||||
if (uiAddr == 0)
|
||||
return 0;
|
||||
|
||||
Reference in New Issue
Block a user