From 02efb309a20d139607e5922915adc4665d55e2c6 Mon Sep 17 00:00:00 2001 From: pancake Date: Wed, 12 Aug 2026 12:55:44 +0200 Subject: [PATCH] Fix ppstring recovery in disasm and add tests --- doc/learn.md | 26 +++++ src/lib/dart_pool_modern.c | 71 +++++++++++++ src/lib/dart_pool_parse_priv.h | 1 + src/r2/flutter_analysis.c | 147 ++++++++++++++++++++++++--- test/db/cmd/analysis-string-comments | 9 ++ 5 files changed, 240 insertions(+), 14 deletions(-) create mode 100644 test/db/cmd/analysis-string-comments diff --git a/doc/learn.md b/doc/learn.md index e5b27ec..742cf8d 100644 --- a/doc/learn.md +++ b/doc/learn.md @@ -1268,3 +1268,29 @@ singletons are still recoverable from the nearby handshake/schedule-filter cluster (`x22 + 0x20` is true, `x22 + 0x30` is false), so the same AArch64 entry patch (`add x0, x22, 0x20; ret`) is valid when the goal is to force acceptance at the Dart bad-certificate wrapper boundary. + +## PP-relative string comments in r2 disassembly + +`r2flutter -AAA` must account for shifted ARM64 PP-base materialization such as +`add x2, x27, 0x18, lsl 12`. radare2 may print this as `lsl 12` without a `#`. +If the shift is ignored, the analyzer records `PP slot +0x470` for a following +`ldr x2, [x2, 0x458]` instead of the real `PP slot +0x18458`, so ObjectPool +string resolution cannot attach the expected string comment. + +The analysis pass now accepts both `lsl #N` and `lsl N`, builds a direct +ObjectPool string map, and annotates PP-relative string loads. On +`ssl_test2/libapp.so`, the blutter-comparable check is: + +```sh +r2 -q -e scr.color=0 -e bin.relocs.apply=true \ + -c 'r2flutter -AAA' \ + -c 's 0x9da730' \ + -c 'pd 16' \ + -c q ssl_test2/libapp.so +``` + +The key instruction should now include: + +```text +0x009da744 ... ldr x2, [x2, 0x458] ; ... "premium" ; dart: PP slot +0x18458 string "premium" +``` diff --git a/src/lib/dart_pool_modern.c b/src/lib/dart_pool_modern.c index 10d65c7..5a0be8e 100644 --- a/src/lib/dart_pool_modern.c +++ b/src/lib/dart_pool_modern.c @@ -2957,6 +2957,77 @@ bool dart_modern_collect_object_pool_string_refs(const DartModernClusterRequest return ok; } +bool dart_modern_collect_direct_object_pool_string_refs(const DartModernClusterRequest *req, HtUP *pp_offsets, DartModernPoolStringRefCallback cb, void *user) { + DartCtx *ctx = req? req->ctx: NULL; + if (!ctx || !cb || !dart_modern_is_supported_snapshot (ctx)) { + return false; + } + ModernClusterMeta *meta = modern_parse_cluster_meta (ctx, req->cluster_start, req->cluster_end, req->num_clusters, req->num_base_objects); + if (!meta) { + return false; + } + ModernRefResolver resolver = { 0 }; + bool ok = modern_ref_resolver_init (&resolver, ctx, meta, req->num_clusters, req->num_base_objects); + if (!ok) { + modern_cluster_meta_free (meta, req->num_clusters); + return false; + } + for (ut64 i = 0; i < req->num_clusters; i++) { + ModernClusterMeta *m = &meta[i]; + if (m->fill_kind != MODERN_FILL_OBJECT_POOL || !m->fill_parsed || !m->fill_ok || m->fill_offset >= m->fill_end || !m->count) { + continue; + } + ClusterStream s = { + .ctx = ctx, + .cursor = m->fill_offset, + .end = m->fill_end, +}; + for (ut64 pool_index = 0; pool_index < m->count; pool_index++) { + ut64 length = 0; + if (!cs_read_unsigned (&s, &length)) { + ok = false; + break; + } + for (ut64 entry_index = 0; entry_index < length; entry_index++) { + ModernPoolEntry entry; + if (!modern_read_pool_entry (&s, ctx, entry_index, &entry)) { + ok = false; + break; + } + ut64 pp_offset = modern_pool_entry_pp_offset (ctx, entry_index); + if (pp_offsets && !ht_up_find (pp_offsets, pp_offset, NULL)) { + continue; + } + if (entry.behavior != 0 || entry.type != 1 || !entry.ref || entry.ref >= resolver.refs_count || R_STR_ISEMPTY (resolver.strings_by_ref[entry.ref])) { + continue; + } + DartModernPoolStringRefInfo info = { + .pp_offset = pp_offset, + .pool_ref = m->start_ref + pool_index, + .pool_index = pool_index, + .entry_index = entry_index, + .target_ref = entry.ref, + .target_kind = "object_pool.entry", + .field_index = entry_index, + .string_ref = entry.ref, + .string_addr = resolver.string_addr_by_ref[entry.ref], + .string_value = resolver.strings_by_ref[entry.ref], +}; + cb (&info, user); + } + if (!ok) { + break; + } + } + if (!ok) { + break; + } + } + modern_ref_resolver_fini (&resolver); + modern_cluster_meta_free (meta, req->num_clusters); + return ok; +} + static bool modern_object_pool_string_ref_exists(RList *refs, ut64 pool_ref, ut64 entry_index) { if (!refs) { return false; diff --git a/src/lib/dart_pool_parse_priv.h b/src/lib/dart_pool_parse_priv.h index 60a0d48..5dea288 100644 --- a/src/lib/dart_pool_parse_priv.h +++ b/src/lib/dart_pool_parse_priv.h @@ -262,6 +262,7 @@ bool dart_modern_emit_cluster_summary(const DartModernClusterSummaryRequest *req bool dart_modern_build_synthetic_pp(const DartModernClusterRequest *req, ut64 snapshot_base, const char *snapshot_label, ut64 data_image_base, DartPpInfo *out); bool dart_modern_resolve_pp_slot(const DartModernClusterRequest *req, ut64 pp_offset, DartModernPoolSlotInfo *out); void dart_modern_pool_slot_info_fini(DartModernPoolSlotInfo *info); +bool dart_modern_collect_direct_object_pool_string_refs(const DartModernClusterRequest *req, HtUP *pp_offsets, DartModernPoolStringRefCallback cb, void *user); bool dart_modern_collect_object_pool_string_refs(const DartModernClusterRequest *req, HtUP *pp_offsets, DartModernPoolStringRefCallback cb, void *user); bool dart_modern_extract_object_pool_strings_from_clusters(const DartModernClusterRequest *req, RList *strings, HtUP *seen_refs, ut64 *ref_counter); bool dart_modern_scan_names_from_clusters(DartCtx *ctx, ut64 cluster_start, ut64 cluster_end, ut64 num_clusters, ut64 itlen); diff --git a/src/r2/flutter_analysis.c b/src/r2/flutter_analysis.c index 54fc8d2..47dd916 100644 --- a/src/r2/flutter_analysis.c +++ b/src/r2/flutter_analysis.c @@ -54,6 +54,19 @@ typedef struct { ut64 comments; } FlutterAnalStats; +typedef struct { + ut64 pp_offset; + ut64 string_addr; + char *string_value; + char *target_kind; + bool direct; +} FlutterPpStringRef; + +typedef struct { + HtUP *by_pp_off; + RList *refs; +} FlutterPpStringMap; + typedef struct { HtUP *seen; RVecFlutterEntry *entries; @@ -63,6 +76,31 @@ static void flutter_stack_slot_free(void *p) { free (p); } +static void flutter_pp_string_ref_free(void *p) { + FlutterPpStringRef *ref = p; + if (!ref) { + return; + } + free (ref->string_value); + free (ref->target_kind); + free (ref); +} + +static void flutter_pp_string_map_init(FlutterPpStringMap *map) { + memset (map, 0, sizeof (*map)); + map->by_pp_off = ht_up_new0 (); + map->refs = r_list_newf (flutter_pp_string_ref_free); +} + +static void flutter_pp_string_map_fini(FlutterPpStringMap *map) { + if (!map) { + return; + } + ht_up_free (map->by_pp_off); + r_list_free (map->refs); + memset (map, 0, sizeof (*map)); +} + static void flutter_model_fini(FlutterAnalModel *model) { dart_recovery_model_fini (model); } @@ -214,11 +252,17 @@ static bool flutter_parse_shifted_imm(const char *line, ut64 *imm) { if (!lsl) { return false; } - const char *hash = strchr (lsl, '#'); - if (!hash) { + const char *p = lsl + 3; + while (isspace ((ut8)*p)) { + p++; + } + if (*p == '#') { + p++; + } + if (!isdigit ((ut8)*p)) { return false; } - int shift = atoi (hash + 1); + int shift = atoi (p); if (shift <= 0 || shift >= 63) { return false; } @@ -351,6 +395,64 @@ static bool flutter_model_load(FlutterAnalModel *model, RCore *core, DartCtx *dc return true; } +static void flutter_pp_string_map_add(FlutterPpStringMap *map, const DartModernPoolStringRefInfo *info) { + if (!map || !map->by_pp_off || !map->refs || !info || R_STR_ISEMPTY (info->string_value) || !info->string_addr) { + return; + } + const bool direct = R_STR_ISNOTEMPTY (info->target_kind) && !strcmp (info->target_kind, "object_pool.entry"); + FlutterPpStringRef *old = ht_up_find (map->by_pp_off, info->pp_offset, NULL); + if (old && (old->direct || !direct)) { + return; + } + FlutterPpStringRef *ref = R_NEW0 (FlutterPpStringRef); + ref->pp_offset = info->pp_offset; + ref->string_addr = info->string_addr; + ref->string_value = strdup (info->string_value); + ref->target_kind = R_STR_ISNOTEMPTY (info->target_kind)? strdup (info->target_kind): NULL; + ref->direct = direct; + r_list_append (map->refs, ref); + ht_up_insert (map->by_pp_off, info->pp_offset, ref); +} + +static void flutter_pp_string_ref_cb(const DartModernPoolStringRefInfo *info, void *user) { + flutter_pp_string_map_add (user, info); +} + +static void flutter_collect_pp_strings_from_snapshot(DartCtx *ctx, ut64 snapshot_base, HtUP *wanted, FlutterPpStringMap *map) { + if (!ctx || !snapshot_base || !map) { + return; + } + DartSnapshotHeader sh = { 0 }; + if (!dart_snapshot_header_read (ctx, snapshot_base, &sh) || !sh.ok) { + return; + } + const DartModernClusterRequest req = { + .ctx = ctx, + .cluster_start = sh.cluster_start, + .cluster_end = snapshot_base + sh.total_len, + .num_clusters = sh.nc, + .num_base_objects = sh.nb, +}; + (void)dart_modern_collect_direct_object_pool_string_refs (&req, wanted, flutter_pp_string_ref_cb, map); +} + +static void flutter_pp_string_map_load(DartCtx *ctx, HtUP *wanted, FlutterPpStringMap *map) { + if (!ctx || !map || find_snapshots (ctx) != 0) { + return; + } + DartVerLayout layout_tmp; + DartVerLayout *layout_owned = ctx->layout? NULL: dart_ctx_init_layout (ctx, &layout_tmp); + flutter_collect_pp_strings_from_snapshot (ctx, ctx->iso_data, wanted, map); + flutter_collect_pp_strings_from_snapshot (ctx, ctx->vm_data, wanted, map); + if (layout_owned) { + dart_ctx_fini_layout (ctx, layout_owned); + } +} + +static const FlutterPpStringRef *flutter_pp_string_map_get(FlutterPpStringMap *map, ut64 pp_off) { + return map && map->by_pp_off? ht_up_find (map->by_pp_off, pp_off, NULL): NULL; +} + static void flutter_apply_model_to_core(FlutterAnalModel *model, RCore *core, FlutterAnalStats *stats) { if (!model || !core) { return; @@ -740,7 +842,7 @@ static bool flutter_process_stack_store(FlutterAnalState *state, const FlutterOp return true; } -static bool flutter_process_pp_load(RCore *core, FlutterAnalState *state, ut64 at, const FlutterOpInfo *info, FlutterAnalStats *stats) { +static bool flutter_process_pp_load(RCore *core, FlutterAnalState *state, FlutterPpStringMap *pp_strings, ut64 at, const FlutterOpInfo *info, FlutterAnalStats *stats) { if (!state || !info || info->n_regs < 1 || !info->has_mem) { return false; } @@ -761,11 +863,23 @@ static bool flutter_process_pp_load(RCore *core, FlutterAnalState *state, ut64 a memset (&state->regs[dst], 0, sizeof (state->regs[dst])); state->regs[dst].has_pp_off = true; state->regs[dst].pp_off = pp_off; - char *msg = r_str_newf ("dart: " - "PP slot +0x%" PFMT64x, - pp_off); - flutter_append_comment (core, at, msg, stats); - free (msg); + const FlutterPpStringRef *string_ref = flutter_pp_string_map_get (pp_strings, pp_off); + if (string_ref) { + flutter_set_flag (core, "dart.str", string_ref->string_value, string_ref->string_addr, (ut32)strlen (string_ref->string_value), true); + flutter_add_ref (core, at, string_ref->string_addr, R_ANAL_REF_TYPE_STRN | R_ANAL_REF_TYPE_READ); + stats->string_refs++; + char *escaped = r_str_escape_utf8 (string_ref->string_value, false, true); + char *msg = r_str_newf ("dart: PP slot +0x%" PFMT64x " string \"%s\"", pp_off, escaped); + flutter_append_comment (core, at, msg, stats); + free (msg); + free (escaped); + } else { + char *msg = r_str_newf ("dart: " + "PP slot +0x%" PFMT64x, + pp_off); + flutter_append_comment (core, at, msg, stats); + free (msg); + } stats->pp_refs++; return true; } @@ -837,7 +951,7 @@ static void flutter_process_indirect_call(RCore *core, FlutterAnalState *state, } } -static void flutter_scan_function(RCore *core, FlutterAnalModel *model, RAnalFunction *fcn, FlutterAnalStats *stats) { +static void flutter_scan_function(RCore *core, FlutterAnalModel *model, FlutterPpStringMap *pp_strings, RAnalFunction *fcn, FlutterAnalStats *stats) { if (!core || !model || !fcn || !stats) { return; } @@ -898,7 +1012,7 @@ static void flutter_scan_function(RCore *core, FlutterAnalModel *model, RAnalFun (void)flutter_process_add (&state, &info); break; case R_ANAL_OP_TYPE_LOAD: - if (!flutter_process_pp_load (core, &state, at, &info, stats) && + if (!flutter_process_pp_load (core, &state, pp_strings, at, &info, stats) && !flutter_process_stack_load (&state, &info) && !flutter_process_field_load (core, model, &state, at, &info, stats) && has_info && info.n_regs > 0) { @@ -938,7 +1052,7 @@ static void flutter_scan_function(RCore *core, FlutterAnalModel *model, RAnalFun flutter_state_fini (&state); } -static void flutter_scan_functions(RCore *core, FlutterAnalModel *model, RVecFlutterEntry *entries, FlutterAnalStats *stats) { +static void flutter_scan_functions(RCore *core, FlutterAnalModel *model, FlutterPpStringMap *pp_strings, RVecFlutterEntry *entries, FlutterAnalStats *stats) { if (!core || !model || !entries || !stats) { return; } @@ -953,7 +1067,7 @@ static void flutter_scan_functions(RCore *core, FlutterAnalModel *model, RVecFlu continue; } ht_up_insert (seen_fcns, fcn->addr, fcn); - flutter_scan_function (core, model, fcn, stats); + flutter_scan_function (core, model, pp_strings, fcn, stats); } ht_up_free (seen_fcns); } @@ -979,9 +1093,13 @@ bool r2flutter_analysis_run(RCore *core, DartCtx *dctx, bool quiet) { flutter_apply_app_methods_to_core (core, app, &stats); flutter_apply_model_to_core (&model, core, &stats); + FlutterPpStringMap pp_strings; + flutter_pp_string_map_init (&pp_strings); + flutter_pp_string_map_load (&app->dctx, NULL, &pp_strings); + RVecFlutterEntry *entries = flutter_collect_entries (core, app, &model); flutter_ensure_functions (core, entries); - flutter_scan_functions (core, &model, entries, &stats); + flutter_scan_functions (core, &model, &pp_strings, entries, &stats); if (!quiet) { R_LOG_INFO ("Flutter analysis: %d functions, %d calls, %d fields, %d classes, %d types, %d strings, %d PP refs", @@ -995,6 +1113,7 @@ bool r2flutter_analysis_run(RCore *core, DartCtx *dctx, bool quiet) { } RVecFlutterEntry_free (entries); + flutter_pp_string_map_fini (&pp_strings); flutter_model_fini (&model); dart_app_free (app); return true; diff --git a/test/db/cmd/analysis-string-comments b/test/db/cmd/analysis-string-comments new file mode 100644 index 0000000..958f605 --- /dev/null +++ b/test/db/cmd/analysis-string-comments @@ -0,0 +1,9 @@ +NAME=r2-analysis-string-comment +FILE=- +CMDS=</dev/null | awk "/_nativeGetFloat64/ && /dart: string/ { print \"string-comment-ok\" }"' +EOF +EXPECT=<