Add deliverable-form discipline, execution identity, and four reusable scripts

Generalized from a long hardened-target engagement. Additions target the failure
modes that cost the most time and are not yet covered; nothing here is sample-specific.

New discipline
- SKILL.md: a third override rule -- deliver in the environment the request requires.
  A root-only or live-instrumentation result can look like success while failing the
  actual requirement (unrooted, installable, self-contained). Also adds two classify
  questions: what form the deliverable must take, and which architecture/library is
  actually executing.
- long-task-discipline.md: "the most expensive drift" -- a constraint table (privilege,
  modification form, ABI, network, persistence, distribution), the checkpoint question,
  and how to report a privileged workaround as a fallback rather than a deliverable.
  Plus "bound every wait": timeouts on every command, budgets calibrated from
  measurement so slow and hung are distinguishable, and sampling an observable you can
  inspect instead of sleeping blind.
- environment.md: look at the screen instead of driving blind; which of the two screen
  evidence kinds is trustworthy (an empty accessibility tree is common for
  runtime-rendered UI, where the image is primary rather than a fallback); emulator
  console control and adb recovery; preflight before every experiment block.
- native-and-so.md: what the device claims vs what the package manager chose vs what is
  actually mapped; translation layers and their consequences for hooks and native
  checks; fat-APK ABI selection.
- runtime-data.md: when the app rewrites your edit (server refresh vs local
  regeneration), why chmod/chown cannot work because the app deletes and recreates the
  file, and the immutable-attribute fix; decoding a value that only looks encrypted.
- framework-runtimes.md: string encoding traps (a UTF-8 scan of a UTF-16 snapshot
  returns nothing and reads as "the strings were stripped"), and non-ASCII obfuscated
  identifiers -- resolve by shape, not by name.
- pitfalls.md: P23 the fix lives in a file the app rewrites; P24 the artifact changed
  but the wrong one is executing; P25 "search found nothing" treated as "not there".

New scripts (parameterized, path-agnostic, all with explicit timeouts)
- preflight.py  environment check: device, root, ABI/translation, clock skew, leftover
                proxy/forwards, dead device server, chosen ABI, free space
- lib_map.py    what is actually mapped into a live process: path, base, architecture
                from the ELF header, and whether it came from the APK or was
                materialized at runtime
- blob_decode.py search (don't guess) the framing of a stored value, then re-encode the
                edited payload with the same parameters
- snap.py       bounded burst screenshots + control-tree capture, with a stall detector
                and an explicit verdict on whether the tree is usable

All four were exercised against a live device while writing: preflight correctly
flags multi-device ambiguity and detected the ARM translation layer; lib_map read real
ELF headers and identified the translation shim; blob_decode round-tripped a
rotation+deflate+base64 blob at realistic size in ~0.2s; snap detected a stalled screen
across three hashes.
This commit is contained in:
inliver233
2026-09-19 22:08:01 +08:00
parent 55432780fc
commit 0ea42265b7
12 changed files with 1537 additions and 34 deletions
+218
View File
@@ -0,0 +1,218 @@
#!/usr/bin/env python3
"""Report which native libraries are ACTUALLY mapped into a live process.
`getprop ro.product.cpu.abi` tells you what the device claims to be. It does not
tell you what is executing. Those differ more often than people expect:
* an x86_64 emulator running an app whose only native libraries are arm64,
because an ARM translation layer is present;
* a hardened app whose real libraries are materialized into its data
directory at runtime, so the paths you see are not the paths inside the APK;
* a fat APK where the package manager picked a different ABI than you assumed,
so you patch a `.so` that never loads.
This script reads the live mapping table and reports, per library: the path, the
base address, and the architecture of the actual ELF on disk. It classifies each
path so the interesting ones stand out (system / from-APK / runtime-materialized).
Requires root to read another process's maps.
Usage:
python lib_map.py --pkg com.example.app
python lib_map.py --pkg com.example.app --app-only
python lib_map.py --pid 1234 --all
python lib_map.py --pkg com.example.app --json
python lib_map.py --pkg com.example.app --grep ssl
Reading it:
* A library whose path sits under the app's data dir (not under the APK's
lib/<abi> dir) was written at runtime. That is a packer/dropper fingerprint
and it will not be present in a repacked APK -- patch or replace with care.
* If app libraries are 64-bit ARM but the device primary ABI is x86_64,
a translator is in play. Timing and some native checks will differ.
* If the library you patched does not appear here at all, your patch cannot
matter. That is a plan problem, not a patch problem.
"""
from __future__ import annotations
import argparse
import json
import os
import re
import subprocess
import sys
from collections import defaultdict
E_MACHINE = {
0x03: "x86", 0x3E: "x86_64", 0x28: "arm", 0xB7: "aarch64",
0x08: "mips", 0xF3: "riscv", 0x16: "s390", 0x15: "ppc",
}
TRANSLATOR_MARKERS = ("houdini", "libndk", "native_bridge", "libntv", "armtrans",
"libhoudini", "ndk_translation", "libarm", "exagear", "box64")
def run(cmd: list[str], timeout: int = 60) -> str:
try:
p = subprocess.run(cmd, capture_output=True, text=True, errors="replace", timeout=timeout)
return (p.stdout or "") + (p.stderr or "")
except Exception as e:
return "ERROR: %s" % e
def classify(path: str, pkg: str | None) -> str:
if path.startswith(("/system/", "/apex/", "/vendor/", "/product/", "/odm/", "/system_ext/")):
return "system"
if re.search(r"/data/app/", path):
return "apk-lib"
if pkg and (("/data/data/%s" % pkg) in path or ("/data/user/0/%s" % pkg) in path):
return "materialized"
if "/data/" in path:
return "data-other"
return "other"
def read_elf_arch(sh_fn, path: str) -> str:
"""Read the ELF header on the device and name the machine. '?' means the read failed."""
hdr = sh_fn("dd if='%s' bs=20 count=1 2>/dev/null | od -An -tx1 2>/dev/null" % path)
hexs = re.sub(r"[^0-9a-fA-F]", "", hdr)
if len(hexs) < 40:
return "?"
b = bytes.fromhex(hexs[:40])
if b[:4] != b"\x7fELF":
return "not-elf"
cls = {1: 32, 2: 64}.get(b[4], 0)
little = b[5] == 1
mach = int.from_bytes(b[18:20], "little" if little else "big")
if mach in E_MACHINE:
return E_MACHINE[mach] # the machine name already implies the width
return "0x%02x/%db" % (mach, cls or 0)
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
tgt = ap.add_mutually_exclusive_group(required=True)
tgt.add_argument("--pkg", help="target package name")
tgt.add_argument("--pid", type=int, help="target pid")
ap.add_argument("--adb", default=os.environ.get("ADB", "adb"))
ap.add_argument("--serial", default=None)
ap.add_argument("--all", action="store_true", help="include system libraries")
ap.add_argument("--app-only", action="store_true", help="only APK libs and runtime-materialized libs")
ap.add_argument("--grep", default=None, help="only paths matching this regex")
ap.add_argument("--no-arch", action="store_true", help="skip reading ELF headers (faster)")
ap.add_argument("--max-libs", type=int, default=120, help="cap how many libraries get an arch probe")
ap.add_argument("--json", action="store_true", dest="as_json")
a = ap.parse_args()
def sh(cmd: str, timeout: int = 60) -> str:
base = [a.adb] + (["-s", a.serial] if a.serial else []) + ["shell", cmd]
return run(base, timeout)
def su(cmd: str, timeout: int = 90) -> str:
base = [a.adb] + (["-s", a.serial] if a.serial else []) + ["shell", 'su -c "%s"' % cmd]
return run(base, timeout)
pid = a.pid
if a.pkg and not pid:
out = sh("pidof %s" % a.pkg).strip()
if not out:
print("no running process for %s -- launch it and let it settle first" % a.pkg, file=sys.stderr)
return 2
pid = int(out.split()[0])
maps = su("cat /proc/%d/maps" % pid)
if not maps.strip() or "No such file" in maps:
print("cannot read /proc/%d/maps (root required, or the process is gone)" % pid, file=sys.stderr)
return 2
entries: dict[str, dict] = {}
for line in maps.splitlines():
m = re.match(r"^([0-9a-f]+)-([0-9a-f]+)\s+(\S+)\s+([0-9a-f]+)\s+\S+\s+\S+\s*(.*)$", line)
if not m:
continue
start, end, perms, off, path = m.groups()
path = path.strip()
if not path or path.startswith("["):
continue
if not path.endswith(".so") and ".so." not in path:
continue
e = entries.setdefault(path, {"path": path, "perms": set(), "base": start, "count": 0})
e["perms"].add(perms)
e["count"] += 1
if perms.startswith("r-x") or perms.startswith("r--"):
e["base"] = min(e["base"], start)
dev_abi = sh("getprop ro.product.cpu.abi").strip()
abilist = sh("getprop ro.product.cpu.abilist").strip()
pkg = a.pkg
if not pkg:
cmdline = su("cat /proc/%d/cmdline" % pid).split("\x00")[0].strip()
pkg = cmdline or None
rows = []
probed = 0
for path, e in sorted(entries.items()):
kind = classify(path, pkg)
if a.app_only and kind not in ("apk-lib", "materialized"):
continue
# --grep is an explicit narrow request, so honour it across all kinds
if not a.all and not a.app_only and not a.grep and kind == "system":
continue
if a.grep and not re.search(a.grep, path):
continue
rows.append({"path": path, "base": e["base"], "kind": kind,
"perms": "/".join(sorted(e["perms"])), "maps": e["count"], "arch": "?"})
if not a.no_arch:
for r in rows[:a.max_libs]:
r["arch"] = read_elf_arch(su, r["path"])
probed += 1
# translation markers
markers = sorted({p for p in entries if any(t in p.lower() for t in TRANSLATOR_MARKERS)})
app_archs = sorted({r["arch"] for r in rows if r["kind"] in ("apk-lib", "materialized") and r["arch"] not in ("?", "not-elf")})
host_is_x86 = "x86" in dev_abi
translated = bool(markers) or (host_is_x86 and any(x.startswith(("arm", "aarch64")) for x in app_archs))
if a.as_json:
print(json.dumps({"pid": pid, "pkg": pkg, "device_abi": dev_abi, "abilist": abilist,
"translation_suspected": translated, "translator_markers": markers,
"libs": rows}, indent=2, ensure_ascii=False))
return 0
print("pid=%s pkg=%s" % (pid, pkg or "?"))
print("device primary abi=%s abilist=[%s]" % (dev_abi, abilist))
print("%-13s %-9s %-6s %-6s %s" % ("arch", "kind", "perms", "maps", "path"))
print("-" * 100)
for r in rows:
print("%-13s %-9s %-6s %-6d %s" % (r["arch"], r["kind"], r["perms"], r["maps"], r["path"]))
print("-" * 100)
print("mapped libraries shown: %d" % len(rows))
if not a.no_arch and probed < len(rows):
print("(architecture probed for the first %d; raise --max-libs to cover the rest -- '?' means not probed)"
% probed)
if markers:
print("\nTRANSLATOR MARKERS PRESENT:")
for mk in markers:
print(" " + mk)
print(" -> native ARM code is being translated. Timing differs from a real ARM device,")
print(" and native integrity checks may behave differently. Verify the final artifact")
print(" on the ABI the user actually runs.")
if app_archs:
print("\napp library architectures: %s" % ", ".join(app_archs))
if translated:
print("translation suspected: YES")
mat = [r["path"] for r in rows if r["kind"] == "materialized"]
if mat:
print("\nRUNTIME-MATERIALIZED libraries (not inside the APK):")
for p in mat[:20]:
print(" " + p)
print(" -> written at runtime by the app or its shell. A repacked APK will not contain")
print(" them in this location; account for that before assuming your patch is loaded.")
if not rows:
print("\nnothing matched. Widen with --all, or check that the process is the one that draws the UI.")
return 0
if __name__ == "__main__":
sys.exit(main())