diff --git a/daemon/web/src/lib/components/ConfigForm.svelte b/daemon/web/src/lib/components/ConfigForm.svelte index 7187cc8..3b7b316 100644 --- a/daemon/web/src/lib/components/ConfigForm.svelte +++ b/daemon/web/src/lib/components/ConfigForm.svelte @@ -335,6 +335,17 @@ Test Heuristic (noisy!) +
+ + +
diff --git a/daemon/web/src/lib/utils.svelte.ts b/daemon/web/src/lib/utils.svelte.ts index 915c7c0..13df13a 100644 --- a/daemon/web/src/lib/utils.svelte.ts +++ b/daemon/web/src/lib/utils.svelte.ts @@ -10,6 +10,7 @@ export interface AnalyzerConfig { nas_null_cipher: boolean; incomplete_sib: boolean; test_analyzer: boolean; + imsi_attach_analyzer: boolean; } export enum enabled_notifications { diff --git a/lib/src/analysis/analyzer.rs b/lib/src/analysis/analyzer.rs index aa31625..af42640 100644 --- a/lib/src/analysis/analyzer.rs +++ b/lib/src/analysis/analyzer.rs @@ -4,6 +4,7 @@ use pcap_file_tokio::pcapng::blocks::enhanced_packet::EnhancedPacketBlock; use serde::{Deserialize, Serialize}; use std::borrow::Cow; +use crate::analysis::imsi_attach::ImsiAttachAnalyzer; use crate::gsmtap::{GsmtapHeader, GsmtapMessage, GsmtapType}; use crate::util::RuntimeMetadata; use crate::{diag::MessagesContainer, gsmtap_parser}; @@ -19,13 +20,14 @@ use super::{ #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(default)] pub struct AnalyzerConfig { - pub imsi_requested: bool, + pub imsi_attach: bool, pub connection_redirect_2g_downgrade: bool, pub lte_sib6_and_7_downgrade: bool, pub null_cipher: bool, pub nas_null_cipher: bool, pub incomplete_sib: bool, pub test_analyzer: bool, + pub imsi_requested: bool, } impl Default for AnalyzerConfig { @@ -38,6 +40,7 @@ impl Default for AnalyzerConfig { nas_null_cipher: true, incomplete_sib: true, test_analyzer: false, + imsi_attach: true, } } } @@ -346,6 +349,10 @@ impl Harness { harness.add_analyzer(Box::new(TestAnalyzer {})) } + if analyzer_config.imsi_attach { + harness.add_analyzer(Box::new(ImsiAttachAnalyzer {})); + } + harness } diff --git a/lib/src/analysis/imsi_attach.rs b/lib/src/analysis/imsi_attach.rs new file mode 100644 index 0000000..3dc4b22 --- /dev/null +++ b/lib/src/analysis/imsi_attach.rs @@ -0,0 +1,131 @@ +use crate::analysis::analyzer::{Analyzer, Event, EventType}; +use crate::analysis::information_element::{InformationElement, LteInformationElement}; +use pycrate_rs::nas::NASMessage; +use pycrate_rs::nas::emm::EMMMessage; +use pycrate_rs::nas::generated::emm::emm_attach_reject::EMMCauseEMMCause as AttachRejectEMMCause; +use pycrate_rs::nas::generated::emm::emm_detach_request_mt::EPSDetachTypeMTType; +use pycrate_rs::nas::generated::emm::emm_identity_request::IDTypeV; +use pycrate_rs::nas::generated::emm::emm_service_reject::EMMCauseEMMCause as ServiceRejectEMMCause; +use pycrate_rs::nas::generated::emm::emm_tracking_area_update_reject::EMMCauseEMMCause as TAURejectEMMCause; +use std::borrow::Cow; + +pub struct ImsiAttachAnalyzer; + +impl ImsiAttachAnalyzer { + pub fn new() -> Self { + ImsiAttachAnalyzer + } + + fn is_imsi_exposing_nas(&self, nas_msg: &NASMessage) -> bool { + match nas_msg { + NASMessage::EMMMessage(emm_msg) => match emm_msg { + EMMMessage::EMMIdentityRequest(req) => req.id_type.inner == IDTypeV::IMSI, + + EMMMessage::EMMTrackingAreaUpdateReject(reject) => { + matches!( + reject.emm_cause.inner, + TAURejectEMMCause::IllegalUE + | TAURejectEMMCause::IllegalME + | TAURejectEMMCause::EPSServicesNotAllowed + | TAURejectEMMCause::EPSServicesAndNonEPSServicesNotAllowed + | TAURejectEMMCause::TrackingAreaNotAllowed + | TAURejectEMMCause::EPSServicesNotAllowedInThisPLMN + | TAURejectEMMCause::RequestedServiceOptionNotAuthorizedInThisPLMN + ) + } + + EMMMessage::EMMAttachReject(reject) => { + matches!( + reject.emm_cause.inner, + AttachRejectEMMCause::IllegalUE + | AttachRejectEMMCause::IllegalME + | AttachRejectEMMCause::EPSServicesNotAllowed + | AttachRejectEMMCause::EPSServicesAndNonEPSServicesNotAllowed + | AttachRejectEMMCause::PLMNNotAllowed + | AttachRejectEMMCause::TrackingAreaNotAllowed + | AttachRejectEMMCause::RoamingNotAllowedInThisTrackingArea + | AttachRejectEMMCause::EPSServicesNotAllowedInThisPLMN + | AttachRejectEMMCause::NoSuitableCellsInTrackingArea + | AttachRejectEMMCause::RequestedServiceOptionNotAuthorizedInThisPLMN + ) + } + + EMMMessage::EMMDetachRequestMT(req) => { + // Original implementation: !(nas_eps.emm.detach_type_dl == 3) + req.eps_detach_type.inner.typ != EPSDetachTypeMTType::IMSIDetach + } + + EMMMessage::EMMServiceReject(reject) => { + matches!( + reject.emm_cause.inner, + ServiceRejectEMMCause::IllegalUE + | ServiceRejectEMMCause::IllegalME + | ServiceRejectEMMCause::EPSServicesNotAllowed + | ServiceRejectEMMCause::UEIdentityCannotBeDerivedByTheNetwork + | ServiceRejectEMMCause::TrackingAreaNotAllowed + | ServiceRejectEMMCause::EPSServicesNotAllowedInThisPLMN + | ServiceRejectEMMCause::RequestedServiceOptionNotAuthorizedInThisPLMN + ) + } + + _ => false, + }, + _ => false, + } + } +} + +impl Analyzer for ImsiAttachAnalyzer { + fn get_name(&self) -> Cow<'_, str> { + "IMSI-Exposed Message Detector".into() + } + + fn get_description(&self) -> Cow<'_, str> { + "Catches any and all messages that may expose IMSI. Can be quite noisy. \ + Based on the detection logic from the Marlin paper (\"They Know Where You Are: Tracking Mobile \ + Devices Using Cellular Infrastructure\"). Since we don't have traffic of many devices, we \ + cannot implement the original exposure ratio calculation, and naively trigger an event on \ + every exposure.".into() + } + + fn get_version(&self) -> u32 { + 1 + } + + fn analyze_information_element(&mut self, ie: &InformationElement, _packet_num: usize) -> Option { + let lte_ie = match ie { + InformationElement::LTE(inner) => inner, + _ => return None, + }; + + match lte_ie.as_ref() { + LteInformationElement::NAS(nas_msg) => { + if self.is_imsi_exposing_nas(nas_msg) { + let message_type = match nas_msg { + NASMessage::EMMMessage(emm_msg) => match emm_msg { + EMMMessage::EMMIdentityRequest(_) => "EMM Identity Request (IMSI)", + EMMMessage::EMMTrackingAreaUpdateReject(_) => { + "EMM Tracking Area Update Reject" + } + EMMMessage::EMMAttachReject(_) => "EMM Attach Reject", + EMMMessage::EMMDetachRequestMT(_) => "EMM Detach Request (MT)", + EMMMessage::EMMServiceReject(_) => "EMM Service Reject", + _ => "Unknown EMM Message", + }, + _ => "Unknown NAS Message", + }; + + Some(Event { + event_type: EventType::Informational, + message: format!( + "IMSI-exposing NAS message detected: {message_type}." + ), + }) + } else { + None + } + } + _ => None, + } + } +} \ No newline at end of file diff --git a/lib/src/analysis/mod.rs b/lib/src/analysis/mod.rs index d6adf39..27e839d 100644 --- a/lib/src/analysis/mod.rs +++ b/lib/src/analysis/mod.rs @@ -1,5 +1,6 @@ pub mod analyzer; pub mod connection_redirect_downgrade; +pub mod imsi_attach; pub mod imsi_requested; pub mod incomplete_sib; pub mod information_element;