diff --git a/dist/plugins-cfg/plugins.def.cfg b/dist/plugins-cfg/plugins.def.cfg index caef3de5f6..af25f65294 100644 --- a/dist/plugins-cfg/plugins.def.cfg +++ b/dist/plugins-cfg/plugins.def.cfg @@ -14,6 +14,7 @@ anal.callargs anal.tcc anal.blaze anal.gopclntab +anal.jni anal.path anal.a2f anal.six diff --git a/dist/plugins-cfg/plugins.sdk.cfg b/dist/plugins-cfg/plugins.sdk.cfg index 94e05f3679..15ad0c51f0 100644 --- a/dist/plugins-cfg/plugins.sdk.cfg +++ b/dist/plugins-cfg/plugins.sdk.cfg @@ -13,6 +13,7 @@ anal.autoname anal.callargs anal.tcc anal.a2f +anal.jni anal.sbpf anal.tp anal.thumb diff --git a/dist/plugins-cfg/plugins.static.cfg b/dist/plugins-cfg/plugins.static.cfg index 7874d9a993..86f7f1ced1 100644 --- a/dist/plugins-cfg/plugins.static.cfg +++ b/dist/plugins-cfg/plugins.static.cfg @@ -14,6 +14,7 @@ anal.null anal.autoname anal.drcov anal.gopclntab +anal.jni arch.arc arch.arm_cs arch.arm_gnu diff --git a/libr/anal/meson.build b/libr/anal/meson.build index 2d22162108..8604909cf6 100644 --- a/libr/anal/meson.build +++ b/libr/anal/meson.build @@ -54,6 +54,7 @@ r_anal_sources = [ 'p/anal_a2f.c', 'p/anal_blaze.c', 'p/anal_gopclntab.c', + 'p/anal_jni.c', 'p/anal_path.c', 'p/anal_drcov.c', 'p/anal_six.c', diff --git a/libr/anal/p/anal_jni.c b/libr/anal/p/anal_jni.c new file mode 100644 index 0000000000..54ebce5a18 --- /dev/null +++ b/libr/anal/p/anal_jni.c @@ -0,0 +1,349 @@ +/* radare - LGPL - Copyright 2026 - pancake */ + +#include + +#define JNI_MIN_TABLE_METHODS 2 +#define JNI_MAX_DATA_SECTION (64 * 1024 * 1024) +#define JNI_MAX_METHOD_NAME 256 +#define JNI_MAX_DESCRIPTOR 2048 + +typedef struct { + ut64 record_addr; + ut64 name_addr; + ut64 descriptor_addr; + ut64 function_addr; + char *name; + char *descriptor; + RBinJavaMember *member; +} JniMethod; + +typedef struct { + ut64 addr; + RList /**/ *methods; +} JniTable; + +typedef struct { + RAnal *anal; + RBinObject *bo; + Sdb *db; + int pointer_size; + bool big_endian; + int tables_count; +} JniScanContext; + +static void jni_method_free(void *ptr) { + JniMethod *method = ptr; + if (method) { + free (method->name); + free (method->descriptor); + r_bin_java_member_free (method->member); + free (method); + } +} + +static void jni_table_free(JniTable *table) { + if (table) { + r_list_free (table->methods); + free (table); + } +} + +static bool jni_add_shift(ut64 addr, st64 shift, ut64 *result) { + if (shift >= 0) { + ut64 delta = (ut64)shift; + if (addr > UT64_MAX - delta) { + return false; + } + *result = addr + delta; + return true; + } + ut64 delta = (ut64)(-(shift + 1)) + 1; + if (addr < delta) { + return false; + } + *result = addr - delta; + return true; +} + +static bool jni_addr_has_perm(JniScanContext *ctx, ut64 addr, int perm) { + RIORegion region; + return ctx->anal->iob.get_region_at (ctx->anal->iob.io, ®ion, addr) + && (region.perm & perm) == perm; +} + +static bool jni_resolve_addr(JniScanContext *ctx, ut64 addr, int perm, ut64 *result) { + if (!addr) { + return false; + } + ut64 shifted; + bool has_shifted = ctx->bo->baddr_shift + && jni_add_shift (addr, ctx->bo->baddr_shift, &shifted); + if (!ctx->bo->is_reloc_patched && has_shifted + && jni_addr_has_perm (ctx, shifted, perm)) { + *result = shifted; + return true; + } + if (jni_addr_has_perm (ctx, addr, perm)) { + *result = addr; + return true; + } + if (has_shifted && jni_addr_has_perm (ctx, shifted, perm)) { + *result = shifted; + return true; + } + return false; +} + +static char *jni_read_string(JniScanContext *ctx, ut64 addr, size_t maxlen) { + RIORegion region; + if (!ctx->anal->iob.get_region_at (ctx->anal->iob.io, ®ion, addr) + || !(region.perm & R_PERM_R)) { + return NULL; + } + ut64 end = r_itv_end (region.itv); + if (end <= addr) { + return NULL; + } + size_t available = R_MIN ((ut64)maxlen, end - addr); + char *str = R_NEWS (char, available + 1); + if (!ctx->anal->iob.read_at (ctx->anal->iob.io, addr, (ut8 *)str, available)) { + free (str); + return NULL; + } + str[available] = 0; + char *nul = memchr (str, 0, available); + if (!nul) { + free (str); + return NULL; + } + *nul = 0; + return str; +} + +static bool jni_method_name_is_valid(const char *name) { + if (R_STR_ISEMPTY (name)) { + return false; + } + const ut8 first = (ut8)*name; + if (first < 0x80 && !isalpha (first) && first != '_' && first != '$') { + return false; + } + const ut8 *p = (const ut8 *)name + 1; + for (; *p; p++) { + if (*p < 0x80 && !isalnum (*p) && *p != '_' && *p != '$') { + return false; + } + } + return true; +} + +static ut64 jni_read_pointer(JniScanContext *ctx, const ut8 *buf) { + return ctx->pointer_size == 8 + ? r_read_ble64 (buf, ctx->big_endian) + : r_read_ble32 (buf, ctx->big_endian); +} + +static bool jni_resolve_function(JniScanContext *ctx, ut64 addr, ut64 *result) { + const char *arch = ctx->anal->config->arch; + if ((addr & 1) && arch && r_str_startswith (arch, "arm")) { + if (jni_resolve_addr (ctx, addr & ~1ULL, R_PERM_X, result)) { + return true; + } + } + return jni_resolve_addr (ctx, addr, R_PERM_X, result); +} + +static JniMethod *jni_parse_method(JniScanContext *ctx, const ut8 *buf, ut64 record_addr) { + ut64 name_addr; + ut64 descriptor_addr; + ut64 function_addr; + const int pointer_size = ctx->pointer_size; + if (!jni_resolve_function (ctx, jni_read_pointer (ctx, buf + pointer_size * 2), + &function_addr)) { + return NULL; + } + if (!jni_resolve_addr (ctx, jni_read_pointer (ctx, buf), R_PERM_R, &name_addr) + || !jni_resolve_addr (ctx, jni_read_pointer (ctx, buf + pointer_size), + R_PERM_R, &descriptor_addr)) { + return NULL; + } + char *name = jni_read_string (ctx, name_addr, JNI_MAX_METHOD_NAME); + if (!jni_method_name_is_valid (name)) { + free (name); + return NULL; + } + char *descriptor = jni_read_string (ctx, descriptor_addr, JNI_MAX_DESCRIPTOR); + if (!descriptor || *descriptor != '(') { + free (name); + free (descriptor); + return NULL; + } + RBinJavaMember *member = r_bin_java_member_parse (NULL, name, descriptor, + R_BIN_JAVA_MEMBER_METHOD, 0); + if (!member) { + free (name); + free (descriptor); + return NULL; + } + JniMethod *method = R_NEW (JniMethod); + method->record_addr = record_addr; + method->name_addr = name_addr; + method->descriptor_addr = descriptor_addr; + method->function_addr = function_addr; + method->name = name; + method->descriptor = descriptor; + method->member = member; + return method; +} + +static JniTable *jni_parse_table(JniScanContext *ctx, const ut8 *buf, size_t size, size_t offset, ut64 section_addr) { + const size_t record_size = ctx->pointer_size * 3; + JniTable *table = R_NEW (JniTable); + table->addr = section_addr + offset; + table->methods = r_list_newf (jni_method_free); + while (offset <= size - record_size) { + JniMethod *method = jni_parse_method (ctx, buf + offset, + section_addr + offset); + if (!method) { + break; + } + r_list_append (table->methods, method); + offset += record_size; + } + if (r_list_length (table->methods) < JNI_MIN_TABLE_METHODS) { + jni_table_free (table); + return NULL; + } + return table; +} + +static void jni_store_table(JniScanContext *ctx, JniTable *table) { + const int table_index = ctx->tables_count++; + sdb_num_setf (ctx->db, table->addr, 0, "table.%d.addr", table_index); + sdb_num_setf (ctx->db, r_list_length (table->methods), 0, + "table.%d.count", table_index); + RListIter *iter; + JniMethod *method; + int method_index = 0; + r_list_foreach (table->methods, iter, method) { + sdb_num_setf (ctx->db, method->record_addr, 0, + "table.%d.method.%d.record", table_index, method_index); + sdb_num_setf (ctx->db, method->name_addr, 0, + "table.%d.method.%d.name_addr", table_index, method_index); + sdb_num_setf (ctx->db, method->descriptor_addr, 0, + "table.%d.method.%d.descriptor_addr", table_index, method_index); + sdb_num_setf (ctx->db, method->function_addr, 0, + "table.%d.method.%d.function", table_index, method_index); + sdb_setf (ctx->db, method->name, 0, + "table.%d.method.%d.name", table_index, method_index); + sdb_setf (ctx->db, method->descriptor, 0, + "table.%d.method.%d.descriptor", table_index, method_index); + sdb_setf (ctx->db, method->member->definition, 0, + "table.%d.method.%d.definition", table_index, method_index); + sdb_setf (ctx->db, method->member->jni_definition, 0, + "table.%d.method.%d.jni_definition", table_index, method_index); + method_index++; + } +} + +static bool jni_section_is_scannable(const RBinSection *section, size_t min_size) { + return section && !section->is_segment + && (section->perm & R_PERM_R) && !(section->perm & R_PERM_X) + && section->size >= min_size && section->size <= JNI_MAX_DATA_SECTION + && section->vaddr != UT64_MAX && section->name + && strstr (section->name, "data"); +} + +static void jni_scan_section(JniScanContext *ctx, const RBinSection *section) { + const size_t record_size = ctx->pointer_size * 3; + const size_t min_size = record_size * JNI_MIN_TABLE_METHODS; + if (!jni_section_is_scannable (section, min_size)) { + return; + } + ut64 section_addr; + if (!jni_resolve_addr (ctx, section->vaddr, R_PERM_R, §ion_addr)) { + return; + } + size_t size = (size_t)section->size; + if (section_addr > UT64_MAX - size) { + return; + } + ut8 *buf = R_NEWS (ut8, size); + if (!ctx->anal->iob.read_at (ctx->anal->iob.io, section_addr, buf, (int)size)) { + free (buf); + return; + } + size_t offset = (ctx->pointer_size - (section_addr % ctx->pointer_size)) + % ctx->pointer_size; + while (offset <= size - min_size) { + JniTable *table = jni_parse_table (ctx, buf, size, offset, section_addr); + if (!table) { + offset += ctx->pointer_size; + continue; + } + jni_store_table (ctx, table); + offset += r_list_length (table->methods) * record_size; + jni_table_free (table); + } + free (buf); +} + +static int jni_scan(RAnal *anal) { + if (!anal->iob.read_at || !anal->iob.get_region_at + || !anal->binb.bin || !anal->binb.bin->cur + || !anal->binb.bin->cur->bo || !anal->binb.get_sections_vec) { + return 0; + } + const int pointer_size = anal->config->bits / 8; + if (pointer_size != 4 && pointer_size != 8) { + return 0; + } + Sdb *db = sdb_ns (anal->sdb, "jni", 1); + sdb_reset (db); + JniScanContext ctx = { + .anal = anal, + .bo = anal->binb.bin->cur->bo, + .db = db, + .pointer_size = pointer_size, + .big_endian = R_ARCH_CONFIG_IS_BIG_ENDIAN (anal->config), + }; + RVecRBinSection *sections = anal->binb.get_sections_vec (anal->binb.bin); + RBinSection *section; + R_VEC_FOREACH (sections, section) { + jni_scan_section (&ctx, section); + } + sdb_num_set (db, "tables", ctx.tables_count, 0); + return ctx.tables_count; +} + +static bool jni_pre_analysis(RAnal *anal) { + return jni_scan (anal) > 0; +} + +static int jni_eligible(RAnal *anal) { + if (!anal || !anal->binb.bin || !anal->binb.bin->cur + || !anal->binb.bin->cur->bo) { + return -1; + } + return R_VPACK_HAS (anal->binb.bin->cur->bo->langs, R_BIN_LANG_JNI)? 0: -1; +} + +RAnalPlugin r_anal_plugin_jni = { + .meta = { + .name = "jni", + .desc = "JNI native method table discovery", + .author = "pancake", + .license = "LGPL3", + }, + .eligible = jni_eligible, + .pre_analysis = jni_pre_analysis, +}; + +#ifndef R2_PLUGIN_INCORE +R_API RLibStruct radare_plugin = { + .type = R_LIB_TYPE_ANAL, + .data = &r_anal_plugin_jni, + .version = R2_VERSION, + .abiversion = R2_ABIVERSION +}; +#endif diff --git a/libr/anal/p/jni.mk b/libr/anal/p/jni.mk new file mode 100644 index 0000000000..96a4fde830 --- /dev/null +++ b/libr/anal/p/jni.mk @@ -0,0 +1,11 @@ +OBJ_JNI=anal_jni.o + +STATIC_OBJ+=${OBJ_JNI} +TARGET_JNI=anal_jni.${EXT_SO} + +ALL_TARGETS+=${TARGET_JNI} + +${TARGET_JNI}: ${OBJ_JNI} + ${CC} $(call libname,anal_jni) ${LDFLAGS} \ + ${CFLAGS} -o anal_jni.${EXT_SO} ${OBJ_JNI} \ + -L../../bin -lr_bin diff --git a/libr/include/r_anal.h b/libr/include/r_anal.h index ae8881648e..ca9507b155 100644 --- a/libr/include/r_anal.h +++ b/libr/include/r_anal.h @@ -1854,6 +1854,7 @@ extern RAnalPlugin r_anal_plugin_path; extern RAnalPlugin r_anal_plugin_sbpf; extern RAnalPlugin r_anal_plugin_tcc; extern RAnalPlugin r_anal_plugin_gopclntab; +extern RAnalPlugin r_anal_plugin_jni; extern RAnalPlugin r_anal_plugin_six; extern RAnalPlugin r_anal_plugin_thumb; extern RAnalPlugin r_anal_plugin_tp; diff --git a/test/unit/test_anal_jni.c b/test/unit/test_anal_jni.c new file mode 100644 index 0000000000..0f26234d10 --- /dev/null +++ b/test/unit/test_anal_jni.c @@ -0,0 +1,173 @@ +#include +#include "minunit.h" + +#define DATA_ADDR 0x1000 +#define STRINGS_ADDR 0x2000 +#define CODE_ADDR 0x3000 +#define RECORD_SIZE64 24 +#define REBASE 0x10000 + +static void write_record(ut8 *buf, int index, int ps, bool be, ut64 name, ut64 desc, ut64 fcn) { + ut8 *record = buf + index * ps * 3; + if (ps == 8) { + r_write_ble64 (record, name, be); + r_write_ble64 (record + 8, desc, be); + r_write_ble64 (record + 16, fcn, be); + } else { + r_write_ble32 (record, name, be); + r_write_ble32 (record + 4, desc, be); + r_write_ble32 (record + 8, fcn, be); + } +} + +static RCore *jni_test_core_new(int bits, bool big_endian, ut64 shift) { + RCore *core = r_core_new (); + core->io->va = true; + r_io_open_at (core->io, "malloc://128", R_PERM_RW, 0644, DATA_ADDR + shift); + r_io_open_at (core->io, "malloc://256", R_PERM_RW, 0644, STRINGS_ADDR + shift); + r_io_open_at (core->io, "malloc://128", R_PERM_RWX, 0644, CODE_ADDR + shift); + r_config_set (core->config, "asm.arch", "x86"); + r_config_set_i (core->config, "asm.bits", bits); + core->anal->config->endian = big_endian? R_SYS_ENDIAN_BIG: R_SYS_ENDIAN_LITTLE; + + RBinFile *bf = R_NEW0 (RBinFile); + RBinObject *bo = R_NEW0 (RBinObject); + bf->bo = bo; + bf->rbin = core->bin; + core->bin->cur = bf; + RVecRBinSection_init (&bo->sections_vec); + RBinSection *section = RVecRBinSection_emplace_back (&bo->sections_vec); + section->name = strdup (".data"); + section->vaddr = DATA_ADDR; + section->paddr = DATA_ADDR; + section->size = (bits / 8) * 3 * 3; + section->vsize = section->size; + section->perm = R_PERM_RW; + bo->baddr_shift = shift; + bo->langs = r_vpack_add (bo->langs, R_BIN_LANG_JNI); + return core; +} + +static void jni_test_core_free(RCore *core) { + RBinFile *bf = core->bin->cur; + core->bin->cur = NULL; + RVecRBinSection_fini (&bf->bo->sections_vec); + free (bf->bo); + free (bf); + r_core_free (core); +} + +static RAnalPlugin *jni_test_plugin(RAnal *anal) { + RListIter *iter; + RAnalPlugin *plugin; + r_list_foreach (anal->libstore->plugins, iter, plugin) { + if (!strcmp (plugin->meta.name, "jni")) { + return plugin; + } + } + return NULL; +} + +static bool test_jni_native_method_table_scan(void) { + RCore *core = jni_test_core_new (64, false, 0); + mu_assert_notnull (core, "create test core"); + ut8 strings[0x90] = { 0 }; + memcpy (strings, "first", sizeof ("first")); + memcpy (strings + 0x10, "(I)V", sizeof ("(I)V")); + memcpy (strings + 0x20, "second", sizeof ("second")); + memcpy (strings + 0x30, "()I", sizeof ("()I")); + memcpy (strings + 0x40, "onlyOne", sizeof ("onlyOne")); + memcpy (strings + 0x50, "not-a-descriptor", sizeof ("not-a-descriptor")); + memcpy (strings + 0x70, "looksValid", sizeof ("looksValid")); + memcpy (strings + 0x80, "(V)V", sizeof ("(V)V")); + mu_assert_true (r_io_write_at (core->io, STRINGS_ADDR, strings, sizeof (strings)), + "write JNI strings"); + + ut8 data[RECORD_SIZE64 * 3] = { 0 }; + write_record (data, 0, 8, false, STRINGS_ADDR, STRINGS_ADDR + 0x10, CODE_ADDR); + write_record (data, 1, 8, false, STRINGS_ADDR + 0x20, STRINGS_ADDR + 0x30, + CODE_ADDR + 0x10); + write_record (data, 2, 8, false, STRINGS_ADDR + 0x40, STRINGS_ADDR + 0x50, + CODE_ADDR + 0x20); + mu_assert_true (r_io_write_at (core->io, DATA_ADDR, data, sizeof (data)), + "write valid JNI table"); + RAnalPlugin *plugin = jni_test_plugin (core->anal); + mu_assert_notnull (plugin, "find JNI analysis plugin"); + mu_assert_eq (plugin->eligible (core->anal), 0, "JNI plugin eligibility"); + mu_assert_true (plugin->pre_analysis (core->anal), "discover JNI table"); + Sdb *db = sdb_ns (core->anal->sdb, "jni", 0); + mu_assert_notnull (db, "JNI analysis database"); + mu_assert_eq (sdb_num_get (db, "tables", 0), 1, "one JNI table"); + mu_assert_eq (sdb_num_get (db, "table.0.addr", 0), DATA_ADDR, "table address"); + mu_assert_eq (sdb_num_get (db, "table.0.count", 0), 2, "method count"); + mu_assert_streq (sdb_const_get (db, "table.0.method.0.name", 0), "first", + "first method name"); + mu_assert_streq (sdb_const_get (db, "table.0.method.0.descriptor", 0), "(I)V", + "first method descriptor"); + mu_assert_eq (sdb_num_get (db, "table.0.method.1.function", 0), CODE_ADDR + 0x10, + "second method function"); + + memset (data, 0, sizeof (data)); + write_record (data, 0, 8, false, STRINGS_ADDR, STRINGS_ADDR + 0x10, CODE_ADDR); + mu_assert_true (r_io_write_at (core->io, DATA_ADDR, data, sizeof (data)), + "write one-record JNI candidate"); + mu_assert_false (plugin->pre_analysis (core->anal), + "reject a single JNI record"); + mu_assert_eq (sdb_num_get (db, "tables", 0), 0, "single record is not a table"); + + memset (data, 0, sizeof (data)); + write_record (data, 0, 8, false, STRINGS_ADDR + 0x40, STRINGS_ADDR + 0x50, + CODE_ADDR); + write_record (data, 1, 8, false, STRINGS_ADDR + 0x70, STRINGS_ADDR + 0x80, + CODE_ADDR + 0x10); + mu_assert_true (r_io_write_at (core->io, DATA_ADDR, data, sizeof (data)), + "write non-JNI pointer triples"); + mu_assert_false (plugin->pre_analysis (core->anal), + "reject malformed JNI descriptors"); + mu_assert_eq (sdb_num_get (db, "tables", 0), 0, "invalid triples are not a table"); + + jni_test_core_free (core); + mu_end; +} + +static bool test_jni_big_endian_32bit_table(void) { + RCore *core = jni_test_core_new (32, true, REBASE); + mu_assert_notnull (core, "create 32-bit test core"); + ut8 strings[0x40] = { 0 }; + memcpy (strings, "first", sizeof ("first")); + memcpy (strings + 0x10, "(I)V", sizeof ("(I)V")); + memcpy (strings + 0x20, "second", sizeof ("second")); + memcpy (strings + 0x30, "()I", sizeof ("()I")); + mu_assert_true (r_io_write_at (core->io, STRINGS_ADDR + REBASE, strings, sizeof (strings)), + "write 32-bit JNI strings"); + ut8 data[36] = { 0 }; + write_record (data, 0, 4, true, STRINGS_ADDR, STRINGS_ADDR + 0x10, CODE_ADDR); + write_record (data, 1, 4, true, STRINGS_ADDR + 0x20, STRINGS_ADDR + 0x30, + CODE_ADDR + 0x10); + mu_assert_true (r_io_write_at (core->io, DATA_ADDR + REBASE, data, sizeof (data)), + "write big-endian JNI table"); + RAnalPlugin *plugin = jni_test_plugin (core->anal); + mu_assert_notnull (plugin, "find JNI analysis plugin"); + mu_assert_true (plugin->pre_analysis (core->anal), "discover big-endian table"); + Sdb *db = sdb_ns (core->anal->sdb, "jni", 0); + mu_assert_eq (sdb_num_get (db, "table.0.addr", 0), DATA_ADDR + REBASE, + "rebased table address"); + mu_assert_eq (sdb_num_get (db, "table.0.count", 0), 2, "32-bit method count"); + mu_assert_eq (sdb_num_get (db, "table.0.method.1.function", 0), + CODE_ADDR + REBASE + 0x10, + "32-bit function pointer"); + jni_test_core_free (core); + mu_end; +} + +static bool all_tests(void) { + mu_run_test (test_jni_native_method_table_scan); + mu_run_test (test_jni_big_endian_32bit_table); + return tests_passed != tests_run; +} + +int main(int argc, char **argv) { + (void)argc; + (void)argv; + return all_tests (); +}