Files
reverse-skills-max/vendor-new/apk-reverse/docs/tool-verification/EXTENSION-kernel-weapons.md

12 KiB

Extension verification — kernel-level weapons (templates, gates, and a closed door)

Covers the extension pass that added scripts/kernelsu_syscall_mask.py and the §4a section of references/kernel-and-environment-hardening.md. Strength labels as defined in this directory's README: observed (exact command + output here), inferred (follows from observed facts, step not executed), unverified (assumed or externally reported, not independently confirmed).

Reference environment: Windows 11 host, Python 3.14.0; <DEVICE> = rooted Android 11 (API 30) arm64-v8a, Magisk alpha, no APatch/KernelPatch.

Headline: the generator, the gate table and the userspace module skeleton are measured. No kernel-side artifact was compiled or loaded anywhere, and every route is closed on the reference device. That is the deliverable — a template plus a measured statement of the door.


1. Kernel gate, measured on the device — observed

$ adb shell 'uname -r; cat /proc/version'
4.14.186+
Linux version 4.14.186+ (nobody@android-build) (Android (6443078 based on r383902) clang version
11.0.1 (https://android.googlesource.com/toolchain/llvm-project b397f81060ce6d701042b782172ed13bee898b79),
LLD 11.0.1 (/buildbot/tmp/tmp6_m7QH ...)) #1 SMP PREEMPT Wed Mar 30 23:32:42 CST 2022

4.14.186 is below the 5.10 GKI gate for the eBPF route, and the device carries no KernelPatch-patched image. Read-only query; no device lock was needed and no state was changed.

2. Host toolchain probe — observed

$ python skills/apk-reverse/scripts/kernelsu_syscall_mask.py gates
== kernel-side routes and their gates ==

  eBPF tracepoint/kprobe       GKI kernel 5.10+ (Android 12+)
                               why:  Android's BPF/tracing support as bpftrace-class tooling and
                                     stackplz expect it is a GKI-era feature; BTF is required for
                                     CO-RE style probes.
                               check: uname -r  ->  expect 5.10.x or newer; ls /sys/kernel/btf/vmlinux

  KernelPatch / APatch KPM     KernelPatch-patched boot image + kpm toolchain
                               why:  KPMs load through kpimg injected into the kernel image's
                                     payload segment; building one needs a bare-metal ARM64
                                     compiler (aarch64-none-elf-gcc), not the NDK.
                               check: ls /data/adb/ | grep -i apatch ; which aarch64-none-elf-gcc

  Out-of-tree LKM              Kernel source matching the device's exact version + vermagic
                               why:  A module built against a different kernel revision is refused
                                     at load time on a mismatched vermagic, and the device's kernel
                                     source is frequently not published at all.
                               check: uname -r ; look for a matching kernel source tree for the device

  seccomp-BPF (for contrast)   any modern kernel, but it is not this route
                               why:  seccomp can make a syscall FAIL (SECCOMP_RET_ERRNO/TRAP); it
                                     cannot rewrite the CONTENT a successful read returns. It closes
                                     doors, it does not paint them -- which is the whole requirement
                                     for a spoofed /proc read.
                               check: n/a -- this is a capability ceiling, not a version check

== host probe (what can be checked from here) ==

  bare-metal ARM64 compiler: NOT FOUND
     the KPM route cannot be built from this host as configured
  ndk-build:   NOT FOUND
  make:        NOT FOUND

  A kernel source tree, a patched boot image and the device's own kernel headers cannot be
  checked from the host; they are device facts.

The three NOT FOUND lines are the measured reason the KPM route cannot be built from here, and they are also consistent with ENV.md's record that the machine has no Android NDK. The LKM route additionally needs kernel source that does not exist for this device.

3. generate — observed

$ python skills/apk-reverse/scripts/kernelsu_syscall_mask.py generate \
      --out tools/_work/t5-vmp/sysmask --package '<PKG>'
generated module scaffold in tools/_work/t5-vmp/sysmask
  module.prop                      165 B
  customize.sh                     166 B
  post-fs-data.sh                  830 B
  service.sh                       467 B
  uninstall.sh                     298 B
  config/syscall_mask.json        1079 B
  kpm/syscall-mask.c              8096 B
  kpm/Makefile                     880 B
  lkm/syscall-mask.c              2434 B
  ebpf/syscall_mask.bpf.c         4338 B
  README.md                       3752 B

STATUS: the userspace module is installable; every kernel-side file is an UNVERIFIED template.
        No kernel code was compiled or loaded.

module.prop, as generated (the package identity is the <PKG> placeholder by design — this file carries no real package name):

id=syscall-mask
name=Syscall Mask
version=0.1.0
versionCode=1
author=apk-reverse
description=config-driven syscall return masking (openat/read/stat on /proc/self/*)

The configuration table rendered into the KPM template from config/syscall_mask.json, so the rule set is data rather than hand-edited C:

static const struct mask_rule rules[] = {
    { "/proc/self/maps", 0, MASK_DENY, -2, 0 },
    { "/proc/self/maps", 0, MASK_FAKE_SIZE, -2, 0 },
    { "/proc/self/status", "TracerPid", MASK_FILTER_LINE, -2, 0 },
    { "/proc/self/status", "TracerPid", MASK_FILTER_LINE, -2, 0 },
};

And the KPM lifecycle surface, which is the part an interface drift would break first:

int kfunc_def(strncpy_from_user)(char *dst, const char __user *src, long count);
/* fp_hook_syscalln(__NR_openat, before_openat, 0, 0);    */
/* fp_hook_syscalln(__NR_newfstatat, before_newfstatat, 0, 0); */
/* fp_hook_syscalln(__NR_read, before_read, 0, 0);        */
KPM_INIT(mask_init);
KPM_EXIT(mask_exit);

Those names (KPM_NAME / KPM_INIT / KPM_EXIT, kfunc_def, fp_hook_syscalln, hook_fargs4_t, syscall_argn, args->skip_origin, args->ret) are taken from a public KPM development write-up rather than invented — blackr0ck, APatch KPM 开发:绕过 DirtySepolicy / Duck Detector 的 SELinux Root 检测, bbs.kanxue.com/thread-291665.htm, fetched 2026-02. They are still unverified here: nobody compiled against a kpmodule.h in this pass, and the generated template says so in its own header for exactly that reason.

The registration calls are left commented out in the template. A generated file that arms a kernel hook by default is a file that arms a kernel hook the first time somebody runs make in a hurry.

4. verify — observed

$ python skills/apk-reverse/scripts/kernelsu_syscall_mask.py verify --dir tools/_work/t5-vmp/sysmask
== verifying tools/_work/t5-vmp/sysmask ==
  module.prop: author=apk-reverse, description=config-driven syscall return masking
               (openat/read/stat on /proc/self/*), id=syscall-mask, name=Syscall Mask,
               version=0.1.0, versionCode=1
  config: 4 rule(s), actions=['deny', 'fake_size', 'filter_line']
  kpm  target present: Makefile, syscall-mask.c
       status: UNVERIFIED TEMPLATE -- not compiled, not loaded
  lkm  target present: syscall-mask.c
       status: UNVERIFIED TEMPLATE -- not compiled, not loaded
  ebpf target present: syscall_mask.bpf.c
       status: UNVERIFIED TEMPLATE -- not compiled, not loaded
  kpm/syscall-mask.c: KPM_* lifecycle macros present, kfunc_def not extern

== 0 problem(s): structure is consistent ==
   (this says nothing about whether the kernel side compiles or loads)

verify is deliberately read-only and deliberately narrow: it checks module.prop fields, the rule JSON, and the one static trap in the KPM template that has a documented consequence (an extern kfunc_def becomes *UND* and the loader refuses the module). It states on its last line what it does not establish, because "0 problems" on a template is the easiest sentence in this file to over-read.

4a. A false positive this check produced, and its fix — observed

The first run reported:

== 1 problem(s) ==
  - syscall-mask.c declares kfunc_def with `extern`, which the loader rejects as an unknown symbol
verify rc=1

The template does not declare it with extern — its comment explains why extern is forbidden, and the check's own regex ran from that prose across to the real declaration's semicolon. A linter that reads comments as code produces reports nobody can act on. The fix strips /* */ and // comments before any token check (_strip_c_comments). Both the false positive and the fix are recorded here rather than quietly repaired, because the failure mode — a check that fires on its own documentation — is one a reader will meet again.

5. The userspace module's ceiling — observed by construction, and worth stating plainly

post-fs-data.sh and service.sh in the generated skeleton do not load anything. That is not an omission: a KernelSU/Magisk/APatch-userspace module runs as root in the normal world and cannot change what a syscall returns. The generated README.md says this in its first paragraph, and post-fs-data.sh carries the reasoning in a comment:

# The kernel side (if you built and loaded one) owns the syscall table; this
# script's whole job is to make the configuration visible to it and to leave a
# record that the module actually ran. Do not put a syscall hook here: this is
# userspace, and nothing here can change what openat/read/stat return.

The uninstall hook likewise refuses to unload a kernel module, because a silent unload during package removal is how a device ends up in a boot loop.

6. eBPF route — gate table and the capability ceiling

Route Gate Check on the device Status here
eBPF tracepoint/kprobe GKI 5.10+ with tracing/BTF uname -r; ls /sys/kernel/btf/vmlinux closed — device is 4.14.186
seccomp-BPF any modern kernel n/a not this route: it can only fail a syscall, not rewrite a successful read
KernelPatch / APatch KPM patched boot image + aarch64-none-elf-gcc ls /data/adb/ | grep -i apatch; which aarch64-none-elf-gcc closed — neither present; patch-the-boot-image risk declined
Out-of-tree LKM kernel source matching vermagic uname -r + a matching source tree closed — no source for this device

The probe template (ebpf/syscall_mask.bpf.c) is a sys_enter_openat tracepoint observer with a uid filter map and a ringbuf event, and it documents two dead ends in the file itself: bpf_override_return() only applies to ALLOW_ERROR_INJECTION functions, which raw syscall entries are not, and a tracepoint cannot rewrite a result at all. A tracepoint observes; it does not spoof. The honest eBPF shape for this job is therefore observation — which is still worth having, because §5 of the reference file requires you to establish whether the target uses a raw svc or a libc call before designing any spoof.

7. What was not done

  • No kernel-side code was compiled. Not the KPM, not the LKM, not the eBPF program. No kpmodule.h, no kernel headers, no bpftool, no bare-metal ARM64 compiler on this host.
  • Nothing was loaded, flashed or activated on <DEVICE>. No boot-image patch, no module load, no kernel modification. The task forbids it for good reason: a kernel module that faults takes the device down before it can report why.
  • The KPM API surface is unverified. The macro and helper names follow a public write-up; the argument arity of fp_hook_syscalln in particular is marked TODO(verify-on-device) in the generated source rather than guessed, because a wrong arity in a hook registration is a load-time failure at best.
  • The struct-stat size offset is a TODO, not a value. The newfstatat handler deliberately does not carry a magic offset: it is ABI-specific and copying one from a different architecture is how a spoof corrupts an unrelated field.
  • verify establishes structure, not capability. Its own output says so.