-
Notifications
You must be signed in to change notification settings - Fork 217
Changelog
This is a small release to fix a few bugs.
Diff: https://github.com/vfsfitvnm/frida-il2cpp-bridge/compare/v0.9.0...v0.9.1
▶ Bind this
to a Il2Cpp.ValueType
Historically, when overriding a method implementation, this
was either a Il2Cpp.Class
(in case of static methods) or a Il2Cpp.Object
(in case of instance methods).
In the following example, we are trying to hook UnityEngine.Rect::get_x
(an instance method of a struct), so we write:
Il2Cpp.perform(() => {
const Rect = Il2Cpp.domain.assembly("UnityEngine.CoreModule").image.class("UnityEngine.Rect");
Rect.method<number>("get_x").implementation = function (this: Il2Cpp.Object): number {
console.log(this);
return this.method<number>("get_x").invoke();
};
});
However, if you run this code on Unity < 2017.x.x (?) or > 2021.2.0, you'll get an access violation error. In fact, in those versions, struct methods are expected to receive structs (or unboxed objects), not pure objects. (struct methods are those methods that are definited within a struct class definition!)
To address this issue, struct methods now bind this
to Il2Cpp.ValueType
, always:
Il2Cpp.perform(() => {
const Rect = Il2Cpp.domain.assembly("UnityEngine.CoreModule").image.class("UnityEngine.Rect");
// @ts-ignore
Rect.method<number>("get_x").implementation = function (this: Il2Cpp.ValueType): number {
console.log(this);
return this.method<number>("get_x").invoke();
};
});
Keep in mind the difference between an unboxed object and a object is the former is just a pointer to raw data, whereas the latter is a pointer to an header (containing a reference to its class) plus the raw data.
In code:
Il2Cpp.perform(() => {
const Rect = Il2Cpp.domain.assembly("UnityEngine.CoreModule").image.class("UnityEngine.Rect");
const rect = Rect.new();
assert(rect.handle.add(Il2Cpp.Object.headerSize).equals(rect.unbox().handle));
});
In other words, unboxing an object simply returns its handle plus the object header size.
▶ Use @ts-ignore
If you override a method implementation and specify types for any of its parameters, you now must tell the TypeScript compiler to stop complaining.
Setters cannot have generics, and TypeScript complains if we try to narrow down an union type, sadly. And I don't want to use any
.
▶ Add Il2Cpp.Class::isStruct
A property to determine whether the class is a struct
.
Il2Cpp.perform(() => {
const MyClass: Il2Cpp.Class = ...;
console.log(MyClass.isStruct); // shortand for MyClass.isValueType && !MyClass.isEnum
});
▶ Add Il2Cpp.ValueType::method
You can now invoke methods against value types, without having to box them! Keep in mind only struct methods can be invoked without boxing.
Il2Cpp.perform(() => {
const Vector2 = Il2Cpp.domain.assembly("UnityEngine.CoreModule").image.class("UnityEngine.Vector2");
const vec = Vector2.new().unbox(); // unbox immediately
vec.method(".ctor").invoke(12, 34); // no boxing!
console.log(vec); // no boxing!
});
▶ Add Il2Cpp.Object::monitor
An object to incapsulate synchronization-related methods. These methods are rarely used, so I decided to move them away from Il2Cpp.Object
to avoid clogging more significant APIs.
Il2Cpp.perform(() => {
const object = Il2Cpp.string("hello").object;
// old
object.enter();
// new
object.monitor.enter();
});
▶ Drop Il2Cpp.Object::enter
Superseded by Il2Cpp.Object.Monitor::enter
Il2Cpp.perform(() => {
const object = Il2Cpp.string("hello").object;
// old
object.enter();
// new
object.monitor.enter();
});
▶ Drop Il2Cpp.Object::exit
Superseded by Il2Cpp.Object.Monitor::exit
▶ Drop Il2Cpp.Object::pulse
Superseded by Il2Cpp.Object.Monitor::pulse
▶ Drop Il2Cpp.Object::pulseAll
Superseded by Il2Cpp.Object.Monitor::pulseAll
▶ Drop Il2Cpp.Object::tryEnter
Superseded by Il2Cpp.Object.Monitor::tryEnter
▶ Drop Il2Cpp.Object::tryWait
Superseded by Il2Cpp.Object.Monitor::tryWait
▶ Drop Il2Cpp.Object::wait
Superseded by Il2Cpp.Object.Monitor::wait
▶ Add IL2CPP_MODULE_NAME
A variable to override the default module name where IL2CPP exports should be searched into. It's for advanced use cases.
(gloablThis as any).IL2CPP_MODULE_NAME = "whatever";
Il2Cpp.perform(() => {
// ...
});
▶ Add IL2CPP_UNITY_VERSION
A variable to set/override the Unity version for when it cannot be detected automatically. It's for advanced use cases.
(gloablThis as any).IL2CPP_UNITY_VERSION = "2019.2.3f1";
Il2Cpp.perform(() => {
console.log(IL2CPP_UNITY_VERSION); // 2019.2.3f1
});
▶ Add IL2CPP_EXPORTS
A variable to specify IL2CPP exports providers. It's for advanced use cases, for instance when some IL2CPP exports are stripped/hidden/renamed and cannot be detected automatically.
declare global {
let IL2CPP_EXPORTS: Record<string, () => NativePointer>;
}
IL2CPP_EXPORTS = {
il2cpp_image_get_class: () => Il2Cpp.module.base.add(0x1204c),
il2cpp_class_get_parent: () => {
return Memory.scanSync(Il2Cpp.module.base, Il2Cpp.module.size, "2f 10 ee 10 34 a8")[0].address;
},
};
Il2Cpp.perform(() => {
// ...
});
▶ Improve and fix some annoyances related to native modules hooking
▶ Add Il2Cpp.Class::generics
A property to get the generics of a class, so that you can quickly inflate another Il2Cpp.Class
or a Il2Cpp.Method
using the same types.
Thanks, @Flechaa!
Il2Cpp.perform(() => {
const SystemObject = Il2Cpp.corlib.class("System.Object");
const SystemAction = Il2Cpp.corlib.class("System.Action`1");
const SystemActionObject = SystemAction.inflate(SystemObject);
console.log(SystemObject.generics); // []
console.log(SystemAction.generics); // [T]
console.log(SystemActionObject.generics); // [System.Object]
});
▶ Add Il2Cpp.Method::generics
A property to get the generics of a method, so that you can quickly inflate another Il2Cpp.Method
or a Il2Cpp.Class
using the same types.
Thanks, @Flechaa!
Il2Cpp.perform(() => {
const SystemArray = Il2Cpp.corlib.class("System.Array");
console.log(SystemArray.method("CreateInstance").generics); // []
console.log(SystemArray.method("AsReadOnly").generics); // [T]
console.log(SystemArray.method("AsReadOnly").inflate(SystemArray).generics); // [System.Array]
});
▶ Drop Il2Cpp.Class::genericParametersLength
Superseded by Il2Cpp.Class::generics
.
▶ Drop Il2Cpp.Method::genericParametersLength
Superseded by Il2Cpp.Method::generics
.
Here we are with a new, small release.
▶ Early instrumentation
You might have been in a scenario were you wanted to trace a set of methods during the startup of the target process. Unfortunately, due to how Il2Cpp::perform
and the internal Il2Cpp::initialize
were designed, you might not have found what you were looking for.
In fact, Il2Cpp::perform
always tried to execute the given block on the thread it was called from (e.g. the Frida's one). It means that it was not possible to execute the given block in a blocking fashion: the main thread was able to perform possibly a lot of method calls and frida-il2cpp-bridge
had no chance to catch them.
I'm thrilled to announce early instrumentation is now possible - just pass "main"
to Il2Cpp::perform
:
Il2Cpp.perform(() => {
// code here
}, "main");
The "main"
flag has a double function:
- If IL2CPP has not been initialized yet, your code is executed right after
il2cpp_init
, so that the rest of the original execution flow is resumed only after your callback. For instance, you now have all the time to set up yourIl2Cpp::Tracer
. - If IL2CPP has already been initialized, your code is just scheduled on the main thread - i.e. a shorthand for:
Il2Cpp.perform(() => { Il2Cpp.mainThread.schedule(() => { // code here }); }, "free");
In other words, the "main"
flag ensures the given callback is executed on the main thread.
▶ Better Il2Cpp::perform
flag names
The second optional Il2Cpp::perform
parameter now accepts:
-
"free"
(it was"always"
) immediately detaches the caller thread after executing the given block; -
"bind"
(it was"lazy"
) (default) detaches the caller thread only when the script is going to be reloaded or destroyed -
"leak"
(it was"never"
) never detaches the caller thread explicitly. -
"main"
(new): ensures the given callback is executed on the main thread.
▶ Drop Il2Cpp.Thread::schedule::delayMs
It relied on Frida's setTimeout
, which schedules the callback on the Frida's thread. It didn't play well with our threading model.
This is another important release!
▶ JS vs C
Internally, frida-il2cpp-bridge
used a CModule
to implement missing or additional IL2CPP internals (let's refer to these ones as internal fallbacks).
This solution allowed the codebase to not be aware whether a specific IL2CPP internal was present or not, and had a positive impact on code consistency.
However, this solution had a practical drawback and mental one.
The former was the impossibility to use any of the internal fallbacks before, well, the creation of the CModule
itself! This is obvious (you can't enter a room without opening its door, can you?), but the nasty thing was some internal fallbacks had a dependency on another internal fallbacks, but the creation of the CModule
is atomic, so I couldn't use some internal fallbacks before having other internal fallbacks compiled.
Long story short: I couldn't implement 20f692316ce3da838447d103386084eaed3f6909 and 7c766ecc8da6a635a7eabf1c53d530e1617e92b2.
The latter is, whenever it came to add a new property to a IL2CPP struct (e.g. Il2Cpp.Domain::object
), I didn't know if it would have made sense to implement it in C (e.g. 43d36599e668808657a7b23353d548b5e5f4b461).
This resulted in a cognitive load spike!
The only advantage of using C is speed. Like, 20x/50x improvement over JS. I'll admit: having Il2Cpp::dump
complete in one fifth of the time and Il2Cpp::backtracer
initialize in one twentieth of the time was very tempting.
However, after writing a proof of concept, I realized it wasn't worth it.
Eventually, my final decision was to implement the internal fallbacks in JS (exception made for Il2Cpp::MemorySnapshot
internals): 20645bb70cf8e77862addaafee779e8b11abbe76.
▶ New threading model
Il2Cpp::perform
does four things jobs: wait for IL2CPP to be loaded and initialized, ensure the caller thread to IL2CPP, execute the given block and then detach the caller thread it has been previously attached by frida-il2cpp-bridge
.
In case you were wondering, I don't exactly know what "attaching to IL2CPP" really does, but it is necessary to do some IL2CPP related stuff. Moreover, when a thread is attached, the garbage collector tracks the resources the thread allocates, so that they can be freed when the thread detaches.
However, the implementation of Il2Cpp::perform
has a major drawback.
Let's consider this snippet:
Il2Cpp.perform(() => {
const Timer = Il2Cpp.corlib.class("System.Threading.Timer").initialize();
const TimerCallback = Il2Cpp.corlib.class("System.Threading.TimerCallback");
let i = 0;
const callback = Il2Cpp.delegate(TimerCallback, () => console.log(">", i++));
console.log("callback @", callback.handle);
const timer = Timer.alloc();
timer.method(".ctor").invoke(callback, NULL, 0, 1000);
Script.bindWeak(globalThis, () => timer.method("Dispose").invoke());
});
If you are lazy, this is equivalent to the following:
let i = 0;
setInterval(() => console.log(">", i++), 1000);
Can you spot the issue?
The snippet is indeed correct, but it possibly makes the process crash.
How so? The problem here is Il2pp::perform
itself. In fact, the following line
() => timer.method("Dispose").invoke();
is invoked when the Frida thread isn't attached to IL2CPP anymore. Sure, you could wrap it inside another Il2Cpp::perform
:
Script.bindWeak(globalThis, () => Il2Cpp.perform(() => timer.method("Dispose").invoke()));
But it still doesn't prevent the garbage collector from eagerly freeing timer
(well, in this specific scenario, it's unlikely to happen).
The same applies to the callbacks passed to setImmediate
, setTimeout
and setIntveral
: they are always executed off IL2CPP.
Il2Cpp.perform(() => {
console.log(">", Il2Cpp.currentThread != null);
setTimeout(() => console.log(">>", Il2Cpp.currentThread != null));
});
// output:
// > true
// >> false
Another important drawback of this approach is it wasn't quite nice to interact with IL2CPP from the Frida's REPL.
Now, Il2Cpp::perform
takes a second (optional) parameter, a string that identifies the detach strategy:
-
"always"
immediately detaches the caller thread after executing the given block (this the classic behaviour); -
"lazy"
detaches the caller thread only when the script is going to be reloaded or destroyed (this is now the default behaviour); -
"never"
never detaches the caller thread explicitly.
"never"
causes the native Il2Cpp::Thread
struct to be freed only when the underlying native thread is freed as well. For instance, in the context where you are prototyping a Frida script (so that you save and reload the script frequently to apply changes), it means Il2Cpp.Thread::managedId
doesn't change across script realods.
Now, the previous snippet has the following outcome:
// > true
// >> true
▶ Tracing
Some major refactorings were made to Il2Cpp::Tracer
.
Firstly, tracers are now thread-specific - the default intercepted thread is the main one.
Secondly, tracers are now not verbose by default: it means that they won't print the exact same call stack twice!
Il2Cpp.perform(() => {
Il2Cpp.trace()
.thread(Il2Cpp::Thread) // optional, defaults to Il2Cpp.mainThread
.verbose(true | false) // optional, defaults to false
.assemblies(Il2Cpp.domain.assembly("Assembly-CSharp"))
.and()
.attach();
});
Thirdly, tracers now have appliers. An applier is just an jerkish OOP concept - you can read the commit 00865d3b1887709e97ab586e200ec01893ab479c to learn more - but the essential impact is the tracer-specific customization happens outside Il2Cpp::Tracer
, so it is more flexible.
For instance:
Il2Cpp.perform(() => {
// before
Il2Cpp.trace()
.parameters(true | false)
.assemblies(Il2Cpp.domain.assembly("Assembly-CSharp"))
.and()
.attach();
// now
Il2Cpp.trace(true | false) // defaults to false
.assemblies(Il2Cpp.domain.assembly("Assembly-CSharp"))
.and()
.attach();
});
Il2Cpp.perform(() => {
// before
Il2Cpp.backtrace()
.strategy("fuzzy" | "accurate")
.verbose(false)
.assemblies(Il2Cpp.domain.assembly("Assembly-CSharp"))
.and()
.attach();
// now
Il2Cpp.backtrace(Backtracer.FUZZY | Backtracer.ACCURATE) // defaults to undefined
.assemblies(Il2Cpp.domain.assembly("Assembly-CSharp"))
.and()
.attach();
});
I strongly suggest you to use TypeScript with a language server, so you can easily inspect what you can or cannot call now.
▶ Make Il2Cpp.Class::initialize
return this
Il2Cpp.perform(() => {
// old
const SystemReflectionModule = Il2Cpp.corlib.class("System.Reflection.Module");
SystemReflectionModule.initialize();
// new, better
const SystemReflectionModule = Il2Cpp.corlib.class("System.Reflection.Module").initialize();
});
▶ Fix Il2Cpp.Thread::id
on Windows (#195)
▶ Rename Il2Cpp::Api
to Il2Cpp::api
It is now an object literal.
▶ Rename Il2Cpp.Class::valueSize
to Il2Cpp.Class::valueTypeSize
Il2Cpp.perform(() => {
const SystemString = Il2Cpp.corlib.class("System.String");
// old
SystemString.valueSize;
// new
SystemString.valueTypeSize;
});
▶ Drop Il2Cpp::Runtime
It had no practical use cases.
▶ Add Il2Cpp.Backtracer
Similar to Il2Cpp.Tracer
, but it prints the method backtrace instead, i.e. a stack of IL2CPP methods.
This is still a very experimental feature, and slows down real large applications. Don't attach it to a whole assembly!
Il2Cpp.perform(() => {
Il2Cpp.backtrace() // creates a Il2Cpp.Bactracer instance
.strategy("fuzzy") // can be either "fuzzy" or "accurate"
.verbose(false) // false to avoid printing the same stack twice
.assemblies(Il2Cpp.domain.assembly("Assembly-CSharp"))
.and()
.attach();
});
▶ Add Il2Cpp::mainThread
A shorthand for Il2Cpp.attachedThreads[0]
.
▶ Add Il2Cpp::domain
A property to access the application domain.
Il2Cpp.perform(() => {
// old
const _ = Il2Cpp.Domain.assembly(...);
// new
const _ = Il2Cpp.domain.assembly(...);
});
▶ Add Il2Cpp::corlib
A property to access the COR library image.
Il2Cpp.perform(() => {
// old
const _ = Il2Cpp.Image.corlib.class(...);
// new
const _ = Il2Cpp.corlib.class(...);
});
▶ Add Il2Cpp::delegate
An helper function for painlessly creating delegates. It returns a Il2Cpp.Object
of the given delegate class.
Il2Cpp.perform(() => {
const SystemString = Il2Cpp.corlib.class("System.String");
const SystemAction = Il2Cpp.corlib.class("System.Action`1").inflate(SystemString);
const delegate: Il2Cpp.Object = Il2Cpp.delegate(SystemAction, (string: Il2Cpp.String) => {
console.log(`Whoa, ${string.content}!`);
});
delegate.method("Invoke").invoke(Il2Cpp.string("it works"));
});
// output:
// Whoa, it works!
▶ Add Il2Cpp::array
An helper function for creating arrays.
Il2Cpp.perform(() => {
// old
const _ = Il2Cpp.Array.from(...);
// new
const _ = Il2Cpp.array(...);
});
▶ Add Il2Cpp::string
An helper function for creating strings.
Il2Cpp.perform(() => {
// old
const _ = Il2Cpp.String.from(...);
// new
const _ = Il2Cpp.string(...);
});
▶ Add Il2Cpp::reference
An helper function for creating references.
Il2Cpp.perform(() => {
// old
const _ = Il2Cpp.Reference.to(...);
// new
const _ = Il2Cpp.reference(...);
});
▶ Add Il2Cpp::memorySnapshot
An helper function for creating a memory snapshot without worrying to free it afterwards.
Il2Cpp.perform(() => {
const SystemString = Il2Cpp.corlib.class("System.String");
const strings = Il2Cpp.memorySnapshot(_ => _.objects).filter(Il2Cpp.is(SystemString));
});
▶ Add Il2Cpp::is
An helper function for creating a filter for Il2Cpp.Object
based on the assignability of a class.
Il2Cpp.perform(() => {
const klass: Il2Cpp.Class = ...;
const objects: Il2Cpp.Object[] = ...;
// old
const strings = objects.filter(Il2Cpp.Filtering.is(klass));
// new
const _ = objects.filter(Il2Cpp.is(klass));
});
▶ Add Il2Cpp::isExactly
An helper function for creating a filter for Il2Cpp.Object
based on the equality of a class.
Il2Cpp.perform(() => {
const klass: Il2Cpp.Class = ...;
const objects: Il2Cpp.Object[] = ...;
// old
const strings = objects.filter(Il2Cpp.Filtering.isExactly(klass));
// new
const _ = objects.filter(Il2Cpp.isExactly(klass));
});
▶ Add Il2Cpp::application
An object literal containing possibly useful information about the application.
Il2Cpp.perform(() => {
// old
console.log(Il2Cpp.applicationIdentifier);
console.log(Il2Cpp.applicationDataPath);
console.log(Il2Cpp.applicationVersion);
// new
console.log(Il2Cpp.application.identifier);
console.log(Il2Cpp.application.dataPath);
console.log(Il2Cpp.application.version);
});
▶ Add Il2Cpp::gc
An object literal containing garbage collector related functions and properties.
Il2Cpp.perform(() => {
// old
Il2Cpp.GC.choose(...);
// new
Il2Cpp.gc.choose(...);
});
▶ Add Il2Cpp.Class::fullName
A convenient accessor for having the namespace of a class concatenated to its name. It may return a different result than Il2Cpp.Type::name
.
Il2Cpp.perform(() => {
const SystemAction = Il2Cpp.corlib.class("System.Action`1");
console.log(SystemAction.fullName);
console.log(SystemAction.type.name);
});
// output:
// System.Action`1
// System.Action<T>
▶ Add Il2Cpp.Thread::managedId
Returns the C# (?) thread id; the main thread is expected to have 1
.
Il2Cpp.perform(() => {
console.log(Il2Cpp.mainThread.managedId); // probably 1
});
▶ Fix Il2Cpp.gc::choose
Previously, it made the application crash on Unity 2021.1 and up; now should be fixed, or it shouldn't happen frequently at least. Please report a bug with a reproducible example if the issue persists.
▶ Fix Il2Cpp.Thread::id
Previously, it wasn't working on Windows due to an access violation error. However, I couldn't test its correctness: please let me know if the following snippet works (or not).
Il2Cpp.perform(() => {
// should be the same!
console.log(Il2Cpp.currentThread?.id, Process.getCurrentThreadId());
});
▶ Rename Il2Cpp.GC.Handle
to Il2Cpp.GCHandle
A consequence of adding Il2Cpp::gc
.
▶ Drop Il2Cpp::scheduleOnInitializerThread
It was a temporary and wobbly solution. Superseded by Il2Cpp.mainThread::schedule
.
▶ Drop Il2Cpp.Image::corlib
Superseded by Il2Cpp::corlib
.
▶ Drop Il2Cpp.Array::from
Superseded by Il2Cpp::array
.
▶ Drop Il2Cpp.String::from
Superseded by Il2Cpp::string
.
▶ Drop Il2Cpp.Reference::to
Superseded by Il2Cpp::reference
.
▶ Drop Il2Cpp::applicationIdentifier
Superseded by Il2Cpp.application::identifier
.
▶ Drop Il2Cpp::applicationDataPath
Superseded by Il2Cpp.application::dataPath
.
▶ Drop Il2Cpp::applicationVersion
Superseded by Il2Cpp.application::version
.
▶ Drop Il2Cpp::GC
Superseded by Il2Cpp::gc
.
▶ Drop Il2Cpp.Filtering::is
Superseded by Il2Cpp::is
.
▶ Drop Il2Cpp.Filtering::isExactly
Superseded by Il2Cpp::isExactly
.