Compare commits

...

37 Commits

Author SHA1 Message Date
Cooper Quintin ee83613757 update readme 2025-02-27 17:29:48 -08:00
Cooper Quintin 840f8ad8b0 stop before upload in case file is locked from writing by running process 2025-02-10 11:26:27 -08:00
Cooper Quintin c9ac834ca7 show warnings in web UI 2025-02-10 11:26:27 -08:00
Cooper Quintin 8629aacf6b switch default to not see trace messages, switch arg from quiet to verbose 2025-02-10 11:26:27 -08:00
Cooper Quintin a3fd1479f9 rename qmdl path so that downloaded files have a qmdl extension 2025-02-10 11:26:27 -08:00
Cooper Quintin 049c563f02 fix shortcodes on rayhunter_check 2025-02-10 11:26:27 -08:00
Cooper Quintin a33b5a3418 Update README.md
Co-authored-by: Will Greenberg <willg@eff.org>
2025-01-31 17:00:44 -08:00
Cooper Quintin 107ba58296 warn if running install scritps from git tree 2025-01-31 17:00:44 -08:00
Cooper Quintin d016279172 some tweaks to readme 2025-01-31 17:00:44 -08:00
Will Greenberg 5a084f1abb lib: set uplink flag for NAS 2025-01-30 11:33:14 -08:00
Will Greenberg 3619df32ab check: give qmdl-path a shorthand arg 2025-01-28 11:02:19 -08:00
Will Greenberg 34d87d1fd7 this macro isn't public, so docstrings won't work 2025-01-28 11:02:19 -08:00
Will Greenberg da4952e70f fix docstring code 2025-01-28 11:02:19 -08:00
Will Greenberg 30323b8329 Keep old 2G downgrade analyzer 2025-01-28 11:02:19 -08:00
Will Greenberg 28b0f409db fix attribution 2025-01-28 11:02:19 -08:00
Will Greenberg 12640cc878 Rewrite our 2G downgrade analyzer 2025-01-28 11:02:19 -08:00
Will Greenberg 26eda5904f Better wording on IMSI requested warning 2025-01-28 11:02:19 -08:00
Will Greenberg 3e26e61b05 check: don't count informational events as warnings, better logging 2025-01-28 11:02:19 -08:00
Will Greenberg 565c0f1e67 serial: fix UTF-8 panic on macOS 2025-01-26 17:05:42 -08:00
Will Greenberg 6bd36921d8 consider early IMSI request medium sev 2025-01-08 15:23:59 -08:00
Will Greenberg c83ae30be8 fix language 2025-01-08 15:23:59 -08:00
Will Greenberg fa612241a5 lib: add IMSI requested heuristic 2025-01-08 15:23:59 -08:00
Will Greenberg 10592bbd9d lib: add inbound/outbound field to NAS 2025-01-06 16:24:11 -08:00
Will Greenberg 327eaddcd7 rayhunter-check: pcapify qmdl 2025-01-06 16:24:11 -08:00
Will Greenberg 32149c3b37 Update tools/nasparse.py 2024-12-17 14:46:31 -08:00
Cooper Quintin e47d4dacc4 raise error on non nas message 2024-12-17 14:46:31 -08:00
Cooper Quintin 4009e3d1ed fix nits 2024-12-17 14:46:31 -08:00
Cooper Quintin b2cd735a07 proof of concept pcap reader for nas heuristic 2024-12-17 14:46:31 -08:00
Cooper Quintin 94e9a88a91 PoC of python nas heuristic 2024-12-17 14:46:31 -08:00
Cooper Quintin f4a6c834d2 remove false positive IMSI heuristic until we get a NAS parser 2024-12-09 10:53:58 -08:00
Cooper Quintin 95e8f846d3 propegate colorblind mode beyond start/stop 2024-11-26 11:05:13 -08:00
Cooper Quintin 15f128add1 remove unneeded import 2024-11-26 11:05:13 -08:00
Cooper Quintin 87f9cc403b add colorblind mode. Fixes #77 2024-11-26 11:05:13 -08:00
Cooper Quintin 7addf3a67f fix reboot timeout 2024-11-18 17:10:16 -08:00
Cooper Quintin 4d8cc9b738 Revert "name binary rayhunter-daemon"
This reverts commit 9cd5ce3394.
2024-11-18 16:16:43 -08:00
Cooper Quintin b0d797d206 name binary rayhunter-daemon 2024-11-18 16:16:43 -08:00
Will Greenberg 1ae3b5020b fix installer script
With the odd permissions issues we've been seeing, we should use
AT_SYSCMD for all mv operations into /data
2024-11-18 16:16:43 -08:00
30 changed files with 556 additions and 66 deletions
Generated
+86
View File
@@ -482,6 +482,16 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 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]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.6" version = "0.8.6"
@@ -602,6 +612,15 @@ dependencies = [
"syn 1.0.109", "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]] [[package]]
name = "derive-into-owned" name = "derive-into-owned"
version = "0.2.0" version = "0.2.0"
@@ -1310,6 +1329,12 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]] [[package]]
name = "num-derive" name = "num-derive"
version = "0.4.2" version = "0.4.2"
@@ -1360,6 +1385,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "num_threads"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "object" name = "object"
version = "0.32.2" version = "0.32.2"
@@ -1487,6 +1521,12 @@ dependencies = [
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.17" version = "0.2.17"
@@ -1679,6 +1719,7 @@ dependencies = [
"rayhunter", "rayhunter",
"serde", "serde",
"serde_json", "serde_json",
"simple_logger",
"tempfile", "tempfile",
"thiserror", "thiserror",
"tokio", "tokio",
@@ -1901,6 +1942,18 @@ dependencies = [
"quote", "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]] [[package]]
name = "slab" name = "slab"
version = "0.4.9" version = "0.4.9"
@@ -2065,6 +2118,39 @@ dependencies = [
"weezl", "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]] [[package]]
name = "tokio" name = "tokio"
version = "1.36.0" version = "1.36.0"
+10 -26
View File
@@ -1,46 +1,30 @@
![Rayhunter Logo - An Orca taking a bite out of a cellular signal bar](https://www.eff.org/files/styles/media_browser_preview/public/banner_library/rayhunter-banner.png)
# Rayhunter # Rayhunter
```
@@@@@@@ @@@@@@ @@@ @@@ @@@ @@@ @@@ @@@ @@@ @@@ @@@@@@@ @@@@@@@@ @@@@@@@
@@! @@@ @@! @@@ @@! !@@ @@! @@@ @@! @@@ @@!@!@@@ @@! @@! @@! @@@
@!@!!@! @!@!@!@! !@!@! @!@!@!@! @!@ !@! @!@@!!@! @!! @!!!:! @!@!!@!
!!: :!! !!: !!! !!: !!: !!! !!: !!! !!: !!! !!: !!: !!: :!!
: : : : : : .: : : : :.:: : :: : : : :: ::: : : :
_ _ _ _ _ _ _ _
)`'-.,_)`'-.,_)`'-.,_)`'-.,_)`'-.,_)`'-.,_)`'-.,_)`'-.,_
O .
O ' '
o ' .
o .'
__________.-' '...___
.-' ### '''...__
/ a### ## ''--.._ ______
'. # ######## ' .-'
'-._ ..**********#### ___...---'''\ '
'-._ __________...---''' \ l
\ | apc '._|
\__;
```
![Tests](https://github.com/EFForg/rayhunter/actions/workflows/check-and-test.yml/badge.svg) ![Tests](https://github.com/EFForg/rayhunter/actions/workflows/check-and-test.yml/badge.svg)
Rayhunter is an IMSI Catcher Catcher for the Orbic mobile hotspot. Rayhunter is an IMSI Catcher Catcher for the Orbic mobile hotspot.
**THIS CODE IS PROOF OF CONCEPT AND SHOULD NOT BE RELIED UPON IN HIGH RISK SITUATIONS** **THIS CODE IS PROOF OF CONCEPT AND SHOULD NOT BE RELIED UPON IN HIGH RISK SITUATIONS**
## The Hardware
Code is built and tested for the Orbic RC400L mobile hotspot, it may work on other orbics and other Code is built and tested for the Orbic RC400L mobile hotspot, it may work on other orbics and other
linux/qualcom devices but this is the only one we have tested on. Buy the orbic [using bezos bucks](https://www.amazon.com/gp/product/B09CLS6Z7X/) linux/qualcom devices but this is the only one we have tested on.
Buy the orbic [using bezos bucks](https://www.amazon.com/Orbic-Verizon-Hotspot-Connect-Enabled/dp/B08N3CHC4Y)
Or on [Ebay](https://www.ebay.com/sch/i.html?_nkw=orbic+rc400l)
## Setup ## Setup
*NOTE: We don't currently support automated installs on windows, you will have to follow the manual install instructions below* *NOTE: We don't currently support automated installs on windows, you will have to follow the manual install instructions below*
1. Download the latest [rayhunter release bundle](https://github.com/EFForg/rayhunter/releases) and extract it. 1. Download the latest [rayhunter release bundle](https://github.com/EFForg/rayhunter/releases) and extract it.
2. Run the install script inside the bundle corresponding to your platform (`install-linux.sh`, `install-mac.sh`). **If you are installing from the cloned github repository please see the development instructions below, running `install-linux.sh` from the git tree will not work.**
2. Run the install script inside the bundle corresponding to your platform (`install-linux.sh`, `install-mac.sh`). The Linux installer has only been tested on the latest version of Ubuntu. If it fails you will need to follow the install steps outlined in **Development** below.
3. Once finished, rayhunter should be running! You can verify this by visiting the web UI as described below. 3. Once finished, rayhunter should be running! You can verify this by visiting the web UI as described below.
## Usage ## Usage
Once installed, rayhunter will run automatically whenever your Orbic device is running. It serves a web UI that provides some basic controls, such as being able to start/stop recordings, download captures, and view heuristic analyses of captures. You can access this UI in one of two ways: Once installed, rayhunter will run automatically whenever your Orbic device is running. It serves a web UI that provides some basic controls, such as being able to start/stop recordings, download captures, and view heuristic analyses of captures. You can access this UI in one of two ways:
+1
View File
@@ -32,3 +32,4 @@ clap = { version = "4.5.2", features = ["derive"] }
serde_json = "1.0.114" serde_json = "1.0.114"
image = "0.25.1" image = "0.25.1"
tempfile = "3.10.1" tempfile = "3.10.1"
simple_logger = "5.0.0"
+76 -13
View File
@@ -1,5 +1,6 @@
use std::{collections::HashMap, future, path::PathBuf, pin::pin}; use std::{collections::HashMap, future, path::PathBuf, pin::pin};
use rayhunter::{analysis::analyzer::Harness, diag::DataType, 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 tokio::fs::{metadata, read_dir, File};
use clap::Parser; use clap::Parser;
use futures::TryStreamExt; use futures::TryStreamExt;
@@ -9,14 +10,20 @@ mod dummy_analyzer;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(version, about)] #[command(version, about)]
struct Args { struct Args {
#[arg(short, long)] #[arg(short = 'p', long)]
qmdl_path: PathBuf, qmdl_path: PathBuf,
#[arg(short = 'c', long)]
pcapify: bool,
#[arg(long)] #[arg(long)]
show_skipped: bool, show_skipped: bool,
#[arg(long)] #[arg(long)]
enable_dummy_analyzer: bool, enable_dummy_analyzer: bool,
#[arg(short, long)]
verbose: bool,
} }
async fn analyze_file(harness: &mut Harness, qmdl_path: &str, show_skipped: bool) { async fn analyze_file(harness: &mut Harness, qmdl_path: &str, show_skipped: bool) {
@@ -38,34 +45,81 @@ async fn analyze_file(harness: &mut Harness, qmdl_path: &str, show_skipped: bool
} }
for analysis in row.analysis { for analysis in row.analysis {
for maybe_event in analysis.events { for maybe_event in analysis.events {
if let Some(event) = maybe_event { let Some(event) = maybe_event else { continue };
warnings += 1; match event.event_type {
println!("{}: {:?}", analysis.timestamp, event); 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 { if show_skipped && skipped > 0 {
println!("{}: messages skipped:", qmdl_path); info!("{}: messages skipped:", qmdl_path);
for (reason, count) in skipped_reasons.iter() { 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) {
let qmdl_file = &mut File::open(&qmdl_path).await.expect("failed to open qmdl file");
let qmdl_file_size = qmdl_file.metadata().await.unwrap().len();
let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(qmdl_file_size as usize));
let mut pcap_path = qmdl_path.clone();
pcap_path.set_extension("pcap");
let pcap_file = &mut File::create(&pcap_path).await.expect("failed to open pcap file");
let mut pcap_writer = GsmtapPcapWriter::new(pcap_file).await.unwrap();
pcap_writer.write_iface_header().await.unwrap();
while let Some(container) = qmdl_reader.get_next_messages_container().await.expect("failed to get container") {
for maybe_msg in container.into_messages() {
if let Ok(msg) = maybe_msg {
if let Ok(Some((timestamp, parsed))) = gsmtap_parser::parse(msg) {
pcap_writer.write_gsmtap_message(parsed, timestamp).await.expect("failed to write");
}
}
}
}
info!("wrote pcap to {:?}", &pcap_path);
} }
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
env_logger::init();
let args = Args::parse(); let args = Args::parse();
let level = if args.verbose {
log::LevelFilter::Trace
} else {
log::LevelFilter::Warn
};
simple_logger::SimpleLogger::new()
.with_colors(true)
.without_timestamps()
.with_level(level)
.init().unwrap();
let mut harness = Harness::new_with_all_analyzers(); let mut harness = Harness::new_with_all_analyzers();
if args.enable_dummy_analyzer { if args.enable_dummy_analyzer {
harness.add_analyzer(Box::new(dummy_analyzer::TestAnalyzer { count: 0 })); harness.add_analyzer(Box::new(dummy_analyzer::TestAnalyzer { count: 0 }));
} }
println!("Analyzers:"); info!("Analyzers:");
for analyzer in harness.get_metadata().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"); let metadata = metadata(&args.qmdl_path).await.expect("failed to get metadata");
@@ -75,10 +129,19 @@ async fn main() {
let name = entry.file_name(); let name = entry.file_name();
let name_str = name.to_str().unwrap(); let name_str = name.to_str().unwrap();
if name_str.ends_with(".qmdl") { if name_str.ends_with(".qmdl") {
analyze_file(&mut harness, entry.path().to_str().unwrap(), args.show_skipped).await; let path = entry.path();
let path_str = path.to_str().unwrap();
analyze_file(&mut harness, path_str, args.show_skipped).await;
if args.pcapify {
pcapify(&path).await;
}
} }
} }
} else { } else {
analyze_file(&mut harness, args.qmdl_path.to_str().unwrap(), args.show_skipped).await; let path = args.qmdl_path.to_str().unwrap();
analyze_file(&mut harness, path, args.show_skipped).await;
if args.pcapify {
pcapify(&args.qmdl_path).await;
}
} }
} }
+4
View File
@@ -9,6 +9,7 @@ struct ConfigFile {
debug_mode: Option<bool>, debug_mode: Option<bool>,
ui_level: Option<u8>, ui_level: Option<u8>,
enable_dummy_analyzer: Option<bool>, enable_dummy_analyzer: Option<bool>,
colorblind_mode: Option<bool>,
} }
#[derive(Debug)] #[derive(Debug)]
@@ -18,6 +19,7 @@ pub struct Config {
pub debug_mode: bool, pub debug_mode: bool,
pub ui_level: u8, pub ui_level: u8,
pub enable_dummy_analyzer: bool, pub enable_dummy_analyzer: bool,
pub colorblind_mode: bool,
} }
impl Default for Config { impl Default for Config {
@@ -28,6 +30,7 @@ impl Default for Config {
debug_mode: false, debug_mode: false,
ui_level: 1, ui_level: 1,
enable_dummy_analyzer: false, enable_dummy_analyzer: false,
colorblind_mode: false,
} }
} }
} }
@@ -42,6 +45,7 @@ pub fn parse_config<P>(path: P) -> Result<Config, RayhunterError> where P: AsRef
parsed_config.debug_mode.map(|v| config.debug_mode = v); parsed_config.debug_mode.map(|v| config.debug_mode = v);
parsed_config.ui_level.map(|v| config.ui_level = v); parsed_config.ui_level.map(|v| config.ui_level = v);
parsed_config.enable_dummy_analyzer.map(|v| config.enable_dummy_analyzer = v); parsed_config.enable_dummy_analyzer.map(|v| config.enable_dummy_analyzer = v);
parsed_config.colorblind_mode.map(|v| config.colorblind_mode = v);
} }
Ok(config) Ok(config)
} }
+7 -1
View File
@@ -59,6 +59,7 @@ async fn run_server(
debug_mode: config.debug_mode, debug_mode: config.debug_mode,
analysis_status_lock, analysis_status_lock,
analysis_sender, analysis_sender,
colorblind_mode: config.colorblind_mode,
}); });
let app = Router::new() let app = Router::new()
@@ -142,12 +143,17 @@ fn run_ctrl_c_thread(
fn update_ui(task_tracker: &TaskTracker, config: &config::Config, mut ui_shutdown_rx: oneshot::Receiver<()>, mut ui_update_rx: Receiver<framebuffer::DisplayState>) -> JoinHandle<()> { fn update_ui(task_tracker: &TaskTracker, config: &config::Config, mut ui_shutdown_rx: oneshot::Receiver<()>, mut ui_update_rx: Receiver<framebuffer::DisplayState>) -> JoinHandle<()> {
static IMAGE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/static/images/"); static IMAGE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/static/images/");
let mut display_color: framebuffer::Color565;
let display_level = config.ui_level; let display_level = config.ui_level;
if display_level == 0 { if display_level == 0 {
info!("Invisible mode, not spawning UI."); info!("Invisible mode, not spawning UI.");
} }
let mut display_color = framebuffer::Color565::Green; if config.colorblind_mode {
display_color = framebuffer::Color565::Blue;
} else {
display_color = framebuffer::Color565::Green;
}
task_tracker.spawn_blocking(move || { task_tracker.spawn_blocking(move || {
let mut fb: Framebuffer = Framebuffer::new(); let mut fb: Framebuffer = Framebuffer::new();
+9 -1
View File
@@ -129,8 +129,16 @@ pub async fn start_recording(State(state): State<Arc<ServerState>>) -> Result<(S
let qmdl_writer = QmdlWriter::new(qmdl_file); let qmdl_writer = QmdlWriter::new(qmdl_file);
state.diag_device_ctrl_sender.send(DiagDeviceCtrlMessage::StartRecording((qmdl_writer, analysis_file))).await state.diag_device_ctrl_sender.send(DiagDeviceCtrlMessage::StartRecording((qmdl_writer, analysis_file))).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't send stop recording message: {}", e)))?; .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't send stop recording message: {}", e)))?;
state.ui_update_sender.send(framebuffer::DisplayState::Recording).await
let display_state: framebuffer::DisplayState;
if state.colorblind_mode {
display_state = framebuffer::DisplayState::RecordingCBM;
} else {
display_state = framebuffer::DisplayState::Recording;
}
state.ui_update_sender.send(display_state).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't send ui update message: {}", e)))?; .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't send ui update message: {}", e)))?;
Ok((StatusCode::ACCEPTED, "ok".to_string())) Ok((StatusCode::ACCEPTED, "ok".to_string()))
} }
+2
View File
@@ -27,6 +27,7 @@ pub enum DisplayState {
Recording, Recording,
Paused, Paused,
WarningDetected, WarningDetected,
RecordingCBM,
} }
impl From<DisplayState> for Color565 { impl From<DisplayState> for Color565 {
@@ -34,6 +35,7 @@ impl From<DisplayState> for Color565 {
match state { match state {
DisplayState::Paused => Color565::White, DisplayState::Paused => Color565::White,
DisplayState::Recording => Color565::Green, DisplayState::Recording => Color565::Green,
DisplayState::RecordingCBM => Color565::Blue,
DisplayState::WarningDetected => Color565::Red, DisplayState::WarningDetected => Color565::Red,
} }
} }
+5 -3
View File
@@ -22,13 +22,15 @@ pub struct ServerState {
pub ui_update_sender: Sender<framebuffer::DisplayState>, pub ui_update_sender: Sender<framebuffer::DisplayState>,
pub analysis_status_lock: Arc<RwLock<AnalysisStatus>>, pub analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
pub analysis_sender: Sender<AnalysisCtrlMessage>, pub analysis_sender: Sender<AnalysisCtrlMessage>,
pub debug_mode: bool pub debug_mode: bool,
pub colorblind_mode: bool,
} }
pub async fn get_qmdl(State(state): State<Arc<ServerState>>, Path(qmdl_name): Path<String>) -> Result<Response, (StatusCode, String)> { pub async fn get_qmdl(State(state): State<Arc<ServerState>>, Path(qmdl_name): Path<String>) -> Result<Response, (StatusCode, String)> {
let qmdl_idx = qmdl_name.trim_end_matches(".qmdl");
let qmdl_store = state.qmdl_store_lock.read().await; let qmdl_store = state.qmdl_store_lock.read().await;
let (entry_index, entry) = qmdl_store.entry_for_name(&qmdl_name) let (entry_index, entry) = qmdl_store.entry_for_name(&qmdl_idx)
.ok_or((StatusCode::NOT_FOUND, format!("couldn't find qmdl file with name {}", qmdl_name)))?; .ok_or((StatusCode::NOT_FOUND, format!("couldn't find qmdl file with name {}", qmdl_idx)))?;
let qmdl_file = qmdl_store.open_entry_qmdl(entry_index).await let qmdl_file = qmdl_store.open_entry_qmdl(entry_index).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("error opening QMDL file: {}", e)))?; .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("error opening QMDL file: {}", e)))?;
let limited_qmdl_file = qmdl_file.take(entry.qmdl_size_bytes as u64); let limited_qmdl_file = qmdl_file.take(entry.qmdl_size_bytes as u64);
+7 -2
View File
@@ -80,6 +80,11 @@ async function updateEntryAnalysisResult(entry) {
entry.analysis_result = `0 warnings!`; entry.analysis_result = `0 warnings!`;
} else { } else {
entry.analysis_result = `!!! ${entry.analysis.warnings.length} warnings !!!`; entry.analysis_result = `!!! ${entry.analysis.warnings.length} warnings !!!`;
for(warning of entry.analysis.warnings){
msg = `${warning.timestamp}: ${warning.warning.events[1].message}`
console.log(msg)
entry.analysis_result += `<br>${msg}`
}
} }
} }
@@ -136,11 +141,11 @@ function createEntryRow(entry, isCurrent) {
row.appendChild(pcapTd); row.appendChild(pcapTd);
const qmdlTd = document.createElement('td'); const qmdlTd = document.createElement('td');
qmdlTd.appendChild(createLink(`/api/qmdl/${entry.name}`, 'qmdl')); qmdlTd.appendChild(createLink(`/api/qmdl/${entry.name}.qmdl`, 'qmdl'));
row.appendChild(qmdlTd); row.appendChild(qmdlTd);
const analysisResult = document.createElement('td'); const analysisResult = document.createElement('td');
analysisResult.innerText = entry.analysis_result; analysisResult.innerHTML = entry.analysis_result;
if (entry.analysis.warnings.length > 0) { if (entry.analysis.warnings.length > 0) {
row.classList.add("warning"); row.classList.add("warning");
} }
+3
View File
@@ -1,6 +1,9 @@
# cat config.toml # cat config.toml
qmdl_store_path = "/data/rayhunter/qmdl" qmdl_store_path = "/data/rayhunter/qmdl"
port = 8080 port = 8080
debug_mode = false
enable_dummy_analyzer = false
colorblind_mode = false
# UI Levels: # UI Levels:
# 0 = invisible mode, no indicator that rayhunter is running # 0 = invisible mode, no indicator that rayhunter is running
# 1 = Subtle mode, display a green line at the top of the screen when rayhunter is running # 1 = Subtle mode, display a green line at the top of the screen when rayhunter is running
+9 -5
View File
@@ -65,16 +65,20 @@ _at_syscmd() {
setup_rayhunter() { setup_rayhunter() {
_at_syscmd "mkdir -p /data/rayhunter" _at_syscmd "mkdir -p /data/rayhunter"
_adb_push config.toml.example /data/rayhunter/config.toml _adb_push config.toml.example /tmp/config.toml
_adb_push rayhunter-daemon /data/rayhunter/ _at_syscmd "mv /tmp/config.toml /data/rayhunter"
_adb_push rayhunter-daemon /tmp/rayhunter-daemon
_at_syscmd "mv /tmp/rayhunter-daemon /data/rayhunter"
_adb_push scripts/rayhunter_daemon /tmp/rayhunter_daemon _adb_push scripts/rayhunter_daemon /tmp/rayhunter_daemon
_at_syscmd "mv /tmp/rayhunter_daemon /etc/init.d/rayhunter_daemon"
_adb_push scripts/misc-daemon /tmp/misc-daemon _adb_push scripts/misc-daemon /tmp/misc-daemon
_at_syscmd "cp /tmp/rayhunter_daemon /etc/init.d/rayhunter_daemon" _at_syscmd "mv /tmp/misc-daemon /etc/init.d/misc-daemon"
_at_syscmd "cp /tmp/misc-daemon /etc/init.d/misc-daemon"
_at_syscmd "chmod 755 /etc/init.d/rayhunter_daemon" _at_syscmd "chmod 755 /etc/init.d/rayhunter_daemon"
_at_syscmd "chmod 755 /etc/init.d/misc-daemon" _at_syscmd "chmod 755 /etc/init.d/misc-daemon"
echo -n "waiting for reboot..." echo -n "waiting for reboot..."
_at_syscmd reboot _at_syscmd "shutdown -r -t 1 now"
# first wait for shutdown (it can take ~10s) # first wait for shutdown (it can take ~10s)
until ! _adb_shell true 2> /dev/null until ! _adb_shell true 2> /dev/null
+7 -1
View File
@@ -1,6 +1,13 @@
#!/bin/env bash #!/bin/env bash
set -e set -e
export SERIAL_PATH="./serial-ubuntu-latest/serial"
if [ ! -x "$SERIAL_PATH" ]; then
echo "The serial binary cannot be found at $SERIAL_PATH. If you are running this from the git tree please instead run it from the latest release bundle https://github.com/EFForg/rayhunter/releases"
exit 1
fi
if ! command -v adb &> /dev/null; then if ! command -v adb &> /dev/null; then
if [ ! -d ./platform-tools ] ; then if [ ! -d ./platform-tools ] ; then
echo "adb not found, downloading local copy" echo "adb not found, downloading local copy"
@@ -12,6 +19,5 @@ else
export ADB=`which adb` export ADB=`which adb`
fi fi
export SERIAL_PATH="./serial-ubuntu-latest/serial"
. "$(dirname "$0")"/install-common.sh . "$(dirname "$0")"/install-common.sh
install install
+7 -2
View File
@@ -1,6 +1,12 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -e set -e
export SERIAL_PATH="./serial-macos-latest/serial"
if [ ! -x "$SERIAL_PATH" ]; then
echo "The serial binary cannot be found at $SERIAL_PATH. If you are running this from the git tree please instead run it from the latest release bundle at https://github.com/EFForg/rayhunter/releases"
exit 1
fi
if ! command -v adb &> /dev/null; then if ! command -v adb &> /dev/null; then
if [ ! -d ./platform-tools ]; then if [ ! -d ./platform-tools ]; then
echo "adb not found, downloading local copy" echo "adb not found, downloading local copy"
@@ -12,6 +18,5 @@ else
export ADB=`which adb` export ADB=`which adb`
fi fi
export SERIAL_PATH="./serial-macos-latest/serial"
. "$(dirname "$0")"/install-common.sh . "$(dirname "$0")"/install-common.sh
install install
+10 -3
View File
@@ -4,7 +4,13 @@ use serde::Serialize;
use crate::{diag::MessagesContainer, gsmtap_parser}; use crate::{diag::MessagesContainer, gsmtap_parser};
use super::{imsi_provided::ImsiProvidedAnalyzer, information_element::InformationElement, lte_downgrade::LteSib6And7DowngradeAnalyzer, null_cipher::NullCipherAnalyzer}; use super::{
imsi_requested::ImsiRequestedAnalyzer,
information_element::InformationElement,
connection_redirect_downgrade::ConnectionRedirect2GDowngradeAnalyzer,
priority_2g_downgrade::LteSib6And7DowngradeAnalyzer,
null_cipher::NullCipherAnalyzer,
};
/// Qualitative measure of how severe a Warning event type is. /// Qualitative measure of how severe a Warning event type is.
/// The levels should break down like this: /// The levels should break down like this:
@@ -18,7 +24,7 @@ pub enum Severity {
High, High,
} }
/// [QualitativeWarning] events will always be shown to the user in some manner, /// `QualitativeWarning` events will always be shown to the user in some manner,
/// while `Informational` ones may be hidden based on user settings. /// while `Informational` ones may be hidden based on user settings.
#[derive(Serialize, Debug, Clone)] #[derive(Serialize, Debug, Clone)]
#[serde(tag = "type")] #[serde(tag = "type")]
@@ -112,8 +118,9 @@ impl Harness {
pub fn new_with_all_analyzers() -> Self { pub fn new_with_all_analyzers() -> Self {
let mut harness = Harness::new(); let mut harness = Harness::new();
harness.add_analyzer(Box::new(ImsiRequestedAnalyzer::new()));
harness.add_analyzer(Box::new(ConnectionRedirect2GDowngradeAnalyzer{}));
harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer{})); harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer{}));
harness.add_analyzer(Box::new(ImsiProvidedAnalyzer{}));
harness.add_analyzer(Box::new(NullCipherAnalyzer{})); harness.add_analyzer(Box::new(NullCipherAnalyzer{}));
harness harness
@@ -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<str> {
Cow::from("Connection Release/Redirected Carrier 2G Downgrade")
}
fn get_description(&self) -> Cow<str> {
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<Event> {
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),
}),
}
}
}
+59
View File
@@ -0,0 +1,59 @@
use std::borrow::Cow;
use super::analyzer::{Analyzer, Event, EventType, Severity};
use super::information_element::{InformationElement, LteInformationElement};
const PACKET_THRESHHOLD: usize = 150;
pub struct ImsiRequestedAnalyzer {
packet_num: usize,
}
impl ImsiRequestedAnalyzer {
pub fn new() -> Self {
Self { packet_num: 0 }
}
}
impl Analyzer for ImsiRequestedAnalyzer {
fn get_name(&self) -> Cow<str> {
Cow::from("IMSI Requested")
}
fn get_description(&self) -> Cow<str> {
Cow::from("Tests whether the ME sends an IMSI Identity Request NAS message")
}
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> {
self.packet_num += 1;
let InformationElement::LTE(LteInformationElement::NAS(payload)) = ie else {
return None;
};
// NAS identity request, ID type IMSI
if payload == &[0x07, 0x55, 0x01] {
if self.packet_num < PACKET_THRESHHOLD {
return Some(Event {
event_type: EventType::QualitativeWarning {
severity: Severity::Medium
},
message: format!(
"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.",
PACKET_THRESHHOLD
)
})
} else {
return Some(Event {
event_type: EventType::QualitativeWarning {
severity: Severity::High
},
message: format!("NAS IMSI identity request detected"),
})
}
}
None
}
}
+7 -1
View File
@@ -5,7 +5,7 @@
use telcom_parser::{decode, lte_rrc}; use telcom_parser::{decode, lte_rrc};
use thiserror::Error; use thiserror::Error;
use crate::gsmtap::{GsmtapType, LteRrcSubtype, GsmtapMessage}; use crate::gsmtap::{GsmtapMessage, GsmtapType, LteNasSubtype, LteRrcSubtype};
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum InformationElementError { pub enum InformationElementError {
@@ -40,6 +40,9 @@ pub enum LteInformationElement {
SbcchSlBch(lte_rrc::SBCCH_SL_BCH_Message), SbcchSlBch(lte_rrc::SBCCH_SL_BCH_Message),
SbcchSlBchV2x(lte_rrc::SBCCH_SL_BCH_Message_V2X_r14), SbcchSlBchV2x(lte_rrc::SBCCH_SL_BCH_Message_V2X_r14),
// FIXME: actually parse NAS messages
NAS(Vec<u8>),
// FIXME: unclear which message these "NB" types map to // FIXME: unclear which message these "NB" types map to
//DlCcchNb(), //DlCcchNb(),
//DlDcchNb(), //DlDcchNb(),
@@ -79,6 +82,9 @@ impl TryFrom<&GsmtapMessage> for InformationElement {
}; };
Ok(InformationElement::LTE(lte)) Ok(InformationElement::LTE(lte))
}, },
GsmtapType::LteNas(LteNasSubtype::Plain) => {
Ok(InformationElement::LTE(LteInformationElement::NAS(gsmtap_msg.payload.clone())))
},
_ => Err(InformationElementError::UnsupportedGsmtapType(gsmtap_msg.header.gsmtap_type)), _ => Err(InformationElementError::UnsupportedGsmtapType(gsmtap_msg.header.gsmtap_type)),
} }
} }
+4 -1
View File
@@ -1,5 +1,8 @@
pub mod analyzer; pub mod analyzer;
pub mod information_element; 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_provided;
pub mod imsi_requested;
pub mod null_cipher; pub mod null_cipher;
pub mod util;
+32
View File
@@ -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<Baz> {
// 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;
+15
View File
@@ -183,6 +183,8 @@ pub enum LogBody {
// * 0xb0ed: plain EMM NAS message (outgoing) // * 0xb0ed: plain EMM NAS message (outgoing)
#[deku(id_pat = "0xb0e2 | 0xb0e3 | 0xb0ec | 0xb0ed")] #[deku(id_pat = "0xb0e2 | 0xb0e3 | 0xb0ec | 0xb0ed")]
Nas4GMessage { Nas4GMessage {
#[deku(ctx = "log_type")]
direction: Nas4GMessageDirection,
ext_header_version: u8, ext_header_version: u8,
rrc_rel: u8, rrc_rel: u8,
rrc_version_minor: u8, rrc_version_minor: u8,
@@ -211,6 +213,19 @@ pub enum LogBody {
} }
} }
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
#[deku(ctx = "log_type: u16", id = "log_type")]
pub enum Nas4GMessageDirection {
// * 0xb0e2: plain ESM NAS message (incoming)
// * 0xb0e3: plain ESM NAS message (outgoing)
// * 0xb0ec: plain EMM NAS message (incoming)
// * 0xb0ed: plain EMM NAS message (outgoing)
#[deku(id_pat = "0xb0e2 | 0xb0ec")]
Downlink,
#[deku(id_pat = "0xb0e3 | 0xb0ed")]
Uplink,
}
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)] #[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
#[deku(ctx = "ext_header_version: u8", id = "ext_header_version")] #[deku(ctx = "ext_header_version: u8", id = "ext_header_version")]
pub enum LteRrcOtaPacket { pub enum LteRrcOtaPacket {
+7
View File
@@ -200,6 +200,11 @@ pub struct GsmtapHeader {
#[deku(update = "self.gsmtap_type.get_type()")] #[deku(update = "self.gsmtap_type.get_type()")]
pub packet_type: u8, pub packet_type: u8,
pub timeslot: u8, pub timeslot: u8,
#[deku(bits = 1)]
pub pcs_band_indicator: bool,
#[deku(bits = 1)]
pub uplink: bool,
#[deku(bits = 14)]
pub arfcn: u16, pub arfcn: u16,
pub signal_dbm: i8, pub signal_dbm: i8,
pub signal_noise_ratio_db: u8, pub signal_noise_ratio_db: u8,
@@ -222,6 +227,8 @@ impl GsmtapHeader {
header_len: 4, header_len: 4,
packet_type: gsmtap_type.get_type(), packet_type: gsmtap_type.get_type(),
timeslot: 0, timeslot: 0,
pcs_band_indicator: false,
uplink: false,
arfcn: 0, arfcn: 0,
signal_dbm: 0, signal_dbm: 0,
signal_noise_ratio_db: 0, signal_noise_ratio_db: 0,
+3 -3
View File
@@ -99,7 +99,6 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
_ => return Err(GsmtapParserError::InvalidLteRrcOtaExtHeaderVersion(ext_header_version)), _ => return Err(GsmtapParserError::InvalidLteRrcOtaExtHeaderVersion(ext_header_version)),
}; };
let mut header = GsmtapHeader::new(gsmtap_type); let mut header = GsmtapHeader::new(gsmtap_type);
// Wireshark GSMTAP only accepts 14 bits of ARFCN
header.arfcn = packet.get_earfcn().try_into().unwrap_or(0); header.arfcn = packet.get_earfcn().try_into().unwrap_or(0);
header.frame_number = packet.get_sfn(); header.frame_number = packet.get_sfn();
header.subslot = packet.get_subfn(); header.subslot = packet.get_subfn();
@@ -108,9 +107,10 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
payload: packet.take_payload(), payload: packet.take_payload(),
})) }))
}, },
LogBody::Nas4GMessage { msg, .. } => { LogBody::Nas4GMessage { msg, direction, .. } => {
// currently we only handle "plain" (i.e. non-secure) NAS messages // currently we only handle "plain" (i.e. non-secure) NAS messages
let header = GsmtapHeader::new(GsmtapType::LteNas(LteNasSubtype::Plain)); let mut header = GsmtapHeader::new(GsmtapType::LteNas(LteNasSubtype::Plain));
header.uplink = matches!(direction, Nas4GMessageDirection::Uplink);
Ok(Some(GsmtapMessage { Ok(Some(GsmtapMessage {
header, header,
payload: msg, payload: msg,
+1 -1
View File
@@ -60,7 +60,7 @@ impl<T> QmdlReader<T> where T: AsyncRead + Unpin {
}) })
} }
async fn get_next_messages_container(&mut self) -> Result<Option<MessagesContainer>, std::io::Error> { pub async fn get_next_messages_container(&mut self) -> Result<Option<MessagesContainer>, std::io::Error> {
if let Some(max_bytes) = self.max_bytes { if let Some(max_bytes) = self.max_bytes {
if self.bytes_read >= max_bytes { if self.bytes_read >= max_bytes {
if self.bytes_read > max_bytes { if self.bytes_read > max_bytes {
+2 -1
View File
@@ -1,4 +1,5 @@
#!/bin/sh #!/bin/sh
cargo build --release --target="armv7-unknown-linux-gnueabihf" #--features debug cargo build --release --target="armv7-unknown-linux-gnueabihf" #--features debug
adb shell '/bin/rootshell -c "/etc/init.d/rayhunter_daemon stop"'
adb push target/armv7-unknown-linux-gnueabihf/release/rayhunter-daemon /data/rayhunter/rayhunter-daemon adb push target/armv7-unknown-linux-gnueabihf/release/rayhunter-daemon /data/rayhunter/rayhunter-daemon
adb shell '/bin/rootshell -c "/etc/init.d/rayhunter_daemon restart"' adb shell '/bin/rootshell -c "/etc/init.d/rayhunter_daemon start"'
+4 -1
View File
@@ -78,7 +78,10 @@ fn send_command<T: UsbContext>(handle: &mut DeviceHandle<T>, command: &str) {
.read_bulk(0x82, &mut response, timeout) .read_bulk(0x82, &mut response, timeout)
.expect("Failed to read response"); .expect("Failed to read response");
let responsestr = str::from_utf8(&response).expect("Failed to parse response"); // For some reason, on macOS the response buffer gets filled with garbage data that's
// rarely valid UTF-8. Luckily we only care about the first couple bytes, so just drop
// the garbage with `from_utf8_lossy` and look for our expected success string.
let responsestr = String::from_utf8_lossy(&response);
if !responsestr.contains("\r\nOK\r\n") { if !responsestr.contains("\r\nOK\r\n") {
println!("Received unexpected response{0}", responsestr); println!("Received unexpected response{0}", responsestr);
std::process::exit(1); std::process::exit(1);
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/python3
import pycrate_mobile
from pycrate_mobile import NASLTE
import pycrate_core
import binascii
import sys
import pprint
from enum import Enum
import pycrate_mobile.TS24301_EMM
EPS_IMSI_ATTACH = 2
def parse_nas_message(buffer, uplink=None):
if isinstance(buffer, str): #handle string argument or raw bytes
bin = binascii.unhexlify(buffer)
else:
bin = buffer
if uplink:
parsed = NASLTE.parse_NASLTE_MO(bin)
elif uplink == None: #We don't know if its an up or downlink
parsed = NASLTE.parse_NASLTE_MO(bin)
if parsed[0] == None:
parsed = NASLTE.parse_NASLTE_MT(bin)
else:
parsed = NASLTE.parse_NASLTE_MT(bin)
if parsed[0] is None: # Not a NAS Packet
raise TypeError("Not a nas packet")
return parsed[0]
def heur_ue_imsi_sent(msg):
output = "device transmitted IMSI to base station!"
if type(msg) not in [pycrate_mobile.TS24301_EMM.EMMAttachRequest, pycrate_mobile.TS24301_EMM.EMMSecProtNASMessage]:
return (False, None)
if isinstance(msg, pycrate_mobile.TS24301_EMM.EMMSecProtNASMessage):
try:
msg = msg['EMMAttachRequest']
except pycrate_core.elt.EltErr:
return (False, None)
if msg['EPSAttachType']['V'].to_int() == EPS_IMSI_ATTACH: #EPSAttachType Value is 'Combined EPS/IMSI Attach (2)'
return (True, output)
return (False, None)
if __name__ == "__main__":
if len(sys.argv) != 2:
print("usage: nasparse.py [hex encoded nas message]")
exit(1)
buffer = sys.argv[1]
msg = parse_nas_message(buffer)
pprint.pprint(msg)
triggered, message = heur_ue_imsi_sent(msg)
if triggered:
print(message)
exit(1)
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/python3
import unittest
import nasparse
class TestNasparse(unittest.TestCase):
imsi_sent_msg = '07412208391185184409309005f0700000100030023ed031d127298080211001000010810600000000830600000000000d00000300ff0003130184000a000005000010005c0a009011034f18a6f15d0103c1000000000000'
sec_imsi_sent_msg = '1727db4b7c0207412208391185184409309005f0700000100030023ed031d127298080211001000010810600000000830600000000000d00000300ff0003130184000a000005000010005c0a009011034f18a6f15d0103c1'
non_nas_msg = 'deadbeefcafe'
other_nas_msg = '074413780004023fd121'
other_nas_mt_msg = "023fd12100000000000000000000000000000000000000000000000000000000"
ciphered_nas_msg = "27ed6146bd0162a5d62d62e1ce501720dc8bd84f1167fd"
def run_heur(self, msg):
buf = nasparse.parse_nas_message(msg)
return nasparse.heur_ue_imsi_sent(buf)[0]
def test_imsi_sent(self):
self.assertEqual(self.run_heur(self.imsi_sent_msg), True, "imsi_sent_msg should trigger heuristic")
def test_sec_imsi_sent(self):
self.assertEqual(self.run_heur(self.imsi_sent_msg), True, "sec_imsi_sent_msg should trigger heuristic")
def test_non_nas_msg(self):
with self.assertRaises(TypeError):
self.run_heur(self.non_nas_msg)
def test_other_nas(self):
self.assertEqual(self.run_heur(self.other_nas_msg), False, "other_nas_msg should not trigger heuristic")
def test_other_nas_mt(self):
self.assertEqual(self.run_heur(self.other_nas_mt_msg), False, "other_nas_mt_msg should not trigger heuristic")
def test_ciphered_nas(self):
self.assertEqual(self.run_heur(self.ciphered_nas_msg), False, "ciphered_nas_msg should not trigger heuristic")
if __name__ == '__main__':
unittest.main()
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/python3
import nasparse
from scapy.utils import RawPcapNgReader
import sys
TYPE_LTE_NAS = 0x12
UDP_LEN = 28
def process_pcap(pcap_path):
print('Opening {}...'.format(pcap_path))
count = 0
for pkt_data, pkt_metadata in RawPcapNgReader(pcap_path):
count += 1
gsmtap_len = pkt_data[UDP_LEN+1] * 4 # gsmtap header length is stored in the 2nd byte of GSMTAP as a number of 32 bit words
header_end = gsmtap_len + UDP_LEN #length of UDP/IP header plus GSMTAP header
gsmtap_hdr = pkt_data[UDP_LEN:header_end]
if gsmtap_hdr[2] != TYPE_LTE_NAS:
continue
# uplink status is the 7th bit of the 5th byte of the GSMTAP header.
# Uplink (Mobile originated) = 0 Downlink (mobile terminated) = 1
uplink = (gsmtap_hdr[4] & 0b01000000) >> 6
buffer = pkt_data[header_end:]
msg = nasparse.parse_nas_message(buffer, uplink)
triggered, message = nasparse.heur_ue_imsi_sent(msg)
if triggered:
print(f"Frame {count} triggered heuristic: {message}")
if __name__ == "__main__":
if len(sys.argv) != 2:
print("usage: pcap_check.py [path/to/pcap/file]")
exit(1)
pcap_path = sys.argv[1]
process_pcap(pcap_path)