From cdf1feec763a835ecef7b70d72a9fd04cd13a5dc Mon Sep 17 00:00:00 2001 From: pancake Date: Fri, 17 Apr 2026 12:55:58 +0200 Subject: [PATCH] Add SBOM support --- src/lib/lib.c | 160 +++++++++++++++++++++++++++++++++++++++++ src/lib/lib.h | 25 +++++++ src/main.c | 195 +++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 378 insertions(+), 2 deletions(-) diff --git a/src/lib/lib.c b/src/lib/lib.c index 89ac229..80ac573 100644 --- a/src/lib/lib.c +++ b/src/lib/lib.c @@ -297,6 +297,166 @@ R_API Il2CppImageDefinition *r2unity_get_images (R2UnityMetadata *meta, size_t * return imgs; } +static void get_assemblies_section (R2UnityMetadata *meta, ut64 *out_off, ut64 *out_size) { + if (meta->version < 27) { + *out_off = (ut64) meta->header.v24.assembliesOffset; + *out_size = (ut64) meta->header.v24.assembliesSize; + } else if (meta->version < 29) { + *out_off = (ut64) meta->header.v27.assembliesOffset; + *out_size = (ut64) meta->header.v27.assembliesSize; + } else { + *out_off = (ut64) meta->header.v29.assembliesOffset; + *out_size = (ut64) meta->header.v29.assembliesSize; + } +} + +static void get_referenced_section (R2UnityMetadata *meta, ut64 *out_off, ut64 *out_size) { + if (meta->version < 27) { + *out_off = (ut64) meta->header.v24.referencedAssembliesOffset; + *out_size = (ut64) meta->header.v24.referencedAssembliesSize; + } else if (meta->version < 29) { + *out_off = (ut64) meta->header.v27.referencedAssembliesOffset; + *out_size = (ut64) meta->header.v27.referencedAssembliesSize; + } else { + *out_off = (ut64) meta->header.v29.referencedAssembliesOffset; + *out_size = (ut64) meta->header.v29.referencedAssembliesSize; + } +} + +R_API Il2CppAssemblyDefinition *r2unity_get_assemblies (R2UnityMetadata *meta, size_t *count) { + if (!meta || !count) { + return NULL; + } + *count = 0; + ut64 aoff = 0, asize = 0; + get_assemblies_section (meta, &aoff, &asize); + if (!asize) { + return NULL; + } + /* Probe the per-row stride by joining against the images table: + * there is one assembly per image on every wire version. Expected + * strides are 56, 60, 64 or 68 bytes (see doc/sbom.md ยง4.1). */ + size_t img_count = 0; + Il2CppImageDefinition *imgs = r2unity_get_images (meta, &img_count); + R_FREE (imgs); + if (!img_count) { + return NULL; + } + if (asize % img_count != 0) { + if (g_debug) { + fprintf (stderr, "[r2unity] assembliesSize=%"PFMT64u" not divisible by image_count=%u\n", + asize, (unsigned) img_count); + } + return NULL; + } + const ut64 entry = asize / img_count; + if (entry < 56 || entry > 80) { + if (g_debug) { + fprintf (stderr, "[r2unity] implausible assembly entry stride %"PFMT64u"\n", entry); + } + return NULL; + } + /* Within a row: + * [0..4) imageIndex + * [4..8) token (wire >= 24.1) OR customAttributeIndex (wire <= 24.0) + * [8..12) referencedAssemblyStart (wire >= 20) + * [12..16) referencedAssemblyCount + * [16..entry) Il2CppAssemblyNameDefinition + * aname size is (entry - 16): 48 when hashValueIndex dropped (>=24.4), + * 52 when it is still present (<=24.3). Layout within aname: + * nameIndex, cultureIndex, [hashValueIndex if 52], publicKeyIndex, + * hash_alg, hash_len, flags, major, minor, build, revision, + * public_key_token[8]. */ + const ut64 aname_size = entry - 16; + bool has_hash_value = (aname_size == 52); + if (aname_size != 48 && aname_size != 52) { + if (g_debug) { + fprintf (stderr, "[r2unity] unexpected aname size %"PFMT64u"\n", aname_size); + } + return NULL; + } + *count = img_count; + Il2CppAssemblyDefinition *out = R_NEWS0 (Il2CppAssemblyDefinition, *count); + if (!out) { + return NULL; + } + ut8 *buf = R_NEWS (ut8, asize); + if (!buf) { + R_FREE (out); + return NULL; + } + if (r_buf_read_at (meta->buf, aoff, buf, asize) != (st64) asize) { + R_FREE (buf); + R_FREE (out); + return NULL; + } + for (size_t i = 0; i < *count; i++) { + const ut8 *p = buf + i * entry; + out[i].image_index = (int32_t) r_read_le32 (p + 0); + out[i].token = (meta->version >= 24) ? r_read_le32 (p + 4) : 0; + out[i].referenced_start = (int32_t) r_read_le32 (p + 8); + out[i].referenced_count = (int32_t) r_read_le32 (p + 12); + const ut8 *an = p + 16; + out[i].aname.name_idx = r_read_le32 (an + 0); + out[i].aname.culture_idx = r_read_le32 (an + 4); + ut64 o; + if (has_hash_value) { + out[i].aname.hash_value_idx = r_read_le32 (an + 8); + out[i].aname.public_key_idx = r_read_le32 (an + 12); + o = 16; + } else { + out[i].aname.hash_value_idx = 0; + out[i].aname.public_key_idx = r_read_le32 (an + 8); + o = 12; + } + out[i].aname.hash_alg = r_read_le32 (an + o); o += 4; + out[i].aname.hash_len = (int32_t) r_read_le32 (an + o); o += 4; + out[i].aname.flags = r_read_le32 (an + o); o += 4; + out[i].aname.major = (int32_t) r_read_le32 (an + o); o += 4; + out[i].aname.minor = (int32_t) r_read_le32 (an + o); o += 4; + out[i].aname.build = (int32_t) r_read_le32 (an + o); o += 4; + out[i].aname.revision = (int32_t) r_read_le32 (an + o); o += 4; + memcpy (out[i].aname.public_key_token, an + o, 8); + } + R_FREE (buf); + return out; +} + +R_API int32_t *r2unity_get_referenced_assemblies (R2UnityMetadata *meta, size_t *count) { + if (!meta || !count) { + return NULL; + } + *count = 0; + ut64 roff = 0, rsize = 0; + get_referenced_section (meta, &roff, &rsize); + if (rsize < 4 || (rsize % 4) != 0) { + return NULL; + } + *count = (size_t) (rsize / 4); + int32_t *out = R_NEWS (int32_t, *count); + if (!out) { + *count = 0; + return NULL; + } + ut8 *buf = R_NEWS (ut8, rsize); + if (!buf) { + R_FREE (out); + *count = 0; + return NULL; + } + if (r_buf_read_at (meta->buf, roff, buf, rsize) != (st64) rsize) { + R_FREE (buf); + R_FREE (out); + *count = 0; + return NULL; + } + for (size_t i = 0; i < *count; i++) { + out[i] = (int32_t) r_read_le32 (buf + i * 4); + } + R_FREE (buf); + return out; +} + R_API bool r2unity_read_method_pointers_at (R2UnityMetadata *meta, const char *exe_path, ut64 addr, size_t count, ut64 **out_ptrs) { (void) meta; (void) exe_path; (void) addr; (void) count; (void) out_ptrs; return false; diff --git a/src/lib/lib.h b/src/lib/lib.h index ee74596..9e1fa46 100644 --- a/src/lib/lib.h +++ b/src/lib/lib.h @@ -290,12 +290,37 @@ typedef struct { uint32_t customAttributeCount; } Il2CppImageDefinition; +typedef struct { + uint32_t name_idx; + uint32_t culture_idx; + uint32_t public_key_idx; + uint32_t hash_value_idx; /* 0 when not present on disk (wire >= 24.4) */ + uint32_t hash_alg; + int32_t hash_len; + uint32_t flags; + int32_t major; + int32_t minor; + int32_t build; + int32_t revision; + uint8_t public_key_token[8]; +} Il2CppAssemblyNameDefinition; + +typedef struct { + int32_t image_index; + uint32_t token; /* 0 when not present (wire <= 24.0) */ + int32_t referenced_start; + int32_t referenced_count; + Il2CppAssemblyNameDefinition aname; +} Il2CppAssemblyDefinition; + 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); R_API Il2CppTypeDefinition *r2unity_get_type_definitions (R2UnityMetadata *meta, size_t *count); R_API Il2CppMethodDefinition *r2unity_get_method_definitions (R2UnityMetadata *meta, size_t *count); 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); /* 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 adf51de..c2a8a67 100644 --- a/src/main.c +++ b/src/main.c @@ -56,7 +56,189 @@ 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] [-v] \n", prog_name); + fprintf (stderr, "Usage: %s [-j] [-q] [-l N] [-f] [-a 0xADDR] [-c N] [-S] [-v] \n", prog_name); +} + +static const char *unity_range_from_wire (int wire) { + switch (wire) { + case 21: return "5.3.0-5.3.5"; + case 22: return "5.3.6-5.4"; + case 23: return "5.5"; + case 24: return "5.6-2020.1"; + case 27: return "2020.2-2021.3"; + case 29: return "2022.1-2022.3"; + case 31: return "2023.x-6000.x"; + default: return "unknown"; + } +} + +static void json_escape (FILE *f, const char *s) { + for (; s && *s; s++) { + unsigned char c = (unsigned char) *s; + switch (c) { + case '"': fputs ("\\\"", f); break; + case '\\': fputs ("\\\\", f); break; + case '\b': fputs ("\\b", f); break; + case '\f': fputs ("\\f", f); break; + case '\n': fputs ("\\n", f); break; + case '\r': fputs ("\\r", f); break; + case '\t': fputs ("\\t", f); break; + default: + if (c < 0x20) fprintf (f, "\\u%04x", c); + else fputc (c, f); + } + } +} + +static int emit_sbom (R2UnityMetadata *meta, const char *exe_path, const char *metadata_path) { + size_t img_count = 0; + Il2CppImageDefinition *imgs = r2unity_get_images (meta, &img_count); + size_t asm_count = 0; + Il2CppAssemblyDefinition *asms = r2unity_get_assemblies (meta, &asm_count); + size_t ref_count = 0; + int32_t *refs = r2unity_get_referenced_assemblies (meta, &ref_count); + if (!asms || !asm_count) { + fprintf (stderr, "r2unity: unable to decode assemblies table for wire version %d\n", + meta->version); + R_FREE (imgs); + R_FREE (asms); + R_FREE (refs); + return 1; + } + + FILE *f = stdout; + fprintf (f, "{\n"); + fprintf (f, " \"bomFormat\": \"CycloneDX\",\n"); + fprintf (f, " \"specVersion\": \"1.5\",\n"); + fprintf (f, " \"version\": 1,\n"); + fprintf (f, " \"metadata\": {\n"); + fprintf (f, " \"tools\": {\n"); + fprintf (f, " \"components\": [\n"); + fprintf (f, " { \"type\": \"application\", \"name\": \"r2unity\" }\n"); + fprintf (f, " ]\n"); + fprintf (f, " },\n"); + fprintf (f, " \"component\": {\n"); + fprintf (f, " \"type\": \"application\",\n"); + fprintf (f, " \"name\": \""); + json_escape (f, exe_path ? exe_path : "unity-build"); + fprintf (f, "\",\n"); + fprintf (f, " \"version\": \"%s\",\n", unity_range_from_wire (meta->version)); + fprintf (f, " \"properties\": [\n"); + fprintf (f, " { \"name\": \"unity.metadata.path\", \"value\": \""); + json_escape (f, metadata_path); + fprintf (f, "\" },\n"); + fprintf (f, " { \"name\": \"unity.metadata.wire_version\", \"value\": \"%d\" },\n", meta->version); + fprintf (f, " { \"name\": \"unity.metadata.confidence\", \"value\": \"range\" }\n"); + fprintf (f, " ]\n"); + fprintf (f, " }\n"); + fprintf (f, " },\n"); + + /* managed components */ + fprintf (f, " \"components\": [\n"); + for (size_t i = 0; i < asm_count; i++) { + Il2CppAssemblyDefinition *a = &asms[i]; + char *name = (char *) r2unity_get_string (meta, a->aname.name_idx); + char *culture = (char *) r2unity_get_string (meta, a->aname.culture_idx); + const char *nm = name ? name : ""; + const char *cl = (culture && *culture) ? culture : "neutral"; + + /* image (DLL filename) via a->image_index */ + const char *img = ""; + char *img_name_owned = NULL; + if (imgs && a->image_index >= 0 && (size_t) a->image_index < img_count) { + img_name_owned = (char *) r2unity_get_string (meta, imgs[a->image_index].nameIndex); + if (img_name_owned) img = img_name_owned; + } + + char pkt_hex[17]; + int empty = 1; + for (int k = 0; k < 8; k++) if (a->aname.public_key_token[k]) { empty = 0; break; } + if (empty) { + strcpy (pkt_hex, "null"); + } else { + snprintf (pkt_hex, sizeof (pkt_hex), "%02x%02x%02x%02x%02x%02x%02x%02x", + a->aname.public_key_token[0], a->aname.public_key_token[1], + a->aname.public_key_token[2], a->aname.public_key_token[3], + a->aname.public_key_token[4], a->aname.public_key_token[5], + a->aname.public_key_token[6], a->aname.public_key_token[7]); + } + + fprintf (f, " {\n"); + fprintf (f, " \"bom-ref\": \"asm:"); + json_escape (f, nm); + fprintf (f, ":%d.%d.%d.%d\",\n", + a->aname.major, a->aname.minor, a->aname.build, a->aname.revision); + fprintf (f, " \"type\": \"library\",\n"); + fprintf (f, " \"name\": \""); + json_escape (f, nm); + fprintf (f, "\",\n"); + fprintf (f, " \"version\": \"%d.%d.%d.%d\",\n", + a->aname.major, a->aname.minor, a->aname.build, a->aname.revision); + fprintf (f, " \"purl\": \"pkg:generic/unity/"); + json_escape (f, nm); + fprintf (f, "@%d.%d.%d.%d\",\n", + a->aname.major, a->aname.minor, a->aname.build, a->aname.revision); + fprintf (f, " \"properties\": [\n"); + fprintf (f, " { \"name\": \"dotnet.culture\", \"value\": \""); + json_escape (f, cl); + fprintf (f, "\" },\n"); + fprintf (f, " { \"name\": \"dotnet.public_key_token\", \"value\": "); + if (empty) fprintf (f, "null"); else fprintf (f, "\"%s\"", pkt_hex); + fprintf (f, " },\n"); + fprintf (f, " { \"name\": \"dotnet.hash_alg\", \"value\": \"0x%08x\" },\n", a->aname.hash_alg); + fprintf (f, " { \"name\": \"dotnet.flags\", \"value\": \"0x%08x\" },\n", a->aname.flags); + fprintf (f, " { \"name\": \"il2cpp.image\", \"value\": \""); + json_escape (f, img); + fprintf (f, "\" },\n"); + fprintf (f, " { \"name\": \"il2cpp.image_index\", \"value\": \"%d\" },\n", a->image_index); + fprintf (f, " { \"name\": \"il2cpp.token\", \"value\": \"0x%08x\" }\n", a->token); + fprintf (f, " ]\n"); + fprintf (f, " }%s\n", (i + 1 < asm_count) ? "," : ""); + + free (name); + free (culture); + free (img_name_owned); + } + fprintf (f, " ],\n"); + + /* dependency edges from referencedAssemblies */ + fprintf (f, " \"dependencies\": [\n"); + bool first_dep = true; + for (size_t i = 0; i < asm_count; i++) { + Il2CppAssemblyDefinition *a = &asms[i]; + if (a->referenced_count <= 0 || a->referenced_start < 0) continue; + if ((size_t) (a->referenced_start + a->referenced_count) > ref_count) continue; + char *name = (char *) r2unity_get_string (meta, a->aname.name_idx); + const char *nm = name ? name : ""; + if (!first_dep) fprintf (f, ",\n"); + first_dep = false; + fprintf (f, " {\n \"ref\": \"asm:"); + json_escape (f, nm); + fprintf (f, ":%d.%d.%d.%d\",\n \"dependsOn\": [", + a->aname.major, a->aname.minor, a->aname.build, a->aname.revision); + bool first_ref = true; + for (int k = 0; k < a->referenced_count; k++) { + int32_t ridx = refs[a->referenced_start + k]; + if (ridx < 0 || (size_t) ridx >= asm_count) continue; + Il2CppAssemblyDefinition *r = &asms[ridx]; + char *rname = (char *) r2unity_get_string (meta, r->aname.name_idx); + if (!first_ref) fprintf (f, ","); + first_ref = false; + fprintf (f, "\n \"asm:"); + json_escape (f, rname ? rname : ""); + fprintf (f, ":%d.%d.%d.%d\"", + r->aname.major, r->aname.minor, r->aname.build, r->aname.revision); + free (rname); + } + fprintf (f, "\n ]\n }"); + free (name); + } + fprintf (f, "\n ]\n}\n"); + + R_FREE (imgs); + R_FREE (asms); + R_FREE (refs); + return 0; } int main (int argc, char *argv[]) { @@ -67,13 +249,15 @@ int main (int argc, char *argv[]) { ut64 gmp_addr = 0; size_t gmp_count = 0; bool fast = false; + bool sbom = false; int opt; - while ((opt = getopt (argc, argv, "jqfvl:a:c:")) != -1) { + while ((opt = getopt (argc, argv, "jqfvSl:a:c:")) != -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 '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; @@ -105,6 +289,13 @@ int main (int argc, char *argv[]) { return 1; } + if (sbom) { + int rc = emit_sbom (meta, exe_path, metadata_path); + r2unity_free_metadata (meta); + r_unref (buf); + return rc; + } + size_t type_count = 0; Il2CppTypeDefinition *types = r2unity_get_type_definitions (meta, &type_count);