Files
CellGuard/cgdetect.sh

100 lines
4.5 KiB
Bash
Raw Normal View History

#!/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
# timeout guards against a blocking dumpsys hanging the whole loop (which
# would freeze the status file — the cause of stale frame counts).
reg=$(timeout 6 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