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()) { 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(); 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 ? "" : 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; } }