-
Notifications
You must be signed in to change notification settings - Fork 210
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,8 +23,12 @@ namespace Il2Cpp { | |
return Il2Cpp.exports.free(pointer); | ||
} | ||
|
||
/** @internal */ | ||
export function read(pointer: NativePointer, type: Il2Cpp.Type): Il2Cpp.Field.Type { | ||
/** | ||
* @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(); | ||
|
@@ -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 */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
|
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 | ||
|
@@ -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 | ||
|
@@ -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) { | ||
|
@@ -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) { | ||
|
@@ -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); | ||
|
||
|
@@ -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 => { | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we derive |
||
|
||
// `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 => { | ||
|
There was a problem hiding this comment.
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 callptr.readPointer()
first.Made this new behaviour backward compatible.