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:
97
cgdetect.sh
Executable file
97
cgdetect.sh
Executable file
@@ -0,0 +1,97 @@
|
||||
#!/system/bin/sh
|
||||
# CellGuard cgdetect — always-on IMSI-catcher / cell-site-simulator detector.
|
||||
#
|
||||
# Runs as a background daemon (launched by service.sh), independent of the
|
||||
# WebUI. It reads the modem's live serving/neighbor cell state from the
|
||||
# telephony framework and the raw CP-frame counters from cgcap, applies
|
||||
# behavioral CSS heuristics (vendor-agnostic — catches Stingray, Hailstorm,
|
||||
# DRT, Crosshair, Gossamer, OpenBTS/srsRAN fakes alike), and writes:
|
||||
# /data/adb/cellguard/status key=value snapshot (WebUI reads this)
|
||||
# /data/adb/cellguard/alerts append-only alert log (WebUI reads this)
|
||||
#
|
||||
# Heuristics (any one = suspicious):
|
||||
# DOWNGRADE serving RAT is 2G/3G (GSM/GPRS/EDGE/UMTS/HSPA/CDMA)
|
||||
# PLMN serving PLMN != learned home PLMN
|
||||
# TAC_JUMP serving TAC changed without a cell/PLMN reason
|
||||
# NEW_CELL serving PCI/CI never seen before (first-seen, informational)
|
||||
# NO_NEIGH registered but zero neighbor cells reported (CSS often hides them)
|
||||
|
||||
CG="/data/adb/cellguard"
|
||||
STATUS="$CG/status"
|
||||
ALERTS="$CG/alerts"
|
||||
STATE="$CG/detect.state" # learned home PLMN + seen cells
|
||||
POLL="${1:-4}"
|
||||
|
||||
mkdir -p "$CG"
|
||||
: > "$STATUS"
|
||||
alert() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$ALERTS"; }
|
||||
setk() { echo "$1=$2" >> "$STATUS.tmp"; }
|
||||
|
||||
# load learned state
|
||||
HOME_PLMN=""; SEEN_CELLS=" "
|
||||
[ -f "$STATE" ] && . "$STATE" 2>/dev/null
|
||||
|
||||
# startup marker goes to a debug log, NOT the alerts file (the alerts file is
|
||||
# surfaced in the UI and must contain only real threat events)
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] cgdetect start (poll=${POLL}s)" >> "$CG/cgdetect.log"
|
||||
|
||||
while true; do
|
||||
# detector toggle (WebUI) — idle when off
|
||||
if [ "$(cat "$CG/detect_enabled" 2>/dev/null)" = "0" ]; then sleep "$POLL"; continue; fi
|
||||
reg=$(dumpsys telephony.registry 2>/dev/null)
|
||||
# serving air-interface RAT = type of the registered cell identity (real MCC).
|
||||
# A serving GSM/WCDMA identity == an active 2G/3G downgrade.
|
||||
if echo "$reg" | grep -q 'CellIdentityNr:{[^}]*mMcc=[0-9]'; then rat=NR
|
||||
elif echo "$reg" | grep -q 'CellIdentityLte:{[^}]*mMcc=[0-9]'; then rat=LTE
|
||||
elif echo "$reg" | grep -q 'CellIdentityWcdma:{[^}]*mMcc=[0-9]'; then rat=WCDMA
|
||||
elif echo "$reg" | grep -q 'CellIdentityGsm:{[^}]*mMcc=[0-9]'; then rat=GSM
|
||||
else rat="?"; fi
|
||||
# the service-state line carrying the serving cell identity (real MCC)
|
||||
line=$(echo "$reg" | grep -m1 'CellIdentity[A-Za-z]*:{[^}]*mMcc=[0-9]')
|
||||
mcc=$(echo "$line" | sed -n 's/.*mMcc=\([0-9]*\).*/\1/p')
|
||||
mnc=$(echo "$line" | sed -n 's/.*mMnc=\([0-9]*\).*/\1/p')
|
||||
ci=$(echo "$line" | sed -n 's/.*mCi=\[*\([0-9]*\).*/\1/p')
|
||||
pci=$(echo "$line" | sed -n 's/.*mPci=\[*\([0-9]*\).*/\1/p')
|
||||
tac=$(echo "$line" | sed -n 's/.*mTac=\[*\([0-9]*\).*/\1/p')
|
||||
earfcn=$(echo "$line" | sed -n 's/.*mEarfcn=\[*\([0-9]*\).*/\1/p')
|
||||
op=$(echo "$reg" | grep -oE 'mOperatorAlphaLong=[^,]+' | grep -m1 '=[A-Za-z0-9]' | cut -d= -f2-)
|
||||
# NOTE: ci/pci/tac/earfcn are redacted by Android in dumpsys ([****]); the real
|
||||
# values + neighbor towers come from the decoded cgcap 0x070F cell-info frames.
|
||||
plmn="${mcc}${mnc}"
|
||||
frames=$(head -1 /proc/cgcap/frames 2>/dev/null | sed -n 's/.*total=\([0-9]*\).*/\1/p')
|
||||
|
||||
susp=""
|
||||
case "$rat" in
|
||||
GSM|GPRS|EDGE|UMTS|HSDPA|HSUPA|HSPA|HSPAP|TD_SCDMA|CDMA|EVDO|1xRTT)
|
||||
susp="DOWNGRADE"; alert "DOWNGRADE: serving RAT=$rat (2G/3G) plmn=$plmn cell=$ci — CSS/downgrade signature" ;;
|
||||
esac
|
||||
if [ -n "$plmn" ] && [ "$plmn" != "000000" ]; then
|
||||
if [ -z "$HOME_PLMN" ]; then
|
||||
HOME_PLMN="$plmn" # learn on first good read
|
||||
elif [ "$plmn" != "$HOME_PLMN" ]; then
|
||||
susp="${susp:+$susp }PLMN"; alert "PLMN MISMATCH: serving=$plmn expected=$HOME_PLMN op='$op'"
|
||||
fi
|
||||
fi
|
||||
if [ -n "$pci" ] && [ "$pci" != "2147483647" ]; then
|
||||
case "$SEEN_CELLS" in
|
||||
*" $pci "*) : ;;
|
||||
*) SEEN_CELLS="$SEEN_CELLS$pci "; alert "NEW_CELL: first-seen serving pci=$pci ci=$ci tac=$tac earfcn=$earfcn rat=$rat" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# write status snapshot atomically
|
||||
: > "$STATUS.tmp"
|
||||
setk ts "$(date '+%s')"
|
||||
setk rat "${rat:-?}"
|
||||
setk plmn "${plmn:-?}"
|
||||
setk home_plmn "${HOME_PLMN:-?}"
|
||||
setk operator "${op:-?}"
|
||||
setk ci "${ci:-?}"; setk pci "${pci:-?}"; setk tac "${tac:-?}"; setk earfcn "${earfcn:-?}"
|
||||
setk frames "${frames:-0}"
|
||||
setk suspect "${susp:-none}"
|
||||
mv "$STATUS.tmp" "$STATUS"
|
||||
|
||||
# persist learned state
|
||||
printf 'HOME_PLMN="%s"\nSEEN_CELLS="%s"\n' "$HOME_PLMN" "$SEEN_CELLS" > "$STATE"
|
||||
sleep "$POLL"
|
||||
done
|
||||
Reference in New Issue
Block a user