diff --git a/bin/src/analysis.rs b/bin/src/analysis.rs index 62cd18d..34504be 100644 --- a/bin/src/analysis.rs +++ b/bin/src/analysis.rs @@ -8,7 +8,7 @@ use axum::{ }; use futures::TryStreamExt; use log::{debug, error, info}; -use rayhunter::analysis::analyzer::Harness; +use rayhunter::analysis::analyzer::{AnalyzerConfig, Harness}; use rayhunter::diag::{DataType, MessagesContainer}; use rayhunter::qmdl::QmdlReader; use serde::Serialize; @@ -35,8 +35,12 @@ pub struct AnalysisWriter { // lets us simply append new rows to the end without parsing the entire JSON // object beforehand. impl AnalysisWriter { - pub async fn new(file: File, enable_dummy_analyzer: bool) -> Result { - let mut harness = Harness::new_with_all_analyzers(); + pub async fn new( + file: File, + enable_dummy_analyzer: bool, + analyzer_config: &AnalyzerConfig, + ) -> Result { + let mut harness = Harness::new_with_config(analyzer_config); if enable_dummy_analyzer { harness.add_analyzer(Box::new(TestAnalyzer { count: 0 })); } @@ -131,6 +135,7 @@ async fn perform_analysis( name: &str, qmdl_store_lock: Arc>, enable_dummy_analyzer: bool, + analyzer_config: &AnalyzerConfig, ) -> Result<(), String> { info!("Opening QMDL and analysis file for {}...", name); let (analysis_file, qmdl_file, entry_index) = { @@ -150,9 +155,10 @@ async fn perform_analysis( (analysis_file, qmdl_file, entry_index) }; - let mut analysis_writer = AnalysisWriter::new(analysis_file, enable_dummy_analyzer) - .await - .map_err(|e| format!("{:?}", e))?; + let mut analysis_writer = + AnalysisWriter::new(analysis_file, enable_dummy_analyzer, analyzer_config) + .await + .map_err(|e| format!("{:?}", e))?; let file_size = qmdl_file .metadata() .await @@ -196,6 +202,7 @@ pub fn run_analysis_thread( qmdl_store_lock: Arc>, analysis_status_lock: Arc>, enable_dummy_analyzer: bool, + analyzer_config: AnalyzerConfig, ) { task_tracker.spawn(async move { loop { @@ -204,9 +211,13 @@ pub fn run_analysis_thread( let count = queued_len(analysis_status_lock.clone()).await; for _ in 0..count { let name = dequeue_to_running(analysis_status_lock.clone()).await; - if let Err(err) = - perform_analysis(&name, qmdl_store_lock.clone(), enable_dummy_analyzer) - .await + if let Err(err) = perform_analysis( + &name, + qmdl_store_lock.clone(), + enable_dummy_analyzer, + &analyzer_config, + ) + .await { error!("failed to analyze {}: {}", name, err); } diff --git a/bin/src/check.rs b/bin/src/check.rs index 8de6222..cca54e5 100644 --- a/bin/src/check.rs +++ b/bin/src/check.rs @@ -2,7 +2,7 @@ use clap::Parser; use futures::TryStreamExt; use log::{info, warn}; use rayhunter::{ - analysis::analyzer::{EventType, Harness}, + analysis::analyzer::{AnalyzerConfig, EventType, Harness}, diag::DataType, gsmtap_parser, pcap::GsmtapPcapWriter, @@ -33,7 +33,7 @@ struct Args { } async fn analyze_file(enable_dummy_analyzer: bool, qmdl_path: &str, show_skipped: bool) { - let mut harness = Harness::new_with_all_analyzers(); + let mut harness = Harness::new_with_config(&AnalyzerConfig::default()); if enable_dummy_analyzer { harness.add_analyzer(Box::new(dummy_analyzer::TestAnalyzer { count: 0 })); } @@ -141,7 +141,7 @@ async fn main() { .unwrap(); info!("Analyzers:"); - let mut harness = Harness::new_with_all_analyzers(); + let mut harness = Harness::new_with_config(&AnalyzerConfig::default()); if args.enable_dummy_analyzer { harness.add_analyzer(Box::new(dummy_analyzer::TestAnalyzer { count: 0 })); } diff --git a/bin/src/config.rs b/bin/src/config.rs index 56ebef0..07ae6a3 100644 --- a/bin/src/config.rs +++ b/bin/src/config.rs @@ -1,7 +1,9 @@ -use crate::error::RayhunterError; - use serde::Deserialize; +use rayhunter::analysis::analyzer::AnalyzerConfig; + +use crate::error::RayhunterError; + #[derive(Debug, Deserialize)] #[serde(default)] pub struct Config { @@ -12,6 +14,7 @@ pub struct Config { pub enable_dummy_analyzer: bool, pub colorblind_mode: bool, pub key_input_mode: u8, + pub analyzers: AnalyzerConfig, } impl Default for Config { @@ -24,6 +27,7 @@ impl Default for Config { enable_dummy_analyzer: false, colorblind_mode: false, key_input_mode: 1, + analyzers: AnalyzerConfig::default(), } } } diff --git a/bin/src/daemon.rs b/bin/src/daemon.rs index 3041ad4..54df436 100644 --- a/bin/src/daemon.rs +++ b/bin/src/daemon.rs @@ -199,6 +199,7 @@ async fn main() -> Result<(), RayhunterError> { qmdl_store_lock.clone(), analysis_tx.clone(), config.enable_dummy_analyzer, + config.analyzers.clone(), ); info!("Starting UI"); display::update_ui(&task_tracker, &config, ui_shutdown_rx, ui_update_rx); @@ -215,6 +216,7 @@ async fn main() -> Result<(), RayhunterError> { qmdl_store_lock.clone(), analysis_status_lock.clone(), config.enable_dummy_analyzer, + config.analyzers.clone(), ); run_ctrl_c_thread( &task_tracker, diff --git a/bin/src/diag.rs b/bin/src/diag.rs index 664260e..bcfb501 100644 --- a/bin/src/diag.rs +++ b/bin/src/diag.rs @@ -8,6 +8,7 @@ use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use futures::{StreamExt, TryStreamExt}; use log::{debug, error, info, warn}; +use rayhunter::analysis::analyzer::AnalyzerConfig; use rayhunter::diag::DataType; use rayhunter::diag_device::DiagDevice; use rayhunter::qmdl::QmdlWriter; @@ -36,12 +37,13 @@ pub fn run_diag_read_thread( qmdl_store_lock: Arc>, analysis_sender: Sender, enable_dummy_analyzer: bool, + analyzer_config: AnalyzerConfig, ) { task_tracker.spawn(async move { let (initial_qmdl_file, initial_analysis_file) = qmdl_store_lock.write().await.new_entry().await.expect("failed creating QMDL file entry"); let mut maybe_qmdl_writer: Option> = Some(QmdlWriter::new(initial_qmdl_file)); let mut diag_stream = pin!(dev.as_stream().into_stream()); - let mut maybe_analysis_writer = Some(AnalysisWriter::new(initial_analysis_file, enable_dummy_analyzer).await + let mut maybe_analysis_writer = Some(AnalysisWriter::new(initial_analysis_file, enable_dummy_analyzer, &analyzer_config).await .expect("failed to create analysis writer")); loop { tokio::select! { @@ -63,7 +65,7 @@ pub fn run_diag_read_thread( analysis_writer.close().await.expect("failed to close analysis writer"); } - maybe_analysis_writer = Some(AnalysisWriter::new(new_analysis_file, enable_dummy_analyzer).await + maybe_analysis_writer = Some(AnalysisWriter::new(new_analysis_file, enable_dummy_analyzer, &analyzer_config).await .expect("failed to write to analysis file")); if let Err(e) = ui_update_sender.send(display::DisplayState::Recording).await { diff --git a/dist/config.toml.example b/dist/config.toml.example index 8ba7600..e20599b 100644 --- a/dist/config.toml.example +++ b/dist/config.toml.example @@ -20,3 +20,12 @@ ui_level = 1 # 0 = rayhunter does not read button presses # 1 = double-tapping the power button starts/stops recordings key_input_mode = 1 + +# Analyzer Configuration +# Enable/disable specific IMSI catcher detection heuristics +# See https://github.com/EFForg/rayhunter/blob/main/doc/heuristics.md for details +[analyzers] +imsi_requested = true +connection_redirect_2g_downgrade = true +lte_sib6_and_7_downgrade = true +null_cipher = false diff --git a/doc/heuristics.md b/doc/heuristics.md index f60b7bc..9b0ad8b 100644 --- a/doc/heuristics.md +++ b/doc/heuristics.md @@ -1,3 +1,15 @@ # Heuristics -TODO +Rayhunter includes several analyzers to detect potential IMSI catcher activity. These can be enabled and disabled in your [config.toml](https://github.com/EFForg/rayhunter/blob/main/dist/config.toml.example) file. + +## Available Analyzers + +- **IMSI Requested**: Tests whether the ME sends an IMSI Identity Request NAS message +- **Connection Release/Redirected Carrier 2G Downgrade**: Tests if a cell + releases our connection and redirects us to a 2G cell. This heuristic only + makes sense in the US, European users may want to disable it. +- **LTE SIB6/7 Downgrade**: Tests for LTE cells broadcasting a SIB type 6 and 7 + which include 2G/3G frequencies with higher priorities +- **Null Cipher** (disabled by default): Tests whether the cell suggests using a null cipher (EEA0). + This is currently disabled by default due to a parsing bug triggering false + positives. diff --git a/lib/src/analysis/analyzer.rs b/lib/src/analysis/analyzer.rs index 255a24d..390d7aa 100644 --- a/lib/src/analysis/analyzer.rs +++ b/lib/src/analysis/analyzer.rs @@ -1,5 +1,5 @@ use chrono::{DateTime, FixedOffset}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use std::borrow::Cow; use crate::util::RuntimeMetadata; @@ -8,9 +8,32 @@ use crate::{diag::MessagesContainer, gsmtap_parser}; use super::{ connection_redirect_downgrade::ConnectionRedirect2GDowngradeAnalyzer, imsi_requested::ImsiRequestedAnalyzer, information_element::InformationElement, - priority_2g_downgrade::LteSib6And7DowngradeAnalyzer, + null_cipher::NullCipherAnalyzer, priority_2g_downgrade::LteSib6And7DowngradeAnalyzer, }; +#[derive(Debug, Clone, Deserialize)] +#[serde(default)] +pub struct AnalyzerConfig { + pub imsi_requested: bool, + pub connection_redirect_2g_downgrade: bool, + pub lte_sib6_and_7_downgrade: bool, + pub null_cipher: bool, +} + +impl Default for AnalyzerConfig { + fn default() -> Self { + AnalyzerConfig { + imsi_requested: true, + connection_redirect_2g_downgrade: true, + lte_sib6_and_7_downgrade: true, + // FIXME: our RRC parser is reporting false positives for this due to an + // upstream hampi bug (https://github.com/ystero-dev/hampi/issues/133). + // once that's fixed, we should regenerate our parser and re-enable this + null_cipher: false, + } + } +} + /// Qualitative measure of how severe a Warning event type is. /// The levels should break down like this: /// * Low: if combined with a large number of other Warnings, user should investigate @@ -122,16 +145,21 @@ impl Harness { } } - pub fn new_with_all_analyzers() -> Self { + pub fn new_with_config(analyzer_config: &AnalyzerConfig) -> Self { let mut harness = Harness::new(); - harness.add_analyzer(Box::new(ImsiRequestedAnalyzer::new())); - harness.add_analyzer(Box::new(ConnectionRedirect2GDowngradeAnalyzer {})); - harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer {})); - // FIXME: our RRC parser is reporting false positives for this due to an - // upstream hampi bug (https://github.com/ystero-dev/hampi/issues/133). - // once that's fixed, we should regenerate our parser and re-enable this - // harness.add_analyzer(Box::new(NullCipherAnalyzer{})); + if analyzer_config.imsi_requested { + harness.add_analyzer(Box::new(ImsiRequestedAnalyzer::new())); + } + if analyzer_config.connection_redirect_2g_downgrade { + harness.add_analyzer(Box::new(ConnectionRedirect2GDowngradeAnalyzer {})); + } + if analyzer_config.lte_sib6_and_7_downgrade { + harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer {})); + } + if analyzer_config.null_cipher { + harness.add_analyzer(Box::new(NullCipherAnalyzer {})); + } harness }