Add scuffed pap handling

This commit is contained in:
pmgr 2024-07-21 16:34:27 +01:00
parent 48ab98bee6
commit 8c34c18643
6 changed files with 446 additions and 4 deletions

View file

@ -0,0 +1,13 @@
using Iced.Intel;
namespace Penumbra.Interop.Hooks.ResourceLoading;
public class MappedCodeReader(UnmanagedMemoryAccessor data, long offset) : CodeReader
{
public override int ReadByte() {
if (offset >= data.Capacity)
return -1;
return data.ReadByte(offset++);
}
}

View file

@ -0,0 +1,23 @@
using Penumbra.GameData;
namespace Penumbra.Interop.Hooks.ResourceLoading;
public sealed class PapHandler(PapRewriter.PapResourceHandlerPrototype papResourceHandler) : IDisposable
{
private readonly PapRewriter _papRewriter = new(papResourceHandler);
public void Enable()
{
_papRewriter.Rewrite(Sigs.LoadAlwaysResidentMotionPacks);
_papRewriter.Rewrite(Sigs.LoadWeaponDependentResidentMotionPacks);
_papRewriter.Rewrite(Sigs.LoadInitialResidentMotionPacks);
_papRewriter.Rewrite(Sigs.LoadMotionPacks);
_papRewriter.Rewrite(Sigs.LoadMotionPacks2);
_papRewriter.Rewrite(Sigs.LoadMigratoryMotionPack);
}
public void Dispose()
{
_papRewriter.Dispose();
}
}

View file

@ -0,0 +1,181 @@
using Dalamud.Hooking;
using Iced.Intel;
using Penumbra.String.Classes;
namespace Penumbra.Interop.Hooks.ResourceLoading;
public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResourceHandler) : IDisposable
{
public unsafe delegate int PapResourceHandlerPrototype(void* self, byte* path, int length);
private PeSigScanner Scanner { get; } = new();
private Dictionary<IntPtr, AsmHook> Hooks { get; }= [];
private List<IntPtr> NativeAllocList { get; } = [];
private PapResourceHandlerPrototype PapResourceHandler { get; } = papResourceHandler;
public void Rewrite(string sig)
{
if (!Scanner.TryScanText(sig, out var addr))
{
throw new Exception($"Sig is fucked: {sig}");
}
var funcInstructions = Scanner.GetFunctionInstructions(addr).ToList();
var hookPoints = ScanPapHookPoints(funcInstructions).ToList();
foreach (var hookPoint in hookPoints)
{
var stackAccesses = ScanStackAccesses(funcInstructions, hookPoint).ToList();
var stringLoc = NativeAlloc(Utf8GamePath.MaxGamePathLength);
{
// We'll need to grab our true hook point; the location where we can change the path at our leisure.
// This is going to be the first call instruction after our 'hookPoint', so, we'll find that.
// Pretty scuffed, this might need a refactoring at some point.
// We're doing it by skipping to our hookPoint's address in the list of instructions inside the function; then getting next CALL
var detourPoint = funcInstructions.Skip(
funcInstructions.FindIndex(instr => instr.IP == hookPoint.IP) + 1
).First(instr => instr.Mnemonic == Mnemonic.Call);
// We'll also remove all the 'hookPoints' from 'stackAccesses'.
// We're handling the char *path redirection here, so we don't want this to hit the later code
foreach (var hp in hookPoints)
{
stackAccesses.RemoveAll(instr => instr.IP == hp.IP);
}
var pDetour = Marshal.GetFunctionPointerForDelegate(PapResourceHandler);
var targetRegister = hookPoint.Op0Register.ToString().ToLower();
var hookAddr = new IntPtr((long)detourPoint.IP);
var caveLoc = NativeAlloc(16);
var hook = new AsmHook(
hookAddr,
[
"use64",
$"mov {targetRegister}, 0x{stringLoc:x8}", // Move our char *path into the relevant register (rdx)
// After this asm stub, we have a call to Crc32(); since r9 is a volatile, unused register, we can use it ourselves
// We're essentially storing the original 2 arguments ('this', 'path'), in case they get mangled in our call
// We technically don't need to save rdx ('path'), since it'll be stringLoc, but eh
$"mov r9, 0x{caveLoc:x8}",
"mov [r9], rcx",
"mov [r9+0x8], rdx",
// We can use 'rax' here too since it's also volatile, and it'll be overwritten by Crc32()'s return anyway
$"mov rax, 0x{pDetour:x8}", // Get a pointer to our detour in place
"call rax", // Call detour
// Do the reverse process and retrieve the stored stuff
$"mov r9, 0x{caveLoc:x8}",
"mov rcx, [r9]",
"mov rdx, [r9+0x8]",
// Plop 'rax' (our return value, the path size) into r8, so it's the third argument for the subsequent Crc32() call
"mov r8, rax",
], "Pap Redirection"
);
Hooks.Add(hookAddr, hook);
hook.Enable();
}
// Now we're adjusting every single reference to the stack allocated 'path' to our substantially bigger 'stringLoc'
foreach (var stackAccess in stackAccesses)
{
var hookAddr = new IntPtr((long)stackAccess.IP + stackAccess.Length);
if (Hooks.ContainsKey(hookAddr))
{
// Hook already exists, means there's reuse of the same stack address across 2 GetResourceAsync; just skip
continue;
}
var targetRegister = stackAccess.Op0Register.ToString().ToLower();
var hook = new AsmHook(
hookAddr,
[
"use64",
$"mov {targetRegister}, 0x{stringLoc:x8}",
], "Pap Stack Accesses"
);
Hooks.Add(hookAddr, hook);
hook.Enable();
}
}
}
private static IEnumerable<Instruction> ScanStackAccesses(IEnumerable<Instruction> instructions, Instruction hookPoint)
{
return instructions.Where(instr =>
instr.Code == hookPoint.Code
&& instr.Op0Kind == hookPoint.Op0Kind
&& instr.Op1Kind == hookPoint.Op1Kind
&& instr.MemoryBase == hookPoint.MemoryBase
&& instr.MemoryDisplacement64 == hookPoint.MemoryDisplacement64)
.GroupBy(instr => instr.IP)
.Select(grp => grp.First());
}
// This is utterly fucked and hardcoded, but, again, it works
// Might be a neat idea for a more versatile kind of signature though
private static IEnumerable<Instruction> ScanPapHookPoints(List<Instruction> funcInstructions)
{
for (var i = 0; i < funcInstructions.Count - 8; i++)
{
if (funcInstructions[i .. (i + 8)] is
[
{Code : Code.Lea_r64_m},
{Code : Code.Lea_r64_m},
{Mnemonic: Mnemonic.Call},
{Code : Code.Lea_r64_m},
{Mnemonic: Mnemonic.Call},
{Code : Code.Lea_r64_m},
..,
]
)
{
yield return funcInstructions[i];
}
}
}
private unsafe IntPtr NativeAlloc(nuint size)
{
var caveLoc = new IntPtr(NativeMemory.Alloc(size));
NativeAllocList.Add(caveLoc);
return caveLoc;
}
private static unsafe void NativeFree(IntPtr mem)
{
NativeMemory.Free(mem.ToPointer());
}
public void Dispose()
{
Scanner.Dispose();
foreach (var hook in Hooks.Values)
{
hook.Disable();
hook.Dispose();
}
Hooks.Clear();
foreach (var mem in NativeAllocList)
{
NativeFree(mem);
}
NativeAllocList.Clear();
}
}

View file

@ -0,0 +1,194 @@
using System.IO.MemoryMappedFiles;
using Iced.Intel;
using PeNet;
using Decoder = Iced.Intel.Decoder;
namespace Penumbra.Interop.Hooks.ResourceLoading;
// A good chunk of this was blatantly stolen from Dalamud's SigScanner 'cause I could not be faffed, maybe I'll rewrite it later
public class PeSigScanner : IDisposable
{
private MemoryMappedFile File { get; }
private uint TextSectionStart { get; }
private uint TextSectionSize { get; }
private IntPtr ModuleBaseAddress { get; }
private uint TextSectionVirtualAddress { get; }
private MemoryMappedViewAccessor TextSection { get; }
public PeSigScanner()
{
var mainModule = Process.GetCurrentProcess().MainModule!;
var fileName = mainModule.FileName;
ModuleBaseAddress = mainModule.BaseAddress;
if (fileName == null)
{
throw new Exception("Can't get main module path, the fuck is going on?");
}
File = MemoryMappedFile.CreateFromFile(fileName, FileMode.Open, null, 0, MemoryMappedFileAccess.Read);
using var fileStream = File.CreateViewStream(0, 0, MemoryMappedFileAccess.Read);
var pe = new PeFile(fileStream);
var textSection = pe.ImageSectionHeaders!.First(header => header.Name == ".text");
TextSectionStart = textSection.PointerToRawData;
TextSectionSize = textSection.SizeOfRawData;
TextSectionVirtualAddress = textSection.VirtualAddress;
TextSection = File.CreateViewAccessor(TextSectionStart, TextSectionSize, MemoryMappedFileAccess.Read);
}
private IntPtr ScanText(string signature)
{
var scanRet = Scan(TextSection, signature);
var instrByte = Marshal.ReadByte(scanRet);
if (instrByte is 0xE8 or 0xE9)
scanRet = ReadJmpCallSig(scanRet);
return scanRet;
}
private static IntPtr ReadJmpCallSig(IntPtr sigLocation)
{
var jumpOffset = Marshal.ReadInt32(sigLocation, 1);
return IntPtr.Add(sigLocation, 5 + jumpOffset);
}
public bool TryScanText(string signature, out IntPtr result)
{
try
{
result = ScanText(signature);
return true;
}
catch (KeyNotFoundException)
{
result = IntPtr.Zero;
return false;
}
}
private IntPtr Scan(MemoryMappedViewAccessor section, string signature)
{
var (needle, mask) = ParseSignature(signature);
var index = IndexOf(section, needle, mask);
if (index < 0)
throw new KeyNotFoundException($"Can't find a signature of {signature}");
return new IntPtr(ModuleBaseAddress + index - section.PointerOffset + TextSectionVirtualAddress);
}
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);
}
private static unsafe int IndexOf(MemoryMappedViewAccessor section, byte[] needle, bool[] mask)
{
if (needle.Length > section.Capacity) return -1;
var badShift = BuildBadCharTable(needle, mask);
var last = needle.Length - 1;
var offset = 0;
var maxOffset = section.Capacity - needle.Length;
byte* buffer = null;
section.SafeMemoryMappedViewHandle.AcquirePointer(ref buffer);
try
{
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)];
}
}
finally
{
section.SafeMemoryMappedViewHandle.ReleasePointer();
}
return -1;
}
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;
}
// Detects function termination; this is done in a really stupid way that will possibly break if looked at wrong, but it'll work for now
// If this shits itself, go bother Winter to implement proper CFG + basic block detection
public IEnumerable<Instruction> GetFunctionInstructions(IntPtr addr)
{
var fileOffset = addr - TextSectionVirtualAddress - ModuleBaseAddress;
var codeReader = new MappedCodeReader(TextSection, fileOffset);
var decoder = Decoder.Create(64, codeReader, (ulong)addr.ToInt64());
do
{
decoder.Decode(out var instr);
// Yes, this is catastrophically bad, but it works for some cases okay
if (instr.Mnemonic == Mnemonic.Int3)
break;
yield return instr;
} while (true);
}
public void Dispose()
{
TextSection.Dispose();
File.Dispose();
}
}

View file

@ -16,6 +16,8 @@ public unsafe class ResourceLoader : IDisposable, IService
private readonly ResourceService _resources;
private readonly FileReadService _fileReadService;
private readonly TexMdlService _texMdlService;
private readonly PapHandler _papHandler;
private ResolveData _resolvedData = ResolveData.Invalid;
@ -30,6 +32,29 @@ public unsafe class ResourceLoader : IDisposable, IService
_resources.ResourceHandleIncRef += IncRefProtection;
_resources.ResourceHandleDecRef += DecRefProtection;
_fileReadService.ReadSqPack += ReadSqPackDetour;
_papHandler = new PapHandler(PapResourceHandler);
_papHandler.Enable();
}
private int PapResourceHandler(void* self, byte* path, int length)
{
Utf8GamePath.FromPointer(path, out var gamePath);
var (resolvedPath, _) = _incMode.Value
? (null, ResolveData.Invalid)
: _resolvedData.Valid
? (_resolvedData.ModCollection.ResolvePath(gamePath), _resolvedData)
: ResolvePath(gamePath, ResourceCategory.Chara, ResourceType.Pap);
if (!resolvedPath.HasValue || !Utf8GamePath.FromString(resolvedPath.Value.FullName, out var utf8ResolvedPath))
{
return length;
}
NativeMemory.Copy(utf8ResolvedPath.Path.Path, path, (nuint)utf8ResolvedPath.Length);
path[utf8ResolvedPath.Length] = 0;
return utf8ResolvedPath.Length;
}
/// <summary> Load a resource for a given path and a specific collection. </summary>
@ -84,6 +109,7 @@ public unsafe class ResourceLoader : IDisposable, IService
_resources.ResourceHandleIncRef -= IncRefProtection;
_resources.ResourceHandleDecRef -= DecRefProtection;
_fileReadService.ReadSqPack -= ReadSqPackDetour;
_papHandler.Dispose();
}
private void ResourceHandler(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path,

View file

@ -23,10 +23,10 @@
<DefineConstants>PROFILING;</DefineConstants>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Mods\Subclasses\**" />
<EmbeddedResource Remove="Mods\Subclasses\**" />
<None Remove="Mods\Subclasses\**" />
<ItemGroup>
<Compile Remove="Mods\Subclasses\**" />
<EmbeddedResource Remove="Mods\Subclasses\**" />
<None Remove="Mods\Subclasses\**" />
</ItemGroup>
<ItemGroup>
@ -68,6 +68,10 @@
<HintPath>$(DalamudLibPath)Newtonsoft.Json.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Iced">
<HintPath>$(DalamudLibPath)Iced.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="OtterTex.dll">
<HintPath>lib\OtterTex.dll</HintPath>
</Reference>
@ -79,6 +83,7 @@
<PackageReference Include="SharpCompress" Version="0.33.0" />
<PackageReference Include="SharpGLTF.Core" Version="1.0.0-alpha0030" />
<PackageReference Include="SharpGLTF.Toolkit" Version="1.0.0-alpha0030" />
<PackageReference Include="PeNet" Version="4.0.5" />
</ItemGroup>
<ItemGroup>