mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-31 10:13:35 -07:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6bd36921d8 | |||
| c83ae30be8 | |||
| fa612241a5 | |||
| 10592bbd9d | |||
| 327eaddcd7 | |||
| 32149c3b37 | |||
| e47d4dacc4 | |||
| 4009e3d1ed | |||
| b2cd735a07 | |||
| 94e9a88a91 | |||
| f4a6c834d2 | |||
| 95e8f846d3 | |||
| 15f128add1 | |||
| 87f9cc403b |
+36
-3
@@ -1,5 +1,5 @@
|
|||||||
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 rayhunter::{analysis::analyzer::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;
|
||||||
@@ -12,6 +12,9 @@ struct Args {
|
|||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
qmdl_path: PathBuf,
|
qmdl_path: PathBuf,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
pcapify: bool,
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
show_skipped: bool,
|
show_skipped: bool,
|
||||||
|
|
||||||
@@ -54,6 +57,27 @@ async fn analyze_file(harness: &mut Harness, qmdl_path: &str, show_skipped: bool
|
|||||||
println!("{}: {} messages analyzed, {} warnings, {} messages skipped", qmdl_path, total_messages, warnings, skipped);
|
println!("{}: {} 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("wrote pcap to {:?}", &pcap_path);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
@@ -75,10 +99,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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -22,7 +22,8 @@ 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)> {
|
||||||
|
|||||||
Vendored
+3
@@ -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
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ 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,
|
||||||
|
lte_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 +23,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")]
|
||||||
@@ -113,7 +118,7 @@ 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(LteSib6And7DowngradeAnalyzer{}));
|
harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer{}));
|
||||||
harness.add_analyzer(Box::new(ImsiProvidedAnalyzer{}));
|
harness.add_analyzer(Box::new(ImsiRequestedAnalyzer::new()));
|
||||||
harness.add_analyzer(Box::new(NullCipherAnalyzer{}));
|
harness.add_analyzer(Box::new(NullCipherAnalyzer{}));
|
||||||
|
|
||||||
harness
|
harness
|
||||||
|
|||||||
@@ -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
|
||||||
|
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 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 request detected"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ pub mod analyzer;
|
|||||||
pub mod information_element;
|
pub mod information_element;
|
||||||
pub mod lte_downgrade;
|
pub mod lte_downgrade;
|
||||||
pub mod imsi_provided;
|
pub mod imsi_provided;
|
||||||
|
pub mod imsi_requested;
|
||||||
pub mod null_cipher;
|
pub mod null_cipher;
|
||||||
|
|||||||
@@ -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")]
|
||||||
|
Inbound,
|
||||||
|
#[deku(id_pat = "0xb0e3 | 0xb0ed")]
|
||||||
|
Outbound,
|
||||||
|
}
|
||||||
|
|
||||||
#[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 {
|
||||||
|
|||||||
+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 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 {
|
||||||
|
|||||||
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