Skip to content

Commit fd1f4ec

Browse files
committed
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.
1 parent 48e73a0 commit fd1f4ec

File tree

8 files changed

+99
-28
lines changed

8 files changed

+99
-28
lines changed

bin/src/analysis.rs

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use axum::{
88
};
99
use futures::TryStreamExt;
1010
use log::{debug, error, info};
11-
use rayhunter::analysis::analyzer::Harness;
11+
use rayhunter::analysis::analyzer::{AnalyzerConfig, Harness};
1212
use rayhunter::diag::{DataType, MessagesContainer};
1313
use rayhunter::qmdl::QmdlReader;
1414
use serde::Serialize;
@@ -35,8 +35,12 @@ pub struct AnalysisWriter {
3535
// lets us simply append new rows to the end without parsing the entire JSON
3636
// object beforehand.
3737
impl AnalysisWriter {
38-
pub async fn new(file: File, enable_dummy_analyzer: bool) -> Result<Self, std::io::Error> {
39-
let mut harness = Harness::new_with_all_analyzers();
38+
pub async fn new(
39+
file: File,
40+
enable_dummy_analyzer: bool,
41+
analyzer_config: &AnalyzerConfig,
42+
) -> Result<Self, std::io::Error> {
43+
let mut harness = Harness::new_with_config(analyzer_config);
4044
if enable_dummy_analyzer {
4145
harness.add_analyzer(Box::new(TestAnalyzer { count: 0 }));
4246
}
@@ -131,6 +135,7 @@ async fn perform_analysis(
131135
name: &str,
132136
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
133137
enable_dummy_analyzer: bool,
138+
analyzer_config: &AnalyzerConfig,
134139
) -> Result<(), String> {
135140
info!("Opening QMDL and analysis file for {}...", name);
136141
let (analysis_file, qmdl_file, entry_index) = {
@@ -150,9 +155,10 @@ async fn perform_analysis(
150155
(analysis_file, qmdl_file, entry_index)
151156
};
152157

153-
let mut analysis_writer = AnalysisWriter::new(analysis_file, enable_dummy_analyzer)
154-
.await
155-
.map_err(|e| format!("{:?}", e))?;
158+
let mut analysis_writer =
159+
AnalysisWriter::new(analysis_file, enable_dummy_analyzer, analyzer_config)
160+
.await
161+
.map_err(|e| format!("{:?}", e))?;
156162
let file_size = qmdl_file
157163
.metadata()
158164
.await
@@ -196,6 +202,7 @@ pub fn run_analysis_thread(
196202
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
197203
analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
198204
enable_dummy_analyzer: bool,
205+
analyzer_config: AnalyzerConfig,
199206
) {
200207
task_tracker.spawn(async move {
201208
loop {
@@ -204,9 +211,13 @@ pub fn run_analysis_thread(
204211
let count = queued_len(analysis_status_lock.clone()).await;
205212
for _ in 0..count {
206213
let name = dequeue_to_running(analysis_status_lock.clone()).await;
207-
if let Err(err) =
208-
perform_analysis(&name, qmdl_store_lock.clone(), enable_dummy_analyzer)
209-
.await
214+
if let Err(err) = perform_analysis(
215+
&name,
216+
qmdl_store_lock.clone(),
217+
enable_dummy_analyzer,
218+
&analyzer_config,
219+
)
220+
.await
210221
{
211222
error!("failed to analyze {}: {}", name, err);
212223
}

bin/src/check.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use clap::Parser;
22
use futures::TryStreamExt;
33
use log::{info, warn};
44
use rayhunter::{
5-
analysis::analyzer::{EventType, Harness},
5+
analysis::analyzer::{AnalyzerConfig, EventType, Harness},
66
diag::DataType,
77
gsmtap_parser,
88
pcap::GsmtapPcapWriter,
@@ -33,7 +33,7 @@ struct Args {
3333
}
3434

3535
async fn analyze_file(enable_dummy_analyzer: bool, qmdl_path: &str, show_skipped: bool) {
36-
let mut harness = Harness::new_with_all_analyzers();
36+
let mut harness = Harness::new_with_config(&AnalyzerConfig::default());
3737
if enable_dummy_analyzer {
3838
harness.add_analyzer(Box::new(dummy_analyzer::TestAnalyzer { count: 0 }));
3939
}
@@ -141,7 +141,7 @@ async fn main() {
141141
.unwrap();
142142
info!("Analyzers:");
143143

144-
let mut harness = Harness::new_with_all_analyzers();
144+
let mut harness = Harness::new_with_config(&AnalyzerConfig::default());
145145
if args.enable_dummy_analyzer {
146146
harness.add_analyzer(Box::new(dummy_analyzer::TestAnalyzer { count: 0 }));
147147
}

bin/src/config.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
use crate::error::RayhunterError;
2-
31
use serde::Deserialize;
42

3+
use rayhunter::analysis::analyzer::AnalyzerConfig;
4+
5+
use crate::error::RayhunterError;
6+
57
#[derive(Debug, Deserialize)]
68
#[serde(default)]
79
pub struct Config {
@@ -12,6 +14,7 @@ pub struct Config {
1214
pub enable_dummy_analyzer: bool,
1315
pub colorblind_mode: bool,
1416
pub key_input_mode: u8,
17+
pub analyzers: AnalyzerConfig,
1518
}
1619

1720
impl Default for Config {
@@ -24,6 +27,7 @@ impl Default for Config {
2427
enable_dummy_analyzer: false,
2528
colorblind_mode: false,
2629
key_input_mode: 1,
30+
analyzers: AnalyzerConfig::default(),
2731
}
2832
}
2933
}

bin/src/daemon.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ async fn main() -> Result<(), RayhunterError> {
199199
qmdl_store_lock.clone(),
200200
analysis_tx.clone(),
201201
config.enable_dummy_analyzer,
202+
config.analyzers.clone(),
202203
);
203204
info!("Starting UI");
204205
display::update_ui(&task_tracker, &config, ui_shutdown_rx, ui_update_rx);
@@ -215,6 +216,7 @@ async fn main() -> Result<(), RayhunterError> {
215216
qmdl_store_lock.clone(),
216217
analysis_status_lock.clone(),
217218
config.enable_dummy_analyzer,
219+
config.analyzers.clone(),
218220
);
219221
run_ctrl_c_thread(
220222
&task_tracker,

bin/src/diag.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use axum::http::StatusCode;
88
use axum::response::{IntoResponse, Response};
99
use futures::{StreamExt, TryStreamExt};
1010
use log::{debug, error, info, warn};
11+
use rayhunter::analysis::analyzer::AnalyzerConfig;
1112
use rayhunter::diag::DataType;
1213
use rayhunter::diag_device::DiagDevice;
1314
use rayhunter::qmdl::QmdlWriter;
@@ -36,12 +37,13 @@ pub fn run_diag_read_thread(
3637
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
3738
analysis_sender: Sender<AnalysisCtrlMessage>,
3839
enable_dummy_analyzer: bool,
40+
analyzer_config: AnalyzerConfig,
3941
) {
4042
task_tracker.spawn(async move {
4143
let (initial_qmdl_file, initial_analysis_file) = qmdl_store_lock.write().await.new_entry().await.expect("failed creating QMDL file entry");
4244
let mut maybe_qmdl_writer: Option<QmdlWriter<File>> = Some(QmdlWriter::new(initial_qmdl_file));
4345
let mut diag_stream = pin!(dev.as_stream().into_stream());
44-
let mut maybe_analysis_writer = Some(AnalysisWriter::new(initial_analysis_file, enable_dummy_analyzer).await
46+
let mut maybe_analysis_writer = Some(AnalysisWriter::new(initial_analysis_file, enable_dummy_analyzer, &analyzer_config).await
4547
.expect("failed to create analysis writer"));
4648
loop {
4749
tokio::select! {
@@ -63,7 +65,7 @@ pub fn run_diag_read_thread(
6365
analysis_writer.close().await.expect("failed to close analysis writer");
6466
}
6567

66-
maybe_analysis_writer = Some(AnalysisWriter::new(new_analysis_file, enable_dummy_analyzer).await
68+
maybe_analysis_writer = Some(AnalysisWriter::new(new_analysis_file, enable_dummy_analyzer, &analyzer_config).await
6769
.expect("failed to write to analysis file"));
6870

6971
if let Err(e) = ui_update_sender.send(display::DisplayState::Recording).await {

dist/config.toml.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,12 @@ ui_level = 1
2020
# 0 = rayhunter does not read button presses
2121
# 1 = double-tapping the power button starts/stops recordings
2222
key_input_mode = 1
23+
24+
# Analyzer Configuration
25+
# Enable/disable specific IMSI catcher detection heuristics
26+
# See https://github.com/EFForg/rayhunter/blob/main/doc/heuristics.md for details
27+
[analyzers]
28+
imsi_requested = true
29+
connection_redirect_2g_downgrade = true
30+
lte_sib6_and_7_downgrade = true
31+
null_cipher = false

doc/heuristics.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
11
# Heuristics
22

3-
TODO
3+
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.
4+
5+
## Available Analyzers
6+
7+
- **IMSI Requested**: Tests whether the ME sends an IMSI Identity Request NAS message
8+
- **Connection Release/Redirected Carrier 2G Downgrade**: Tests if a cell
9+
releases our connection and redirects us to a 2G cell. This heuristic only
10+
makes sense in the US, european users may want to disable it.
11+
- **LTE SIB6/7 Downgrade**: Tests for LTE cells broadcasting a SIB type 6 and 7
12+
which include 2G/3G frequencies with higher priorities
13+
14+
### Disabled by default
15+
16+
- **Null Cipher**: Tests whether the cell suggests using a null cipher (EEA0).
17+
This is currently disabled by default due to a parsing bug triggering false
18+
positives.

lib/src/analysis/analyzer.rs

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use chrono::{DateTime, FixedOffset};
2-
use serde::Serialize;
2+
use serde::{Deserialize, Serialize};
33
use std::borrow::Cow;
44

55
use crate::util::RuntimeMetadata;
@@ -8,9 +8,32 @@ use crate::{diag::MessagesContainer, gsmtap_parser};
88
use super::{
99
connection_redirect_downgrade::ConnectionRedirect2GDowngradeAnalyzer,
1010
imsi_requested::ImsiRequestedAnalyzer, information_element::InformationElement,
11-
priority_2g_downgrade::LteSib6And7DowngradeAnalyzer,
11+
null_cipher::NullCipherAnalyzer, priority_2g_downgrade::LteSib6And7DowngradeAnalyzer,
1212
};
1313

14+
#[derive(Debug, Clone, Deserialize)]
15+
#[serde(default)]
16+
pub struct AnalyzerConfig {
17+
pub imsi_requested: bool,
18+
pub connection_redirect_2g_downgrade: bool,
19+
pub lte_sib6_and_7_downgrade: bool,
20+
pub null_cipher: bool,
21+
}
22+
23+
impl Default for AnalyzerConfig {
24+
fn default() -> Self {
25+
AnalyzerConfig {
26+
imsi_requested: true,
27+
connection_redirect_2g_downgrade: true,
28+
lte_sib6_and_7_downgrade: true,
29+
// FIXME: our RRC parser is reporting false positives for this due to an
30+
// upstream hampi bug (https://github.com/ystero-dev/hampi/issues/133).
31+
// once that's fixed, we should regenerate our parser and re-enable this
32+
null_cipher: false,
33+
}
34+
}
35+
}
36+
1437
/// Qualitative measure of how severe a Warning event type is.
1538
/// The levels should break down like this:
1639
/// * Low: if combined with a large number of other Warnings, user should investigate
@@ -122,16 +145,21 @@ impl Harness {
122145
}
123146
}
124147

125-
pub fn new_with_all_analyzers() -> Self {
148+
pub fn new_with_config(analyzer_config: &AnalyzerConfig) -> Self {
126149
let mut harness = Harness::new();
127-
harness.add_analyzer(Box::new(ImsiRequestedAnalyzer::new()));
128-
harness.add_analyzer(Box::new(ConnectionRedirect2GDowngradeAnalyzer {}));
129-
harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer {}));
130-
131-
// FIXME: our RRC parser is reporting false positives for this due to an
132-
// upstream hampi bug (https://github.com/ystero-dev/hampi/issues/133).
133-
// once that's fixed, we should regenerate our parser and re-enable this
134-
// harness.add_analyzer(Box::new(NullCipherAnalyzer{}));
150+
151+
if analyzer_config.imsi_requested {
152+
harness.add_analyzer(Box::new(ImsiRequestedAnalyzer::new()));
153+
}
154+
if analyzer_config.connection_redirect_2g_downgrade {
155+
harness.add_analyzer(Box::new(ConnectionRedirect2GDowngradeAnalyzer {}));
156+
}
157+
if analyzer_config.lte_sib6_and_7_downgrade {
158+
harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer {}));
159+
}
160+
if analyzer_config.null_cipher {
161+
harness.add_analyzer(Box::new(NullCipherAnalyzer {}));
162+
}
135163

136164
harness
137165
}

0 commit comments

Comments
 (0)