-
Notifications
You must be signed in to change notification settings - Fork 213
Snippets
import "frida-il2cpp-bridge";
Il2Cpp.perform(() => {
// code here
console.log(Il2Cpp.unityVersion);
});
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();
// the file name is overridden: /<default_path>/custom_file_name.cs
Il2Cpp.dump("custom_file_name");
// the file name and directory path are overridden: /i/can/write/to/this/path/custom_file_name.cs
Il2Cpp.dump("custom_file_name", "/i/can/write/to/this/path");
});
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.
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
}
import "frida-il2cpp-bridge";
Il2Cpp.perform(() => {
const SystemString = Il2Cpp.corlib.class("System.String");
// it traces method calls and returns
Il2Cpp.trace()
.classes(SystemString)
.filterMethods(method => method.isStatic && method.returnType.equals(SystemString.type) && !method.isExternal)
.and()
.attach();
// detailed trace, it traces method calls and returns and it reports every parameter
Il2Cpp.trace(true)
.assemblies(Il2Cpp.corlib.assembly)
.filterClasses(klass => klass.namespace == "System")
.filterParameters(param => param.type.equals(SystemString) && param.name == "msg")
.and()
.assemblies(Il2Cpp.corlib.assembly)
.filterMethods(method => method.name.toLowerCase().includes("begin"))
.and()
.attach();
});
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 and
. In this way, you can combine multiple requirements without writing complex filters (see the third example).
After and
you can start over or you can start the actual tracing calling attach
.
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.corlib.class("System.String").method("Equals");
Il2Cpp.trace().methods(Equals).and().attach();
// I know, this is verbose
});
There are three already defined strategies you can follow in order to trace methods.
-
Il2Cpp.trace(false)
:0x01a2f3e4 ┌─System.String::Concat 0x01a3cfbc │ ┌─System.String::FastAllocateString 0x01a3cfbc │ └─System.String::FastAllocateString 0x01a3f118 │ ┌─System.String::FillStringChecked 0x01a3f118 │ └─System.String::FillStringChecked 0x01a3f118 │ ┌─System.String::FillStringChecked 0x01a3f118 │ └─System.String::FillStringChecked 0x01a2f3e4 └─System.String::Concat 0x01a41f60 ┌─System.String::Replace 0x01a4346c │ ┌─System.String::IndexOfUnchecked 0x01a4346c │ └─System.String::IndexOfUnchecked 0x01a3cfbc │ ┌─System.String::FastAllocateString 0x01a3cfbc │ └─System.String::FastAllocateString 0x01a41f60 └─System.String::Replace
-
Il2Cpp.trace(true)
(a LOT slower thenfalse
!):0x01a2f3e4 ┌─System.String::Concat(str0 = "Creating AndroidJavaClass from ", str1 = "com.unity3d.player.UnityPlayer"); 0x01a3cfbc │ ┌─System.String::FastAllocateString(length = 61); 0x01a3cfbc │ └─System.String::FastAllocateString = "Creating AndroidJavaClass from com.unity3d.player.UnityPlayer"; 0x01a3f118 │ ┌─System.String::FillStringChecked(dest = "Creating AndroidJavaClass from com.unity3d.player.UnityPlayer", destPos = 0, src = "Creating AndroidJavaClass from "); 0x01a3f118 │ └─System.String::FillStringChecked; 0x01a3f118 │ ┌─System.String::FillStringChecked(dest = "Creating AndroidJavaClass from com.unity3d.player.UnityPlayer", destPos = 31, src = "com.unity3d.player.UnityPlayer"); 0x01a3f118 │ └─System.String::FillStringChecked; 0x01a2f3e4 └─System.String::Concat = "Creating AndroidJavaClass from com.unity3d.player.UnityPlayer"; 0x01a41f60 ┌─System.String::Replace(this = com.unity3d.player.UnityPlayer, oldChar = 46, newChar = 47); 0x01a4346c │ ┌─System.String::IndexOfUnchecked(this = com.unity3d.player.UnityPlayer, value = 46, startIndex = 0, count = 30); 0x01a4346c │ └─System.String::IndexOfUnchecked = 3; 0x01a3cfbc │ ┌─System.String::FastAllocateString(length = 30); 0x01a3cfbc │ └─System.String::FastAllocateString = "com/unity3d/player/UnityPlayer"; 0x01a41f60 └─System.String::Replace = "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.assembly("mscorlib").image;
const SystemType = mscorlib.class("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.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 SystemObject = Il2Cpp.corlib.class("System.Object");
const SystemInt32 = Il2Cpp.corlib.class("System.Int32");
// +++ inflating a generic class +++
const GenericList = Il2Cpp.corlib.class("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 = Il2Cpp.corlib.class("System.Array").method("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 ---
});
import "frida-il2cpp-bridge";
Il2Cpp.perform(() => {
const mscorlib = Il2Cpp.domain.assembly("mscorlib").image;
const SystemString = mscorlib.class("System.String");
const IsNullOrEmpty = mscorlib.class("System.String").method<boolean>("IsNullOrEmpty");
const MemberwiseClone = mscorlib.class("System.Object").method("MemberwiseClone");
const string = Il2Cpp.string("Hello, il2cpp!");
// static method invocation, it will return false
const result0 = IsNullOrEmpty.invoke(string);
// instance method invocation, it will return true
const result1 = string.object.method<boolean>("Contains").invoke(Il2Cpp.string("il2cpp"));
//
IsNullOrEmpty.implementation = function (value: Il2Cpp.String): boolean {
value.content = "!"; // <--- onEnter
// <--- onEnter
const result = this.method<boolean>("IsNullOrEmpty").invoke(value);
// <--- onLeave
console.log(result); // <--- onLeave
return !!result; // <--- onLeave
};
//
MemberwiseClone.implementation = function (): Il2Cpp.Object {
// `this` is a "System.Object", because MemberwiseClone is a System.Object method
// `originalInstance` can be any type
const originalInstance = new Il2Cpp.Object(this.handle);
// not cloning!
return this as Il2Cpp.Object;
};
});
-
You can invoke any method using
invoke
(this is just an abstraction overNativeFunction
). -
You can replace and intercept any method implementation using
implementation
(this is just an abstraction overInterceptor.replace
andNativeCallback
). If the method is static,this
will be aIl2Cpp.Class
, orIl2Cpp.Object
otherwise: the instance is artificially down-casted to the method declaring class.
Some other examples:// System.Int32 GetByteCount(System.Char[] chars, System.Int32 index, System.Int32 count, System.Boolean flush); GetByteCount.implementation = function (chars: Il2Cpp.Array<number>, index: number, count: number, flush: boolean): number {}; // System.Boolean InternalFallback(System.Char ch, System.Char*& chars); InternalFallback.implementation = function (ch: number, chars: Il2Cpp.Reference<Il2Cpp.Pointer<number>>): boolean {};
-
You can get the overload of any method specifying the parameter type names:
// mscorlib.dll class Locale : System.Object { static System.String GetText(System.String msg); // 0x00a281b0 static System.String GetText(System.String fmt, System.Object[] args); // 0x00a281c0 }
const Locale = Il2Cpp.corlib.class("Locale"); const GetText = Locale.method("GetText"); // <--- 0x00a281b0 const GetText2 = GetText.overload("System.String", "System.Object[]"); // <--- 0x00a281c0