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:
326
system/bin/cellguard
Executable file
326
system/bin/cellguard
Executable file
@@ -0,0 +1,326 @@
|
||||
#!/system/bin/sh
|
||||
# cellguard — CLI driver for the cellguard kernel module.
|
||||
#
|
||||
# Invoked by the KernelSU-Next WebUI through ksu.exec("cellguard <cmd>").
|
||||
# Every subcommand prints a single JSON object on stdout so the WebUI
|
||||
# can JSON.parse() the result directly.
|
||||
|
||||
CONFIG_DIR="/data/adb/cellguard"
|
||||
CONFIG_FILE="$CONFIG_DIR/config.sh"
|
||||
DEV=/dev/cellguard
|
||||
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
[ -f "$CONFIG_FILE" ] || cat > "$CONFIG_FILE" << 'EOF'
|
||||
ENABLED=1
|
||||
BLOCK_2G=1
|
||||
BLOCK_3G=1
|
||||
BLOCK_NULL_CIPHER=1
|
||||
POLL_MS=1500
|
||||
CARRIER=none
|
||||
EOF
|
||||
|
||||
. "$CONFIG_FILE"
|
||||
[ -z "$CARRIER" ] && CARRIER=none
|
||||
|
||||
VALID_CARRIERS="none tmobile verizon att uscellular dish cricket metro googlefi generic_us"
|
||||
|
||||
# JSON escaping — strings are put through sed to escape \ and "
|
||||
j_esc() {
|
||||
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' \
|
||||
-e ':a;N;$!ba;s/\n/\\n/g'
|
||||
}
|
||||
|
||||
emit_error() {
|
||||
printf '{"ok":false,"error":"%s"}\n' "$(j_esc "$1")"
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_dev() {
|
||||
[ -c "$DEV" ] || emit_error "kernel module not loaded (/dev/cellguard missing)"
|
||||
}
|
||||
|
||||
save_config() {
|
||||
cat > "$CONFIG_FILE" << EOF
|
||||
ENABLED=$ENABLED
|
||||
BLOCK_2G=$BLOCK_2G
|
||||
BLOCK_3G=$BLOCK_3G
|
||||
BLOCK_NULL_CIPHER=$BLOCK_NULL_CIPHER
|
||||
POLL_MS=$POLL_MS
|
||||
CARRIER=$CARRIER
|
||||
EOF
|
||||
}
|
||||
|
||||
#####################################################################
|
||||
# status — read /proc/cellguard/status and reformat as JSON
|
||||
#####################################################################
|
||||
cmd_status() {
|
||||
require_dev
|
||||
if [ ! -f /proc/cellguard/status ]; then
|
||||
emit_error "/proc/cellguard/status missing"
|
||||
fi
|
||||
|
||||
# /proc/cellguard/status is key: value, one per line
|
||||
awk -v cfg2g="$BLOCK_2G" -v cfg3g="$BLOCK_3G" \
|
||||
-v cfgnull="$BLOCK_NULL_CIPHER" -v cfgpoll="$POLL_MS" '
|
||||
BEGIN { FS=":"; printf "{" ; first=1 }
|
||||
{
|
||||
k=$1; sub(/^ +/,"",k); sub(/ +$/,"",k)
|
||||
v=$2; for (i=3;i<=NF;i++) v=v ":" $i
|
||||
sub(/^ +/,"",v); sub(/ +$/,"",v)
|
||||
gsub(/\\/,"\\\\",v); gsub(/"/,"\\\"",v)
|
||||
if (!first) printf ","
|
||||
first=0
|
||||
# numeric fields
|
||||
if (k=="enabled" || k=="rat_code" || k=="cipher_known" || \
|
||||
k=="cipher_null" || k=="block_2g" || k=="block_3g" || \
|
||||
k=="block_null" || k=="poll_ms" || k=="rsrp_dbm" || \
|
||||
k=="tx_pwr_dbm" || \
|
||||
k=="downgrades" || k=="relocks" || k=="polls" || \
|
||||
k=="bands_locked") {
|
||||
# rat field might be mixed — strip trailing junk
|
||||
n=v; sub(/[^-0-9].*/,"",n); if (n=="") n=0
|
||||
printf "\"%s\":%s", k, n
|
||||
} else if (k=="modem" || k=="rat" || k=="operator" || \
|
||||
k=="carrier" || k=="bands_method") {
|
||||
printf "\"%s\":\"%s\"", k, v
|
||||
} else {
|
||||
printf "\"%s\":\"%s\"", k, v
|
||||
}
|
||||
}
|
||||
END { printf ",\"ok\":true}" }
|
||||
' /proc/cellguard/status
|
||||
echo
|
||||
}
|
||||
|
||||
#####################################################################
|
||||
# on / off — flip the guard, persist to config
|
||||
#####################################################################
|
||||
cmd_on() {
|
||||
require_dev
|
||||
echo "on" > "$DEV" 2>/dev/null || emit_error "write /dev/cellguard failed"
|
||||
ENABLED=1
|
||||
save_config
|
||||
printf '{"ok":true,"enabled":1}\n'
|
||||
}
|
||||
|
||||
cmd_off() {
|
||||
require_dev
|
||||
echo "off" > "$DEV" 2>/dev/null || emit_error "write /dev/cellguard failed"
|
||||
ENABLED=0
|
||||
save_config
|
||||
printf '{"ok":true,"enabled":0}\n'
|
||||
}
|
||||
|
||||
#####################################################################
|
||||
# apply-config — push saved policy, then on/off accordingly
|
||||
# (called by service.sh at boot, and by any config-change flow)
|
||||
#####################################################################
|
||||
cmd_apply_config() {
|
||||
require_dev
|
||||
# Select the carrier profile first so that when we flip the guard on,
|
||||
# the kernel's full push_lock_policy() applies WS46 + band-lock in
|
||||
# one CFUN cycle. When the guard is off we still record the carrier
|
||||
# for next enable.
|
||||
if [ -n "$CARRIER" ] && [ "$CARRIER" != "none" ]; then
|
||||
echo "carrier $CARRIER" > "$DEV" 2>/dev/null
|
||||
fi
|
||||
if [ "$ENABLED" = "1" ]; then
|
||||
echo "on" > "$DEV" 2>/dev/null
|
||||
else
|
||||
echo "off" > "$DEV" 2>/dev/null
|
||||
fi
|
||||
printf '{"ok":true,"applied":true,"enabled":%s,"carrier":"%s"}\n' \
|
||||
"$ENABLED" "$CARRIER"
|
||||
}
|
||||
|
||||
#####################################################################
|
||||
# relock — force an immediate policy push even if state looks fine
|
||||
#####################################################################
|
||||
cmd_relock() {
|
||||
require_dev
|
||||
echo "relock" > "$DEV" 2>/dev/null || emit_error "relock write failed"
|
||||
printf '{"ok":true,"relocked":true}\n'
|
||||
}
|
||||
|
||||
#####################################################################
|
||||
# scan — trigger AT+COPS=?, parse result, emit JSON list
|
||||
#
|
||||
# AT+COPS=? response:
|
||||
# +COPS: (stat,"long","short","numeric",AcT),(...),...
|
||||
# stat: 0=unknown 1=available 2=current 3=forbidden
|
||||
# AcT : 0/1/3/8 = 2G, 2/4-6 = 3G, 7/9/10 = LTE, 11/13 = NR
|
||||
#####################################################################
|
||||
cmd_scan() {
|
||||
require_dev
|
||||
# Kick the scan and wait. AT+COPS=? can take up to ~180s on a cold
|
||||
# SIM; the module runs it synchronously in the ioctl path.
|
||||
echo "scan" > "$DEV" 2>/dev/null || emit_error "scan write failed"
|
||||
|
||||
# Poll /proc/cellguard/cells until it has a +COPS: line or we time out
|
||||
n=0
|
||||
while [ $n -lt 200 ]; do
|
||||
if grep -q "+COPS:" /proc/cellguard/cells 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
n=$((n + 1))
|
||||
done
|
||||
|
||||
raw=$(cat /proc/cellguard/cells 2>/dev/null)
|
||||
[ -z "$raw" ] && emit_error "scan produced no output"
|
||||
|
||||
printf '%s' "$raw" | awk '
|
||||
BEGIN {
|
||||
printf "{\"ok\":true,\"cells\":["
|
||||
first=1
|
||||
}
|
||||
{
|
||||
line=$0
|
||||
# Work through each (...) tuple
|
||||
while (match(line, /\([^()]+\)/)) {
|
||||
tup=substr(line, RSTART+1, RLENGTH-2)
|
||||
line=substr(line, RSTART+RLENGTH)
|
||||
|
||||
# Split tup on commas, but ignore commas inside quotes
|
||||
n=0
|
||||
buf=""
|
||||
inq=0
|
||||
for (i=1;i<=length(tup);i++) {
|
||||
c=substr(tup,i,1)
|
||||
if (c=="\"") { inq=1-inq; continue }
|
||||
if (c=="," && !inq) { n++; f[n]=buf; buf=""; continue }
|
||||
buf = buf c
|
||||
}
|
||||
n++; f[n]=buf
|
||||
|
||||
# A scan tuple has 5 fields: stat,long,short,numeric,act
|
||||
# The trailing "supported modes" / "supported formats" tuples
|
||||
# have 1-2 numeric fields — skip those.
|
||||
if (n < 4) continue
|
||||
stat=f[1]; longn=f[2]; shortn=f[3]; num=f[4]; act=f[5]
|
||||
if (stat !~ /^[0-9]+$/) continue
|
||||
|
||||
# Map AcT to RAT name and a security-ranking score
|
||||
act_n = act + 0
|
||||
rat="?"; score=0
|
||||
if (act_n==0||act_n==1||act_n==3||act_n==8) { rat="2G"; score=1 }
|
||||
else if (act_n==2||act_n==4||act_n==5||act_n==6) { rat="3G"; score=2 }
|
||||
else if (act_n==7||act_n==9||act_n==10) { rat="LTE"; score=3 }
|
||||
else if (act_n==11||act_n==13) { rat="5G-SA"; score=4 }
|
||||
|
||||
avail="unknown"
|
||||
if (stat=="0") avail="unknown"
|
||||
else if (stat=="1") avail="available"
|
||||
else if (stat=="2") avail="current"
|
||||
else if (stat=="3") avail="forbidden"
|
||||
|
||||
# Escape
|
||||
gsub(/\\/,"\\\\",longn); gsub(/"/,"\\\"",longn)
|
||||
gsub(/\\/,"\\\\",shortn); gsub(/"/,"\\\"",shortn)
|
||||
gsub(/\\/,"\\\\",num); gsub(/"/,"\\\"",num)
|
||||
|
||||
if (!first) printf ","
|
||||
first=0
|
||||
printf "{\"operator\":\"%s\",\"short\":\"%s\",\"mcc_mnc\":\"%s\",\"rat\":\"%s\",\"act\":%d,\"avail\":\"%s\",\"score\":%d}", \
|
||||
longn, shortn, num, rat, act_n, avail, score
|
||||
}
|
||||
}
|
||||
END { printf "]}\n" }
|
||||
'
|
||||
}
|
||||
|
||||
#####################################################################
|
||||
# carrier — set the band-lock profile
|
||||
# cellguard carrier list
|
||||
# cellguard carrier <name>
|
||||
#####################################################################
|
||||
cmd_carrier() {
|
||||
name="$1"
|
||||
if [ -z "$name" ] || [ "$name" = "list" ]; then
|
||||
printf '{"ok":true,"current":"%s","carriers":[' "$CARRIER"
|
||||
first=1
|
||||
for c in $VALID_CARRIERS; do
|
||||
if [ "$first" = "1" ]; then first=0; else printf ','; fi
|
||||
printf '"%s"' "$c"
|
||||
done
|
||||
printf ']}\n'
|
||||
return
|
||||
fi
|
||||
|
||||
ok=0
|
||||
for c in $VALID_CARRIERS; do
|
||||
[ "$c" = "$name" ] && ok=1
|
||||
done
|
||||
[ "$ok" = "1" ] || emit_error "unknown carrier: $name"
|
||||
|
||||
require_dev
|
||||
echo "carrier $name" > "$DEV" 2>/dev/null || emit_error "write failed"
|
||||
CARRIER="$name"
|
||||
save_config
|
||||
printf '{"ok":true,"carrier":"%s"}\n' "$name"
|
||||
}
|
||||
|
||||
#####################################################################
|
||||
# config — read/write persisted policy from the WebUI
|
||||
#
|
||||
# Usage:
|
||||
# cellguard config get
|
||||
# cellguard config set block_2g=1 block_3g=0 block_null_cipher=1 poll_ms=2000
|
||||
#####################################################################
|
||||
cmd_config() {
|
||||
sub="$1"; shift
|
||||
case "$sub" in
|
||||
get|"")
|
||||
printf '{"ok":true,"enabled":%s,"block_2g":%s,"block_3g":%s,"block_null_cipher":%s,"poll_ms":%s,"carrier":"%s"}\n' \
|
||||
"$ENABLED" "$BLOCK_2G" "$BLOCK_3G" "$BLOCK_NULL_CIPHER" "$POLL_MS" "$CARRIER"
|
||||
;;
|
||||
set)
|
||||
while [ -n "$1" ]; do
|
||||
k="${1%%=*}"
|
||||
v="${1#*=}"
|
||||
case "$k" in
|
||||
enabled) ENABLED="$v" ;;
|
||||
block_2g) BLOCK_2G="$v" ;;
|
||||
block_3g) BLOCK_3G="$v" ;;
|
||||
block_null_cipher) BLOCK_NULL_CIPHER="$v" ;;
|
||||
poll_ms) POLL_MS="$v" ;;
|
||||
*) emit_error "unknown key: $k" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
save_config
|
||||
cmd_apply_config
|
||||
;;
|
||||
*)
|
||||
emit_error "config: unknown subcommand '$sub'"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
#####################################################################
|
||||
# dispatch
|
||||
#####################################################################
|
||||
case "$1" in
|
||||
status) cmd_status ;;
|
||||
on) cmd_on ;;
|
||||
off) cmd_off ;;
|
||||
relock) cmd_relock ;;
|
||||
scan) cmd_scan ;;
|
||||
carrier) shift; cmd_carrier "$@" ;;
|
||||
apply-config) cmd_apply_config ;;
|
||||
config) shift; cmd_config "$@" ;;
|
||||
*)
|
||||
cat << USAGE
|
||||
cellguard — driver for /dev/cellguard
|
||||
|
||||
cellguard status show current RAT, operator, cipher, counters
|
||||
cellguard on enable the guard (force LTE/NR, re-lock on drop)
|
||||
cellguard off disable and release the modem
|
||||
cellguard relock force one relock pass now
|
||||
cellguard scan full AT+COPS=? scan, JSON list of visible networks
|
||||
cellguard config get|set read or update persisted policy
|
||||
|
||||
All subcommands emit a single-line JSON object on stdout.
|
||||
USAGE
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user