Add the new jni analysis plugin ##analysis

This commit is contained in:
pancake 2026-08-21 14:18:23 +02:00
parent e9dc573b3f
commit 961e10bc39
8 changed files with 538 additions and 0 deletions

View file

@ -14,6 +14,7 @@ anal.callargs
anal.tcc
anal.blaze
anal.gopclntab
anal.jni
anal.path
anal.a2f
anal.six

View file

@ -13,6 +13,7 @@ anal.autoname
anal.callargs
anal.tcc
anal.a2f
anal.jni
anal.sbpf
anal.tp
anal.thumb

View file

@ -14,6 +14,7 @@ anal.null
anal.autoname
anal.drcov
anal.gopclntab
anal.jni
arch.arc
arch.arm_cs
arch.arm_gnu

View file

@ -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',

349
libr/anal/p/anal_jni.c Normal file
View file

@ -0,0 +1,349 @@
/* radare - LGPL - Copyright 2026 - pancake */
#include <r_anal.h>
#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 /*<JniMethod *>*/ *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, &region, 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, &region, 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, &section_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

11
libr/anal/p/jni.mk Normal file
View file

@ -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

View file

@ -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;

173
test/unit/test_anal_jni.c Normal file
View file

@ -0,0 +1,173 @@
#include <r_core.h>
#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 ();
}