Merge branch 'master' into caquino/builder-cleanup

This commit is contained in:
Cassiano Aquino 2019-10-08 13:35:39 +01:00 committed by GitHub
commit f9754927fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 917 additions and 681 deletions

1
.gitignore vendored
View File

@ -2,6 +2,7 @@
*.img.bmap
*.pcap
*.po~
preview.png
__pycache__
_backups
_emulation

View File

@ -14,14 +14,13 @@
[Pwnagotchi](https://twitter.com/pwnagotchi) is an [A2C](https://hackernoon.com/intuitive-rl-intro-to-advantage-actor-critic-a2c-4ff545978752)-based "AI" leveraging [bettercap](https://www.bettercap.org/) that learns from its surrounding WiFi environment in order to maximize the crackable WPA key material it captures (either passively, or by performing deauthentication and association attacks). This material is collected as PCAP files containing any form of handshake supported by [hashcat](https://hashcat.net/hashcat/), including [PMKIDs](https://www.evilsocket.net/2019/02/13/Pwning-WiFi-networks-with-bettercap-and-the-PMKID-client-less-attack/),
full and half WPA handshakes.
![handshake](https://i.imgur.com/pdA4vCZ.png)
![ui](https://i.imgur.com/c7xh4hN.png)
Specifically, Pwnagotchi is using an [LSTM with MLP feature extractor](https://stable-baselines.readthedocs.io/en/master/modules/policies.html#stable_baselines.common.policies.MlpLstmPolicy) as its policy network for the [A2C agent](https://stable-baselines.readthedocs.io/en/master/modules/a2c.html). If you're unfamiliar with A2C, here is [a very good introductory explanation](https://hackernoon.com/intuitive-rl-intro-to-advantage-actor-critic-a2c-4ff545978752) (in comic form!) of the basic principles behind how Pwnagotchi learns. (You can read more about how Pwnagotchi learns in the [Usage](https://github.com/evilsocket/pwnagotchi/blob/master/docs/usage.md#training-the-ai) doc.)
Instead of merely playing [Super Mario or Atari games](https://becominghuman.ai/getting-mario-back-into-the-gym-setting-up-super-mario-bros-in-openais-gym-8e39a96c1e41?gi=c4b66c3d5ced) like most reinforcement learning based "AI" *(yawn)*, Pwnagotchi tunes [its own parameters](https://github.com/evilsocket/pwnagotchi/blob/master/pwnagotchi/defaults.yml#L73) over time to **get better at pwning WiFi things** in the environments you expose it to.
More specifically, Pwnagotchi is using an [LSTM with MLP feature extractor](https://stable-baselines.readthedocs.io/en/master/modules/policies.html#stable_baselines.common.policies.MlpLstmPolicy) as its policy network for the [A2C agent](https://stable-baselines.readthedocs.io/en/master/modules/a2c.html). If you're unfamiliar with A2C, here is [a very good introductory explanation](https://hackernoon.com/intuitive-rl-intro-to-advantage-actor-critic-a2c-4ff545978752) (in comic form!) of the basic principles behind how Pwnagotchi learns. (You can read more about how Pwnagotchi learns in the [Usage](https://github.com/evilsocket/pwnagotchi/blob/master/docs/usage.md#training-the-ai) doc.)
Instead of merely playing [Super Mario or Atari games](https://becominghuman.ai/getting-mario-back-into-the-gym-setting-up-super-mario-bros-in-openais-gym-8e39a96c1e41?gi=c4b66c3d5ced) like most reinforcement learning based "AI" *(yawn)*, Pwnagotchi tunes [its own parameters](https://github.com/evilsocket/pwnagotchi/blob/master/sdcard/rootfs/root/pwnagotchi/config.yml#L54) over time to **get better at pwning WiFi things** in the environments you expose it to.
**Keep in mind:** Unlike the usual RL simulations, Pwnagotchi actually learns over time. Time for a Pwnagotchi is measured in epochs; a single epoch can last from a few seconds to minutes, depending on how many access points and client stations are visible. Do not expect your Pwnagotchi to perform amazingly well at the very beginning, as it will be [exploring](https://hackernoon.com/intuitive-rl-intro-to-advantage-actor-critic-a2c-4ff545978752) several combinations of [key parameters](https://github.com/evilsocket/pwnagotchi/blob/master/docs/usage.md#training-the-ai) to determine ideal adjustments for pwning the particular environment you are exposing it to during its beginning epochs ... but **definitely listen to your pwnagotchi when it tells you it's bored!** Bring it into novel WiFi environments with you and have it observe new networks and capture new handshakes—and you'll see. :)
**Keep in mind:** Unlike the usual RL simulations, Pwnagotchi actually learns over time. Time for a Pwnagotchi is measured in epochs; a single epoch can last from a few seconds to minutes, depending on how many access points and client stations are visible. Do not expect your Pwnagotchi to perform amazingly well at the very beginning, as it will be [exploring](https://hackernoon.com/intuitive-rl-intro-to-advantage-actor-critic-a2c-4ff545978752) several combinations of [key parameters](https://github.com/evilsocket/pwnagotchi/blob/master/docs/usage.md#training-the-ai) to determine ideal adjustments for pwning the particular environment you are exposing it to during its beginning epochs ... but **definitely listen to your Pwnagotchi when it tells you it's bored!** Bring it into novel WiFi environments with you and have it observe new networks and capture new handshakes—and you'll see. :)
Multiple units within close physical proximity can "talk" to each other, advertising their own presence to each other by broadcasting custom information elements using a parasite protocol I've built on top of the existing dot11 standard. Over time, two or more units trained together will learn to cooperate upon detecting each other's presence by dividing the available channels among them for optimal pwnage.
@ -29,21 +28,15 @@ Multiple units within close physical proximity can "talk" to each other, adverti
For hackers to learn reinforcement learning, WiFi networking, and have an excuse to get out for more walks. Also? **It's cute as f---**.
**In case you're curious about the name:** *Pwnagotchi* is a portmanteau of *pwn* (which we shouldn't have to explain if you are interested in this project :kissing_heart:) and *-gotchi*. It is a nostalgic reference made in homage to a very popular children's toy from the 1990s called the [Tamagotchi](https://en.wikipedia.org/wiki/Tamagotchi). The Tamagotchi (たまごっち, derived from *tamago* (たまご) "egg" + *uotchi* (ウオッチ) "watch") is a cultural touchstone for many Millennial hackers as a formative electronic toy from our collective childhoods. Were you lucky enough to possess a Tamagotchi as a kid? Well, with your Pwnagotchi, you too can enjoy the nostalgic delight of being strangely emotionally attached to a handheld automata *yet again!* Except, this time around...you get to #HackThePlanet. >:D
## Documentation
---
:warning: **THE FOLLOWING DOCUMENTATION IS BEING PREPARED FOR THE v1.0 RELEASE OF PWNAGOTCHI. Since this effort is an active (and unstable) work-in-progress, the docs displayed here are in various stages of [in]completion. There will be dead links and placeholders throughout as we are still building things out in preparation for the v1.0 release.** :warning:
**IMPORTANT NOTE:** If you'd like to alphatest Pwnagotchi and are trying to get yours up and running while the project is still very unstable, please understand that the documentation here may not reflect what is currently implemented. If you have questions, ask the community of alphatesters in the [official Pwnagotchi Slack](https://pwnagotchi.herokuapp.com). The Pwnagotchi dev team is entirely focused on the v1.0 release and will NOT be providing support for alphatesters trying to get their Pwnagotchis working in the meantime. All technical support during this period of development is being provided by your fellow alphatesters in the Slack (thanks, everybody! :heart:).
---
- [About the Project](https://github.com/evilsocket/pwnagotchi/blob/master/docs/about.md)
- [FAQ](https://github.com/evilsocket/pwnagotchi/blob/master/docs/faq.md)
- [How to Install](https://github.com/evilsocket/pwnagotchi/blob/master/docs/install.md)
- [Configuration](https://github.com/evilsocket/pwnagotchi/blob/master/docs/configure.md)
- [Usage](https://github.com/evilsocket/pwnagotchi/blob/master/docs/usage.md)
- [Plugins](https://github.com/evilsocket/pwnagotchi/blob/master/docs/plugins.md)
- [Development](https://github.com/evilsocket/pwnagotchi/blob/master/docs/dev.md)
- [Community Hacks](https://github.com/evilsocket/pwnagotchi/blob/master/docs/hacks.md)
https://www.pwnagotchi.ai
## Links

View File

@ -10,13 +10,14 @@ if __name__ == '__main__':
import pwnagotchi.plugins as plugins
from pwnagotchi.log import SessionParser
from pwnagotchi.identity import KeyPair
from pwnagotchi.agent import Agent
from pwnagotchi.ui.display import Display
parser = argparse.ArgumentParser()
parser.add_argument('-C', '--config', action='store', dest='config',
default=os.path.join(os.path.abspath(os.path.dirname(pwnagotchi.__file__)), '/defaults.yml'),
default=os.path.join(os.path.dirname(pwnagotchi.__file__), 'defaults.yml'),
help='Main configuration file.')
parser.add_argument('-U', '--user-config', action='store', dest='user_config', default='/etc/pwnagotchi/config.yml',
help='If this file exists, configuration will be merged and this will override default values.')
@ -34,10 +35,11 @@ if __name__ == '__main__':
plugins.load(config)
keypair = KeyPair()
display = Display(config=config, state={'name': '%s>' % pwnagotchi.name()})
agent = Agent(view=display, config=config)
agent = Agent(view=display, config=config, keypair=keypair)
logging.info("%s@%s (v%s)" % (pwnagotchi.name(), agent._identity, pwnagotchi.version))
logging.info("%s@%s (v%s)" % (pwnagotchi.name(), agent._keypair.fingerprint, pwnagotchi.version))
for _, plugin in plugins.loaded.items():
logging.debug("plugin '%s' v%s loaded from %s" % (plugin.__name__, plugin.__version__, plugin.__file__))
@ -64,7 +66,7 @@ if __name__ == '__main__':
time.sleep(1)
if Agent.is_connected():
plugins.on('internet_available', display, config, log)
plugins.on('internet_available', display, keypair, config, log)
else:
logging.info("entering auto mode ...")

View File

@ -256,6 +256,22 @@
#!/usr/bin/env bash
ifconfig mon0 down && iw dev mon0 del
- name: create hdmion script
copy:
dest: /usr/bin/hdmion
mode: 0755
content: |
#!/usr/bin/env bash
sudo /opt/vc/bin/tvservice -p
- name: create hdmioff script
copy:
dest: /usr/bin/hdmioff
mode: 0755
content: |
#!/usr/bin/env bash
sudo /opt/vc/bin/tvservice -o
- name: add HDMI powersave to rc.local
blockinfile:
path: /etc/rc.local

View File

@ -1,23 +0,0 @@
# About the Project
[Pwnagotchi](https://twitter.com/pwnagotchi) is an [A2C](https://hackernoon.com/intuitive-rl-intro-to-advantage-actor-critic-a2c-4ff545978752)-based "AI" leveraging [bettercap](https://www.bettercap.org/) that learns from its surrounding WiFi environment in order to maximize the WPA key material it captures (either passively, or by performing deauthentication and association attacks). This material is collected as PCAP files containing any form of handshake supported by [hashcat](https://hashcat.net/hashcat/), including [PMKIDs](https://www.evilsocket.net/2019/02/13/Pwning-WiFi-networks-with-bettercap-and-the-PMKID-client-less-attack/),
full and half WPA handshakes.
![handshake](https://i.imgur.com/pdA4vCZ.png)
Instead of merely playing [Super Mario or Atari games](https://becominghuman.ai/getting-mario-back-into-the-gym-setting-up-super-mario-bros-in-openais-gym-8e39a96c1e41?gi=c4b66c3d5ced) like most reinforcement learning based "AI" *(yawn)*, Pwnagotchi tunes [its own parameters](https://github.com/evilsocket/pwnagotchi/blob/master/sdcard/rootfs/root/pwnagotchi/config.yml#L54) over time to **get better at pwning WiFi things** in the environments you expose it to.
**Keep in mind:** Unlike the usual RL simulations, Pwnagotchi actually learns over time. Time for a Pwnagotchi is measured in epochs; a single epoch can last from a few seconds to minutes, depending on how many access points and client stations are visible. Do not expect your Pwnagotchi to perform amazingly well at the very beginning, as it will be [exploring](https://hackernoon.com/intuitive-rl-intro-to-advantage-actor-critic-a2c-4ff545978752) several combinations of [key parameters](https://github.com/evilsocket/pwnagotchi/blob/master/docs/usage.md#training-the-ai) to determine ideal adjustments for pwning the particular environment you are exposing it to during its beginning epochs ... but **definitely listen to your pwnagotchi when it tells you it's bored!** Bring it into novel WiFi environments with you and have it observe new networks and capture new handshakes—and you'll see. :)
Multiple units within close physical proximity can "talk" to each other, advertising their own presence to each other by broadcasting custom information elements using a parasite protocol I've built on top of the existing dot11 standard. Over time, two or more units trained together will learn to cooperate upon detecting each other's presence by dividing the available channels among them for optimal pwnage.
![peers](https://i.imgur.com/Ywr5aqx.png)
[Depending on the status of the unit](), several states and states transitions are configurable and represented on the display as different moods, expressions and sentences. Pwnagotchi speaks [many languages](https://github.com/evilsocket/pwnagotchi/blob/master/docs/configure.md#configuration), too!
Of course, it is possible to run your Pwnagotchi with the AI disabled (configurable in `config.yml`). Why might you want to do this? Perhaps you simply want to use your own fixed parameters (instead of letting the AI decide for you), or maybe you want to save battery and CPU cycles, or maybe it's just you have strong concerns about aiding and abetting baby Skynet. Whatever your particular reasons may be: an AI-disabled Pwnagotchi is still a simple and very effective automated deauther, WPA handshake sniffer, and portable [bettercap](https://www.bettercap.org/) + [webui](https://github.com/evilsocket/pwnagotchi/blob/master/docs/usage.md#bettercaps-web-ui) dedicated hardware.
## License
`pwnagotchi` is made with ♥ by [@evilsocket](https://twitter.com/evilsocket) and the [amazing dev team](https://github.com/evilsocket/pwnagotchi/graphs/contributors). It's released under the GPL3 license.

View File

@ -1,58 +0,0 @@
# Connecting to your Pwnagotchi
Once you wrote the image file on the SD card, there're a few steps you'll have to follow in order to configure your unit properly, first, start with connecting the USB cable to the data port of the Raspberry Pi and the RPi to your computer. After a few seconds the board will boot and you will see a new Ethernet interface on your host computer.
You'll need to configure it with a static IP address:
- IP: `10.0.0.1`
- Netmask: `255.255.255.0`
- Gateway: `10.0.0.1`
- DNS (if required): `8.8.8.8` (or whatever)
If everything's been configured properly, you will now be able to `ping` both `10.0.0.2` or `pwnagotchi.local` (if you haven't customized the hostname yet).
You can now connect to your unit using SSH:
```bash
ssh pi@10.0.0.2
```
The default password is `raspberry`, you should change it as soon as you log in for the first time by issuing the `passwd`command and selecting a new and more complex passphrase.
Moreover, it is recommended that you copy your SSH public key among the unit's authorized ones, so you can directly log in without entering a password:
```bash
ssh-copy-id -i ~/.ssh/id_rsa.pub pi@10.0.0.2
```
## Configuration
You can now set a new name for your unit by [changing the hostname](https://geek-university.com/raspberry-pi/change-raspberry-pis-hostname/). Create the `/root/custom.yml` file (either via SSH or by direclty editing the SD card contents from a computer) that will override the [default configuration](https://github.com/evilsocket/pwnagotchi/blob/master/sdcard/rootfs/root/pwnagotchi/config.yml) with your custom values.
## Language Selection
For instance, you can change `main.lang` to one of the supported languages:
- **english** (default)
- german
- dutch
- greek
- macedonian
- italian
- french
- russian
- swedish
## Display Selection
Set the type of display you want to use via `ui.display.type` (if your display does not work after changing this setting, you might need to completely remove power from the Raspberry and make a clean boot).
You can configure the refresh interval of the display via `ui.fps`, we advise to use a slow refresh to not shorten the lifetime of your display. The default value is 0, which will only refresh when changes are made to the screen.
## Host Connection Share
If you connect to the unit via `usb0` (thus using the data port), you might want to use the `scripts/linux_connection_share.sh`, `scripts/macos_connection_share.sh` or `scripts/win_connection_share.ps1` script to bring the interface up on your end and share internet connectivity from another interface, so you can update the unit and generally download things from the internet on it.
## Troubleshooting
If your network connection keeps flapping on your device connecting to your pwnagotchi, check if `usb0` (or equivalent) device is being controlled by NetworkManager. You can check this via `nmcli dev status`.

View File

@ -1,61 +0,0 @@
# Software
- Raspbian + [nexmon patches](https://re4son-kernel.com/re4son-pi-kernel/) for monitor mode, or any Linux with a monitor mode enabled interface (if you tune config.yml).
**Do not try with Kali on the Raspberry Pi 0 W, it is compiled without hardware floating point support and TensorFlow is simply not available for it, use Raspbian.**
## Creating an Image
You can use the `scripts/create_sibling.sh` script to create an - ready to flash - rasbian image with pwnagotchi.
```shell
usage: ./scripts/create_sibling.sh [OPTIONS]
Options:
-n <name> # Name of the pwnagotchi (default: pwnagotchi)
-i <file> # Provide the path of an already downloaded raspbian image
-o <file> # Name of the img-file (default: pwnagotchi.img)
-s <size> # Size which should be added to second partition (in Gigabyte) (default: 4)
-v <version> # Version of raspbian (Supported: latest; default: latest)
-p # Only run provisioning (assumes the image is already mounted)
-d # Only run dependencies checks
-h # Show this help
```
#### Known Issues
`GLib-ERROR **: 20:50:46.361: getauxval () failed: No such file or directory`
- Affected DEB & Versions: QEMU <= 2.11
- Fix: Upgrade QEMU to >= 3.1
- Bug Link: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=923289
## Adding a Language
If you want to add a language use the `language.sh` script. If you want to add for example the language **italian** you would type:
```shell
./scripts/language.sh add it
# Now make your changes to the file
# sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/it/LC_MESSAGES/voice.po
./scripts/language.sh compile it
# DONE
```
If you changed the `voice.py`- File, the translations need an update. Do it like this:
```shell
./scripts/language.sh update it
# Now make your changes to the file (changed lines are marked with "fuzzy")
# sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/it/LC_MESSAGES/voice.po
./scripts/language.sh compile it
# DONE
```
Now you can use the `preview.py`-script to preview the changes:
```shell
./scripts/preview.py --lang it --display ws2 --port 8080 &
./scripts/preview.py --lang it --display inky --port 8081 &
# Now open http://localhost:8080 and http://localhost:8081
```

View File

@ -1,13 +0,0 @@
# FAQ
## Why eINK?
Because!
## Why the AI takes 30 minutes to load?
Because Python sucks and TF is huge.
## Why ...?
Because!

View File

@ -1,38 +0,0 @@
# Unofficial Hacks
---
**IMPORTANT DISCLAIMER:** The information provided on this page is NOT officially supported by the Pwnagotchi development team. These are unofficial "hacks" that users have worked out while customizing their units and decided to document for anybody else who might want to do something similar.
- **Please do NOT open issues if you cannot get something described in this document to work.**
- It (almost) goes without saying, but obviously: **we are NOT responsible if you break your hardware by following any instructions documented here. Use this information at your own risk.**
---
If you test one of these hacks yourself and it still works, it's extra nice if you update the **Last Tested On** table and note any minor adjustments you may have had to make to the instructions to make it work with your particular Pwnagotchi setup. :heart:
## Screens
### Waveshare 3.5" SPI TFT screen
Last tested on | Pwnagotchi version | Working? | Reference
---------------|--------------------|----------|-----------|
2019 October 3 | Unknown | :white_check_mark: | ([link](https://github.com/evilsocket/pwnagotchi/issues/124#issue-502346040))
Some of this guide will work with other framebuffer-based displays.
- First: SSH into your Pwnagotchi, and give it some internet!
- Don't forget to check your default gateway and `apt-get update`.
- Follow the guide here: [www.waveshare.com/wiki/3.5inch_RPi_LCD_(A)#Method_1._Driver_installation](https://www.waveshare.com/wiki/3.5inch_RPi_LCD_(A)#Method_1._Driver_installation)
- At the step with `./LCD35-show`, add `lite` to the command prompt (e.g., `./LCD35-show lite`).
- Reboot.
- As root, make three symlinks:
- `cd ~`
- `ln -s pwnagotchi.png pwnagotchi_1.png`
- `ln -s pwnagotchi.png pwnagotchi_2.png`
- `ln -s pwnagotchi.png pwnagotchi_3.png`
- `apt install fbi`
- Change display type to `inky` in `config.yml`
- Add `modules-load=dwc2,g_ether` to your kernel command line (`/boot/cmdline.txt`) or it will break!
- Also must add `dtoverlay=dwc2` to the bottom of (`/boot/config.txt`)
- Edit `/etc/rc.local` and add: `fbi -T 1 -a -noverbose -t 15 -cachemem 0 /root/pwnagotchi_1.png /root/pwnagotchi_2.png /root/pwnagotchi_3.png &`
- Reboot.
And you should be good!

View File

@ -1,23 +0,0 @@
# Documentation
- [About the Project](https://github.com/evilsocket/pwnagotchi/blob/master/docs/about.md)
- [How to Install](https://github.com/evilsocket/pwnagotchi/blob/master/docs/install.md)
- [Configuration](https://github.com/evilsocket/pwnagotchi/blob/master/docs/configure.md)
- [Usage](https://github.com/evilsocket/pwnagotchi/blob/master/docs/usage.md)
- [Plugins](https://github.com/evilsocket/pwnagotchi/blob/master/docs/plugins.md)
- [Development](https://github.com/evilsocket/pwnagotchi/blob/master/docs/dev.md)
- [FAQ](https://github.com/evilsocket/pwnagotchi/blob/master/docs/faq.md)
- [Community Hacks](https://github.com/evilsocket/pwnagotchi/blob/master/docs/hacks.md)
## Links
&nbsp; | Official Links
---------|-------
Slack | [pwnagotchi.slack.com](https://pwnagotchi.herokuapp.com)
Twitter | [@pwnagotchi](https://twitter.com/pwnagotchi)
Subreddit | [r/pwnagotchi](https://www.reddit.com/r/pwnagotchi/)
Website | [pwnagotchi.ai](https://pwnagotchi.ai/)
## License
`pwnagotchi` is made with ♥ by [@evilsocket](https://twitter.com/evilsocket) and the [amazing dev team](https://github.com/evilsocket/pwnagotchi/graphs/contributors). It's released under the GPL3 license.

View File

@ -1,48 +0,0 @@
# Installation
The project has been developed to run on a Raspberry Pi 0 W configured as an [USB Ethernet gadget](https://learn.adafruit.com/turning-your-raspberry-pi-zero-into-a-usb-gadget/ethernet-gadget) device in order to connect to it via USB. However, given the proper configuration tweaks, any GNU/Linux computer with a WiFi interface that supports monitor mode could be used.
**An important note about the AI:** a network trained with a specific WiFi interface will only work with another interface if it supports
the same exact WiFi channels of the first one. For instance, you can not use a neural network trained on a Raspberry Pi Zero W (that only supports 2.4Ghz channels) with a 5Ghz antenna, but you'll need to train one from scratch for those channels.
## Required Hardware
- [Raspberry Pi Zero W](https://www.raspberrypi.org/products/raspberry-pi-zero-w/).
- A micro SD card, 8GB recomended, **preferably of good quality and speed**.
- A decent power bank (with 1500 mAh you get ~2 hours with AI on).
- One of the supported displays (optional).
### Display
The display is an optional component as the UI is also rendered via a web interface available via the USB cable. If you connect to `usb0` (by using the data port on the unit) and point your browser to the web ui (see config.yml), your unit can work in "headless mode".
If instead you want to fully enjoy walking around and literally looking at your unit's face, the supported display models are:
- [Waveshare eInk Display (both V1 and V2)](https://www.waveshare.com/2.13inch-e-paper-hat.htm)
- [Pimoroni Inky pHAT](https://shop.pimoroni.com/products/inky-phat)
- [PaPiRus eInk Screen](https://uk.pi-supply.com/products/papirus-zero-epaper-screen-phat-pi-zero)
Needless to say, we are always happy to receive pull requests adding support for new models.
One thing to note, not all displays are created equaly, TFT displays for example work similar to an HDMI display, and they are not supported, currently all the displays supported are I2C displays.
#### Color and Black & White displays
Some of the supported displays support Black & White and Coloured versions, one common question is regarding refresh speed of said displays.
Color displays have a much slower refresh rate, in some cases it can take up to 15 seconds, if slow refresh rates is something that you want to avoid we advise you to use Black & White displays
## Flashing an Image
The easiest way to create a new Pwnagotchi is downloading the latest stable image from [our release page](https://github.com/evilsocket/pwnagotchi/releases) and write it to your SD card. You will need to use an image writing tool to install the image you have downloaded on your SD card.
[balenaEtcher](https://www.balena.io/etcher/) is a graphical SD card writing tool that works on Mac OS, Linux and Windows, and is the easiest option for most users. balenaEtcher also supports writing images directly from the zip file, without any unzipping required. To write your image with balenaEtcher:
- Download the latest [Pwnagotchi .img file](https://github.com/evilsocket/pwnagotchi/releases).
- Download [balenaEtcher](https://www.balena.io/etcher/) and install it.
- Connect an SD card reader with the SD card inside.
- Open balenaEtcher and select from your hard drive the Raspberry Pi .img or .zip file you wish to write to the SD card.
- Select the SD card you wish to write your image to.
- Review your selections and click 'Flash!' to begin writing data to the SD card.
Your SD card is now ready for the first boot!

View File

@ -1,56 +0,0 @@
# Plugins
Pwnagotchi has a simple plugins system that you can use to customize your unit and its behaviour. You can place your plugins anywhere
as python files and then edit the `config.yml` file (`main.plugins` value) to point to their containing folder. Check the [plugins folder](https://github.com/evilsocket/pwnagotchi/tree/master/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/plugins/default/) for a list of default plugins and all the callbacks that you can define for your own customizations.
Here's as an example the GPS plugin:
```python
__author__ = 'evilsocket@gmail.com'
__version__ = '1.0.0'
__name__ = 'gps'
__license__ = 'GPL3'
__description__ = 'Save GPS coordinates whenever an handshake is captured.'
__enabled__ = True # set to false if you just don't use GPS
import core
import json
import os
device = '/dev/ttyUSB0'
speed = 19200
running = False
def on_loaded():
logging.info("GPS plugin loaded for %s" % device)
def on_ready(agent):
global running
if os.path.exists(device):
logging.info("enabling GPS bettercap's module for %s" % device)
try:
agent.run('gps off')
except:
pass
agent.run('set gps.device %s' % device)
agent.run('set gps.speed %d' % speed)
agent.run('gps on')
running = True
else:
logging.info("no GPS detected")
def on_handshake(agent, filename, access_point, client_station):
if running:
info = agent.session()
gps = info['gps']
gps_filename = filename.replace('.pcap', '.gps.json')
logging.info("saving GPS to %s (%s)" % (gps_filename, gps))
with open(gps_filename, 'w+t') as fp:
json.dump(gps, fp)
```

View File

@ -1,146 +0,0 @@
# Usage
## User Interface
The UI is available either via display if installed, or via http://pwnagotchi.local:8080/ if you connect to the unit via `usb0` and set a static address on the network interface (change `pwnagotchi` with the hostname of your unit).
![ui](https://i.imgur.com/XgIrcur.png)
* **CH**: Current channel the unit is operating on or `*` when hopping on all channels.
* **APS**: Number of access points on the current channel and total visible access points.
* **UP**: Time since the unit has been activated.
* **PWND**: Number of handshakes captured in this session and number of unique networks we own at least one handshake of, from the beginning.
* **AUTO**: This indicates that the algorithm is running with AI disabled (or still loading), it disappears once the AI dependencies have been bootrapped and the neural network loaded.
## Training the AI
At its core Pwnagotchi is a very simple creature: we could summarize its main algorithm as:
```python
# main loop
while True:
# ask bettercap for all visible access points and their clients
aps = get_all_visible_access_points()
# loop each AP
for ap in aps:
# send an association frame in order to grab the PMKID
send_assoc(ap)
# loop each client station of the AP
for client in ap.clients:
# deauthenticate the client to get its half or full handshake
deauthenticate(client)
wait_for_loot()
```
Despite its simplicity, this logic is controlled by several parameters that regulate the wait times, the timeouts, on which channels to hop and so on.
From `config.yml`:
```yaml
personality:
# advertise our presence
advertise: true
# perform a deauthentication attack to client stations in order to get full or half handshakes
deauth: true
# send association frames to APs in order to get the PMKID
associate: true
# list of channels to recon on, or empty for all channels
channels: []
# minimum WiFi signal strength in dBm
min_rssi: -200
# number of seconds for wifi.ap.ttl
ap_ttl: 120
# number of seconds for wifi.sta.ttl
sta_ttl: 300
# time in seconds to wait during channel recon
recon_time: 30
# number of inactive epochs after which recon_time gets multiplied by recon_inactive_multiplier
max_inactive_scale: 2
# if more than max_inactive_scale epochs are inactive, recon_time *= recon_inactive_multiplier
recon_inactive_multiplier: 2
# time in seconds to wait during channel hopping if activity has been performed
hop_recon_time: 10
# time in seconds to wait during channel hopping if no activity has been performed
min_recon_time: 5
# maximum amount of deauths/associations per BSSID per session
max_interactions: 3
# maximum amount of misses before considering the data stale and triggering a new recon
max_misses_for_recon: 5
# number of active epochs that triggers the excited state
excited_num_epochs: 10
# number of inactive epochs that triggers the bored state
bored_num_epochs: 15
# number of inactive epochs that triggers the sad state
sad_num_epochs: 25
```
There is no optimal set of parameters for every situation: when the unit is moving (during a walk for instance) smaller timeouts and RSSI thresholds might be preferred in order to quickly remove routers that are not in range anymore, while when stationary in high density areas (like an office) other parameters might be better. The role of the AI is to observe what's going on at the WiFi level, and adjust those parameters in order to maximize the cumulative reward of that loop / epoch.
## Reward Function
After each iteration of the main loop (an `epoch`), the reward, a score that represents how well the parameters performed, is computed as (an excerpt from `pwnagotchi/ai/reward.py`):
```python
# state contains the information of the last epoch
# epoch_n is the number of the last epoch
tot_epochs = epoch_n + 1e-20 # 1e-20 is added to avoid a division by 0
tot_interactions = max(state['num_deauths'] + state['num_associations'], state['num_handshakes']) + 1e-20
tot_channels = wifi.NumChannels
# ideally, for each interaction we would have an handshake
h = state['num_handshakes'] / tot_interactions
# small positive rewards the more active epochs we have
a = .2 * (state['active_for_epochs'] / tot_epochs)
# make sure we keep hopping on the widest channel spectrum
c = .1 * (state['num_hops'] / tot_channels)
# small negative reward if we don't see aps for a while
b = -.3 * (state['blind_for_epochs'] / tot_epochs)
# small negative reward if we interact with things that are not in range anymore
m = -.3 * (state['missed_interactions'] / tot_interactions)
# small negative reward for inactive epochs
i = -.2 * (state['inactive_for_epochs'] / tot_epochs)
reward = h + a + c + b + i + m
```
By maximizing this reward value, the AI learns over time to find the set of parameters that better perform with the current environmental conditions.
## BetterCAP's Web UI
Moreover, given that the unit is running bettercap with API and Web UI, you'll be able to use the unit as a WiFi penetration testing portable station by accessing `http://pwnagotchi.local/`.
![webui](https://raw.githubusercontent.com/bettercap/media/master/ui-events.png)
## Update your Pwnagotchi
You can use the `scripts/update_pwnagotchi.sh` script to update to the most recent version of pwnagotchi.
```shell
usage: ./update_pwnagitchi.sh [OPTIONS]
Options:
-v # Version to update to, can be a branch or commit. (default: master)
-u # Url to clone from. (default: https://github.com/evilsocket/pwnagotchi)
-m # Mode to restart to. (Supported: auto manual; default: auto)
-b # Backup the current pwnagotchi config.
-r # Restore the current pwnagotchi config. -b will be enabled.
-h # Shows this help. Shows this help.
```
## Backup your Pwnagotchi
You can use the `scripts/backup.sh` script to backup the important files of your unit.
```shell
usage: ./scripts/backup.sh HOSTNAME backup.zip
```
## Random Info
* **On a rpi0w, it'll take approximately 30 minutes to load the AI**.
* `/var/log/pwnagotchi.log` is your friend.
* if connected to a laptop via usb data port, with internet connectivity shared, magic things will happen.
* checkout the `ui.video` section of the `config.yml` - if you don't want to use a display, you can connect to it with the browser and a cable.
* If you get `[FAILED] Failed to start Remount Root and Kernel File Systems.` while booting pwnagotchi, make sure the `PARTUUID`s for `rootfs` and `boot` partitions are the same in `/etc/fstab`. Use `sudo blkid` to find those values when you are using `create_sibling.sh`.

View File

@ -1,6 +1,10 @@
import subprocess
import os
import logging
import time
import pwnagotchi.ui.view as view
version = '1.0.0plz4'
version = '1.0.0a'
_name = None
@ -46,3 +50,13 @@ def temperature(celsius=True):
temp = int(fp.read().strip())
c = int(temp / 1000)
return c if celsius else ((c * (9 / 5)) + 32)
def shutdown():
logging.warning("shutting down ...")
if view.ROOT:
view.ROOT.on_shutdown()
# give it some time to refresh the ui
time.sleep(5)
os.system("sync")
os.system("halt")

View File

@ -17,13 +17,13 @@ RECOVERY_DATA_FILE = '/root/.pwnagotchi-recovery'
class Agent(Client, AsyncAdvertiser, AsyncTrainer):
def __init__(self, view, config):
def __init__(self, view, config, keypair):
Client.__init__(self, config['bettercap']['hostname'],
config['bettercap']['scheme'],
config['bettercap']['port'],
config['bettercap']['username'],
config['bettercap']['password'])
AsyncAdvertiser.__init__(self, config, view)
AsyncAdvertiser.__init__(self, config, view, keypair)
AsyncTrainer.__init__(self, config)
self._started_at = time.time()
@ -296,7 +296,8 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
def _update_peers(self):
peer = self._advertiser.closest_peer()
self._view.set_closest_peer(peer)
tot = self._advertiser.num_peers()
self._view.set_closest_peer(peer, tot)
def _save_recovery_data(self):
logging.warning("writing recovery data to %s ..." % RECOVERY_DATA_FILE)

View File

@ -39,4 +39,4 @@ def load(config, agent, epoch, from_disk=True):
for key, value in config['params'].items():
logging.info(" %s: %s" % (key, value))
return a2c
return a2c

View File

@ -6,6 +6,11 @@ main:
custom_plugins:
# which plugins to load and enable
plugins:
grid:
enabled: true
report: false # don't report pwned networks by default!
exclude: # do not report the following networks (accepts both ESSIDs and BSSIDs)
- YourHomeNetworkHere
auto-update:
enabled: false
interval: 1 # every day
@ -15,9 +20,8 @@ main:
files:
- /root/brain.nn
- /root/brain.json
- /root/custom.yml
- /root/handshakes
- /etc/ssh
- /root/handshakes/
- /etc/pwnagotchi/
- /etc/hostname
- /etc/hosts
- /etc/motd
@ -27,6 +31,8 @@ main:
- 'scp /tmp/backup.tar.gz pwnagotchi@10.0.0.1:/home/pwnagotchi/backups/backup-$(date +%s).tar.gz'
gps:
enabled: false
speed: 19200
device: /dev/ttyUSB0
twitter:
enabled: false
consumer_key: aaa
@ -39,6 +45,9 @@ main:
wpa-sec:
enabled: false
api_key: ~
wigle:
enabled: false
api_key: ~
# monitor interface to use
iface: mon0

47
pwnagotchi/identity.py Normal file
View File

@ -0,0 +1,47 @@
from Crypto.Signature import PKCS1_PSS
from Crypto.PublicKey import RSA
import Crypto.Hash.SHA256 as SHA256
import base64
import hashlib
import os
import logging
DefaultPath = "/etc/pwnagotchi/"
class KeyPair(object):
def __init__(self, path=DefaultPath):
self.path = path
self.priv_path = os.path.join(path, "id_rsa")
self.priv_key = None
self.pub_path = "%s.pub" % self.priv_path
self.pub_key = None
if not os.path.exists(self.path):
os.makedirs(self.path)
if not os.path.exists(self.priv_path) or not os.path.exists(self.pub_path):
logging.info("generating %s ..." % self.priv_path)
os.system("/usr/bin/ssh-keygen -t rsa -m PEM -b 4096 -N '' -f '%s'" % self.priv_path)
with open(self.priv_path) as fp:
self.priv_key = RSA.importKey(fp.read())
with open(self.pub_path) as fp:
self.pub_key = RSA.importKey(fp.read())
self.pub_key_pem = self.pub_key.exportKey('PEM').decode("ascii")
# python is special
if 'RSA PUBLIC KEY' not in self.pub_key_pem:
self.pub_key_pem = self.pub_key_pem.replace('PUBLIC KEY', 'RSA PUBLIC KEY')
pem = self.pub_key_pem.encode("ascii")
self.pub_key_pem_b64 = base64.b64encode(pem).decode("ascii")
self.fingerprint = hashlib.sha256(pem).hexdigest()
def sign(self, message):
hasher = SHA256.new(message.encode("ascii"))
signer = PKCS1_PSS.new(self.priv_key, saltLen=16)
signature = signer.sign(hasher)
signature_b64 = base64.b64encode(signature).decode("ascii")
return signature, signature_b64

View File

@ -33,11 +33,11 @@ msgid "AI ready."
msgstr "ΤΝ έτοιμη."
msgid "The neural network is ready."
msgstr "Το νευρωνικό δίκτυοείναι έτοιμο."
msgstr "Το νευρωνικό δίκτυο είναι έτοιμο."
#, python-brace-format
msgid "Hey, channel {channel} is free! Your AP will say thanks."
msgstr "Ε, το κανάλι {channel} είναιελεύθερο! Το AP σου θαείναι ευγνώμων."
msgstr "Ε, το κανάλι {channel} είναιελεύθερο! Το AP σου θα είναι ευγνώμων."
msgid "I'm bored ..."
msgstr "Βαριέμαι ..."

View File

@ -25,20 +25,20 @@ msgid "Hi, I'm Pwnagotchi! Starting ..."
msgstr "Bonjour, je suis Pwnagotchi! Démarrage ..."
msgid "New day, new hunt, new pwns!"
msgstr "Nouvelle journée, nouvelle chasse, nouveau pwns!"
msgstr "Nouveau jour, nouvelle chasse, nouveaux pwns !"
msgid "Hack the Planet!"
msgstr "Hack la planète!"
msgid "AI ready."
msgstr "IA prête."
msgstr "L'IA est prête."
msgid "The neural network is ready."
msgstr "Le réseau neuronal est prêt."
#, python-brace-format
msgid "Hey, channel {channel} is free! Your AP will say thanks."
msgstr "Hey, le channel {channel} est libre! Ton AP va dis merci."
msgstr "Hey, le channel {channel} est libre! Ton point d'accès va te remercier."
msgid "I'm bored ..."
msgstr "Je m'ennuie ..."
@ -68,17 +68,17 @@ msgid "I pwn therefore I am."
msgstr "Je pwn donc je suis."
msgid "So many networks!!!"
msgstr "Autant de réseaux!!!"
msgstr "Tellement de réseaux!!!"
msgid "I'm having so much fun!"
msgstr "Je m'amuse tellement!"
msgid "My crime is that of curiosity ..."
msgstr "Mon crime est celui de la curiosité ..."
msgstr "Mon crime, c'est la curiosité ..."
#, python-brace-format
msgid "Hello {name}! Nice to meet you. {name}"
msgstr "Bonjour {name}! Ravis de te rencontrer. {name}"
msgstr "Bonjour {name}! Ravi de te rencontrer. {name}"
#, python-brace-format
msgid "Unit {name} is nearby! {name}"
@ -145,7 +145,7 @@ msgstr ""
#, python-brace-format
msgid "Just decided that {mac} needs no WiFi!"
msgstr "Décidé à l'instant que {mac} n'a pas besoin de WiFi!"
msgstr "Je viens de décider que {mac} n'a pas besoin de WiFi!"
#, python-brace-format
msgid "Deauthenticating {mac}"
@ -153,11 +153,11 @@ msgstr "Désauthentification de {mac}"
#, python-brace-format
msgid "Kickbanning {mac}!"
msgstr ""
msgstr "Je kick et je bannis {mac}!"
#, python-brace-format
msgid "Cool, we got {num} new handshake{plural}!"
msgstr "Cool, nous avons {num} nouveaux handshake{plural}!"
msgstr "Cool, on a {num} nouveaux handshake{plural}!"
msgid "Ops, something went wrong ... Rebooting ..."
msgstr "Oups, quelque chose s'est mal passé ... Redémarrage ..."
@ -188,7 +188,7 @@ msgid ""
"#pwnlog #pwnlife #hacktheplanet #skynet"
msgstr ""
"J'ai pwn durant {duration} et kick {deauthed} clients! J'ai aussi rencontré "
"{associated} nouveaux amis and mangé {handshakes} handshakes! #pwnagotchi "
"{associated} nouveaux amis et dévoré {handshakes} handshakes! #pwnagotchi "
"#pwnlog #pwnlife #hacktheplanet #skynet"
msgid "hours"

View File

@ -13,7 +13,7 @@ LAST_SESSION_FILE = '/root/.pwnagotchi-last-session'
class SessionParser(object):
EPOCH_TOKEN = '[epoch '
EPOCH_PARSER = re.compile(r'^\s*\[epoch (\d+)\] (.+)')
EPOCH_PARSER = re.compile(r'^.+\[epoch (\d+)\] (.+)')
EPOCH_DATA_PARSER = re.compile(r'([a-z_]+)=([^\s]+)')
TRAINING_TOKEN = ' training epoch '
START_TOKEN = 'connecting to http'

View File

@ -1,14 +1,4 @@
import os
from Crypto.PublicKey import RSA
import hashlib
def new_session_id():
return ':'.join(['%02x' % b for b in os.urandom(6)])
def get_identity(config):
pubkey = None
with open(config['main']['pubkey']) as fp:
pubkey = RSA.importKey(fp.read())
return pubkey, hashlib.sha1(pubkey.exportKey('DER')).hexdigest()

View File

@ -152,7 +152,7 @@ class Advertiser(object):
if self._is_broadcasted_advertisement(dot11):
try:
dot11elt = p.getlayer(Dot11Elt)
if dot11elt.ID == wifi.Dot11ElemID_Identity:
if dot11elt.ID == wifi.Dot11ElemID_Whisper:
self._parse_identity(p[RadioTap], dot11, dot11elt)
else:

View File

@ -3,14 +3,13 @@ import logging
import pwnagotchi
import pwnagotchi.plugins as plugins
from pwnagotchi.mesh import get_identity
class AsyncAdvertiser(object):
def __init__(self, config, view):
def __init__(self, config, view, keypair):
self._config = config
self._view = view
self._public_key, self._identity = get_identity(config)
self._keypair = keypair
self._advertiser = None
def start_advertising(self):
@ -24,7 +23,7 @@ class AsyncAdvertiser(object):
self._config['main']['iface'],
pwnagotchi.name(),
pwnagotchi.version,
self._identity,
self._keypair.fingerprint,
period=0.3,
data=self._config['personality'])

View File

@ -1,6 +1,6 @@
SignatureAddress = 'de:ad:be:ef:de:ad'
BroadcastAddress = 'ff:ff:ff:ff:ff:ff'
Dot11ElemID_Identity = 222
Dot11ElemID_Whisper = 222
NumChannels = 140
def freq_to_channel(freq):
@ -30,7 +30,7 @@ def encapsulate(payload, addr_from, addr_to=BroadcastAddress):
while data_left > 0:
sz = min(chunk_size, data_left)
chunk = payload[data_off: data_off + sz]
frame /= Dot11Elt(ID=Dot11ElemID_Identity, info=chunk, len=sz)
frame /= Dot11Elt(ID=Dot11ElemID_Whisper, info=chunk, len=sz)
data_off += sz
data_left -= sz

View File

@ -33,7 +33,7 @@ def on_loaded():
logging.info("AUTO-BACKUP: Successfuly loaded.")
def on_internet_available(display, config, log):
def on_internet_available(display, keypair, config, log):
global STATUS
if READY:

View File

@ -23,7 +23,7 @@ def on_loaded():
READY = True
def on_internet_available(display, config, log):
def on_internet_available(display, keypair, config, log):
global STATUS
if READY:

View File

@ -20,7 +20,7 @@ def on_loaded():
# called in manual mode when there's internet connectivity
def on_internet_available(ui, config, log):
def on_internet_available(ui, keypair, config, log):
pass

View File

@ -8,9 +8,8 @@ import logging
import json
import os
device = '/dev/ttyUSB0'
speed = 19200
running = False
OPTIONS = dict()
def on_loaded():
@ -27,8 +26,8 @@ def on_ready(agent):
except:
pass
agent.run('set gps.device %s' % device)
agent.run('set gps.speed %d' % speed)
agent.run('set gps.device %s' % OPTIONS['device'])
agent.run('set gps.speed %d' % OPTIONS['speed'])
agent.run('gps on')
running = True
else:

View File

@ -0,0 +1,161 @@
__author__ = 'evilsocket@gmail.com'
__version__ = '1.0.0'
__name__ = 'grid'
__license__ = 'GPL3'
__description__ = 'This plugin signals the unit cryptographic identity and list of pwned networks and list of pwned networks to api.pwnagotchi.ai'
import os
import logging
import requests
import glob
import subprocess
import pwnagotchi
import pwnagotchi.utils as utils
from pwnagotchi.utils import WifiInfo, extract_from_pcap
OPTIONS = dict()
AUTH = utils.StatusFile('/root/.api-enrollment.json', data_format='json')
REPORT = utils.StatusFile('/root/.api-report.json', data_format='json')
def on_loaded():
logging.info("api plugin loaded.")
def get_api_token(log, keys):
global AUTH
if AUTH.newer_then_minutes(25) and AUTH.data is not None and 'token' in AUTH.data:
return AUTH.data['token']
if AUTH.data is None:
logging.info("api: enrolling unit ...")
else:
logging.info("api: refreshing token ...")
identity = "%s@%s" % (pwnagotchi.name(), keys.fingerprint)
# sign the identity string to prove we own both keys
_, signature_b64 = keys.sign(identity)
api_address = 'https://api.pwnagotchi.ai/api/v1/unit/enroll'
enrollment = {
'identity': identity,
'public_key': keys.pub_key_pem_b64,
'signature': signature_b64,
'data': {
'duration': log.duration,
'epochs': log.epochs,
'train_epochs': log.train_epochs,
'avg_reward': log.avg_reward,
'min_reward': log.min_reward,
'max_reward': log.max_reward,
'deauthed': log.deauthed,
'associated': log.associated,
'handshakes': log.handshakes,
'peers': log.peers,
'uname': subprocess.getoutput("uname -a")
}
}
r = requests.post(api_address, json=enrollment)
if r.status_code != 200:
raise Exception("(status %d) %s" % (r.status_code, r.json()))
AUTH.update(data=r.json())
logging.info("api: done")
return AUTH.data["token"]
def parse_pcap(filename):
logging.info("api: parsing %s ..." % filename)
net_id = os.path.basename(filename).replace('.pcap', '')
if '_' in net_id:
# /root/handshakes/ESSID_BSSID.pcap
essid, bssid = net_id.split('_')
else:
# /root/handshakes/BSSID.pcap
essid, bssid = '', net_id
it = iter(bssid)
bssid = ':'.join([a + b for a, b in zip(it, it)])
info = {
WifiInfo.ESSID: essid,
WifiInfo.BSSID: bssid,
}
try:
info = extract_from_pcap(filename, [WifiInfo.BSSID, WifiInfo.ESSID])
except Exception as e:
logging.error("api: %s" % e)
return info[WifiInfo.ESSID], info[WifiInfo.BSSID]
def api_report_ap(log, keys, token, essid, bssid):
while True:
token = AUTH.data['token']
logging.info("api: reporting %s (%s)" % (essid, bssid))
try:
api_address = 'https://api.pwnagotchi.ai/api/v1/unit/report/ap'
headers = {'Authorization': 'access_token %s' % token}
report = {
'essid': essid,
'bssid': bssid,
}
r = requests.post(api_address, headers=headers, json=report)
if r.status_code != 200:
if r.status_code == 401:
logging.warning("token expired")
token = get_api_token(log, keys)
continue
else:
raise Exception("(status %d) %s" % (r.status_code, r.text))
else:
return True
except Exception as e:
logging.error("api: %s" % e)
return False
def on_internet_available(ui, keys, config, log):
global REPORT
try:
pcap_files = glob.glob(os.path.join(config['bettercap']['handshakes'], "*.pcap"))
num_networks = len(pcap_files)
reported = REPORT.data_field_or('reported', default=[])
num_reported = len(reported)
num_new = num_networks - num_reported
if num_new > 0:
logging.info("api: %d new networks to report" % num_new)
token = get_api_token(log, keys)
if OPTIONS['report']:
for pcap_file in pcap_files:
net_id = os.path.basename(pcap_file).replace('.pcap', '')
do_skip = False
for skip in OPTIONS['exclude']:
skip = skip.lower()
net = net_id.lower()
if skip in net or skip.replace(':', '') in net:
do_skip = True
break
if net_id not in reported and not do_skip:
essid, bssid = parse_pcap(pcap_file)
if bssid:
if api_report_ap(log, keys, token, essid, bssid):
reported.append(net_id)
REPORT.update(data={'reported': reported})
else:
logging.info("api: reporting disabled")
except Exception as e:
logging.exception("error while enrolling the unit")

View File

@ -55,14 +55,14 @@ def _upload_to_ohc(path, timeout=30):
raise e
def on_internet_available(display, config, log):
def on_internet_available(display, keypair, config, log):
"""
Called in manual mode when there's internet connectivity
"""
if READY:
handshake_dir = config['bettercap']['handshakes']
handshake_filenames = os.listdir(handshake_dir)
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames]
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if filename.endswith('.pcap')]
handshake_new = set(handshake_paths) - set(ALREADY_UPLOADED)
if handshake_new:

View File

@ -14,7 +14,7 @@ def on_loaded():
# called in manual mode when there's internet connectivity
def on_internet_available(ui, config, log):
def on_internet_available(ui, keypair, config, log):
if log.is_new() and log.handshakes > 0:
try:
import tweepy

View File

@ -0,0 +1,275 @@
__author__ = '33197631+dadav@users.noreply.github.com'
__version__ = '1.0.0'
__name__ = 'wigle'
__license__ = 'GPL3'
__description__ = 'This plugin automatically uploades collected wifis to wigle.net'
import os
import logging
import json
from io import StringIO
import csv
from datetime import datetime
import requests
from pwnagotchi.mesh.wifi import freq_to_channel
from pwnagotchi.utils import WifiInfo, FieldNotFoundError, extract_from_pcap
READY = False
ALREADY_UPLOADED = None
SKIP = None
OPTIONS = dict()
AKMSUITE_TYPES = {
0x00: "Reserved",
0x01: "802.1X",
0x02: "PSK",
}
def _handle_packet(packet, result):
from scapy.all import RadioTap, Dot11Elt, Dot11Beacon, rdpcap, Scapy_Exception, Dot11, Dot11ProbeResp, Dot11AssoReq, \
Dot11ReassoReq, Dot11EltRSN, Dot11EltVendorSpecific, Dot11EltMicrosoftWPA
"""
Analyze each packet and extract the data from Dot11 layers
"""
if hasattr(packet, 'cap') and 'privacy' in packet.cap:
# packet is encrypted
if 'encryption' not in result:
result['encryption'] = set()
if packet.haslayer(Dot11Beacon):
if packet.haslayer(Dot11Beacon)\
or packet.haslayer(Dot11ProbeResp)\
or packet.haslayer(Dot11AssoReq)\
or packet.haslayer(Dot11ReassoReq):
if 'bssid' not in result and hasattr(packet[Dot11], 'addr3'):
result['bssid'] = packet[Dot11].addr3
if 'essid' not in result and hasattr(packet[Dot11Elt], 'info'):
result['essid'] = packet[Dot11Elt].info
if 'channel' not in result and hasattr(packet[Dot11Elt:3], 'info'):
result['channel'] = int(ord(packet[Dot11Elt:3].info))
if packet.haslayer(RadioTap):
if 'rssi' not in result and hasattr(packet[RadioTap], 'dBm_AntSignal'):
result['rssi'] = packet[RadioTap].dBm_AntSignal
if 'channel' not in result and hasattr(packet[RadioTap], 'ChannelFrequency'):
result['channel'] = freq_to_channel(packet[RadioTap].ChannelFrequency)
# see: https://fossies.org/linux/scapy/scapy/layers/dot11.py
if packet.haslayer(Dot11EltRSN):
if hasattr(packet[Dot11EltRSN], 'akm_suites'):
auth = AKMSUITE_TYPES.get(packet[Dot11EltRSN].akm_suites[0].suite)
result['encryption'].add(f"WPA2/{auth}")
else:
result['encryption'].add("WPA2")
if packet.haslayer(Dot11EltVendorSpecific)\
and (packet.haslayer(Dot11EltMicrosoftWPA)
or packet.info.startswith(b'\x00P\xf2\x01\x01\x00')):
if hasattr(packet, 'akm_suites'):
auth = AKMSUITE_TYPES.get(packet.akm_suites[0].suite)
result['encryption'].add(f"WPA2/{auth}")
else:
result['encryption'].add("WPA2")
# end see
return result
def _analyze_pcap(pcap):
from scapy.all import RadioTap, Dot11Elt, Dot11Beacon, rdpcap, Scapy_Exception, Dot11, Dot11ProbeResp, Dot11AssoReq, \
Dot11ReassoReq, Dot11EltRSN, Dot11EltVendorSpecific, Dot11EltMicrosoftWPA
"""
Iterate over the packets and extract data
"""
result = dict()
try:
packets = rdpcap(pcap)
for packet in packets:
result = _handle_packet(packet, result)
except Scapy_Exception as sc_e:
raise sc_e
return result
def on_loaded():
"""
Gets called when the plugin gets loaded
"""
global READY
global ALREADY_UPLOADED
global SKIP
SKIP = list()
if 'api_key' not in OPTIONS or ('api_key' in OPTIONS and OPTIONS['api_key'] is None):
logging.error("WIGLE: api_key isn't set. Can't upload to wigle.net")
return
try:
with open('/root/.wigle_uploads', 'r') as f:
ALREADY_UPLOADED = f.read().splitlines()
except OSError:
logging.warning('WIGLE: No upload-file found.')
ALREADY_UPLOADED = []
READY = True
def _extract_gps_data(path):
"""
Extract data from gps-file
return json-obj
"""
try:
with open(path, 'r') as json_file:
return json.load(json_file)
except OSError as os_err:
raise os_err
except json.JSONDecodeError as json_err:
raise json_err
def _format_auth(data):
out = ""
for auth in data:
out = f"{out}[{auth}]"
return out
def _transform_wigle_entry(gps_data, pcap_data):
"""
Transform to wigle entry in file
"""
dummy = StringIO()
# write kismet header
dummy.write("WigleWifi-1.4,appRelease=20190201,model=Kismet,release=2019.02.01.{},device=kismet,display=kismet,board=kismet,brand=kismet\n")
dummy.write("MAC,SSID,AuthMode,FirstSeen,Channel,RSSI,CurrentLatitude,CurrentLongitude,AltitudeMeters,AccuracyMeters,Type")
writer = csv.writer(dummy, delimiter=",", quoting=csv.QUOTE_NONE)
writer.writerow([
pcap_data[WifiInfo.BSSID],
pcap_data[WifiInfo.ESSID],
_format_auth(pcap_data[WifiInfo.ENCRYPTION]),
datetime.strptime(gps_data['Updated'].rsplit('.')[0],
"%Y-%m-%dT%H:%M:%S").strftime('%Y-%m-%d %H:%M:%S'),
pcap_data[WifiInfo.CHANNEL],
pcap_data[WifiInfo.RSSI],
gps_data['Latitude'],
gps_data['Longitude'],
gps_data['Altitude'],
0, # accuracy?
'WIFI'])
return dummy.getvalue()
def _send_to_wigle(lines, api_key, timeout=30):
"""
Uploads the file to wigle-net
"""
dummy = StringIO()
for line in lines:
dummy.write(f"{line}")
dummy.seek(0)
headers = {'Authorization': f"Basic {api_key}",
'Accept': 'application/json'}
data = {'donate': 'false'}
payload = {'file': dummy, 'type': 'text/csv'}
try:
res = requests.post('https://api.wigle.net/api/v2/file/upload',
data=data,
headers=headers,
files=payload,
timeout=timeout)
json_res = res.json()
if not json_res['success']:
raise requests.exceptions.RequestException(json_res['message'])
except requests.exceptions.RequestException as re_e:
raise re_e
def on_internet_available(display, keypair, config, log):
from scapy.all import RadioTap, Dot11Elt, Dot11Beacon, rdpcap, Scapy_Exception, Dot11, Dot11ProbeResp, Dot11AssoReq, \
Dot11ReassoReq, Dot11EltRSN, Dot11EltVendorSpecific, Dot11EltMicrosoftWPA
"""
Called in manual mode when there's internet connectivity
"""
global ALREADY_UPLOADED
global SKIP
if READY:
handshake_dir = config['bettercap']['handshakes']
all_files = os.listdir(handshake_dir)
all_gps_files = [os.path.join(handshake_dir, filename)
for filename in all_files
if filename.endswith('.gps.json')]
new_gps_files = set(all_gps_files) - set(ALREADY_UPLOADED) - set(SKIP)
if new_gps_files:
logging.info("WIGLE: Internet connectivity detected. Uploading new handshakes to wigle.net")
csv_entries = list()
no_err_entries = list()
for gps_file in new_gps_files:
pcap_filename = gps_file.replace('.gps.json', '.pcap')
if not os.path.exists(pcap_filename):
logging.error("WIGLE: Can't find pcap for %s", gps_file)
SKIP.append(gps_file)
continue
try:
gps_data = _extract_gps_data(gps_file)
except OSError as os_err:
logging.error("WIGLE: %s", os_err)
SKIP.append(gps_file)
continue
except json.JSONDecodeError as json_err:
logging.error("WIGLE: %s", json_err)
SKIP.append(gps_file)
continue
try:
pcap_data = extract_from_pcap(pcap_filename, [WifiInfo.BSSID,
WifiInfo.ESSID,
WifiInfo.ENCRYPTION,
WifiInfo.CHANNEL,
WifiInfo.RSSI])
except FieldNotFoundError:
logging.error("WIGLE: Could not extract all informations. Skip %s", gps_file)
SKIP.append(gps_file)
continue
except Scapy_Exception as sc_e:
logging.error("WIGLE: %s", sc_e)
SKIP.append(gps_file)
continue
new_entry = _transform_wigle_entry(gps_data, pcap_data)
csv_entries.append(new_entry)
no_err_entries.append(gps_file)
if csv_entries:
display.set('status', "Uploading gps-data to wigle.net ...")
display.update(force=True)
try:
_send_to_wigle(csv_entries, OPTIONS['api_key'])
ALREADY_UPLOADED += no_err_entries
with open('/root/.wigle_uploads', 'a') as up_file:
for gps in no_err_entries:
up_file.write(gps + "\n")
logging.info("WIGLE: Successfuly uploaded %d files", len(no_err_entries))
except requests.exceptions.RequestException as re_e:
SKIP += no_err_entries
logging.error("WIGLE: Got an exception while uploading %s", re_e)
except OSError as os_e:
SKIP += no_err_entries
logging.error("WIGLE: Got the following error: %s", os_e)

View File

@ -54,14 +54,14 @@ def _upload_to_wpasec(path, timeout=30):
raise e
def on_internet_available(display, config, log):
def on_internet_available(display, keypair, config, log):
"""
Called in manual mode when there's internet connectivity
"""
if READY:
handshake_dir = config['bettercap']['handshakes']
handshake_filenames = os.listdir(handshake_dir)
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames]
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if filename.endswith('.pcap')]
handshake_new = set(handshake_paths) - set(ALREADY_UPLOADED)
if handshake_new:

View File

@ -15,11 +15,29 @@ class VideoHandler(BaseHTTPRequestHandler):
_lock = Lock()
_index = """<html>
<head>
<title>%s</title>
<title>%s</title>
<style>
.block {
-webkit-appearance: button;
-moz-appearance: button;
appearance: button;
display: block;
cursor: pointer;
text-align: center;
}
</style>
</head>
<body>
<img src="/ui" id="ui"/>
<div style="position: absolute; top:0; left:0; width:100%%;">
<img src="/ui" id="ui" style="width:100%%"/>
<br/>
<hr/>
<form action="/shutdown" onsubmit="return confirm('This will halt the unit, continue?');">
<input type="submit" class="block" value="Shutdown"/>
</form>
</div>
<script type="text/javascript">
window.onload = function() {
var image = document.getElementById("ui");
@ -50,6 +68,9 @@ class VideoHandler(BaseHTTPRequestHandler):
except:
pass
elif self.path.startswith('/shutdown'):
pwnagotchi.shutdown()
elif self.path.startswith('/ui'):
with self._lock:
self.send_response(200)

View File

@ -15,7 +15,7 @@ from pwnagotchi.ui.state import State
WHITE = 0xff
BLACK = 0x00
ROOT = None
def setup_display_specifics(config):
width = 0
@ -57,9 +57,12 @@ def setup_display_specifics(config):
class View(object):
def __init__(self, config, state={}):
global ROOT
self._render_cbs = []
self._config = config
self._canvas = None
self._frozen = False
self._lock = Lock()
self._voice = Voice(lang=config['main']['lang'])
@ -119,6 +122,8 @@ class View(object):
logging.warning("ui.fps is 0, the display will only update for major changes")
self._ignore_changes = ('uptime', 'name')
ROOT = self
def add_element(self, key, elem):
self._state.add_element(key, elem)
@ -153,7 +158,7 @@ class View(object):
self.set('face', faces.AWAKE)
def on_ai_ready(self):
self.set('mode', '')
self.set('mode', ' AI')
self.set('face', faces.HAPPY)
self.set('status', self._voice.on_ai_ready())
self.update()
@ -168,7 +173,7 @@ class View(object):
self.set('aps', "%d" % log.associated)
self.set('shakes', '%d (%s)' % (log.handshakes, \
utils.total_unique_handshakes(self._config['bettercap']['handshakes'])))
self.set_closest_peer(log.last_peer)
self.set_closest_peer(log.last_peer, log.peers)
def is_normal(self):
return self._state.get('face') not in (
@ -188,7 +193,7 @@ class View(object):
self.set('status', self._voice.on_normal())
self.update()
def set_closest_peer(self, peer):
def set_closest_peer(self, peer, num_total):
if peer is None:
self.set('friend_face', None)
self.set('friend_name', None)
@ -207,6 +212,12 @@ class View(object):
name += '' * (4 - num_bars)
name += ' %s %d (%d)' % (peer.name(), peer.pwnd_run(), peer.pwnd_total())
if num_total > 1:
if num_total > 9000:
name += ' of over 9000'
else:
name += ' of %d' % num_total
self.set('friend_face', peer.face())
self.set('friend_name', name)
self.update()
@ -255,6 +266,12 @@ class View(object):
self.on_normal()
def on_shutdown(self):
self.set('face', faces.SLEEP)
self.set('status', self._voice.on_shutdown())
self.update(force=True)
self._frozen = True
def on_bored(self):
self.set('face', faces.BORED)
self.set('status', self._voice.on_bored())
@ -317,6 +334,9 @@ class View(object):
def update(self, force=False):
with self._lock:
if self._frozen:
return
changes = self._state.changes(ignore=self._ignore_changes)
if force or len(changes):
self._canvas = Image.new('1', (self._width, self._height), WHITE)

View File

@ -1,10 +1,12 @@
from datetime import datetime
from enum import Enum
import logging
import glob
import os
import time
import subprocess
import yaml
import json
# https://stackoverflow.com/questions/823196/yaml-merge-in-python
@ -19,13 +21,15 @@ def merge_config(user, default):
def load_config(args):
with open(args.config, 'rt') as fp:
with open(args.config) as fp:
config = yaml.safe_load(fp)
if os.path.exists(args.user_config):
with open(args.user_config, 'rt') as fp:
with open(args.user_config) as fp:
user_config = yaml.safe_load(fp)
config = merge_config(user_config, config)
# if the file is empty, safe_load will return None and merge_config will boom.
if user_config:
config = merge_config(user_config, config)
return config
@ -80,19 +84,138 @@ def blink(times=1, delay=0.3):
time.sleep(delay)
led(True)
class WifiInfo(Enum):
"""
Fields you can extract from a pcap file
"""
BSSID = 0
ESSID = 1
ENCRYPTION = 2
CHANNEL = 3
RSSI = 4
class FieldNotFoundError(Exception):
pass
def extract_from_pcap(path, fields):
"""
Search in pcap-file for specified information
path: Path to pcap file
fields: Array of fields that should be extracted
If a field is not found, FieldNotFoundError is raised
"""
results = dict()
for field in fields:
if not isinstance(field, WifiInfo):
raise TypeError("Invalid field")
subtypes = set()
if field == WifiInfo.BSSID:
from scapy.all import Dot11Beacon, Dot11ProbeResp, Dot11AssoReq, Dot11ReassoReq, Dot11, sniff
subtypes.add('beacon')
bpf_filter = " or ".join([f"wlan type mgt subtype {subtype}" for subtype in subtypes])
packets = sniff(offline=path, filter=bpf_filter)
try:
for packet in packets:
if packet.haslayer(Dot11Beacon):
if hasattr(packet[Dot11], 'addr3'):
results[field] = packet[Dot11].addr3
break
else: # magic
raise FieldNotFoundError("Could not find field [BSSID]")
except Exception:
raise FieldNotFoundError("Could not find field [BSSID]")
elif field == WifiInfo.ESSID:
from scapy.all import Dot11Beacon, Dot11ReassoReq, Dot11AssoReq, Dot11, sniff, Dot11Elt
subtypes.add('beacon')
subtypes.add('assoc-req')
subtypes.add('reassoc-req')
bpf_filter = " or ".join([f"wlan type mgt subtype {subtype}" for subtype in subtypes])
packets = sniff(offline=path, filter=bpf_filter)
try:
for packet in packets:
if packet.haslayer(Dot11Elt) and hasattr(packet[Dot11Elt], 'info'):
results[field] = packet[Dot11Elt].info.decode('utf-8')
break
else: # magic
raise FieldNotFoundError("Could not find field [ESSID]")
except Exception:
raise FieldNotFoundError("Could not find field [ESSID]")
elif field == WifiInfo.ENCRYPTION:
from scapy.all import Dot11Beacon, sniff
subtypes.add('beacon')
bpf_filter = " or ".join([f"wlan type mgt subtype {subtype}" for subtype in subtypes])
packets = sniff(offline=path, filter=bpf_filter)
try:
for packet in packets:
if packet.haslayer(Dot11Beacon) and hasattr(packet[Dot11Beacon], 'network_stats'):
stats = packet[Dot11Beacon].network_stats()
if 'crypto' in stats:
results[field] = stats['crypto'] # set with encryption types
break
else: # magic
raise FieldNotFoundError("Could not find field [ENCRYPTION]")
except Exception:
raise FieldNotFoundError("Could not find field [ENCRYPTION]")
elif field == WifiInfo.CHANNEL:
from scapy.all import sniff, RadioTap
from pwnagotchi.mesh.wifi import freq_to_channel
packets = sniff(offline=path, count=1)
try:
results[field] = freq_to_channel(packets[0][RadioTap].ChannelFrequency)
except Exception:
raise FieldNotFoundError("Could not find field [CHANNEL]")
elif field == WifiInfo.RSSI:
from scapy.all import sniff, RadioTap
from pwnagotchi.mesh.wifi import freq_to_channel
packets = sniff(offline=path, count=1)
try:
results[field] = packets[0][RadioTap].dBm_AntSignal
except Exception:
raise FieldNotFoundError("Could not find field [RSSI]")
return results
class StatusFile(object):
def __init__(self, path):
def __init__(self, path, data_format='raw'):
self._path = path
self._updated = None
self._format = data_format
self.data = None
if os.path.exists(path):
self._updated = datetime.fromtimestamp(os.path.getmtime(path))
with open(path) as fp:
if data_format == 'json':
self.data = json.load(fp)
else:
self.data = fp.read()
def data_field_or(self, name, default=""):
if self.data is not None and name in self.data:
return self.data[name]
return default
def newer_then_minutes(self, minutes):
return self._updated is not None and ((datetime.now() - self._updated).seconds / 60) < minutes
def newer_then_days(self, days):
return self._updated is not None and (datetime.now() - self._updated).days < days
def update(self, data=None):
self._updated = datetime.now()
self.data = data
with open(self._path, 'w') as fp:
fp.write(str(self._updated) if data is None else data)
if data is None:
fp.write(str(self._updated))
elif self._format == 'json':
json.dump(self.data, fp)
else:
fp.write(data)

View File

@ -90,6 +90,11 @@ class Voice:
self._('Zzzzz'),
self._('ZzzZzzz ({secs}s)').format(secs=secs)])
def on_shutdown(self):
return random.choice([
self._('Good night.'),
self._('Zzz')])
def on_awakening(self):
return random.choice(['...', '!'])

View File

@ -1,13 +1,15 @@
Crypto
requests
pyyaml
scapy
gym
stable-baselines
tensorflow
tweepy
file_read_backwards
numpy
inky
smbus
pillow
crypto==1.4.1
requests==2.21.0
PyYAML==5.1
scapy==2.4.3
gym==0.14.0
stable-baselines==2.7.0
tensorflow==1.13.1
tensorflow-estimator==1.14.0
tweepy==3.6.0
file-read-backwards==2.0.0
numpy==1.17.2
inky==0.0.5
smbus2==0.3.0
Pillow==5.4.1
spidev==3.4

View File

@ -94,9 +94,8 @@ function provide_raspbian() {
function setup_raspbian(){
# Detect the ability to create sparse files
if [ "${OPT_SPARSE}" -eq 0 ]; then
if [ which bmaptool -eq 0 ]; then
if ! type "bmaptool" >/dev/null 2>&1; then
echo "[!] bmaptool not available, not creating a sparse image"
else
echo "[+] Defaulting to sparse image generation as bmaptool is available"
OPT_SPARSE=1

View File

@ -1,99 +1,99 @@
#!/usr/bin/env python3
import sys
import os
import time
import argparse
from http.server import HTTPServer
import shutil
import logging
import yaml
sys.path.insert(0,
os.path.join(os.path.dirname(os.path.realpath(__file__)),
'../sdcard/rootfs/root/pwnagotchi/scripts/'))
'../'))
import pwnagotchi.ui.faces as faces
from pwnagotchi.ui.display import Display, VideoHandler
from PIL import Image
class CustomDisplay(Display):
def __init__(self, config, state):
self.last_image = None
super(CustomDisplay, self).__init__(config, state)
def _http_serve(self):
if self._video_address is not None:
self._httpd = HTTPServer((self._video_address, self._video_port),
CustomVideoHandler)
logging.info("ui available at http://%s:%d/" % (self._video_address,
self._video_port))
self._httpd.serve_forever()
else:
logging.info("could not get ip of usb0, video server not starting")
# do nothing
pass
def _on_view_rendered(self, img):
CustomVideoHandler.render(img)
self.last_image = img
if self._enabled:
self.canvas = (img if self._rotation == 0 else img.rotate(self._rotation))
if self._render_cb is not None:
self._render_cb()
class CustomVideoHandler(VideoHandler):
@staticmethod
def render(img):
with CustomVideoHandler._lock:
try:
img.save("/tmp/pwnagotchi-{rand}.png".format(rand=id(CustomVideoHandler)), format='PNG')
except BaseException:
logging.exception("could not write preview")
def do_GET(self):
if self.path == '/':
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
try:
self.wfile.write(
bytes(
self._index %
('localhost', 1000), "utf8"))
except BaseException:
pass
elif self.path.startswith('/ui'):
with self._lock:
self.send_response(200)
self.send_header('Content-type', 'image/png')
self.end_headers()
try:
with open("/tmp/pwnagotchi-{rand}.png".format(rand=id(CustomVideoHandler)), 'rb') as fp:
shutil.copyfileobj(fp, self.wfile)
except BaseException:
logging.exception("could not open preview")
else:
self.send_response(404)
def get_image(self):
"""
Return the saved image
"""
return self.last_image
class DummyPeer:
def __init__(self):
self.rssi = -50
@staticmethod
def name():
return "beta"
@staticmethod
def pwnd_run():
return 50
@staticmethod
def pwnd_total():
return 100
@staticmethod
def face():
return faces.FRIEND
def append_images(images, horizontal=True, xmargin=0, ymargin=0):
w, h = zip(*(i.size for i in images))
if horizontal:
t_w = sum(w)
t_h = max(h)
else:
t_w = max(w)
t_h = sum(h)
result = Image.new('RGB', (t_w, t_h))
x_offset = 0
y_offset = 0
for im in images:
result.paste(im, (x_offset, y_offset))
if horizontal:
x_offset += im.size[0] + xmargin
else:
y_offset += im.size[1] + ymargin
return result
def main():
parser = argparse.ArgumentParser(description="This program emulates\
the pwnagotchi display")
parser.add_argument('--display', help="Which display to use.",
parser.add_argument('--displays', help="Which displays to use.", nargs="+",
default="waveshare_2")
parser.add_argument('--port', help="Which port to use",
default=8080)
parser.add_argument('--sleep', type=int, help="Time between emotions",
default=2)
parser.add_argument('--lang', help="Language to use",
default="en")
parser.add_argument('--output', help="Path to output image (PNG)", default="preview.png")
parser.add_argument('--show-peer', dest="showpeer", help="This options will show a dummy peer", action="store_true")
parser.add_argument('--xmargin', help="Add X-Margin", type=int, default=5)
parser.add_argument('--ymargin', help="Add Y-Margin", type=int, default=5)
args = parser.parse_args()
CONFIG = yaml.load('''
config_template = '''
main:
lang: {lang}
ui:
@ -107,64 +107,80 @@ def main():
video:
enabled: true
address: "0.0.0.0"
port: {port}
'''.format(display=args.display,
port=args.port,
lang=args.lang))
port: 8080
'''
DISPLAY = CustomDisplay(config=CONFIG, state={'name': '%s>' % 'preview'})
list_of_displays = list()
for display_type in args.displays:
config = yaml.safe_load(config_template.format(display=display_type,
lang=args.lang))
display = CustomDisplay(config=config, state={'name': f"{display_type}>"})
list_of_displays.append(display)
while True:
DISPLAY.on_starting()
DISPLAY.update()
time.sleep(args.sleep)
DISPLAY.on_ai_ready()
DISPLAY.update()
time.sleep(args.sleep)
DISPLAY.on_normal()
DISPLAY.update()
time.sleep(args.sleep)
DISPLAY.on_new_peer(DummyPeer())
DISPLAY.update()
time.sleep(args.sleep)
DISPLAY.on_lost_peer(DummyPeer())
DISPLAY.update()
time.sleep(args.sleep)
DISPLAY.on_free_channel('6')
DISPLAY.update()
time.sleep(args.sleep)
DISPLAY.wait(args.sleep)
DISPLAY.update()
DISPLAY.on_bored()
DISPLAY.update()
time.sleep(args.sleep)
DISPLAY.on_sad()
DISPLAY.update()
time.sleep(args.sleep)
DISPLAY.on_motivated(1)
DISPLAY.update()
time.sleep(args.sleep)
DISPLAY.on_demotivated(-1)
DISPLAY.update()
time.sleep(args.sleep)
DISPLAY.on_excited()
DISPLAY.update()
time.sleep(args.sleep)
DISPLAY.on_deauth({'mac': 'DE:AD:BE:EF:CA:FE'})
DISPLAY.update()
time.sleep(args.sleep)
DISPLAY.on_miss('test')
DISPLAY.update()
time.sleep(args.sleep)
DISPLAY.on_lonely()
DISPLAY.update()
time.sleep(args.sleep)
DISPLAY.on_handshakes(1)
DISPLAY.update()
time.sleep(args.sleep)
DISPLAY.on_rebooting()
DISPLAY.update()
time.sleep(args.sleep)
columns = list()
for display in list_of_displays:
emotions = list()
if args.showpeer:
display.set_closest_peer(DummyPeer(), 10)
display.on_starting()
display.update()
emotions.append(display.get_image())
display.on_ai_ready()
display.update()
emotions.append(display.get_image())
display.on_normal()
display.update()
emotions.append(display.get_image())
display.on_new_peer(DummyPeer())
display.update()
emotions.append(display.get_image())
display.on_lost_peer(DummyPeer())
display.update()
emotions.append(display.get_image())
display.on_free_channel('6')
display.update()
emotions.append(display.get_image())
display.wait(2)
display.update()
emotions.append(display.get_image())
display.on_bored()
display.update()
emotions.append(display.get_image())
display.on_sad()
display.update()
emotions.append(display.get_image())
display.on_motivated(1)
display.update()
emotions.append(display.get_image())
display.on_demotivated(-1)
display.update()
emotions.append(display.get_image())
display.on_excited()
display.update()
emotions.append(display.get_image())
display.on_deauth({'mac': 'DE:AD:BE:EF:CA:FE'})
display.update()
emotions.append(display.get_image())
display.on_miss('test')
display.update()
emotions.append(display.get_image())
display.on_lonely()
display.update()
emotions.append(display.get_image())
display.on_handshakes(1)
display.update()
emotions.append(display.get_image())
display.on_rebooting()
display.update()
emotions.append(display.get_image())
# append them all together (vertical)
columns.append(append_images(emotions, horizontal=False, xmargin=args.xmargin, ymargin=args.ymargin))
# append columns side by side
final_image = append_images(columns, horizontal=True, xmargin=args.xmargin, ymargin=args.ymargin)
final_image.save(args.output, 'PNG')
if __name__ == '__main__':

6
scripts/pypi_upload.sh Executable file
View File

@ -0,0 +1,6 @@
#!/bin/bash
rm -rf build dist pwnagotchi.egg-info &&
python3 setup.py sdist bdist_wheel &&
clear &&
twine upload dist/*

View File

@ -1,4 +1,5 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from setuptools import setup, find_packages
import pwnagotchi
@ -18,10 +19,12 @@ setup(name='pwnagotchi',
license='GPL',
install_requires=required,
scripts=['bin/pwnagotchi'],
package_data={'pwnagotchi': ('pwnagotchi/defaults.yml',)},
package_data={'pwnagotchi': ['defaults.yml', 'pwnagotchi/defaults.yml']},
include_package_data=True,
packages=find_packages(),
classifiers=[
'Programming Language :: Python :: 3',
'Development Status :: 5 - Production/Stable',
'License :: OSI Approved :: GNU General Public License (GPL)',
'Environment :: Console',
])
])