mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-31 02:03:35 -07:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ee83613757 | |||
| 840f8ad8b0 | |||
| c9ac834ca7 | |||
| 8629aacf6b | |||
| a3fd1479f9 | |||
| 049c563f02 | |||
| a33b5a3418 | |||
| 107ba58296 | |||
| d016279172 | |||
| 5a084f1abb | |||
| 3619df32ab | |||
| 34d87d1fd7 | |||
| da4952e70f | |||
| 30323b8329 | |||
| 28b0f409db | |||
| 12640cc878 | |||
| 26eda5904f | |||
| 3e26e61b05 | |||
| 565c0f1e67 | |||
| 6bd36921d8 | |||
| c83ae30be8 | |||
| fa612241a5 | |||
| 10592bbd9d | |||
| 327eaddcd7 | |||
| 32149c3b37 | |||
| e47d4dacc4 | |||
| 4009e3d1ed | |||
| b2cd735a07 | |||
| 94e9a88a91 | |||
| f4a6c834d2 | |||
| 95e8f846d3 | |||
| 15f128add1 | |||
| 87f9cc403b | |||
| 7addf3a67f | |||
| 4d8cc9b738 | |||
| b0d797d206 | |||
| 1ae3b5020b |
Generated
+86
@@ -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"
|
||||
|
||||
@@ -1,46 +1,30 @@
|
||||

|
||||
# Rayhunter
|
||||
|
||||
```
|
||||
@@@@@@@ @@@@@@ @@@ @@@ @@@ @@@ @@@ @@@ @@@ @@@ @@@@@@@ @@@@@@@@ @@@@@@@
|
||||
@@! @@@ @@! @@@ @@! !@@ @@! @@@ @@! @@@ @@!@!@@@ @@! @@! @@! @@@
|
||||
@!@!!@! @!@!@!@! !@!@! @!@!@!@! @!@ !@! @!@@!!@! @!! @!!!:! @!@!!@!
|
||||
!!: :!! !!: !!! !!: !!: !!! !!: !!! !!: !!! !!: !!: !!: :!!
|
||||
: : : : : : .: : : : :.:: : :: : : : :: ::: : : :
|
||||
|
||||
|
||||
_ _ _ _ _ _ _ _
|
||||
)`'-.,_)`'-.,_)`'-.,_)`'-.,_)`'-.,_)`'-.,_)`'-.,_)`'-.,_
|
||||
|
||||
O .
|
||||
O ' '
|
||||
o ' .
|
||||
o .'
|
||||
__________.-' '...___
|
||||
.-' ### '''...__
|
||||
/ a### ## ''--.._ ______
|
||||
'. # ######## ' .-'
|
||||
'-._ ..**********#### ___...---'''\ '
|
||||
'-._ __________...---''' \ l
|
||||
\ | apc '._|
|
||||
\__;
|
||||
```
|
||||

|
||||
|
||||
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**
|
||||
|
||||
|
||||
## The Hardware
|
||||
|
||||
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
|
||||
|
||||
*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.
|
||||
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.
|
||||
|
||||
|
||||
## 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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
+76
-13
@@ -1,5 +1,6 @@
|
||||
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 clap::Parser;
|
||||
use futures::TryStreamExt;
|
||||
@@ -9,14 +10,20 @@ mod dummy_analyzer;
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about)]
|
||||
struct Args {
|
||||
#[arg(short, long)]
|
||||
#[arg(short = 'p', long)]
|
||||
qmdl_path: PathBuf,
|
||||
|
||||
#[arg(short = 'c', long)]
|
||||
pcapify: bool,
|
||||
|
||||
#[arg(long)]
|
||||
show_skipped: bool,
|
||||
|
||||
#[arg(long)]
|
||||
enable_dummy_analyzer: bool,
|
||||
|
||||
#[arg(short, long)]
|
||||
verbose: 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 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) {
|
||||
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]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
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();
|
||||
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");
|
||||
@@ -75,10 +129,19 @@ async fn main() {
|
||||
let name = entry.file_name();
|
||||
let name_str = name.to_str().unwrap();
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ struct ConfigFile {
|
||||
debug_mode: Option<bool>,
|
||||
ui_level: Option<u8>,
|
||||
enable_dummy_analyzer: Option<bool>,
|
||||
colorblind_mode: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -18,6 +19,7 @@ pub struct Config {
|
||||
pub debug_mode: bool,
|
||||
pub ui_level: u8,
|
||||
pub enable_dummy_analyzer: bool,
|
||||
pub colorblind_mode: bool,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
@@ -28,6 +30,7 @@ impl Default for Config {
|
||||
debug_mode: false,
|
||||
ui_level: 1,
|
||||
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.ui_level.map(|v| config.ui_level = 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)
|
||||
}
|
||||
|
||||
+7
-1
@@ -59,6 +59,7 @@ async fn run_server(
|
||||
debug_mode: config.debug_mode,
|
||||
analysis_status_lock,
|
||||
analysis_sender,
|
||||
colorblind_mode: config.colorblind_mode,
|
||||
});
|
||||
|
||||
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<()> {
|
||||
static IMAGE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/static/images/");
|
||||
let mut display_color: framebuffer::Color565;
|
||||
let display_level = config.ui_level;
|
||||
if display_level == 0 {
|
||||
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 || {
|
||||
let mut fb: Framebuffer = Framebuffer::new();
|
||||
|
||||
+9
-1
@@ -129,8 +129,16 @@ pub async fn start_recording(State(state): State<Arc<ServerState>>) -> Result<(S
|
||||
let qmdl_writer = QmdlWriter::new(qmdl_file);
|
||||
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)))?;
|
||||
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)))?;
|
||||
|
||||
Ok((StatusCode::ACCEPTED, "ok".to_string()))
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ pub enum DisplayState {
|
||||
Recording,
|
||||
Paused,
|
||||
WarningDetected,
|
||||
RecordingCBM,
|
||||
}
|
||||
|
||||
impl From<DisplayState> for Color565 {
|
||||
@@ -34,6 +35,7 @@ impl From<DisplayState> for Color565 {
|
||||
match state {
|
||||
DisplayState::Paused => Color565::White,
|
||||
DisplayState::Recording => Color565::Green,
|
||||
DisplayState::RecordingCBM => Color565::Blue,
|
||||
DisplayState::WarningDetected => Color565::Red,
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -22,13 +22,15 @@ pub struct ServerState {
|
||||
pub ui_update_sender: Sender<framebuffer::DisplayState>,
|
||||
pub analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
|
||||
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)> {
|
||||
let qmdl_idx = qmdl_name.trim_end_matches(".qmdl");
|
||||
let qmdl_store = state.qmdl_store_lock.read().await;
|
||||
let (entry_index, entry) = qmdl_store.entry_for_name(&qmdl_name)
|
||||
.ok_or((StatusCode::NOT_FOUND, format!("couldn't find qmdl file with 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_idx)))?;
|
||||
let qmdl_file = qmdl_store.open_entry_qmdl(entry_index).await
|
||||
.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);
|
||||
|
||||
@@ -80,6 +80,11 @@ async function updateEntryAnalysisResult(entry) {
|
||||
entry.analysis_result = `0 warnings!`;
|
||||
} else {
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
const analysisResult = document.createElement('td');
|
||||
analysisResult.innerText = entry.analysis_result;
|
||||
analysisResult.innerHTML = entry.analysis_result;
|
||||
if (entry.analysis.warnings.length > 0) {
|
||||
row.classList.add("warning");
|
||||
}
|
||||
|
||||
Vendored
+3
@@ -1,6 +1,9 @@
|
||||
# cat config.toml
|
||||
qmdl_store_path = "/data/rayhunter/qmdl"
|
||||
port = 8080
|
||||
debug_mode = false
|
||||
enable_dummy_analyzer = false
|
||||
colorblind_mode = false
|
||||
# UI Levels:
|
||||
# 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
|
||||
|
||||
Vendored
+9
-5
@@ -65,16 +65,20 @@ _at_syscmd() {
|
||||
|
||||
setup_rayhunter() {
|
||||
_at_syscmd "mkdir -p /data/rayhunter"
|
||||
_adb_push config.toml.example /data/rayhunter/config.toml
|
||||
_adb_push rayhunter-daemon /data/rayhunter/
|
||||
_adb_push config.toml.example /tmp/config.toml
|
||||
_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
|
||||
_at_syscmd "mv /tmp/rayhunter_daemon /etc/init.d/rayhunter_daemon"
|
||||
_adb_push scripts/misc-daemon /tmp/misc-daemon
|
||||
_at_syscmd "cp /tmp/rayhunter_daemon /etc/init.d/rayhunter_daemon"
|
||||
_at_syscmd "cp /tmp/misc-daemon /etc/init.d/misc-daemon"
|
||||
_at_syscmd "mv /tmp/misc-daemon /etc/init.d/misc-daemon"
|
||||
|
||||
_at_syscmd "chmod 755 /etc/init.d/rayhunter_daemon"
|
||||
_at_syscmd "chmod 755 /etc/init.d/misc-daemon"
|
||||
|
||||
echo -n "waiting for reboot..."
|
||||
_at_syscmd reboot
|
||||
_at_syscmd "shutdown -r -t 1 now"
|
||||
|
||||
# first wait for shutdown (it can take ~10s)
|
||||
until ! _adb_shell true 2> /dev/null
|
||||
|
||||
Vendored
+7
-1
@@ -1,6 +1,13 @@
|
||||
#!/bin/env bash
|
||||
|
||||
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 [ ! -d ./platform-tools ] ; then
|
||||
echo "adb not found, downloading local copy"
|
||||
@@ -12,6 +19,5 @@ else
|
||||
export ADB=`which adb`
|
||||
fi
|
||||
|
||||
export SERIAL_PATH="./serial-ubuntu-latest/serial"
|
||||
. "$(dirname "$0")"/install-common.sh
|
||||
install
|
||||
|
||||
Vendored
+7
-2
@@ -1,6 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
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 [ ! -d ./platform-tools ]; then
|
||||
echo "adb not found, downloading local copy"
|
||||
@@ -12,6 +18,5 @@ else
|
||||
export ADB=`which adb`
|
||||
fi
|
||||
|
||||
export SERIAL_PATH="./serial-macos-latest/serial"
|
||||
. "$(dirname "$0")"/install-common.sh
|
||||
install
|
||||
|
||||
@@ -4,7 +4,13 @@ use serde::Serialize;
|
||||
|
||||
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.
|
||||
/// The levels should break down like this:
|
||||
@@ -18,7 +24,7 @@ pub enum Severity {
|
||||
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.
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
@@ -112,8 +118,9 @@ impl Harness {
|
||||
|
||||
pub fn new_with_all_analyzers() -> 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{}));
|
||||
harness.add_analyzer(Box::new(ImsiProvidedAnalyzer{}));
|
||||
harness.add_analyzer(Box::new(NullCipherAnalyzer{}));
|
||||
|
||||
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),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
use telcom_parser::{decode, lte_rrc};
|
||||
use thiserror::Error;
|
||||
use crate::gsmtap::{GsmtapType, LteRrcSubtype, GsmtapMessage};
|
||||
use crate::gsmtap::{GsmtapMessage, GsmtapType, LteNasSubtype, LteRrcSubtype};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum InformationElementError {
|
||||
@@ -40,6 +40,9 @@ pub enum LteInformationElement {
|
||||
SbcchSlBch(lte_rrc::SBCCH_SL_BCH_Message),
|
||||
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
|
||||
//DlCcchNb(),
|
||||
//DlDcchNb(),
|
||||
@@ -79,6 +82,9 @@ impl TryFrom<&GsmtapMessage> for InformationElement {
|
||||
};
|
||||
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)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +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;
|
||||
|
||||
@@ -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;
|
||||
@@ -183,6 +183,8 @@ pub enum LogBody {
|
||||
// * 0xb0ed: plain EMM NAS message (outgoing)
|
||||
#[deku(id_pat = "0xb0e2 | 0xb0e3 | 0xb0ec | 0xb0ed")]
|
||||
Nas4GMessage {
|
||||
#[deku(ctx = "log_type")]
|
||||
direction: Nas4GMessageDirection,
|
||||
ext_header_version: u8,
|
||||
rrc_rel: 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)]
|
||||
#[deku(ctx = "ext_header_version: u8", id = "ext_header_version")]
|
||||
pub enum LteRrcOtaPacket {
|
||||
|
||||
+8
-1
@@ -17,7 +17,7 @@ pub enum GsmtapType {
|
||||
UmtsRlcMac,
|
||||
UmtsRrc(UmtsRrcSubtype),
|
||||
LteRrc(LteRrcSubtype), /* LTE interface */
|
||||
LteMac, /* LTE MAC interface */
|
||||
LteMac, /* LTE MAC interface */
|
||||
LteMacFramed, /* LTE MAC with context hdr */
|
||||
OsmocoreLog, /* libosmocore logging */
|
||||
QcDiag, /* Qualcomm DIAG frame */
|
||||
@@ -200,6 +200,11 @@ pub struct GsmtapHeader {
|
||||
#[deku(update = "self.gsmtap_type.get_type()")]
|
||||
pub packet_type: 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 signal_dbm: i8,
|
||||
pub signal_noise_ratio_db: u8,
|
||||
@@ -222,6 +227,8 @@ impl GsmtapHeader {
|
||||
header_len: 4,
|
||||
packet_type: gsmtap_type.get_type(),
|
||||
timeslot: 0,
|
||||
pcs_band_indicator: false,
|
||||
uplink: false,
|
||||
arfcn: 0,
|
||||
signal_dbm: 0,
|
||||
signal_noise_ratio_db: 0,
|
||||
|
||||
@@ -99,7 +99,6 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
|
||||
_ => return Err(GsmtapParserError::InvalidLteRrcOtaExtHeaderVersion(ext_header_version)),
|
||||
};
|
||||
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.frame_number = packet.get_sfn();
|
||||
header.subslot = packet.get_subfn();
|
||||
@@ -108,9 +107,10 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
|
||||
payload: packet.take_payload(),
|
||||
}))
|
||||
},
|
||||
LogBody::Nas4GMessage { msg, .. } => {
|
||||
LogBody::Nas4GMessage { msg, direction, .. } => {
|
||||
// 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 {
|
||||
header,
|
||||
payload: msg,
|
||||
|
||||
+1
-1
@@ -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 self.bytes_read >= max_bytes {
|
||||
if self.bytes_read > max_bytes {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/bin/sh
|
||||
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 shell '/bin/rootshell -c "/etc/init.d/rayhunter_daemon restart"'
|
||||
adb shell '/bin/rootshell -c "/etc/init.d/rayhunter_daemon start"'
|
||||
|
||||
+4
-1
@@ -78,7 +78,10 @@ fn send_command<T: UsbContext>(handle: &mut DeviceHandle<T>, command: &str) {
|
||||
.read_bulk(0x82, &mut response, timeout)
|
||||
.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") {
|
||||
println!("Received unexpected response{0}", responsestr);
|
||||
std::process::exit(1);
|
||||
|
||||
Executable
+60
@@ -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)
|
||||
@@ -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()
|
||||
Executable
+38
@@ -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)
|
||||
Reference in New Issue
Block a user