From fd1f4ecb91d0804e75017be60f21badc9c1ecfbf Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Sun, 8 Jun 2025 13:15:03 +0200 Subject: [PATCH 1/2] Allow enabling/disabling analyzers from config file This is just a simple way to be able to use the 2G analyzer in europe, and to disable maybe IMSI requests if they are too noisy. In a later version we can: * expose config editing in the UI (this is already ongoing) * make the level configurable * add ability to define presets (though i think people could just copy configs from the docs or github issues) Also fill out heuristics.md with some basic information. --- bin/src/analysis.rs | 29 ++++++++++++++------- bin/src/check.rs | 6 ++--- bin/src/config.rs | 8 ++++-- bin/src/daemon.rs | 2 ++ bin/src/diag.rs | 6 +++-- dist/config.toml.example | 9 +++++++ doc/heuristics.md | 17 +++++++++++- lib/src/analysis/analyzer.rs | 50 ++++++++++++++++++++++++++++-------- 8 files changed, 99 insertions(+), 28 deletions(-) diff --git a/bin/src/analysis.rs b/bin/src/analysis.rs index 62cd18da..34504be5 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 8de6222e..cca54e56 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 56ebef01..07ae6a3b 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 3041ad4a..54df4361 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 664260e2..bcfb5019 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 8ba76007..e20599b3 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 f60b7bcd..8136a3b8 100644 --- a/doc/heuristics.md +++ b/doc/heuristics.md @@ -1,3 +1,18 @@ # 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 + +### Disabled by default + +- **Null Cipher**: 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 255a24d6..390d7aa8 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 } From 5d283c0eda66b97c3d49a14dd35dbf2e1c5af710 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 10 Jun 2025 20:56:54 +0200 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: Will Greenberg --- doc/heuristics.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/doc/heuristics.md b/doc/heuristics.md index 8136a3b8..9b0ad8bd 100644 --- a/doc/heuristics.md +++ b/doc/heuristics.md @@ -7,12 +7,9 @@ Rayhunter includes several analyzers to detect potential IMSI catcher activity. - **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. + 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 - -### Disabled by default - -- **Null Cipher**: Tests whether the cell suggests using a null cipher (EEA0). +- **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.