From eca9c19ee492bd24cfb4a72d67f4caf73f8cfac7 Mon Sep 17 00:00:00 2001 From: dabao1955 Date: Mon, 19 Oct 2026 22:08:14 +0800 Subject: [PATCH] Fourth audit pass: seccomp as anti-instrumentation, and the anti-debug sweep Two focused audits: the seccomp article under the lens of seccomp as the thing that blocks your tools (not just a sandbox fact), and a corpus-wide sweep for anti-debug material against the detection file that owns the topic. The seccomp audit's strongest finding was repo-internal: advanced-unpacking claimed seccomp filters were among the 5.10-gated layers "closed" on the 4.14 reference device -- while the repo's own extension notes observed Seccomp: 2 active on that very device, and the hardening file says "any modern kernel". The contradiction is resolved in favour of the measurement: seccomp is now named as the one syscall-layer mechanism NOT closed there. The hardening row gains the full decision set (TRACE hands a syscall to your tracer; KILL_PROCESS is the action that impersonates a detector), the bionic-installs-at-init timing with its unverified-network marker (the cnb.cool mirror carries frameworks/base, libcore and art but not system/seccomp or bionic -- the URLs to retry are recorded at the claim), the external-tracer-exempt mechanism note (a filter evaluates the installing process's own syscalls, so "they seccomp-blocked my attach" is structurally wrong), and Seccomp_filters>1 as the target-installed- filter signal. The detection funnel gains the fourth cause its environment table lacked: the process's own filter denying the agent's syscall -- Seccomp: 2 is the normal state of every zygote-forked app, and the discriminator is a SIGSYS/SYS_SECCOMP tombstone naming YOUR syscall, vanishing in the control run. This also finally interprets the Seccomp field anti_detect_probe.js has been capturing all along. Snowblind (2024) lands as a named pattern only -- recognition strings in the third-party-builds marker table and a Step-2C rationale row; the filter+handler implementation stays out on the stated line: recovery changes the analyst's observation channel, weaponization makes the target's policy stop applying. svc_scan.py learns syscall 277; seccomp-tools joins the toolchain table as an offline filter reader with unverified grades. The anti-debug sweep's verdict was the opposite shape, stated plainly: on this topic the corpus is the shallow party and the repo is the owner -- TracerPid, loader discipline, Frida-detection specifics are all already covered deeper, and no corpus-driven table was manufactured. What it did add: the explain-the-target half of SELinux policy reading (wildcard grants, attribute memberships, missing type transitions -- why the app reaches what you thought was closed, with the read/inject boundary drawn), the magiskpolicy one-liner for the measured frida-server denial, and the three kernel-observation gate knobs (kptr_restrict, dmesg_restrict, perf_event_paranoid) in the ladder rule. Gates re-run: routing coherent, 414 section references zero dangling, repo clean (after check_repo caught a broken string literal this pass introduced -- the gate did its job), pytest 751 passed / 194 skipped / 25 xfailed. --- .../references/advanced-unpacking.md | 6 ++++-- .../references/detection-and-anti-analysis.md | 17 ++++++++++++++++- .../kernel-and-environment-hardening.md | 9 +++++++-- .../apk-reverse/references/system-ipc-audit.md | 13 ++++++++++++- .../references/third-party-builds.md | 2 +- skills/apk-reverse/references/toolchain.md | 1 + skills/apk-reverse/scripts/svc_scan.py | 2 +- 7 files changed, 42 insertions(+), 8 deletions(-) diff --git a/skills/apk-reverse/references/advanced-unpacking.md b/skills/apk-reverse/references/advanced-unpacking.md index 59d471a..4af8ffe 100644 --- a/skills/apk-reverse/references/advanced-unpacking.md +++ b/skills/apk-reverse/references/advanced-unpacking.md @@ -525,8 +525,10 @@ Three rules that keep the descent honest: happens to be. **The kernel layer is not reachable on this device, and the gate is a kernel version.** -eBPF tracing (`stackplz`, kprobes, `seccomp` filters pushed from userspace) requires -kernel **5.10+**; the measured device is **4.14.186**, so that entire layer is closed +eBPF tracing (`stackplz`, kprobes) requires +kernel **5.10+**; the measured device is **4.14.186**, so that layer is closed (seccomp is the +one syscall-layer mechanism *not* closed there — a filter was observed active on this very device; +see `kernel-and-environment-hardening.md` §4) here regardless of tooling. `kernel-and-environment-hardening.md` maps the route and its version gate; do not spend a session on the layer before checking `adb shell uname -r`. The consequence for this file: the descent above stops at the `svc` boundary, and a check diff --git a/skills/apk-reverse/references/detection-and-anti-analysis.md b/skills/apk-reverse/references/detection-and-anti-analysis.md index bdad00d..fc41fd1 100644 --- a/skills/apk-reverse/references/detection-and-anti-analysis.md +++ b/skills/apk-reverse/references/detection-and-anti-analysis.md @@ -60,7 +60,11 @@ nothing at delivery time. **C. Accept and report.** If the detection blocks the *deliverable* rather than your analysis — for example, the app verifies something the user's device will not have — then the honest output is a statement of what is blocked, with the evidence. Do not ship a build whose only purpose is to defeat a -detector you were not asked about. +detector you were not asked about. One named threat pattern fits here: **Snowblind (2024)** — an added +native library installs a seccomp filter (`SECCOMP_RET_TRAP` on `open`-family) plus a `SIGSYS` +handler that fakes the results, so a *runtime* observation contradicts a *static* integrity check +(recognition strings in `third-party-builds.md`'s marker table; the technique itself is not reproduced +here). **One interactive escape hatch exists before B:** a human-driven gdbserver session (`references/toolchain.md`, the GDB form in the Tier-2 row) — the AI names the commands, the user runs them and pastes the output. @@ -68,6 +72,17 @@ It survives *frida-specific* detection (the Stage-1 measured mappings) while sti detection (`TracerPid`) — so it discriminates the two families before you decide the check is unbeatable. Observer-only: the session must not also modify state (Step 2B). +**The seccomp discriminator** — the fourth cause the environment table does not name: *the +process's own filter denied the syscall my agent just issued.* `Seccomp: 2` in +`/proc//status` is the **normal** state of every zygote-forked app — its presence is not +hostility. The discriminator is a death whose tombstone reads `signal 31 (SIGSYS), code 1 +(SYS_SECCOMP)` **naming the syscall your instrumentation called** (or an `EPERM`/`ENOSYS` on +exactly that syscall), vanishing in the instrumentation-free control run. `Seccomp_filters: >1` +means a *target-installed* extra filter — see `kernel-and-environment-hardening.md` §4 for the +mechanism and the decision set; observe denials via `dmesg` seccomp-audit lines and +`logcat -s seccomp SIGSYS`. *(unverified — absorbed from milksu.org notes, retrieved 2026-10-19; +the probe already captures the `Seccomp:` field, this gives it its reading)* + **Signal to stop escalating:** you have spent more effort on the analysis environment than on the change the user asked for. That is the drift this skill treats as most expensive, in its runtime form (`long-task-discipline.md` §the most expensive drift). diff --git a/skills/apk-reverse/references/kernel-and-environment-hardening.md b/skills/apk-reverse/references/kernel-and-environment-hardening.md index d87fc2b..536094d 100644 --- a/skills/apk-reverse/references/kernel-and-environment-hardening.md +++ b/skills/apk-reverse/references/kernel-and-environment-hardening.md @@ -146,7 +146,7 @@ uname -r # on the device. The gate for most of this section is 5.10+ | Route | What it gives you | Prereqs | Strength | |---|---|---|---| | **eBPF kprobes/uprobes** | Fire a BPF program on kernel or userspace function entry; observe syscall arguments/results system-wide, invisible to the target process | GKI 5.10+ kernel with the BTF/tracing config; root; toolchain (bpftrace or a custom loader) | `inferred` — documented upstream, not run here | -| **seccomp-BPF filter** | Per-process syscall allow/deny — can make a syscall **fail**, and seccomp can be installed by the app on itself (no root needed for self-filtering) | Any modern kernel; but a filter you install into the target requires ptrace/zygisk injection first | `inferred`. **Key limitation**: seccomp can reject or error a syscall (SECCOMP_RET_ERRNO/TRAP); it cannot rewrite the *content* of what a successful read returns. It closes doors; it does not paint them. | +| **seccomp-BPF filter** | Per-process syscall allow/deny — decision set ALLOW / ERRNO / TRAP / **TRACE** (hands the syscall to a tracer) / **KILL_PROCESS** (the action that impersonates a detector: app just dies) | Any modern kernel; but a filter you install into the target requires ptrace/zygisk injection first | `inferred`. **Key limitation**: seccomp can reject or error a syscall (SECCOMP_RET_ERRNO/TRAP); it cannot rewrite the *content* of what a successful read returns. It closes doors; it does not paint them. Platform timing *(unverified — network: cnb.cool mirror carries no system/seccomp or bionic repo; URLs to check when possible: `…/platform/system/seccomp/-/git/raw/main/bpf/app.cc`, `…/platform/bionic/-/git/raw/main/libc/bionic/seccomp.cpp`)*: the app-process filter is installed by bionic at libc init — *before any app code runs* — so an in-process agent always operates under it; but a filter evaluates the **installing process's own** syscalls, so the platform filter cannot deny an *external* tracer's ptrace — "they seccomp-blocked my attach" is structurally wrong; what it blocks is your in-process agent's syscalls. `Seccomp: 2` in `/proc//status` is the normal state of every zygote-forked app; `Seccomp_filters: >1` is the signal of a *target-installed* extra filter. | | **Kernel module hooking** (KPM on APatch; out-of-tree LKM elsewhere) | Inline hooks and syscall-table hooks in kernel space — the layer that *can* rewrite what a `/proc` read returns | APatch (KPM) or a self-built kernel/LKM load path; kernel-dev skills | `inferred`. This is genuine kernel development — out of scope for this skill () | | **Zygisk injection** (not kernel, but below the target's defences) | Run code in the target's process from zygote fork, before `.init_array` of the app's own libs | Magisk/KernelSU+ZygiskNext + a Zygisk module; no ptrace involved | `measured` as a framework (LSPosed runs this way on the reference device); a purpose-built module for a given target is `inferred` here | @@ -269,7 +269,12 @@ honest outcome (`observed` on that device): The generalisable rule: **enumerate the ladder for *your* device once, write the one-line environment facts, and stop re-deriving them mid-task.** A closed rung is a route decision, not a -failure. +failure. Three one-line gate reads belong in that enumeration, because they decide whether +kernel-side observation is open before anyone plans it *(inferred — absorbed from milksu.org notes, +retrieved 2026-10-19)*: `/proc/sys/kernel/kptr_restrict` (0 = kallsyms addresses readable), +`/proc/sys/kernel/dmesg_restrict` (0 = dmesg readable without root), and +`/proc/sys/kernel/perf_event_paranoid` (≤1 = userspace perf counters) — with zeroed kallsyms +addresses as the visible symptom of the first. ## 7. When to stop escalating — the decision table diff --git a/skills/apk-reverse/references/system-ipc-audit.md b/skills/apk-reverse/references/system-ipc-audit.md index 6650114..47dfb91 100644 --- a/skills/apk-reverse/references/system-ipc-audit.md +++ b/skills/apk-reverse/references/system-ipc-audit.md @@ -224,7 +224,18 @@ binary, or generate the exact allow rule with `adb shell dmesg | audit2allow -p pulling the policy from `/sys/fs/selinux/policy`, and audit what it emits before applying it (`audit2allow` is deliberately over-broad). For *reading* what an app domain may do, `sesearch -A -s untrusted_app sepolicy.bin` answers "can this app even reach X" questions that -save a hook that could never fire. +save a hook that could never fire. For the measured frida-server execution denial specifically, +the live one-line fix is `magiskpolicy --live "allow file execute"` — or run the +binary in a different domain altogether. + +The same policy file answers the *explain-the-target* half — why the app reaches something you +thought was closed: query wildcard grants (`sesearch -A -s -t ` — an `allow … *` +row like `system_app vendor_data_file:file *` is the one-line explanation for "why can this +preinstalled app read vendor data"), attribute memberships (`seinfo -t -x`), and missing +`type_transition` rules. A policy read explains the behaviour without touching the running +system — and that is the boundary: reading the target's policy to explain it is in scope; +injecting rules or flipping enforcement to defeat it is root-framework development, not this +skill. *(unverified — absorbed from milksu.org notes, retrieved 2026-10-19)* ## 5. Keystore attestation — the check that survives client-side patching diff --git a/skills/apk-reverse/references/third-party-builds.md b/skills/apk-reverse/references/third-party-builds.md index a4d2ab8..10cd36b 100644 --- a/skills/apk-reverse/references/third-party-builds.md +++ b/skills/apk-reverse/references/third-party-builds.md @@ -50,7 +50,7 @@ re-protected an already-protected app. Look for independent, coexisting families |---|---| | Commercial packer | shell `Application`/`appComponentFactory` in the manifest, a `lib*protect*.so`, encrypted assets, `assets/*.jar` with near-maximal entropy | | DEX-to-native (dex2c) | a "stub" `lib*.so` containing the tool's name, `EntryPoint`/`protect`-style symbols, and **business class names as strings** alongside a near-empty dex layer | -| Anti-debugging / RASP | strings such as `TracerPid`, `ptrace`, `NoNewPrivs`, `CheckPtraceSelf`, `AntiFrida`, plus a self-protection log path | +| Anti-debugging / RASP | strings such as `TracerPid`, `ptrace`, `NoNewPrivs`, `CheckPtraceSelf`, `AntiFrida`, plus a self-protection log path; **Snowblind-shape**: a `PR_SET_SECCOMP`/`SECCOMP_MODE_FILTER` call together with `sigaction(SIGSYS)` — a filter+handler pair that fakes syscall results (see `detection-and-anti-analysis.md` Step 2C) | | Generic hardening marker files | small text files under `assets/` naming a protection vendor and a protection timestamp | The practical consequence of an anti-debugging layer is that Frida-based dynamic work will hit diff --git a/skills/apk-reverse/references/toolchain.md b/skills/apk-reverse/references/toolchain.md index 01f9ca9..0c98740 100644 --- a/skills/apk-reverse/references/toolchain.md +++ b/skills/apk-reverse/references/toolchain.md @@ -306,6 +306,7 @@ deadline reported nothing about the target, only about your deadline. | `capstone` (python) | library | decoding in your own scripts — see the silent-stop trap below | | **`svc_scan.py`** | `python scripts/svc_scan.py libfoo.so [--context 3]` | **names the syscall behind an inline `svc`** and reports which PT_LOAD segment each hit is in. Use it *before* planning a libc-level hook: a module with its own `exit`/`kill` `svc` sites is not observable through libc, and one without them means a missing event is a finding about your hook. Read the neighbours (`--context`) — a byte scan for `svc` also matches inside data. Measured on the test device: device `libc.so` = 4 termination sites (its own exports), a shell library's 21 hits = all data | | `keystone` (python) | library | assembling a short patch when you are editing bytes by hand | +| `seccomp-tools` | `gem install seccomp-tools`; `seccomp-tools disasm ./filter.bpf`, `emu ./filter.bpf` | offline disassembly/emulation of a captured seccomp BPF filter — the reading tool when a target-installed filter is the blocker (`detection-and-anti-analysis.md` §the seccomp discriminator). `dump --pid` catches the filter at install time by attaching to the install syscall — its on-device Android behaviour is unproven here. *(unverified — tool URL not reachable this session; absorbed from milksu.org notes, 2026-10-19)* | | `pyelftools` (python) | library | **section-based — unreliable on hardened targets.** Prefer hand-walking `PT_LOAD`/`PT_DYNAMIC`, as `scripts/elf_plt.py` does | | IDA Pro | GUI, **or headless via `idalib-mcp`** | the strongest decompiler output. Do not assume it needs a human in front of it — a headless MCP server exists (see *MCP tool servers* below). Ask for a human only when that setup is genuinely unavailable | | `x64dbg` | GUI, Windows | human-driven live debugging of a native Windows target | diff --git a/skills/apk-reverse/scripts/svc_scan.py b/skills/apk-reverse/scripts/svc_scan.py index 9676fef..ce800e0 100644 --- a/skills/apk-reverse/scripts/svc_scan.py +++ b/skills/apk-reverse/scripts/svc_scan.py @@ -68,7 +68,7 @@ ARM64_SYSCALLS = { 173: "getppid", 174: "getuid", 175: "geteuid", 178: "gettid", 214: "brk", 215: "munmap", 216: "mremap", 220: "clone", 221: "execve", 222: "mmap", 226: "mprotect", 260: "wait4", 273: "set_robust_list", 276: "renameat", - 278: "getrandom", 280: "utimensat", 291: "statx", + 277: "seccomp", 278: "getrandom", 280: "utimensat", 291: "statx", } # The three groups that decide whether a userspace strategy is even applicable.