A hardened app brackets a sensitive code region with two timestamps (e.g. a monotonic clock read via System.nanoTime() / mach_absolute_time) and measures the elapsed time to execute N instructions between them:
Δt = t(checkpoint B) − t(checkpoint A)
Under normal execution each instruction costs roughly a fixed CPU time ti, so Δtclean ≈ N·ti plus small scheduler jitter. The app calibrates a baseline over several clean runs and derives a statistical threshold:
µ, σ = mean, stdev of clean Δt samples
Threshold = µ + k·σ
A debugger attached via ptrace (Android) or a jailbreak-tier hook (iOS) intercepts the process to single-step or insert breakpoints, adding a per-instruction trap-and-resume overhead ttrap:
Δtdebugged ≈ N·(ti + ttrap)
Because ttrap is orders of magnitude larger than a normal instruction, Δtdebugged blows past µ + k·σ and the check flags tampering — this is the same timing side-channel behind real anti-debug primitives (ptrace(PTRACE_TRACEME) self-attach races, rdtsc-delta checks, syscall-latency probes). Raising k trades detection sensitivity for a lower false-positive rate on slow or throttled devices; too low a k trips on ordinary jitter, too high a k lets a patient attacker single-step under the threshold.
- Instructions per check — length of the guarded code region; longer regions accumulate more trap overhead when debugged, but also more natural jitter.
- Single-step trap overhead — µs added per instruction while a debugger is attached and stepping.
- Detection sensitivity k·σ — how many standard deviations above the clean baseline mean before the check fires.
- Attach Debugger — toggles the attack; the instruction stream turns red and the timing gate reacts on the next run.