Files
inliver233 e193f425ba Measure the toolkit on a real Flutter target and correct what it claimed
Ran the scripts against a real Flutter AOT / multi-dex / 36-.so APK (Dart 3.6.0,
com.tingfeng.tool) and recorded what each one actually did. Two of them were wrong
in the direction that reads as a target finding, one Coverage claim overstated what
ships, and four documentation statements were false.

Fixed:

  dart_pprefs.py decoded only the general-purpose register file, so `ldr dN,[x27,#imm]`
  (0xFD400000) was never matched and every double constant read from the object pool
  was dropped -- 302 pool offsets and 1802 sites on this sample, one of them with 150
  references that would have been reported as zero. The documented advice is to count
  references before editing a shared constant, so a one-sided quiet gap here produces
  exactly the wrong decision. Now covers both register files; a purpose-written
  arbiter agrees with it to 0/0 across .text, against 0/302 before.

Documented but deliberately not fixed, because a fix would not have been run:

  elf_plt.py misses the last PLT stub slot (`off < fsz - 16`), which on libloader.so
  turned a real, called import (dl_iterate_phdr) into "not a resolved stub".
  find_refs.py advertises dex-directory input but iter_files() accepts only .smali,
  so it answers "no references" with exit 0 for input it never read.
  so_constpatch.py's rebuild drops the zip `extra` field and destroys 4 KB alignment.
  Each has its root cause and fix sketch recorded.

Documentation corrections in dart-aot.md:

  blutter's asm/ does contain full instruction listings with pool annotations -- the
  file said to ignore it, and it is the richest artifact in the chain.
  The first build costs ~78 s, not "tens of minutes"; MSVC 19.34 suffices, not VS 2022.
  The file-PP offset mapping is not recoverable from the binary (4241 shared strings,
  4237 distinct deltas), so a snapshot-decoding front end is a dependency, now stated.
  Documented aotopsy as the no-toolchain route, and the trap that its version label is
  a structural profile name (it says 3.6.2 for a 3.6.0 snapshot).

Coverage now separates what was measured from what is inferred, names blutter/aotopsy
as a prerequisite instead of implying the pool decodes itself, lists the scripts this
pass did not run, and says plainly that the packer / VMP / integrity-check scenarios
were not exercised at all.

Added hand-off points for JNI, dex-observed hardening, and the existing native boundary
documents, plus docs/verification-jiongnew/ -- the evidence record, including the four
gate outputs verbatim. No second skill: the gap is one front-end dependency, not a
subject area, and relocating Flutter docs would not have supplied it.
2026-09-20 04:59:38 +08:00

16 KiB

Target facts — jiongnew.apk

Everything here is observed unless labelled otherwise. Method: read-only analysis of the APK (zipfile + androguard 4.x + direct dex structure parsing) plus install/launch on two devices. Nothing in this file comes from the handover document — where the handover and the measurements disagree, the disagreement is recorded.

sha256   0C2B063E6D1A3BD05777611ABF2F1A64056E290E4F6728C1B12DC202747DC645
size     74,394,920 B (70.95 MiB)
entries  1,982   (zip testzip() == None)

Manifest

Read with androguard's AXML parser. The handover document warned that hand-reading the binary AXML gets the class-name prefix wrong; that warning held up — the parser is the reason these values are trustworthy rather than the string scrape the handover was reduced to.

Field Value
package com.tingfeng.tool
application android:name app.video.guoguo.GApplication
main activity app.video.guoguo.SplashActivity
versionCode / versionName 84 / 1.5.8.0
minSdk / targetSdk / compileSdk 24 / 35 / 34
extractNativeLibs true
usesCleartextTraffic true
debuggable absent (release build)
allowBackup false
application tag count 1

Two questions the handover explicitly left open, now closed:

  1. application android:name is app.video.guoguo.GApplication — not a packer stub. This is an ordinary app class. The handover's string scrape only surfaced a 穿山甲/pangle stub and could not rule out a shell; the parsed manifest rules it out. Combined with the absence of hardening-library names in the native list, this target has no packer on the dex side (observed) — though see FINDINGS.md for the loader signal that muddies this.
  2. The app does not run. It aborts during startup on every attempt, on two different devices. Detail below; this is the single most consequential fact about the target.

Counts: 115 activities, 14 services, 2 receivers, 18 providers, 28 permissions. All non-app components belong to bundled ad SDKs (kwad/ksad, qq.e, baidu mobads, beizi, sigmob, meishu, bytedance openlive, windmill) plus Flutter plugins (io.flutter.plugins.*, urllauncher, imagepicker) and Sentry (io.sentry.android.core.*).

Dex

File Bytes Compressed
classes.dex 8,636,420 3,243,680
classes2.dex 7,910,800 2,918,337
classes3.dex 7,067,828 2,503,205
classes4.dex 4,032,596 1,509,375
total 27,647,644 10,174,597

Per-dex string-table sizes measured directly from the header (string_ids_size at 0x38): 63,267 / 55,279 / 46,693 / 27,549.

Cross-check against the handover: the four dex sizes match the handover's table to the byte. Also present at APK root and not mentioned in the handover: DebugProbesKt.bin, kotlin-tooling-metadata.json, androidsupportmultidexversion.txt — a Kotlin app.

Native libraries

36 libraries, 18 per ABI (arm64-v8a + armeabi-v7a), total 92,097,048 B uncompressed. The handover's 36/18+18 count reproduces exactly.

Largest: lib/armeabi-v7a/libapp.so 13,730,388 · lib/arm64-v8a/libapp.so 12,256,160 · libjingle_peerconnection_so.so (WebRTC/声网) 11,377,944 / 6,536,952 · libflutter.so 10,814,192 / 7,614,304 · libcore.so 6,723,088 / 4,937,444 · libloader.so 6,221,896 / 4,540,264 · libalivcffmpeg.so 6,136,472 / 5,092,604 · libsaasCorePlayer.so 4,885,216 / 3,440,932 · libsentry.so 681,784 / 440,816 · liblua-core.so 384,472 / 265,796 · libplt-base.so 373,056 / 245,956 · libsaasDownloader.so 354,496 / 267,916 · libdevInfo.so 280,632 / 153,228 · libti-monitor.so 115,872 / 66,324 · libsgcore.so 92,360 / 112,500 · libsentry-android.so 16,832 / 12,080 · libzeus_direct_dex.so 14,160 / 9,756 · libpangleflipped.so 10,064 / 9,756 · libdatastore_shared_counter.so 7,112 / 4,416.

Packer-name scan (rglob over libjiagu, libDexHelper, libnesec, libmobisec, libsecexe, libshella, libsgmain, libtup, libprotectClass, libexec.so, libexecmain, StubApp, libnativehelper, libapktool): zero hits. Consistent with the handover.

libloader.so is the interesting one — 6.2 MB with the largest .dynsym in the set, and the reason it was the right sample for elf_plt.py. See TOOL-VERDICTS.md.

Flutter / Dart

Pinned from the engine banner, which is the authoritative source and does not depend on symbols:

lib/arm64-v8a/libflutter.so @ 0x1fe035:
  3.6.0 (stable) (Thu Dec 5 07:46:24 2024 -0800) on "android_arm64"

Snapshot identity, from blutter's own extract_dart_info:

Dart 3.6.0
Snapshot  f956f595844a2f845a55707faaaa51e4
Target    arm64 / android
Flags     product no-code_comments no-dwarf_stack_traces_mode
          dedup_instructions no-tsan no-msan compressed-pointers

So: Dart 3.6.0, compiled with compressed pointers, in a PRODUCT build with code comments and Dwarf stack traces stripped. The product flag matters — it is the reason field and local names are absent at the format level, not merely hard to recover.

libapp.so is stripped: .dynsym is 144 bytes. Its ELF header is EM_AARCH64 (e_machine 0xB7) for the arm64 ABI and EM_ARM (0x28) for the v7a one.

No assets/flutter_assets/version file exists (21 entries under flutter_assets/), so the usual cheap version probe fails here — the banner above is the reliable route.

Discrepancy worth recording: aotopsy doctor reports Dart 3.6.2 for the same libapp.so, while the engine banner and the snapshot hash both say 3.6.0. See FINDINGS.md.

Assets

Asset Bytes Note
assets/1274036544 6,632,594 穿山甲/pangle plugin container (ZEUS_PLUGIN_PANGLE in manifest)
assets/gdt_plugin/gdtadv2.jar 2,238,863 腾讯优量汇 plugin (contains dex)
assets/bdxadsdk.jar 1,302,180 百度 ad SDK
assets/flutter_assets/assets/jiong_white.zip 986,623 theme resources
assets/flutter_assets/assets/jiong_black.zip 986,413 theme resources
assets/flutter_assets/NOTICES.Z 123,708 Flutter licence bundle
assets/reward.json 16,404 ← see below
assets/feed.json 16,251 ←
assets/splash.json 10,843 ←
assets/intertitial.json 10,592 ← note the spelling
assets/banner.json 9,151 ←
assets/draw.json 4,644 not in the handover inventory
assets/fullscreen.json 4,201 not in the handover inventory
assets/ksad_idc.json 327 Kuaishou IDC list
assets/dexopt/baseline.prof, .profm 4,235 / 385 Kotlin/Android baseline profiles

Also assets/flutter_assets/packages/luavm/lua/*.lua (10 Lua files: dkjson, http, url, ftp, ltn12, smtp, socket, tp, headers, mbox, mime) and liblua-core.so — a third logic surface beside dex and Dart. Not pursued in this pass.

The handover listed 5 ad-config JSONs; the measurement finds 7. The two additions (draw.json, fullscreen.json) are the same shape as the other five.

The ad-config JSONs are not ad configuration. They are test fixtures bundled inside the 美数/meishu aggregation SDK, and their read path is dead code in this build. The load-bearing parts:

  • Reader: classes3.dex → Lcom/meishu/sdk/core/utils/TestToolUtil;->getTest{Splash,Banner, Intertitial,Fullscreen,Reward,Draw,Feed}Json(Landroid/content/Context;Z)Ljava/lang/String; → Context.getAssets() + const-string/jumbo "splash.json". Seven methods, seven filenames. (observed)
  • The only callers are TestToolUtil.getJsonByType and AdLoader.getTestData(), and neither has a single invoke referencing it anywhere in the four dex (coverage: 175,829 method bodies with code; the 362 bodies that decoded uncleanly were rechecked by raw byte search for the target method_idx — zero hits). (observed)
  • Dart cannot be the reader: intertitial (the distinctive misspelling) has zero hits in libapp.so in both UTF-8 and UTF-16LE, so the Dart side cannot even form the filename. The files also sit in assets/ rather than assets/flutter_assets/ and are absent from AssetManifest.json, so rootBundle structurally cannot reach them. (observed)
  • Content self-identifies as fixture data: reward.title="测试激励视频", feed.title="测试", splash.clk_area="测试测试测试", appstore_id="1234567", monitor hosts contain -demo. (observed)
  • Runtime confirmation of the dead-path conclusion (run independently by the lead after the static analysis): getExternalCacheDir()/.adConfig/jsonConfig/ does not exist, and no *adConfig* / *jsonConfig* path exists anywhere under either the external or the private data directory. The only directory the app did create under cache/ is sentry/. (observed)
  • No on/off field exists in any of the seven files: matching enable|show|switch|open|close|disable|isOpen|isPlay|visible|frequency|interval yields zero candidates. (observed)

Consequence: deleting the seven JSONs is safe and pointless — no caller, so no dependency, so no effect on ads. The handover's hypothesis that "the convergence point is probably the config consumption layer" is refuted for this asset family. The real convergence point is a two-layer aggregation boundary (Dart windmill_ad_plugin ↔ com.windmill.windmill_ad_plugin MethodChannel at WindmillAd.createAdInstance, channel names com.windmill/{splash,reward,interstitial,banner,native}; below that com.meishu.sdk.core.loader.AdLoader.loadAd → https://sdk.1rtb.net/sdk/req_ad fanning out to CSJ/GDT/KS/BAIDU/OPPO/HW/TopOn). (observed for the mechanism, inferred for its completeness)

Launch behaviour — the app does not start

observed. This is the fact that constrains everything else.

Installed and launched on the Android 11/arm64 physical device:

adb -s SSBYPJKFNVEU6PGA install -r -t jiongnew.apk     -> Success (4.1 s)
   codePath  = /data/app/~~fDojk_Ku0C15jUkrmP36vA==/com.tingfeng.tool-gncxaby8OxxhG9E1TuF3aw==
   primaryCpuAbi = arm64-v8a, versionCode 84, targetSdk 35
adb -s SSBYPJKFNVEU6PGA shell am start -n com.tingfeng.tool/app.video.guoguo.SplashActivity

Timeline on the device (logcat):

04:41:59.217  Start proc 20512 for pre-top-activity {...SplashActivity}
04:41:59.280  E LoaderLog: 10026 / 101004
04:41:59.307  E Instrumentation: Uninitialized ActivityThread, likely app-created
               Instrumentation, disabling AppComponentFactory
04:41:59.765  I InputDispatcher: Focus entered window ... SplashActivity    <- window shown
04:42:00.297  F libc: FORTIFY: pthread_mutex_lock called on a destroyed mutex (0x7260f77b30)
04:42:00.301  F OpenGLRenderer: Failed to set damage region ... EGL_NOT_INITIALIZED
04:42:00.352  F libc: Fatal signal 6 (SIGABRT), code -1 (SI_QUEUE) in tid 20512
               (m.tingfeng.tool), pid 20512
04:42:00.409  E DEBUG: failed to open directory /proc/20512/fd: Permission denied
04:42:00.410  F crash_dump64: failed to attach to thread 20512, already traced by 0 ()
04:42:00.411  W tombstoned: crash socket received short read of length 0 (expected 12)
04:42:00.444  I Zygote: Process 20512 exited cleanly (0)
04:42:00.453  I ActivityManager: Process com.tingfeng.tool (pid 20512) has died: fg TOP

Survival time ≈ 1.1 s from window focus to abort, ~1.3 s from process start.

Environmental control (single variable changed): the same APK on the other attached device — emulator-5554, x86_64, SDK 28, a different ROM and a different ABI — reproduces the identical signature:

05:42:54.501  E LoaderLog: 10020 / 101003
05:42:55.547  F libc: FORTIFY: pthread_mutex_lock called on a destroyed mutex (0x7639156ff120)
05:42:55.855  I Zygote: Process 16507 exited cleanly (0)

Same loader line, same FORTIFY abort, ≈1.04 s vs ≈1.07 s. Two devices, two ABIs, two Android versions, same death. This rules out target-specific-to-one-ROM behaviour and rules out a pure root/Magisk detection as the sole cause. The crash is a property of the app, not of the device.

The crash channel is owned by a third party. Three independent signals say so:

  1. crash_dump64 cannot attach — already traced by 0 (). Something else already holds the thread via ptrace.
  2. No tombstone is written. /data/tombstones/ newest file is tombstone_23 from 03:54, while this crash happened at 04:42; tombstoned logs crash socket received short read of length 0.
  3. tombstoned receives the request for a different pid than the one that died (received crash request for pid 23771 while the dying pid was 23813), and crash_dump64 can no longer find the threads (failed to attach to thread ...: No such process).

The mechanism is visible in the emulator log immediately before the abort: Sentry's NDK layer is live and installing itself —

05:42:54.877  V libnb: enter native_bridge2_getTrampoline
               Java_io_sentry_ndk_NativeScope_nativeSetTrace, trampoline_addr 0x76391344a080
05:42:54.878  V libnb: ...Java_io_sentry_ndk_NativeScope_nativeAddBreadcrumb, trampoline_addr ...

Sentry's native crash handler replaces the signal handlers; the platform handler then cannot attach and the ordinary evidence trail (tombstone, backtrace, AndroidRuntime fatal block) is destroyed. libsentry.so / libsentry-android.so are in the APK and io.sentry.android.core.SentryInitProvider is a declared provider. (observed)

Root cause of the abort itself: not localized. What is established is the abort site's shape — a pthread_mutex_lock on an already-destroyed mutex, on the main thread (tid == pid), i.e. a native lifecycle/teardown ordering bug reached from the startup path, not the fault of an obfuscation trick or a deliberate anti-tamper abort. Attempts to get a symbolized stack were made and failed for stated reasons:

  • Platform route: impossible. crash_dump is pre-empted (above); no tombstone exists to read.
  • setenforce 0 to relax SELinux: rejected — setenforce: Couldn't set enforcing status to '0': Invalid argument. Permissive mode was not obtainable on this ROM.
  • Frida spawn-gating (the documented workaround when platform crash handling is unavailable): the device carries a disguised frida-server (/data/local/tmp/odm_service, listening 127.0.0.1:27099). frida-ps -H 127.0.0.1:27099 enumerates processes, but spawn fails with Failed to spawn: connection closed, so the pre-signal-handler window cannot be opened through that server. unverified whether a plain frida-server would succeed where this one does not.

So the honest statement is: the app aborts deterministically in native code during startup, on the main thread, and the platform's ability to show us where has been taken away by a bundled crash reporter. The LoaderLog line and the app-created Instrumentation indicate non-standard startup plumbing is also in play, but their contribution to the abort is inferred, not established — see FINDINGS.md for why that distinction was kept rather than collapsed.

What this target can and cannot exercise

Kept explicit, because the useful output of this pass is partly a list of what was not testable.

Exercisable Not exercisable
Flutter/Dart AOT analysis (Dart 3.6.0, object pool, call graph) native_crash.py's "artificially constructed crash" verdict — the crash here is a real internal fault; there is no decoy to detect
Multi-dex scale (27.6 MB over 4 files) so_constpatch.py's "redirect the checker library" scenario — there is no integrity-check library in this target
elf_plt.py at scale (libloader.so, 36,974 relocations) Every hardening scenario in packers.md / code-virtualization-and-custom-linkers.md — no packer
lib_map.py against a live process A durable live process at all — the 1.1 s window is the constraint
dart_* scripts on a real stripped libapp.so Any repack/install verification loop — the unmodified app already fails to start, so there is no working baseline to regress against
Config-driven ad analysis (and its refutation) —