82 Commits

Author SHA1 Message Date
oopsbagel
431a97ca65 chore: bump all Cargo.toml versions to 0.2.6 2025-03-25 17:02:01 -07:00
Will Greenberg
0364bfbc98 bump version number
we uhh forgot to do this for every release lol
2025-03-25 16:53:20 -07:00
Ben Brown
996e47684c Fix typo on readme
sensetive -> sensitive
2025-03-25 16:52:16 -07:00
Cooper Quintin
266f2b2e53 more nesting 2025-03-25 16:49:08 -07:00
Will Greenberg
2080cd7845 web ui: fix issue causing no entries
We weren't correctly handling all possible events from the heuristics
list
2025-03-25 16:49:08 -07:00
oopsbagel
9af8e006b0 fix(serial): use tokio's timeout with USB bulk in/out
Replace futures_lite::future::block_on (which will block indefinitely) with
tokio::time::timeout to restore the original behaviour of this utility, where
communication over USB interface bulk endpoints times out after 1 second.
2025-03-25 16:46:35 -07:00
oopsbagel
e841e22774 refactor(serial): replace rusb with nusb
nusb is a pure Rust library providing the same low level access to USB devices
that rusb/libusb provide.

This commit removes rusb (and thus the dependence on libusb) and replaces it
with nusb in the serial utility.

The only functional change is that nusb does not support timeouts for bulk data
commands. nusb is async. This commit contains a naïve implementation that simply
blocks on bulk reads and writes in send_command().
2025-03-25 16:46:35 -07:00
Will Greenberg
0d9f53f602 Update make.sh
reboot the orbic instead of starting up the process again, since rootshell seems to have insufficient privileges to start rayhunter
2025-03-25 16:34:23 -07:00
Will Greenberg
c9dcbbe5d6 daemon: if we fail to parse the QMDL manifest, make a new one
If rayhunter doesn't exit cleanly (e.g. during a battery outage), the
QMDL manifest may end up in a corrupted state. If that's the case,
rayhunter should try to recover by creating a new manifest. This'll let
it continue, and will preserve previous recordings, but they won't be
visible through the UI.
2025-03-25 15:36:12 -07:00
Will Greenberg
61d6ff6510 Add an update section 2025-03-25 15:14:54 -07:00
Will Greenberg
e79dc4a8f0 lib: diable null-cipher heuristic due to false positives
Due to an upstream hampi bug (https://github.com/ystero-dev/hampi/issues/133),
our RRC parser is reporting false-positives for the null cipher
heuristic.
2025-03-25 15:13:36 -07:00
Will Greenberg
6204bc0195 update installer script for macOS Intel 2025-03-24 16:42:58 -07:00
Will Greenberg
65b9843e39 test macOS intel builds 2025-03-24 16:42:58 -07:00
Sashanoraa
d0d01089dd Fix various clippy warnings
This commit fixes various clippy warnings that do not affect the
function of the code and aren't stylistic in nature.
2025-03-24 13:47:20 -07:00
Sashanoraa
9c26e89b24 Modify config load to use serde default
This commit refactors the config loading code to no longer require a
separate ConfigFile struct by taking advantage of serde's `default`
attribute. This causes serde to use the Config struct's default value
for that attribute for any missing attributes, which is what the
existing code was doing anyway.

This also fixes several clippy warnings.

Serde docs: https://serde.rs/container-attrs.html#default
2025-03-24 13:47:20 -07:00
Sashanoraa
1f4786db19 Have rootshell print errors and exit 1 if exec fails
Previously was ignoring the possible error retuned by exec, this commit
has rootshell print the error if exec returns and have the process exit
with a code of 1 instead of 0.
2025-03-24 13:47:20 -07:00
Kirk Strauser
88f81d86fa Remove the quarantine bit from the serial command on macOS 2025-03-20 10:49:07 -07:00
oopsbagel
0b3c0de481 fix(lib/util): use better names for runtime metadata
- document RuntimeMetadata fields
- rename RayhunterMetadata to RuntimeMetadata
- rename RuntimeMetadata.os to RuntimeMetadata.system_os
- remove unpopulated hardware field
- remove unnecessary duplication of datastructure in analyzer harness
2025-03-19 11:48:54 -07:00
oopsbagel
188e9f436b fix(qmdl-manifest): store os/arch/hardware in qmdl manifest.toml
Do not superfluously prefix these names with rayhunter_, as they describe the
hardware and not the binary.
2025-03-19 11:48:54 -07:00
oopsbagel
f2b5aa2743 feat: show rayhunter version/os/arch in pcap, ndjson, qmdl manifest
Create a util mod to provide information about the rayhunter binary and
system.
2025-03-19 11:48:54 -07:00
oopsbagel
b785a7f21c feat(qmdl): add rayhunter version and os to manifest.toml 2025-03-19 11:48:54 -07:00
oopsbagel
09d35ccec7 feat(pcap): add operating system kernel name and release
Display the uname sysname and release as the OS option in the pcap Section
Header Block, falling back on just the std::env::consts::OS name ("linux") in
the case of runtime errors.

Co-authored-by: Nat Budin <natbudin@gmail.com>
2025-03-19 11:48:54 -07:00
oopsbagel
5ae186bc73 feat(pcap): add rayhunter name and version to metadata
Add the compile-time name and version to the pcap's Section Header Block
as the shb_userappl option, the canonical place for storing the name of
the application used to create the pcap.[0]

[0] https://ietf-opsawg-wg.github.io/draft-ietf-opsawg-pcap/draft-ietf-opsawg-pcapng.html#section-4.1-10
2025-03-19 11:48:54 -07:00
Inhishonor
c765a40426 Improve grammer. 2025-03-19 09:27:01 -07:00
Inhishonor
93cfbea361 Fix various sentences in README. 2025-03-19 09:27:01 -07:00
Cooper Quintin
8e6bed97b7 Merge branch 'allpoints-132_Merge_OS_variant_install_scripts' 2025-03-18 18:22:33 -07:00
Cooper Quintin
4214b27c0f fix nits in install.sh and update readme with new instructions 2025-03-18 18:21:43 -07:00
rbomze
f69487853a minimized the binary size 2025-03-18 17:59:07 -07:00
Jeremy
7eb61748d7 Update readme: Add link to PGP key for contact email address 2025-03-18 17:59:07 -07:00
Will Greenberg
ca4e560e92 Update README.md 2025-03-18 17:59:07 -07:00
Alexis
2ffb1d4620 Update SECURITY.md
Just fixing the relative link to this project
2025-03-18 17:59:07 -07:00
Cooper Quintin
77944dd17c add security file 2025-03-18 17:59:07 -07:00
rbomze
50301076f0 minimized the binary size 2025-03-18 17:37:24 -07:00
Jeremy
21c839678b Update readme: Add link to PGP key for contact email address 2025-03-17 11:24:19 -07:00
Will Greenberg
332a7ffbd0 Update README.md 2025-03-12 11:56:12 -07:00
Alexis
8d250553b7 Update SECURITY.md
Just fixing the relative link to this project
2025-03-11 15:35:47 -07:00
Cooper Quintin
fa897e73fa add security file 2025-03-11 14:53:28 -07:00
Paul Beltrani
c3494e338f Merge install scripts into a single, isntall.sh 2025-03-09 22:27:48 -04:00
Cooper Quintin
f9b2cd6a59 add link to code of conduct 2025-03-07 11:40:37 -08:00
Will Greenberg
eb072fb38c fix various typos 2025-03-07 11:28:29 -08:00
Will Greenberg
91f82fc71d add curl to apt install list 2025-03-07 11:21:36 -08:00
Will Greenberg
6fda8450dc a few more FAQ adjustments 2025-03-07 11:21:36 -08:00
Cooper Quintin
bbfe5877fe More FAQ work 2025-03-07 11:21:36 -08:00
Will Greenberg
75d3740f66 Add FAQ to readme 2025-03-07 11:21:36 -08:00
oopsbagel
94c576fd96 fix(tools): add pycrate dependency to requirements.txt
nasparse.py and nasparse_test.py require the pycrate_mobile and
pycrate_core libraries provided by the pycrate package.

This commit adds the required package to requirements.txt.
2025-03-07 11:08:20 -08:00
Cooper Quintin
ee83613757 update readme 2025-02-27 17:29:48 -08:00
Cooper Quintin
840f8ad8b0 stop before upload in case file is locked from writing by running process 2025-02-10 11:26:27 -08:00
Cooper Quintin
c9ac834ca7 show warnings in web UI 2025-02-10 11:26:27 -08:00
Cooper Quintin
8629aacf6b switch default to not see trace messages, switch arg from quiet to verbose 2025-02-10 11:26:27 -08:00
Cooper Quintin
a3fd1479f9 rename qmdl path so that downloaded files have a qmdl extension 2025-02-10 11:26:27 -08:00
Cooper Quintin
049c563f02 fix shortcodes on rayhunter_check 2025-02-10 11:26:27 -08:00
Cooper Quintin
a33b5a3418 Update README.md
Co-authored-by: Will Greenberg <willg@eff.org>
2025-01-31 17:00:44 -08:00
Cooper Quintin
107ba58296 warn if running install scritps from git tree 2025-01-31 17:00:44 -08:00
Cooper Quintin
d016279172 some tweaks to readme 2025-01-31 17:00:44 -08:00
Will Greenberg
5a084f1abb lib: set uplink flag for NAS 2025-01-30 11:33:14 -08:00
Will Greenberg
3619df32ab check: give qmdl-path a shorthand arg 2025-01-28 11:02:19 -08:00
Will Greenberg
34d87d1fd7 this macro isn't public, so docstrings won't work 2025-01-28 11:02:19 -08:00
Will Greenberg
da4952e70f fix docstring code 2025-01-28 11:02:19 -08:00
Will Greenberg
30323b8329 Keep old 2G downgrade analyzer 2025-01-28 11:02:19 -08:00
Will Greenberg
28b0f409db fix attribution 2025-01-28 11:02:19 -08:00
Will Greenberg
12640cc878 Rewrite our 2G downgrade analyzer 2025-01-28 11:02:19 -08:00
Will Greenberg
26eda5904f Better wording on IMSI requested warning 2025-01-28 11:02:19 -08:00
Will Greenberg
3e26e61b05 check: don't count informational events as warnings, better logging 2025-01-28 11:02:19 -08:00
Will Greenberg
565c0f1e67 serial: fix UTF-8 panic on macOS 2025-01-26 17:05:42 -08:00
Will Greenberg
6bd36921d8 consider early IMSI request medium sev 2025-01-08 15:23:59 -08:00
Will Greenberg
c83ae30be8 fix language 2025-01-08 15:23:59 -08:00
Will Greenberg
fa612241a5 lib: add IMSI requested heuristic 2025-01-08 15:23:59 -08:00
Will Greenberg
10592bbd9d lib: add inbound/outbound field to NAS 2025-01-06 16:24:11 -08:00
Will Greenberg
327eaddcd7 rayhunter-check: pcapify qmdl 2025-01-06 16:24:11 -08:00
Will Greenberg
32149c3b37 Update tools/nasparse.py 2024-12-17 14:46:31 -08:00
Cooper Quintin
e47d4dacc4 raise error on non nas message 2024-12-17 14:46:31 -08:00
Cooper Quintin
4009e3d1ed fix nits 2024-12-17 14:46:31 -08:00
Cooper Quintin
b2cd735a07 proof of concept pcap reader for nas heuristic 2024-12-17 14:46:31 -08:00
Cooper Quintin
94e9a88a91 PoC of python nas heuristic 2024-12-17 14:46:31 -08:00
Cooper Quintin
f4a6c834d2 remove false positive IMSI heuristic until we get a NAS parser 2024-12-09 10:53:58 -08:00
Cooper Quintin
95e8f846d3 propegate colorblind mode beyond start/stop 2024-11-26 11:05:13 -08:00
Cooper Quintin
15f128add1 remove unneeded import 2024-11-26 11:05:13 -08:00
Cooper Quintin
87f9cc403b add colorblind mode. Fixes #77 2024-11-26 11:05:13 -08:00
Cooper Quintin
7addf3a67f fix reboot timeout 2024-11-18 17:10:16 -08:00
Cooper Quintin
4d8cc9b738 Revert "name binary rayhunter-daemon"
This reverts commit 9cd5ce3394.
2024-11-18 16:16:43 -08:00
Cooper Quintin
b0d797d206 name binary rayhunter-daemon 2024-11-18 16:16:43 -08:00
Will Greenberg
1ae3b5020b fix installer script
With the odd permissions issues we've been seeing, we should use
AT_SYSCMD for all mv operations into /data
2024-11-18 16:16:43 -08:00
45 changed files with 928 additions and 281 deletions

View File

@@ -1,3 +1,11 @@
[target.armv7-unknown-linux-gnueabihf] [target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc" linker = "arm-linux-gnueabihf-gcc"
rustflags = ["-C", "target-feature=+crt-static"] rustflags = ["-C", "target-feature=+crt-static"]
# optimizations to reduce the binary size
[profile.release]
strip = true
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"

View File

@@ -12,12 +12,12 @@ jobs:
strategy: strategy:
matrix: matrix:
platform: platform:
- os: ubuntu-latest - name: ubuntu-24
serial_build_name: serial os: ubuntu-latest
check_build_name: rayhunter-check - name: macos-arm
- os: macos-latest os: macos-latest
serial_build_name: serial - name: macos-intel
check_build_name: rayhunter-check os: macos-13
runs-on: ${{ matrix.platform.os }} runs-on: ${{ matrix.platform.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -25,8 +25,8 @@ jobs:
run: cargo build --bin serial --release run: cargo build --bin serial --release
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: serial-${{ matrix.platform.os }} name: serial-${{ matrix.platform.name }}
path: ./target/release/${{ matrix.platform.serial_build_name }} path: ./target/release/serial
if-no-files-found: error if-no-files-found: error
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Build check - name: Build check
@@ -34,7 +34,7 @@ jobs:
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: rayhunter-check-${{ matrix.platform.os }} name: rayhunter-check-${{ matrix.platform.os }}
path: ./target/release/${{ matrix.platform.check_build_name }} path: ./target/release/rayhunter-check
if-no-files-found: error if-no-files-found: error
build_rootshell_and_rayhunter: build_rootshell_and_rayhunter:
runs-on: ubuntu-latest runs-on: ubuntu-latest

1
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1 @@
This project is governed by [EFF's Public Projects Code of Conduct](https://www.eff.org/pages/eppcode).

185
Cargo.lock generated
View File

@@ -482,6 +482,26 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "colored"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.52.0",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.6" version = "0.8.6"
@@ -602,6 +622,15 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
]
[[package]] [[package]]
name = "derive-into-owned" name = "derive-into-owned"
version = "0.2.0" version = "0.2.0"
@@ -767,6 +796,19 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
[[package]]
name = "futures-lite"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532"
dependencies = [
"fastrand",
"futures-core",
"futures-io",
"parking",
"pin-project-lite",
]
[[package]] [[package]]
name = "futures-macro" name = "futures-macro"
version = "0.3.30" version = "0.3.30"
@@ -1088,6 +1130,16 @@ dependencies = [
"syn 2.0.50", "syn 2.0.50",
] ]
[[package]]
name = "io-kit-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b"
dependencies = [
"core-foundation-sys",
"mach2",
]
[[package]] [[package]]
name = "is-terminal" name = "is-terminal"
version = "0.4.12" version = "0.4.12"
@@ -1158,18 +1210,6 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "libusb1-sys"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d0e2afce4245f2c9a418511e5af8718bcaf2fa408aefb259504d1a9cb25f27"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.4.14" version = "0.4.14"
@@ -1201,6 +1241,15 @@ dependencies = [
"imgref", "imgref",
] ]
[[package]]
name = "mach2"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "matchit" name = "matchit"
version = "0.7.3" version = "0.7.3"
@@ -1310,6 +1359,12 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]] [[package]]
name = "num-derive" name = "num-derive"
version = "0.4.2" version = "0.4.2"
@@ -1360,6 +1415,34 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "num_threads"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
dependencies = [
"libc",
]
[[package]]
name = "nusb"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99a726776e551f3ee9b467fe47202f26e64b9bbf715df5443b0904df6f2dcc41"
dependencies = [
"atomic-waker",
"core-foundation",
"core-foundation-sys",
"futures-core",
"io-kit-sys",
"libc",
"log",
"once_cell",
"rustix",
"slab",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "object" name = "object"
version = "0.32.2" version = "0.32.2"
@@ -1375,6 +1458,12 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.1" version = "0.12.1"
@@ -1487,6 +1576,12 @@ dependencies = [
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.17" version = "0.2.17"
@@ -1654,6 +1749,7 @@ dependencies = [
"futures-core", "futures-core",
"libc", "libc",
"log", "log",
"nix",
"pcap-file-tokio", "pcap-file-tokio",
"serde", "serde",
"telcom-parser", "telcom-parser",
@@ -1679,6 +1775,7 @@ dependencies = [
"rayhunter", "rayhunter",
"serde", "serde",
"serde_json", "serde_json",
"simple_logger",
"tempfile", "tempfile",
"thiserror", "thiserror",
"tokio", "tokio",
@@ -1761,16 +1858,6 @@ dependencies = [
"nix", "nix",
] ]
[[package]]
name = "rusb"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45fff149b6033f25e825cbb7b2c625a11ee8e6dac09264d49beb125e39aa97bf"
dependencies = [
"libc",
"libusb1-sys",
]
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.23" version = "0.1.23"
@@ -1874,7 +1961,8 @@ dependencies = [
name = "serial" name = "serial"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"rusb", "futures-lite",
"nusb",
] ]
[[package]] [[package]]
@@ -1901,6 +1989,18 @@ dependencies = [
"quote", "quote",
] ]
[[package]]
name = "simple_logger"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8c5dfa5e08767553704aa0ffd9d9794d527103c736aba9854773851fd7497eb"
dependencies = [
"colored",
"log",
"time",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.9" version = "0.4.9"
@@ -2065,6 +2165,39 @@ dependencies = [
"weezl", "weezl",
] ]
[[package]]
name = "time"
version = "0.3.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
dependencies = [
"deranged",
"itoa",
"libc",
"num-conv",
"num_threads",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
dependencies = [
"num-conv",
"time-core",
]
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.36.0" version = "1.36.0"
@@ -2263,12 +2396,6 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]] [[package]]
name = "version-compare" name = "version-compare"
version = "0.2.0" version = "0.2.0"

View File

@@ -1,54 +1,61 @@
![Rayhunter Logo - An Orca taking a bite out of a cellular signal bar](https://www.eff.org/files/styles/media_browser_preview/public/banner_library/rayhunter-banner.png)
# Rayhunter # Rayhunter
```
@@@@@@@ @@@@@@ @@@ @@@ @@@ @@@ @@@ @@@ @@@ @@@ @@@@@@@ @@@@@@@@ @@@@@@@
@@! @@@ @@! @@@ @@! !@@ @@! @@@ @@! @@@ @@!@!@@@ @@! @@! @@! @@@
@!@!!@! @!@!@!@! !@!@! @!@!@!@! @!@ !@! @!@@!!@! @!! @!!!:! @!@!!@!
!!: :!! !!: !!! !!: !!: !!! !!: !!! !!: !!! !!: !!: !!: :!!
: : : : : : .: : : : :.:: : :: : : : :: ::: : : :
_ _ _ _ _ _ _ _
)`'-.,_)`'-.,_)`'-.,_)`'-.,_)`'-.,_)`'-.,_)`'-.,_)`'-.,_
O .
O ' '
o ' .
o .'
__________.-' '...___
.-' ### '''...__
/ a### ## ''--.._ ______
'. # ######## ' .-'
'-._ ..**********#### ___...---'''\ '
'-._ __________...---''' \ l
\ | apc '._|
\__;
```
![Tests](https://github.com/EFForg/rayhunter/actions/workflows/check-and-test.yml/badge.svg) ![Tests](https://github.com/EFForg/rayhunter/actions/workflows/check-and-test.yml/badge.svg)
Rayhunter is an IMSI Catcher Catcher for the Orbic mobile hotspot. Rayhunter is an IMSI Catcher Catcher for the Orbic mobile hotspot.
**THIS CODE IS PROOF OF CONCEPT AND SHOULD NOT BE RELIED UPON IN HIGH RISK SITUATIONS** **THIS CODE IS A PROOF OF CONCEPT AND SHOULD NOT BE RELIED UPON IN HIGH RISK SITUATIONS!**
Code is built and tested for the Orbic RC400L mobile hotspot, it may work on other orbics and other
linux/qualcom devices but this is the only one we have tested on. Buy the orbic [using bezos bucks](https://www.amazon.com/gp/product/B09CLS6Z7X/) ## The Hardware
Rayhunter has been built and tested for the Orbic RC400L mobile hotspot. It may work on other orbics and other
linux/qualcom devices, but this is the only one we have tested on.
Buy the 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).
## Setup ## Setup
*NOTE: We don't currently support automated installs on windows, you will have to follow the manual install instructions below*
1. Download the latest [rayhunter release bundle](https://github.com/EFForg/rayhunter/releases) and extract it. 1. Download the latest [Rayhunter release bundle](https://github.com/EFForg/rayhunter/releases) and extract it.
2. Run the install script inside the bundle corresponding to your platform (`install-linux.sh`, `install-mac.sh`). **If you are installing from the cloned github repository please see the development instructions below, running `install.sh` from the git tree will not work.**
3. Once finished, rayhunter should be running! You can verify this by visiting the web UI as described below. 2. Turn on the Orbic device and plug it into your computer using a USB-C Cable.
3. On MacOS or Linux run the install script `install.sh`.
4. Once finished, Rayhunter should be running! You can verify this by visiting the web UI as described below.
### Notes
* The install script has only been tested for Linux on the latest version of Ubuntu. If it fails you will need to follow the install steps outlined in **Development** below.
* The install script also won't work on older macs with intel chips, for those macs you will need to follow the instructions at https://github.com/EFForg/rayhunter/wiki/Install-Rayhunter-on-Mac-Intel-devices
* We don't currently support automated installs on windows, you will have to follow the manual install instructions below*
## Updating
Great news: if you've successfully installed rayhunter, you already know how to update it! Our update process is identical to the setup process: simply download the latest release and follow the steps in the [setup section](#Setup).
## Usage ## Usage
Once installed, rayhunter will run automatically whenever your Orbic device is running. It serves a web UI that provides some basic controls, such as being able to start/stop recordings, download captures, and view heuristic analyses of captures. You can access this UI in one of two ways: Once installed, Rayhunter will run automatically whenever your Orbic device is running. It serves a web UI that provides some basic controls, such as being able to start/stop recordings, download captures, and view heuristic analyses of captures. You can access this UI in one of two ways:
1. Over wifi: Connect your phone/laptop to the Orbic's wifi network and visit `http://192.168.1.1:8080` (click past your browser warning you about the connection not being secure, rayhunter doesn't have HTTPS yet!) 1. Over wifi: Connect your phone/laptop to the Orbic's wifi network and visit `http://192.168.1.1:8080` (click past your browser warning you about the connection not being secure, Rayhunter doesn't have HTTPS yet!).
* Note that you'll need the Orbic's wifi password for this, which can be retrieved by pressing the "MENU" button on the device and opening the 2.4 GHz menu. * Note that you'll need the Orbic's wifi password for this, which can be retrieved by pressing the "MENU" button on the device and opening the 2.4 GHz menu.
2. Over usb: Connect the Orbic device to your laptop via usb. Run `adb forward tcp:8080 tcp:8080`, then visit `http://localhost:8080`. For this you will need to install the Android Debug Bridge (ADB) on your computer, you can copy the version that was downloaded inside the releases/platform-tools/` folder to somewhere else in your path or you can install it manually. You can find instructions for doing so on your platform [here](https://www.xda-developers.com/install-adb-windows-macos-linux/#how-to-set-up-adb-on-your-computer), (don't worry about instructions for installing it on a phone/device yet). 2. Over usb: Connect the Orbic device to your laptop via usb. Run `adb forward tcp:8080 tcp:8080`, then visit `http://localhost:8080`. For this you will need to install the Android Debug Bridge (ADB) on your computer, you can copy the version that was downloaded inside the releases/platform-tools/` folder to somewhere else in your path or you can install it manually. You can find instructions for doing so on your platform [here](https://www.xda-developers.com/install-adb-windows-macos-linux/#how-to-set-up-adb-on-your-computer), (don't worry about instructions for installing it on a phone/device yet).
## Frequently Asked Questions
### 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!
### Help, Rayhunter's line is red! What should I do?
Unfortunately, the circumstances that might lead to a positive CSS signal are quite varied, so we don't have a universal recommendation for how to deal with the a positive signal. You might also 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!
Please feel free to contact an EFF technologist with more information & a copy of the QMDL in question at [info@eff.org](mailto:info@eff.org). 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. We encourage you to use PGP encryption when sending your message. You can find the [PGP public key for info@eff.org here](https://www.eff.org/about/contact#main-content).
### Does Rayhunter work outside of the US?
**Probably**. Some Rayhunter users have reported successfully using it in other countries with unlocked devices and SIM cards from local telcos. We can't guarantee whether or not it will work for you though.
### 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.
### Does Rayhunter work on any other devices besides the Orbic RC400L?
**Maybe**. We have not tested Rayhunter on any other hardware but we would love to expand the supported platforms. We will consider giving official support to any hardware platform that can be bought for around $20-30USD. The Rayhunter daemon should theoretically work on any linux/android device that has a qualcomm chip with a /dev/diag interface and root access, though our installer script has only been tested with an Orbic. If you get it working on another device, please let us know!
## Development ## Development
* Install ADB on your computer using the instructions above, and make sure it's in your terminal's PATH * Install ADB on your computer using the instructions above, and make sure it's in your terminal's PATH
* You can verify if ADB is in your PATH by running `which adb` in a terminal. If it prints the filepath to where ADB is installed, you're set! Otherwise, try following one of these guides: * You can verify if ADB is in your PATH by running `which adb` in a terminal. If it prints the filepath to where ADB is installed, you're set! Otherwise, try following one of these guides:
@@ -56,28 +63,26 @@ Once installed, rayhunter will run automatically whenever your Orbic device is r
* [macOS](https://www.repeato.app/setting-up-adb-on-macos-a-step-by-step-guide/) * [macOS](https://www.repeato.app/setting-up-adb-on-macos-a-step-by-step-guide/)
* [Windows](https://medium.com/@yadav-ajay/a-step-by-step-guide-to-setting-up-adb-path-on-windows-0b833faebf18) * [Windows](https://medium.com/@yadav-ajay/a-step-by-step-guide-to-setting-up-adb-path-on-windows-0b833faebf18)
### If your are on x86 linux ### If you're on x86 linux
* on your linux laptop install rust the usual way and then install cross compiling dependences. Install rust the usual way and then install cross compiling dependences:
* run `sudo apt install build-essential libc6-armhf-cross libc6-dev-armhf-cross gcc-arm-linux-gnueabihf`
* set up cross compliing for rust:
``` ```
sudo apt install curl build-essential libc6-armhf-cross libc6-dev-armhf-cross gcc-arm-linux-gnueabihf
rustup target add x86_64-unknown-linux-gnu rustup target add x86_64-unknown-linux-gnu
rustup target add armv7-unknown-linux-gnueabihf rustup target add armv7-unknown-linux-gnueabihf
``` ```
Now you can root your device and install rayhunter by running `./tools/install-dev.sh` Now you can root your device and install Rayhunter by running `./tools/install-dev.sh`
### If you are on windows or can't run the install scripts ### If you're on windows or can't run the install scripts
* Root your device on windows using the instructions here: https://xdaforums.com/t/resetting-verizon-orbic-speed-rc400l-firmware-flash-kajeet.4334899/#post-87855183 * Root your device on windows using the instructions here: https://xdaforums.com/t/resetting-verizon-orbic-speed-rc400l-firmware-flash-kajeet.4334899/#post-87855183
* Build for arm using `cargo build` * Build for arm using `cargo build`
* Run tests using `cargo test_pc` * Run tests using `cargo test_pc`
* Push the scripts in `scripts/` to /etc/init.d on device and make a directory called /data/rayhunter using `adb shell` (and sshell for your root shell if you followed the steps above) * Push the scripts in `scripts/` to `/etc/init.d` on device and make a directory called `/data/rayhunter` using `adb shell` (and sshell for your root shell if you followed the steps above)
* you also need to copy `config.toml.example` to /data/rayhunter/config.toml * you also need to copy `config.toml.example` to `/data/rayhunter/config.toml`
* Then run `./make.sh` this will build the binary and push it over adb. Restart your device or run `/etc/init.d/rayhunter_daemon start` on the device and you are good to go. * Then run `./make.sh` this will build the binary and push it over adb. Restart your device or run `/etc/init.d/rayhunter_daemon start` on the device and you are good to go.
@@ -89,9 +94,13 @@ Now you can root your device and install rayhunter by running `./tools/install-d
* push to the device with `./make.sh` * push to the device with `./make.sh`
## Documentation ## Support and Discussion
* Build docs locallly using `RUSTDOCFLAGS="--cfg docsrs" cargo doc --no-deps --all-features --open`
**LEGAL DISCLAIMER:** Use this program at your own risk. We beilieve 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. If you're having issues installing or using Rayhunter, please open an issue in this repo. Join us in the `#rayhunter` channel of [EFF's Mattermost](https://opensource.eff.org/signup_user_complete/?id=6iqur37ucfrctfswrs14iscobw&md=link&sbr=su) instance to chat!
## Documentation
* Build docs locally using `RUSTDOCFLAGS="--cfg docsrs" cargo doc --no-deps --all-features --open`
**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!* *Good Hunting!*

5
SECURITY.md Normal file
View File

@@ -0,0 +1,5 @@
# Security Policy
## Reporting a Vulnerability
Security vulnerabilities can be reported using GitHub's [private vulnerability reporting tool](https://github.com/EFForg/rayhunter/security/advisories/new).

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "rayhunter-daemon" name = "rayhunter-daemon"
version = "0.1.0" version = "0.2.6"
edition = "2021" edition = "2021"
[[bin]] [[bin]]
@@ -32,3 +32,4 @@ clap = { version = "4.5.2", features = ["derive"] }
serde_json = "1.0.114" serde_json = "1.0.114"
image = "0.25.1" image = "0.25.1"
tempfile = "3.10.1" tempfile = "3.10.1"
simple_logger = "5.0.0"

View File

@@ -1,5 +1,6 @@
use std::{collections::HashMap, future, path::PathBuf, pin::pin}; use std::{collections::HashMap, future, path::PathBuf, pin::pin};
use rayhunter::{analysis::analyzer::Harness, diag::DataType, qmdl::QmdlReader}; use log::{info, warn};
use rayhunter::{analysis::analyzer::{EventType, Harness}, diag::DataType, gsmtap_parser, pcap::GsmtapPcapWriter, qmdl::QmdlReader};
use tokio::fs::{metadata, read_dir, File}; use tokio::fs::{metadata, read_dir, File};
use clap::Parser; use clap::Parser;
use futures::TryStreamExt; use futures::TryStreamExt;
@@ -9,14 +10,20 @@ mod dummy_analyzer;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(version, about)] #[command(version, about)]
struct Args { struct Args {
#[arg(short, long)] #[arg(short = 'p', long)]
qmdl_path: PathBuf, qmdl_path: PathBuf,
#[arg(short = 'c', long)]
pcapify: bool,
#[arg(long)] #[arg(long)]
show_skipped: bool, show_skipped: bool,
#[arg(long)] #[arg(long)]
enable_dummy_analyzer: bool, enable_dummy_analyzer: bool,
#[arg(short, long)]
verbose: bool,
} }
async fn analyze_file(harness: &mut Harness, qmdl_path: &str, show_skipped: bool) { async fn analyze_file(harness: &mut Harness, qmdl_path: &str, show_skipped: bool) {
@@ -38,34 +45,79 @@ async fn analyze_file(harness: &mut Harness, qmdl_path: &str, show_skipped: bool
} }
for analysis in row.analysis { for analysis in row.analysis {
for maybe_event in analysis.events { for maybe_event in analysis.events {
if let Some(event) = maybe_event { let Some(event) = maybe_event else { continue };
warnings += 1; match event.event_type {
println!("{}: {:?}", analysis.timestamp, event); EventType::Informational => {
info!(
"{}: INFO - {} {}",
qmdl_path,
analysis.timestamp,
event.message,
);
}
EventType::QualitativeWarning { severity } => {
warn!(
"{}: WARNING (Severity: {:?}) - {} {}",
qmdl_path,
severity,
analysis.timestamp,
event.message,
);
warnings += 1;
}
} }
} }
} }
} }
if show_skipped && skipped > 0 { if show_skipped && skipped > 0 {
println!("{}: messages skipped:", qmdl_path); info!("{}: messages skipped:", qmdl_path);
for (reason, count) in skipped_reasons.iter() { for (reason, count) in skipped_reasons.iter() {
println!(" - {}: \"{}\"", count, reason); info!(" - {}: \"{}\"", count, reason);
} }
} }
println!("{}: {} messages analyzed, {} warnings, {} messages skipped", qmdl_path, total_messages, warnings, skipped); info!("{}: {} messages analyzed, {} warnings, {} messages skipped", qmdl_path, total_messages, warnings, skipped);
}
async fn pcapify(qmdl_path: &PathBuf) {
let qmdl_file = &mut File::open(&qmdl_path).await.expect("failed to open qmdl file");
let qmdl_file_size = qmdl_file.metadata().await.unwrap().len();
let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(qmdl_file_size as usize));
let mut pcap_path = qmdl_path.clone();
pcap_path.set_extension("pcap");
let pcap_file = &mut File::create(&pcap_path).await.expect("failed to open pcap file");
let mut pcap_writer = GsmtapPcapWriter::new(pcap_file).await.unwrap();
pcap_writer.write_iface_header().await.unwrap();
while let Some(container) = qmdl_reader.get_next_messages_container().await.expect("failed to get container") {
for msg in container.into_messages().into_iter().flatten() {
if let Ok(Some((timestamp, parsed))) = gsmtap_parser::parse(msg) {
pcap_writer.write_gsmtap_message(parsed, timestamp).await.expect("failed to write");
}
}
}
info!("wrote pcap to {:?}", &pcap_path);
} }
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
env_logger::init();
let args = Args::parse(); let args = Args::parse();
let level = if args.verbose {
log::LevelFilter::Trace
} else {
log::LevelFilter::Warn
};
simple_logger::SimpleLogger::new()
.with_colors(true)
.without_timestamps()
.with_level(level)
.init().unwrap();
let mut harness = Harness::new_with_all_analyzers(); let mut harness = Harness::new_with_all_analyzers();
if args.enable_dummy_analyzer { if args.enable_dummy_analyzer {
harness.add_analyzer(Box::new(dummy_analyzer::TestAnalyzer { count: 0 })); harness.add_analyzer(Box::new(dummy_analyzer::TestAnalyzer { count: 0 }));
} }
println!("Analyzers:"); info!("Analyzers:");
for analyzer in harness.get_metadata().analyzers { for analyzer in harness.get_metadata().analyzers {
println!(" - {}: {}", analyzer.name, analyzer.description); info!(" - {}: {}", analyzer.name, analyzer.description);
} }
let metadata = metadata(&args.qmdl_path).await.expect("failed to get metadata"); let metadata = metadata(&args.qmdl_path).await.expect("failed to get metadata");
@@ -75,10 +127,19 @@ async fn main() {
let name = entry.file_name(); let name = entry.file_name();
let name_str = name.to_str().unwrap(); let name_str = name.to_str().unwrap();
if name_str.ends_with(".qmdl") { if name_str.ends_with(".qmdl") {
analyze_file(&mut harness, entry.path().to_str().unwrap(), args.show_skipped).await; let path = entry.path();
let path_str = path.to_str().unwrap();
analyze_file(&mut harness, path_str, args.show_skipped).await;
if args.pcapify {
pcapify(&path).await;
}
} }
} }
} else { } else {
analyze_file(&mut harness, args.qmdl_path.to_str().unwrap(), args.show_skipped).await; let path = args.qmdl_path.to_str().unwrap();
analyze_file(&mut harness, path, args.show_skipped).await;
if args.pcapify {
pcapify(&args.qmdl_path).await;
}
} }
} }

View File

@@ -2,22 +2,16 @@ use crate::error::RayhunterError;
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize)]
struct ConfigFile {
qmdl_store_path: Option<String>,
port: Option<u16>,
debug_mode: Option<bool>,
ui_level: Option<u8>,
enable_dummy_analyzer: Option<bool>,
}
#[derive(Debug)] #[derive(Debug)]
#[derive(Deserialize)]
#[serde(default)]
pub struct Config { pub struct Config {
pub qmdl_store_path: String, pub qmdl_store_path: String,
pub port: u16, pub port: u16,
pub debug_mode: bool, pub debug_mode: bool,
pub ui_level: u8, pub ui_level: u8,
pub enable_dummy_analyzer: bool, pub enable_dummy_analyzer: bool,
pub colorblind_mode: bool,
} }
impl Default for Config { impl Default for Config {
@@ -28,22 +22,17 @@ impl Default for Config {
debug_mode: false, debug_mode: false,
ui_level: 1, ui_level: 1,
enable_dummy_analyzer: false, enable_dummy_analyzer: false,
colorblind_mode: false,
} }
} }
} }
pub fn parse_config<P>(path: P) -> Result<Config, RayhunterError> where P: AsRef<std::path::Path> { pub fn parse_config<P>(path: P) -> Result<Config, RayhunterError> where P: AsRef<std::path::Path> {
let mut config = Config::default();
if let Ok(config_file) = std::fs::read_to_string(&path) { if let Ok(config_file) = std::fs::read_to_string(&path) {
let parsed_config: ConfigFile = toml::from_str(&config_file) Ok(toml::from_str(&config_file).map_err(RayhunterError::ConfigFileParsingError)?)
.map_err(RayhunterError::ConfigFileParsingError)?; } else {
parsed_config.qmdl_store_path.map(|v| config.qmdl_store_path = v); Ok(Config::default())
parsed_config.port.map(|v| config.port = v);
parsed_config.debug_mode.map(|v| config.debug_mode = v);
parsed_config.ui_level.map(|v| config.ui_level = v);
parsed_config.enable_dummy_analyzer.map(|v| config.enable_dummy_analyzer = v);
} }
Ok(config)
} }
pub struct Args { pub struct Args {

View File

@@ -22,6 +22,7 @@ use analysis::{get_analysis_status, run_analysis_thread, start_analysis, Analysi
use axum::response::Redirect; use axum::response::Redirect;
use diag::{get_analysis_report, start_recording, stop_recording, DiagDeviceCtrlMessage}; use diag::{get_analysis_report, start_recording, stop_recording, DiagDeviceCtrlMessage};
use log::{info, error}; use log::{info, error};
use qmdl_store::RecordingStoreError;
use rayhunter::diag_device::DiagDevice; use rayhunter::diag_device::DiagDevice;
use axum::routing::{get, post}; use axum::routing::{get, post};
use axum::Router; use axum::Router;
@@ -59,6 +60,7 @@ async fn run_server(
debug_mode: config.debug_mode, debug_mode: config.debug_mode,
analysis_status_lock, analysis_status_lock,
analysis_sender, analysis_sender,
colorblind_mode: config.colorblind_mode,
}); });
let app = Router::new() let app = Router::new()
@@ -89,13 +91,31 @@ async fn server_shutdown_signal(server_shutdown_rx: oneshot::Receiver<()>) {
info!("Server received shutdown signal, exiting..."); info!("Server received shutdown signal, exiting...");
} }
// Loads a QmdlStore if one exists, and if not, only create one if we're not in // Loads a RecordingStore if one exists, and if not, only create one if we're
// debug mode. // not in debug mode. If we fail to parse the manifest AND we're not in debug
// mode, try to recover by making a new (empty) manifest in the same directory.
async fn init_qmdl_store(config: &config::Config) -> Result<RecordingStore, RayhunterError> { async fn init_qmdl_store(config: &config::Config) -> Result<RecordingStore, RayhunterError> {
match (RecordingStore::exists(&config.qmdl_store_path).await?, config.debug_mode) { let store_exists = RecordingStore::exists(&config.qmdl_store_path).await?;
(true, _) => Ok(RecordingStore::load(&config.qmdl_store_path).await?), if config.debug_mode {
(false, false) => Ok(RecordingStore::create(&config.qmdl_store_path).await?), if store_exists {
(false, true) => Err(RayhunterError::NoStoreDebugMode(config.qmdl_store_path.clone())), Ok(RecordingStore::load(&config.qmdl_store_path).await?)
} else {
Err(RayhunterError::NoStoreDebugMode(config.qmdl_store_path.clone()))
}
} else {
if store_exists {
match RecordingStore::load(&config.qmdl_store_path).await {
Ok(store) => Ok(store),
Err(RecordingStoreError::ParseManifestError(err)) => {
error!("failed to parse QMDL manifest: {}", err);
info!("creating new empty manifest...");
Ok(RecordingStore::create(&config.qmdl_store_path).await?)
},
Err(err) => Err(err.into()),
}
} else {
Ok(RecordingStore::create(&config.qmdl_store_path).await?)
}
} }
} }
@@ -142,12 +162,17 @@ fn run_ctrl_c_thread(
fn update_ui(task_tracker: &TaskTracker, config: &config::Config, mut ui_shutdown_rx: oneshot::Receiver<()>, mut ui_update_rx: Receiver<framebuffer::DisplayState>) -> JoinHandle<()> { fn update_ui(task_tracker: &TaskTracker, config: &config::Config, mut ui_shutdown_rx: oneshot::Receiver<()>, mut ui_update_rx: Receiver<framebuffer::DisplayState>) -> JoinHandle<()> {
static IMAGE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/static/images/"); static IMAGE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/static/images/");
let mut display_color: framebuffer::Color565;
let display_level = config.ui_level; let display_level = config.ui_level;
if display_level == 0 { if display_level == 0 {
info!("Invisible mode, not spawning UI."); info!("Invisible mode, not spawning UI.");
} }
let mut display_color = framebuffer::Color565::Green; if config.colorblind_mode {
display_color = framebuffer::Color565::Blue;
} else {
display_color = framebuffer::Color565::Green;
}
task_tracker.spawn_blocking(move || { task_tracker.spawn_blocking(move || {
let mut fb: Framebuffer = Framebuffer::new(); let mut fb: Framebuffer = Framebuffer::new();
@@ -189,7 +214,8 @@ fn update_ui(task_tracker: &TaskTracker, config: &config::Config, mut ui_shutdo
fb.draw_line(framebuffer::Color565::Pink, 50); fb.draw_line(framebuffer::Color565::Pink, 50);
fb.draw_line(framebuffer::Color565::Cyan, 25); fb.draw_line(framebuffer::Color565::Cyan, 25);
}, },
1 | _ => { _ => { // this branch id for ui_level 1, which is also the default if an
// unknown value is used
fb.draw_line(display_color, 2); fb.draw_line(display_color, 2);
}, },
}; };

View File

@@ -104,7 +104,7 @@ pub fn run_diag_read_thread(
} }
let mut qmdl_store = qmdl_store_lock.write().await; let mut qmdl_store = qmdl_store_lock.write().await;
let index = qmdl_store.current_entry.expect("DiagDevice had qmdl_writer, but QmdlStore didn't have current entry???"); let index = qmdl_store.current_entry.expect("DiagDevice had qmdl_writer, but QmdlStore didn't have current entry???");
qmdl_store.update_entry_analysis_size(index, analysis_file_len as usize).await qmdl_store.update_entry_analysis_size(index, analysis_file_len).await
.expect("failed to update analysis file size"); .expect("failed to update analysis file size");
} }
}, },
@@ -129,8 +129,15 @@ pub async fn start_recording(State(state): State<Arc<ServerState>>) -> Result<(S
let qmdl_writer = QmdlWriter::new(qmdl_file); let qmdl_writer = QmdlWriter::new(qmdl_file);
state.diag_device_ctrl_sender.send(DiagDeviceCtrlMessage::StartRecording((qmdl_writer, analysis_file))).await state.diag_device_ctrl_sender.send(DiagDeviceCtrlMessage::StartRecording((qmdl_writer, analysis_file))).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't send stop recording message: {}", e)))?; .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't send stop recording message: {}", e)))?;
state.ui_update_sender.send(framebuffer::DisplayState::Recording).await
let display_state = if state.colorblind_mode {
framebuffer::DisplayState::RecordingCBM
} else {
framebuffer::DisplayState::Recording
};
state.ui_update_sender.send(display_state).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't send ui update message: {}", e)))?; .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't send ui update message: {}", e)))?;
Ok((StatusCode::ACCEPTED, "ok".to_string())) Ok((StatusCode::ACCEPTED, "ok".to_string()))
} }

View File

@@ -27,6 +27,7 @@ pub enum DisplayState {
Recording, Recording,
Paused, Paused,
WarningDetected, WarningDetected,
RecordingCBM,
} }
impl From<DisplayState> for Color565 { impl From<DisplayState> for Color565 {
@@ -34,6 +35,7 @@ impl From<DisplayState> for Color565 {
match state { match state {
DisplayState::Paused => Color565::White, DisplayState::Paused => Color565::White,
DisplayState::Recording => Color565::Green, DisplayState::Recording => Color565::Green,
DisplayState::RecordingCBM => Color565::Blue,
DisplayState::WarningDetected => Color565::Red, DisplayState::WarningDetected => Color565::Red,
} }
} }

View File

@@ -1,3 +1,4 @@
use rayhunter::util::RuntimeMetadata;
use chrono::{DateTime, Local}; use chrono::{DateTime, Local};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -43,17 +44,24 @@ pub struct ManifestEntry {
pub last_message_time: Option<DateTime<Local>>, pub last_message_time: Option<DateTime<Local>>,
pub qmdl_size_bytes: usize, pub qmdl_size_bytes: usize,
pub analysis_size_bytes: usize, pub analysis_size_bytes: usize,
pub rayhunter_version: Option<String>,
pub system_os: Option<String>,
pub arch: Option<String>,
} }
impl ManifestEntry { impl ManifestEntry {
fn new() -> Self { fn new() -> Self {
let now = Local::now(); let now = Local::now();
let metadata = RuntimeMetadata::new();
ManifestEntry { ManifestEntry {
name: format!("{}", now.timestamp()), name: format!("{}", now.timestamp()),
start_time: now, start_time: now,
last_message_time: None, last_message_time: None,
qmdl_size_bytes: 0, qmdl_size_bytes: 0,
analysis_size_bytes: 0, analysis_size_bytes: 0,
rayhunter_version: Some(metadata.rayhunter_version),
system_os: Some(metadata.system_os),
arch: Some(metadata.arch),
} }
} }
@@ -321,6 +329,20 @@ mod tests {
)); ));
} }
#[tokio::test]
async fn test_create_on_existing_store() {
let dir = make_temp_dir();
let mut store = RecordingStore::create(dir.path()).await.unwrap();
let _ = store.new_entry().await.unwrap();
let entry_index = store.current_entry.unwrap();
store
.update_entry_qmdl_size(entry_index, 1000)
.await
.unwrap();
let store = RecordingStore::create(dir.path()).await.unwrap();
assert_eq!(store.manifest.entries.len(), 0);
}
#[tokio::test] #[tokio::test]
async fn test_repeated_new_entries() { async fn test_repeated_new_entries() {
let dir = make_temp_dir(); let dir = make_temp_dir();

View File

@@ -22,13 +22,15 @@ pub struct ServerState {
pub ui_update_sender: Sender<framebuffer::DisplayState>, pub ui_update_sender: Sender<framebuffer::DisplayState>,
pub analysis_status_lock: Arc<RwLock<AnalysisStatus>>, pub analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
pub analysis_sender: Sender<AnalysisCtrlMessage>, pub analysis_sender: Sender<AnalysisCtrlMessage>,
pub debug_mode: bool pub debug_mode: bool,
pub colorblind_mode: bool,
} }
pub async fn get_qmdl(State(state): State<Arc<ServerState>>, Path(qmdl_name): Path<String>) -> Result<Response, (StatusCode, String)> { pub async fn get_qmdl(State(state): State<Arc<ServerState>>, Path(qmdl_name): Path<String>) -> Result<Response, (StatusCode, String)> {
let qmdl_idx = qmdl_name.trim_end_matches(".qmdl");
let qmdl_store = state.qmdl_store_lock.read().await; let qmdl_store = state.qmdl_store_lock.read().await;
let (entry_index, entry) = qmdl_store.entry_for_name(&qmdl_name) let (entry_index, entry) = qmdl_store.entry_for_name(qmdl_idx)
.ok_or((StatusCode::NOT_FOUND, format!("couldn't find qmdl file with name {}", qmdl_name)))?; .ok_or((StatusCode::NOT_FOUND, format!("couldn't find qmdl file with name {}", qmdl_idx)))?;
let qmdl_file = qmdl_store.open_entry_qmdl(entry_index).await let qmdl_file = qmdl_store.open_entry_qmdl(entry_index).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("error opening QMDL file: {}", e)))?; .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("error opening QMDL file: {}", e)))?;
let limited_qmdl_file = qmdl_file.take(entry.qmdl_size_bytes as u64); let limited_qmdl_file = qmdl_file.take(entry.qmdl_size_bytes as u64);

View File

@@ -80,6 +80,13 @@ async function updateEntryAnalysisResult(entry) {
entry.analysis_result = `0 warnings!`; entry.analysis_result = `0 warnings!`;
} else { } else {
entry.analysis_result = `!!! ${entry.analysis.warnings.length} warnings !!!`; entry.analysis_result = `!!! ${entry.analysis.warnings.length} warnings !!!`;
for (const warning of entry.analysis.warnings) {
for (const event of warning.warning.events) {
if (event === null) continue;
msg = `${warning.timestamp}: ${event.message}`
entry.analysis_result += `<br>${msg}`
}
}
} }
} }
@@ -136,11 +143,11 @@ function createEntryRow(entry, isCurrent) {
row.appendChild(pcapTd); row.appendChild(pcapTd);
const qmdlTd = document.createElement('td'); const qmdlTd = document.createElement('td');
qmdlTd.appendChild(createLink(`/api/qmdl/${entry.name}`, 'qmdl')); qmdlTd.appendChild(createLink(`/api/qmdl/${entry.name}.qmdl`, 'qmdl'));
row.appendChild(qmdlTd); row.appendChild(qmdlTd);
const analysisResult = document.createElement('td'); const analysisResult = document.createElement('td');
analysisResult.innerText = entry.analysis_result; analysisResult.innerHTML = entry.analysis_result;
if (entry.analysis.warnings.length > 0) { if (entry.analysis.warnings.length > 0) {
row.classList.add("warning"); row.classList.add("warning");
} }

View File

@@ -1,6 +1,9 @@
# cat config.toml # cat config.toml
qmdl_store_path = "/data/rayhunter/qmdl" qmdl_store_path = "/data/rayhunter/qmdl"
port = 8080 port = 8080
debug_mode = false
enable_dummy_analyzer = false
colorblind_mode = false
# UI Levels: # UI Levels:
# 0 = invisible mode, no indicator that rayhunter is running # 0 = invisible mode, no indicator that rayhunter is running
# 1 = Subtle mode, display a green line at the top of the screen when rayhunter is running # 1 = Subtle mode, display a green line at the top of the screen when rayhunter is running

17
dist/install-linux.sh vendored
View File

@@ -1,17 +0,0 @@
#!/bin/env bash
set -e
if ! command -v adb &> /dev/null; then
if [ ! -d ./platform-tools ] ; then
echo "adb not found, downloading local copy"
curl -O "https://dl.google.com/android/repository/platform-tools-latest-linux.zip"
unzip platform-tools-latest-linux.zip
fi
export ADB="./platform-tools/adb"
else
export ADB=`which adb`
fi
export SERIAL_PATH="./serial-ubuntu-latest/serial"
. "$(dirname "$0")"/install-common.sh
install

17
dist/install-mac.sh vendored
View File

@@ -1,17 +0,0 @@
#!/usr/bin/env bash
set -e
if ! command -v adb &> /dev/null; then
if [ ! -d ./platform-tools ]; then
echo "adb not found, downloading local copy"
curl -O "https://dl.google.com/android/repository/platform-tools-latest-darwin.zip"
unzip platform-tools-latest-darwin.zip
fi
export ADB="./platform-tools/adb"
else
export ADB=`which adb`
fi
export SERIAL_PATH="./serial-macos-latest/serial"
. "$(dirname "$0")"/install-common.sh
install

View File

@@ -1,18 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
install() { set -e
if [[ -z "${SERIAL_PATH}" ]]; then
echo "\$SERIAL_PATH not set, did you run this from install-linux.sh or install-mac.sh?"
exit 1
fi
if [[ -z "${ADB}" ]]; then
echo "\$ADB not set, did you run this from install-linux.sh or install-mac.sh?"
exit 1
fi
force_debug_mode
setup_rootshell
setup_rayhunter
test_rayhunter
}
force_debug_mode() { force_debug_mode() {
echo "Using adb at $ADB" echo "Using adb at $ADB"
@@ -65,16 +52,20 @@ _at_syscmd() {
setup_rayhunter() { setup_rayhunter() {
_at_syscmd "mkdir -p /data/rayhunter" _at_syscmd "mkdir -p /data/rayhunter"
_adb_push config.toml.example /data/rayhunter/config.toml _adb_push config.toml.example /tmp/config.toml
_adb_push rayhunter-daemon /data/rayhunter/ _at_syscmd "mv /tmp/config.toml /data/rayhunter"
_adb_push rayhunter-daemon /tmp/rayhunter-daemon
_at_syscmd "mv /tmp/rayhunter-daemon /data/rayhunter"
_adb_push scripts/rayhunter_daemon /tmp/rayhunter_daemon _adb_push scripts/rayhunter_daemon /tmp/rayhunter_daemon
_at_syscmd "mv /tmp/rayhunter_daemon /etc/init.d/rayhunter_daemon"
_adb_push scripts/misc-daemon /tmp/misc-daemon _adb_push scripts/misc-daemon /tmp/misc-daemon
_at_syscmd "cp /tmp/rayhunter_daemon /etc/init.d/rayhunter_daemon" _at_syscmd "mv /tmp/misc-daemon /etc/init.d/misc-daemon"
_at_syscmd "cp /tmp/misc-daemon /etc/init.d/misc-daemon"
_at_syscmd "chmod 755 /etc/init.d/rayhunter_daemon" _at_syscmd "chmod 755 /etc/init.d/rayhunter_daemon"
_at_syscmd "chmod 755 /etc/init.d/misc-daemon" _at_syscmd "chmod 755 /etc/init.d/misc-daemon"
echo -n "waiting for reboot..." echo -n "waiting for reboot..."
_at_syscmd reboot _at_syscmd "shutdown -r -t 1 now"
# first wait for shutdown (it can take ~10s) # first wait for shutdown (it can take ~10s)
until ! _adb_shell true 2> /dev/null until ! _adb_shell true 2> /dev/null
@@ -104,3 +95,43 @@ test_rayhunter() {
done done
echo "timeout reached! failed to reach rayhunter url $URL, something went wrong :(" echo "timeout reached! failed to reach rayhunter url $URL, something went wrong :("
} }
##### ##### #####
##### Main #####
##### ##### #####
if [[ `uname -s` == "Linux" ]]; then
export SERIAL_PATH="./serial-ubuntu-latest/serial"
export PLATFORM_TOOLS="platform-tools-latest-linux.zip"
elif [[ `uname -s` == "Darwin" ]]; then
if [[ `uname -m` == "arm64" ]]; then
export SERIAL_PATH="./serial-macos-arm/serial"
elif [[ `uname -m` == "x86_64" ]]; then
export SERIAL_PATH="./serial-macos-intel/serial"
fi
export PLATFORM_TOOLS="platform-tools-latest-darwin.zip"
xattr -d com.apple.quarantine "$SERIAL_PATH"
else
echo "This script only supports Linux or macOS"
exit 1
fi
if [ ! -x "$SERIAL_PATH" ]; then
echo "The serial binary cannot be found at $SERIAL_PATH. If you are running this from the git tree please instead run it from the latest release bundle https://github.com/EFForg/rayhunter/releases"
exit 1
fi
if ! command -v adb &> /dev/null; then
if [ ! -d ./platform-tools ] ; then
echo "adb not found, downloading local copy"
curl -O "https://dl.google.com/android/repository/${PLATFORM_TOOLS}"
unzip $PLATFORM_TOOLS
fi
export ADB="./platform-tools/adb"
else
export ADB=`which adb`
fi
force_debug_mode
setup_rootshell
setup_rayhunter
test_rayhunter

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "rayhunter" name = "rayhunter"
version = "0.1.0" version = "0.2.6"
edition = "2021" edition = "2021"
description = "Realtime cellular data decoding and analysis for IMSI catcher detection" description = "Realtime cellular data decoding and analysis for IMSI catcher detection"
@@ -17,6 +17,7 @@ deku = { version = "0.16.0", features = ["logging"] }
env_logger = "0.10.1" env_logger = "0.10.1"
libc = "0.2.150" libc = "0.2.150"
log = "0.4.20" log = "0.4.20"
nix = { version = "0.29.0", features = ["feature"] }
pcap-file-tokio = "0.1.0" pcap-file-tokio = "0.1.0"
thiserror = "1.0.50" thiserror = "1.0.50"
telcom-parser = { path = "../telcom-parser" } telcom-parser = { path = "../telcom-parser" }

View File

@@ -3,8 +3,15 @@ use chrono::{DateTime, FixedOffset};
use serde::Serialize; use serde::Serialize;
use crate::{diag::MessagesContainer, gsmtap_parser}; use crate::{diag::MessagesContainer, gsmtap_parser};
use crate::util::RuntimeMetadata;
use super::{imsi_provided::ImsiProvidedAnalyzer, information_element::InformationElement, lte_downgrade::LteSib6And7DowngradeAnalyzer, null_cipher::NullCipherAnalyzer}; use super::{
imsi_requested::ImsiRequestedAnalyzer,
information_element::InformationElement,
connection_redirect_downgrade::ConnectionRedirect2GDowngradeAnalyzer,
priority_2g_downgrade::LteSib6And7DowngradeAnalyzer,
null_cipher::NullCipherAnalyzer,
};
/// Qualitative measure of how severe a Warning event type is. /// Qualitative measure of how severe a Warning event type is.
/// The levels should break down like this: /// The levels should break down like this:
@@ -18,7 +25,7 @@ pub enum Severity {
High, High,
} }
/// [QualitativeWarning] events will always be shown to the user in some manner, /// `QualitativeWarning` events will always be shown to the user in some manner,
/// while `Informational` ones may be hidden based on user settings. /// while `Informational` ones may be hidden based on user settings.
#[derive(Serialize, Debug, Clone)] #[derive(Serialize, Debug, Clone)]
#[serde(tag = "type")] #[serde(tag = "type")]
@@ -67,6 +74,7 @@ pub struct AnalyzerMetadata {
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct ReportMetadata { pub struct ReportMetadata {
pub analyzers: Vec<AnalyzerMetadata>, pub analyzers: Vec<AnalyzerMetadata>,
pub rayhunter: RuntimeMetadata,
} }
#[derive(Serialize, Debug, Clone)] #[derive(Serialize, Debug, Clone)]
@@ -89,11 +97,9 @@ impl AnalysisRow {
pub fn contains_warnings(&self) -> bool { pub fn contains_warnings(&self) -> bool {
for analysis in &self.analysis { for analysis in &self.analysis {
for maybe_event in &analysis.events { for event in analysis.events.iter().flatten() {
if let Some(event) = maybe_event { if matches!(event.event_type, EventType::QualitativeWarning { .. }) {
if matches!(event.event_type, EventType::QualitativeWarning { .. }) { return true;
return true;
}
} }
} }
} }
@@ -112,9 +118,14 @@ impl Harness {
pub fn new_with_all_analyzers() -> Self { pub fn new_with_all_analyzers() -> Self {
let mut harness = Harness::new(); let mut harness = Harness::new();
harness.add_analyzer(Box::new(ImsiRequestedAnalyzer::new()));
harness.add_analyzer(Box::new(ConnectionRedirect2GDowngradeAnalyzer{}));
harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer{})); harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer{}));
harness.add_analyzer(Box::new(ImsiProvidedAnalyzer{}));
harness.add_analyzer(Box::new(NullCipherAnalyzer{})); // FIXME: our RRC parser is reporting false positives for this due to an
// upstream hampi bug (https://github.com/ystero-dev/hampi/issues/133).
// once that's fixed, we should regenerate our parser and re-enable this
// harness.add_analyzer(Box::new(NullCipherAnalyzer{}));
harness harness
} }
@@ -198,8 +209,11 @@ impl Harness {
}); });
} }
let rayhunter = RuntimeMetadata::new();
ReportMetadata { ReportMetadata {
analyzers, analyzers,
rayhunter,
} }
} }
} }

View File

@@ -0,0 +1,42 @@
use std::borrow::Cow;
use super::analyzer::{Analyzer, Event, EventType, Severity};
use super::information_element::{InformationElement, LteInformationElement};
use telcom_parser::lte_rrc::{DL_DCCH_Message, DL_DCCH_MessageType, DL_DCCH_MessageType_c1, RRCConnectionReleaseCriticalExtensions, RRCConnectionReleaseCriticalExtensions_c1, RedirectedCarrierInfo};
use super::util::unpack;
// Based on HITBSecConf presentation "Forcing a targeted LTE cellphone into an
// eavesdropping network" by Lin Huang
pub struct ConnectionRedirect2GDowngradeAnalyzer {
}
// TODO: keep track of SIB state to compare LTE reselection blocks w/ 2g/3g ones
impl Analyzer for ConnectionRedirect2GDowngradeAnalyzer {
fn get_name(&self) -> Cow<str> {
Cow::from("Connection Release/Redirected Carrier 2G Downgrade")
}
fn get_description(&self) -> Cow<str> {
Cow::from("Tests if a cell releases our connection and redirects us to a 2G cell.")
}
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> {
unpack!(InformationElement::LTE(lte_ie) = ie);
unpack!(LteInformationElement::DlDcch(DL_DCCH_Message { message }) = lte_ie);
unpack!(DL_DCCH_MessageType::C1(c1) = message);
unpack!(DL_DCCH_MessageType_c1::RrcConnectionRelease(release) = c1);
unpack!(RRCConnectionReleaseCriticalExtensions::C1(c1) = &release.critical_extensions);
unpack!(RRCConnectionReleaseCriticalExtensions_c1::RrcConnectionRelease_r8(r8_ies) = c1);
unpack!(Some(carrier_info) = &r8_ies.redirected_carrier_info);
match carrier_info {
RedirectedCarrierInfo::Geran(_carrier_freqs_geran) => Some(Event {
event_type: EventType::QualitativeWarning { severity: Severity::High },
message: "Detected 2G downgrade".to_owned(),
}),
_ => Some(Event {
event_type: EventType::Informational,
message: format!("RRCConnectionRelease CarrierInfo: {:?}", carrier_info),
}),
}
}
}

View File

@@ -0,0 +1,59 @@
use std::borrow::Cow;
use super::analyzer::{Analyzer, Event, EventType, Severity};
use super::information_element::{InformationElement, LteInformationElement};
const PACKET_THRESHHOLD: usize = 150;
pub struct ImsiRequestedAnalyzer {
packet_num: usize,
}
impl ImsiRequestedAnalyzer {
pub fn new() -> Self {
Self { packet_num: 0 }
}
}
impl Analyzer for ImsiRequestedAnalyzer {
fn get_name(&self) -> Cow<str> {
Cow::from("IMSI Requested")
}
fn get_description(&self) -> Cow<str> {
Cow::from("Tests whether the ME sends an IMSI Identity Request NAS message")
}
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> {
self.packet_num += 1;
let InformationElement::LTE(LteInformationElement::NAS(payload)) = ie else {
return None;
};
// NAS identity request, ID type IMSI
if payload == &[0x07, 0x55, 0x01] {
if self.packet_num < PACKET_THRESHHOLD {
return Some(Event {
event_type: EventType::QualitativeWarning {
severity: Severity::Medium
},
message: format!(
"NAS IMSI identity request detected, however it was within \
the first {} packets of this analysis. If you just \
turned your device on, this is likely a \
false-positive.",
PACKET_THRESHHOLD
)
})
} else {
return Some(Event {
event_type: EventType::QualitativeWarning {
severity: Severity::High
},
message: "NAS IMSI identity request detected".to_owned(),
})
}
}
None
}
}

View File

@@ -5,7 +5,7 @@
use telcom_parser::{decode, lte_rrc}; use telcom_parser::{decode, lte_rrc};
use thiserror::Error; use thiserror::Error;
use crate::gsmtap::{GsmtapType, LteRrcSubtype, GsmtapMessage}; use crate::gsmtap::{GsmtapMessage, GsmtapType, LteNasSubtype, LteRrcSubtype};
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum InformationElementError { pub enum InformationElementError {
@@ -40,6 +40,9 @@ pub enum LteInformationElement {
SbcchSlBch(lte_rrc::SBCCH_SL_BCH_Message), SbcchSlBch(lte_rrc::SBCCH_SL_BCH_Message),
SbcchSlBchV2x(lte_rrc::SBCCH_SL_BCH_Message_V2X_r14), SbcchSlBchV2x(lte_rrc::SBCCH_SL_BCH_Message_V2X_r14),
// FIXME: actually parse NAS messages
NAS(Vec<u8>),
// FIXME: unclear which message these "NB" types map to // FIXME: unclear which message these "NB" types map to
//DlCcchNb(), //DlCcchNb(),
//DlDcchNb(), //DlDcchNb(),
@@ -79,6 +82,9 @@ impl TryFrom<&GsmtapMessage> for InformationElement {
}; };
Ok(InformationElement::LTE(lte)) Ok(InformationElement::LTE(lte))
}, },
GsmtapType::LteNas(LteNasSubtype::Plain) => {
Ok(InformationElement::LTE(LteInformationElement::NAS(gsmtap_msg.payload.clone())))
},
_ => Err(InformationElementError::UnsupportedGsmtapType(gsmtap_msg.header.gsmtap_type)), _ => Err(InformationElementError::UnsupportedGsmtapType(gsmtap_msg.header.gsmtap_type)),
} }
} }

View File

@@ -1,5 +1,8 @@
pub mod analyzer; pub mod analyzer;
pub mod information_element; pub mod information_element;
pub mod lte_downgrade; pub mod priority_2g_downgrade;
pub mod connection_redirect_downgrade;
pub mod imsi_provided; pub mod imsi_provided;
pub mod imsi_requested;
pub mod null_cipher; pub mod null_cipher;
pub mod util;

View File

@@ -29,18 +29,18 @@ impl NullCipherAnalyzer {
} }
// Use map/flatten to dig into a long chain of nested Option types // Use map/flatten to dig into a long chain of nested Option types
let maybe_v1250 = c1.non_critical_extension.as_ref() let maybe_v1250 = c1.non_critical_extension.as_ref()
.map(|v890| v890.non_critical_extension.as_ref()).flatten() .and_then(|v890| v890.non_critical_extension.as_ref())
.map(|v920| v920.non_critical_extension.as_ref()).flatten() .and_then(|v920| v920.non_critical_extension.as_ref())
.map(|v1020| v1020.non_critical_extension.as_ref()).flatten() .and_then(|v1020| v1020.non_critical_extension.as_ref())
.map(|v1130| v1130.non_critical_extension.as_ref()).flatten(); .and_then(|v1130| v1130.non_critical_extension.as_ref());
let Some(v1250) = maybe_v1250 else { let Some(v1250) = maybe_v1250 else {
return false; return false;
}; };
if let Some(SCG_Configuration_r12::Setup(scg_setup)) = v1250.scg_configuration_r12.as_ref() { if let Some(SCG_Configuration_r12::Setup(scg_setup)) = v1250.scg_configuration_r12.as_ref() {
let maybe_cipher = scg_setup.scg_config_part_scg_r12.as_ref() let maybe_cipher = scg_setup.scg_config_part_scg_r12.as_ref()
.map(|scg| scg.mobility_control_info_scg_r12.as_ref()).flatten() .and_then(|scg| scg.mobility_control_info_scg_r12.as_ref())
.map(|mci| mci.ciphering_algorithm_scg_r12.as_ref()).flatten(); .and_then(|mci| mci.ciphering_algorithm_scg_r12.as_ref());
if let Some(cipher) = maybe_cipher { if let Some(cipher) = maybe_cipher {
if cipher.0 == CipheringAlgorithm_r12::EEA0 { if cipher.0 == CipheringAlgorithm_r12::EEA0 {
return true; return true;
@@ -49,10 +49,10 @@ impl NullCipherAnalyzer {
} }
let maybe_v1530_security_config = v1250.non_critical_extension.as_ref() let maybe_v1530_security_config = v1250.non_critical_extension.as_ref()
.map(|v1310| v1310.non_critical_extension.as_ref()).flatten() .and_then(|v1310| v1310.non_critical_extension.as_ref())
.map(|v1430| v1430.non_critical_extension.as_ref()).flatten() .and_then(|v1430| v1430.non_critical_extension.as_ref())
.map(|v1510| v1510.non_critical_extension.as_ref()).flatten() .and_then(|v1510| v1510.non_critical_extension.as_ref())
.map(|v1530| v1530.security_config_ho_v1530.as_ref()).flatten(); .and_then(|v1530| v1530.security_config_ho_v1530.as_ref());
let Some(v1530_security_config) = maybe_v1530_security_config else { let Some(v1530_security_config) = maybe_v1530_security_config else {
return false; return false;
}; };

32
lib/src/analysis/util.rs Normal file
View File

@@ -0,0 +1,32 @@
// Unpacks a pattern, or returns None.
//
// # Examples
// You can use `unpack!` to unroll highly nested enums like this:
// ```
// enum Foo {
// A(Bar),
// B,
// }
//
// enum Bar {
// C(Baz)
// }
//
// struct Baz;
//
// fn get_bang(foo: Foo) -> Option<Baz> {
// unpack!(Foo::A(bar) = foo);
// unpack!(Bar::C(baz) = bar);
// baz
// }
// ```
//
macro_rules! unpack {
($pat:pat = $val:expr) => {
let $pat = $val else { return None; };
};
}
// this is apparently how you make a macro publicly usable from this module
pub(crate) use unpack;

View File

@@ -183,6 +183,8 @@ pub enum LogBody {
// * 0xb0ed: plain EMM NAS message (outgoing) // * 0xb0ed: plain EMM NAS message (outgoing)
#[deku(id_pat = "0xb0e2 | 0xb0e3 | 0xb0ec | 0xb0ed")] #[deku(id_pat = "0xb0e2 | 0xb0e3 | 0xb0ec | 0xb0ed")]
Nas4GMessage { Nas4GMessage {
#[deku(ctx = "log_type")]
direction: Nas4GMessageDirection,
ext_header_version: u8, ext_header_version: u8,
rrc_rel: u8, rrc_rel: u8,
rrc_version_minor: u8, rrc_version_minor: u8,
@@ -211,6 +213,19 @@ pub enum LogBody {
} }
} }
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
#[deku(ctx = "log_type: u16", id = "log_type")]
pub enum Nas4GMessageDirection {
// * 0xb0e2: plain ESM NAS message (incoming)
// * 0xb0e3: plain ESM NAS message (outgoing)
// * 0xb0ec: plain EMM NAS message (incoming)
// * 0xb0ed: plain EMM NAS message (outgoing)
#[deku(id_pat = "0xb0e2 | 0xb0ec")]
Downlink,
#[deku(id_pat = "0xb0e3 | 0xb0ed")]
Uplink,
}
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)] #[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
#[deku(ctx = "ext_header_version: u8", id = "ext_header_version")] #[deku(ctx = "ext_header_version: u8", id = "ext_header_version")]
pub enum LteRrcOtaPacket { pub enum LteRrcOtaPacket {

View File

@@ -200,6 +200,11 @@ pub struct GsmtapHeader {
#[deku(update = "self.gsmtap_type.get_type()")] #[deku(update = "self.gsmtap_type.get_type()")]
pub packet_type: u8, pub packet_type: u8,
pub timeslot: u8, pub timeslot: u8,
#[deku(bits = 1)]
pub pcs_band_indicator: bool,
#[deku(bits = 1)]
pub uplink: bool,
#[deku(bits = 14)]
pub arfcn: u16, pub arfcn: u16,
pub signal_dbm: i8, pub signal_dbm: i8,
pub signal_noise_ratio_db: u8, pub signal_noise_ratio_db: u8,
@@ -222,6 +227,8 @@ impl GsmtapHeader {
header_len: 4, header_len: 4,
packet_type: gsmtap_type.get_type(), packet_type: gsmtap_type.get_type(),
timeslot: 0, timeslot: 0,
pcs_band_indicator: false,
uplink: false,
arfcn: 0, arfcn: 0,
signal_dbm: 0, signal_dbm: 0,
signal_noise_ratio_db: 0, signal_noise_ratio_db: 0,

View File

@@ -99,7 +99,6 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
_ => return Err(GsmtapParserError::InvalidLteRrcOtaExtHeaderVersion(ext_header_version)), _ => return Err(GsmtapParserError::InvalidLteRrcOtaExtHeaderVersion(ext_header_version)),
}; };
let mut header = GsmtapHeader::new(gsmtap_type); let mut header = GsmtapHeader::new(gsmtap_type);
// Wireshark GSMTAP only accepts 14 bits of ARFCN
header.arfcn = packet.get_earfcn().try_into().unwrap_or(0); header.arfcn = packet.get_earfcn().try_into().unwrap_or(0);
header.frame_number = packet.get_sfn(); header.frame_number = packet.get_sfn();
header.subslot = packet.get_subfn(); header.subslot = packet.get_subfn();
@@ -108,9 +107,10 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
payload: packet.take_payload(), payload: packet.take_payload(),
})) }))
}, },
LogBody::Nas4GMessage { msg, .. } => { LogBody::Nas4GMessage { msg, direction, .. } => {
// currently we only handle "plain" (i.e. non-secure) NAS messages // currently we only handle "plain" (i.e. non-secure) NAS messages
let header = GsmtapHeader::new(GsmtapType::LteNas(LteNasSubtype::Plain)); let mut header = GsmtapHeader::new(GsmtapType::LteNas(LteNasSubtype::Plain));
header.uplink = matches!(direction, Nas4GMessageDirection::Uplink);
Ok(Some(GsmtapMessage { Ok(Some(GsmtapMessage {
header, header,
payload: msg, payload: msg,

View File

@@ -7,6 +7,7 @@ pub mod gsmtap;
pub mod gsmtap_parser; pub mod gsmtap_parser;
pub mod pcap; pub mod pcap;
pub mod analysis; pub mod analysis;
pub mod util;
// re-export telcom_parser, since we use its types in our API // re-export telcom_parser, since we use its types in our API
pub use telcom_parser; pub use telcom_parser;

View File

@@ -9,8 +9,9 @@ use chrono::prelude::*;
use deku::prelude::*; use deku::prelude::*;
use pcap_file_tokio::pcapng::blocks::enhanced_packet::EnhancedPacketBlock; use pcap_file_tokio::pcapng::blocks::enhanced_packet::EnhancedPacketBlock;
use pcap_file_tokio::pcapng::blocks::interface_description::InterfaceDescriptionBlock; use pcap_file_tokio::pcapng::blocks::interface_description::InterfaceDescriptionBlock;
use pcap_file_tokio::pcapng::blocks::section_header::{SectionHeaderBlock, SectionHeaderOption};
use pcap_file_tokio::pcapng::PcapNgWriter; use pcap_file_tokio::pcapng::PcapNgWriter;
use pcap_file_tokio::PcapError; use pcap_file_tokio::{Endianness, PcapError};
use thiserror::Error; use thiserror::Error;
#[derive(Error, Debug)] #[derive(Error, Debug)]
@@ -60,7 +61,20 @@ struct UdpHeader {
impl<T> GsmtapPcapWriter<T> where T: AsyncWrite + Unpin + Send { impl<T> GsmtapPcapWriter<T> where T: AsyncWrite + Unpin + Send {
pub async fn new(writer: T) -> Result<Self, GsmtapPcapError> { pub async fn new(writer: T) -> Result<Self, GsmtapPcapError> {
let writer = PcapNgWriter::new(writer).await?; let metadata = crate::util::RuntimeMetadata::new();
let package = format!("{} {}", env!("CARGO_PKG_NAME").to_owned(), metadata.rayhunter_version);
let section = SectionHeaderBlock {
endianness: Endianness::Big,
major_version: 1,
minor_version: 0,
section_length: -1,
options: vec![
SectionHeaderOption::Hardware(Cow::from(metadata.arch)),
SectionHeaderOption::OS(Cow::from(metadata.system_os)),
SectionHeaderOption::UserApplication(Cow::from(package)),
],
};
let writer = PcapNgWriter::with_section_header(writer, section).await?;
Ok(GsmtapPcapWriter { writer, ip_id: 0 }) Ok(GsmtapPcapWriter { writer, ip_id: 0 })
} }

View File

@@ -60,7 +60,7 @@ impl<T> QmdlReader<T> where T: AsyncRead + Unpin {
}) })
} }
async fn get_next_messages_container(&mut self) -> Result<Option<MessagesContainer>, std::io::Error> { pub async fn get_next_messages_container(&mut self) -> Result<Option<MessagesContainer>, std::io::Error> {
if let Some(max_bytes) = self.max_bytes { if let Some(max_bytes) = self.max_bytes {
if self.bytes_read >= max_bytes { if self.bytes_read >= max_bytes {
if self.bytes_read > max_bytes { if self.bytes_read > max_bytes {

37
lib/src/util.rs Normal file
View File

@@ -0,0 +1,37 @@
use nix::sys::utsname::uname;
use serde::Serialize;
/// Expose binary and system information.
#[derive(Serialize, Debug)]
pub struct RuntimeMetadata {
/// The cargo package version from this library's cargo.toml, e.g., "1.2.3".
pub rayhunter_version: String,
/// The operating system `sysname` and optionally `release`. e.g., "Linux 3.18.48" or "linux".
pub system_os: String,
/// The CPU architecture in use. e.g., "armv7l" or "arm".
pub arch: String,
}
impl RuntimeMetadata {
/// Return the binary and system information, attempting to retrieve
/// attributes from `uname(2)` and falling back to values from
/// `std::env::consts`.
pub fn new() -> Self {
match uname() {
Ok(utsname) => RuntimeMetadata {
rayhunter_version: env!("CARGO_PKG_VERSION").to_owned(),
arch: format!("{}", utsname.machine().to_string_lossy()),
system_os: format!(
"{} {}",
utsname.sysname().to_string_lossy(),
utsname.release().to_string_lossy(),
),
},
Err(_) => RuntimeMetadata {
rayhunter_version: env!("CARGO_PKG_VERSION").to_owned(),
arch: std::env::consts::ARCH.to_string(),
system_os: std::env::consts::OS.to_string(),
},
}
}
}

View File

@@ -1,4 +1,6 @@
#!/bin/sh #!/bin/sh
cargo build --release --target="armv7-unknown-linux-gnueabihf" #--features debug cargo build --release --target="armv7-unknown-linux-gnueabihf" #--features debug
adb shell '/bin/rootshell -c "/etc/init.d/rayhunter_daemon stop"'
adb push target/armv7-unknown-linux-gnueabihf/release/rayhunter-daemon /data/rayhunter/rayhunter-daemon adb push target/armv7-unknown-linux-gnueabihf/release/rayhunter-daemon /data/rayhunter/rayhunter-daemon
adb shell '/bin/rootshell -c "/etc/init.d/rayhunter_daemon restart"' echo "rebooting the device..."
adb shell '/bin/rootshell -c "reboot"'

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "rootshell" name = "rootshell"
version = "0.1.0" version = "0.2.6"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -25,9 +25,12 @@ fn main() {
// discard argv[0] // discard argv[0]
let _ = args.next(); let _ = args.next();
Command::new("/bin/bash") // This call will only return if there is an error
let error = Command::new("/bin/bash")
.args(args) .args(args)
.uid(0) .uid(0)
.gid(0) .gid(0)
.exec(); .exec();
eprintln!("Error running command: {error}");
std::process::exit(1);
} }

View File

@@ -1,9 +1,10 @@
[package] [package]
name = "serial" name = "serial"
version = "0.1.0" version = "0.2.6"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
rusb = { version = "0.9.3", features = ["vendored"] } nusb = "0.1.13"
tokio = { version = "1.44.1", features = ["macros", "rt", "time"] }

View File

@@ -7,24 +7,14 @@
//! //!
//! No device found - make sure your device is plugged in and turned on. If it is, it's possible you have a device with a different //! No device found - make sure your device is plugged in and turned on. If it is, it's possible you have a device with a different
//! usb id, file a bug with the output of `lsusb` attached. //! usb id, file a bug with the output of `lsusb` attached.
//!
//! # Examples
//! ```
//! match rusb::Context::new() {
//! Ok(mut context) => match open_orbic(&mut context) {
//! Some(mut handle) => {
//! send_command(&mut handle, &args[1])
//! },
//! None => panic!("No Orbic device found"),
//! },
//! Err(e) => panic!("Failed to initialize libusb: {0}", e),
//! ````
use std::str; use std::str;
use std::time::Duration; use std::time::Duration;
use rusb::{Context, DeviceHandle, UsbContext}; use nusb::transfer::{Control, ControlType, Recipient, RequestBuffer};
use nusb::{Device, Interface};
fn main() { #[tokio::main(flavor = "current_thread")]
async fn main() {
let args: Vec<String> = std::env::args().collect(); let args: Vec<String> = std::env::args().collect();
if args.len() != 2 { if args.len() != 2 {
@@ -32,53 +22,64 @@ fn main() {
return; return;
} }
match Context::new() { if args[1] == "--root" {
Ok(mut context) => { enable_command_mode();
if args[1] == "--root" { } else {
enable_command_mode(&mut context); match open_orbic() {
} else { Some(interface) => send_command(interface, &args[1]).await,
match open_orbic(&mut context) { None => panic!("No Orbic device found"),
Some(mut handle) => send_command(&mut handle, &args[1]), }
None => panic!("No Orbic device found"),
}
}
},
Err(e) => panic!("Failed to initialize libusb: {0}", e),
} }
} }
/// Sends an AT command to the usb device over the serial port /// Sends an AT command to the usb device over the serial port
/// ///
/// First establish a USB handle and context by calling `open_orbic(<T>) /// First establish a USB handle and context by calling `open_orbic(<T>)
fn send_command<T: UsbContext>(handle: &mut DeviceHandle<T>, command: &str) { async fn send_command(interface: Interface, command: &str) {
let mut data = String::new(); let mut data = String::new();
data.push_str("\r\n"); data.push_str("\r\n");
data.push_str(command); data.push_str(command);
data.push_str("\r\n"); data.push_str("\r\n");
let timeout = Duration::from_secs(1); let timeout = Duration::from_secs(1);
let mut response = [0; 256];
let enable_serial_port = Control {
control_type: ControlType::Class,
recipient: Recipient::Interface,
request: 0x22,
value: 3,
index: 1,
};
// Set up the serial port appropriately // Set up the serial port appropriately
handle interface
.write_control(0x21, 0x22, 3, 1, &[], timeout) .control_out_blocking(enable_serial_port, &[], timeout)
.expect("Failed to send control request"); .expect("Failed to send control request");
// Send the command // Send the command
handle tokio::time::timeout(timeout, interface.bulk_out(0x2, data.as_bytes().to_vec()))
.write_bulk(0x2, data.as_bytes(), timeout) .await
.expect("Timed out writing command")
.into_result()
.expect("Failed to write command"); .expect("Failed to write command");
// Consume the echoed command // Consume the echoed command
handle tokio::time::timeout(timeout, interface.bulk_in(0x82, RequestBuffer::new(256)))
.read_bulk(0x82, &mut response, timeout) .await
.expect("Timed out reading submitted command")
.into_result()
.expect("Failed to read submitted command"); .expect("Failed to read submitted command");
// Read the actual response // Read the actual response
handle let response = tokio::time::timeout(timeout, interface.bulk_in(0x82, RequestBuffer::new(256)))
.read_bulk(0x82, &mut response, timeout) .await
.expect("Timed out reading response")
.into_result()
.expect("Failed to read response"); .expect("Failed to read response");
let responsestr = str::from_utf8(&response).expect("Failed to parse response"); // For some reason, on macOS the response buffer gets filled with garbage data that's
// rarely valid UTF-8. Luckily we only care about the first couple bytes, so just drop
// the garbage with `from_utf8_lossy` and look for our expected success string.
let responsestr = String::from_utf8_lossy(&response);
if !responsestr.contains("\r\nOK\r\n") { if !responsestr.contains("\r\nOK\r\n") {
println!("Received unexpected response{0}", responsestr); println!("Received unexpected response{0}", responsestr);
std::process::exit(1); std::process::exit(1);
@@ -88,18 +89,26 @@ fn send_command<T: UsbContext>(handle: &mut DeviceHandle<T>, command: &str) {
/// Send a command to switch the device into generic mode, exposing serial /// Send a command to switch the device into generic mode, exposing serial
/// ///
/// If the device reboots while the command is still executing you may get a pipe error here, not sure what to do about this race condition. /// If the device reboots while the command is still executing you may get a pipe error here, not sure what to do about this race condition.
fn enable_command_mode<T: UsbContext>(context: &mut T) { fn enable_command_mode() {
if open_orbic(context).is_some() { if open_orbic().is_some() {
println!("Device already in command mode. Doing nothing..."); println!("Device already in command mode. Doing nothing...");
return; return;
} }
let timeout = Duration::from_secs(1); let timeout = Duration::from_secs(1);
if let Some(handle) = open_device(context, 0x05c6, 0xf626) {
if let Err(e) = handle.write_control(0x40, 0xa0, 0, 0, &[], timeout) { if let Some(interface) = open_device(0x05c6, 0xf626) {
let enable_command_mode = Control {
control_type: ControlType::Vendor,
recipient: Recipient::Device,
request: 0xa0,
value: 0,
index: 0,
};
if let Err(e) = interface.control_out_blocking(enable_command_mode, &[], timeout) {
// If the device reboots while the command is still executing we // If the device reboots while the command is still executing we
// may get a pipe error here // may get a pipe error here
if e == rusb::Error::Pipe { if e == nusb::transfer::TransferError::Stall {
return; return;
} }
panic!("Failed to send device switch control request: {0}", e) panic!("Failed to send device switch control request: {0}", e)
@@ -110,41 +119,38 @@ fn enable_command_mode<T: UsbContext>(context: &mut T) {
panic!("No Orbic device found"); panic!("No Orbic device found");
} }
/// Get a handle and contet for the orbic device /// Get an Interface for the orbic device
fn open_orbic<T: UsbContext>(context: &mut T) -> Option<DeviceHandle<T>> { fn open_orbic() -> Option<Interface> {
// Device after initial mode switch // Device after initial mode switch
if let Some(mut handle) = open_device(context, 0x05c6, 0xf601) { if let Some(device) = open_device(0x05c6, 0xf601) {
handle.set_auto_detach_kernel_driver(true).expect("set_auto_detach_kernel_driver failed"); let interface = device
handle.claim_interface(1).expect("claim_interface(1) failed"); .detach_and_claim_interface(1) // will reattach drivers on release
return Some(handle); .expect("detach_and_claim_interface(1) failed");
return Some(interface);
} }
// Device with rndis enabled as well // Device with rndis enabled as well
if let Some(mut handle) = open_device(context, 0x05c6, 0xf622) { if let Some(device) = open_device(0x05c6, 0xf622) {
handle.set_auto_detach_kernel_driver(true).expect("set_auto_detach_kernel_driver failed"); let interface = device
handle.claim_interface(1).expect("claim_interface(1) failed"); .detach_and_claim_interface(1) // will reattach drivers on release
return Some(handle); .expect("detach_and_claim_interface(1) failed");
return Some(interface);
} }
None None
} }
/// Generic function to open a USB device /// General function to open a USB device
fn open_device<T: UsbContext>(context: &mut T, vid: u16, pid: u16) -> Option<DeviceHandle<T>> { fn open_device(vid: u16, pid: u16) -> Option<Device> {
let devices = match context.devices() { let devices = match nusb::list_devices() {
Ok(d) => d, Ok(d) => d,
Err(_) => return None, Err(_) => return None,
}; };
for device in devices.iter() { for device in devices {
let device_desc = match device.device_descriptor() { if device.vendor_id() == vid && device.product_id() == pid {
Ok(d) => d,
Err(_) => continue,
};
if device_desc.vendor_id() == vid && device_desc.product_id() == pid {
match device.open() { match device.open() {
Ok(handle) => return Some(handle), Ok(d) => return Some(d),
Err(e) => panic!("device found but failed to open: {}", e), Err(e) => panic!("device found but failed to open: {}", e),
} }
} }

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "telcom-parser" name = "telcom-parser"
version = "0.1.0" version = "0.2.6"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

60
tools/nasparse.py Executable file
View File

@@ -0,0 +1,60 @@
#!/usr/bin/python3
import pycrate_mobile
from pycrate_mobile import NASLTE
import pycrate_core
import binascii
import sys
import pprint
from enum import Enum
import pycrate_mobile.TS24301_EMM
EPS_IMSI_ATTACH = 2
def parse_nas_message(buffer, uplink=None):
if isinstance(buffer, str): #handle string argument or raw bytes
bin = binascii.unhexlify(buffer)
else:
bin = buffer
if uplink:
parsed = NASLTE.parse_NASLTE_MO(bin)
elif uplink == None: #We don't know if its an up or downlink
parsed = NASLTE.parse_NASLTE_MO(bin)
if parsed[0] == None:
parsed = NASLTE.parse_NASLTE_MT(bin)
else:
parsed = NASLTE.parse_NASLTE_MT(bin)
if parsed[0] is None: # Not a NAS Packet
raise TypeError("Not a nas packet")
return parsed[0]
def heur_ue_imsi_sent(msg):
output = "device transmitted IMSI to base station!"
if type(msg) not in [pycrate_mobile.TS24301_EMM.EMMAttachRequest, pycrate_mobile.TS24301_EMM.EMMSecProtNASMessage]:
return (False, None)
if isinstance(msg, pycrate_mobile.TS24301_EMM.EMMSecProtNASMessage):
try:
msg = msg['EMMAttachRequest']
except pycrate_core.elt.EltErr:
return (False, None)
if msg['EPSAttachType']['V'].to_int() == EPS_IMSI_ATTACH: #EPSAttachType Value is 'Combined EPS/IMSI Attach (2)'
return (True, output)
return (False, None)
if __name__ == "__main__":
if len(sys.argv) != 2:
print("usage: nasparse.py [hex encoded nas message]")
exit(1)
buffer = sys.argv[1]
msg = parse_nas_message(buffer)
pprint.pprint(msg)
triggered, message = heur_ue_imsi_sent(msg)
if triggered:
print(message)
exit(1)

38
tools/nasparse_test.py Normal file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/python3
import unittest
import nasparse
class TestNasparse(unittest.TestCase):
imsi_sent_msg = '07412208391185184409309005f0700000100030023ed031d127298080211001000010810600000000830600000000000d00000300ff0003130184000a000005000010005c0a009011034f18a6f15d0103c1000000000000'
sec_imsi_sent_msg = '1727db4b7c0207412208391185184409309005f0700000100030023ed031d127298080211001000010810600000000830600000000000d00000300ff0003130184000a000005000010005c0a009011034f18a6f15d0103c1'
non_nas_msg = 'deadbeefcafe'
other_nas_msg = '074413780004023fd121'
other_nas_mt_msg = "023fd12100000000000000000000000000000000000000000000000000000000"
ciphered_nas_msg = "27ed6146bd0162a5d62d62e1ce501720dc8bd84f1167fd"
def run_heur(self, msg):
buf = nasparse.parse_nas_message(msg)
return nasparse.heur_ue_imsi_sent(buf)[0]
def test_imsi_sent(self):
self.assertEqual(self.run_heur(self.imsi_sent_msg), True, "imsi_sent_msg should trigger heuristic")
def test_sec_imsi_sent(self):
self.assertEqual(self.run_heur(self.imsi_sent_msg), True, "sec_imsi_sent_msg should trigger heuristic")
def test_non_nas_msg(self):
with self.assertRaises(TypeError):
self.run_heur(self.non_nas_msg)
def test_other_nas(self):
self.assertEqual(self.run_heur(self.other_nas_msg), False, "other_nas_msg should not trigger heuristic")
def test_other_nas_mt(self):
self.assertEqual(self.run_heur(self.other_nas_mt_msg), False, "other_nas_mt_msg should not trigger heuristic")
def test_ciphered_nas(self):
self.assertEqual(self.run_heur(self.ciphered_nas_msg), False, "ciphered_nas_msg should not trigger heuristic")
if __name__ == '__main__':
unittest.main()

38
tools/pcap_check.py Executable file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/python3
import nasparse
from scapy.utils import RawPcapNgReader
import sys
TYPE_LTE_NAS = 0x12
UDP_LEN = 28
def process_pcap(pcap_path):
print('Opening {}...'.format(pcap_path))
count = 0
for pkt_data, pkt_metadata in RawPcapNgReader(pcap_path):
count += 1
gsmtap_len = pkt_data[UDP_LEN+1] * 4 # gsmtap header length is stored in the 2nd byte of GSMTAP as a number of 32 bit words
header_end = gsmtap_len + UDP_LEN #length of UDP/IP header plus GSMTAP header
gsmtap_hdr = pkt_data[UDP_LEN:header_end]
if gsmtap_hdr[2] != TYPE_LTE_NAS:
continue
# uplink status is the 7th bit of the 5th byte of the GSMTAP header.
# Uplink (Mobile originated) = 0 Downlink (mobile terminated) = 1
uplink = (gsmtap_hdr[4] & 0b01000000) >> 6
buffer = pkt_data[header_end:]
msg = nasparse.parse_nas_message(buffer, uplink)
triggered, message = nasparse.heur_ue_imsi_sent(msg)
if triggered:
print(f"Frame {count} triggered heuristic: {message}")
if __name__ == "__main__":
if len(sys.argv) != 2:
print("usage: pcap_check.py [path/to/pcap/file]")
exit(1)
pcap_path = sys.argv[1]
process_pcap(pcap_path)

View File

@@ -1,4 +1,5 @@
asn1tools==0.166.0 asn1tools==0.166.0
bitstruct==8.19.0 bitstruct==8.19.0
diskcache==5.6.3 diskcache==5.6.3
pycrate==0.7.8
pyparsing==3.1.2 pyparsing==3.1.2