diff --git a/Cargo.lock b/Cargo.lock index 2ec92b2..e0b9020 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -482,6 +482,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.52.0", +] + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -602,6 +612,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "derive-into-owned" version = "0.2.0" @@ -1310,6 +1329,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-derive" version = "0.4.2" @@ -1360,6 +1385,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "object" version = "0.32.2" @@ -1487,6 +1521,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1679,6 +1719,7 @@ dependencies = [ "rayhunter", "serde", "serde_json", + "simple_logger", "tempfile", "thiserror", "tokio", @@ -1901,6 +1942,18 @@ dependencies = [ "quote", ] +[[package]] +name = "simple_logger" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c5dfa5e08767553704aa0ffd9d9794d527103c736aba9854773851fd7497eb" +dependencies = [ + "colored", + "log", + "time", + "windows-sys 0.48.0", +] + [[package]] name = "slab" version = "0.4.9" @@ -2065,6 +2118,39 @@ dependencies = [ "weezl", ] +[[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tokio" version = "1.36.0" diff --git a/bin/Cargo.toml b/bin/Cargo.toml index 939985a..28d4e97 100644 --- a/bin/Cargo.toml +++ b/bin/Cargo.toml @@ -32,3 +32,4 @@ clap = { version = "4.5.2", features = ["derive"] } serde_json = "1.0.114" image = "0.25.1" tempfile = "3.10.1" +simple_logger = "5.0.0" diff --git a/bin/src/check.rs b/bin/src/check.rs index 02fa0bd..9a535a8 100644 --- a/bin/src/check.rs +++ b/bin/src/check.rs @@ -1,5 +1,6 @@ use std::{collections::HashMap, future, path::PathBuf, pin::pin}; -use rayhunter::{analysis::analyzer::Harness, diag::DataType, gsmtap_parser, pcap::GsmtapPcapWriter, qmdl::QmdlReader}; +use log::{info, warn}; +use rayhunter::{analysis::analyzer::{EventType, Harness}, diag::DataType, gsmtap_parser, pcap::GsmtapPcapWriter, qmdl::QmdlReader}; use tokio::fs::{metadata, read_dir, File}; use clap::Parser; use futures::TryStreamExt; @@ -9,7 +10,7 @@ mod dummy_analyzer; #[derive(Parser, Debug)] #[command(version, about)] struct Args { - #[arg(short, long)] + #[arg(short = 'p', long)] qmdl_path: PathBuf, #[arg(short, long)] @@ -20,6 +21,9 @@ struct Args { #[arg(long)] enable_dummy_analyzer: bool, + + #[arg(short, long)] + quiet: bool, } async fn analyze_file(harness: &mut Harness, qmdl_path: &str, show_skipped: bool) { @@ -41,20 +45,37 @@ async fn analyze_file(harness: &mut Harness, qmdl_path: &str, show_skipped: bool } for analysis in row.analysis { for maybe_event in analysis.events { - if let Some(event) = maybe_event { - warnings += 1; - println!("{}: {:?}", analysis.timestamp, event); + let Some(event) = maybe_event else { continue }; + match event.event_type { + EventType::Informational => { + info!( + "{}: INFO - {} {}", + qmdl_path, + analysis.timestamp, + event.message, + ); + } + EventType::QualitativeWarning { severity } => { + warn!( + "{}: WARNING (Severity: {:?}) - {} {}", + qmdl_path, + severity, + analysis.timestamp, + event.message, + ); + warnings += 1; + } } } } } if show_skipped && skipped > 0 { - println!("{}: messages skipped:", qmdl_path); + info!("{}: messages skipped:", qmdl_path); for (reason, count) in skipped_reasons.iter() { - println!(" - {}: \"{}\"", count, reason); + info!(" - {}: \"{}\"", count, reason); } } - println!("{}: {} messages analyzed, {} warnings, {} messages skipped", qmdl_path, total_messages, warnings, skipped); + info!("{}: {} messages analyzed, {} warnings, {} messages skipped", qmdl_path, total_messages, warnings, skipped); } async fn pcapify(qmdl_path: &PathBuf) { @@ -75,21 +96,30 @@ async fn pcapify(qmdl_path: &PathBuf) { } } } - println!("wrote pcap to {:?}", &pcap_path); + info!("wrote pcap to {:?}", &pcap_path); } #[tokio::main] async fn main() { - env_logger::init(); let args = Args::parse(); + let level = if args.quiet { + log::LevelFilter::Warn + } else { + log::LevelFilter::Trace + }; + simple_logger::SimpleLogger::new() + .with_colors(true) + .without_timestamps() + .with_level(level) + .init().unwrap(); let mut harness = Harness::new_with_all_analyzers(); if args.enable_dummy_analyzer { harness.add_analyzer(Box::new(dummy_analyzer::TestAnalyzer { count: 0 })); } - println!("Analyzers:"); + info!("Analyzers:"); for analyzer in harness.get_metadata().analyzers { - println!(" - {}: {}", analyzer.name, analyzer.description); + info!(" - {}: {}", analyzer.name, analyzer.description); } let metadata = metadata(&args.qmdl_path).await.expect("failed to get metadata"); diff --git a/lib/src/analysis/analyzer.rs b/lib/src/analysis/analyzer.rs index 03958d5..c1f8da6 100644 --- a/lib/src/analysis/analyzer.rs +++ b/lib/src/analysis/analyzer.rs @@ -7,7 +7,8 @@ use crate::{diag::MessagesContainer, gsmtap_parser}; use super::{ imsi_requested::ImsiRequestedAnalyzer, information_element::InformationElement, - lte_downgrade::LteSib6And7DowngradeAnalyzer, + connection_redirect_downgrade::ConnectionRedirect2GDowngradeAnalyzer, + priority_2g_downgrade::LteSib6And7DowngradeAnalyzer, null_cipher::NullCipherAnalyzer, }; @@ -117,8 +118,9 @@ impl Harness { pub fn new_with_all_analyzers() -> Self { let mut harness = Harness::new(); - harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer{})); harness.add_analyzer(Box::new(ImsiRequestedAnalyzer::new())); + harness.add_analyzer(Box::new(ConnectionRedirect2GDowngradeAnalyzer{})); + harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer{})); harness.add_analyzer(Box::new(NullCipherAnalyzer{})); harness diff --git a/lib/src/analysis/connection_redirect_downgrade.rs b/lib/src/analysis/connection_redirect_downgrade.rs new file mode 100644 index 0000000..e75c089 --- /dev/null +++ b/lib/src/analysis/connection_redirect_downgrade.rs @@ -0,0 +1,42 @@ +use std::borrow::Cow; + +use super::analyzer::{Analyzer, Event, EventType, Severity}; +use super::information_element::{InformationElement, LteInformationElement}; +use telcom_parser::lte_rrc::{DL_DCCH_Message, DL_DCCH_MessageType, DL_DCCH_MessageType_c1, RRCConnectionReleaseCriticalExtensions, RRCConnectionReleaseCriticalExtensions_c1, RedirectedCarrierInfo}; +use super::util::unpack; + +// Based on HITBSecConf presentation "Forcing a targeted LTE cellphone into an +// eavesdropping network" by Lin Huang +pub struct ConnectionRedirect2GDowngradeAnalyzer { +} + +// TODO: keep track of SIB state to compare LTE reselection blocks w/ 2g/3g ones +impl Analyzer for ConnectionRedirect2GDowngradeAnalyzer { + fn get_name(&self) -> Cow { + Cow::from("Connection Release/Redirected Carrier 2G Downgrade") + } + + fn get_description(&self) -> Cow { + Cow::from("Tests if a cell releases our connection and redirects us to a 2G cell.") + } + + fn analyze_information_element(&mut self, ie: &InformationElement) -> Option { + unpack!(InformationElement::LTE(lte_ie) = ie); + unpack!(LteInformationElement::DlDcch(DL_DCCH_Message { message }) = lte_ie); + unpack!(DL_DCCH_MessageType::C1(c1) = message); + unpack!(DL_DCCH_MessageType_c1::RrcConnectionRelease(release) = c1); + unpack!(RRCConnectionReleaseCriticalExtensions::C1(c1) = &release.critical_extensions); + unpack!(RRCConnectionReleaseCriticalExtensions_c1::RrcConnectionRelease_r8(r8_ies) = c1); + unpack!(Some(carrier_info) = &r8_ies.redirected_carrier_info); + match carrier_info { + RedirectedCarrierInfo::Geran(_carrier_freqs_geran) => Some(Event { + event_type: EventType::QualitativeWarning { severity: Severity::High }, + message: format!("Detected 2G downgrade"), + }), + _ => Some(Event { + event_type: EventType::Informational, + message: format!("RRCConnectionRelease CarrierInfo: {:?}", carrier_info), + }), + } + } +} diff --git a/lib/src/analysis/imsi_requested.rs b/lib/src/analysis/imsi_requested.rs index 7b15621..75bb623 100644 --- a/lib/src/analysis/imsi_requested.rs +++ b/lib/src/analysis/imsi_requested.rs @@ -30,7 +30,7 @@ impl Analyzer for ImsiRequestedAnalyzer { return None; }; - // NAS identity request + // NAS identity request, ID type IMSI if payload == &[0x07, 0x55, 0x01] { if self.packet_num < PACKET_THRESHHOLD { return Some(Event { @@ -38,7 +38,7 @@ impl Analyzer for ImsiRequestedAnalyzer { severity: Severity::Medium }, message: format!( - "NAS IMSI request detected, however it was within \ + "NAS IMSI identity request detected, however it was within \ the first {} packets of this analysis. If you just \ turned your device on, this is likely a \ false-positive.", @@ -50,7 +50,7 @@ impl Analyzer for ImsiRequestedAnalyzer { event_type: EventType::QualitativeWarning { severity: Severity::High }, - message: format!("NAS IMSI request detected"), + message: format!("NAS IMSI identity request detected"), }) } } diff --git a/lib/src/analysis/mod.rs b/lib/src/analysis/mod.rs index d777f98..755172d 100644 --- a/lib/src/analysis/mod.rs +++ b/lib/src/analysis/mod.rs @@ -1,6 +1,8 @@ pub mod analyzer; pub mod information_element; -pub mod lte_downgrade; +pub mod priority_2g_downgrade; +pub mod connection_redirect_downgrade; pub mod imsi_provided; pub mod imsi_requested; pub mod null_cipher; +pub mod util; diff --git a/lib/src/analysis/lte_downgrade.rs b/lib/src/analysis/priority_2g_downgrade.rs similarity index 100% rename from lib/src/analysis/lte_downgrade.rs rename to lib/src/analysis/priority_2g_downgrade.rs diff --git a/lib/src/analysis/util.rs b/lib/src/analysis/util.rs new file mode 100644 index 0000000..2076c61 --- /dev/null +++ b/lib/src/analysis/util.rs @@ -0,0 +1,32 @@ + +// Unpacks a pattern, or returns None. +// +// # Examples +// You can use `unpack!` to unroll highly nested enums like this: +// ``` +// enum Foo { +// A(Bar), +// B, +// } +// +// enum Bar { +// C(Baz) +// } +// +// struct Baz; +// +// fn get_bang(foo: Foo) -> Option { +// unpack!(Foo::A(bar) = foo); +// unpack!(Bar::C(baz) = bar); +// baz +// } +// ``` +// +macro_rules! unpack { + ($pat:pat = $val:expr) => { + let $pat = $val else { return None; }; + }; +} + +// this is apparently how you make a macro publicly usable from this module +pub(crate) use unpack;