- 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
305 lines
8.5 KiB
C
305 lines
8.5 KiB
C
// 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);
|