mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-30 06:09:26 -07:00
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bc8a7892b | ||
|
|
431a97ca65 | ||
|
|
0364bfbc98 | ||
|
|
996e47684c | ||
|
|
266f2b2e53 | ||
|
|
2080cd7845 | ||
|
|
9af8e006b0 | ||
|
|
e841e22774 | ||
|
|
0d9f53f602 | ||
|
|
c9dcbbe5d6 | ||
|
|
61d6ff6510 | ||
|
|
e79dc4a8f0 | ||
|
|
6204bc0195 | ||
|
|
65b9843e39 | ||
|
|
d0d01089dd | ||
|
|
9c26e89b24 | ||
|
|
1f4786db19 | ||
|
|
88f81d86fa | ||
|
|
0b3c0de481 | ||
|
|
188e9f436b | ||
|
|
f2b5aa2743 | ||
|
|
b785a7f21c | ||
|
|
09d35ccec7 | ||
|
|
5ae186bc73 | ||
|
|
c765a40426 | ||
|
|
93cfbea361 | ||
|
|
8e6bed97b7 | ||
|
|
4214b27c0f | ||
|
|
f69487853a | ||
|
|
7eb61748d7 | ||
|
|
ca4e560e92 | ||
|
|
2ffb1d4620 | ||
|
|
77944dd17c | ||
|
|
50301076f0 | ||
|
|
21c839678b | ||
|
|
332a7ffbd0 | ||
|
|
8d250553b7 | ||
|
|
fa897e73fa | ||
|
|
c3494e338f | ||
|
|
f9b2cd6a59 | ||
|
|
eb072fb38c | ||
|
|
91f82fc71d | ||
|
|
6fda8450dc | ||
|
|
bbfe5877fe | ||
|
|
75d3740f66 | ||
|
|
94c576fd96 | ||
|
|
ee83613757 | ||
|
|
840f8ad8b0 | ||
|
|
c9ac834ca7 | ||
|
|
8629aacf6b | ||
|
|
a3fd1479f9 | ||
|
|
049c563f02 | ||
|
|
a33b5a3418 | ||
|
|
107ba58296 | ||
|
|
d016279172 | ||
|
|
5a084f1abb | ||
|
|
3619df32ab | ||
|
|
34d87d1fd7 | ||
|
|
da4952e70f | ||
|
|
30323b8329 | ||
|
|
28b0f409db | ||
|
|
12640cc878 | ||
|
|
26eda5904f | ||
|
|
3e26e61b05 | ||
|
|
565c0f1e67 |
@@ -1,3 +1,11 @@
|
||||
[target.armv7-unknown-linux-gnueabihf]
|
||||
linker = "arm-linux-gnueabihf-gcc"
|
||||
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"
|
||||
|
||||
18
.github/workflows/build-release.yml
vendored
18
.github/workflows/build-release.yml
vendored
@@ -12,12 +12,12 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
- os: ubuntu-latest
|
||||
serial_build_name: serial
|
||||
check_build_name: rayhunter-check
|
||||
- os: macos-latest
|
||||
serial_build_name: serial
|
||||
check_build_name: rayhunter-check
|
||||
- name: ubuntu-24
|
||||
os: ubuntu-latest
|
||||
- name: macos-arm
|
||||
os: macos-latest
|
||||
- name: macos-intel
|
||||
os: macos-13
|
||||
runs-on: ${{ matrix.platform.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -25,8 +25,8 @@ jobs:
|
||||
run: cargo build --bin serial --release
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: serial-${{ matrix.platform.os }}
|
||||
path: ./target/release/${{ matrix.platform.serial_build_name }}
|
||||
name: serial-${{ matrix.platform.name }}
|
||||
path: ./target/release/serial
|
||||
if-no-files-found: error
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build check
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rayhunter-check-${{ matrix.platform.os }}
|
||||
path: ./target/release/${{ matrix.platform.check_build_name }}
|
||||
path: ./target/release/rayhunter-check
|
||||
if-no-files-found: error
|
||||
build_rootshell_and_rayhunter:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
1
CODE_OF_CONDUCT.md
Normal file
1
CODE_OF_CONDUCT.md
Normal 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
185
Cargo.lock
generated
@@ -482,6 +482,26 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.6"
|
||||
@@ -602,6 +622,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "derive-into-owned"
|
||||
version = "0.2.0"
|
||||
@@ -767,6 +796,19 @@ version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.30"
|
||||
@@ -1088,6 +1130,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.12"
|
||||
@@ -1158,18 +1210,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.14"
|
||||
@@ -1201,6 +1241,15 @@ dependencies = [
|
||||
"imgref",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.7.3"
|
||||
@@ -1310,6 +1359,12 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.4.2"
|
||||
@@ -1360,6 +1415,34 @@ dependencies = [
|
||||
"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]]
|
||||
name = "object"
|
||||
version = "0.32.2"
|
||||
@@ -1375,6 +1458,12 @@ version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.1"
|
||||
@@ -1487,6 +1576,12 @@ dependencies = [
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.17"
|
||||
@@ -1654,6 +1749,7 @@ dependencies = [
|
||||
"futures-core",
|
||||
"libc",
|
||||
"log",
|
||||
"nix",
|
||||
"pcap-file-tokio",
|
||||
"serde",
|
||||
"telcom-parser",
|
||||
@@ -1679,6 +1775,7 @@ dependencies = [
|
||||
"rayhunter",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"simple_logger",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
@@ -1761,16 +1858,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.23"
|
||||
@@ -1874,7 +1961,8 @@ dependencies = [
|
||||
name = "serial"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rusb",
|
||||
"futures-lite",
|
||||
"nusb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1901,6 +1989,18 @@ dependencies = [
|
||||
"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]]
|
||||
name = "slab"
|
||||
version = "0.4.9"
|
||||
@@ -2065,6 +2165,39 @@ dependencies = [
|
||||
"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]]
|
||||
name = "tokio"
|
||||
version = "1.36.0"
|
||||
@@ -2263,12 +2396,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.0"
|
||||
|
||||
99
README.md
99
README.md
@@ -1,54 +1,61 @@
|
||||

|
||||
# Rayhunter
|
||||
|
||||
```
|
||||
@@@@@@@ @@@@@@ @@@ @@@ @@@ @@@ @@@ @@@ @@@ @@@ @@@@@@@ @@@@@@@@ @@@@@@@
|
||||
@@! @@@ @@! @@@ @@! !@@ @@! @@@ @@! @@@ @@!@!@@@ @@! @@! @@! @@@
|
||||
@!@!!@! @!@!@!@! !@!@! @!@!@!@! @!@ !@! @!@@!!@! @!! @!!!:! @!@!!@!
|
||||
!!: :!! !!: !!! !!: !!: !!! !!: !!! !!: !!! !!: !!: !!: :!!
|
||||
: : : : : : .: : : : :.:: : :: : : : :: ::: : : :
|
||||
|
||||
|
||||
_ _ _ _ _ _ _ _
|
||||
)`'-.,_)`'-.,_)`'-.,_)`'-.,_)`'-.,_)`'-.,_)`'-.,_)`'-.,_
|
||||
|
||||
O .
|
||||
O ' '
|
||||
o ' .
|
||||
o .'
|
||||
__________.-' '...___
|
||||
.-' ### '''...__
|
||||
/ a### ## ''--.._ ______
|
||||
'. # ######## ' .-'
|
||||
'-._ ..**********#### ___...---'''\ '
|
||||
'-._ __________...---''' \ l
|
||||
\ | apc '._|
|
||||
\__;
|
||||
```
|
||||

|
||||
|
||||
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
|
||||
|
||||
*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.
|
||||
2. Run the install script inside the bundle corresponding to your platform (`install-linux.sh`, `install-mac.sh`).
|
||||
3. Once finished, rayhunter should be running! You can verify this by visiting the web UI as described below.
|
||||
1. Download the latest [Rayhunter release bundle](https://github.com/EFForg/rayhunter/releases) and extract it.
|
||||
**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.**
|
||||
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
|
||||
|
||||
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.
|
||||
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
|
||||
* 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:
|
||||
@@ -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/)
|
||||
* [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
|
||||
* on your linux laptop 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:
|
||||
### If you're on x86 linux
|
||||
Install rust the usual way and then install cross compiling dependences:
|
||||
```
|
||||
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 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
|
||||
|
||||
* Build for arm using `cargo build`
|
||||
|
||||
* 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.
|
||||
|
||||
@@ -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`
|
||||
|
||||
## Documentation
|
||||
* Build docs locallly using `RUSTDOCFLAGS="--cfg docsrs" cargo doc --no-deps --all-features --open`
|
||||
## Support and Discussion
|
||||
|
||||
**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!*
|
||||
|
||||
5
SECURITY.md
Normal file
5
SECURITY.md
Normal 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).
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rayhunter-daemon"
|
||||
version = "0.1.0"
|
||||
version = "0.2.6"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
@@ -32,3 +32,4 @@ clap = { version = "4.5.2", features = ["derive"] }
|
||||
serde_json = "1.0.114"
|
||||
image = "0.25.1"
|
||||
tempfile = "3.10.1"
|
||||
simple_logger = "5.0.0"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::{collections::HashMap, future, path::PathBuf, pin::pin};
|
||||
use rayhunter::{analysis::analyzer::Harness, diag::DataType, gsmtap_parser, pcap::GsmtapPcapWriter, 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 clap::Parser;
|
||||
use futures::TryStreamExt;
|
||||
@@ -9,10 +10,10 @@ mod dummy_analyzer;
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about)]
|
||||
struct Args {
|
||||
#[arg(short, long)]
|
||||
#[arg(short = 'p', long)]
|
||||
qmdl_path: PathBuf,
|
||||
|
||||
#[arg(short, long)]
|
||||
#[arg(short = 'c', long)]
|
||||
pcapify: bool,
|
||||
|
||||
#[arg(long)]
|
||||
@@ -20,6 +21,9 @@ struct Args {
|
||||
|
||||
#[arg(long)]
|
||||
enable_dummy_analyzer: bool,
|
||||
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
async fn analyze_file(harness: &mut Harness, qmdl_path: &str, show_skipped: bool) {
|
||||
@@ -41,20 +45,37 @@ async fn analyze_file(harness: &mut Harness, qmdl_path: &str, show_skipped: bool
|
||||
}
|
||||
for analysis in row.analysis {
|
||||
for maybe_event in analysis.events {
|
||||
if let Some(event) = maybe_event {
|
||||
warnings += 1;
|
||||
println!("{}: {:?}", analysis.timestamp, event);
|
||||
let Some(event) = maybe_event else { continue };
|
||||
match event.event_type {
|
||||
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 {
|
||||
println!("{}: messages skipped:", qmdl_path);
|
||||
info!("{}: messages skipped:", qmdl_path);
|
||||
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) {
|
||||
@@ -67,29 +88,36 @@ async fn pcapify(qmdl_path: &PathBuf) {
|
||||
let mut pcap_writer = GsmtapPcapWriter::new(pcap_file).await.unwrap();
|
||||
pcap_writer.write_iface_header().await.unwrap();
|
||||
while let Some(container) = qmdl_reader.get_next_messages_container().await.expect("failed to get container") {
|
||||
for maybe_msg in container.into_messages() {
|
||||
if let Ok(msg) = maybe_msg {
|
||||
if let Ok(Some((timestamp, parsed))) = gsmtap_parser::parse(msg) {
|
||||
pcap_writer.write_gsmtap_message(parsed, timestamp).await.expect("failed to write");
|
||||
}
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("wrote pcap to {:?}", &pcap_path);
|
||||
info!("wrote pcap to {:?}", &pcap_path);
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
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();
|
||||
if args.enable_dummy_analyzer {
|
||||
harness.add_analyzer(Box::new(dummy_analyzer::TestAnalyzer { count: 0 }));
|
||||
}
|
||||
println!("Analyzers:");
|
||||
info!("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");
|
||||
|
||||
@@ -2,17 +2,9 @@ use crate::error::RayhunterError;
|
||||
|
||||
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>,
|
||||
colorblind_mode: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct Config {
|
||||
pub qmdl_store_path: String,
|
||||
pub port: u16,
|
||||
@@ -36,18 +28,11 @@ impl Default for Config {
|
||||
}
|
||||
|
||||
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) {
|
||||
let parsed_config: ConfigFile = toml::from_str(&config_file)
|
||||
.map_err(RayhunterError::ConfigFileParsingError)?;
|
||||
parsed_config.qmdl_store_path.map(|v| config.qmdl_store_path = v);
|
||||
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);
|
||||
parsed_config.colorblind_mode.map(|v| config.colorblind_mode = v);
|
||||
Ok(toml::from_str(&config_file).map_err(RayhunterError::ConfigFileParsingError)?)
|
||||
} else {
|
||||
Ok(Config::default())
|
||||
}
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub struct Args {
|
||||
|
||||
@@ -22,6 +22,7 @@ use analysis::{get_analysis_status, run_analysis_thread, start_analysis, Analysi
|
||||
use axum::response::Redirect;
|
||||
use diag::{get_analysis_report, start_recording, stop_recording, DiagDeviceCtrlMessage};
|
||||
use log::{info, error};
|
||||
use qmdl_store::RecordingStoreError;
|
||||
use rayhunter::diag_device::DiagDevice;
|
||||
use axum::routing::{get, post};
|
||||
use axum::Router;
|
||||
@@ -90,13 +91,31 @@ async fn server_shutdown_signal(server_shutdown_rx: oneshot::Receiver<()>) {
|
||||
info!("Server received shutdown signal, exiting...");
|
||||
}
|
||||
|
||||
// Loads a QmdlStore if one exists, and if not, only create one if we're not in
|
||||
// debug mode.
|
||||
// Loads a RecordingStore if one exists, and if not, only create one if we're
|
||||
// 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> {
|
||||
match (RecordingStore::exists(&config.qmdl_store_path).await?, config.debug_mode) {
|
||||
(true, _) => Ok(RecordingStore::load(&config.qmdl_store_path).await?),
|
||||
(false, false) => Ok(RecordingStore::create(&config.qmdl_store_path).await?),
|
||||
(false, true) => Err(RayhunterError::NoStoreDebugMode(config.qmdl_store_path.clone())),
|
||||
let store_exists = RecordingStore::exists(&config.qmdl_store_path).await?;
|
||||
if config.debug_mode {
|
||||
if store_exists {
|
||||
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?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,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::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);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -104,7 +104,7 @@ pub fn run_diag_read_thread(
|
||||
}
|
||||
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???");
|
||||
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");
|
||||
}
|
||||
},
|
||||
@@ -130,12 +130,11 @@ pub async fn start_recording(State(state): State<Arc<ServerState>>) -> Result<(S
|
||||
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)))?;
|
||||
|
||||
let display_state: framebuffer::DisplayState;
|
||||
if state.colorblind_mode {
|
||||
display_state = framebuffer::DisplayState::RecordingCBM;
|
||||
let display_state = if state.colorblind_mode {
|
||||
framebuffer::DisplayState::RecordingCBM
|
||||
} else {
|
||||
display_state = framebuffer::DisplayState::Recording;
|
||||
}
|
||||
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)))?;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use rayhunter::util::RuntimeMetadata;
|
||||
use chrono::{DateTime, Local};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -43,17 +44,24 @@ pub struct ManifestEntry {
|
||||
pub last_message_time: Option<DateTime<Local>>,
|
||||
pub qmdl_size_bytes: usize,
|
||||
pub analysis_size_bytes: usize,
|
||||
pub rayhunter_version: Option<String>,
|
||||
pub system_os: Option<String>,
|
||||
pub arch: Option<String>,
|
||||
}
|
||||
|
||||
impl ManifestEntry {
|
||||
fn new() -> Self {
|
||||
let now = Local::now();
|
||||
let metadata = RuntimeMetadata::new();
|
||||
ManifestEntry {
|
||||
name: format!("{}", now.timestamp()),
|
||||
start_time: now,
|
||||
last_message_time: None,
|
||||
qmdl_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]
|
||||
async fn test_repeated_new_entries() {
|
||||
let dir = make_temp_dir();
|
||||
|
||||
@@ -27,9 +27,10 @@ pub struct ServerState {
|
||||
}
|
||||
|
||||
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 (entry_index, entry) = qmdl_store.entry_for_name(&qmdl_name)
|
||||
.ok_or((StatusCode::NOT_FOUND, format!("couldn't find qmdl file with 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_idx)))?;
|
||||
let qmdl_file = qmdl_store.open_entry_qmdl(entry_index).await
|
||||
.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);
|
||||
|
||||
@@ -80,6 +80,13 @@ async function updateEntryAnalysisResult(entry) {
|
||||
entry.analysis_result = `0 warnings!`;
|
||||
} else {
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
const analysisResult = document.createElement('td');
|
||||
analysisResult.innerText = entry.analysis_result;
|
||||
analysisResult.innerHTML = entry.analysis_result;
|
||||
if (entry.analysis.warnings.length > 0) {
|
||||
row.classList.add("warning");
|
||||
}
|
||||
|
||||
17
dist/install-linux.sh
vendored
17
dist/install-linux.sh
vendored
@@ -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
17
dist/install-mac.sh
vendored
@@ -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
|
||||
55
dist/install-common.sh → dist/install.sh
vendored
55
dist/install-common.sh → dist/install.sh
vendored
@@ -1,18 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
install() {
|
||||
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
|
||||
}
|
||||
set -e
|
||||
|
||||
force_debug_mode() {
|
||||
echo "Using adb at $ADB"
|
||||
@@ -108,3 +95,43 @@ test_rayhunter() {
|
||||
done
|
||||
echo "timeout reached! failed to reach rayhunter url $URL, something went wrong :("
|
||||
}
|
||||
|
||||
##### ##### #####
|
||||
##### Main #####
|
||||
##### ##### #####
|
||||
if [[ `uname -s` == "Linux" ]]; then
|
||||
export SERIAL_PATH="./serial-ubuntu-24/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
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rayhunter"
|
||||
version = "0.1.0"
|
||||
version = "0.2.6"
|
||||
edition = "2021"
|
||||
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"
|
||||
libc = "0.2.150"
|
||||
log = "0.4.20"
|
||||
nix = { version = "0.29.0", features = ["feature"] }
|
||||
pcap-file-tokio = "0.1.0"
|
||||
thiserror = "1.0.50"
|
||||
telcom-parser = { path = "../telcom-parser" }
|
||||
|
||||
@@ -3,11 +3,13 @@ use chrono::{DateTime, FixedOffset};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{diag::MessagesContainer, gsmtap_parser};
|
||||
use crate::util::RuntimeMetadata;
|
||||
|
||||
use super::{
|
||||
imsi_requested::ImsiRequestedAnalyzer,
|
||||
information_element::InformationElement,
|
||||
lte_downgrade::LteSib6And7DowngradeAnalyzer,
|
||||
connection_redirect_downgrade::ConnectionRedirect2GDowngradeAnalyzer,
|
||||
priority_2g_downgrade::LteSib6And7DowngradeAnalyzer,
|
||||
null_cipher::NullCipherAnalyzer,
|
||||
};
|
||||
|
||||
@@ -72,6 +74,7 @@ pub struct AnalyzerMetadata {
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct ReportMetadata {
|
||||
pub analyzers: Vec<AnalyzerMetadata>,
|
||||
pub rayhunter: RuntimeMetadata,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
@@ -94,11 +97,9 @@ impl AnalysisRow {
|
||||
|
||||
pub fn contains_warnings(&self) -> bool {
|
||||
for analysis in &self.analysis {
|
||||
for maybe_event in &analysis.events {
|
||||
if let Some(event) = maybe_event {
|
||||
if matches!(event.event_type, EventType::QualitativeWarning { .. }) {
|
||||
return true;
|
||||
}
|
||||
for event in analysis.events.iter().flatten() {
|
||||
if matches!(event.event_type, EventType::QualitativeWarning { .. }) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,9 +118,14 @@ impl Harness {
|
||||
|
||||
pub fn new_with_all_analyzers() -> Self {
|
||||
let mut harness = Harness::new();
|
||||
harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer{}));
|
||||
harness.add_analyzer(Box::new(ImsiRequestedAnalyzer::new()));
|
||||
harness.add_analyzer(Box::new(NullCipherAnalyzer{}));
|
||||
harness.add_analyzer(Box::new(ConnectionRedirect2GDowngradeAnalyzer{}));
|
||||
harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer{}));
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -203,8 +209,11 @@ impl Harness {
|
||||
});
|
||||
}
|
||||
|
||||
let rayhunter = RuntimeMetadata::new();
|
||||
|
||||
ReportMetadata {
|
||||
analyzers,
|
||||
rayhunter,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
42
lib/src/analysis/connection_redirect_downgrade.rs
Normal file
42
lib/src/analysis/connection_redirect_downgrade.rs
Normal 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),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ impl Analyzer for ImsiRequestedAnalyzer {
|
||||
return None;
|
||||
};
|
||||
|
||||
// NAS identity request
|
||||
// NAS identity request, ID type IMSI
|
||||
if payload == &[0x07, 0x55, 0x01] {
|
||||
if self.packet_num < PACKET_THRESHHOLD {
|
||||
return Some(Event {
|
||||
@@ -38,7 +38,7 @@ impl Analyzer for ImsiRequestedAnalyzer {
|
||||
severity: Severity::Medium
|
||||
},
|
||||
message: format!(
|
||||
"NAS IMSI request detected, however it was within \
|
||||
"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.",
|
||||
@@ -50,7 +50,7 @@ impl Analyzer for ImsiRequestedAnalyzer {
|
||||
event_type: EventType::QualitativeWarning {
|
||||
severity: Severity::High
|
||||
},
|
||||
message: format!("NAS IMSI request detected"),
|
||||
message: "NAS IMSI identity request detected".to_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
pub mod analyzer;
|
||||
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_requested;
|
||||
pub mod null_cipher;
|
||||
pub mod util;
|
||||
|
||||
@@ -29,18 +29,18 @@ impl NullCipherAnalyzer {
|
||||
}
|
||||
// Use map/flatten to dig into a long chain of nested Option types
|
||||
let maybe_v1250 = c1.non_critical_extension.as_ref()
|
||||
.map(|v890| v890.non_critical_extension.as_ref()).flatten()
|
||||
.map(|v920| v920.non_critical_extension.as_ref()).flatten()
|
||||
.map(|v1020| v1020.non_critical_extension.as_ref()).flatten()
|
||||
.map(|v1130| v1130.non_critical_extension.as_ref()).flatten();
|
||||
.and_then(|v890| v890.non_critical_extension.as_ref())
|
||||
.and_then(|v920| v920.non_critical_extension.as_ref())
|
||||
.and_then(|v1020| v1020.non_critical_extension.as_ref())
|
||||
.and_then(|v1130| v1130.non_critical_extension.as_ref());
|
||||
let Some(v1250) = maybe_v1250 else {
|
||||
return false;
|
||||
};
|
||||
|
||||
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()
|
||||
.map(|scg| scg.mobility_control_info_scg_r12.as_ref()).flatten()
|
||||
.map(|mci| mci.ciphering_algorithm_scg_r12.as_ref()).flatten();
|
||||
.and_then(|scg| scg.mobility_control_info_scg_r12.as_ref())
|
||||
.and_then(|mci| mci.ciphering_algorithm_scg_r12.as_ref());
|
||||
if let Some(cipher) = maybe_cipher {
|
||||
if cipher.0 == CipheringAlgorithm_r12::EEA0 {
|
||||
return true;
|
||||
@@ -49,10 +49,10 @@ impl NullCipherAnalyzer {
|
||||
}
|
||||
|
||||
let maybe_v1530_security_config = v1250.non_critical_extension.as_ref()
|
||||
.map(|v1310| v1310.non_critical_extension.as_ref()).flatten()
|
||||
.map(|v1430| v1430.non_critical_extension.as_ref()).flatten()
|
||||
.map(|v1510| v1510.non_critical_extension.as_ref()).flatten()
|
||||
.map(|v1530| v1530.security_config_ho_v1530.as_ref()).flatten();
|
||||
.and_then(|v1310| v1310.non_critical_extension.as_ref())
|
||||
.and_then(|v1430| v1430.non_critical_extension.as_ref())
|
||||
.and_then(|v1510| v1510.non_critical_extension.as_ref())
|
||||
.and_then(|v1530| v1530.security_config_ho_v1530.as_ref());
|
||||
let Some(v1530_security_config) = maybe_v1530_security_config else {
|
||||
return false;
|
||||
};
|
||||
|
||||
32
lib/src/analysis/util.rs
Normal file
32
lib/src/analysis/util.rs
Normal 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;
|
||||
@@ -221,9 +221,9 @@ pub enum Nas4GMessageDirection {
|
||||
// * 0xb0ec: plain EMM NAS message (incoming)
|
||||
// * 0xb0ed: plain EMM NAS message (outgoing)
|
||||
#[deku(id_pat = "0xb0e2 | 0xb0ec")]
|
||||
Inbound,
|
||||
Downlink,
|
||||
#[deku(id_pat = "0xb0e3 | 0xb0ed")]
|
||||
Outbound,
|
||||
Uplink,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
|
||||
@@ -17,7 +17,7 @@ pub enum GsmtapType {
|
||||
UmtsRlcMac,
|
||||
UmtsRrc(UmtsRrcSubtype),
|
||||
LteRrc(LteRrcSubtype), /* LTE interface */
|
||||
LteMac, /* LTE MAC interface */
|
||||
LteMac, /* LTE MAC interface */
|
||||
LteMacFramed, /* LTE MAC with context hdr */
|
||||
OsmocoreLog, /* libosmocore logging */
|
||||
QcDiag, /* Qualcomm DIAG frame */
|
||||
@@ -200,6 +200,11 @@ pub struct GsmtapHeader {
|
||||
#[deku(update = "self.gsmtap_type.get_type()")]
|
||||
pub packet_type: 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 signal_dbm: i8,
|
||||
pub signal_noise_ratio_db: u8,
|
||||
@@ -222,6 +227,8 @@ impl GsmtapHeader {
|
||||
header_len: 4,
|
||||
packet_type: gsmtap_type.get_type(),
|
||||
timeslot: 0,
|
||||
pcs_band_indicator: false,
|
||||
uplink: false,
|
||||
arfcn: 0,
|
||||
signal_dbm: 0,
|
||||
signal_noise_ratio_db: 0,
|
||||
|
||||
@@ -99,7 +99,6 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
|
||||
_ => return Err(GsmtapParserError::InvalidLteRrcOtaExtHeaderVersion(ext_header_version)),
|
||||
};
|
||||
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.frame_number = packet.get_sfn();
|
||||
header.subslot = packet.get_subfn();
|
||||
@@ -108,9 +107,10 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
|
||||
payload: packet.take_payload(),
|
||||
}))
|
||||
},
|
||||
LogBody::Nas4GMessage { msg, .. } => {
|
||||
LogBody::Nas4GMessage { msg, direction, .. } => {
|
||||
// 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 {
|
||||
header,
|
||||
payload: msg,
|
||||
|
||||
@@ -7,6 +7,7 @@ pub mod gsmtap;
|
||||
pub mod gsmtap_parser;
|
||||
pub mod pcap;
|
||||
pub mod analysis;
|
||||
pub mod util;
|
||||
|
||||
// re-export telcom_parser, since we use its types in our API
|
||||
pub use telcom_parser;
|
||||
|
||||
@@ -9,8 +9,9 @@ use chrono::prelude::*;
|
||||
use deku::prelude::*;
|
||||
use pcap_file_tokio::pcapng::blocks::enhanced_packet::EnhancedPacketBlock;
|
||||
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::PcapError;
|
||||
use pcap_file_tokio::{Endianness, PcapError};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
@@ -60,7 +61,20 @@ struct UdpHeader {
|
||||
|
||||
impl<T> GsmtapPcapWriter<T> where T: AsyncWrite + Unpin + Send {
|
||||
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 })
|
||||
}
|
||||
|
||||
|
||||
37
lib/src/util.rs
Normal file
37
lib/src/util.rs
Normal 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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
4
make.sh
4
make.sh
@@ -1,4 +1,6 @@
|
||||
#!/bin/sh
|
||||
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 shell '/bin/rootshell -c "/etc/init.d/rayhunter_daemon restart"'
|
||||
echo "rebooting the device..."
|
||||
adb shell '/bin/rootshell -c "reboot"'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rootshell"
|
||||
version = "0.1.0"
|
||||
version = "0.2.6"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -25,9 +25,12 @@ fn main() {
|
||||
|
||||
// discard argv[0]
|
||||
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)
|
||||
.uid(0)
|
||||
.gid(0)
|
||||
.exec();
|
||||
eprintln!("Error running command: {error}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
[package]
|
||||
name = "serial"
|
||||
version = "0.1.0"
|
||||
version = "0.2.6"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
rusb = { version = "0.9.3", features = ["vendored"] }
|
||||
nusb = "0.1.13"
|
||||
tokio = { version = "1.44.1", features = ["macros", "rt", "time"] }
|
||||
|
||||
@@ -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
|
||||
//! 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::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();
|
||||
|
||||
if args.len() != 2 {
|
||||
@@ -32,53 +22,64 @@ fn main() {
|
||||
return;
|
||||
}
|
||||
|
||||
match Context::new() {
|
||||
Ok(mut context) => {
|
||||
if args[1] == "--root" {
|
||||
enable_command_mode(&mut context);
|
||||
} else {
|
||||
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),
|
||||
if args[1] == "--root" {
|
||||
enable_command_mode();
|
||||
} else {
|
||||
match open_orbic() {
|
||||
Some(interface) => send_command(interface, &args[1]).await,
|
||||
None => panic!("No Orbic device found"),
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Sends an AT command to the usb device over the serial port
|
||||
///
|
||||
/// 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();
|
||||
data.push_str("\r\n");
|
||||
data.push_str(command);
|
||||
data.push_str("\r\n");
|
||||
|
||||
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
|
||||
handle
|
||||
.write_control(0x21, 0x22, 3, 1, &[], timeout)
|
||||
interface
|
||||
.control_out_blocking(enable_serial_port, &[], timeout)
|
||||
.expect("Failed to send control request");
|
||||
|
||||
// Send the command
|
||||
handle
|
||||
.write_bulk(0x2, data.as_bytes(), timeout)
|
||||
tokio::time::timeout(timeout, interface.bulk_out(0x2, data.as_bytes().to_vec()))
|
||||
.await
|
||||
.expect("Timed out writing command")
|
||||
.into_result()
|
||||
.expect("Failed to write command");
|
||||
|
||||
// Consume the echoed command
|
||||
handle
|
||||
.read_bulk(0x82, &mut response, timeout)
|
||||
tokio::time::timeout(timeout, interface.bulk_in(0x82, RequestBuffer::new(256)))
|
||||
.await
|
||||
.expect("Timed out reading submitted command")
|
||||
.into_result()
|
||||
.expect("Failed to read submitted command");
|
||||
|
||||
// Read the actual response
|
||||
handle
|
||||
.read_bulk(0x82, &mut response, timeout)
|
||||
let response = tokio::time::timeout(timeout, interface.bulk_in(0x82, RequestBuffer::new(256)))
|
||||
.await
|
||||
.expect("Timed out reading response")
|
||||
.into_result()
|
||||
.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") {
|
||||
println!("Received unexpected response{0}", responsestr);
|
||||
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
|
||||
///
|
||||
/// 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) {
|
||||
if open_orbic(context).is_some() {
|
||||
fn enable_command_mode() {
|
||||
if open_orbic().is_some() {
|
||||
println!("Device already in command mode. Doing nothing...");
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
// may get a pipe error here
|
||||
if e == rusb::Error::Pipe {
|
||||
if e == nusb::transfer::TransferError::Stall {
|
||||
return;
|
||||
}
|
||||
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");
|
||||
}
|
||||
|
||||
/// Get a handle and contet for the orbic device
|
||||
fn open_orbic<T: UsbContext>(context: &mut T) -> Option<DeviceHandle<T>> {
|
||||
/// Get an Interface for the orbic device
|
||||
fn open_orbic() -> Option<Interface> {
|
||||
// Device after initial mode switch
|
||||
if let Some(mut handle) = open_device(context, 0x05c6, 0xf601) {
|
||||
handle.set_auto_detach_kernel_driver(true).expect("set_auto_detach_kernel_driver failed");
|
||||
handle.claim_interface(1).expect("claim_interface(1) failed");
|
||||
return Some(handle);
|
||||
if let Some(device) = open_device(0x05c6, 0xf601) {
|
||||
let interface = device
|
||||
.detach_and_claim_interface(1) // will reattach drivers on release
|
||||
.expect("detach_and_claim_interface(1) failed");
|
||||
return Some(interface);
|
||||
}
|
||||
|
||||
// Device with rndis enabled as well
|
||||
if let Some(mut handle) = open_device(context, 0x05c6, 0xf622) {
|
||||
handle.set_auto_detach_kernel_driver(true).expect("set_auto_detach_kernel_driver failed");
|
||||
handle.claim_interface(1).expect("claim_interface(1) failed");
|
||||
return Some(handle);
|
||||
if let Some(device) = open_device(0x05c6, 0xf622) {
|
||||
let interface = device
|
||||
.detach_and_claim_interface(1) // will reattach drivers on release
|
||||
.expect("detach_and_claim_interface(1) failed");
|
||||
return Some(interface);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Generic function to open a USB device
|
||||
fn open_device<T: UsbContext>(context: &mut T, vid: u16, pid: u16) -> Option<DeviceHandle<T>> {
|
||||
let devices = match context.devices() {
|
||||
/// General function to open a USB device
|
||||
fn open_device(vid: u16, pid: u16) -> Option<Device> {
|
||||
let devices = match nusb::list_devices() {
|
||||
Ok(d) => d,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
for device in devices.iter() {
|
||||
let device_desc = match device.device_descriptor() {
|
||||
Ok(d) => d,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
if device_desc.vendor_id() == vid && device_desc.product_id() == pid {
|
||||
for device in devices {
|
||||
if device.vendor_id() == vid && device.product_id() == pid {
|
||||
match device.open() {
|
||||
Ok(handle) => return Some(handle),
|
||||
Ok(d) => return Some(d),
|
||||
Err(e) => panic!("device found but failed to open: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "telcom-parser"
|
||||
version = "0.1.0"
|
||||
version = "0.2.6"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
asn1tools==0.166.0
|
||||
bitstruct==8.19.0
|
||||
diskcache==5.6.3
|
||||
pycrate==0.7.8
|
||||
pyparsing==3.1.2
|
||||
|
||||
Reference in New Issue
Block a user