# CellGuard — Project Dossier (rango / Pixel 10 Pro Fold) Kernel-enforced cellular security for a stock-GKI Pixel: block 2G/3G + weak/null ciphers, force LTE/5G-SA, detect IMSI-catchers/stingrays. This dossier records everything established so far — device facts, the solved build/load problem, the reverse-engineered modem control path, and the detection/blocking architecture. --- ## 1. Device baseline | Item | Value | |---|---| | Device | Pixel 10 Pro Fold, `rango`, Tensor G5 (`laguna`) | | Modem | Samsung **Shannon S5400** via **cpif/PCIe** (no UART AT) | | Kernel | stock GKI `6.6.102-android15-8-g6eb5b2a8c46b-ab14739656-4k` | | Kernel cfg | RANDSTRUCT_NONE, CFI_CLANG (strict), MODVERSIONS, LTO_NONE, **MODULE_SIG_PROTECT**, MODULE_SIG_FORCE **off**, KPROBES=y | | Root | KernelSU **LKM** | | Bootloader | unlocked (props spoofed locked/green) | | RIL | `/vendor/bin/hw/rild_exynos` + `libsitril*` (SIT protocol) | --- ## 2. Kernel module build & load — **SOLVED** ### Root cause of the original failures 1. **`Exec format error`** — the shipped `cellguard.ko` was built against a **GrapheneOS** `laguna` kernel tree (`6.6.129`, `RANDSTRUCT_FULL`). Under MODVERSIONS the kernel ignores the release string but compares the vermagic **flag suffix** + symbol CRCs; the RANDSTRUCT flag and mismatched CRCs (`module_layout` etc.) → `ENOEXEC`. 2. **`Protected symbol: kernel_write/kernel_read (-13)`** — stock GKI `MODULE_SIG_PROTECT` lets unsigned modules load but forbids importing a protected symbol subset. `kernel_read`/`kernel_write` are protected. ### Fix - **Build against stock GKI**, not Graphene: cloned `kernel/common` at the exact build SHA `6eb5b2a8c46b`, used the device's real `/proc/config.gz`, and the build's `vmlinux.symvers` (real CRCs). Native Debian **clang 19** (kCFI type-IDs are ABI-stable across clang versions). Result vermagic flags + all 49 harvestable CRCs (incl. `module_layout 0x4e276f37`) match the device. - **Resolve protected symbols at runtime**: `cellguard.c` now bootstraps `kallsyms_lookup_name` via a one-shot **kprobe** (`cg_lookup_name`) and calls `kernel_read`/`kernel_write` through pointers (`cg_kernel_read/write`, `cg_resolve_protected`). Zero protected symbols imported → loads unsigned, and is **portable across all stock android15/6.6 GKI devices** (KMI-frozen imports). ### Result `insmod` succeeds; `/dev/cellguard` (major 475) + `/proc/cellguard/status` live; module inert by default. Confirmed loaded via the KSU module path (`/data/adb/modules/cellguard/common/kmod/cellguard.ko`, loaded by `service.sh`). ### Artifacts - `common/kmod/cellguard.ko` — working stock-GKI build (sha256 `676ee6d1…`) - `common/kmod/cellguard.ko.badvermagic` — old broken build (backup) - `common/kmod/build-stock-gki.sh` — reproducible pipeline (auto-detects build, downloads GKI artifacts, clones exact source, builds) - `common/kmod/device.config` — offline config fallback --- ## 3. Modem control path — reverse-engineered (cpif / SIPC5) **Key finding: rango exposes NO AT tty.** The DT `iodevs` node has no `umts_atc*`/`nr_atc*`; `cpif.ko` contains zero AT strings and no AT parser. Both `cellguard.c` and RadioControl's `rc_shannon_cmd.c` assume an AT tty that does not exist — they only ever opened `/dev/umts_router` (a RAW relay) by luck, where `AT+…` bytes become an opaque SIPC5 payload the modem ignores. Channel names are **not in the driver** — `cpif` reads them from the device tree (`parse_dt_iodevs_pdata`). Extracted from the decompiled DTB (dtbo → `iodevs`): ### rango channel map (DT `iodevs`, enums proven vs `cpif.ko`) | /dev node | iod,ch | format | io_type | attrs | role | |---|---|---|---|---|---| | `umts_ipc0/1` | 0xf5 | FMT | MISC | 0x6000 | primary control — **held by RIL** | | **`oem_ipc0..7`** | **0x81** | **FMT** | MISC | 0x2000 | **OEM/SIT control (best for us)** | | `oem_test` | 0x89 | RAW | MISC | 0 | OEM test sink | | `umts_router` | 0x15 | RAW | MISC | 0 | opaque relay (no command parser) | | `umts_dm0` | 0x51 | RAW/DIAG | MISC | 0x4000 | **CP diagnostic egress (detection)** | | `rmnet` | 0xb5 | RAW | NET | 0x2000 | packet data (30 ch) | | `umts_rfs0` | 0x29 | RAW | MISC | 0 | remote fs | | `umts_boot0` | 0xf1 | BOOT | BOOTDUMP | 0x4200 | modem boot/flash | | `umts_rcs0/1`,`umts_wfc0/1`,`gnss_*`,`esim_tracer` | … | … | … | … | misc | Enum decodings (proven in `cpif.ko`): `iod,format {0=FMT,1=RAW,3=MULTI_RAW,4=BOOT}`; `iod,io_type {0=BOOTDUMP,1=MISC(char dev),2=NET,3=DUMMY}` (jump table `sipc5_init_io_device` @0xce78); `attrs` bit8 `0x100`=NO_LINK_HEADER, bit13 `0x2000`=MULTI_CHANNEL. ### Framing contract — **PROVEN** from `cpif.ko` (non-stripped) cpif frames/deframes **SIPC5 itself**; userspace exchanges **bare payloads**: - **TX** `ipc_write@0x1ad8`: `_copy_from_user` the buffer verbatim as payload, `skb_push(4)`, then `sipc5_build_config@0xccc8` (config base **0xF8**) + `sipc5_build_header@0xcd80` (`hdr[1]=iod,ch` from DT). On the wire a small frame = **`F8 `**. The channel byte + header come from the kernel — **never prepend your own**. - **RX** `rx_fmt_ipc@0xd87c` / `rx_raw_misc@0xdde0`: `skb_pull` the SIPC5 header, queue bare payload to `iod->rxq (+0x138)`; `ipc_read@0x18c4` returns **bare binary payload** (no `\r\n`, no `OK/ERROR`). - FMT channels carry a 7-bit transaction id (`sipc5_build_config` @0xcd60) → request/reply correlation. RAW channels don't (why `oem_ipc` FMT beats `umts_router` RAW for control). ### Corrected driver design (proposed patch, not yet applied) - `modem_paths[]` → `{umts_dm0, oem_ipc, umts_router}` (drop nonexistent AT ttys) - `at_cmd()` → bare-payload I/O; **no** CRLF, **no** OK/ERROR scan - add `sit_tx_enabled` gate, **default false** = passive/read-only, **zero baseband TX** (honors the modem-sensitivity rule) - same fix for `rc_shannon_cmd.c`; note it also needs the kprobe/kallsyms bootstrap before it can load on stock GKI --- ## 4. IMSI-catcher detection + band blocking — architecture Maximum control = a kernel module at the **cpif SIPC5 layer**, three stacked capabilities: 1. **Detection — DIAG (`umts_dm0`, ch 0x51), passive/read-only.** CP diagnostic stream carries layer-3 (RRC/NAS), cell measurements, cipher/auth params, paging. Flag: forced 2G/3G downgrade, null/weak cipher (A5/0, EEA0/NEA0), IMSI/IMEI identity requests, silent paging/SMS, abnormal cell (LAC/TAC without handover, MCC/MNC mismatch, implausible signal), skipped AKA. Reference: **P1sec SCAT** parses Samsung/Shannon DM. 2. **Blocking — SIT over `oem_ipc` 0x81 / `umts_ipc` 0xf5 (FMT).** `SET_ALLOWED_NETWORK_TYPE` → LTE+NR only; band-lock; null-cipher-off. Needs the SIT opcodes (RE in progress). 3. **Max control — kprobe-hook `cpif`** (symbols in kallsyms): hook RX (`rx_fmt_ipc`,`rx_raw_misc`) to see *all* AP↔CP frames; hook TX (`ipc_write`/`sipc5_build_header`) to **veto/rewrite** commands so 2G/3G can't be re-enabled behind us — a baseband IDS/firewall at the driver boundary. Build order: (1) passive DIAG detector now → (2) SIT blocking after opcode RE → (3) cpif kprobe hooks. --- ## 5. SIT / DIAG RE — in progress Pulled read-only to `/home/snake/re/`: `rild_exynos`, `libsitril.so`, `libril_sitril.so`, `libsit_oem.so`, `libsit_oem_proto.so`, `libsitril-client.so`, `vendor.radio.protocol.sit.{base,stream}.so`. Control handlers confirmed in `libsitril.so` strings: - RAT: `NETWORK_TYPE_BITMAP_LTE_ONLY` / `_LTE_WCDMA` / `_GSM_ONLY` … (setAllowedNetworkType) - Band: `DoSetBandMode` / `DoSetManualBandMode` / `DoQueryAvailableBandMode` - Cipher: `IsNullCipherAndIntegrityEnabledHandler` (+ Set) **Fable RE workflow running** (`rango-sit-diag-re`) to extract: the on-wire SIT frame layout (`vendor.radio.protocol.sit.stream.so`), the main/sub command IDs + param encoding for RAT/band/cipher, and the Shannon DM/DIAG record framing (SCAT). Output: SIT encoder spec + DIAG parser spec + proposed cellguard code blocks. --- ## 6. Toolchain & artifact locations | What | Where | |---|---| | cellguard source | `/home/snake/CellGuard/common/kmod/cellguard.c` | | build pipeline | `/home/snake/CellGuard/common/kmod/build-stock-gki.sh` | | stock kernel src (KDIR) | scratchpad `src/common` (@ SHA `6eb5b2a8c46b`) | | cpif driver (RE, non-stripped) | scratchpad `re/cpif_factory.ko` | | decompiled DT | scratchpad `re/alldts.txt` (`iodevs` @ line 4911) | | SIT/RIL binaries | `/home/snake/re/` | | Ghidra | `/home/snake/tools/samsung-dev/ghidra_12.0.4_PUBLIC/` (headless in `support/`) | | RadioControl twin module | `/home/snake/RadioControl/common/kmod/rc_shannon_cmd.c` | --- ## 7. Constraints (standing rules) - **No device ops (adb/insmod/push) without explicit per-action permission.** - **Never TX to the baseband without consent** — malformed OEM/FMT frames can reset the modem; `setenforce` also bounces RIL. Default builds are passive. - No AI attribution in any code/output. No stubs. `/home/snake` not `/tmp` (Pi `/tmp` is RAM). Surgical edits, one change at a time. --- ## 8. Next steps 1. Finish SIT/DIAG RE (workflow) → SIT encoder + DIAG parser specs. 2. Apply the corrected-channel/framing patch (passive/read-only default) → rebuild → load via KSU path. 3. Add the Shannon-DIAG detector (SCAT-derived) → real IMSI-catcher detection. 4. After opcode verification, gate-enable SIT blocking (RAT/band/cipher). 5. Optional: cpif kprobe RX/TX hooks for full monitor + veto enforcement. 6. Port `rc_shannon_cmd.c` to the same I/O layer + kallsyms bootstrap. ## 9. Backlog (deferred) - **IMSI-paging blocker (toggle).** No exposed modem command suppresses IMSI-paged responses (paging is baseband-autonomous). Buildable tiers: 1. Force **IMSI/SUPI encryption** via SIT `DoSetCarrierInfoImsiEncryption` (stops IMSI harvesting at the source) — available command, needs capture/verify. 2. **Detect + react**: flag IMSI paging from captured frames → toggle triggers detach / RAT-relock / alert. 3. **True suppression**: modem.bin firmware patch to drop IMSI-paged responses (deep, risky — separate effort). - **Full modem diagnostic-features access** (interactive DIAG/engineering mode, band control, signaling readouts) is a **separate app** (RadioControl scope), not CellGuard. CellGuard's raw CP-frame capture (`cgcap`) exists only to feed detection. Related: device anti-forensics hardening plan → `docs/anti-forensics-hardening.md`.