mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-06-21 19:51:03 -07:00
check: support pcaps
rayhunter-check will now analyze any PCAP files it finds in addition to QMDL
This commit is contained in:
committed by
Cooper Quintin
parent
3ddbaa07ca
commit
c783831e78
Generated
+24
@@ -1927,6 +1927,28 @@ dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_enum"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a"
|
||||
dependencies = [
|
||||
"num_enum_derive",
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_enum_derive"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_threads"
|
||||
version = "0.1.7"
|
||||
@@ -2356,6 +2378,7 @@ dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"nix",
|
||||
"num_enum",
|
||||
"pcap-file-tokio",
|
||||
"pycrate-rs",
|
||||
"serde",
|
||||
@@ -2371,6 +2394,7 @@ dependencies = [
|
||||
"clap",
|
||||
"futures",
|
||||
"log",
|
||||
"pcap-file-tokio",
|
||||
"rayhunter",
|
||||
"simple_logger",
|
||||
"tokio",
|
||||
|
||||
@@ -8,6 +8,7 @@ rayhunter = { path = "../lib" }
|
||||
futures = { version = "0.3.30", default-features = false }
|
||||
log = "0.4.20"
|
||||
tokio = { version = "1.44.2", default-features = false, features = ["fs", "signal", "process", "rt-multi-thread"] }
|
||||
pcap-file-tokio = "0.1.0"
|
||||
clap = { version = "4.5.2", features = ["derive"] }
|
||||
simple_logger = "5.0.0"
|
||||
walkdir = "2.5.0"
|
||||
|
||||
+98
-50
@@ -1,12 +1,9 @@
|
||||
use clap::Parser;
|
||||
use futures::TryStreamExt;
|
||||
use log::{error, info, warn};
|
||||
use log::{error, info, warn, debug};
|
||||
use pcap_file_tokio::pcapng::{Block, PcapNgReader};
|
||||
use rayhunter::{
|
||||
analysis::analyzer::{AnalyzerConfig, EventType, Harness},
|
||||
diag::DataType,
|
||||
gsmtap_parser,
|
||||
pcap::GsmtapPcapWriter,
|
||||
qmdl::QmdlReader,
|
||||
analysis::analyzer::{AnalysisRow, AnalyzerConfig, EventType, Harness}, diag::DataType, gsmtap_parser, pcap::GsmtapPcapWriter, qmdl::QmdlReader
|
||||
};
|
||||
use std::{collections::HashMap, future, path::PathBuf, pin::pin};
|
||||
use tokio::fs::File;
|
||||
@@ -28,7 +25,90 @@ struct Args {
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
async fn analyze_file(qmdl_path: &str, show_skipped: bool) {
|
||||
#[derive(Default)]
|
||||
struct Report {
|
||||
skipped_reasons: HashMap<String, u32>,
|
||||
total_messages: u32,
|
||||
warnings: u32,
|
||||
skipped: u32,
|
||||
file_path: String,
|
||||
}
|
||||
|
||||
impl Report {
|
||||
fn new(file_path: &str) -> Self {
|
||||
let mut report = Report::default();
|
||||
report.file_path = file_path.to_string();
|
||||
report
|
||||
}
|
||||
|
||||
fn process_row(&mut self, row: AnalysisRow) {
|
||||
self.total_messages += 1;
|
||||
if let Some(reason) = row.skipped_message_reason {
|
||||
*self.skipped_reasons.entry(reason).or_insert(0) += 1;
|
||||
self.skipped += 1;
|
||||
return;
|
||||
}
|
||||
for maybe_event in row.events {
|
||||
let Some(event) = maybe_event else { continue };
|
||||
let Some(timestamp) = row.packet_timestamp else { continue };
|
||||
match event.event_type {
|
||||
EventType::Informational => {
|
||||
info!(
|
||||
"{}: INFO - {} {}",
|
||||
self.file_path, timestamp, event.message,
|
||||
);
|
||||
}
|
||||
EventType::QualitativeWarning { severity } => {
|
||||
warn!(
|
||||
"{}: WARNING (Severity: {:?}) - {} {}",
|
||||
self.file_path, severity, timestamp, event.message,
|
||||
);
|
||||
self.warnings += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_summary(&self, show_skipped: bool) {
|
||||
if show_skipped && self.skipped > 0 {
|
||||
info!("{}: messages skipped:", self.file_path);
|
||||
for (reason, count) in self.skipped_reasons.iter() {
|
||||
info!(" - {count}: \"{reason}\"");
|
||||
}
|
||||
}
|
||||
info!(
|
||||
"{}: {} messages analyzed, {} warnings, {} messages skipped",
|
||||
self.file_path,
|
||||
self.total_messages,
|
||||
self.warnings,
|
||||
self.skipped
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn analyze_pcap(pcap_path: &str, show_skipped: bool) {
|
||||
let mut harness = Harness::new_with_config(&AnalyzerConfig::default());
|
||||
let pcap_file = &mut File::open(&pcap_path)
|
||||
.await
|
||||
.expect("failed to open file");
|
||||
let mut pcap_reader = PcapNgReader::new(pcap_file)
|
||||
.await
|
||||
.expect("failed to read PCAP file");
|
||||
let mut report = Report::new(pcap_path);
|
||||
while let Some(Ok(block)) = pcap_reader.next_block().await {
|
||||
let row = match block {
|
||||
Block::EnhancedPacket(packet) => harness.analyze_pcap_packet(packet),
|
||||
other => {
|
||||
debug!("{pcap_path}: skipping pcap packet {other:?}");
|
||||
continue;
|
||||
},
|
||||
};
|
||||
report.process_row(row);
|
||||
}
|
||||
report.print_summary(show_skipped);
|
||||
}
|
||||
|
||||
async fn analyze_qmdl(qmdl_path: &str, show_skipped: bool) {
|
||||
let mut harness = Harness::new_with_config(&AnalyzerConfig::default());
|
||||
let qmdl_file = &mut File::open(&qmdl_path).await.expect("failed to open file");
|
||||
let file_size = qmdl_file
|
||||
@@ -42,52 +122,17 @@ async fn analyze_file(qmdl_path: &str, show_skipped: bool) {
|
||||
.as_stream()
|
||||
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace))
|
||||
);
|
||||
let mut skipped_reasons: HashMap<String, i32> = HashMap::new();
|
||||
let mut total_messages = 0;
|
||||
let mut warnings = 0;
|
||||
let mut skipped = 0;
|
||||
let mut report = Report::new(qmdl_path);
|
||||
while let Some(container) = qmdl_stream
|
||||
.try_next()
|
||||
.await
|
||||
.expect("failed getting QMDL container")
|
||||
{
|
||||
for row in harness.analyze_qmdl_messages(container) {
|
||||
total_messages += 1;
|
||||
if let Some(reason) = row.skipped_message_reason {
|
||||
*skipped_reasons.entry(reason).or_insert(0) += 1;
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
for maybe_event in row.events {
|
||||
let Some(event) = maybe_event else { continue };
|
||||
let Some(timestamp) = row.packet_timestamp else { continue };
|
||||
match event.event_type {
|
||||
EventType::Informational => {
|
||||
info!(
|
||||
"{}: INFO - {} {}",
|
||||
qmdl_path, timestamp, event.message,
|
||||
);
|
||||
}
|
||||
EventType::QualitativeWarning { severity } => {
|
||||
warn!(
|
||||
"{}: WARNING (Severity: {:?}) - {} {}",
|
||||
qmdl_path, severity, timestamp, event.message,
|
||||
);
|
||||
warnings += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
report.process_row(row);
|
||||
}
|
||||
}
|
||||
if show_skipped && skipped > 0 {
|
||||
info!("{qmdl_path}: messages skipped:");
|
||||
for (reason, count) in skipped_reasons.iter() {
|
||||
info!(" - {count}: \"{reason}\"");
|
||||
}
|
||||
}
|
||||
info!(
|
||||
"{qmdl_path}: {total_messages} messages analyzed, {warnings} warnings, {skipped} messages skipped"
|
||||
);
|
||||
report.print_summary(show_skipped);
|
||||
}
|
||||
|
||||
async fn pcapify(qmdl_path: &PathBuf) {
|
||||
@@ -136,11 +181,11 @@ async fn main() {
|
||||
.with_module_level("asn1_codecs", log::LevelFilter::Error)
|
||||
.init()
|
||||
.unwrap();
|
||||
info!("Analyzers:");
|
||||
|
||||
let harness = Harness::new_with_config(&AnalyzerConfig::default());
|
||||
info!("Analyzers:");
|
||||
for analyzer in harness.get_metadata().analyzers {
|
||||
info!(" - {}: {} (v{})", analyzer.name, analyzer.description, analyzer.version);
|
||||
info!(" - {} (v{}): {}", analyzer.name, analyzer.description, analyzer.version);
|
||||
}
|
||||
|
||||
for maybe_entry in WalkDir::new(&args.path) {
|
||||
@@ -150,15 +195,18 @@ async fn main() {
|
||||
};
|
||||
let name = entry.file_name();
|
||||
let name_str = name.to_str().unwrap();
|
||||
let path = entry.path();
|
||||
let path_str = path.to_str().unwrap();
|
||||
// instead of relying on the QMDL extension, can we check if a file is
|
||||
// QMDL by inspecting the contents?
|
||||
if name_str.ends_with(".qmdl") {
|
||||
let path = entry.path();
|
||||
let path_str = path.to_str().unwrap();
|
||||
analyze_file(path_str, args.show_skipped).await;
|
||||
analyze_qmdl(path_str, args.show_skipped).await;
|
||||
if args.pcapify {
|
||||
pcapify(&path.to_path_buf()).await;
|
||||
}
|
||||
} else if name_str.ends_with(".pcap") {
|
||||
// TODO: if we've already analyzed a QMDL, skip its corresponding pcap
|
||||
analyze_pcap(path_str, args.show_skipped).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -17,7 +17,7 @@ wingtech = []
|
||||
|
||||
[dependencies]
|
||||
bytes = "1.5.0"
|
||||
chrono = "0.4.31"
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
crc = "3.0.1"
|
||||
deku = { version = "0.18.0", features = ["logging"] }
|
||||
libc = "0.2.150"
|
||||
@@ -27,6 +27,7 @@ pcap-file-tokio = "0.1.0"
|
||||
pycrate-rs = { git = "https://github.com/wgreenberg/pycrate-rs" }
|
||||
thiserror = "1.0.50"
|
||||
telcom-parser = { path = "../telcom-parser" }
|
||||
tokio = { version = "1.44.2", default-features = false }
|
||||
tokio = { version = "1.44.2", default-features = false, features = ["time", "rt", "macros"] }
|
||||
futures = { version = "0.3.30", default-features = false }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
num_enum = "0.7.4"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use pcap_file_tokio::pcapng::blocks::enhanced_packet::EnhancedPacketBlock;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::gsmtap::{GsmtapHeader, GsmtapMessage, GsmtapType};
|
||||
use crate::util::RuntimeMetadata;
|
||||
use crate::{diag::MessagesContainer, gsmtap_parser};
|
||||
|
||||
@@ -167,6 +169,39 @@ impl Harness {
|
||||
self.analyzers.push(analyzer);
|
||||
}
|
||||
|
||||
pub fn analyze_pcap_packet(&mut self, packet: EnhancedPacketBlock) -> AnalysisRow {
|
||||
let epoch = DateTime::parse_from_rfc3339("1980-01-06T00:00:00-00:00").unwrap();
|
||||
let mut row = AnalysisRow {
|
||||
packet_timestamp: Some(epoch + packet.timestamp),
|
||||
skipped_message_reason: None,
|
||||
events: Vec::new(),
|
||||
};
|
||||
let gsmtap_offset = 20 + 8;
|
||||
let gsmtap_data = &packet.data[gsmtap_offset..];
|
||||
// the type and subtype are at byte offsets 3 and 13, respectively
|
||||
let gsmtap_header = match GsmtapType::new(gsmtap_data[2], gsmtap_data[12]) {
|
||||
Ok(gsmtap_type) => GsmtapHeader::new(gsmtap_type),
|
||||
Err(err) => {
|
||||
row.skipped_message_reason = Some(format!("failed to read GsmtapHeader: {err:?}"));
|
||||
return row;
|
||||
},
|
||||
};
|
||||
let packet_offset = gsmtap_offset + 16;
|
||||
let packet_data = &packet.data[packet_offset..];
|
||||
let gsmtap_message = GsmtapMessage {
|
||||
header: gsmtap_header,
|
||||
payload: packet_data.to_vec(),
|
||||
};
|
||||
row.events = match InformationElement::try_from(&gsmtap_message) {
|
||||
Ok(element) => self.analyze_information_element(&element),
|
||||
Err(err) => {
|
||||
row.skipped_message_reason = Some(format!("failed to convert gsmtap message to IE: {err:?}"));
|
||||
return row;
|
||||
},
|
||||
};
|
||||
return row;
|
||||
}
|
||||
|
||||
pub fn analyze_qmdl_messages(&mut self, container: MessagesContainer) -> Vec<AnalysisRow> {
|
||||
let mut rows = Vec::new();
|
||||
for maybe_qmdl_message in container.into_messages() {
|
||||
@@ -211,7 +246,7 @@ impl Harness {
|
||||
rows
|
||||
}
|
||||
|
||||
fn analyze_information_element(&mut self, ie: &InformationElement) -> Vec<Option<Event>> {
|
||||
pub fn analyze_information_element(&mut self, ie: &InformationElement) -> Vec<Option<Event>> {
|
||||
self.analyzers
|
||||
.iter_mut()
|
||||
.map(|analyzer| analyzer.analyze_information_element(ie))
|
||||
|
||||
@@ -293,7 +293,7 @@ impl DiagDevice {
|
||||
// TPLINK M7350 v5 source code can be downloaded at https://www.tp-link.com/de/support/gpl-code/?app=omada
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct diag_logging_mode_param_t {
|
||||
struct DiagLoggingModeParam {
|
||||
req_mode: u32,
|
||||
peripheral_mask: u32,
|
||||
mode_param: u8,
|
||||
@@ -303,16 +303,16 @@ struct diag_logging_mode_param_t {
|
||||
fn enable_frame_readwrite(fd: i32, mode: u32) -> DiagResult<()> {
|
||||
unsafe {
|
||||
if libc::ioctl(fd, DIAG_IOCTL_SWITCH_LOGGING, mode, 0, 0, 0) < 0 {
|
||||
let try_params: &[diag_logging_mode_param_t] = &[
|
||||
let try_params: &[DiagLoggingModeParam] = &[
|
||||
// tplink M7350 HW revision 3-8 need this mode
|
||||
#[cfg(feature = "tplink")]
|
||||
diag_logging_mode_param_t {
|
||||
DiagLoggingModeParam {
|
||||
req_mode: mode,
|
||||
peripheral_mask: 0,
|
||||
mode_param: 1,
|
||||
},
|
||||
// tplink M7350 HW revision v9 requires the same parameters as orbic
|
||||
diag_logging_mode_param_t {
|
||||
DiagLoggingModeParam {
|
||||
req_mode: mode,
|
||||
peripheral_mask: u32::MAX,
|
||||
mode_param: 0,
|
||||
@@ -326,8 +326,8 @@ fn enable_frame_readwrite(fd: i32, mode: u32) -> DiagResult<()> {
|
||||
ret = libc::ioctl(
|
||||
fd,
|
||||
DIAG_IOCTL_SWITCH_LOGGING,
|
||||
&mut params as *mut diag_logging_mode_param_t,
|
||||
std::mem::size_of::<diag_logging_mode_param_t>(),
|
||||
&mut params as *mut DiagLoggingModeParam,
|
||||
std::mem::size_of::<DiagLoggingModeParam>(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
|
||||
+52
-4
@@ -1,6 +1,7 @@
|
||||
//! The spec for GSMTAP is here: https://github.com/osmocom/libosmocore/blob/master/include/osmocom/core/gsmtap.h
|
||||
|
||||
use deku::prelude::*;
|
||||
use num_enum::TryFromPrimitive;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub enum GsmtapType {
|
||||
@@ -28,14 +29,14 @@ pub enum GsmtapType {
|
||||
|
||||
// based on https://github.com/fgsect/scat/blob/97442580e628de414c9f7c2a185f4e28d0ee7523/src/scat/parsers/qualcomm/diagltelogparser.py#L1337
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, TryFromPrimitive)]
|
||||
pub enum LteNasSubtype {
|
||||
Plain = 0,
|
||||
Secure = 1,
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, TryFromPrimitive)]
|
||||
pub enum UmSubtype {
|
||||
Unknown = 0x00,
|
||||
Bcch = 0x01,
|
||||
@@ -56,7 +57,7 @@ pub enum UmSubtype {
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, TryFromPrimitive)]
|
||||
pub enum UmtsRrcSubtype {
|
||||
DlDcch = 0,
|
||||
UlDcch = 1,
|
||||
@@ -123,7 +124,7 @@ pub enum UmtsRrcSubtype {
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, TryFromPrimitive)]
|
||||
pub enum LteRrcSubtype {
|
||||
DlCcch = 0,
|
||||
DlDcch = 1,
|
||||
@@ -150,7 +151,54 @@ pub enum LteRrcSubtype {
|
||||
ScMcchNb = 22,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GsmtapTypeError {
|
||||
InvalidTypeSubtypeCombo(u8, u8),
|
||||
}
|
||||
|
||||
impl GsmtapType {
|
||||
pub fn new(gsmtap_type: u8, gsmtap_subtype: u8) -> Result<Self, GsmtapTypeError> {
|
||||
let maybe_result = match gsmtap_type {
|
||||
0x01 => match UmSubtype::try_from(gsmtap_subtype) {
|
||||
Ok(subtype) => Some(GsmtapType::Um(subtype)),
|
||||
_ => None,
|
||||
},
|
||||
0x02 => Some(GsmtapType::Abis),
|
||||
0x03 => Some(GsmtapType::UmBurst),
|
||||
0x04 => Some(GsmtapType::SIM),
|
||||
0x05 => Some(GsmtapType::TetraI1),
|
||||
0x06 => Some(GsmtapType::TetraI1Burst),
|
||||
0x07 => Some(GsmtapType::WmxBurst),
|
||||
0x08 => Some(GsmtapType::GbLlc),
|
||||
0x09 => Some(GsmtapType::GbSndcp),
|
||||
0x0a => Some(GsmtapType::Gmr1Um),
|
||||
0x0b => Some(GsmtapType::UmtsRlcMac),
|
||||
0x0c => match UmtsRrcSubtype::try_from(gsmtap_subtype) {
|
||||
Ok(subtype) => Some(GsmtapType::UmtsRrc(subtype)),
|
||||
_ => None,
|
||||
},
|
||||
0x0d => match LteRrcSubtype::try_from(gsmtap_subtype) {
|
||||
Ok(subtype) => Some(GsmtapType::LteRrc(subtype)),
|
||||
_ => None,
|
||||
},
|
||||
0x0e => Some(GsmtapType::LteMac),
|
||||
0x0f => Some(GsmtapType::LteMacFramed),
|
||||
0x10 => Some(GsmtapType::OsmocoreLog),
|
||||
0x11 => Some(GsmtapType::QcDiag),
|
||||
0x12 => match LteNasSubtype::try_from(gsmtap_subtype) {
|
||||
Ok(subtype) => Some(GsmtapType::LteNas(subtype)),
|
||||
_ => None,
|
||||
},
|
||||
0x13 => Some(GsmtapType::E1T1),
|
||||
0x14 => Some(GsmtapType::GsmRlp),
|
||||
_ => None,
|
||||
};
|
||||
match maybe_result {
|
||||
Some(result) => Ok(result),
|
||||
None => Err(GsmtapTypeError::InvalidTypeSubtypeCombo(gsmtap_type, gsmtap_subtype)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_type(&self) -> u8 {
|
||||
match self {
|
||||
GsmtapType::Um(_) => 0x01,
|
||||
|
||||
Reference in New Issue
Block a user