Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions bin/src/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<Self, std::io::Error> {
let mut harness = Harness::new_with_all_analyzers();
pub async fn new(
file: File,
enable_dummy_analyzer: bool,
analyzer_config: &AnalyzerConfig,
) -> Result<Self, std::io::Error> {
let mut harness = Harness::new_with_config(analyzer_config);
if enable_dummy_analyzer {
harness.add_analyzer(Box::new(TestAnalyzer { count: 0 }));
}
Expand Down Expand Up @@ -131,6 +135,7 @@ async fn perform_analysis(
name: &str,
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
enable_dummy_analyzer: bool,
analyzer_config: &AnalyzerConfig,
) -> Result<(), String> {
info!("Opening QMDL and analysis file for {}...", name);
let (analysis_file, qmdl_file, entry_index) = {
Expand All @@ -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
Expand Down Expand Up @@ -196,6 +202,7 @@ pub fn run_analysis_thread(
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
enable_dummy_analyzer: bool,
analyzer_config: AnalyzerConfig,
) {
task_tracker.spawn(async move {
loop {
Expand All @@ -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);
}
Expand Down
6 changes: 3 additions & 3 deletions bin/src/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 }));
}
Expand Down Expand Up @@ -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 }));
}
Expand Down
8 changes: 6 additions & 2 deletions bin/src/config.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 {
Expand All @@ -24,6 +27,7 @@ impl Default for Config {
enable_dummy_analyzer: false,
colorblind_mode: false,
key_input_mode: 1,
analyzers: AnalyzerConfig::default(),
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions bin/src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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,
Expand Down
6 changes: 4 additions & 2 deletions bin/src/diag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -36,12 +37,13 @@ pub fn run_diag_read_thread(
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
analysis_sender: Sender<AnalysisCtrlMessage>,
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<QmdlWriter<File>> = 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! {
Expand All @@ -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 {
Expand Down
9 changes: 9 additions & 0 deletions dist/config.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
17 changes: 16 additions & 1 deletion doc/heuristics.md
Original file line number Diff line number Diff line change
@@ -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.
50 changes: 39 additions & 11 deletions lib/src/analysis/analyzer.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use chrono::{DateTime, FixedOffset};
use serde::Serialize;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;

use crate::util::RuntimeMetadata;
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand Down