16 KiB
Extension pass — on-device run against a hardened Flutter sample
This file records the device-side half of the extension pass: what was actually executed against a live, hardened sample, what the sample did in response, and which conclusions the evidence supports.
It follows the same three labels the rest of this directory uses:
- 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.
Target identity is deliberately absent. The sample is referred to as the sample, and its package
identifier appears as <PKG>. The findings are about the tools and the methods, and stay valid
whichever hardened APK produced them.
Environment (observed)
| Item | Value |
|---|---|
| Device | one physical arm64 device, Android 11 (API 30), kernel 4.14.186, Magisk root |
| Host | Windows 11, Python 3.14.0, frida 16.7.19, frida-tools 13.7.1, frida-dexdump 2.0.1, java 17, adb 1.0.41 |
| Device server | frida-server 16.7.19 (version-matched to the host by frida-server --version) |
| Sample | 360-jiagu hardened, application android:name = a third-party stub class; Flutter AOT payload |
| Host capability report | python scripts/doctor.py → static/repack/smali/native-patch/device/root/frida all OK; 28 / 28 scripts runnable |
| Pre-flight warnings | clock skew 3.32 s; leftover adb forward entries (8787, 8651, 65534, 27043); a device-side frida-server already running |
Two environment facts from doctor.py are worth carrying forward, because each of them silently
invalidates a measurement: the clock skew, and a frida process already up on the device — the
second one means the target may already be reacting to instrumentation before your first command.
The measurement that mattered: the sample fights instrumentation
(1) Control — the sample starts normally with no instrumentation (observed)
$ adb shell 'su -c "am force-stop <PKG>; sleep 1; am start -n <PKG>/.MainActivity"'
$ sleep 12 && adb shell 'su -c "ps -A | grep -i <PKG>"'
u0_a623 22691 17278 14123672 642488 SyS_epoll_wait 0 S <PKG>
Resident, 642 MB, healthy. This is the control build the rest of the section is measured against.
(2) The pid drifts — but not because of frida (observed, corrected)
During the session the sample's pid changed repeatedly:
22691 → 23452 → 24267 → 27274 → 31284 → 32259 → 1368 → 8161
The natural reading is "the sample fights instrumentation", and the first write-up of this file said
exactly that. The follow-up measurement refuted it. With frida-server stopped and no attach of
any kind, the sample was restarted under observation for 90 seconds:
20:28:22 pid=[9961] load=36.43 | Mem: 11484 total 11077 used 406 free
20:28:33 pid=[9961] load=36.26 | Mem: 11484 total 10767 used 716 free
20:28:45 pid=[9961] load=35.61 | Mem: 11484 total 10739 used 745 free
20:28:56 pid=[9961] load=34.64 | Mem: 11484 total 10767 used 716 free
20:29:08 pid=[11246] load=34.91 | Mem: 11484 total 11151 used 333 free
20:29:19 pid=[11246] load=35.69 | Mem: 11484 total 11142 used 342 free
20:29:31 pid=[12238] load=35.14 | Mem: 11484 total 11131 used 353 free
20:29:42 pid=[13212] load=37.03 | Mem: 11484 total 11059 used 424 free
The pid changes with no frida present, roughly every 10-20 seconds, while the device sits at
300-750 MB of free memory out of 11.5 GB. The first correction to this file attributed that to memory
pressure. That attribution was also wrong, and the device-side dumping run refuted it with a
better instrument than sampling: logcat.
$ adb logcat | grep <PKG>
Process <PKG> (pid N) has died: fg TOP
Start proc <pid> for top-activity <PKG>
The process dies as the foreground top activity, with no crash, no tombstone and no ANR record,
and the platform immediately relaunches it — a clean self-exit in a loop, on a 7-18 second cycle. A
reclaiming device does not do that to a foreground process, and a memory-pressure kill leaves an
lmkd line and a low-memory record. What produces exactly this shape is the sample checking its own
environment and quitting on purpose.
And the environment it finds is decisive: on this device magisk --denylist status reports enforced
with an empty list, Zygisk-Assistant is installed but nothing is scoped, and
/data/adb/shamiko does not exist — so no application on this device is hidden from root
(EXTENSION-lsposed.md, observed). A hardened sample that refuses to run on a rooted device will do
exactly this, forever, and no amount of dump-retrying changes it.
Reading, corrected twice: the pid drift is an artefact of the root environment, not of instrumentation and not of memory pressure. The practical consequence is the useful part — before any dynamic analysis of a hardened sample, configure root hiding and re-run the baseline, because with nothing hidden every dynamic result on this device is measured against a process that is trying to die. The observer's first control run showed a "stable" sample for 12 seconds only because it happened to sample inside one 17-second lifetime.
This is the skill's own rule applied twice to its own record: a conclusion was reopened each time a
measurement disagreed with it (SKILL.md §Stop conditions, last bullet). Two wrong attributions were
cheaper to delete than for a reader to inherit — and the second one only fell because a different
method (logcat, not sampling) was pointed at the same question.
The platform's own view of a failed launch was also captured, and is worth keeping as a shape:
W ActivityManager: Process ProcessRecord{…:<PKG>/u0a623} failed to attach
I ActivityManager: Killing <pid>:<PKG>/u0a623 (adj -10000): start timeout
A start timeout kill reads the same whether the cause is a slow shell init, a reclaiming device, or a
deliberate delay. It does not discriminate, and it is not evidence either way.
(3) frida-dexdump cannot complete a memory dump — reproduced twice (observed)
Both spawn and attach were tried, with the parameter mistakes fixed between attempts:
$ frida-dexdump -U -f <PKG> -d -o <out> # spawn
Failed to spawn: need Gadget to attach on jailed Android; its default location is: …gadget-android-arm64.so
$ frida-dexdump -U -f <PKG> -d 12 -o <out> # 12 was parsed as an argv entry, not a delay
Failed to spawn: the 'argv' option is not supported when spawning Android apps
$ frida-dexdump -U -f <PKG> -d -o <out> # corrected
Failed to spawn: unexpectedly timed out while waiting for app to launch
$ frida-dexdump -U -p <pid> -d -o <out> # attach, twice, second run with the device idle
frida.InvalidOperationError: script has been destroyed: {'addr': '0x721e24c02c', 'size': 409556}
INFO:frida-dexdump:[*] All done... Process terminated
Attach was run twice. The second run was the clean one: no Stalker traffic anywhere on the device
(see the crash note below), the package enabled and verified running, the pid read immediately before
the attach, and a single variable changed relative to the control. It produced the same
script has been destroyed failure, and the sample's pid changed again in the aftermath.
What the failed runs left behind, checked with the kit's own validator (observed):
$ python skills/apk-reverse/scripts/dex_dump_validate.py <dump_dir>
== dex dump validation: 2 file(s), 2 parse, 0 rejected ==
name size sha256[:12] ver cksum sig class noco code stub%
classes.dex 500 9461987e5464 035 BAD BAD 1986754560 0 0 0.0%
walk stopped early: index out of range
classes02.dex 10285489 116d8b84b486 035 BAD BAD 1986754560 0 0 0.0%
walk stopped early: index out of range
Both artifacts pass the magic check and then fail everything else: checksum and signature both BAD,
no classes recovered, the walk stopping on an out-of-range index. They are memory fragments cut
short by the process dying mid-read, not dex images — and this is exactly the case
dex_dump_validate.py exists for. A size-based heuristic would have kept the 10.2 MB file and
discarded the 500-byte one; both are worthless, and only a structural walk says so.
Reading: under these conditions the memory-dump route is refused by the target, not by the host
tooling. Two shape-identical failures on a single variable is the skill's own stop signal: the correct
move is to re-classify (references/advanced-unpacking.md §the four shapes, then
references/detection-and-anti-analysis.md), not to run a third variant of the same attach.
Two of the four failures above were the observer's own mistakes, and they are worth keeping because
they are cheap to repeat: -d is a switch, not a delay, and any extra positional argument reaches
spawn(argv). -n <name> on this version of frida-dexdump also still routes through the spawn
path, so "attach by name" does not exist as a way around the spawn refusal — use -p <pid>.
(4) What the sample looks like at the moment it is attacked (observed)
$ adb shell 'su -c "top -n 1 -b"'
PID USER PR NI VIRT RES SHR S[%CPU] %MEM TIME+ ARGS
31284 u0_a623 10 -10 13G 561M 268M S 439 4.8 0:05.52 <PKG>
154 root 20 0 0 0 0 S 66.6 0.0 0:02.18 [kswapd0]
17638 system 18 -2 12G 590M 419M S 60.6 5.1 1:15.93 system_server
A Flutter application in its startup window saturates several cores (439 % CPU) while the Dart VM
initialises, and the device — 11.7 GB RAM with 357 MB free at that moment — runs its reclaim thread at
two thirds of a core. The load on this device during the experiment was the sample's, not the
observer's instrumentation. That matters for attribution: "the device is slow, so the dump failed"
is not available as an explanation here, because the failing command is a protocol failure
(script has been destroyed) rather than a timeout.
The device's own framework was already damaged (observed)
Recorded here because it is a third candidate mechanism for "things keep restarting", alongside the sample's self-exit and the memory pressure — and because it invalidates any dynamic result that depends on the hooking framework:
LSPosed Manageropened to "LSPosed is not installed", with no modules tab, while thelspddaemon was still alive.- The LSPosed configuration database had not been updated since 19:37 — it still pointed at the
codePathof an app that had been uninstalled, and never registered the replacement. logcatshowed a loop:system server died,am is dead,no response from bridge, andMagisk: zygote crashed too many times, rolling-back.
A zygote in a crash-and-rollback cycle restarts application processes by itself, so "the pid changed"
had at least three independent possible causes on this device (the sample exiting, reclaim, and a
broken framework). That is exactly why the corrected §2 attributes the drift to instability of the
observation target rather than to any single mechanism, and why a dynamic result needs its device's
framework state recorded next to it — scripts/preflight.py reads device state for the same reason.
Recovery is a reboot (Magisk re-initialises Zygisk and lspd at late_start; the LSPosed daemon is
started once and does not return if killed). Whether the framework came back after the reboot, and
whether the sample's self-exit behaviour changed with it, is recorded in
EXTENSION-lsposed.md — and any change there must stay inferred, because a reboot moves several
variables at once.
A shared-device hazard, measured (observed)
While the run above was in progress, a second agent on the same device used Frida Stalker against
com.android.settings and com.android.systemui, including a libc malloc export trigger that
entered and left follow hundreds of times. Result:
Stalker.followon the main thread reportedblocks=0,blk=0,calls=0— the follow installed and no events arrived.systemuithen died withSIGSEGV, the faulting frame sitting in gum's anonymous code cache and the backtrace continuing intolibartatart::Reference_getReferent; the process restarted (18030 → 26932).- Device load average reached 34.11 on an eight-core phone.
Reading: two conclusions, kept separate on purpose. First, on this ROM + frida-server 16.7.19
combination a Stalker follow produced no events — that is a boundary for
references/native-dbi-and-deobfuscation.md, not a contradiction of Stalker's documentation. Second,
high-frequency follow/unfollow on every malloc entry crashed a system process — a measured
failure mode for the pattern, and one worth stating before anyone puts it in a shipped script.
The third conclusion is about method rather than about Stalker: instrumentation on a shared device is a shared variable. The concurrent load damaged the other experiment's validity — the dumped-run above could not be attributed until it was repeated with the device idle, and the repeat is what turned "maybe the load broke it" into a reproducible protocol refusal. R2 (one variable at a time) applies across agents, not only within one script.
Conclusions, graded
| Claim | Label |
|---|---|
| The sample runs healthy with no instrumentation on the device | observed |
| The sample's pid drifts repeatedly during an experiment, roughly every 10-20 s | observed — cause not instrumentation: reproduced with frida-server stopped, under 300-750 MB free memory (§2) |
frida-dexdump spawn refuses (jailed / argv / timeout) and attach fails with script has been destroyed, reproduced twice with the parameter mistakes fixed |
observed |
| The attach failure is deliberate armour rather than a host-tool bug | unverified — memory pressure leaving the process an unstable target is a competing explanation this pass did not rule out; the single-variable repeat eliminated concurrent load, not baseline load |
The shipped shell classes.dex is 8.9 MB and defines only 4 classes / 29 methods with code |
observed (recorded in EXTENSION-unpacking.md) |
Recovering the real bodies needs a route other than a frida memory dump on this target |
inferred |
| Stalker follow delivers no events on this ROM with this frida-server version | observed, single sample, unverified as a general rule |
High-frequency follow/unfollow on malloc can crash a system process |
observed, once |
No app on this device is currently hidden from root (magisk --denylist status enforced, list empty; Zygisk-Assistant installed but nothing configured) |
observed (recorded in EXTENSION-lsposed.md) — offered here because a hardened sample's sensitivity to a rooted device has a configuration, not just a cause |
What this pass did not establish
- Not a successful end-to-end unpack of the sample. The memory-dump route was refused, and the pass stopped at the skill's own two-strike rule instead of escalating into an evasion arms race.
- Not evidence that the sample is un-unpackable. The routes that do not use
fridaon the device — root-side/proc/<pid>/memextraction, the module route inreferences/lsposed-and-modules.md, and the static route already recorded for the Flutter payload — were not exhausted here. - Not a general claim about 360-jiagu, Flutter, or Android 11. Everything above is one device, one ROM, one server version, one sample.