- 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
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
Exec format error— the shippedcellguard.kowas built against a GrapheneOSlagunakernel 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_layoutetc.) →ENOEXEC.Protected symbol: kernel_write/kernel_read (-13)— stock GKIMODULE_SIG_PROTECTlets unsigned modules load but forbids importing a protected symbol subset.kernel_read/kernel_writeare protected.
Fix
- Build against stock GKI, not Graphene: cloned
kernel/commonat the exact build SHA6eb5b2a8c46b, used the device's real/proc/config.gz, and the build'svmlinux.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.cnow bootstrapskallsyms_lookup_namevia a one-shot kprobe (cg_lookup_name) and callskernel_read/kernel_writethrough 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 (sha256676ee6d1…)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_userthe buffer verbatim as payload,skb_push(4), thensipc5_build_config@0xccc8(config base 0xF8) +sipc5_build_header@0xcd80(hdr[1]=iod,chfrom 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_pullthe SIPC5 header, queue bare payload toiod->rxq (+0x138);ipc_read@0x18c4returns bare binary payload (no\r\n, noOK/ERROR). - FMT channels carry a 7-bit transaction id (
sipc5_build_config@0xcd60) → request/reply correlation. RAW channels don't (whyoem_ipcFMT beatsumts_routerRAW 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_enabledgate, 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:
- 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. - Blocking — SIT over
oem_ipc0x81 /umts_ipc0xf5 (FMT).SET_ALLOWED_NETWORK_TYPE→ LTE+NR only; band-lock; null-cipher-off. Needs the SIT opcodes (RE in progress). - 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;
setenforcealso bounces RIL. Default builds are passive. - No AI attribution in any code/output. No stubs.
/home/snakenot/tmp(Pi/tmpis RAM). Surgical edits, one change at a time.
8. Next steps
- Finish SIT/DIAG RE (workflow) → SIT encoder + DIAG parser specs.
- Apply the corrected-channel/framing patch (passive/read-only default) → rebuild → load via KSU path.
- Add the Shannon-DIAG detector (SCAT-derived) → real IMSI-catcher detection.
- After opcode verification, gate-enable SIT blocking (RAT/band/cipher).
- Optional: cpif kprobe RX/TX hooks for full monitor + veto enforcement.
- Port
rc_shannon_cmd.cto 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:
- Force IMSI/SUPI encryption via SIT
DoSetCarrierInfoImsiEncryption(stops IMSI harvesting at the source) — available command, needs capture/verify. - Detect + react: flag IMSI paging from captured frames → toggle triggers detach / RAT-relock / alert.
- True suppression: modem.bin firmware patch to drop IMSI-paged responses (deep, risky — separate effort).
- Force IMSI/SUPI encryption via SIT
-
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.