mirror of
https://codeberg.org/reverse-skill-lab/apk-reverse
synced 2026-11-15 20:44:22 +08:00
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.
This commit is contained in:
@@ -525,8 +525,10 @@ Three rules that keep the descent honest:
|
|||||||
happens to be.
|
happens to be.
|
||||||
|
|
||||||
**The kernel layer is not reachable on this device, and the gate is a kernel version.**
|
**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
|
eBPF tracing (`stackplz`, kprobes) requires
|
||||||
kernel **5.10+**; the measured device is **4.14.186**, so that entire layer is closed
|
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
|
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`.
|
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
|
The consequence for this file: the descent above stops at the `svc` boundary, and a check
|
||||||
|
|||||||
@@ -60,7 +60,11 @@ nothing at delivery time.
|
|||||||
**C. Accept and report.** If the detection blocks the *deliverable* rather than your analysis — for
|
**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
|
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
|
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`,
|
**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.
|
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.
|
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).
|
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/<pid>/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
|
**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
|
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).
|
(`long-task-discipline.md` §the most expensive drift).
|
||||||
|
|||||||
@@ -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 |
|
| 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 |
|
| **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/<pid>/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 () |
|
| **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 |
|
| **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
|
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
|
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
|
## 7. When to stop escalating — the decision table
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
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,
|
(`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
|
`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 <domain> <type> 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 <domain> -t <type>` — 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 <type> -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
|
## 5. Keystore attestation — the check that survives client-side patching
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
|
| 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 |
|
| 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 |
|
| 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
|
The practical consequence of an anti-debugging layer is that Frida-based dynamic work will hit
|
||||||
|
|||||||
@@ -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 |
|
| `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 |
|
| **`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 |
|
| `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 |
|
| `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 |
|
| 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 |
|
| `x64dbg` | GUI, Windows | human-driven live debugging of a native Windows target |
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ ARM64_SYSCALLS = {
|
|||||||
173: "getppid", 174: "getuid", 175: "geteuid", 178: "gettid", 214: "brk",
|
173: "getppid", 174: "getuid", 175: "geteuid", 178: "gettid", 214: "brk",
|
||||||
215: "munmap", 216: "mremap", 220: "clone", 221: "execve", 222: "mmap",
|
215: "munmap", 216: "mremap", 220: "clone", 221: "execve", 222: "mmap",
|
||||||
226: "mprotect", 260: "wait4", 273: "set_robust_list", 276: "renameat",
|
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.
|
# The three groups that decide whether a userspace strategy is even applicable.
|
||||||
|
|||||||
Reference in New Issue
Block a user