From 5192b295b22aef843cce5156d581b7b9b519d325 Mon Sep 17 00:00:00 2001 From: pancake Date: Fri, 17 Apr 2026 13:31:47 +0200 Subject: [PATCH] Add -P to enumerate all PInvokes --- doc/r2unity.md | 9 +++ src/lib/lib.c | 176 ++++++++++++++++++++++++++++++++++++++++++++++--- src/lib/lib.h | 30 +++++++++ src/main.c | 130 +++++++++++++++++++++++++++++++++++- 4 files changed, 333 insertions(+), 12 deletions(-) diff --git a/doc/r2unity.md b/doc/r2unity.md index 2f99837..e8b36a4 100644 --- a/doc/r2unity.md +++ b/doc/r2unity.md @@ -58,6 +58,15 @@ full radare2 core. --- +## 1.1. Files + +| OS / target | Native file required | Metadata file required | Typical metadata location relative to binary | Current status in docs | +|---|---|---|---|---| +| Android | libil2cpp.so | global-metadata.dat | ../../assets/bin/Data/Managed/Metadata/global-metadata.dat | Main supported target; arm64 works best (doc/r2unity.md:419, doc/r2unity.md:469) | +| iOS | UnityFramework | global-metadata.dat | ../../Data/Managed/Metadata/global-metadata.dat | Main supported target for ARM64 Mach-O (doc/r2unity.md:389, doc/r2unity.md:470) | +| macOS standalone | GameAssembly.dylib | global-metadata.dat | ../Resources/Data/il2cpp_data/Metadata/global- metadata.dat | Mach-O path technically works, but CLI auto-discovery is not wired yet (doc/r2unity.md:447, +| Windows standalone | GameAssembly.dll | global-metadata.dat | *_Data/il2cpp_data/Metadata/global- metadata.dat | Good | + ## 2. `global-metadata.dat` parser (`src/lib/lib.c`, `src/lib/lib.h`) ### 2.1 Header versions diff --git a/src/lib/lib.c b/src/lib/lib.c index 80ac573..6f0dda8 100644 --- a/src/lib/lib.c +++ b/src/lib/lib.c @@ -203,9 +203,22 @@ R_API Il2CppMethodDefinition *r2unity_get_method_definitions (R2UnityMetadata *m if (!meta || !count) { return NULL; } - // On-disk Il2CppMethodDefinition is 32 bytes (little-endian) - const ut64 entry = 32; - if ((ut64) (ut64) meta->methodsSize < entry) { + /* On-disk Il2CppMethodDefinition: + * v24.1 - v30: 32 bytes + * v31+: 36 bytes (returnParameterToken inserted at +12, + * pushing parameterStart.. by +4) + * See doc/future.md §4.1 and third_party/Il2CppDumper/.../MetadataClass.cs. */ + const bool v31_layout = (meta->version >= 31); + const ut64 entry = v31_layout ? 36 : 32; + const int off_parameterStart = v31_layout ? 16 : 12; + const int off_genericContainer = v31_layout ? 20 : 16; + const int off_token = v31_layout ? 24 : 20; + const int off_flags = v31_layout ? 28 : 24; + const int off_iflags = v31_layout ? 30 : 26; + const int off_slot = v31_layout ? 32 : 28; + const int off_parameterCount = v31_layout ? 34 : 30; + + if ((ut64) meta->methodsSize < entry) { *count = 0; return NULL; } @@ -229,13 +242,13 @@ R_API Il2CppMethodDefinition *r2unity_get_method_definitions (R2UnityMetadata *m methods[i].nameIndex = r_read_le32 (p + 0); methods[i].declaringType = (int32_t) r_read_le32 (p + 4); methods[i].returnType = (int32_t) r_read_le32 (p + 8); - methods[i].parameterStart = (int32_t) r_read_le32 (p + 12); - methods[i].genericContainerIndex = (int32_t) r_read_le32 (p + 16); - methods[i].token = r_read_le32 (p + 20); - methods[i].flags = r_read_le16 (p + 24); - methods[i].iflags = r_read_le16 (p + 26); - methods[i].slot = r_read_le16 (p + 28); - methods[i].parameterCount = r_read_le16 (p + 30); + methods[i].parameterStart = (int32_t) r_read_le32 (p + off_parameterStart); + methods[i].genericContainerIndex = (int32_t) r_read_le32 (p + off_genericContainer); + methods[i].token = r_read_le32 (p + off_token); + methods[i].flags = r_read_le16 (p + off_flags); + methods[i].iflags = r_read_le16 (p + off_iflags); + methods[i].slot = r_read_le16 (p + off_slot); + methods[i].parameterCount = r_read_le16 (p + off_parameterCount); } R_FREE (buf); return methods; @@ -461,3 +474,146 @@ R_API bool r2unity_read_method_pointers_at (R2UnityMetadata *meta, const char *e (void) meta; (void) exe_path; (void) addr; (void) count; (void) out_ptrs; return false; } + +static char *build_qualified_name (R2UnityMetadata *meta, + const Il2CppTypeDefinition *td, uint32_t method_name_idx) { + char *ns = td ? (char *) r2unity_get_string (meta, td->namespaceIndex) : NULL; + char *tn = td ? (char *) r2unity_get_string (meta, td->nameIndex) : NULL; + char *mn = (char *) r2unity_get_string (meta, method_name_idx); + if (!mn) { + free (ns); + free (tn); + return NULL; + } + char buf[1024]; + if (ns && *ns && tn && *tn) { + snprintf (buf, sizeof (buf), "%s.%s.%s", ns, tn, mn); + } else if (tn && *tn) { + snprintf (buf, sizeof (buf), "%s.%s", tn, mn); + } else { + snprintf (buf, sizeof (buf), "%s", mn); + } + char *out = strdup (buf); + free (ns); + free (tn); + free (mn); + return out; +} + +R_API R2UnityInterop *r2unity_enumerate_pinvokes (R2UnityMetadata *meta, size_t *count) { + if (!meta || !count) { + return NULL; + } + *count = 0; + + size_t method_count = 0; + Il2CppMethodDefinition *methods = r2unity_get_method_definitions (meta, &method_count); + if (!methods || !method_count) { + R_FREE (methods); + return NULL; + } + size_t type_count = 0; + Il2CppTypeDefinition *types = r2unity_get_type_definitions (meta, &type_count); + size_t image_count = 0; + Il2CppImageDefinition *images = r2unity_get_images (meta, &image_count); + + /* Build typeIndex -> imageIndex map once. */ + int *type2img = NULL; + if (images && type_count) { + type2img = R_NEWS (int, type_count); + if (type2img) { + for (size_t ti = 0; ti < type_count; ti++) { + type2img[ti] = -1; + } + for (size_t ii = 0; ii < image_count; ii++) { + int start = images[ii].typeStart; + int tcount = (int) images[ii].typeCount; + if (start < 0 || tcount <= 0) { + continue; + } + for (int k = 0; k < tcount && (size_t) (start + k) < type_count; k++) { + type2img[start + k] = (int) ii; + } + } + } + } + + /* First pass: count P/Invoke methods. */ + size_t n = 0; + for (size_t j = 0; j < method_count; j++) { + if (methods[j].flags & IL2CPP_METHOD_ATTRIBUTE_PINVOKE_IMPL) { + n++; + } + } + if (!n) { + R_FREE (methods); + R_FREE (types); + R_FREE (images); + R_FREE (type2img); + return NULL; + } + + R2UnityInterop *out = R_NEWS0 (R2UnityInterop, n); + if (!out) { + R_FREE (methods); + R_FREE (types); + R_FREE (images); + R_FREE (type2img); + return NULL; + } + + size_t k = 0; + for (size_t j = 0; j < method_count && k < n; j++) { + Il2CppMethodDefinition *m = &methods[j]; + if (!(m->flags & IL2CPP_METHOD_ATTRIBUTE_PINVOKE_IMPL)) { + continue; + } + R2UnityInterop *it = &out[k++]; + it->kind = R2U_INTEROP_PINVOKE; + it->method_index = (int32_t) j; + it->token = m->token; + it->flags = m->flags; + it->iflags = m->iflags; + it->wrapper_va = 0; + it->wrapper_index = UINT32_MAX; + it->confidence = 100; + + const Il2CppTypeDefinition *td = NULL; + if (types && m->declaringType >= 0 && (size_t) m->declaringType < type_count) { + td = &types[m->declaringType]; + } + it->name = build_qualified_name (meta, td, m->nameIndex); + + int img_idx = -1; + if (type2img && m->declaringType >= 0 && (size_t) m->declaringType < type_count) { + img_idx = type2img[m->declaringType]; + } + it->image_index = img_idx; + if (img_idx >= 0 && images && (size_t) img_idx < image_count) { + it->image_name = (char *) r2unity_get_string (meta, images[img_idx].nameIndex); + } + /* DLL name / entry-point recovery requires attribute-BLOB decoding + * (v29+) or generator disassembly (v<=27.2). Not implemented yet. */ + } + + R_FREE (methods); + R_FREE (types); + R_FREE (images); + R_FREE (type2img); + + *count = n; + return out; +} + +R_API void r2unity_free_interop (R2UnityInterop *items, size_t count) { + if (!items) { + return; + } + for (size_t i = 0; i < count; i++) { + free (items[i].name); + free (items[i].image_name); + free (items[i].dll_name); + free (items[i].entry_name); + } + free (items); +} diff --git a/src/lib/lib.h b/src/lib/lib.h index 9e1fa46..93cd375 100644 --- a/src/lib/lib.h +++ b/src/lib/lib.h @@ -313,6 +313,34 @@ typedef struct { Il2CppAssemblyNameDefinition aname; } Il2CppAssemblyDefinition; +/* ECMA-335 MethodAttributes.PinvokeImpl — bit 13 on methodDef.flags */ +#define IL2CPP_METHOD_ATTRIBUTE_PINVOKE_IMPL 0x2000 + +enum { + R2U_INTEROP_PINVOKE = 1, /* managed -> native, [DllImport] */ + R2U_INTEROP_REVERSE_PINVOKE = 2, /* native -> managed, [MonoPInvokeCallback] */ + R2U_INTEROP_UNMANAGED_ONLY = 3, /* [UnmanagedCallersOnly] (.NET 5+) */ + R2U_INTEROP_INTEROP_DATA = 4 /* Il2CppInteropData entry (opaque) */ +}; + +/* A single P/Invoke or reverse-P/Invoke entry. + * String fields are owned by the struct and freed by r2unity_free_interop. */ +typedef struct { + int32_t method_index; /* -1 for anonymous native wrapper */ + uint32_t token; /* 0 if not applicable */ + int32_t image_index; /* -1 if unresolved */ + uint16_t flags; /* copy of methodDef.flags */ + uint16_t iflags; /* copy of methodDef.iflags */ + char *name; /* "Ns.Class.Method" or NULL */ + char *image_name; /* "Assembly-CSharp" or NULL */ + char *dll_name; /* NULL if unrecovered */ + char *entry_name; /* NULL if unrecovered (=> method name) */ + ut64 wrapper_va; /* 0 if no native wrapper */ + uint32_t wrapper_index; /* global reversePInvokeWrappers index, or UINT32_MAX */ + uint8_t kind; /* R2U_INTEROP_* */ + uint8_t confidence; /* 0..100 */ +} R2UnityInterop; + R_API R2UnityMetadata *r2unity_parse_metadata (RBuffer *buf); R_API void r2unity_free_metadata (R2UnityMetadata *meta); R_API const char *r2unity_get_string (R2UnityMetadata *meta, uint32_t index); @@ -321,6 +349,8 @@ R_API Il2CppMethodDefinition *r2unity_get_method_definitions (R2UnityMetadata *m R_API Il2CppImageDefinition *r2unity_get_images (R2UnityMetadata *meta, size_t *count); R_API Il2CppAssemblyDefinition *r2unity_get_assemblies (R2UnityMetadata *meta, size_t *count); R_API int32_t *r2unity_get_referenced_assemblies (R2UnityMetadata *meta, size_t *count); +R_API R2UnityInterop *r2unity_enumerate_pinvokes (R2UnityMetadata *meta, size_t *count); +R_API void r2unity_free_interop (R2UnityInterop *items, size_t count); /* Simplified API: use format-specific finders, or manual read stub. */ R_API bool r2unity_read_method_pointers_at (R2UnityMetadata *meta, const char *exe_path, ut64 addr, size_t count, ut64 **out_ptrs); R_API bool r2unity_find_method_pointers_macho (R2UnityMetadata *meta, const char *macho_path, ut64 **out_ptrs); diff --git a/src/main.c b/src/main.c index c2a8a67..758faf1 100644 --- a/src/main.c +++ b/src/main.c @@ -17,6 +17,7 @@ static void build_method_attrs_string (char *o, size_t osz, unsigned int flags) const unsigned int MdFinal = 0x0020; const unsigned int MdVirtual = 0x0040; const unsigned int MdAbstract = 0x0400; + const unsigned int MdPinvokeImpl = IL2CPP_METHOD_ATTRIBUTE_PINVOKE_IMPL; const char *vis = ""; switch (flags & MemberAccessMask) { case MdPublic: vis = "public"; break; @@ -47,6 +48,10 @@ static void build_method_attrs_string (char *o, size_t osz, unsigned int flags) if (*tmp) strncat (tmp, " ", sizeof (tmp) - 1); strncat (tmp, "final", sizeof (tmp) - 1); } + if (flags & MdPinvokeImpl) { + if (*tmp) strncat (tmp, " ", sizeof (tmp) - 1); + strncat (tmp, "extern", sizeof (tmp) - 1); + } if (!*tmp) { strncpy (o, "", osz); } else { @@ -56,7 +61,7 @@ static void build_method_attrs_string (char *o, size_t osz, unsigned int flags) } static void print_usage (const char *prog_name) { - fprintf (stderr, "Usage: %s [-j] [-q] [-l N] [-f] [-a 0xADDR] [-c N] [-S] [-v] \n", prog_name); + fprintf (stderr, "Usage: %s [-j] [-q] [-l N] [-f] [-a 0xADDR] [-c N] [-S] [-P] [-F text|json|r2] [-v] \n", prog_name); } static const char *unity_range_from_wire (int wire) { @@ -241,6 +246,110 @@ static int emit_sbom (R2UnityMetadata *meta, const char *exe_path, const char *m return 0; } +/* Cross-entry sort for stable output: sort by image_name, then name. */ +static int interop_cmp (const void *a, const void *b) { + const R2UnityInterop *x = (const R2UnityInterop *) a; + const R2UnityInterop *y = (const R2UnityInterop *) b; + const char *xi = x->image_name ? x->image_name : ""; + const char *yi = y->image_name ? y->image_name : ""; + int c = strcmp (xi, yi); + if (c) { + return c; + } + const char *xn = x->name ? x->name : ""; + const char *yn = y->name ? y->name : ""; + return strcmp (xn, yn); +} + +static int emit_pinvokes (R2UnityMetadata *meta, const char *exe_path, + const char *format, bool quiet) { + (void) exe_path; + size_t n = 0; + R2UnityInterop *items = r2unity_enumerate_pinvokes (meta, &n); + if (items && n > 1) { + qsort (items, n, sizeof (R2UnityInterop), interop_cmp); + } + + const bool is_json = format && !strcmp (format, "json"); + const bool is_r2 = format && !strcmp (format, "r2"); + + if (is_json) { + FILE *f = stdout; + fprintf (f, "{\"ok\":true,\"version\":%d,\"pinvokes\":[", meta->version); + for (size_t i = 0; i < n; i++) { + R2UnityInterop *it = &items[i]; + if (i) fputc (',', f); + fprintf (f, "{\"image\":\""); + json_escape (f, it->image_name ? it->image_name : ""); + fprintf (f, "\",\"method\":\""); + json_escape (f, it->name ? it->name : ""); + fprintf (f, "\",\"token\":\"0x%08x\",\"flags\":\"0x%04x\",\"iflags\":\"0x%04x\",\"confidence\":%u", + it->token, it->flags, it->iflags, (unsigned) it->confidence); + if (it->dll_name) { + fprintf (f, ",\"dll\":\""); + json_escape (f, it->dll_name); + fprintf (f, "\""); + } else { + fprintf (f, ",\"dll\":null"); + } + if (it->entry_name) { + fprintf (f, ",\"entry\":\""); + json_escape (f, it->entry_name); + fprintf (f, "\""); + } else { + fprintf (f, ",\"entry\":null"); + } + fputc ('}', f); + } + fprintf (f, "],\"reverse_pinvokes\":[],\"anonymous_reverse_pinvokes\":[],\"interop_data\":null}\n"); + } else if (is_r2) { + for (size_t i = 0; i < n; i++) { + R2UnityInterop *it = &items[i]; + if (!it->name) { + continue; + } + char buf[1024]; + if (it->image_name && *it->image_name) { + snprintf (buf, sizeof (buf), "sym.unity.%s.%s", it->image_name, it->name); + } else { + snprintf (buf, sizeof (buf), "sym.unity.%s", it->name); + } + r_name_filter (buf, -1); + if (it->dll_name) { + printf ("# PInvoke %s -> %s!%s\n", buf, + it->dll_name, + it->entry_name ? it->entry_name : it->name); + } else { + printf ("# PInvoke %s -> \n", buf); + } + } + } else { + if (!quiet) { + printf ("# P/Invoke methods (managed -> native), metadata wire version %d\n", meta->version); + printf ("# IMAGE\tMETHOD\tDLL\tENTRY\tFLAGS\tCONFIDENCE\n"); + } + for (size_t i = 0; i < n; i++) { + R2UnityInterop *it = &items[i]; + char attrs[128]; + attrs[0] = 0; + build_method_attrs_string (attrs, sizeof (attrs), it->flags); + printf ("%s\t%s\t%s\t%s\t%s\t%u\n", + it->image_name ? it->image_name : "", + it->name ? it->name : "", + it->dll_name ? it->dll_name : "", + it->entry_name ? it->entry_name : "", + *attrs ? attrs : "-", + (unsigned) it->confidence); + } + if (!quiet) { + printf ("# summary: pinvokes=%zu reverse_pinvokes=0 (wrapper resolution not yet implemented)\n", n); + } + } + + r2unity_free_interop (items, n); + return 0; +} + int main (int argc, char *argv[]) { bool json_one_line = false; bool quiet = false; @@ -250,14 +359,18 @@ int main (int argc, char *argv[]) { size_t gmp_count = 0; bool fast = false; bool sbom = false; + bool pinvokes = false; + const char *format = "text"; int opt; - while ((opt = getopt (argc, argv, "jqfvSl:a:c:")) != -1) { + while ((opt = getopt (argc, argv, "jqfvSPl:a:c:F:")) != -1) { switch (opt) { case 'j': json_one_line = true; break; case 'q': quiet = true; break; case 'f': fast = true; break; case 'v': debug = true; break; case 'S': sbom = true; break; + case 'P': pinvokes = true; break; + case 'F': format = optarg; break; case 'l': limit = strtol (optarg, NULL, 0); break; case 'a': gmp_addr = (ut64) strtoull (optarg, NULL, 0); break; case 'c': gmp_count = (size_t) strtoull (optarg, NULL, 0); break; @@ -296,6 +409,19 @@ int main (int argc, char *argv[]) { return rc; } + if (pinvokes) { + if (strcmp (format, "text") && strcmp (format, "json") && strcmp (format, "r2")) { + fprintf (stderr, "r2unity: unknown -F format %s (expected text|json|r2)\n", format); + r2unity_free_metadata (meta); + r_unref (buf); + return 1; + } + int rc = emit_pinvokes (meta, exe_path, format, quiet); + r2unity_free_metadata (meta); + r_unref (buf); + return rc; + } + size_t type_count = 0; Il2CppTypeDefinition *types = r2unity_get_type_definitions (meta, &type_count);