# R8 family-prefix masquerade (adversarial PoC)

**Why this lives in the repo:** the original threat model on
`KNOWN_FP_FAMILY` described family suppression as a pure name-prefix
filter. Building this fixture surfaced a second defense layer the
docstring understated: the recogniser also requires `ACC_SYNTHETIC`
(0x1000) on the helper class (`r8_inversion.rs:796`).
kotlinc-emitted developer classes don't carry that flag, so the naive
masquerade — name a class in `androidx.*`, satisfy I4–I13 — doesn't
trip the recogniser at all. The fixture is the **negative control**:
proof that the gate works, and a regression sentinel for the day it
gets dropped or weakened.

A complete masquerade requires the attacker to additionally forge
`ACC_SYNTHETIC` (via smali, raw DEX emit, or post-process bit-flip).
The fixture's Kotlin source is the pre-image of that attack; the
forgery step is left as a future tooling extension (smali requires
additional CI dependencies).

This fixture is the **security-side mirror** of the `library_helper_rename`
fixture next door. That fixture demonstrates a benign FP: R8 minifies a
library helper class to a short name and the structural recogniser
fires on the minified shape. This fixture demonstrates the
**adversarial** counterpart: an attacker who controls the source can
name a malicious helper inside a `KNOWN_FP_FAMILY` namespace (here
`androidx.*`) and satisfy the recogniser's I4–I13 invariants by
construction, slipping past the family-prefix FP suppression in
mapping-less analysis.

The threat model is documented in detail on the `KNOWN_FP_FAMILY`
const in `tests/r8_fdroid_apk_sweep.rs`. Empirical data shows
0–2% structural attestation across the F-Droid `androidx`, `j$`,
`io.flutter`, `h1` family buckets in surveys of major app repositories
(n=643 samples).

## What the fixture does

`source/Main.kt` declares (verbatim, in the spoofed namespace):

```kotlin
package androidx.adversarial.poc

object Backdoor {
    @JvmStatic
    fun execute(i: Int, s: String): String = "PWNED:$i:$s"
}

class Main {
    fun call01() { println(Backdoor.execute(1, "a")) }
    // ... 24 more callers
}
```

DEX has no namespace enforcement: `androidx.*` is owned by AndroidX
toolchains by *convention*, not by runtime restriction. A direct DEX
toolchain (smali, raw bytecode emit) — or, as here, a Kotlin compiler
given `package androidx.adversarial.poc` — produces a class whose
FQCN starts with `androidx.` regardless of any AndroidX provenance.

The `call01..call25` methods give the `Backdoor.execute` helper 25
distinct callers, satisfying I9 (≥ 20 distinct callers). The
`execute` body is straight-line, arity 2, body size in I7's 3–99
byte range. R8 is run with `-dontoptimize` so the callers stay
separated and the helper is not inlined.

## What the obvious part is

The helper is literally named `Backdoor` and returns the string
`"PWNED:<i>:<s>"`. Anyone reading the decompiled output sees both
immediately — the masquerade does NOT hide the code from a human
analyst. What it hides is the **marker** in the mapping-less sweep's
summary statistics:

- `marker_count` records the recogniser firing on `Backdoor.execute`.
- `namespace_rollup` puts the marker in the `androidx` bucket.
- `namespace_rollup_attested` puts it in **zero** count, because
  `classify_synthetic_kind()` returns `Unknown` (no
  `$$ExternalSynthetic<kind>` infix, no `$EnumUnboxingLocalUtility`
  suffix — the attacker didn't bother to forge those).
- `is_in_known_fp_family("androidx.adversarial.poc.Backdoor")` returns
  `true`, so in the F-Droid sweep harness this marker is counted as
  family-classified and NOT counted toward `off_family_marker_count`
  (the F2 input).

Result: a real backdoor with a literal `"PWNED"` string sits inside the
recogniser's "this looks like legitimate library code, skip" bucket.

## Layout

```
source/
  Main.kt                # 25-caller masquerade
  proguard-rules.pro     # keep Backdoor + callers, allowshrinking,allowobfuscation
artifacts/
  .gitkeep               # placeholder
  classes.dex            # generated by regen script
  mapping.txt            # generated
  METADATA.toml          # generated
scripts/
  regen-family-prefix-masquerade-fixture.sh   # kotlinc + R8 pipeline
```

## Regenerating

Requires JDK 11+, kotlinc, and either `r8` on PATH or `R8_JAR` env var.

```sh
./tests/fixtures/r8/family_prefix_masquerade/scripts/regen-family-prefix-masquerade-fixture.sh
```

The script asserts at regen time that R8 did NOT outline the
adversarial slice (any non-zero outline annotation on
`androidx.adversarial.poc.*` would defeat the masquerade
demonstration) and that the class FQCN actually starts with
`androidx.adversarial.poc.*` in the mapping LHS.

## What the ratchet test asserts

`tests/r8_family_prefix_masquerade_fixture_ratchet.rs`:

1. **SKIP cleanly** if `artifacts/classes.dex` is missing.
2. **Recogniser fires.** Walking the minified DEX, the structural
   recogniser produces at least one BlockOutline marker on a
   `Landroidx/adversarial/poc/...` helper class (R8 may have
   shortened the class name within the namespace; the assertion is
   prefix-scoped).
3. **`is_in_known_fp_family()` returns true** on the marker class.
   Confirms the marker would be suppressed in a mapping-less sweep.
4. **`classify_synthetic_kind()` returns `Unknown`** on the marker
   class. Confirms zero structural attestation; the marker would
   contribute zero to `namespace_rollup_attested[androidx]`.
5. **Mapping carries no `com.android.tools.r8.outline` annotation**
   on the adversarial slice. Confirms this is NOT a real R8 outline;
   it's a hand-crafted lookalike.
6. **`"PWNED"` survives in the DEX string table.** Confirms the
   masquerade does NOT hide the code itself — only the summary
   statistics. An analyst's strings/grep pass would surface it; only
   the `marker_count` / `off_family` aggregate hides it.

The fifth and sixth assertions together capture the threat: the
recogniser's filter is wrong about this marker (5), but the rest of
the analyst's toolkit still works (6). The masquerade is a
**lens-blinding** attack on one specific summary, not a
**code-hiding** attack on the malware itself.

## Mitigation

The ground-truth gauge is **mapping-paired analysis**. When `mapping.txt`
is available (e.g., a product corpus with R8 build outputs), classify
markers via the `com.android.tools.r8.outline` annotation on the LHS
class name rather than the family prefix. An attacker without control
of the R8 toolchain cannot forge that annotation.

For mapping-less analysis, the `namespace_rollup_attested` ratio is
the masquerade-window diagnostic: low attestation rates per bucket
signal a wide window. See the threat-model section of the
`KNOWN_FP_FAMILY` docstring for the full discussion.
