Compare commits

...

16 Commits

Author SHA1 Message Date
Will Greenberg 6b07b4e460 lib: support mac ul as well 2026-02-06 13:13:46 -08:00
Will Greenberg 9b6051380f lib: write ltemac packets as gsmtap 2026-02-06 13:13:46 -08:00
Will Greenberg f8581559e7 lib: add diag parsing for LTE MAC DL packets 2026-02-06 13:13:46 -08:00
Cooper Quintin ed2781a4be appease clippy 2026-02-05 15:41:54 -08:00
Cooper Quintin ffcf683ae5 appease npm 2026-02-05 15:41:54 -08:00
Cooper Quintin 49fd777c83 fix nits and add to config.toml 2026-02-05 15:41:54 -08:00
Cooper Quintin 84a3155a1f remove broken attach request and format 2026-02-05 15:41:54 -08:00
Cooper Quintin 184f4bd7a2 rename to diagnostic and add docs 2026-02-05 15:41:54 -08:00
Cooper Quintin f7759721e3 rebase against main 2026-02-05 15:41:54 -08:00
Cooper Quintin 744d0772c2 add message type 2026-02-05 15:41:54 -08:00
Cooper Quintin 2cd49b3757 show false postive attach reject message 2026-02-05 15:41:54 -08:00
Cooper Quintin e44230c043 imsi revealing message diagnostic heuristic 2026-02-05 15:41:54 -08:00
Cooper Quintin e27da68b5d bump version to 0.10.1 2026-02-05 12:27:09 -08:00
Markus Unterwaditzer 2a68c99897 Revert "Add warning about default routes"
This reverts commit 9ae1563286.

Reverts #804
Reverts #830

Reopens #345
2026-02-05 12:21:07 -08:00
dependabot[bot] 987d95c23e Bump rsa from 0.9.8 to 0.9.10 (#853)
Bumps [rsa](https://github.com/RustCrypto/RSA) from 0.9.8 to 0.9.10.
- [Changelog](https://github.com/RustCrypto/RSA/blob/v0.9.10/CHANGELOG.md)
- [Commits](https://github.com/RustCrypto/RSA/compare/v0.9.8...v0.9.10)

---
updated-dependencies:
- dependency-name: rsa
  dependency-version: 0.9.10
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-05 21:06:41 +01:00
dependabot[bot] 9ef6b43dac Bump time from 0.3.41 to 0.3.47
Bumps [time](https://github.com/time-rs/time) from 0.3.41 to 0.3.47.
- [Release notes](https://github.com/time-rs/time/releases)
- [Changelog](https://github.com/time-rs/time/blob/main/CHANGELOG.md)
- [Commits](https://github.com/time-rs/time/compare/v0.3.41...v0.3.47)

---
updated-dependencies:
- dependency-name: time
  dependency-version: 0.3.47
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-05 10:45:54 -08:00
27 changed files with 333 additions and 176 deletions
Generated
+21 -21
View File
@@ -1329,12 +1329,12 @@ dependencies = [
[[package]]
name = "deranged"
version = "0.4.0"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
dependencies = [
"powerfmt",
"serde",
"serde_core",
]
[[package]]
@@ -2725,7 +2725,7 @@ dependencies = [
[[package]]
name = "installer"
version = "0.10.0"
version = "0.10.1"
dependencies = [
"adb_client",
"aes",
@@ -2753,7 +2753,7 @@ dependencies = [
[[package]]
name = "installer-gui"
version = "0.10.0"
version = "0.10.1"
dependencies = [
"anyhow",
"installer",
@@ -3430,9 +3430,9 @@ dependencies = [
[[package]]
name = "num-conv"
version = "0.1.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
[[package]]
name = "num-derive"
@@ -4671,7 +4671,7 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
[[package]]
name = "rayhunter"
version = "0.10.0"
version = "0.10.1"
dependencies = [
"bytes",
"chrono",
@@ -4694,7 +4694,7 @@ dependencies = [
[[package]]
name = "rayhunter-check"
version = "0.10.0"
version = "0.10.1"
dependencies = [
"clap",
"futures",
@@ -4707,7 +4707,7 @@ dependencies = [
[[package]]
name = "rayhunter-daemon"
version = "0.10.0"
version = "0.10.1"
dependencies = [
"anyhow",
"async-trait",
@@ -4895,16 +4895,16 @@ dependencies = [
[[package]]
name = "rootshell"
version = "0.10.0"
version = "0.10.1"
dependencies = [
"nix 0.29.0",
]
[[package]]
name = "rsa"
version = "0.9.8"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
dependencies = [
"const-oid",
"digest",
@@ -5951,7 +5951,7 @@ dependencies = [
[[package]]
name = "telcom-parser"
version = "0.10.0"
version = "0.10.1"
dependencies = [
"asn1-codecs",
"asn1-compiler",
@@ -6057,30 +6057,30 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.41"
version = "0.3.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"serde_core",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.4"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]]
name = "time-macros"
version = "0.2.22"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
dependencies = [
"num-conv",
"time-core",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rayhunter-check"
version = "0.10.0"
version = "0.10.1"
edition = "2024"
[dependencies]
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rayhunter-daemon"
version = "0.10.0"
version = "0.10.1"
edition = "2024"
rust-version = "1.88.0"
+1 -2
View File
@@ -25,7 +25,7 @@ use crate::server::{
ServerState, debug_set_display_state, get_config, get_qmdl, get_time, get_zip, serve_static,
set_config, set_time_offset, test_notification,
};
use crate::stats::{get_qmdl_manifest, get_route_status, get_system_stats};
use crate::stats::{get_qmdl_manifest, get_system_stats};
use analysis::{
AnalysisCtrlMessage, AnalysisStatus, get_analysis_status, run_analysis_thread, start_analysis,
@@ -58,7 +58,6 @@ fn get_router() -> AppRouter {
.route("/api/qmdl/{name}", get(get_qmdl))
.route("/api/zip/{name}", get(get_zip))
.route("/api/system-stats", get(get_system_stats))
.route("/api/route-status", get(get_route_status))
.route("/api/qmdl-manifest", get(get_qmdl_manifest))
.route("/api/log", get(get_log))
.route("/api/start-recording", post(start_recording))
-30
View File
@@ -174,33 +174,3 @@ pub async fn get_log() -> Result<String, (StatusCode, String)> {
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
}
#[derive(Debug, Serialize)]
pub struct RouteStatus {
pub has_default_route: bool,
}
pub async fn get_route_status() -> Json<RouteStatus> {
let has_default_route = match check_default_route().await {
Ok(result) => result,
Err(err) => {
log::warn!("Failed to check default route: {err}");
// More likely than not, this is a portability issue. The logic hasn't been fully
// tested on all devices. We shouldn't scare users unnecessarily.
true
}
};
Json(RouteStatus { has_default_route })
}
// Checks for an IPv4 default route by reading /proc/net/route
// instead of shelling out to `ip route`, which may not be available on all devices.
async fn check_default_route() -> std::io::Result<bool> {
let contents = tokio::fs::read_to_string("/proc/net/route").await?;
Ok(contents.lines().skip(1).any(|line| {
line.split_whitespace()
.nth(1)
.is_some_and(|dest| dest == "00000000")
}))
}
@@ -1,6 +1,5 @@
<script lang="ts">
import { action_errors } from '../action_errors.svelte';
import WarningIcon from './WarningIcon.svelte';
let pos = $state(0);
let current_error = $derived(action_errors[pos]);
@@ -26,7 +25,21 @@
>
<div class="flex flex-row justify-between">
<span class="text-xl font-bold mb-2 mr-5 flex flex-row items-center gap-1 text-red-600">
<WarningIcon class="w-6 h-6 text-red-600" />
<svg
class="w-6 h-6 text-red-600"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
fill-rule="evenodd"
d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm11-4a1 1 0 1 0-2 0v5a1 1 0 1 0 2 0V8Zm-1 7a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H12Z"
clip-rule="evenodd"
/>
</svg>
Error Completing Action {current_error.times > 1 ? `x${current_error.times}` : ''}
</span>
<div class="flex items-center mb-2">
@@ -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>
@@ -1,52 +0,0 @@
<script lang="ts">
import { get_route_status } from '$lib/utils.svelte';
import WarningIcon from './WarningIcon.svelte';
let show_alert = $state(false);
let check_completed = $state(false);
async function check_route() {
if (check_completed) return;
try {
const status = await get_route_status();
if (!status.has_default_route) {
show_alert = true;
}
} catch (err) {
console.error('Failed to check route status:', err);
}
check_completed = true;
}
function dismiss() {
show_alert = false;
}
$effect(() => {
check_route();
});
</script>
{#if show_alert}
<div
class="bg-yellow-100 border-yellow-400 drop-shadow p-4 flex flex-col gap-2 border rounded-md"
>
<span class="text-xl font-bold flex flex-row items-center gap-2 text-yellow-700">
<WarningIcon class="w-6 h-6 text-yellow-600" />
No Default Route Detected
</span>
<p>
This device didn't get an IP address from the network operator. Presumably the SIM card
is not inserted or very old. Try a different SIM card.
</p>
<div class="flex flex-row gap-2 justify-end">
<button
class="font-medium py-2 px-4 rounded-md border border-gray-400 hover:bg-yellow-200"
onclick={dismiss}
>
Dismiss
</button>
</div>
</div>
{/if}
@@ -1,19 +0,0 @@
<script lang="ts">
let { class: className = 'w-6 h-6' }: { class?: string } = $props();
</script>
<svg
class={className}
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
fill-rule="evenodd"
d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm11-4a1 1 0 1 0-2 0v5a1 1 0 1 0 2 0V8Zm-1 7a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H12Z"
clip-rule="evenodd"
/>
</svg>
+1 -8
View File
@@ -10,6 +10,7 @@ export interface AnalyzerConfig {
nas_null_cipher: boolean;
incomplete_sib: boolean;
test_analyzer: boolean;
diagnostic_analyzer: boolean;
}
export enum enabled_notifications {
@@ -101,14 +102,6 @@ export async function test_notification(): Promise<void> {
}
}
export interface RouteStatus {
has_default_route: boolean;
}
export async function get_route_status(): Promise<RouteStatus> {
return JSON.parse(await req('GET', '/api/route-status'));
}
export interface TimeResponse {
system_time: string;
adjusted_time: string;
+30 -5
View File
@@ -10,10 +10,8 @@
import RecordingControls from '$lib/components/RecordingControls.svelte';
import ConfigForm from '$lib/components/ConfigForm.svelte';
import ActionErrors from '$lib/components/ActionErrors.svelte';
import IPRouteAlert from '$lib/components/IPRouteAlert.svelte';
import ClockDriftAlert from '$lib/components/ClockDriftAlert.svelte';
import LogView from '$lib/components/LogView.svelte';
import WarningIcon from '$lib/components/WarningIcon.svelte';
let manager: AnalysisManager = new AnalysisManager();
let loaded = $state(false);
@@ -180,7 +178,21 @@
class="bg-red-100 border-red-100 drop-shadow p-4 flex flex-col gap-2 border rounded-md flex-1 justify-between"
>
<span class="text-2xl font-bold mb-2 flex flex-row items-center gap-2 text-red-600">
<WarningIcon class="w-8 h-8 text-red-600" />
<svg
class="w-8 h-8 text-red-600"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
fill-rule="evenodd"
d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm11-4a1 1 0 1 0-2 0v5a1 1 0 1 0 2 0V8Zm-1 7a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H12Z"
clip-rule="evenodd"
/>
</svg>
Connection Error
</span>
<span
@@ -196,7 +208,6 @@
</div>
{/if}
<ActionErrors />
<IPRouteAlert />
<ClockDriftAlert />
{#if loaded}
<div class="flex flex-col lg:flex-row gap-4">
@@ -214,7 +225,21 @@
<span
class="text-2xl font-bold mb-2 flex flex-row items-center gap-2 text-red-600"
>
<WarningIcon class="w-8 h-8 text-red-600" />
<svg
class="w-8 h-8 text-red-600"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
fill-rule="evenodd"
d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm11-4a1 1 0 1 0-2 0v5a1 1 0 1 0 2 0V8Zm-1 7a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H12Z"
clip-rule="evenodd"
/>
</svg>
WARNING: Not Running
</span>
<span>
+1 -1
View File
@@ -5,7 +5,7 @@ export default defineConfig({
server: {
proxy: {
'/api': {
target: process.env.API_TARGET || 'http://localhost:8080',
target: 'http://localhost:8080',
changeOrigin: true,
secure: false,
configure: (proxy, _options) => {
+1
View File
@@ -39,3 +39,4 @@ null_cipher = true
nas_null_cipher = true
incomplete_sib = true
test_analyzer = false
diagnostic_analyzer = true
+3
View File
@@ -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!
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "installer-gui"
version = "0.10.0"
version = "0.10.1"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "installer"
version = "0.10.0"
version = "0.10.1"
edition = "2024"
[lib]
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rayhunter"
version = "0.10.0"
version = "0.10.1"
edition = "2024"
description = "Realtime cellular data decoding and analysis for IMSI catcher detection"
+8 -1
View File
@@ -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
}
+166
View File
@@ -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,
}
}
}
-5
View File
@@ -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
View File
@@ -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;
+28
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+3
View File
@@ -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;
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rootshell"
version = "0.10.0"
version = "0.10.1"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "telcom-parser"
version = "0.10.0"
version = "0.10.1"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html