Files
CellGuard/common/docs/cellguard-dossier.md
sssnake 9a3e3a0501 CellGuard: LTE/5G-NR lock + IMSI-catcher detection KSU module
- cgcap.ko: cpif CP-frame capture via kprobe/kallsyms, stock-GKI build
- ratlock.sh: hold modem to LTE+NR(5G), block 2G/3G downgrade
- cgdetect.sh: always-on behavioral cell-site-simulator detection
- cgctl.sh + WebUI: block/detect toggles, status + alerts
- build-stock-gki.sh: reproducible stock-GKI module build pipeline
2026-07-12 09:21:54 -07:00

10 KiB

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 drivercpif 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 <ch> <u16 len LE> <payload>. 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.