Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Additional interceptors: onEnter + onLeave #586

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions src/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@ namespace Il2Cpp {
return Il2Cpp.exports.free(pointer);
}

/** @internal */
export function read(pointer: NativePointer, type: Il2Cpp.Type): Il2Cpp.Field.Type {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was an interesting one. Parameters are different from eg Fields, in that they don't need to be dereferenced first. If you get a pointer that's meant to be a Il2Cpp.String, then no need to call ptr.readPointer() first.

Made this new behaviour backward compatible.

/**
* @param dereference If a pointer, dereference before reading? Usually `true`, but `false` for parameters for example.
*/
export function read(pointer: NativePointer, type: Il2Cpp.Type, derefPointer: boolean=true): Il2Cpp.Field.Type {
const dereferenced = derefPointer ? pointer.readPointer() : pointer;

switch (type.typeEnum) {
case Il2Cpp.Type.enum.boolean:
return !!pointer.readS8();
Expand Down Expand Up @@ -52,27 +56,27 @@ namespace Il2Cpp {
return pointer.readDouble();
case Il2Cpp.Type.enum.nativePointer:
case Il2Cpp.Type.enum.unsignedNativePointer:
return pointer.readPointer();
return dereferenced;
case Il2Cpp.Type.enum.pointer:
return new Il2Cpp.Pointer(pointer.readPointer(), type.class.baseType!);
return new Il2Cpp.Pointer(dereferenced, type.class.baseType!);
case Il2Cpp.Type.enum.valueType:
// Never needs dereferencing
return new Il2Cpp.ValueType(pointer, type);
case Il2Cpp.Type.enum.object:
case Il2Cpp.Type.enum.class:
return new Il2Cpp.Object(pointer.readPointer());
return new Il2Cpp.Object(dereferenced);
case Il2Cpp.Type.enum.genericInstance:
return type.class.isValueType ? new Il2Cpp.ValueType(pointer, type) : new Il2Cpp.Object(pointer.readPointer());
return type.class.isValueType ? new Il2Cpp.ValueType(pointer, type) : new Il2Cpp.Object(dereferenced);
case Il2Cpp.Type.enum.string:
return new Il2Cpp.String(pointer.readPointer());
return new Il2Cpp.String(dereferenced);
case Il2Cpp.Type.enum.array:
case Il2Cpp.Type.enum.multidimensionalArray:
return new Il2Cpp.Array(pointer.readPointer());
return new Il2Cpp.Array(dereferenced);
}

raise(`couldn't read the value from ${pointer} using an unhandled or unknown type ${type.name} (${type.typeEnum}), please file an issue`);
}

/** @internal */
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Err unrelated but could we have these exposed in the API? Bit more low level but super useful.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That method is indeed here - if you invoke it, it works. It just isn't exposed (yet?)!

export function write(pointer: NativePointer, value: any, type: Il2Cpp.Type): NativePointer {
switch (type.typeEnum) {
case Il2Cpp.Type.enum.boolean:
Expand Down
58 changes: 54 additions & 4 deletions src/structs/method.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
namespace Il2Cpp {
type ImplementationCallback<T extends Il2Cpp.Method.ReturnType> = (this: Il2Cpp.Class | Il2Cpp.Object | Il2Cpp.ValueType, ...parameters: Il2Cpp.Parameter.Type[]) => T;
type OnEnterCallback = (this: Il2Cpp.Class | Il2Cpp.Object | Il2Cpp.ValueType, ...parameters: Il2Cpp.Parameter.Type[]) => void;
type OnLeaveCallback<T extends Il2Cpp.Method.ReturnType> = (this: Il2Cpp.Class | Il2Cpp.Object | Il2Cpp.ValueType, retval: T) => T | void;

export class Method<T extends Il2Cpp.Method.ReturnType = Il2Cpp.Method.ReturnType> extends NativeStruct {
/** Gets the class in which this method is defined. */
@lazy
Expand Down Expand Up @@ -154,7 +158,7 @@ namespace Il2Cpp {
const FilterTypeNameMethod = FilterTypeName.field<NativePointer>("method").value;

// prettier-ignore
const offset = FilterTypeNameMethod.offsetOf(_ => _.readPointer().equals(FilterTypeNameMethodPointer))
const offset = FilterTypeNameMethod.offsetOf(_ => _.readPointer().equals(FilterTypeNameMethodPointer))
?? raise("couldn't find the virtual address offset in the native method struct");

// prettier-ignore
Expand All @@ -174,7 +178,7 @@ namespace Il2Cpp {
}

/** Replaces the body of this method. */
set implementation(block: (this: Il2Cpp.Class | Il2Cpp.Object | Il2Cpp.ValueType, ...parameters: Il2Cpp.Parameter.Type[]) => T) {
set implementation(block: ImplementationCallback<T>) {
try {
Interceptor.replace(this.virtualAddress, this.wrap(block));
} catch (e: any) {
Expand All @@ -193,6 +197,18 @@ namespace Il2Cpp {
}
}

set onEnter(block: OnEnterCallback) {
Interceptor.attach(this.virtualAddress, {
onEnter: this.wrapOnEnter(block)
});
}

set onLeave(block: OnLeaveCallback<T>) {
Interceptor.attach(this.virtualAddress, {
onLeave: this.wrapOnLeave(block)
});
}

/** Creates a generic instance of the current generic method. */
inflate<R extends Il2Cpp.Method.ReturnType = T>(...classes: Il2Cpp.Class[]): Il2Cpp.Method<R> {
if (!this.isGeneric) {
Expand All @@ -218,7 +234,6 @@ namespace Il2Cpp {
return this.invokeRaw(NULL, ...parameters);
}

/** @internal */
invokeRaw(instance: NativePointerValue, ...parameters: Il2Cpp.Parameter.Type[]): T {
const allocatedParameters = parameters.map(toFridaValue);

Expand Down Expand Up @@ -348,7 +363,7 @@ ${this.virtualAddress.isNull() ? `` : ` // 0x${this.relativeVirtualAddress.toStr
}

/** @internal */
wrap(block: (this: Il2Cpp.Class | Il2Cpp.Object | Il2Cpp.ValueType, ...parameters: Il2Cpp.Parameter.Type[]) => T): NativeCallback<any, any> {
wrap(block: ImplementationCallback<T>): NativeCallback<any, any> {
const startIndex = +!this.isStatic | +Il2Cpp.unityVersionIsBelow201830;
return new NativeCallback(
(...args: NativeCallbackArgumentValue[]): NativeCallbackReturnValue => {
Expand All @@ -366,6 +381,41 @@ ${this.virtualAddress.isNull() ? `` : ` // 0x${this.relativeVirtualAddress.toStr
this.fridaSignature
);
}

/** @internal */
wrapOnEnter(block: OnEnterCallback): ((this: InvocationContext, args: InvocationArguments) => void) {
const startIndex = +!this.isStatic | +Il2Cpp.unityVersionIsBelow201830;
return (args: InvocationArguments) => {
const thisObject = this.isStatic
? this.class
: this.class.isValueType
? new Il2Cpp.ValueType((args[0] as NativePointer).add(Il2Cpp.Object.headerSize - maybeObjectHeaderSize()), this.class.type)
: new Il2Cpp.Object(args[0] as NativePointer);
// As opposed to `Interceptor.replace`, `Interceptor.attach` doesn't
// interpret pointers, so use `read` instead of `fromFridaValue`
const parameters = this.parameters.map((_, i) => read(args[i + startIndex], _.type, false));
block.call(thisObject, ...parameters);
}
}

/** @internal */
wrapOnLeave(block: OnLeaveCallback<T>): ((this: InvocationContext, retval: InvocationReturnValue) => void) {
return (retval: InvocationReturnValue) => {
// TODO grab `this` pointer during `onEnter`
const thisObject = this.class
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we derive this from the first argument sometimes, I suppose we'll always have to hijack both onEnter and onLeave at the same time, otherwise we can't propagate this to onLeave. Edit coming up...


// `retval` is always a pointer, even if a primitive type
const returnValue = this.returnType.typeEnum != Il2Cpp.Type.enum.void ? read(retval, this.returnType) as T : undefined as T;
const newReturnValue = block.call(thisObject, returnValue);

// If callback returned nothing, replace nothing, leave the old return value
if (newReturnValue == null) return;

const handle = Memory.alloc(this.returnType.class.valueTypeSize);
write(handle, newReturnValue, this.returnType);
retval.replace(handle);
}
}
}

let maybeObjectHeaderSize = (): number => {
Expand Down
2 changes: 2 additions & 0 deletions src/structs/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ namespace Il2Cpp {
unsignedInt: _("System.UInt32"),
long: _("System.Int64"),
unsignedLong: _("System.UInt64"),


nativePointer: _("System.IntPtr"),
unsignedNativePointer: _("System.UIntPtr"),
float: _("System.Single"),
Expand Down