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.
285 lines
16 KiB
Markdown
285 lines
16 KiB
Markdown
# 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) | — |
|