using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Dalamud.IoC; using Dalamud.IoC.Internal; using Serilog; namespace Dalamud.Game { /// /// A SigScanner facilitates searching for memory signatures in a given ProcessModule. /// [PluginInterface] [InterfaceVersion("1.0")] public sealed class SigScanner : IDisposable { private IntPtr moduleCopyPtr; private long moduleCopyOffset; /// /// Initializes a new instance of the class using the main module of the current process. /// /// Whether or not to copy the module upon initialization for search operations to use, as to not get disturbed by possible hooks. public SigScanner(bool doCopy = false) : this(Process.GetCurrentProcess().MainModule!, doCopy) { } /// /// Initializes a new instance of the class. /// /// The ProcessModule to be used for scanning. /// Whether or not to copy the module upon initialization for search operations to use, as to not get disturbed by possible hooks. public SigScanner(ProcessModule module, bool doCopy = false) { this.Module = module; this.Is32BitProcess = !Environment.Is64BitProcess; this.IsCopy = doCopy; // Limit the search space to .text section. this.SetupSearchSpace(module); if (this.IsCopy) this.SetupCopiedSegments(); Log.Verbose($"Module base: 0x{this.TextSectionBase.ToInt64():X}"); Log.Verbose($"Module size: 0x{this.TextSectionSize:X}"); } /// /// Gets a value indicating whether or not the search on this module is performed on a copy. /// public bool IsCopy { get; } /// /// Gets a value indicating whether or not the ProcessModule is 32-bit. /// public bool Is32BitProcess { get; } /// /// Gets the base address of the search area. When copied, this will be the address of the copy. /// public IntPtr SearchBase => this.IsCopy ? this.moduleCopyPtr : this.Module.BaseAddress; /// /// Gets the base address of the .text section search area. /// public IntPtr TextSectionBase => new(this.SearchBase.ToInt64() + this.TextSectionOffset); /// /// Gets the offset of the .text section from the base of the module. /// public long TextSectionOffset { get; private set; } /// /// Gets the size of the text section. /// public int TextSectionSize { get; private set; } /// /// Gets the base address of the .data section search area. /// public IntPtr DataSectionBase => new(this.SearchBase.ToInt64() + this.DataSectionOffset); /// /// Gets the offset of the .data section from the base of the module. /// public long DataSectionOffset { get; private set; } /// /// Gets the size of the .data section. /// public int DataSectionSize { get; private set; } /// /// Gets the base address of the .rdata section search area. /// public IntPtr RDataSectionBase => new(this.SearchBase.ToInt64() + this.RDataSectionOffset); /// /// Gets the offset of the .rdata section from the base of the module. /// public long RDataSectionOffset { get; private set; } /// /// Gets the size of the .rdata section. /// public int RDataSectionSize { get; private set; } /// /// Gets the ProcessModule on which the search is performed. /// public ProcessModule Module { get; } private IntPtr TextSectionTop => this.TextSectionBase + this.TextSectionSize; /// /// Scan memory for a signature. /// /// The base address to scan from. /// The amount of bytes to scan. /// The signature to search for. /// The found offset. public static IntPtr Scan(IntPtr baseAddress, int size, string signature) { var (needle, mask) = ParseSignature(signature); var index = IndexOf(baseAddress, size, needle, mask); if (index < 0) throw new KeyNotFoundException($"Can't find a signature of {signature}"); return baseAddress + index; } /// /// Try scanning memory for a signature. /// /// The base address to scan from. /// The amount of bytes to scan. /// The signature to search for. /// The offset, if found. /// true if the signature was found. public static bool TryScan(IntPtr baseAddress, int size, string signature, out IntPtr result) { try { result = Scan(baseAddress, size, signature); return true; } catch (KeyNotFoundException) { result = IntPtr.Zero; return false; } } /// /// Scan for a .data address using a .text function. /// This is intended to be used with IDA sigs. /// Place your cursor on the line calling a static address, and create and IDA sig. /// /// The signature of the function using the data. /// The offset from function start of the instruction using the data. /// An IntPtr to the static memory location. public IntPtr GetStaticAddressFromSig(string signature, int offset = 0) { var instrAddr = this.ScanText(signature); instrAddr = IntPtr.Add(instrAddr, offset); var bAddr = (long)this.Module.BaseAddress; long num; do { instrAddr = IntPtr.Add(instrAddr, 1); num = Marshal.ReadInt32(instrAddr) + (long)instrAddr + 4 - bAddr; } while (!(num >= this.DataSectionOffset && num <= this.DataSectionOffset + this.DataSectionSize) && !(num >= this.RDataSectionOffset && num <= this.RDataSectionOffset + this.RDataSectionSize)); return IntPtr.Add(instrAddr, Marshal.ReadInt32(instrAddr) + 4); } /// /// Try scanning for a .data address using a .text function. /// This is intended to be used with IDA sigs. /// Place your cursor on the line calling a static address, and create and IDA sig. /// /// The signature of the function using the data. /// An IntPtr to the static memory location, if found. /// The offset from function start of the instruction using the data. /// true if the signature was found. public bool TryGetStaticAddressFromSig(string signature, out IntPtr result, int offset = 0) { try { result = this.GetStaticAddressFromSig(signature, offset); return true; } catch (KeyNotFoundException) { result = IntPtr.Zero; return false; } } /// /// Scan for a byte signature in the .data section. /// /// The signature. /// The real offset of the found signature. public IntPtr ScanData(string signature) { var scanRet = Scan(this.DataSectionBase, this.DataSectionSize, signature); if (this.IsCopy) scanRet = new IntPtr(scanRet.ToInt64() - this.moduleCopyOffset); return scanRet; } /// /// Try scanning for a byte signature in the .data section. /// /// The signature. /// The real offset of the signature, if found. /// true if the signature was found. public bool TryScanData(string signature, out IntPtr result) { try { result = this.ScanData(signature); return true; } catch (KeyNotFoundException) { result = IntPtr.Zero; return false; } } /// /// Scan for a byte signature in the whole module search area. /// /// The signature. /// The real offset of the found signature. public IntPtr ScanModule(string signature) { var scanRet = Scan(this.SearchBase, this.Module.ModuleMemorySize, signature); if (this.IsCopy) scanRet = new IntPtr(scanRet.ToInt64() - this.moduleCopyOffset); return scanRet; } /// /// Try scanning for a byte signature in the whole module search area. /// /// The signature. /// The real offset of the signature, if found. /// true if the signature was found. public bool TryScanModule(string signature, out IntPtr result) { try { result = this.ScanModule(signature); return true; } catch (KeyNotFoundException) { result = IntPtr.Zero; return false; } } /// /// Resolve a RVA address. /// /// The address of the next instruction. /// The relative offset. /// The calculated offset. public IntPtr ResolveRelativeAddress(IntPtr nextInstAddr, int relOffset) { if (this.Is32BitProcess) throw new NotSupportedException("32 bit is not supported."); return nextInstAddr + relOffset; } /// /// Scan for a byte signature in the .text section. /// /// The signature. /// The real offset of the found signature. public IntPtr ScanText(string signature) { var mBase = this.IsCopy ? this.moduleCopyPtr : this.TextSectionBase; var scanRet = Scan(mBase, this.TextSectionSize, signature); if (this.IsCopy) scanRet = new IntPtr(scanRet.ToInt64() - this.moduleCopyOffset); var insnByte = Marshal.ReadByte(scanRet); if (insnByte == 0xE8 || insnByte == 0xE9) return ReadJmpCallSig(scanRet); return scanRet; } /// /// Try scanning for a byte signature in the .text section. /// /// The signature. /// The real offset of the signature, if found. /// true if the signature was found. public bool TryScanText(string signature, out IntPtr result) { try { result = this.ScanText(signature); return true; } catch (KeyNotFoundException) { result = IntPtr.Zero; return false; } } /// /// Free the memory of the copied module search area on object disposal, if applicable. /// public void Dispose() { Marshal.FreeHGlobal(this.moduleCopyPtr); } /// /// Helper for ScanText to get the correct address for IDA sigs that mark the first JMP or CALL location. /// /// The address the JMP or CALL sig resolved to. /// The real offset of the signature. private static IntPtr ReadJmpCallSig(IntPtr sigLocation) { var jumpOffset = Marshal.ReadInt32(sigLocation, 1); return IntPtr.Add(sigLocation, 5 + jumpOffset); } private static (byte[] Needle, bool[] Mask) ParseSignature(string signature) { signature = signature.Replace(" ", string.Empty); if (signature.Length % 2 != 0) throw new ArgumentException("Signature without whitespaces must be divisible by two.", nameof(signature)); var needleLength = signature.Length / 2; var needle = new byte[needleLength]; var mask = new bool[needleLength]; for (var i = 0; i < needleLength; i++) { var hexString = signature.Substring(i * 2, 2); if (hexString == "??" || hexString == "**") { needle[i] = 0; mask[i] = true; continue; } needle[i] = byte.Parse(hexString, NumberStyles.AllowHexSpecifier); mask[i] = false; } return (needle, mask); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe int IndexOf(IntPtr bufferPtr, int bufferLength, byte[] needle, bool[] mask) { if (needle.Length > bufferLength) return -1; var badShift = BuildBadCharTable(needle, mask); var last = needle.Length - 1; var offset = 0; var maxoffset = bufferLength - needle.Length; var buffer = (byte*)bufferPtr; while (offset <= maxoffset) { int position; for (position = last; needle[position] == *(buffer + position + offset) || mask[position]; position--) { if (position == 0) return offset; } offset += badShift[*(buffer + offset + last)]; } return -1; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int[] BuildBadCharTable(byte[] needle, bool[] mask) { int idx; var last = needle.Length - 1; var badShift = new int[256]; for (idx = last; idx > 0 && !mask[idx]; --idx) { } var diff = last - idx; if (diff == 0) diff = 1; for (idx = 0; idx <= 255; ++idx) badShift[idx] = diff; for (idx = last - diff; idx < last; ++idx) badShift[needle[idx]] = last - idx; return badShift; } private void SetupSearchSpace(ProcessModule module) { var baseAddress = module.BaseAddress; // We don't want to read all of IMAGE_DOS_HEADER or IMAGE_NT_HEADER stuff so we cheat here. var ntNewOffset = Marshal.ReadInt32(baseAddress, 0x3C); var ntHeader = baseAddress + ntNewOffset; // IMAGE_NT_HEADER var fileHeader = ntHeader + 4; var numSections = Marshal.ReadInt16(ntHeader, 6); // IMAGE_OPTIONAL_HEADER var optionalHeader = fileHeader + 20; IntPtr sectionHeader; if (this.Is32BitProcess) // IMAGE_OPTIONAL_HEADER32 sectionHeader = optionalHeader + 224; else // IMAGE_OPTIONAL_HEADER64 sectionHeader = optionalHeader + 240; // IMAGE_SECTION_HEADER var sectionCursor = sectionHeader; for (var i = 0; i < numSections; i++) { var sectionName = Marshal.ReadInt64(sectionCursor); // .text switch (sectionName) { case 0x747865742E: // .text this.TextSectionOffset = Marshal.ReadInt32(sectionCursor, 12); this.TextSectionSize = Marshal.ReadInt32(sectionCursor, 8); break; case 0x617461642E: // .data this.DataSectionOffset = Marshal.ReadInt32(sectionCursor, 12); this.DataSectionSize = Marshal.ReadInt32(sectionCursor, 8); break; case 0x61746164722E: // .rdata this.RDataSectionOffset = Marshal.ReadInt32(sectionCursor, 12); this.RDataSectionSize = Marshal.ReadInt32(sectionCursor, 8); break; } sectionCursor += 40; } } private unsafe void SetupCopiedSegments() { // .text this.moduleCopyPtr = Marshal.AllocHGlobal(this.Module.ModuleMemorySize); Buffer.MemoryCopy( this.Module.BaseAddress.ToPointer(), this.moduleCopyPtr.ToPointer(), this.Module.ModuleMemorySize, this.Module.ModuleMemorySize); this.moduleCopyOffset = this.moduleCopyPtr.ToInt64() - this.Module.BaseAddress.ToInt64(); } } }