Files
Radicade/Tools/AugustInterop/LiteralFinder.cs
T
2026-10-16 23:33:13 -07:00

135 lines
6.5 KiB
C#

using System.Reflection;
using System.Reflection.PortableExecutable;
using Iced.Intel;
// Find the il2cpp methods that reference a given string literal.
//
// Neither the bytes nor the index of a literal appear directly in the code: on
// metadata v27+ the code loads a pointer from a metadata-usage slot, and statically
// that slot holds the encoded usage token `(type << 29) | (index << 1) | 1` with
// type 5 for string literals. So the search is:
// 1. resolve the literal's index from its text,
// 2. scan the image for the 8-byte token (this finds the slots),
// 3. scan the executable sections for instructions whose rip-relative operand
// lands on one of those slots. The displacement is the tail of such an
// instruction, so a cheap arithmetic pre-check narrows candidates and Iced
// then confirms by decoding from the candidate's start.
internal static class LiteralFinder
{
const ulong TokenBase = 5UL << 29;
static object Prop(object o, string n) => o.GetType().GetProperty(n)!.GetValue(o)!;
sealed class LineOutput : FormatterOutput
{
public string Value = "";
public override void Write(string text, FormatterTextKind kind) => Value += text;
}
public static void Run(object main, object metadata, string binaryPath, string needle)
{
var defs = (System.Collections.IEnumerable)metadata.GetType().GetField("methodDefs")!.GetValue(metadata)!;
var methods = new List<(object Method, ulong Address)>();
foreach (var m in defs.Cast<object>())
{
var address = (ulong)Prop(m, "MethodPointer");
if (address != 0) methods.Add((m, address));
}
methods.Sort((a, b) => a.Address.CompareTo(b.Address));
var table = metadata.GetType().GetField("stringLiterals",
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)!.GetValue(metadata) as Array;
var getLiteralText = metadata.GetType().GetMethod("GetStringLiteralFromIndex")!;
var tokens = new List<ulong>();
for (int i = 0; i < (table?.Length ?? 0); i++)
{
var text = (string)getLiteralText.Invoke(metadata, new object[] { (uint)i })!;
if (!text.Contains(needle, StringComparison.Ordinal)) continue;
// Both low-bit spellings resolve to the same index downstream.
tokens.Add(TokenBase | ((ulong)i << 1) | 1);
tokens.Add(TokenBase | ((ulong)i << 1));
Console.WriteLine($" literal #{i} = \"{text}\" token 0x{TokenBase | ((ulong)i << 1):X}");
}
if (tokens.Count == 0)
{
Console.WriteLine($"no literal contains \"{needle}\"");
return;
}
var bytes = File.ReadAllBytes(binaryPath);
using var pe = new PEReader(new MemoryStream(bytes));
var imageBase = pe.PEHeaders.PEHeader!.ImageBase;
// 1. slots: 8-byte tokens, so only probe 8-byte aligned positions.
var slots = new List<(string Section, ulong Address, ulong Value)>();
foreach (var section in pe.PEHeaders.SectionHeaders)
{
var size = Math.Min(section.SizeOfRawData, Math.Max(0, bytes.Length - section.PointerToRawData));
for (int i = section.PointerToRawData; i < section.PointerToRawData + size - 8; i += 8)
{
var value = BitConverter.ToUInt64(bytes, i);
if (!tokens.Contains(value)) continue;
var slot = imageBase + (ulong)section.VirtualAddress + (ulong)(i - section.PointerToRawData);
slots.Add((section.Name, slot, value));
Console.WriteLine($" slot 0x{slot:X} ({section.Name}, raw 0x{i:X}) = 0x{value:X}");
}
}
if (slots.Count == 0)
{
Console.WriteLine("no metadata-usage slot found");
return;
}
var slotSet = slots.Select(s => s.Address).ToHashSet();
// 2. references: pre-check the displacement, confirm by decoding from the start.
var formatter = new IntelFormatter();
int hits = 0, candidates = 0;
foreach (var section in pe.PEHeaders.SectionHeaders.Where(s => (s.SectionCharacteristics & SectionCharacteristics.MemExecute) != 0))
{
var rawToVa = (long)imageBase + section.VirtualAddress - section.PointerToRawData;
var size = Math.Min(section.SizeOfRawData, Math.Max(0, bytes.Length - section.PointerToRawData));
for (int i = section.PointerToRawData; i < section.PointerToRawData + size - 4; i++)
{
var disp = BitConverter.ToInt32(bytes, i);
var target = (ulong)((long)i + 4 + rawToVa + disp);
if (!slotSet.Contains(target)) continue;
candidates++;
for (int back = 0; back <= 7; back++)
{
var candidateRaw = i - back;
if (candidateRaw < section.PointerToRawData) break;
var reader = new ByteArrayCodeReader(bytes, candidateRaw, Math.Min(16, bytes.Length - candidateRaw));
var decoder = Decoder.Create(64, reader);
decoder.IP = (ulong)((long)candidateRaw + rawToVa);
decoder.Decode(out var instruction);
if (instruction.IsInvalid || !instruction.IsIPRelativeMemoryOperand) continue;
if (instruction.IPRelativeMemoryAddress != target) continue;
hits++;
var method = Nearest(methods, instruction.IP);
var name = method == null
? "<none>"
: Prop(Prop(method, "DeclaringType"), "Name") + "::" + Prop(method, "Name");
var offset = method == null ? 0 : instruction.IP - (ulong)Prop(method, "MethodPointer");
var output = new LineOutput();
formatter.Format(instruction, output);
Console.WriteLine($" REF 0x{instruction.IP:X} -> slot 0x{target:X} in {name} +0x{offset:X} [{output.Value}]");
break;
}
}
}
Console.WriteLine($"slots {slots.Count}; displacement candidates {candidates}; confirmed references {hits}");
}
static object? Nearest(List<(object Method, ulong Address)> methods, ulong address)
{
object? best = null;
foreach (var (method, addr) in methods)
{
if (addr > address) break;
best = method;
}
return best;
}
}