mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-31 02:03:35 -07:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| df55c04e85 | |||
| 7475cd5cd9 | |||
| cef94ba6b0 | |||
| d7c973ea95 | |||
| 64d657efd6 | |||
| 16447ed8bf | |||
| 663d0abb57 | |||
| f49d11f034 | |||
| 56dcfdb47c | |||
| a46ede37b6 | |||
| 69dc528f34 | |||
| 29ce6729ee | |||
| 5919a19aba | |||
| 35ca590e46 | |||
| 56122f6559 | |||
| bbab29ae0b | |||
| 2a620fd1fb | |||
| 515bb40a76 | |||
| a5ec1c9505 | |||
| 806bd62a0e |
@@ -344,7 +344,7 @@ jobs:
|
||||
else
|
||||
mv installer-$platform/installer "$dest"/installer
|
||||
fi
|
||||
cp -r rayhunter-daemon rootshell/rootshell dist/* installer/install.ps1 "$dest"/
|
||||
cp -r rayhunter-check-* rayhunter-daemon rootshell/rootshell dist/* installer/install.ps1 "$dest"/
|
||||
zip -r "$dest.zip" "$dest"
|
||||
sha256sum "$dest.zip" > "$dest.zip.sha256"
|
||||
|
||||
|
||||
Generated
+7
-8
@@ -5,7 +5,7 @@ version = 4
|
||||
[[package]]
|
||||
name = "adb_client"
|
||||
version = "2.1.11"
|
||||
source = "git+https://github.com/EFForg/adb_client.git?rev=e511662394e4fa32865c154c40f81a3d846f700c#e511662394e4fa32865c154c40f81a3d846f700c"
|
||||
source = "git+https://github.com/EFForg/adb_client.git?rev=208a302367727554d7530e937ca8aee20a74fa51#208a302367727554d7530e937ca8aee20a74fa51"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"base64",
|
||||
@@ -22,7 +22,6 @@ dependencies = [
|
||||
"nusb",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"rsa",
|
||||
"rusb",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
@@ -1729,7 +1728,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "installer"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"adb_client",
|
||||
"aes",
|
||||
@@ -2707,7 +2706,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rayhunter"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"chrono",
|
||||
@@ -2729,7 +2728,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rayhunter-check"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"futures",
|
||||
@@ -2743,7 +2742,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rayhunter-daemon"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -2888,7 +2887,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rootshell"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"nix",
|
||||
]
|
||||
@@ -3365,7 +3364,7 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||
|
||||
[[package]]
|
||||
name = "telcom-parser"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"asn1-codecs",
|
||||
"asn1-compiler",
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||

|
||||
|
||||
# Rayhunter
|
||||
|
||||

|
||||
|
||||
Rayhunter is an IMSI Catcher Catcher for the Orbic mobile hotspot. To learn more, check out the [Rayhunter Book](https://efforg.github.io/rayhunter/).
|
||||

|
||||
|
||||
Rayhunter is a project for detecting IMSI catchers, also known as cell-site simulators or stingrays. It was first designed to run on a cheap mobile hotspot called the Orbic RC400L, but thanks to community efforts can [support some other devices as well](https://efforg.github.io/rayhunter/supported-devices.html).
|
||||
It's also designed to be as easy to install and use as possible, regardless of your level of technical skills, and to minimize false positives.
|
||||
|
||||
→ Check out the [installation guide](https://efforg.github.io/rayhunter/installation.html) to get started.
|
||||
|
||||
→ To learn more about the aim of the project, and about IMSI catchers in general, please check out our [introductory blog post](https://www.eff.org/deeplinks/2025/03/meet-rayhunter-new-open-source-tool-eff-detect-cellular-spying).
|
||||
|
||||
→ For discussion, help, or to join the mattermost channel and get involved with the project and community check out the [many ways listed here](https://efforg.github.io/rayhunter/support-feedback-community.html)!
|
||||
|
||||
→ To learn more about the project in general check out the [Rayhunter Book](https://efforg.github.io/rayhunter/).
|
||||
|
||||
**LEGAL DISCLAIMER:** Use this program at your own risk. We believe running this program does not currently violate any laws or regulations in the United States. However, we are not responsible for civil or criminal liability resulting from the use of this software. If you are located outside of the US please consult with an attorney in your country to help you assess the legal risks of running this program.
|
||||
|
||||
*Good Hunting!*
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rayhunter-check"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rayhunter-daemon"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
edition = "2024"
|
||||
rust-version = "1.88.0"
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
use std::path::Path;
|
||||
|
||||
use rayhunter::Device;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::error::RayhunterError;
|
||||
|
||||
pub mod orbic;
|
||||
pub mod tmobile;
|
||||
pub mod wingtech;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Debug, Serialize)]
|
||||
pub struct BatteryState {
|
||||
level: u8,
|
||||
is_plugged_in: bool,
|
||||
}
|
||||
|
||||
async fn is_plugged_in_from_file(path: &Path) -> Result<bool, RayhunterError> {
|
||||
match tokio::fs::read_to_string(path)
|
||||
.await
|
||||
.map_err(RayhunterError::TokioError)?
|
||||
.chars()
|
||||
.next()
|
||||
{
|
||||
Some('0') => Ok(false),
|
||||
Some('1') => Ok(true),
|
||||
_ => Err(RayhunterError::BatteryPluggedInStatusParseError),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_level_from_percentage_file(path: &Path) -> Result<u8, RayhunterError> {
|
||||
tokio::fs::read_to_string(path)
|
||||
.await
|
||||
.map_err(RayhunterError::TokioError)?
|
||||
.trim_end()
|
||||
.parse()
|
||||
.or(Err(RayhunterError::BatteryLevelParseError))
|
||||
}
|
||||
|
||||
pub async fn get_battery_status(device: &Device) -> Result<BatteryState, RayhunterError> {
|
||||
Ok(match device {
|
||||
Device::Orbic => orbic::get_battery_state().await?,
|
||||
Device::Wingtech => wingtech::get_battery_state().await?,
|
||||
Device::Tmobile => tmobile::get_battery_state().await?,
|
||||
_ => return Err(RayhunterError::FunctionNotSupportedForDeviceError),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{
|
||||
battery::{BatteryState, is_plugged_in_from_file},
|
||||
error::RayhunterError,
|
||||
};
|
||||
|
||||
const BATTERY_LEVEL_FILE: &str = "/sys/kernel/chg_info/level";
|
||||
const PLUGGED_IN_STATE_FILE: &str = "/sys/kernel/chg_info/chg_en";
|
||||
|
||||
pub async fn get_battery_state() -> Result<BatteryState, RayhunterError> {
|
||||
Ok(BatteryState {
|
||||
level: match tokio::fs::read_to_string(&BATTERY_LEVEL_FILE)
|
||||
.await
|
||||
.map_err(RayhunterError::TokioError)?
|
||||
.chars()
|
||||
.next()
|
||||
{
|
||||
Some('1') => Ok(10),
|
||||
Some('2') => Ok(25),
|
||||
Some('3') => Ok(50),
|
||||
Some('4') => Ok(75),
|
||||
Some('5') => Ok(100),
|
||||
_ => Err(RayhunterError::BatteryLevelParseError),
|
||||
}?,
|
||||
is_plugged_in: is_plugged_in_from_file(Path::new(PLUGGED_IN_STATE_FILE)).await?,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{
|
||||
battery::{BatteryState, get_level_from_percentage_file, is_plugged_in_from_file},
|
||||
error::RayhunterError,
|
||||
};
|
||||
|
||||
const BATTERY_LEVEL_FILE: &str = "/sys/class/power_supply/bms/capacity";
|
||||
const PLUGGED_IN_STATE_FILE: &str = "/sys/devices/78d9000.usb/power_supply/usb/online";
|
||||
|
||||
pub async fn get_battery_state() -> Result<BatteryState, RayhunterError> {
|
||||
Ok(BatteryState {
|
||||
level: get_level_from_percentage_file(Path::new(BATTERY_LEVEL_FILE)).await?,
|
||||
is_plugged_in: is_plugged_in_from_file(Path::new(PLUGGED_IN_STATE_FILE)).await?,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{
|
||||
battery::{BatteryState, get_level_from_percentage_file, is_plugged_in_from_file},
|
||||
error::RayhunterError,
|
||||
};
|
||||
|
||||
const BATTERY_LEVEL_FILE: &str =
|
||||
"/sys/devices/78b7000.i2c/i2c-3/3-0063/power_supply/cw2017-bat/capacity";
|
||||
const PLUGGED_IN_STATE_FILE: &str = "/sys/devices/8a00000.ssusb/power_supply/usb/online";
|
||||
|
||||
pub async fn get_battery_state() -> Result<BatteryState, RayhunterError> {
|
||||
Ok(BatteryState {
|
||||
level: get_level_from_percentage_file(Path::new(BATTERY_LEVEL_FILE)).await?,
|
||||
is_plugged_in: is_plugged_in_from_file(Path::new(PLUGGED_IN_STATE_FILE)).await?,
|
||||
})
|
||||
}
|
||||
@@ -15,4 +15,10 @@ pub enum RayhunterError {
|
||||
QmdlStoreError(#[from] RecordingStoreError),
|
||||
#[error("No QMDL store found at path {0}, but can't create a new one due to debug mode")]
|
||||
NoStoreDebugMode(String),
|
||||
#[error("Error parsing file to determine battery level")]
|
||||
BatteryLevelParseError,
|
||||
#[error("Error parsing file to determine whether device is plugged in")]
|
||||
BatteryPluggedInStatusParseError,
|
||||
#[error("The requested functionality is not supported for this device")]
|
||||
FunctionNotSupportedForDeviceError,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod analysis;
|
||||
mod battery;
|
||||
mod config;
|
||||
mod diag;
|
||||
mod display;
|
||||
@@ -39,6 +40,7 @@ use log::{error, info};
|
||||
use qmdl_store::RecordingStoreError;
|
||||
use rayhunter::Device;
|
||||
use rayhunter::diag_device::DiagDevice;
|
||||
use stats::get_log;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::select;
|
||||
use tokio::sync::mpsc::{self, Sender};
|
||||
@@ -55,6 +57,7 @@ fn get_router() -> AppRouter {
|
||||
.route("/api/zip/{name}", get(get_zip))
|
||||
.route("/api/system-stats", get(get_system_stats))
|
||||
.route("/api/qmdl-manifest", get(get_qmdl_manifest))
|
||||
.route("/api/log", get(get_log))
|
||||
.route("/api/start-recording", post(start_recording))
|
||||
.route("/api/stop-recording", post(stop_recording))
|
||||
.route("/api/delete-recording/{name}", post(delete_recording))
|
||||
|
||||
+19
-1
@@ -1,7 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::qmdl_store::ManifestEntry;
|
||||
use crate::battery::get_battery_status;
|
||||
use crate::error::RayhunterError;
|
||||
use crate::server::ServerState;
|
||||
use crate::{battery::BatteryState, qmdl_store::ManifestEntry};
|
||||
|
||||
use axum::Json;
|
||||
use axum::extract::State;
|
||||
@@ -16,6 +18,8 @@ pub struct SystemStats {
|
||||
pub disk_stats: DiskStats,
|
||||
pub memory_stats: MemoryStats,
|
||||
pub runtime_metadata: RuntimeMetadata,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub battery_status: Option<BatteryState>,
|
||||
}
|
||||
|
||||
impl SystemStats {
|
||||
@@ -24,6 +28,14 @@ impl SystemStats {
|
||||
disk_stats: DiskStats::new(qmdl_path, device).await?,
|
||||
memory_stats: MemoryStats::new(device).await?,
|
||||
runtime_metadata: RuntimeMetadata::new(),
|
||||
battery_status: match get_battery_status(device).await {
|
||||
Ok(status) => Some(status),
|
||||
Err(RayhunterError::FunctionNotSupportedForDeviceError) => None,
|
||||
Err(err) => {
|
||||
log::error!("Failed to get battery status: {err}");
|
||||
None
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -156,3 +168,9 @@ pub async fn get_qmdl_manifest(
|
||||
current_entry,
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn get_log() -> Result<String, (StatusCode, String)> {
|
||||
tokio::fs::read_to_string("/data/rayhunter/rayhunter.log")
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<body data-sveltekit-preload-data="hover" style="width: 100%">
|
||||
<div style="display: contents" class="m-4 xl:m-8">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
export class ActionError extends Error {
|
||||
// The number of this an identical error has happened.
|
||||
// This is shown as a number next to the error in the UI.
|
||||
times = $state(1);
|
||||
|
||||
constructor(message: string, cause: Error) {
|
||||
super(message);
|
||||
this.cause = cause;
|
||||
}
|
||||
}
|
||||
|
||||
export const action_errors: ActionError[] = $state([]);
|
||||
|
||||
export function add_error(e: Error, msg: string): void {
|
||||
for (const existing of action_errors) {
|
||||
if (existing.message === msg) {
|
||||
existing.times += 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
const action_error = new ActionError(msg, e);
|
||||
action_errors.unshift(action_error);
|
||||
console.log(action_errors.length);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<script lang="ts">
|
||||
import { action_errors } from '../action_errors.svelte';
|
||||
|
||||
let pos = $state(0);
|
||||
let current_error = $derived(action_errors[pos]);
|
||||
|
||||
function prev_error() {
|
||||
if (pos > 0) pos -= 1;
|
||||
else pos = action_errors.length - 1;
|
||||
}
|
||||
function next_error() {
|
||||
if (pos + 1 < action_errors.length) pos += 1;
|
||||
else pos = 0;
|
||||
}
|
||||
function clear_errors() {
|
||||
pos = 0;
|
||||
action_errors.length = 0;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if action_errors.length > 0}
|
||||
<div
|
||||
class="bg-red-100 border-red-100 drop-shadow p-4 flex flex-col gap-2
|
||||
border rounded-md flex-1 justify-between fixed z-10 right-3 bottom-3 ml-3"
|
||||
>
|
||||
<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">
|
||||
<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">
|
||||
{#if action_errors.length > 1}
|
||||
<span>{pos + 1}/{action_errors.length}</span>
|
||||
<button title="previous error" aria-label="previous error" onclick={prev_error}>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m 15.499979,19.499979 -6.9999997,-7 6.9999997,-6.9999997"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button title="next error" aria-label="next error" onclick={next_error}>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m 8.5000207,5.4999793 7.0000003,6.9999997 -7.0000003,7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
<button title="clear errors" aria-label="clear errors" onclick={clear_errors}>
|
||||
<svg style="width:24px;height:24px" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<span>{current_error.message}</span>
|
||||
{#if current_error.cause}
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
<code>{current_error.cause}</code>
|
||||
</details>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { req } from '$lib/utils.svelte';
|
||||
import { user_action_req } from '$lib/utils.svelte';
|
||||
|
||||
let {
|
||||
url,
|
||||
@@ -11,6 +11,7 @@
|
||||
icon,
|
||||
onclick,
|
||||
ariaLabel,
|
||||
errorMessage,
|
||||
}: {
|
||||
url: string;
|
||||
method?: string;
|
||||
@@ -21,6 +22,7 @@
|
||||
icon?: any; // Svelte snippet
|
||||
onclick?: () => void | Promise<void>;
|
||||
ariaLabel?: string;
|
||||
errorMessage?: string;
|
||||
} = $props();
|
||||
|
||||
let is_requesting = $state(false);
|
||||
@@ -46,7 +48,11 @@
|
||||
|
||||
is_requesting = true;
|
||||
try {
|
||||
await req(method, url);
|
||||
await user_action_req(
|
||||
method,
|
||||
url,
|
||||
errorMessage ? errorMessage : 'Error performing action'
|
||||
);
|
||||
if (onclick) {
|
||||
await onclick();
|
||||
}
|
||||
|
||||
@@ -7,5 +7,6 @@
|
||||
text="Delete ALL Recordings"
|
||||
prompt={`Are you sure you want to delete ALL recordings?`}
|
||||
url={`/api/delete-all-recordings`}
|
||||
name="all recodings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { req } from '$lib/utils.svelte';
|
||||
import { user_action_req } from '$lib/utils.svelte';
|
||||
let {
|
||||
text,
|
||||
url,
|
||||
prompt,
|
||||
name,
|
||||
}: {
|
||||
text?: string;
|
||||
url: string;
|
||||
prompt: string;
|
||||
name: string;
|
||||
} = $props();
|
||||
|
||||
function confirmDelete() {
|
||||
if (window.confirm(prompt)) {
|
||||
req('POST', url);
|
||||
user_action_req('POST', url, 'Unable to delete recording ' + name);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<script lang="ts">
|
||||
import { get_logs } from '$lib/utils.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let { shown = $bindable() }: { shown: boolean } = $props();
|
||||
let content: string | undefined = $state(undefined);
|
||||
|
||||
onMount(() => {
|
||||
// Used by LogView modal
|
||||
window.addEventListener('scroll', () => {
|
||||
document.documentElement.style.setProperty('--scroll-y', `${window.scrollY}px`);
|
||||
});
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (shown) {
|
||||
const scrollY = document.documentElement.style.getPropertyValue('--scroll-y');
|
||||
const body = document.body;
|
||||
body.style.position = 'fixed';
|
||||
body.style.top = `-${scrollY}`;
|
||||
} else {
|
||||
const body = document.body;
|
||||
const scrollY = body.style.top;
|
||||
body.style.position = '';
|
||||
body.style.top = '';
|
||||
window.scrollTo(0, parseInt(scrollY || '0') * -1);
|
||||
}
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
// Don't update UI if browser tab isn't visible
|
||||
if (content !== undefined && (document.hidden || !shown)) {
|
||||
return;
|
||||
}
|
||||
content = await get_logs();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if shown}
|
||||
<div
|
||||
class="fixed left-5 right-5 top-5 bottom-5 z-50 bg-white border border-white rounded-md
|
||||
flex flex-col p-2 drop-shadow"
|
||||
>
|
||||
<div class="flex h-20 justify-between items-center p-1">
|
||||
<span class="text-2xl mb-2">Log</span>
|
||||
<button onclick={() => (shown = false)} aria-label="close">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M5.29289 5.29289C5.68342 4.90237 6.31658 4.90237 6.70711 5.29289L12 10.5858L17.2929 5.29289C17.6834 4.90237 18.3166 4.90237 18.7071 5.29289C19.0976 5.68342 19.0976 6.31658 18.7071 6.70711L13.4142 12L18.7071 17.2929C19.0976 17.6834 19.0976 18.3166 18.7071 18.7071C18.3166 19.0976 17.6834 19.0976 17.2929 18.7071L12 13.4142L6.70711 18.7071C6.31658 19.0976 5.68342 19.0976 5.29289 18.7071C4.90237 18.3166 4.90237 17.6834 5.29289 17.2929L10.5858 12L5.29289 6.70711C4.90237 6.31658 4.90237 5.68342 5.29289 5.29289Z"
|
||||
fill="#0F1729"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="bg-gray-100 border border-gray-100 rounded-md overflow-scroll">
|
||||
<pre class="m-2">{content}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -91,6 +91,7 @@
|
||||
<DeleteButton
|
||||
prompt={`Are you sure you want to delete entry ${entry.name}?`}
|
||||
url={entry.get_delete_url()}
|
||||
name={entry.name}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
<DeleteButton
|
||||
prompt={`Are you sure you want to delete entry ${entry.name}?`}
|
||||
url={entry.get_delete_url()}
|
||||
name={entry.name}
|
||||
/>
|
||||
</td>
|
||||
{/if}
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
variant="blue"
|
||||
onclick={handleReAnalyze}
|
||||
ariaLabel="re-analyze"
|
||||
errorMessage="Error re-analyzing recoding"
|
||||
>
|
||||
{#snippet icon()}
|
||||
<svg style="width:20px;height:20px" viewBox="0 0 24 24">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import ApiRequestButton from './ApiRequestButton.svelte';
|
||||
|
||||
let {
|
||||
server_is_recording,
|
||||
}: {
|
||||
@@ -10,7 +9,12 @@
|
||||
|
||||
<div>
|
||||
{#if server_is_recording}
|
||||
<ApiRequestButton url="/api/stop-recording" label="Stop" variant="red">
|
||||
<ApiRequestButton
|
||||
url="/api/stop-recording"
|
||||
label="Stop"
|
||||
variant="red"
|
||||
errorMessage="Error stoppping recording"
|
||||
>
|
||||
{#snippet icon()}
|
||||
<svg
|
||||
class="w-6 h-6 text-white"
|
||||
@@ -28,7 +32,12 @@
|
||||
{/snippet}
|
||||
</ApiRequestButton>
|
||||
{:else}
|
||||
<ApiRequestButton url="/api/start-recording" label="Start" variant="blue">
|
||||
<ApiRequestButton
|
||||
url="/api/start-recording"
|
||||
label="Start"
|
||||
variant="blue"
|
||||
errorMessage="Error starting recording"
|
||||
>
|
||||
{#snippet icon()}
|
||||
<svg
|
||||
class="w-6 h-6 text-white"
|
||||
|
||||
@@ -7,6 +7,32 @@
|
||||
} = $props();
|
||||
|
||||
const table_cell_classes = 'border p-1 lg:p-2';
|
||||
|
||||
let battery_level = $derived(stats.battery_status ? stats.battery_status.level : 0);
|
||||
let bar_color = $derived.by(() => {
|
||||
if (stats.battery_status === undefined) {
|
||||
return '';
|
||||
}
|
||||
if (battery_level <= 10) {
|
||||
return 'fill-red-500';
|
||||
}
|
||||
if (battery_level <= 25) {
|
||||
return 'fill-yellow-300';
|
||||
}
|
||||
return 'fill-green-500';
|
||||
});
|
||||
let title_text = $derived.by(() => {
|
||||
if (stats.battery_status === undefined) {
|
||||
return 'Rayhunter does not yet support displaying the battery level for this device.';
|
||||
}
|
||||
|
||||
let text = `Battery is ${stats.battery_status.level}% full`;
|
||||
|
||||
if (stats.battery_status.is_plugged_in) {
|
||||
text += ' and plugged in';
|
||||
}
|
||||
return text;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -32,6 +58,64 @@
|
||||
Free: {stats.memory_stats.free}, Used: {stats.memory_stats.used}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b">
|
||||
<th class={table_cell_classes}> Battery </th>
|
||||
<td class={table_cell_classes}>
|
||||
<svg
|
||||
width="80"
|
||||
height="30"
|
||||
viewBox="0 0 80 30"
|
||||
role="img"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="battery-icon"
|
||||
>
|
||||
<title>{title_text}</title>
|
||||
<!-- Battery body -->
|
||||
<rect
|
||||
class="fill-none stroke-neutral-800 stroke-2"
|
||||
width="70"
|
||||
height="30"
|
||||
rx="3"
|
||||
ry="3"
|
||||
/>
|
||||
<!-- Battery terminal -->
|
||||
<rect
|
||||
class="fill-neutral-800"
|
||||
x="70"
|
||||
y="7"
|
||||
width="8"
|
||||
height="16"
|
||||
rx="2"
|
||||
ry="2"
|
||||
/>
|
||||
<!-- Battery charge bar -->
|
||||
<rect
|
||||
class={bar_color}
|
||||
x="2"
|
||||
y="2"
|
||||
height="26"
|
||||
rx="2"
|
||||
ry="2"
|
||||
style="width: {battery_level * 0.66}px;"
|
||||
/>
|
||||
{#if stats.battery_status && stats.battery_status.is_plugged_in}
|
||||
<!-- Lightning bolt icon -->
|
||||
<path
|
||||
class="fill-yellow-300 stroke-neutral-800 stroke-1"
|
||||
d="M38 3 L28 17 L34 17 L30 27 L40 13 L34 13 Z"
|
||||
/>
|
||||
{/if}
|
||||
{#if !stats.battery_status}
|
||||
<!-- Question mark icon -->
|
||||
<text
|
||||
class="fill-neutral-500 text-[20px] font-bold [text-anchor:middle] [dominant-baseline:central]"
|
||||
x="35"
|
||||
y="15">?</text
|
||||
>
|
||||
{/if}
|
||||
</svg>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ export interface SystemStats {
|
||||
disk_stats: DiskStats;
|
||||
memory_stats: MemoryStats;
|
||||
runtime_metadata: RuntimeMetadata;
|
||||
battery_status?: BatteryStatus;
|
||||
}
|
||||
|
||||
export interface RuntimeMetadata {
|
||||
@@ -24,3 +25,8 @@ export interface MemoryStats {
|
||||
used: string;
|
||||
free: string;
|
||||
}
|
||||
|
||||
export interface BatteryStatus {
|
||||
level: number;
|
||||
is_plugged_in: boolean;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { add_error } from './action_errors.svelte';
|
||||
import { Manifest } from './manifest.svelte';
|
||||
import type { SystemStats } from './systemStats';
|
||||
|
||||
@@ -31,6 +32,23 @@ export async function req(method: string, url: string): Promise<string> {
|
||||
}
|
||||
}
|
||||
|
||||
// A wrapper around req that reports errors to the UI
|
||||
export async function user_action_req(
|
||||
method: string,
|
||||
url: string,
|
||||
error_msg: string
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
return await req(method, url);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.log('beeeo');
|
||||
add_error(error, error_msg);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function get_manifest(): Promise<Manifest> {
|
||||
const manifest_json = JSON.parse(await req('GET', '/api/qmdl-manifest'));
|
||||
return new Manifest(manifest_json);
|
||||
@@ -40,6 +58,10 @@ export async function get_system_stats(): Promise<SystemStats> {
|
||||
return JSON.parse(await req('GET', '/api/system-stats'));
|
||||
}
|
||||
|
||||
export async function get_logs(): Promise<string> {
|
||||
return await req('GET', '/api/log');
|
||||
}
|
||||
|
||||
export async function get_config(): Promise<Config> {
|
||||
return JSON.parse(await req('GET', '/api/config'));
|
||||
}
|
||||
|
||||
@@ -7,34 +7,97 @@
|
||||
import { AnalysisManager } from '$lib/analysisManager.svelte';
|
||||
import SystemStatsTable from '$lib/components/SystemStatsTable.svelte';
|
||||
import DeleteAllButton from '$lib/components/DeleteAllButton.svelte';
|
||||
import RecordingControls from '$lib/components//RecordingControls.svelte';
|
||||
import RecordingControls from '$lib/components/RecordingControls.svelte';
|
||||
import ConfigForm from '$lib/components/ConfigForm.svelte';
|
||||
import ActionErrors from '$lib/components/ActionErrors.svelte';
|
||||
import LogView from '$lib/components/LogView.svelte';
|
||||
|
||||
let manager: AnalysisManager = new AnalysisManager();
|
||||
let loaded = $state(false);
|
||||
let entries: ManifestEntry[] = $state([]);
|
||||
let current_entry: ManifestEntry | undefined = $state(undefined);
|
||||
let system_stats: SystemStats | undefined = $state(undefined);
|
||||
let update_error: string | undefined = $state(undefined);
|
||||
let logview_shown: boolean = $state(false);
|
||||
$effect(() => {
|
||||
const interval = setInterval(async () => {
|
||||
await manager.update();
|
||||
let new_manifest = await get_manifest();
|
||||
await new_manifest.set_analysis_status(manager);
|
||||
entries = new_manifest.entries;
|
||||
current_entry = new_manifest.current_entry;
|
||||
try {
|
||||
// Don't update UI if browser tab isn't visible
|
||||
if (document.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
system_stats = await get_system_stats();
|
||||
loaded = true;
|
||||
await manager.update();
|
||||
let new_manifest = await get_manifest();
|
||||
await new_manifest.set_analysis_status(manager);
|
||||
entries = new_manifest.entries;
|
||||
current_entry = new_manifest.current_entry;
|
||||
|
||||
system_stats = await get_system_stats();
|
||||
update_error = undefined;
|
||||
loaded = true;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
update_error = error.message;
|
||||
} else {
|
||||
update_error = '';
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
</script>
|
||||
|
||||
<LogView bind:shown={logview_shown} />
|
||||
<div class="p-4 xl:px-8 bg-rayhunter-blue drop-shadow flex flex-row justify-between items-center">
|
||||
<!-- https://www.w3.org/WAI/tutorials/images/decorative/ -->
|
||||
<img src="/rayhunter_text.png" alt="" class="h-10 xl:h-12" />
|
||||
<div class="flex flex-row gap-4">
|
||||
<button onclick={() => (logview_shown = true)} class="flex flex-row gap-1 group">
|
||||
<span class="hidden text-white group-hover:text-gray-400 lg:flex">Logs</span>
|
||||
<svg
|
||||
class="w-6 h-6 text-white group-hover:text-gray-400"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M10 14H3"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M10 18H3"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M14 15L17.5 18L21 15"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M3 6L13.5 6M20 6L17.75 6"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M20 10L9.5 10M3 10H5.25"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<a
|
||||
class="flex flex-row gap-1 group"
|
||||
href="https://github.com/EFForg/rayhunter/issues"
|
||||
@@ -84,6 +147,41 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="m-4 xl:mx-8 flex flex-col gap-4">
|
||||
{#if update_error !== undefined}
|
||||
<div
|
||||
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">
|
||||
<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
|
||||
>This webpage is not currently receiving updates from your Rayhunter device. This
|
||||
could be do loss of connection or some issue with your device.</span
|
||||
>
|
||||
{#if update_error}
|
||||
<details>
|
||||
<summary>Error</summary>
|
||||
<code>{update_error}</code>
|
||||
</details>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<ActionErrors />
|
||||
{#if loaded}
|
||||
<div class="flex flex-col lg:flex-row gap-4">
|
||||
{#if current_entry}
|
||||
@@ -117,9 +215,9 @@
|
||||
</svg>
|
||||
WARNING: Not Running
|
||||
</span>
|
||||
<span
|
||||
>Rayhunter is not currently running and will not detect abnormal behavior!</span
|
||||
>
|
||||
<span>
|
||||
Rayhunter is not currently running and will not detect abnormal behavior!
|
||||
</span>
|
||||
<div class="flex flex-row justify-end mt-2">
|
||||
<RecordingControls server_is_recording={!!current_entry} />
|
||||
</div>
|
||||
|
||||
+2
-1
@@ -10,9 +10,10 @@
|
||||
- [Uninstalling](./uninstalling.md)
|
||||
- [Using Rayhunter](./using-rayhunter.md)
|
||||
- [Rayhunter's heuristics](./heuristics.md)
|
||||
- [Re-analyzing recordings](./reanalyzing.md)
|
||||
- [How we analyze a capture](./analyzing-a-capture.md)
|
||||
- [Supported devices](./supported-devices.md)
|
||||
- [Orbic RC400L](./orbic.md)
|
||||
- [Orbic/Kajeet RC400L](./orbic.md)
|
||||
- [TP-Link M7350](./tplink-m7350.md)
|
||||
- [TP-Link M7310](./tplink-m7310.md)
|
||||
- [Tmobile TMOHS1](./tmobile-tmohs1.md)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# How we analyze a capture
|
||||
|
||||
TODO
|
||||
Teams of highly trained squirrles. Video coming soon!
|
||||
+8
-6
@@ -2,23 +2,25 @@
|
||||
|
||||
### Do I need an active SIM card to use Rayhunter?
|
||||
|
||||
**It Depends**. Operation of Rayhunter does require the insertion of a SIM card into the device, but whether that SIM card has to be currently active for our tests to work is still under investigation. If you want to use the device as a hotspot in addition to a research device an active plan would of course be necessary, however we have not done enough testing yet to know whether an active subscription is required for detection. If you want to test the device with an inactive SIM card, we would certainly be interested in seeing any data you collect, and especially any runs that trigger an alert!
|
||||
**It Depends**. Operation of Rayhunter does require the insertion of a SIM card into the device, but that sim card does not have to be actively registered with a service plan. If you want to use the device as a hotspot in addition to a research device, or get [notifications](./configuration.md), an active plan would of course be necessary.
|
||||
|
||||
### How can I test that my device is working?
|
||||
You can enable the `Test Heuristic` under `Analyzer Heuristic Settings` in the config section on your web dashboard. This will cause an alert to trigger every time your device sees a cell tower, you might need to reboot your device or move around a bit to get this one to trigger, but it will be very noisey once it does. People have also tested it by building IMSI catchers at home, but we don't reccomend that, since it violates FCC regulations and will probably upset your neighbors.
|
||||
|
||||
<a name="red"></a>
|
||||
|
||||
### Help, Rayhunter's line is red! What should I do?
|
||||
### Help, Rayhunter's line is red/orange/yellow/dotted/dashed! What should I do?
|
||||
|
||||
Unfortunately, the circumstances that might lead to a positive cell site simulator (CSS) signal are quite varied, so we don't have a universal recommendation for how to deal with the a positive signal. Depending on your circumstances and threat model, you may want to turn off your phone until you are out of the area (or put it on airplane mode) and tell your friends to do the same!
|
||||
Unfortunately, the circumstances that might lead to a positive cell site simulator (CSS) signal are quite varied, so we don't have a universal recommendation for how to deal with the a positive signal. Depending on your circumstances and threat model, you may want to turn off your phone until you are out of the area and tell your friends to do the same!
|
||||
|
||||
If you've received a Rayhunter warning and would like to help us with our research, please send your Rayhunter data captures (QMDL and PCAP logs) to us at our [Signal](https://signal.org/) username [**ElectronicFrontierFoundation.90**](https://signal.me/#eu/HZbPPED5LyMkbTxJsG2PtWc2TXxPUR1OxBMcJGLOPeeCDGPuaTpOi5cfGRY6RrGf) with the following information: capture date, capture location, device, device model, and Rayhunter version. If you're unfamiliar with Signal, feel free to check out our [Security Self Defense guide on it](https://ssd.eff.org/module/how-to-use-signal).
|
||||
If you've received a Rayhunter warning and would like to help us with our research, please send your Rayhunter data captures (Zip file downloaded from the web interface) to us at our [Signal](https://signal.org/) username [**ElectronicFrontierFoundation.90**](https://signal.me/#eu/HZbPPED5LyMkbTxJsG2PtWc2TXxPUR1OxBMcJGLOPeeCDGPuaTpOi5cfGRY6RrGf) with the following information: capture date, capture location, device, device model, and Rayhunter version. If you're unfamiliar with Signal, feel free to check out our [Security Self Defense guide on it](https://ssd.eff.org/module/how-to-use-signal).
|
||||
|
||||
Please note that this file may contain sensitive information such as your IMSI and the unique IDs of cell towers you were near which could be used to ascertain your location at the time.
|
||||
|
||||
|
||||
### Should I get a locked or unlocked orbic device? What is the difference?
|
||||
|
||||
If you want to use a non-Verizon SIM card you will probably need an unlocked device. But it's not clear how locked the locked devices are nor how to unlock them, we welcome any experimentation and information regarding the use of unlocked devices.
|
||||
|
||||
If you want to use a non-Verizon SIM card you will probably need an unlocked device. But it's not clear which devices are locked nor how to unlock them, we welcome any experimentation and information regarding the use of unlocked devices. So far most verizon branded orbic devices we have encountered are actually unlocked.
|
||||
|
||||
### How do I re-enable USB tethering after installing Rayhunter?
|
||||
|
||||
|
||||
+26
-7
@@ -4,23 +4,40 @@ Rayhunter includes several analyzers to detect potential IMSI catcher activity.
|
||||
|
||||
## Available Analyzers
|
||||
|
||||
### IMSI Requested
|
||||
### IMSI Requested (v3)
|
||||
|
||||
This analyser tests whether the eNodeB sends an IMSI Identity Request NAS message.
|
||||
This analyser tests whether the eNodeB sends an IMSI or IMEI Identity Request NAS message under suspicous .
|
||||
|
||||
Mobile network primarily requests IMSI number from mobile device during initial network attachment or when the network cannot identify the mobile device by its temporary identification (TMSI - *Temporary Mobile Subscriber Identity* or GUTI - *Globally Unique Temporary Identifier* in 4G/5G terminology).
|
||||
Mobile networks primarily request IMSI or IMEI from a mobile device during initial network attachment or when the network cannot identify the mobile device by its temporary identification (TMSI - *Temporary Mobile Subscriber Identity* or GUTI - *Globally Unique Temporary Identifier* in 4G/5G terminology).
|
||||
|
||||
IMSI request therefore usually happens when you first turn the device on especially after it has been off for a long time. Another possibility is, that you reboot your mobile device and your temporary ID expired. Sometimes temporary identification can expire if you have been in an area where there is absolutely no connection to your service provider or after you left your device on an airplane mode and then reconnect to the network (especially being disconnected for a long time). IMSI could also be requested when you connect to a new network (for instance for roaming), when you swap she SIM card or when your device moves to a new *Tracking Area* or *Location Area* and the network can not map the temporary identification to your device. IMSI number can also be requested after core network reboot.
|
||||
|
||||
It should also be noted that the network periodically reassigns your device new temporary identification to enhance security and avoid tracking, but in that cases usually does not request IMSI.
|
||||
|
||||
However, if you get this warning at a time when you have been steadily connected to towers and the device has been on for a while, this could be a sign of IMSI catcher use.
|
||||
During these events the phone will typically go on to authenticate that the network is legitimate and then establish service with the network it is connected to.
|
||||
|
||||
What we consider suspicious is the following chain of events:
|
||||
|
||||
* Phone connects to a new tower.
|
||||
* Tower asks for phones identity (IMEI or IMSI.)
|
||||
* Authentication does *NOT* happen.
|
||||
* Tower requests phoen to disconnect.
|
||||
|
||||
Looking for this chain of events is much less prone to false positives than naively looking for any time the IMSI/IMEI is sent. We do still sometimes get false positives when users are in an airplane that is coming in for a landing however. This is likely do to having been disconnected for a while and then being over towers that are not able to route to your home network, but we are still researching.
|
||||
|
||||
This is the attack used by commercial IMSI catchers used by law enforcement.
|
||||
|
||||
This heuristic will also alert you if any of the following happen:
|
||||
* Identity is requested after authentication.
|
||||
* Identity is requested without your phone connecting to the tower.
|
||||
* Identity is requested and then authentication doesn't happen shortly thereafter.
|
||||
|
||||
This heuristic will also issue a notification every time your identity is sent to the network under non suspicious circumstances. This is for diagnostic purposes.
|
||||
|
||||
### Connection Release/Redirected Carrier 2G Downgrade
|
||||
|
||||
This analyser tests if a base station releases your device's connection and redirects your device to a 2G base station. This heuristics is useful, because many commercial IMSI catchers operate in a such way that they downgrade connection to 2G where they can intercept the communication (by performing man-in-the-middle attack).
|
||||
This analyser tests if a base station releases your device's connection and redirects your device to a 2G base station. This heuristic is useful, because some IMSI catchers may operate in a such way that they downgrade connection to 2G where they can intercept the communication (by performing man-in-the-middle attack).
|
||||
|
||||
This heuristic is the most useful in the United States or other countries where there are no more operating 2G base stations. See [Wikipedia page on past 2G networks](https://en.wikipedia.org/wiki/2G#Past_2G_networks) for information about your country. In countries where 2G is still in service (such as most of EU), this heuristics may trigger false positives. In that case you should consider disabling it. However this heuristics has been vastly improved to reduce false positive warnings and new tests in European networks show that false positives are vastly reduced.
|
||||
|
||||
### LTE SIB6/7 Downgrade
|
||||
|
||||
@@ -28,10 +45,12 @@ This analyser tests if LTE base station is broadcasting a SIB type 6 and 7 messa
|
||||
|
||||
SIB (*System Information Block*) Type 6 and 7 are specific types of broadcast messages sent by the base station (eNodeB in 4G networks) to mobile devices. They contain essential radio-related configuration parameters to help mobile device perform cell reselection.
|
||||
|
||||
IMSI catchers exploit the fact that many SIB broadcast messages are not encrypted or authenticated. This allows them to pretend to be a legitimate cell by broadcasting fake system information in order to force mobile devices to downgrade from more secure 4G (LTE) to less secure 2G (GSM) network and then steal IMSI and/or perform man-in-the-middle attack. That is why this is also called a downgrade attack.
|
||||
This attack exploits the fact that SIB broadcast messages are not encrypted or authenticated. This allows them to pretend to be a legitimate cell by broadcasting fake system information in order to force mobile devices to downgrade from more secure 4G (LTE) to less secure 2G (GSM) network and then steal IMSI and/or perform man-in-the-middle attack. That is why this is also called a downgrade attack.
|
||||
|
||||
SIB6 is used for cell reselecion to CDMA2000 systems which are not supported by many modern mobile phones, and SIB7 Provides the mobile device with information to perform cell reselection to GSM/EDGE networks. Therefore SIB6 messages are quite rare, while malformed SIB7 messages are much more frequent in practice.
|
||||
|
||||
This heuristic is the most useful in the United States or other countries where there are no more operating 2G base stations. See [Wikipedia page on past 2G networks](https://en.wikipedia.org/wiki/2G#Past_2G_networks) for information about your country. In countries where 2G is still in service (such as most of EU), this heuristics may trigger false positives. In that case you should consider disabling it. However this heuristics has been vastly improved to reduce false positive warnings and new tests in European networks show that false positives are vastly reduced.
|
||||
|
||||
### Null Cipher
|
||||
|
||||
This analyser tests whether the cell suggests using a null cipher (EEA0) in the RRC layer. That means that encryption between your mobile device and base station is turned off.
|
||||
|
||||
@@ -14,7 +14,7 @@ Windows support in Rayhunter's installer is a work-in-progress. Depending on the
|
||||
|
||||
<div class=warning><strong>
|
||||
|
||||
[The Windows installer is known to be buggy](https://github.com/EFForg/rayhunter/issues/366). Consider using the [Network-based installer](./orbic.md#the-network-installer).
|
||||
[The Windows USB installer is known to be buggy](https://github.com/EFForg/rayhunter/issues/366). We strongly reccomend using the [Network-based installer](./orbic.md#the-network-installer).
|
||||
|
||||
</strong></div>
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ Make sure you've got one of Rayhunter's [supported devices](./supported-devices.
|
||||
4. Turn on your device by holding the power button on the front.
|
||||
|
||||
* For the Orbic, connect the device using a USB-C cable.
|
||||
* Or connect to the network if using the network based installer, this is especially reccomended on Windows.
|
||||
* For TP-Link, connect to its network using either WiFi or USB Tethering.
|
||||
|
||||
5. Run the installer:
|
||||
@@ -32,8 +33,7 @@ Make sure you've got one of Rayhunter's [supported devices](./supported-devices.
|
||||
Then run the installer:
|
||||
```bash
|
||||
./installer orbic
|
||||
# or: ./installer tplink
|
||||
# or: ./installer wingtech
|
||||
# or: ./installer [orbic-network|tplink|tmobile|uz801|pinephone|wingtech]
|
||||
```
|
||||
|
||||
The device will restart multiple times over the next few minutes.
|
||||
@@ -44,6 +44,8 @@ Make sure you've got one of Rayhunter's [supported devices](./supported-devices.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
* You can test your device by enabling the test heuristic. This will be very noisy and fire an alert every time you see a new tower. Be sure to turn it off when you are done testing.
|
||||
|
||||
* On MacOS if you encounter an error that says "No Orbic device found," it may because you have the "Allow accessories to connect" security setting set to "Ask for approval." You may need to temporarily change it to "Always" for the script to run. Make sure to change it back to a more secure setting when you're done.
|
||||
|
||||
```bash
|
||||
|
||||
+6
-3
@@ -2,11 +2,14 @@
|
||||
|
||||
<img style="display: block; margin: 0 auto" alt="Rayhunter Logo - An Orca taking a bite out of a cellular signal bar" src="https://www.eff.org/files/styles/media_browser_preview/public/banner_library/rayhunter-banner.png" />
|
||||
|
||||
Rayhunter is a project for detecting IMSI catchers, also known as cell-site simulators or stingrays. It's designed to run on a cheap mobile hotspot called the Orbic RC400L, but thanks to community efforts can [support some other devices as well](./supported-devices.md).
|
||||
|
||||
Rayhunter is a project for detecting IMSI catchers, also known as cell-site simulators or stingrays. It was first designed to run on a cheap mobile hotspot called the Orbic RC400L, but thanks to community efforts can [support some other devices as well](./supported-devices.md).
|
||||
It's also designed to be as easy to install and use as possible, regardless of your level of technical skills. This guide should provide you all you need to acquire a compatible device, install Rayhunter, and start catching IMSI catchers.
|
||||
|
||||
To learn more about the aim of the project, and about IMSI catchers in general, please check out our [introductory blog post](https://www.eff.org/deeplinks/2025/03/meet-rayhunter-new-open-source-tool-eff-detect-cellular-spying). Otherwise, check out the [installation guide](./installation.md) to get started.
|
||||
→ Check out the [installation guide](./installation.md) to get started.
|
||||
|
||||
→ To learn more about the aim of the project, and about IMSI catchers in general, please check out our [introductory blog post](https://www.eff.org/deeplinks/2025/03/meet-rayhunter-new-open-source-tool-eff-detect-cellular-spying).
|
||||
|
||||
→ For discussion, help, or to join the mattermost channel and get involved with the project and community check out the [many ways listed here](./support-feedback-community.md)!
|
||||
|
||||
**LEGAL DISCLAIMER:** Use this program at your own risk. We believe running this program does not currently violate any laws or regulations in the United States. However, we are not responsible for civil or criminal liability resulting from the use of this software. If you are located outside of the US please consult with an attorney in your country to help you assess the legal risks of running this program.
|
||||
|
||||
|
||||
+6
-2
@@ -1,7 +1,9 @@
|
||||
# Orbic RC400L
|
||||
# Orbic/Kajeet RC400L
|
||||
|
||||
The Orbic RC400L is an inexpensive LTE modem primarily designed for the US market, and the original device for which Rayhunter is developed.
|
||||
|
||||
It is also sometimes sold under the brand Kajeet RC400L. This is the exact same hardware and can be treated the same.
|
||||
|
||||
You can buy an Orbic [using bezos
|
||||
bucks](https://www.amazon.com/Orbic-Verizon-Hotspot-Connect-Enabled/dp/B08N3CHC4Y),
|
||||
or on [eBay](https://www.ebay.com/sch/i.html?_nkw=orbic+rc400l).
|
||||
@@ -27,13 +29,15 @@ procedure at `./installer orbic-network` that is supposed to eventually replace
|
||||
identically on Windows, Mac and Linux. From our testing it works much more
|
||||
reliably on Windows than `./installer orbic` does.
|
||||
|
||||
The drawback is that the device's admin password is required. If you have a Kajeet-branded "SmartSpot" you currently have to use the USB-based `./installer orbic`, as we currently don't know of a way to get that admin password.
|
||||
The drawback is that the device's admin password is required.
|
||||
|
||||
1. Connect to the Orbic's network via WiFi or USB tethering
|
||||
2. Run `./installer orbic-network`
|
||||
3. The installer will ask you to log into the admin UI on `localhost:4000`. The password for that is the same as the WiFi password.
|
||||
4. As soon as you're logged in, the installer will continue and reboot the device.
|
||||
|
||||
*note*: On Kajeet devices the default admin password is `$m@rt$p0tc0nf!g`, on most other orbic devices the default admin password is the same as the wifi password. If the password has been changed you can reset it by pressing the button under the back case until the unit restarts.
|
||||
|
||||
## Obtaining a shell
|
||||
|
||||
After running through the installation procedure, you can obtain a root shell
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# Re-analyzing recordings
|
||||
|
||||
Every once in a while, Rayhunter refines its heuristics to detect more kinds of
|
||||
suspicious behavior, and to reduce noise from incorrect alerts.
|
||||
|
||||
This means that your old green recordings may actually contain data that is now
|
||||
deemed suspicious, and also old red recordings may become green.
|
||||
|
||||
You can re-analyze any old recording inside of Rayhunter by clicking on "N
|
||||
warnings" to expand details, then clicking the "re-analyze" button.
|
||||
|
||||
## Analyzing recordings on Desktop
|
||||
|
||||
If you have a PCAP or QMDL file but no rayhunter, you can analyze it on desktop
|
||||
using the `rayhunter-check` CLI tool. That tool contains the same heuristics as
|
||||
Rayhunter and will also work on traffic data captured with other tools, such as
|
||||
QCSuper.
|
||||
|
||||
Since, 0.6.1, `rayhunter-check` is included in the release zipfile.
|
||||
|
||||
You can build `rayhunter-check` from source with the following command:
|
||||
`cargo build --bin rayhunter-check`
|
||||
|
||||
## Usage
|
||||
```sh
|
||||
rayhunter-check [OPTIONS] --path <PATH>
|
||||
|
||||
Options:
|
||||
-p, --path <PATH> Path to the PCAP, or QMDL file. If given a directory will
|
||||
recursively scan all pcap, qmdl, and subdirectories
|
||||
-P, --pcapify Turn QMDL file into PCAP
|
||||
--show-skipped Show skipped messages
|
||||
-q, --quiet Print only warnings
|
||||
-d, --debug Print debug info
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
```
|
||||
### Examples
|
||||
`rayhunter-check -p ~/Downloads/myfile.qmdl`
|
||||
|
||||
`rayhunter-check -p ~/Downloads/myfile.pcap`
|
||||
|
||||
`rayhunter-check -p ~/Downloads #Check all files in downloads`
|
||||
|
||||
`rayhunter-check -d -p ~/Downloads/myfile.qmdl #run in debug mode`
|
||||
@@ -7,7 +7,7 @@ These devices have been extensively tested by the core developers and are widely
|
||||
|
||||
| Device | Recommended region |
|
||||
| ------ | ------ |
|
||||
| [Orbic RC400L](./orbic.md) | Americas |
|
||||
| [Orbic RC400L](./orbic.md) Sometimes also branded Kajeet RC400L | Americas |
|
||||
| [TP-Link M7350](./tplink-m7350.md) | Africa, Europe, Middle East |
|
||||
|
||||
The TP-Link M7350 also works in the Americas but is usually more expensive.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Using Rayhunter
|
||||
|
||||
Once installed, Rayhunter will run automatically whenever your device is running. You'll see a green line on top of the device's display to indicate that it's running and recording. [The line will turn red](./faq.md#red) once a potential IMSI catcher has been found, until the device is rebooted or a new recording is started through the web UI.
|
||||
Once installed, Rayhunter will run automatically whenever your device is running. You'll see a green line on top of the device's display to indicate that it's running and recording. [The line will turn yellow dots, orange dashes, or solid red](./faq.md#red) once a potential IMSI catcher has been found, depending on the severity of the alert, until the device is rebooted or a new recording is started through the web UI.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "installer"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
@@ -26,12 +26,12 @@ tokio-stream = "0.1.17"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies.adb_client]
|
||||
git = "https://github.com/EFForg/adb_client.git"
|
||||
rev = "e511662394e4fa32865c154c40f81a3d846f700c"
|
||||
rev = "208a302367727554d7530e937ca8aee20a74fa51"
|
||||
default-features = false
|
||||
features = ["trans-nusb"]
|
||||
|
||||
[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies.adb_client]
|
||||
git = "https://github.com/EFForg/adb_client.git"
|
||||
rev = "e511662394e4fa32865c154c40f81a3d846f700c"
|
||||
rev = "208a302367727554d7530e937ca8aee20a74fa51"
|
||||
default-features = false
|
||||
features = ["trans-libusb"]
|
||||
|
||||
@@ -246,7 +246,7 @@ async fn get_adb() -> Result<ADBUSBDevice> {
|
||||
const MAX_FAILURES: u32 = 10;
|
||||
let mut failures = 0;
|
||||
loop {
|
||||
match ADBUSBDevice::new(VENDOR_ID, PRODUCT_ID) {
|
||||
match ADBUSBDevice::new_no_auth(VENDOR_ID, PRODUCT_ID) {
|
||||
Ok(dev) => match adb_echo_test(dev).await {
|
||||
Ok(dev) => return Ok(dev),
|
||||
Err(e) => {
|
||||
|
||||
@@ -22,7 +22,7 @@ pub async fn install() -> Result<()> {
|
||||
echo!("Unlocking modem ... ");
|
||||
start_adb().await?;
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
let mut adb = ADBUSBDevice::new(USB_VENDOR_ID, USB_PRODUCT_ID).unwrap();
|
||||
let mut adb = ADBUSBDevice::new_no_auth(USB_VENDOR_ID, USB_PRODUCT_ID).unwrap();
|
||||
println!("ok");
|
||||
|
||||
adb.run_command(&["mount", "-o", "remount,rw", "/"], "exit code 0")?;
|
||||
@@ -57,7 +57,7 @@ pub async fn install() -> Result<()> {
|
||||
echo!("Unlocking modem ... ");
|
||||
start_adb().await?;
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
let mut adb = ADBUSBDevice::new(USB_VENDOR_ID, USB_PRODUCT_ID).unwrap();
|
||||
let mut adb = ADBUSBDevice::new_no_auth(USB_VENDOR_ID, USB_PRODUCT_ID).unwrap();
|
||||
println!("ok");
|
||||
|
||||
echo!("Testing rayhunter ... ");
|
||||
|
||||
@@ -94,7 +94,7 @@ async fn wait_for_adb() -> Result<ADBUSBDevice> {
|
||||
|
||||
// UZ801 USB vendor and product IDs.
|
||||
// TODO: Research if other variants use different IDs.
|
||||
match ADBUSBDevice::new(0x05c6, 0x90b6) {
|
||||
match ADBUSBDevice::new_no_auth(0x05c6, 0x90b6) {
|
||||
Ok(mut device) => {
|
||||
// Test ADB connection
|
||||
if test_adb_connection(&mut device).await.is_ok() {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rayhunter"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
edition = "2024"
|
||||
description = "Realtime cellular data decoding and analysis for IMSI catcher detection"
|
||||
|
||||
|
||||
@@ -56,15 +56,25 @@ impl ImsiRequestedAnalyzer {
|
||||
self.timeout_counter = 0;
|
||||
}
|
||||
|
||||
// IMSI or IMEI requested after auth accept
|
||||
(State::AuthAccept, State::IdentityRequest) => {
|
||||
self.flag = Some(Event {
|
||||
event_type: EventType::High,
|
||||
message: format!(
|
||||
"Identity requested after auth request (frame {})",
|
||||
self.packet_num
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
// Unexpected IMSI without AttachRequest
|
||||
(current, State::IdentityRequest) if *current != State::AttachRequest => {
|
||||
(State::Disconnect, State::IdentityRequest) => {
|
||||
self.flag = Some(Event {
|
||||
event_type: EventType::High,
|
||||
message: format!(
|
||||
"Identity requested without Attach Request (frame {})",
|
||||
self.packet_num
|
||||
)
|
||||
.to_string(),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -75,6 +85,17 @@ impl ImsiRequestedAnalyzer {
|
||||
message: format!(
|
||||
"Disconnected after Identity Request without Auth Accept (frame {})",
|
||||
self.packet_num
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
// Notify on any identity reqeust (IMEI or IMSI)
|
||||
(_, State::IdentityRequest) => {
|
||||
self.flag = Some(Event {
|
||||
event_type: EventType::Informational,
|
||||
message: format!(
|
||||
"Identity Request happened but its not suspicious yet. (frame {})",
|
||||
self.packet_num
|
||||
)
|
||||
.to_string(),
|
||||
});
|
||||
@@ -105,7 +126,7 @@ impl Analyzer for ImsiRequestedAnalyzer {
|
||||
}
|
||||
|
||||
fn get_version(&self) -> u32 {
|
||||
2
|
||||
3
|
||||
}
|
||||
|
||||
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rootshell"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "telcom-parser"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
Reference in New Issue
Block a user