-
Notifications
You must be signed in to change notification settings - Fork 273
Snippets
import "frida-il2cpp-bridge";
Il2Cpp.perform(() => {
// code here
console.log(Unity.version);
});You import the global Il2Cpp object in the following way.
Before executing any Il2Cpp operation, the caller thread should be attached to the application domain; after the
execution, it should be detached. I said "should" because it's not mandatory, however you can bump into some abort
or access violation errors if you skip this step.
You can ensure this behavior wrapping your code inside a Il2Cpp.perform function - this wrapper also ensures any
initialization process has finished. Given so, this function is asynchronous because it may need to wait for Il2Cpp
module to load and initialize (il2cpp_init).
import "frida-il2cpp-bridge";
Il2Cpp.perform(() => {
// it will use default directory path and file name: /<default_path>/<default_name>.cs
Il2Cpp.dump()
.classes() // or methods
.build();
// the file name is overridden: /<default_path>/custom_file_name.cs
Il2Cpp.dump()
.fileName("custom_file_name")
.classes() // or methods
.build();
// the file name and directory path are overridden: /i/can/write/to/this/path/custom_file_name.cs
Il2Cpp.dump()
.directoryPath("/i/can/write/to/this/path")
.fileName("custom_file_name")
.classes() // or methods
.build()
});Ok, this is probably exaggerated, but I liked the new Il2Cpp.Tracer implementation so I wanted to apply it to Il2Cpp.Dumper too.
This operation may require a directory path (e.g. a place where the application can write to) and a file name. If not provided, the code will just guess them; however it's not guaranteed to succeed.
There are two possible configurations.
-
classeswill diplay every class like the following:class Mono.DataConverter.PackContext : System.Object { System.Byte[] buffer; // 0x10 System.Int32 next; // 0x18 System.String description; // 0x20 System.Int32 i; // 0x28 Mono.DataConverter conv; // 0x30 System.Int32 repeat; // 0x38 System.Int32 align; // 0x3c System.Void Add(System.Byte[] group); // 0x012ef4f0 System.Byte[] Get(); // 0x012ef6ec System.Void .ctor(); // 0x012ef78c }
-
methodswill produce a input for static analysis tools:0x012ef4f0 Mono.DataConverter.PackContext.Add 0x012ef6ec Mono.DataConverter.PackContext.Get 0x012ef78c Mono.DataConverter.PackContext..ctorFor more information, see ghidra script.
import "frida-il2cpp-bridge";
Il2Cpp.perform(() => {
const SystemString = Il2Cpp.Image.corlib.classes["System.String"];
// it traces method calls
Il2Cpp.trace()
.assemblies(Il2Cpp.Image.corlib.assembly, Il2Cpp.Domain.assemblies.System)
.filterMethods(method => method.isStatic && method.returnType.equals(SystemString.type) && !method.isExternal)
.commit()
.simple()
.build();
// it traces method calls and returns
Il2Cpp.trace()
.classes(SystemString)
.commit()
.full()
.build();
// full trace, it traces method calls and returns and it reports every value
Il2Cpp.trace()
.assemblies(Il2Cpp.Image.corlib.assembly)
.filterClasses(klass => klass.namespace == "System")
.filterParameters(param => param.type.equals(SystemString) && param.name == "msg")
.commit()
.assemblies(Il2Cpp.Image.corlib.assembly)
.filterMethods(method => method.name.toLowerCase().includes("begin"))
.commit()
.detailed()
.build();
// custom behaviour
Il2Cpp.trace()
.domain()
.filterMethods(method => method.parameterCount == 0)
.commit()
.special((target: Il2Cpp.Method) => {
const signature = `${target.name} (${target.parameterCount})`;
return {
onLeave(returnValue: Il2Cpp.Method.ReturnType) {
console.log(`[custom log] ${signature} ----> ${returnValue}`);
}
};
})
.build();
});Il2Cpp.Tracer follows the builder pattern in order to be flexible and, you know, this pattern is better than
a custom domain specific language. A new builder must be created via Il2Cpp.trace(). After that, you can start searching in the whole domain or in a set of assemblies, classes or methods. Then, you can apply a custom filter via filter*. You push all the methods that meet the requirements in a private field using commit. In this way, you can combine multiple requirements without writing complex filters (see the third example).
After commit you can start over or you can choose a generator (simple, full or detailed). A generator is just a function that produces the callbacks that will be called when a method is invoked. You can roll your own generator using special (see the last example). Finally, you begin the actual tracing calling build.
Uh, you don't need al this black magic? Do you just want to trace a single method?
import "frida-il2cpp-bridge";
Il2Cpp.perform(() => {
const Equals = Il2Cpp.Image.corlib.classes["System.String"].methods.Equals;
Il2Cpp.trace()
.methods(Equals)
.commit()
.full()
.build();
// I know, this is verbose
});There are three already defined strategies you can follow in order to trace methods. I will use onEnter and onLeave
words, however Il2Cpp.Tracer does not use Interceptor.attach, but a combination of Interceptor.replace and
NativeFunction (here's why).
-
simpleonly reportsonEntercalls.[il2cpp] 0x01a2f3e4 System.String.Concat [il2cpp] 0x01a3cfbc System.String.FastAllocateString [il2cpp] 0x01a3f118 System.String.FillStringChecked [il2cpp] 0x01a3f118 System.String.FillStringChecked [il2cpp] 0x01a41f60 System.String.Replace [il2cpp] 0x01a4346c System.String.IndexOfUnchecked [il2cpp] 0x01a3cfbc System.String.FastAllocateString -
fullreports bothonEnterandonLeavenicely.[il2cpp] 0x01a2f3e4 ┌─System.String.Concat [il2cpp] 0x01a3cfbc │ ┌─System.String.FastAllocateString [il2cpp] 0x01a3cfbc │ └─System.String.FastAllocateString [il2cpp] 0x01a3f118 │ ┌─System.String.FillStringChecked [il2cpp] 0x01a3f118 │ └─System.String.FillStringChecked [il2cpp] 0x01a3f118 │ ┌─System.String.FillStringChecked [il2cpp] 0x01a3f118 │ └─System.String.FillStringChecked [il2cpp] 0x01a2f3e4 └─System.String.Concat [il2cpp] 0x01a41f60 ┌─System.String.Replace [il2cpp] 0x01a4346c │ ┌─System.String.IndexOfUnchecked [il2cpp] 0x01a4346c │ └─System.String.IndexOfUnchecked [il2cpp] 0x01a3cfbc │ ┌─System.String.FastAllocateString [il2cpp] 0x01a3cfbc │ └─System.String.FastAllocateString [il2cpp] 0x01a41f60 └─System.String.Replace -
detailedreports bothonEnterandonLeavenicely, plus every printable value.[il2cpp] 0x01a2f3e4 ┌─System.String.Concat(System.String str0 = "Creating AndroidJavaClass from ", System.String str1 = "com.unity3d.player.UnityPlayer") [il2cpp] 0x01a3cfbc │ ┌─System.String.FastAllocateString(System.Int32 length = 61) [il2cpp] 0x01a3cfbc │ └─System.String.FastAllocateString System.String = "Creating AndroidJavaClass from com.unity3d.player.UnityPlayer" [il2cpp] 0x01a3f118 │ ┌─System.String.FillStringChecked(System.String dest = "Creating AndroidJavaClass from com.unity3d.player.UnityPlayer", System.Int32 destPos = 0, System.String src = "Creating AndroidJavaClass from ") [il2cpp] 0x01a3f118 │ └─System.String.FillStringChecked System.Void = undefined [il2cpp] 0x01a3f118 │ ┌─System.String.FillStringChecked(System.String dest = "Creating AndroidJavaClass from com.unity3d.player.UnityPlayer", System.Int32 destPos = 31, System.String src = "com.unity3d.player.UnityPlayer") [il2cpp] 0x01a3f118 │ └─System.String.FillStringChecked System.Void = undefined [il2cpp] 0x01a2f3e4 └─System.String.Concat System.String = "Creating AndroidJavaClass from com.unity3d.player.UnityPlayer" [il2cpp] 0x01a41f60 ┌─System.String.Replace(this = com.unity3d.player.UnityPlayer, System.Char oldChar = 46, System.Char newChar = 47) [il2cpp] 0x01a4346c │ ┌─System.String.IndexOfUnchecked(this = com.unity3d.player.UnityPlayer, System.Char value = 46, System.Int32 startIndex = 0, System.Int32 count = 30) [il2cpp] 0x01a4346c │ └─System.String.IndexOfUnchecked System.Int32 = 3 [il2cpp] 0x01a3cfbc │ ┌─System.String.FastAllocateString(System.Int32 length = 30) [il2cpp] 0x01a3cfbc │ └─System.String.FastAllocateString System.String = "com/unity3d/player/UnityPlayer" [il2cpp] 0x01a41f60 └─System.String.Replace System.String = "com/unity3d/player/UnityPlayer"
The output is nicely coloured so you won't get crazy when inspecting the console.
import "frida-il2cpp-bridge";
Il2Cpp.perform(() => {
const mscorlib = Il2Cpp.Domain.assemblies.mscorlib.image;
const SystemType = mscorlib.classes["System.Type"];
// +++ heap scanning using class descriptors +++
Il2Cpp.GC.choose(SystemType).forEach((instance: Il2Cpp.Object) => {
// instance.class.type.name == "System.Type"
});
// --- heap scanning using class descriptors ---
// +++ heap scanning using a memory snapshot +++
const snapshot = Il2Cpp.MemorySnapshot.capture();
snapshot.objects.filter(Il2Cpp.Filtering.IsExactly(SystemType)).forEach((instance: Il2Cpp.Object) => {
// instance.class.type.name == "System.Type"
});
snapshot.free();
// --- heap scanning using a memory snapshot ---
});You can "scan" the heap or whatever the place where the objects get allocated in to find instances of the given class. There are two ways of doing this: reading classes GC descriptors or taking a memory snapshot. However, I don't really know how they internally work, I read enough uncommented C++ source code for my taste.
Dealing with generics is problematic when the global-metadata.dat file is ignored. You can
gather the inflated version (if any) via Il2Cpp.Class.inflate and Il2Cpp.Method.inflate.
Reference types (aka objects) all shares the same code: it is easy to retrieve virtual address in this case. Value types (aka primitives and structs) does not share any code.
inflate will always return an inflated class or method (you must match the number of type arguments with the number of types you pass to inflate), but the returned value it's not
necessarily a class or method that has been implemented.
import "frida-il2cpp-bridge";
Il2Cpp.perform(() => {
const classes = Il2Cpp.Image.corlib.classes;
const SystemObject = classes["System.Object"];
const SystemInt32 = classes["System.Int32"];
// +++ inflating a generic class +++
const GenericList = classes["System.Collections.Generic.List<T>"];
// This class is shared among all reference types
const SystemObjectList = GenericList.inflate(SystemObject);
// This class is specific to System.Int32, because it's a value type
const SystemInt32List = GenericList.inflate(SystemInt32);
// --- inflating a generic class ---
// +++ inflating a generic method +++
const FindAll = classes["System.Array"].methods.FindAll;
// FindAll is a generic method, its virtual address is null
// This is the FindAll for every reference type
const SystemObjectFindAll = FindAll.inflate(SystemObject);
// This is the FindAll specific to System.Int32, because it's a value type
const SystemInt32FindAll = FindAll.inflate(SystemInt32);
// --- inflating a generic method ---
});