mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-06-01 10:43:34 -07:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b07b4e460 | |||
| 9b6051380f | |||
| f8581559e7 | |||
| ed2781a4be | |||
| ffcf683ae5 | |||
| 49fd777c83 | |||
| 84a3155a1f | |||
| 184f4bd7a2 | |||
| f7759721e3 | |||
| 744d0772c2 | |||
| 2cd49b3757 | |||
| e44230c043 |
Generated
+1
-1
@@ -2753,7 +2753,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "installer-gui"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"installer",
|
||||
|
||||
@@ -335,6 +335,20 @@
|
||||
Test Heuristic (noisy!)
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
id="diagnostic_analyzer"
|
||||
type="checkbox"
|
||||
bind:checked={config.analyzers.diagnostic_analyzer}
|
||||
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded"
|
||||
/>
|
||||
<label
|
||||
for="diagnostic_analyzer"
|
||||
class="ml-2 block text-sm text-gray-700"
|
||||
>
|
||||
Diagnostic Analyzer
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface AnalyzerConfig {
|
||||
nas_null_cipher: boolean;
|
||||
incomplete_sib: boolean;
|
||||
test_analyzer: boolean;
|
||||
diagnostic_analyzer: boolean;
|
||||
}
|
||||
|
||||
export enum enabled_notifications {
|
||||
|
||||
Vendored
+1
@@ -39,3 +39,4 @@ null_cipher = true
|
||||
nas_null_cipher = true
|
||||
incomplete_sib = true
|
||||
test_analyzer = false
|
||||
diagnostic_analyzer = true
|
||||
|
||||
@@ -73,6 +73,9 @@ This analyzer tests whether the SIB1 message contains a complete SIB chain (SIB3
|
||||
|
||||
On its own this might just be a misconfigured base station (though we have only seen it in the wild under suspicious circumstances) but combined with other heuristics such as **IMSI Requested** detection it should be considered as a strong indicator of malicious activity.
|
||||
|
||||
### Diagnostic Information
|
||||
This analyzer displays some diagnostic information about when your device connects and disconnects from certain towers. It is helpful for analysis of suspicious PCAPs. The informational warnings in here can safely be ignored until there is a low, medium, or high severity warning.
|
||||
|
||||
### Test Analyzer
|
||||
|
||||
This analyzer is great for testing if your Rayhunter installation works. It will alert every time a new tower is seen (specifically every time a tower broadcasts a SIB1 message.) It is designed to be very noisy so we do not recommend leaving it on but if this alerts it means your Rayhunter device is working!
|
||||
|
||||
@@ -4,6 +4,7 @@ use pcap_file_tokio::pcapng::blocks::enhanced_packet::EnhancedPacketBlock;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::analysis::diagnostic::DiagnosticAnalyzer;
|
||||
use crate::gsmtap::{GsmtapHeader, GsmtapMessage, GsmtapType};
|
||||
use crate::util::RuntimeMetadata;
|
||||
use crate::{diag::MessagesContainer, gsmtap_parser};
|
||||
@@ -19,19 +20,21 @@ use super::{
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct AnalyzerConfig {
|
||||
pub imsi_requested: bool,
|
||||
pub diagnostic_analyzer: bool,
|
||||
pub connection_redirect_2g_downgrade: bool,
|
||||
pub lte_sib6_and_7_downgrade: bool,
|
||||
pub null_cipher: bool,
|
||||
pub nas_null_cipher: bool,
|
||||
pub incomplete_sib: bool,
|
||||
pub test_analyzer: bool,
|
||||
pub imsi_requested: bool,
|
||||
}
|
||||
|
||||
impl Default for AnalyzerConfig {
|
||||
fn default() -> Self {
|
||||
AnalyzerConfig {
|
||||
imsi_requested: true,
|
||||
diagnostic_analyzer: true,
|
||||
connection_redirect_2g_downgrade: true,
|
||||
lte_sib6_and_7_downgrade: true,
|
||||
null_cipher: true,
|
||||
@@ -346,6 +349,10 @@ impl Harness {
|
||||
harness.add_analyzer(Box::new(TestAnalyzer {}))
|
||||
}
|
||||
|
||||
if analyzer_config.diagnostic_analyzer {
|
||||
harness.add_analyzer(Box::new(DiagnosticAnalyzer {}));
|
||||
}
|
||||
|
||||
harness
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
use crate::analysis::analyzer::{Analyzer, Event, EventType};
|
||||
use crate::analysis::information_element::{InformationElement, LteInformationElement};
|
||||
use pycrate_rs::nas::NASMessage;
|
||||
use pycrate_rs::nas::emm::EMMMessage;
|
||||
use pycrate_rs::nas::generated::emm::emm_attach_reject::EMMCauseEMMCause as AttachRejectEMMCause;
|
||||
use pycrate_rs::nas::generated::emm::emm_detach_request_mt::EPSDetachTypeMTType;
|
||||
use pycrate_rs::nas::generated::emm::emm_service_reject::EMMCauseEMMCause as ServiceRejectEMMCause;
|
||||
use pycrate_rs::nas::generated::emm::emm_tracking_area_update_reject::EMMCauseEMMCause as TAURejectEMMCause;
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub struct DiagnosticAnalyzer;
|
||||
|
||||
impl Default for DiagnosticAnalyzer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl DiagnosticAnalyzer {
|
||||
pub fn new() -> Self {
|
||||
DiagnosticAnalyzer
|
||||
}
|
||||
|
||||
fn is_imsi_exposing_nas(&self, nas_msg: &NASMessage) -> bool {
|
||||
match nas_msg {
|
||||
NASMessage::EMMMessage(emm_msg) => match emm_msg {
|
||||
EMMMessage::EMMIdentityRequest(_) => true, // Alert on all identity requests (IMSI, IMEI, IMEISV)
|
||||
|
||||
EMMMessage::EMMTrackingAreaUpdateReject(reject) => {
|
||||
matches!(
|
||||
reject.emm_cause.inner,
|
||||
TAURejectEMMCause::IllegalUE
|
||||
| TAURejectEMMCause::IllegalME
|
||||
| TAURejectEMMCause::EPSServicesNotAllowed
|
||||
| TAURejectEMMCause::EPSServicesAndNonEPSServicesNotAllowed
|
||||
| TAURejectEMMCause::TrackingAreaNotAllowed
|
||||
| TAURejectEMMCause::EPSServicesNotAllowedInThisPLMN
|
||||
| TAURejectEMMCause::RequestedServiceOptionNotAuthorizedInThisPLMN
|
||||
)
|
||||
}
|
||||
|
||||
EMMMessage::EMMAttachReject(reject) => {
|
||||
matches!(
|
||||
reject.emm_cause.inner,
|
||||
AttachRejectEMMCause::IllegalUE
|
||||
| AttachRejectEMMCause::IllegalME
|
||||
| AttachRejectEMMCause::EPSServicesNotAllowed
|
||||
| AttachRejectEMMCause::EPSServicesAndNonEPSServicesNotAllowed
|
||||
| AttachRejectEMMCause::PLMNNotAllowed
|
||||
| AttachRejectEMMCause::TrackingAreaNotAllowed
|
||||
| AttachRejectEMMCause::RoamingNotAllowedInThisTrackingArea
|
||||
| AttachRejectEMMCause::EPSServicesNotAllowedInThisPLMN
|
||||
| AttachRejectEMMCause::NoSuitableCellsInTrackingArea
|
||||
| AttachRejectEMMCause::RequestedServiceOptionNotAuthorizedInThisPLMN
|
||||
)
|
||||
}
|
||||
|
||||
EMMMessage::EMMDetachRequestMT(req) => {
|
||||
// Original implementation: !(nas_eps.emm.detach_type_dl == 3)
|
||||
req.eps_detach_type.inner.typ != EPSDetachTypeMTType::IMSIDetach
|
||||
}
|
||||
|
||||
EMMMessage::EMMAttachRequest(_) => {
|
||||
// just because eps_attach_type is IMSI doesn't mean that the phoen transmitted its IMSI
|
||||
// It often sends the GUTI instead. We could check the req.epsid structure but it appears to actually
|
||||
// not be parsed. So for now we are just ignoreing this message
|
||||
// req.eps_attach_type.inner == EPSAttachTypeV::CombinedEPSIMSIAttach
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
EMMMessage::EMMServiceReject(reject) => {
|
||||
matches!(
|
||||
reject.emm_cause.inner,
|
||||
ServiceRejectEMMCause::IllegalUE
|
||||
| ServiceRejectEMMCause::IllegalME
|
||||
| ServiceRejectEMMCause::EPSServicesNotAllowed
|
||||
| ServiceRejectEMMCause::UEIdentityCannotBeDerivedByTheNetwork
|
||||
| ServiceRejectEMMCause::TrackingAreaNotAllowed
|
||||
| ServiceRejectEMMCause::EPSServicesNotAllowedInThisPLMN
|
||||
| ServiceRejectEMMCause::RequestedServiceOptionNotAuthorizedInThisPLMN
|
||||
)
|
||||
}
|
||||
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Analyzer for DiagnosticAnalyzer {
|
||||
fn get_name(&self) -> Cow<'_, str> {
|
||||
"Diagnostic detector for messages which might lead to IMSI exposure".into()
|
||||
}
|
||||
|
||||
fn get_description(&self) -> Cow<'_, str> {
|
||||
"Catches any messages that may lead to IMSI Exposure. Can be quite noisy. \
|
||||
Useful as a diagnostic for finding out why an IMSI was sent or what \
|
||||
the reason for a reject message was. Not a useful indicator on its own \
|
||||
but a helpful diagnostic for understanding why another indicator was \
|
||||
triggered. Based on the list of IMSI exposing messages identified in \
|
||||
the 'Marlin' paper."
|
||||
.into()
|
||||
}
|
||||
|
||||
fn get_version(&self) -> u32 {
|
||||
1
|
||||
}
|
||||
|
||||
fn analyze_information_element(
|
||||
&mut self,
|
||||
ie: &InformationElement,
|
||||
_packet_num: usize,
|
||||
) -> Option<Event> {
|
||||
let lte_ie = match ie {
|
||||
InformationElement::LTE(inner) => inner,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
match lte_ie.as_ref() {
|
||||
LteInformationElement::NAS(nas_msg) => {
|
||||
if self.is_imsi_exposing_nas(nas_msg) {
|
||||
let message_type = match nas_msg {
|
||||
NASMessage::EMMMessage(emm_msg) => match emm_msg {
|
||||
EMMMessage::EMMIdentityRequest(request) => {
|
||||
format!("EMM Identity Request ({:?})", request.id_type.inner)
|
||||
}
|
||||
EMMMessage::EMMTrackingAreaUpdateReject(reject) => {
|
||||
format!(
|
||||
"EMM Tracking Area Update Reject ({:?})",
|
||||
reject.emm_cause.inner
|
||||
)
|
||||
}
|
||||
EMMMessage::EMMAttachReject(reject) => {
|
||||
format!("EMM Attach Reject ({:?})", reject.emm_cause.inner)
|
||||
}
|
||||
EMMMessage::EMMDetachRequestMT(request) => {
|
||||
format!(
|
||||
"EMM Detach Request ({:?}:{:?})",
|
||||
request.eps_detach_type.inner, request.emm_cause.inner
|
||||
)
|
||||
}
|
||||
EMMMessage::EMMServiceReject(reject) => {
|
||||
format!("EMM Service Reject ({:?})", reject.emm_cause.inner)
|
||||
}
|
||||
EMMMessage::EMMAttachRequest(request) => {
|
||||
format!("EPS Attach Request ({:?})", request.epsid.inner)
|
||||
}
|
||||
_ => "Unknown EMM Message".to_string(),
|
||||
},
|
||||
_ => "Unknown NAS Message".to_string(),
|
||||
};
|
||||
|
||||
Some(Event {
|
||||
event_type: EventType::Informational,
|
||||
message: format!("Diagnostic: {message_type}."),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,12 +78,7 @@ impl ImsiRequestedAnalyzer {
|
||||
});
|
||||
}
|
||||
|
||||
// Notify on any identity reqeust (IMEI or IMSI)
|
||||
(_, State::IdentityRequest) => {
|
||||
self.flag = Some(Event {
|
||||
event_type: EventType::Informational,
|
||||
message: "Identity Request happened but its not suspicious yet.".to_string(),
|
||||
});
|
||||
self.timeout_counter = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod analyzer;
|
||||
pub mod connection_redirect_downgrade;
|
||||
pub mod diagnostic;
|
||||
pub mod imsi_requested;
|
||||
pub mod incomplete_sib;
|
||||
pub mod information_element;
|
||||
|
||||
@@ -210,6 +210,16 @@ pub enum LogBody {
|
||||
#[deku(count = "hdr_len.saturating_sub(8)")]
|
||||
msg: Vec<u8>,
|
||||
},
|
||||
#[deku(id = "0xb063")]
|
||||
LteMac {
|
||||
#[deku(ctx = "log_type")]
|
||||
direction: LteMacMessageDirection,
|
||||
version: u8,
|
||||
#[deku(pad_bytes_after = "2")]
|
||||
num_subpacket: u8,
|
||||
#[deku(count = "num_subpacket")]
|
||||
subpackets: Vec<LteMacSubpacket>,
|
||||
},
|
||||
#[deku(id = "0x713a")]
|
||||
UmtsNasOtaMessage {
|
||||
is_uplink: u8,
|
||||
@@ -224,6 +234,24 @@ pub enum LogBody {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
pub struct LteMacSubpacket {
|
||||
pub id: u8,
|
||||
pub version: u8,
|
||||
pub size: u16,
|
||||
#[deku(count = "size - 4")]
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(ctx = "log_type: u16", id = "log_type")]
|
||||
pub enum LteMacMessageDirection {
|
||||
#[deku(id_pat = "0xb063")]
|
||||
Downlink,
|
||||
#[deku(id_pat = "0xb064")]
|
||||
Uplink,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(ctx = "log_type: u16", id = "log_type")]
|
||||
pub enum Nas4GMessageDirection {
|
||||
|
||||
+15
-12
@@ -40,22 +40,25 @@ pub enum DiagDeviceError {
|
||||
ParseMessagesContainerError(deku::DekuError),
|
||||
}
|
||||
|
||||
pub const LOG_CODES_FOR_RAW_PACKET_LOGGING: [u32; 11] = [
|
||||
pub const LOG_CODES_FOR_RAW_PACKET_LOGGING: [u32; 13] = [
|
||||
// Layer 2:
|
||||
log_codes::LOG_GPRS_MAC_SIGNALLING_MESSAGE_C, // 0x5226
|
||||
log_codes::LOG_GPRS_MAC_SIGNALLING_MESSAGE_C,
|
||||
// Layer 3:
|
||||
log_codes::LOG_GSM_RR_SIGNALING_MESSAGE_C, // 0x512f
|
||||
log_codes::WCDMA_SIGNALLING_MESSAGE, // 0x412f
|
||||
log_codes::LOG_LTE_RRC_OTA_MSG_LOG_C, // 0xb0c0
|
||||
log_codes::LOG_NR_RRC_OTA_MSG_LOG_C, // 0xb821
|
||||
log_codes::LOG_GSM_RR_SIGNALING_MESSAGE_C,
|
||||
log_codes::WCDMA_SIGNALLING_MESSAGE,
|
||||
log_codes::LOG_LTE_RRC_OTA_MSG_LOG_C,
|
||||
log_codes::LOG_NR_RRC_OTA_MSG_LOG_C,
|
||||
// NAS:
|
||||
log_codes::LOG_UMTS_NAS_OTA_MESSAGE_LOG_PACKET_C, // 0x713a
|
||||
log_codes::LOG_LTE_NAS_ESM_OTA_IN_MSG_LOG_C, // 0xb0e2
|
||||
log_codes::LOG_LTE_NAS_ESM_OTA_OUT_MSG_LOG_C, // 0xb0e3
|
||||
log_codes::LOG_LTE_NAS_EMM_OTA_IN_MSG_LOG_C, // 0xb0ec
|
||||
log_codes::LOG_LTE_NAS_EMM_OTA_OUT_MSG_LOG_C, // 0xb0ed
|
||||
log_codes::LOG_UMTS_NAS_OTA_MESSAGE_LOG_PACKET_C,
|
||||
log_codes::LOG_LTE_NAS_ESM_OTA_IN_MSG_LOG_C,
|
||||
log_codes::LOG_LTE_NAS_ESM_OTA_OUT_MSG_LOG_C,
|
||||
log_codes::LOG_LTE_NAS_EMM_OTA_IN_MSG_LOG_C,
|
||||
log_codes::LOG_LTE_NAS_EMM_OTA_OUT_MSG_LOG_C,
|
||||
// MAC
|
||||
log_codes::LOG_LTE_MAC_DL,
|
||||
log_codes::LOG_LTE_MAC_UL,
|
||||
// User IP traffic:
|
||||
log_codes::LOG_DATA_PROTOCOL_LOGGING_C, // 0x11eb
|
||||
log_codes::LOG_DATA_PROTOCOL_LOGGING_C,
|
||||
];
|
||||
|
||||
const BUFFER_LEN: usize = 1024 * 1024 * 10;
|
||||
|
||||
+18
-11
@@ -1,6 +1,7 @@
|
||||
use crate::diag::*;
|
||||
use crate::gsmtap::*;
|
||||
|
||||
use deku::DekuContainerWrite;
|
||||
use log::error;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -13,16 +14,13 @@ pub enum GsmtapParserError {
|
||||
}
|
||||
|
||||
pub fn parse(msg: Message) -> Result<Option<(Timestamp, GsmtapMessage)>, GsmtapParserError> {
|
||||
if let Message::Log {
|
||||
timestamp, body, ..
|
||||
} = msg
|
||||
{
|
||||
match log_to_gsmtap(body)? {
|
||||
Some(msg) => Ok(Some((timestamp, msg))),
|
||||
None => Ok(None),
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
let Message::Log { timestamp, body, .. } = msg else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
match log_to_gsmtap(body)? {
|
||||
Some(msg) => Ok(Some((timestamp, msg))),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +149,16 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
|
||||
header,
|
||||
payload: msg,
|
||||
}))
|
||||
}
|
||||
},
|
||||
LogBody::LteMac { direction, subpackets, .. } => {
|
||||
let mut header = GsmtapHeader::new(GsmtapType::LteMac);
|
||||
header.uplink = matches!(direction, LteMacMessageDirection::Uplink);
|
||||
let mut payload = Vec::new();
|
||||
for packet in subpackets {
|
||||
payload.extend(packet.to_bytes().unwrap());
|
||||
}
|
||||
Ok(Some(GsmtapMessage { header, payload }))
|
||||
},
|
||||
_ => {
|
||||
error!("gsmtap_sink: ignoring unhandled log type: {value:?}");
|
||||
Ok(None)
|
||||
|
||||
@@ -36,6 +36,9 @@ pub const LOG_LTE_NAS_ESM_OTA_OUT_MSG_LOG_C: u32 = 0xb0e3;
|
||||
pub const LOG_LTE_NAS_EMM_OTA_IN_MSG_LOG_C: u32 = 0xb0ec;
|
||||
pub const LOG_LTE_NAS_EMM_OTA_OUT_MSG_LOG_C: u32 = 0xb0ed;
|
||||
|
||||
pub const LOG_LTE_MAC_DL: u32 = 0xb063;
|
||||
pub const LOG_LTE_MAC_UL: u32 = 0xb064;
|
||||
|
||||
pub const LTE_BCCH_BCH_V0: u32 = 1;
|
||||
pub const LTE_BCCH_DL_SCH_V0: u32 = 2;
|
||||
pub const LTE_MCCH_V0: u32 = 3;
|
||||
|
||||
Reference in New Issue
Block a user