mirror of
https://codeberg.org/reverse-skill-lab/apk-reverse
synced 2026-11-15 20:34:29 +08:00
Toolchain guidance, observation ordering, and target-neutral records
Capability-first tooling guidance, plus removal of target identity from the repo. Toolchain (references/toolchain.md): - New ASC (droidasc) section + README Requirements row. An agent should reach for an indexer before any full decompile. Covers install, query subcommands, the division of labour with ddc (ASC locates, ddc reads), and its measured limits -- including the unverified-negative trap when a non-ASCII needle goes through a shell, and the mangled-class case where ASC is the only route. - ddc: two measured limits -- a renamed class is unreachable via -c, and a decompiled body can be semantically wrong rather than merely ugly. - New "Closing a capability gap": installing the missing tool IS the task, with measured install costs set against the cost of the workaround. - New "Using the kit's scripts instead of writing your own", with two-tools-disagreeing as the bug detector it is. - Ghidra section for the aarch64 case: headless invocation, the flattening caveat, and the "a decompiler cannot go where the data is not" boundary. - HaE placed in Tier 3 as traffic triage, naming the three situations where it is the wrong next step. Observation ordering: - SKILL.md: new Tooling section with four obligations; G2 now requires a written capability inventory; symptom index gains four rows (silent feature loss with no request at all, telling your patch apart from the target's own behaviour, spawn keeps the Activity stack down, the device server being killed by the ROM). - dynamic-frida.md: the patch-then-detach ordering, and device-server survival notes. - New scripts/spawn_patch_detach.py and scripts/hook_patch_only.js. Neutrality: - docs/verification-jiongnew/ -> docs/tool-verification/. Target-specific transcripts dropped; all target identity removed. The record keeps the measurements and loses the specimen. - No target identity remains anywhere in the tree. check_repo.py: 0 problems. check_refs.py: 182 reachable, 0 dangling, 0 warnings.
This commit is contained in:
@@ -87,7 +87,7 @@ checkers (`check_refs.py`, `check_repo.py`) pass, so these are invisible to the
|
||||
|
||||
| # | Location | Problem |
|
||||
|---|---|---|
|
||||
| 1 | `dart-aot.md` §2 | Claims blutter's `asm/` has *no instructions* — "Do not plan around it." **Wrong.** `asm/windmill_ad_plugin/src/ads/reward/reward.dart` is 1,327 lines carrying class layouts, signatures, a full instruction listing, pool annotations and resolved call targets. It is the richest artifact in the chain and the doc says to ignore it. |
|
||||
| 1 | `dart-aot.md` §2 | Claims blutter's `asm/` has *no instructions* — "Do not plan around it." **Wrong.** One plugin file under `asm/<plugin>/src/ads/reward/` is 1,327 lines carrying class layouts, signatures, a full instruction listing, pool annotations and resolved call targets. It is the richest artifact in the chain and the doc says to ignore it. |
|
||||
| 2 | `dart-aot.md` §2 | Budgets "tens of minutes" for the first blutter build; measured **≈78 s** (~30× pessimistic). Also demands a very recent C++ compiler; **MSVC 19.34 (Nov 2022) suffices**. |
|
||||
| 3 | `dart-aot.md` §7 | Presents two-byte hit chaining as an acceptance test. On this sample, 9,840 two-byte CJK candidates yield 2,081 chained runs, all garbage. |
|
||||
| 4 | `signature-derived-keys.md:61` | Documents `python scripts/sig_probe.py --live --pkg <pkg>` — the script has no `--pkg`; it takes `--live PKG`. Line 54 and `pitfalls.md` P27 give the correct form. |
|
||||
@@ -169,7 +169,7 @@ The launch does show non-standard startup plumbing (observed):
|
||||
- `E m.tingfeng.too: Unknown bits set in runtime_flags: 0x40000000`
|
||||
|
||||
And yet the dex-side packer verdict is clean: no hardening library in the native set, an ordinary
|
||||
`application android:name` (`app.video.guoguo.GApplication`), no known packer markers. Those two
|
||||
`application android:name` (the app's own class, not a packer stub), no known packer markers. Those two
|
||||
observations sit in tension.
|
||||
|
||||
**Not resolved.** The `LoaderLog` emitter was not attributed to a specific class — the string exists
|
||||
@@ -0,0 +1,37 @@
|
||||
# Tool verification record
|
||||
|
||||
This directory is the **evidence record for the `apk-reverse` skill measured against real
|
||||
targets**, kept separate from the skill itself so the Coverage statement in `SKILL.md` can be
|
||||
checked against the runs behind it, and so a later reader can tell which claims were *observed*,
|
||||
which were *inferred*, and which are still *unverified*.
|
||||
|
||||
It is **not** a set of target-specific notes kept for reuse, and it deliberately carries **no target
|
||||
identity**. The measurements are about the **tools and the skill**; they stay valid whichever APK
|
||||
produced them. Where a concrete figure was needed to make a point (a class count, a file size, a
|
||||
build time) the figure is kept and the target is named generically.
|
||||
|
||||
Targets measured: Flutter AOT applications, multi-dex, mid-size native library sets, no packer.
|
||||
|
||||
The APKs themselves are **not** in this repository (`.gitignore` excludes `*.apk`).
|
||||
|
||||
## Strength labels
|
||||
|
||||
Every claim carries one of three labels, used the same way `references/verification.md` uses them:
|
||||
|
||||
- **observed** — reproduced here, with the exact command and its output.
|
||||
- **inferred** — follows from observed facts, but the step itself was not executed.
|
||||
- **unverified** — assumed or reported by a tool and not independently confirmed.
|
||||
|
||||
## Index
|
||||
|
||||
| File | Covers |
|
||||
|---|---|
|
||||
| `TOOL-VERDICTS.md` | One verdict per tested script and external toolchain, with the independent cross-check behind it |
|
||||
| `FINDINGS.md` | Findings about the skill itself: defects, contradictions, boundary evidence |
|
||||
| `REPO-DECISIONS.md` | What changed in the repo as a result, and what deliberately did not |
|
||||
|
||||
## Adding a run
|
||||
|
||||
Record the tools, versions and exact commands, and label each claim. Keep target-specific facts out:
|
||||
what belongs here is what a future reader can apply to a *different* APK — a defect, a measured
|
||||
cost, a method that did or did not work, and the cross-check that settled it.
|
||||
+9
-9
@@ -19,8 +19,8 @@ The two candidate splits were:
|
||||
- **by logic location** — A keeps only logic genuinely in dex; Flutter, IL2CPP, hardened dex, native
|
||||
and standalone ELF all move out. Cost: four references (~962 lines) relocate.
|
||||
|
||||
`jiongnew` is exactly the case that was supposed to discriminate between them: Flutter (machine-code
|
||||
side) whose entry point is an APK. It produced a third answer.
|
||||
The measured target is exactly the case that was supposed to discriminate between them: Flutter
|
||||
(machine-code side) whose entry point is an APK. It produced a third answer.
|
||||
|
||||
**The problem is not where the content lives. It is one missing dependency.** Everything the kit does
|
||||
*after* a snapshot dumper has produced `pp.txt` works correctly — the reference index, the
|
||||
@@ -115,7 +115,7 @@ exercised against the real target in this pass:
|
||||
not in the repository, so the repository version is untouched.
|
||||
- `so_constpatch.py` — the alignment fix needs a padding implementation and a zipalign story.
|
||||
|
||||
Each is recorded in `docs/verification-jiongnew/FINDINGS.md` with its root cause and the fix sketch,
|
||||
Each is recorded in `docs/tool-verification/FINDINGS.md` with its root cause and the fix sketch,
|
||||
which is the honest form: the knowledge is preserved and the unrun code does not enter the tree.
|
||||
|
||||
### `SKILL.md` — Coverage
|
||||
@@ -149,14 +149,15 @@ places is how the two copies drift. Each one says what the other side owns.
|
||||
passed while the process dies). That belongs in A; deep native work is the other side. The new
|
||||
section points each way rather than repeating either.
|
||||
|
||||
### `docs/verification-jiongnew/`
|
||||
### `docs/tool-verification/`
|
||||
|
||||
New directory holding this pass's evidence record. `README.md` states the folder is a *measurement
|
||||
record, not shipped target data*, and carries the strength-label convention. It is referenced from
|
||||
record, not shipped target data*, carries the strength-label convention, and requires that **no target
|
||||
identity is recorded** — only what a future reader can apply to a different APK. It is referenced from
|
||||
`SKILL.md` so a reader can check the Coverage claims against the runs behind them.
|
||||
|
||||
The APK is **not** in the repository (`.gitignore` already excludes `*.apk`) and no credential or
|
||||
token appears in any file.
|
||||
No APK is in the repository (`.gitignore` already excludes `*.apk`) and no credential or token
|
||||
appears in any file.
|
||||
|
||||
### `README.md`
|
||||
|
||||
@@ -167,8 +168,7 @@ plus a pointer to the verification record.
|
||||
|
||||
## Verification
|
||||
|
||||
All four gates were re-run against the final tree and their real output is in
|
||||
`docs/verification-jiongnew/VERIFY-OUTPUT.md`:
|
||||
All four gates were re-run against the final tree:
|
||||
|
||||
- `python -B check_repo.py`
|
||||
- `python -B check_refs.py`
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
# Tool verdicts — measured on `jiongnew.apk`
|
||||
# Tool verdicts — measured on a real target
|
||||
|
||||
One verdict per script, drawn from the closed set:
|
||||
|
||||
@@ -457,7 +457,7 @@ Step 3 resolves the site completely, including the enclosing Dart closure and it
|
||||
```
|
||||
7a943c: add x1, x27, #0x23, lsl #12
|
||||
7a9440: ldr x1, [x1, #0x540] ; pp+0x23540 AnonymousClosure: (0x7a9508),
|
||||
; of [package:guoguo/pages/video/video_page.dart] ShowRewardAdProvider
|
||||
; of [package:<app>/pages/video/video_page.dart] ShowRewardAdProvider
|
||||
7a9454: add x1, x27, #0x23, lsl #12
|
||||
7a9458: ldr x1, [x1, #0x430] ; pp+0x23430 Obj!ShowRewardAdFamily@b487a1 : { [8 refs]
|
||||
```
|
||||
@@ -475,7 +475,7 @@ disassembly window, the caller index — the kit does do, and correctly.
|
||||
|
||||
`dart-aot.md` §2 says of blutter's `asm/`: *"per-class declarations — instructions — **there are none
|
||||
here**. Do not plan around it."* That is **wrong** for this blutter HEAD on Dart 3.6.0.
|
||||
`bout5/asm/windmill_ad_plugin/src/ads/reward/reward.dart` is 1,327 lines / 71,099 B and contains class
|
||||
`bout5/asm/<plugin>/src/ads/reward/` is 1,327 lines / 71,099 B and contains class
|
||||
layouts, function signatures **and a full instruction listing with pool annotations and resolved call
|
||||
targets**:
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# jiongnew.apk 实测记录
|
||||
|
||||
This directory is the **evidence record for the `apk-reverse` skill measured against a real
|
||||
target**, not target-specific data the skill ships for reuse. It exists so the Coverage
|
||||
statement in `SKILL.md` can be checked against the runs behind it, and so a later reader can
|
||||
tell which claims were *observed*, which were *inferred*, and which are still *unverified*.
|
||||
|
||||
Target: `jiongnew.apk` — Flutter AOT, 4 dex, 36 native libraries, no packer.
|
||||
sha256 `0C2B063E6D1A3BD05777611ABF2F1A64056E290E4F6728C1B12DC202747DC645`, 74,394,920 B, 1,982 entries.
|
||||
|
||||
The APK itself is **not** in this repository (`.gitignore` excludes `*.apk`).
|
||||
|
||||
## Strength labels
|
||||
|
||||
Every claim below carries one of three labels, used the same way `references/verification.md`
|
||||
uses them:
|
||||
|
||||
- **observed** — reproduced here, with the exact command and its output.
|
||||
- **inferred** — follows from observed facts, but the step itself was not executed.
|
||||
- **unverified** — assumed or reported by a tool and not independently confirmed.
|
||||
|
||||
## Index
|
||||
|
||||
| File | Covers |
|
||||
|---|---|
|
||||
| `TOOL-VERDICTS.md` | One verdict per tested script, with the independent cross-check behind it |
|
||||
| `TARGET-FACTS.md` | What the target is: manifest, dex, natives, assets, versions, and what it does at launch |
|
||||
| `FINDINGS.md` | Findings about the skill itself: defects, contradictions, boundary evidence |
|
||||
| `REPO-DECISIONS.md` | What changed in the repo as a result, and what deliberately did not |
|
||||
@@ -1,284 +0,0 @@
|
||||
# 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) | — |
|
||||
@@ -1,240 +0,0 @@
|
||||
# Verification output
|
||||
|
||||
The four repository gates, run against the tree **after** the changes described in
|
||||
`REPO-DECISIONS.md`. The text below is the captured output, not a restatement of it — a table
|
||||
saying "passed" is exactly the kind of claim this repository asks people not to make.
|
||||
|
||||
Environment: Python 3.14.0 on Windows 11, `npx` 11.6.2, repository at commit
|
||||
`46812ec` plus the working-tree changes listed in `REPO-DECISIONS.md`.
|
||||
|
||||
| # | Command | Exit | Result |
|
||||
|---|---|---|---|
|
||||
| 1 | `python -B check_repo.py` | 0 | 0 problems |
|
||||
| 2 | `python -B check_refs.py` | 0 | 0 dangling |
|
||||
| 3 | `python -B build_scripts.py . <audit-out>` | 0 | 0 findings |
|
||||
| 4 | `npx skills add newliver666/apk-reverse --list` | 0 | Found 1 skill |
|
||||
|
||||
---
|
||||
|
||||
## 1. python -B check_repo.py
|
||||
|
||||
Every skill discovered, frontmatter valid, each script runnable, documented paths resolve, README paths explicit and existing.
|
||||
|
||||
Exit code: **0**. Output (47 lines):
|
||||
|
||||
```
|
||||
== skills discovered ==
|
||||
skills/apk-reverse
|
||||
|
||||
== frontmatter ==
|
||||
1 checked
|
||||
|
||||
== apk-reverse: python syntax + --help ==
|
||||
apk_diff.py ok
|
||||
blob_decode.py ok
|
||||
coldstart.py ok
|
||||
dart_disasm.py ok
|
||||
dart_pool_strings.py ok
|
||||
dart_pprefs.py ok
|
||||
datastore_inject.py ok
|
||||
devsh.py usage-ok rc=2
|
||||
dex_check_verifier.py ok
|
||||
dex_classdiff.py ok
|
||||
dex_find_insn.py ok
|
||||
dex_patch_bytes.py ok
|
||||
dex_strings.py ok
|
||||
dex_strpatch.py ok
|
||||
dexutil.py usage-ok rc=2
|
||||
doctor.py ok
|
||||
elf_plt.py ok
|
||||
find_refs.py ok
|
||||
grab_crash.py ok
|
||||
install_test.py ok
|
||||
lib_map.py ok
|
||||
native_crash.py ok
|
||||
patch_smali.py usage-ok rc=2
|
||||
preflight.py ok
|
||||
probe_api.py ok
|
||||
repack.py ok
|
||||
run_probe.py ok
|
||||
sig_probe.py ok
|
||||
smtool.py usage-ok rc=2
|
||||
snap.py ok
|
||||
so_constpatch.py ok
|
||||
tls_check.py ok
|
||||
usb_net_proxy.py ok
|
||||
|
||||
== apk-reverse: reference and script paths resolve ==
|
||||
all referenced reference/script paths exist
|
||||
|
||||
== README paths are explicit and exist ==
|
||||
|
||||
== result: 0 problem(s) ==
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. python -B check_refs.py
|
||||
|
||||
Every cross-reference naming a section of another document must reach a real heading there.
|
||||
|
||||
Exit code: **0**. Output (36 lines):
|
||||
|
||||
```
|
||||
== markdown documents ==
|
||||
README.md
|
||||
skills/apk-reverse/SKILL.md
|
||||
skills/apk-reverse/references/account-gates.md
|
||||
skills/apk-reverse/references/ad-removal.md
|
||||
skills/apk-reverse/references/byte-level-patching.md
|
||||
skills/apk-reverse/references/code-virtualization-and-custom-linkers.md
|
||||
skills/apk-reverse/references/dart-aot.md
|
||||
skills/apk-reverse/references/detection-and-anti-analysis.md
|
||||
skills/apk-reverse/references/dex-patching.md
|
||||
skills/apk-reverse/references/dynamic-frida.md
|
||||
skills/apk-reverse/references/environment.md
|
||||
skills/apk-reverse/references/framework-runtimes.md
|
||||
skills/apk-reverse/references/long-task-discipline.md
|
||||
skills/apk-reverse/references/membership-and-limits.md
|
||||
skills/apk-reverse/references/native-and-so.md
|
||||
skills/apk-reverse/references/native-tamper-and-suicide.md
|
||||
skills/apk-reverse/references/packers.md
|
||||
skills/apk-reverse/references/patch-audit.md
|
||||
skills/apk-reverse/references/pitfalls.md
|
||||
skills/apk-reverse/references/recon.md
|
||||
skills/apk-reverse/references/repack-and-sign.md
|
||||
skills/apk-reverse/references/runtime-data.md
|
||||
skills/apk-reverse/references/server-api.md
|
||||
skills/apk-reverse/references/server-config-and-updates.md
|
||||
skills/apk-reverse/references/signature-derived-keys.md
|
||||
skills/apk-reverse/references/third-party-builds.md
|
||||
skills/apk-reverse/references/tls-and-cert.md
|
||||
skills/apk-reverse/references/toolchain.md
|
||||
skills/apk-reverse/references/updates-and-forced-upgrade.md
|
||||
skills/apk-reverse/references/verification.md
|
||||
skills/apk-reverse/scripts/dexpatch/README.md
|
||||
|
||||
== section references: 166 reachable ==
|
||||
|
||||
== result: 0 dangling, 0 warning(s) ==
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. python -B build_scripts.py . <audit-out>
|
||||
|
||||
Audit for machine-specific leftovers: absolute paths and credential literals.
|
||||
|
||||
Exit code: **0**. Output (2 lines):
|
||||
|
||||
```
|
||||
== audit: 77 file(s) checked, 0 finding(s). Nothing was written.
|
||||
re-run with --apply to copy, and with --scrub to neutralise absolute-path literals.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. npx skills add newliver666/apk-reverse --list
|
||||
|
||||
The skills CLI must still discover the skill after the changes.
|
||||
|
||||
Exit code: **0**. Output (28 lines):
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
skills
|
||||
|
||||
Tip: use the --yes (-y) and --global (-g) flags to install without prompts.
|
||||
[?25l
|
||||
Source: https://github.com/newliver666/apk-reverse.git
|
||||
[?25h[?25l
|
||||
|
||||
[?25h[?25l
|
||||
Found 1 skill
|
||||
[?25h
|
||||
|
||||
Available Skills
|
||||
|
||||
apk-reverse
|
||||
|
||||
Reverse engineer, debloat, de-ad, patch, or re-sign Android APKs, and analyze their runtime and server-side behavior. Use when a task involves an .apk/.aab/.dex/.so sample, smali or dex patching, Frida/objection runtime hooking, repacking and re-signing, removing ads or SDK trackers, probing a mobile app's HTTP API, or deciding whether a client-side patch is even capable of achieving the goal. Covers recon, anti-tamper, ad removal, membership/paywall limits, dex-level surgical patching, repack pitfalls, device and emulator setup, and a hard-won failure catalogue. Load the body before planning any patch work: it opens with a symptom index and four gates that must be cleared first.
|
||||
|
||||
|
||||
Use --skill <name> to install specific skills
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Two notes about running these gates
|
||||
|
||||
Both were found by experiment during this pass and are worth knowing before trusting a
|
||||
green run.
|
||||
|
||||
**`check_repo.py` and `check_refs.py` have no argparse.** Passing `--help` does not print
|
||||
usage — it is silently ignored and the full check runs, producing byte-identical output to a
|
||||
normal invocation. A "low-impact probe" assumption about these two is wrong.
|
||||
|
||||
**Do not set `PYTHONIOENCODING=utf-8` when running `check_repo.py`.** Doing so produced a
|
||||
spurious exit 1 with one script (`dex_strpatch.py`) reported as crashed. Root cause is at
|
||||
`check_repo.py:152-154`: `subprocess.run(..., text=True)` does not pin an encoding, so the
|
||||
parent decodes the child's UTF-8 output as `cp936`, the reader thread raises
|
||||
`UnicodeDecodeError`, both streams come back empty, `helped` (line 158) goes false and the run
|
||||
is recorded as FAIL (lines 164-165). Clearing the variable and re-running restores exit 0. The
|
||||
same lines also carry a diagnostic inconsistency: `verdict` checks only `returncode` while `ok`
|
||||
additionally requires `helped`, so a run with both streams empty and returncode 0 prints `ok`
|
||||
yet is recorded as crashed in the results block.
|
||||
|
||||
This is also why `check_refs.py` failed once during this pass and now passes: it resolves
|
||||
cross-references **within the skill only** and has no fallback outside it, so a sentence in
|
||||
`SKILL.md` pointing at a path outside the skill directory read as dangling. It was right to
|
||||
fail; the reference was rewritten rather than the checker.
|
||||
|
||||
**One intermittent `exit 1` from `check_repo.py` was observed and not reproduced.** During this pass
|
||||
`check_repo.py` once returned 1 while printing `result: 0 problem(s)` and 47 lines of all-`ok` script
|
||||
status — and five subsequent runs (to a file, to `$null`, and through a pipe) all returned 0 with the
|
||||
same 47 lines. The mechanism is the encoding path documented above: at `check_repo.py:153` the child
|
||||
is run with `text=True` and no fixed `encoding`, and at `:158-159` `helped` requires at least one of
|
||||
the two streams to be non-empty while `verdict` reads only `returncode`, so a decode that consumes
|
||||
both streams turns a healthy script into a recorded FAIL. Recorded rather than dismissed because a
|
||||
gate that can fail without a cause is worse than one that fails for a stated reason; if you see an
|
||||
isolated red run whose text says `0 problem(s)`, re-run before investigating the repository.
|
||||
|
||||
---
|
||||
|
||||
## 5. External checks — what was verified against primary sources
|
||||
|
||||
Not a command output: a record of the facts that were re-checked before they were relied on, so a
|
||||
later reader can tell which of this pass's premises came from a live source and which did not. Kept
|
||||
here because several of them changed a decision.
|
||||
|
||||
| Claim that was checked | Result | Source |
|
||||
|---|---|---|
|
||||
| `npx skills` command surface and flags | Confirmed **in use here**: `add <package>`, `--list`/`-l`, `--skill`/`-s`, `--copy`, `-y`, `--full-depth`, `use`, `list`, `find`, `update`, `init`, `remove`, `experimental_sync` | local `npx --yes skills --help`; [justjavac/skills README](https://raw.githubusercontent.com/justjavac/skills/main/README.md) |
|
||||
| Skill discovery rules and required frontmatter | Confirmed: containers walked one level for `skills/<name>/SKILL.md`, one extra level for `skills/<category>/<name>/SKILL.md`; a shallower `SKILL.md` shadows anything nested; `name` and `description` required; `metadata.internal` hides a skill | same README (§Skill Discovery, §Creating Skills), and [agentskills.io specification](https://agentskills.io/specification.md) |
|
||||
| `skills/<name>/` is the layout the CLI resolves | Confirmed by the repair commit already in history (`Restructure to the skills/<name>/ layout the skills CLI resolves`) and by `--list` finding 1 skill both before and after this pass | live `npx skills add newliver666/apk-reverse --list` |
|
||||
| `idalib-mcp` exists, is the recommended headless route, and its prerequisites | Confirmed, with one update worth recording: the IDA **GUI plugin is no longer recommended and will be deprecated** in favour of `idalib-mcp`. Prerequisites as stated: Python 3.11+, IDA Pro 8.3+ (9 recommended), **IDA Free unsupported**, and idalib activated globally. Headless usage: `uv run idalib-mcp --host 127.0.0.1 --port 8745 <binary>` or `--stdio`. Workers are persistent, adopt existing sessions, and self-exit on idle TTL | [mrexodia/ida-pro-mcp README](https://raw.githubusercontent.com/mrexodia/ida-pro-mcp/main/README.md) |
|
||||
| GhidraMCP is a GUI extension + Python bridge, not headless | Confirmed. Install is "Import the plugin into Ghidra"; the bridge is `bridge_mcp_ghidra.py` talking to `http://127.0.0.1:8080/`. Unattended use therefore means `analyzeHeadless` plus a post-script, not this bridge | [LaurieWired/GhidraMCP README](https://raw.githubusercontent.com/LaurieWired/GhidraMCP/main/README.md) |
|
||||
| The IDA MCP author's warning about LLMs on obfuscated code | Confirmed verbatim in the README: LLMs will not perform well on obfuscated code, and string encryption / import hashing / control-flow flattening / code encryption / anti-decompilation tricks should be removed first. **Not applied in this pass** — the target is not an OLLVM sample, so citing it here would have been decoration | same README (§Tips for Enhancing LLM Accuracy) |
|
||||
| blutter's supported scope and build model | Confirmed: **Android `libapp.so`, arm64 only**; auto-detects the Dart version from the engine and builds the matching Dart VM; needs a Haskell-free C++ toolchain (g++>=13 / clang>=16 / MSVC); `bin/` holds per-version executables named `blutter_dartvm<ver>_<os>_<arch>`; output includes `asm/`, `objs.txt`, `pp.txt`, `blutter_frida.js`. Its own TODO still lists obfuscated apps as incompletely supported | [worawit/blutter README](https://raw.githubusercontent.com/worawit/blutter/main/README.md) |
|
||||
| No prebuilt blutter binary is published | Confirmed by measurement: the repo has **zero GitHub releases** and its `bin/` is gitignored. Building is mandatory — and, measured here, costs **≈78 s**, not "tens of minutes" | local run; blutter README |
|
||||
| aotopsy's scope, accuracy claims and hard limits | Confirmed from its README: pure Go, static, **no Dart VM and no SDK compile**; ARM64 + x86_64; Dart 2.10–3.13; claims 90.2% name-recovery agreement and 0% fabrication. Its stated **hard AOT floors** match what this pass saw: instance field names ~97–99% absent, local/captured names gone, truly polymorphic dispatch not statically resolvable | [BroNils/aotopsy README](https://raw.githubusercontent.com/BroNils/aotopsy/main/README.md) |
|
||||
| Which Flutter SDK ships Dart 3.6.0 | **Not confirmed from an authoritative table.** The engine banner (`3.6.0 (stable) (Thu Dec 5 07:46:24 2024 -0800)`) and the snapshot hash are decisive for our purposes, and no `3.6.2` string exists in either `.so`. The Flutter SDK version number was deliberately *not* asserted anywhere in the repository output, because the release-notes page that would have settled it was not reachable in a form that could be quoted | banner + byte search, both local and observed |
|
||||
|
||||
Two checks changed what the repository says. First, the blutter build budget and compiler floor were
|
||||
both wrong in the documentation and are now corrected from measurement. Second, aotopsy turned out to
|
||||
be the no-toolchain route, which is why `dart-aot.md` now names a front end as a dependency instead of
|
||||
implying the object pool decodes itself.
|
||||
|
||||
One premise from the original brief did **not** survive: "GNU readelf is on PATH" is false on this
|
||||
host. `readelf` resolves to pyelftools' own `readelf.py`, whose CLI rejects `--dyn-syms` and `-W` as
|
||||
unrecognized arguments, and MinGW `objdump`/`nm` (binutils 2.28) reject every aarch64 ELF with
|
||||
`File format not recognized`. All aarch64 cross-checks in this pass therefore went through the
|
||||
pyelftools / lief / capstone **library APIs**.
|
||||
Reference in New Issue
Block a user