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

vitualAddress of generic method #1

Closed
samogot opened this issue Feb 28, 2021 · 11 comments
Closed

vitualAddress of generic method #1

samogot opened this issue Feb 28, 2021 · 11 comments
Labels
feature New feature or request

Comments

@samogot
Copy link

samogot commented Feb 28, 2021

I've noticed that actualPointer of generic methods and all methods in generic classes are null and it is impossible to do anything with them.

As a workaround for methods of generic classes I look for any successor with specified types and take parent from it. This doesn't work if the class has no successors or for generic methods.

As per https://www.codeproject.com/Articles/22088/Reflecting-on-Generics, there is difference between open and closed generic methods and actualPointer is only valid for closed ones. There are System.Type.MakeGenericType and System.MethodInfo.MakeGenericMethod methods to get closed versions from open ones. But is unclear to me how to convert between Il2Cpp.Method/Il2Cpp.Type and Il2Cpp.Object of type System.MethodInfo/System.Type if this is even possible. System.Type has Il2CppType handle as an internal field, so it will be possible to convert this way, but the other way around is much harder without simple public constructor (I'm even not sure if it's supposed to be constructed or each type is a singleton and there is a global cache for them)

Alternatively it should be useful to just list all available closed instantiations and to be able to select one with Accessor.

Alternatively, as far as I can see, all different instantiations of a method has the same actualPointer value, so it would be very convenient to recognize the fact that the method is generic and just pick actualPointer value from any of instantiations automatically instead of returning null.

@vfsfitvnm
Copy link
Owner

Hi @samogot, thanks for pointing this out.

I'll be honest, I absolutely do not have enough knowledge to completely understand how IL's ecosystem works. Also a bit of time has passed since I wrote this.
This is what I noticed looking at any IL disassembled source / learnt (it may be wrong ofc).

I've noticed that actualPointer of generic methods and all methods in generic classes are null and it is impossible to do anything with them.

Generic methods are not really a thing in IL code.
There is no actual implementation for generic methods, when looking at the assembled code (e.g libil2cpp.so). This is why actualPointer is null.
So what happens?

Basically, a new method is generated for each Object class that will use that method. Example:

class List<T> {
    T get(int index);
}

becomes

class List<int> {
    int get(int index);
}
class List<Foo> {
    Foo get(int index);
}
// ...

In the compiled result, int get(int index) and Foo get(int index) will have two different locations (e.g +0xAAA and +0xBBB), but they will look VERY similar.
So, List<int> and List<Foo> are two different things when looking at final compiled code. This is why generic methods in generic classes don't have an actualPointer.

Alternatively, as far as I can see, all different instantiations of a method has the same actualPointer value

This is interesting. Does it always happens? Because as far as I can remember, this is not true.

As a workaround for methods of generic classes I look for any successor with specified types and take parent from it. This doesn't work if the class has no successors or for generic methods.

As you said, if you want to get the actualPointer to a 'instanced' generic method (e.g Foo get(int index)), you will have to get a reference to the List<Foo> class first, and then list its methods.

As per https://www.codeproject.com/Articles/22088/Reflecting-on-Generics, there is difference between open and closed generic methods and actualPointer is only valid for closed ones. There are System.Type.MakeGenericType and System.MethodInfo.MakeGenericMethod methods to get closed versions from open ones.

I'm sorry, I don't know a thing about closed / open generic methods. I also don't rely on C# methods or types (e.g System.Type.MakeGenericType) because they may be "flawed" - remember that IL code is different.

But is unclear to me how to convert between Il2Cpp.Method/Il2Cpp.Type and Il2Cpp.Object of type System.MethodInfo/System.Type if this is even possible.

IL2CPP's (MethodInfo = Il2Cpp.Method, Il2CppType = Il2Cpp.Type) are built on top of, respectively, System.MethodInfo and System.Type. I rely on IL2CPP's MethodInfo and Il2CppType, which do not include a reference to System.MethodInfo and System.Type, so this "conversion" cannot be done straightforwardly, but it's possible. By the way why would you need this? I remember spending few hours "exploring" the C# types (System.MethodInfo and System.Type in this case), but I din't find anything really useful.

@samogot
Copy link
Author

samogot commented Mar 1, 2021

This is interesting. Does it always happens? Because as far as I can remember, this is not true.

Now when you gave an example with int and object I understand that most likely several versions of function code will be generated for basic type instatiations (int, boolean, etc.), maybe even for other value-types. Although I believe that for all object types there will be only one version of function code - additional type data is stored as rgctx_data in relevant instantiations of MethodInfo and Il2CppType.

I'm sorry, I don't know a thing about closed / open generic methods.

"closed" is a specific instantiation (ex List<int>) while "open" is unbound version that doesn't have parameter types specified (ex List<T>). Il2CppType and MethodInfo are generated for both "open" and all "closed" variants.

I also don't rely on C# methods or types (e.g System.Type.MakeGenericType) because they may be "flawed" - remember that IL code is different.

I don't entirely agree. The problem isn't between different languages (C# vs IL) but between different runtimes (.NET/Mono/Il2Cpp). Il2Cpp does have MakeGenericType/MakeGenericMethod and it should work as long as parameter types provided in runtime match one of existing instantiations during compile time. I looked into decompiled code of it, but it's too complex to grasp it and reimplement in CModule

By the way why would you need this?

To be able to call MakeGenericType.

As you said, if you want to get the actualPointer to a 'instanced' generic method (e.g Foo get(int index)), you will have to get a reference to the List class first, and then list its methods.

So basically the root of my problem is how to get this 'instanced' versions. They are not listed in Il2Cpp.Image.classes. I figured a workaround for one specific case where generic class does have non-generic successor. But this doesn't cover cases with no successors or generic methods.

@vfsfitvnm
Copy link
Owner

@samogot

Thank you for the further explanation.

Generic 'instanced' classes are not listed in Il2Cpp.Image.classes because they aren't there. In fact, each Il2CppType has its own index, but generic 'instanced' classes do not have one.
By the way, it should be possible to include them there, but I don't think it's worth the effort (now).

By the way, would you post a minimal example to reproduce the "issue" you are facing? I played around with generics too (few months ago), I honestly didn't have any serious problem with them (maybe I didn't dig enough).

Thanks!

@samogot
Copy link
Author

samogot commented Mar 1, 2021

class Data {}
inteface InterfaceA<T>
{
	public T method0();
}
class ClassA<T> : InterfaceA<T>
{
	public T method0() { }
	public T method1() { }
}
class ClassB<T>
{
	public T method2() { }
}
class ClassBChild : ClassB<Data>
{
}
class ClassC
{
	public E method3<E>() { }
	public Data method4() { return method3<Data>(); }
}
class Factory
{
      public InterfaceA<Data> getA() { return new ClassA<Data>(); }
      public ClassBChild getB() {}
      public ClassC getC() {}
}

Let's say we have classes like this. I want to intercept each method.

  • method4 just works as it's non-generic
  • I can workaround to intercept method2 like this:
    assembly.classes.ClassBChild.parent!.methods.method2.intercept
    
  • I see no way to intercept method1 and method3
  • As for method0 - I can get it on interface using the following code, but I'm not sure if interface methods are inteceptable, or they has null actualPointer as well. Never had a need to try in on practice.
    assembly.classes.Factory.methods.getA.returnType.class.methods.method0
    

PS: I haven't actually tried to compile the code above :) But it should be good enough as a pseudocode to showcase the problem

@vfsfitvnm
Copy link
Owner

vfsfitvnm commented Mar 1, 2021

@samogot

but I'm not sure if interface methods are inteceptable

No, they aren't. They mostly behaves like an abstract class, an interface only contains method definitions.

assembly.classes.Factory.methods.getA.returnType.class.methods.method0

Yup, this is how I used to do it: assembly.classes.Factory.methods.getA.returnType.class refers to ClassA<Data> (not InterfaceA<Data>).

assembly.classes.ClassBChild.parent!.methods.method2.intercept

Also correct: assembly.classes.ClassBChild.parent! is ClassB<Data> (not ClassB<T>).

I see no way to intercept method1 and method3

This also seems correct. Does this "pattern" happens in a common assembly / namespace (e.g. mscorlib.dll, System ...)? Maybe I can take a closer look.

@samogot
Copy link
Author

samogot commented Mar 1, 2021

I guess any generic container should work. Ideally I'm looking for more robust solution that relies on internal reflection metadata rather than on specific usages of the class from other places (return type, subclasses etc.)

Before I found this bridge packet, I used to first extract metadata from il2cpp.so+global-metadata.dat using https://github.com/Perfare/Il2CppDumper. It does handles generics quite well, but looking into sources didn't help much, because it reads it from global-metadata.dat file and it is still not clear how to access this data in runtime.

@vfsfitvnm
Copy link
Owner

vfsfitvnm commented Mar 1, 2021

@samogot
There is no way to access Il2CppGlobalMetadata at runtime (well, you could hook an internal function called by il2cpp_init, but this is not a reliable method).

Also, please notice that few applications have their global-metadata.dat encrypted or missing (e.g. packed somewhere else), so a static analysis tool won't work in such cases.

However, it should be possible to handle generics better, but unfortunately this Frida module didn't get a decent followage/support to justify the extra required amount of work.
So I guess I won't investigate on that any time soon (PRs are welcome, of course).

Anyway thank you again for pointing this issue out, it's always nice to see people taking care of your things! 😃

@vfsfitvnm
Copy link
Owner

vfsfitvnm commented Mar 4, 2021

@samogot

Ok today I took a closer look, and I remembered why this isn't possible.
So, let's start with a MethodInfo.
This is what we can obtain from it:

  • const struct Il2CppRGCTXData * rgctx_data (valid when is_inflated is true and is_generic is false);
  • const struct Il2CppMethodDefinition * methodDefinition (valid when is_inflated is false and is_generic is true)
  • const struct Il2CppGenericMethod * genericMethod; (valid when is_inflated is true)
  • const struct Il2CppGenericContainer * genericContainer; (valid when is_inflated is false and is_generic is true)

Now, these structs (you can see them here) don't contain the information needed to obtain the instanced methods. Sure, there may be spicy data, but keep in mind we don't have the metadata table, so any "indexed" value is just useless (StringIndex and so on).

However, another solution could be:

  1. Taking a memory snapshot (like Il2Cpp.choose2);
  2. Take the metadata;
  3. Iterate its types;
  4. And from here we should be able to convert them to Il2Cpp.Type, which will be converted to Il2Cpp.Class;
  5. Freeing the snapshot.

We should be able to seek all the "instanced" generic classes (like ClassB<Data>) and so we could potentially get the actualPointer of their methods.

However, it seems impossible to do: generic method -> list of its instances.

@vfsfitvnm vfsfitvnm added the help me The maintainer needs help label Mar 10, 2021
@vfsfitvnm vfsfitvnm added feature New feature or request and removed help me The maintainer needs help labels Jul 8, 2021
@vfsfitvnm vfsfitvnm changed the title actualPointer of generic method vitualAddress of generic method Aug 6, 2021
vfsfitvnm pushed a commit that referenced this issue Aug 25, 2021
…hanged to static, and few more
@vfsfitvnm
Copy link
Owner

@samogot
Copy link
Author

samogot commented Aug 25, 2021

Thanks! I'm not actively work on project where I needed that, but it would be helpful if I'll need to do something new there.

I went to see how this was implemented. So apparently there were some exposed native il2cpp methods to convert between Il2Cpp.Method/Il2Cpp.Type and Il2Cpp.Object of type System.MethodInfo/System.Type in both directions?

@vfsfitvnm
Copy link
Owner

Yep, il2cpp_type_get_object (Il2CppType -> Il2CppObject), il2cpp_method_get_object (MethodInfo -> Il2CppObject), il2cpp_class_from_system_type (Il2CppObject -> Il2CppClass) and il2cpp_method_get_from_reflection (Il2CppObject -> MethodInfo).

I just noticed il2cpp_method_get_from_reflection appeared at 2018.2.0, but its implementation is simple, fortunately.

vfsfitvnm pushed a commit that referenced this issue Oct 13, 2021
…hanged to static, and few more
vfsfitvnm pushed a commit that referenced this issue Dec 13, 2021
…hanged to static, and few more
vfsfitvnm added a commit that referenced this issue Dec 16, 2021
…hanged to static, and few more
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants