PE: Add LoadLibrary DLL unpacker load strategy
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2017-2019 Katy Coe - http://www.hearthcode.org - http://www.djkaty.com
|
||||
Copyright 2017-2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
|
||||
|
||||
All rights reserved.
|
||||
*/
|
||||
@@ -17,7 +17,10 @@ namespace Il2CppInspector
|
||||
|
||||
IMAGE_SCN_CNT_CODE = 0x00000020,
|
||||
IMAGE_SCN_CNT_INITIALIZED_DATA = 0x00000040,
|
||||
IMAGE_SCN_CNT_UNINITIALIZED_DATA = 0x00000080
|
||||
IMAGE_SCN_CNT_UNINITIALIZED_DATA = 0x00000080,
|
||||
IMAGE_SCN_MEM_EXECUTE = 0x20000000,
|
||||
IMAGE_SCN_MEM_READ = 0x40000000,
|
||||
IMAGE_SCN_MEM_WRITE = 0x80000000
|
||||
}
|
||||
|
||||
#pragma warning disable CS0649
|
||||
@@ -38,7 +41,7 @@ namespace Il2CppInspector
|
||||
{
|
||||
PE ExpectedMagic { get; }
|
||||
ushort Magic { get; }
|
||||
ulong ImageBase { get; }
|
||||
ulong ImageBase { get; set; }
|
||||
uint BaseOfCode { get; }
|
||||
RvaEntry[] DataDirectory { get; }
|
||||
}
|
||||
@@ -47,7 +50,7 @@ namespace Il2CppInspector
|
||||
{
|
||||
public PE ExpectedMagic => PE.IMAGE_NT_OPTIONAL_HDR32_MAGIC;
|
||||
public ushort Magic => f_Magic;
|
||||
public ulong ImageBase => f_ImageBase;
|
||||
public ulong ImageBase { get => f_ImageBase; set => f_ImageBase = (uint) value; }
|
||||
public uint BaseOfCode => f_BaseOfCode;
|
||||
public RvaEntry[] DataDirectory => f_DataDirectory;
|
||||
|
||||
@@ -90,7 +93,7 @@ namespace Il2CppInspector
|
||||
{
|
||||
public PE ExpectedMagic => PE.IMAGE_NT_OPTIONAL_HDR64_MAGIC;
|
||||
public ushort Magic => f_Magic;
|
||||
public ulong ImageBase => f_ImageBase;
|
||||
public ulong ImageBase { get => f_ImageBase; set => f_ImageBase = value; }
|
||||
public uint BaseOfCode => f_BaseOfCode;
|
||||
public RvaEntry[] DataDirectory => f_DataDirectory;
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ namespace Il2CppInspector
|
||||
public ulong ImageBase { get; set; }
|
||||
|
||||
// For Linux process memory map inputs, we need the full path so we can find the .bin files
|
||||
// For packed PE files, we need the full path to reload the file via Win32 API
|
||||
// Ignored for all other cases
|
||||
public string BinaryFilePath { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -581,8 +581,8 @@ namespace Il2CppInspector
|
||||
processors.Add(new Il2CppInspector(binary, metadata));
|
||||
}
|
||||
else {
|
||||
Console.Error.WriteLine("Could not process IL2CPP image. This may mean the binary file is packed, encrypted or obfuscated, that the file is not an IL2CPP image or that Il2CppInspector was not able to automatically find the required data.");
|
||||
Console.Error.WriteLine("Please check the binary file in a disassembler to ensure that it is an unencrypted IL2CPP binary before submitting a bug report!");
|
||||
Console.Error.WriteLine("Could not process IL2CPP image. This may mean the binary file is packed, encrypted or obfuscated in a way Il2CppInspector cannot process, that the file is not an IL2CPP image or that Il2CppInspector was not able to automatically find the required data.");
|
||||
Console.Error.WriteLine("Please check the binary file in a disassembler to ensure that it is a valid IL2CPP binary before submitting a bug report!");
|
||||
}
|
||||
}
|
||||
// Unknown architecture
|
||||
|
||||
Reference in New Issue
Block a user