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.
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:
application android:nameisapp.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 seeFINDINGS.mdfor the loader signal that muddies this.- 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.getJsonByTypeandAdLoader.getTestData(), and neither has a singleinvokereferencing 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 targetmethod_idx— zero hits). (observed) - Dart cannot be the reader:
intertitial(the distinctive misspelling) has zero hits inlibapp.soin both UTF-8 and UTF-16LE, so the Dart side cannot even form the filename. The files also sit inassets/rather thanassets/flutter_assets/and are absent fromAssetManifest.json, sorootBundlestructurally 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 undercache/issentry/. (observed) - No on/off field exists in any of the seven files: matching
enable|show|switch|open|close|disable|isOpen|isPlay|visible|frequency|intervalyields 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:
crash_dump64cannot attach —already traced by 0 (). Something else already holds the thread via ptrace.- No tombstone is written.
/data/tombstones/newest file istombstone_23from 03:54, while this crash happened at 04:42;tombstonedlogscrash socket received short read of length 0. tombstonedreceives the request for a different pid than the one that died (received crash request for pid 23771while the dying pid was 23813), andcrash_dump64can 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_dumpis pre-empted (above); no tombstone exists to read. setenforce 0to 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, listening127.0.0.1:27099).frida-ps -H 127.0.0.1:27099enumerates processes, butspawnfails withFailed 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) | — |