mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-31 02:03:35 -07:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6bd36921d8 | |||
| c83ae30be8 | |||
| fa612241a5 | |||
| 10592bbd9d | |||
| 327eaddcd7 | |||
| 32149c3b37 | |||
| e47d4dacc4 | |||
| 4009e3d1ed | |||
| b2cd735a07 | |||
| 94e9a88a91 |
+36
-3
@@ -1,5 +1,5 @@
|
||||
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 clap::Parser;
|
||||
use futures::TryStreamExt;
|
||||
@@ -12,6 +12,9 @@ struct Args {
|
||||
#[arg(short, long)]
|
||||
qmdl_path: PathBuf,
|
||||
|
||||
#[arg(short, long)]
|
||||
pcapify: bool,
|
||||
|
||||
#[arg(long)]
|
||||
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);
|
||||
}
|
||||
|
||||
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]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
@@ -75,10 +99,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,12 @@ 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,
|
||||
lte_downgrade::LteSib6And7DowngradeAnalyzer,
|
||||
null_cipher::NullCipherAnalyzer,
|
||||
};
|
||||
|
||||
/// Qualitative measure of how severe a Warning event type is.
|
||||
/// The levels should break down like this:
|
||||
@@ -18,7 +23,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")]
|
||||
@@ -113,7 +118,7 @@ impl Harness {
|
||||
pub fn new_with_all_analyzers() -> Self {
|
||||
let mut harness = Harness::new();
|
||||
harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer{}));
|
||||
//harness.add_analyzer(Box::new(ImsiProvidedAnalyzer{}));
|
||||
harness.add_analyzer(Box::new(ImsiRequestedAnalyzer::new()));
|
||||
harness.add_analyzer(Box::new(NullCipherAnalyzer{}));
|
||||
|
||||
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 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)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,5 @@ pub mod analyzer;
|
||||
pub mod information_element;
|
||||
pub mod lte_downgrade;
|
||||
pub mod imsi_provided;
|
||||
pub mod imsi_requested;
|
||||
pub mod null_cipher;
|
||||
|
||||
@@ -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")]
|
||||
Inbound,
|
||||
#[deku(id_pat = "0xb0e3 | 0xb0ed")]
|
||||
Outbound,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(ctx = "ext_header_version: u8", id = "ext_header_version")]
|
||||
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 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