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
This commit is contained in:
sssnake
2026-07-12 09:21:54 -07:00
commit 9a3e3a0501
27 changed files with 3289 additions and 0 deletions

304
common/kmod/cgcap.c Normal file
View File

@@ -0,0 +1,304 @@
// SPDX-License-Identifier: GPL-2.0
/*
* cgcap.ko — Shannon SIT/RCM frame capture (read-only)
*
* Kprobes cpif's ipc_write() — the .write of the FMT/RAW char devices
* (/dev/umts_ipc0 ch 0xf5, /dev/oem_ipc0 ch 0x81, ...). rild/libsitril
* write the BARE SIT frame there and cpif prepends the SIPC5 link header
* in-kernel, so the buffer handed to ipc_write IS the exact SIT payload.
*
* We copy that payload out (nofault, atomic-safe), tag it with the io_device
* channel id, and expose a decoded ring at /proc/cgcap/frames. This lets us
* learn the real on-wire command bytes for setAllowedNetworkType / band-lock /
* null-cipher by triggering those operations from Android and reading what
* rild actually sent — no transmission, no guessing.
*
* SIT/RCM header (LE), proven from vendor.radio.protocol.sit.base.so:
* off0 u8 type 0=REQ 1=RESP 2=IND
* off2 u16 id (hi=group/main, lo=sub-cmd)
* off4 u16 len total frame length incl header
* off6 u32 token transaction id (REQ/RESP only)
* off10 u8 error (RESP only)
* off12 params
* io_device->ch lives at struct offset 0x114 (proven from cpif_probe).
*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/kprobes.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/spinlock.h>
#include <linux/ktime.h>
#include <linux/skbuff.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("snake");
MODULE_DESCRIPTION("Shannon SIT/RCM frame capture via cpif ipc_write kprobe");
MODULE_VERSION("1.0");
#define IOD_CH_OFFSET 0x114 /* io_device->ch (u8/u16), cpif_probe RE */
#define CAP_DATA 160 /* bytes of payload kept per frame */
#define CAP_RING 512 /* ring capacity */
struct cap_ent {
u64 seq;
u64 ns;
u32 ch;
u32 len; /* full byte count of the transfer */
u32 n; /* bytes actually captured */
u8 dir; /* 0 = TX (ipc_write), 1 = RX (ipc_read) */
u8 data[CAP_DATA];
};
static struct cap_ent ring[CAP_RING];
static u32 ring_head;
static u64 ring_seq;
static DEFINE_SPINLOCK(ring_lock);
/* Optional channel filter: 0 = capture all; else only this iod->ch. */
static int cap_ch = 0;
module_param(cap_ch, int, 0644);
MODULE_PARM_DESC(cap_ch, "capture only this cpif channel id (0=all)");
/* nofault readers, resolved via kallsyms (may be GKI-protected). */
static long (*p_cfu_nofault)(void *dst, const void __user *src, size_t size);
static long (*p_cfk_nofault)(void *dst, const void *src, size_t size);
static unsigned long cg_lookup_name(const char *name)
{
static unsigned long (*kln)(const char *name);
if (!kln) {
struct kprobe kp = { .symbol_name = "kallsyms_lookup_name" };
int ret = register_kprobe(&kp);
if (ret < 0) {
pr_err("cgcap: kprobe bootstrap failed (%d)\n", ret);
return 0;
}
kln = (void *)kp.addr;
unregister_kprobe(&kp);
}
return kln ? kln(name) : 0;
}
/* iod = file->private_data; iod->ch @0x114 (proven from cpif_probe). */
static u32 iod_ch(struct file *f)
{
u16 v;
if (f && f->private_data &&
!get_kernel_nofault(v, (u16 *)((char *)f->private_data + IOD_CH_OFFSET)))
return v;
return 0xffffffff;
}
static void store_frame(u8 dir, u32 ch, size_t count, const void __user *ubuf)
{
unsigned long flags;
struct cap_ent *e;
u32 n = min_t(u32, (u32)count, CAP_DATA);
if (cap_ch && ch != (u32)cap_ch)
return;
spin_lock_irqsave(&ring_lock, flags);
e = &ring[ring_head];
e->seq = ++ring_seq;
e->ns = ktime_get_ns();
e->ch = ch;
e->len = (u32)count;
e->dir = dir;
e->n = n;
if (!p_cfu_nofault || p_cfu_nofault(e->data, ubuf, n))
e->n = 0;
ring_head = (ring_head + 1) % CAP_RING;
spin_unlock_irqrestore(&ring_lock, flags);
}
/* TX: ipc_write(file, user_buf, count, pos): arm64 x0=f, x1=buf, x2=count. */
static int cap_pre(struct kprobe *p, struct pt_regs *regs)
{
struct file *f = (struct file *)regs->regs[0];
const void __user *ubuf = (const void __user *)regs->regs[1];
size_t count = (size_t)regs->regs[2];
if (f && ubuf && count)
store_frame(0, iod_ch(f), count, ubuf);
return 0;
}
static struct kprobe kp_ipc_write = {
.symbol_name = "ipc_write",
.pre_handler = cap_pre,
};
/* Store from a KERNEL buffer (skb->data), read as system — no user copy. */
static void store_kframe(u8 dir, u32 ch, const void *kbuf, u32 len)
{
unsigned long flags;
struct cap_ent *e;
u32 n = min_t(u32, len, CAP_DATA);
if (cap_ch && ch != (u32)cap_ch)
return;
spin_lock_irqsave(&ring_lock, flags);
e = &ring[ring_head];
e->seq = ++ring_seq;
e->ns = ktime_get_ns();
e->ch = ch;
e->len = len;
e->dir = dir;
e->n = n;
if (!p_cfk_nofault || p_cfk_nofault(e->data, kbuf, n))
e->n = 0;
ring_head = (ring_head + 1) % CAP_RING;
spin_unlock_irqrestore(&ring_lock, flags);
}
/* RX: io_dev_recv_skb_single_from_link_dev(x0 = mld, x1 = iod, x2 = skb) —
* the common CP->AP entry for BOTH FMT and RAW channels, skb in kernel memory.
* Read skb->data directly (as root/system), no copy_from_user. */
static int rx_pre(struct kprobe *p, struct pt_regs *regs)
{
void *iod = (void *)regs->regs[1];
struct sk_buff *skb = (struct sk_buff *)regs->regs[2];
u16 v;
u32 ch = 0xffffffff;
if (!skb)
return 0;
if (iod && !get_kernel_nofault(v, (u16 *)((char *)iod + IOD_CH_OFFSET)))
ch = v;
store_kframe(1, ch, skb->data, skb->len);
return 0;
}
static struct kprobe kp_rx = {
.symbol_name = "io_dev_recv_skb_single_from_link_dev",
.pre_handler = rx_pre,
};
/* ---- /proc/cgcap/frames ---- */
static const char *sit_type(u8 t)
{
switch (t) {
case 0: return "REQ";
case 1: return "RSP";
case 2: return "IND";
default: return "???";
}
}
static int frames_show(struct seq_file *m, void *v)
{
static struct cap_ent snap[CAP_RING];
unsigned long flags;
u32 i, cnt;
spin_lock_irqsave(&ring_lock, flags);
memcpy(snap, ring, sizeof(snap));
cnt = ring_head; /* not strictly needed; we sort by seq */
(void)cnt;
spin_unlock_irqrestore(&ring_lock, flags);
seq_printf(m, "# total=%llu fields: seq dir ch type id(grp/cmd) len token err payload\n",
ring_seq);
for (i = 0; i < CAP_RING; i++) {
struct cap_ent *e = &snap[i];
u8 type, err = 0;
u16 id = 0, flen = 0;
u32 tok = 0, j;
if (!e->seq)
continue;
if (e->n == 0) {
seq_printf(m, "%llu %s ch=0x%02x len=%u [nocopy]\n",
e->seq, e->dir ? "RX" : "TX", e->ch, e->len);
continue;
}
type = e->data[0];
if (e->n >= 6) {
id = e->data[2] | (e->data[3] << 8);
flen = e->data[4] | (e->data[5] << 8);
}
if (e->n >= 10)
tok = e->data[6] | (e->data[7] << 8) |
(e->data[8] << 16) | (e->data[9] << 24);
if (type == 1 && e->n >= 11)
err = e->data[10];
seq_printf(m, "%llu %s ch=0x%02x %s id=0x%04x(%02x/%02x) len=%u tok=%u err=%u ",
e->seq, e->dir ? "RX" : "TX", e->ch, sit_type(type), id,
(id >> 8) & 0xff, id & 0xff, flen, tok, err);
for (j = 0; j < e->n; j++)
seq_printf(m, "%02x", e->data[j]);
if (e->len > e->n)
seq_printf(m, "..(+%u)", e->len - e->n);
seq_putc(m, '\n');
}
return 0;
}
static int frames_open(struct inode *i, struct file *f)
{ return single_open(f, frames_show, NULL); }
static const struct proc_ops frames_pops = {
.proc_open = frames_open,
.proc_read = seq_read,
.proc_lseek = seq_lseek,
.proc_release = single_release,
};
static struct proc_dir_entry *proc_root;
static int __init cgcap_init(void)
{
int ret;
p_cfu_nofault = (void *)cg_lookup_name("copy_from_user_nofault");
p_cfk_nofault = (void *)cg_lookup_name("copy_from_kernel_nofault");
if (!p_cfu_nofault || !p_cfk_nofault)
pr_warn("cgcap: nofault reader unresolved (u=%px k=%px); some payloads empty\n",
p_cfu_nofault, p_cfk_nofault);
ret = register_kprobe(&kp_ipc_write);
if (ret < 0) {
pr_err("cgcap: register_kprobe(ipc_write) failed (%d) — is cpif loaded?\n", ret);
return ret;
}
ret = register_kprobe(&kp_rx);
if (ret < 0)
pr_warn("cgcap: register_kprobe(queue_skb_to_iod) failed (%d); TX-only\n", ret);
proc_root = proc_mkdir("cgcap", NULL);
if (proc_root)
proc_create("frames", 0444, proc_root, &frames_pops);
pr_info("cgcap: capturing cpif ipc_write @ %p (filter ch=0x%x); read /proc/cgcap/frames\n",
kp_ipc_write.addr, cap_ch);
return 0;
}
static void __exit cgcap_exit(void)
{
unregister_kprobe(&kp_ipc_write);
unregister_kprobe(&kp_rx);
if (proc_root) {
remove_proc_entry("frames", proc_root);
proc_remove(proc_root);
}
pr_info("cgcap: unloaded (%llu frames seen)\n", ring_seq);
}
module_init(cgcap_init);
module_exit(cgcap_exit);