├── .gitignore ├── LICENSE ├── README.md ├── requirements.txt ├── twinhunter.py └── twinhunter ├── __init__.py ├── accesspoint.py ├── deauther.py └── scanner.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | .DS_Store 106 | 107 | # vscode 108 | .vscode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Josh 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 | # TwinHunter 2 | 3 | A proof of concept project to detect an stop Evil Twin Attacks. 4 | 5 | ## What It Does 6 | 7 | Scans the wireless airspace and looks for wireless access points that may be imitating a legitmate one (this is called an evil twin attack). If an Evil Twin is discovered then it will be instantly flooded with deauth frames preventing any clients from connecting to it. 8 | 9 | ## Known Issues 10 | 11 | - No channel hopping 12 | - Deauthing never stops even if the access point goes away 13 | - Threads opened by the `Deauther` are not stopped properly 14 | - Observers of the `Scanner` object are not stopped properly 15 | 16 | ## Goals 17 | 18 | - Fix the known issues 19 | - Allow white listing of multiple wifi access points 20 | - Allow the use of multiple wifi interfaces to limit the amount of channel hopping a single interface needs to do 21 | - Be able to detect and respond to other wifi events not just beacons 22 | 23 | ## Setup 24 | 25 | This projects requires Python 3.5 or greater. 26 | 27 | - Clone the repo: 28 | 29 | ```bash 30 | git clone https://github.com/frozenjava/TwinHunter.git 31 | ``` 32 | 33 | - Install the requirements 34 | 35 | ```bash 36 | cd TwinHunter 37 | pip install -r requirements.txt 38 | ``` 39 | 40 | - Identify your wireless card with `ifconfig`. Assuming `wlan0` for this README. 41 | 42 | The remaining steps will need to be run with root privileges. 43 | 44 | - Put your wireless interface into monitor mode 45 | 46 | ```bash 47 | airmon-ng start wlan0 48 | ``` 49 | 50 | - Run the script using the WiFi name (ESSID) and a mac address serving that WiFi (BSSID) 51 | 52 | ```bash 53 | chmod +x twinhunter.py 54 | ./twinhunter.py --iface wlan0mon --essid home --bssid 00:11:22:33:44:55 55 | ``` 56 | 57 | Once it successfully discovers the net work you will see a log similar to 58 | 59 | ```bash 60 | Discorvered Accesspoint => BSSID: 00:11:22:33:44:55 | ESSID: home 61 | ``` 62 | 63 | The process will continue to scan for evil twins. If one is discovered, additional logs will appear 64 | 65 | ```bash 66 | FOUND EVIL TWIN!!! BSSID: 00:11:22:33:44:55 | ESSID: home 67 | ``` 68 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | scapy==2.4.3 2 | -------------------------------------------------------------------------------- /twinhunter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | 5 | from twinhunter.scanner import Scanner 6 | from twinhunter.deauther import Deauther 7 | from twinhunter.accesspoint import AccessPoint 8 | 9 | 10 | def logger(ap): 11 | print("Discorvered Accesspoint => BSSID: {0!s} | ESSID: {1!s}".format(ap.bssid, ap.essid)) 12 | 13 | 14 | if __name__ == "__main__": 15 | parser = argparse.ArgumentParser(description="Detect and stop Evil Twin attacks.") 16 | parser.add_argument("--iface", help="The monitor interface to watch on (ex: wlan0mon).", required=True) 17 | parser.add_argument("--essid", help="The SSID of the access point to protect.", required=True) 18 | parser.add_argument("--bssid", help="The BSSID of the legitimate access point to protect.", required=True) 19 | args = parser.parse_args() 20 | 21 | # The legitimate access point to trust 22 | trusted_ap = AccessPoint(bssid=args.bssid, essid=args.essid) 23 | 24 | # Create the deauth service 25 | deauther = Deauther(args.iface, trusted_ap) 26 | 27 | # Create the network scanner 28 | scanr = Scanner(args.iface) 29 | 30 | # Subscribe the logger to AP discovery events 31 | scanr.subscribe(logger) 32 | 33 | # Subscribe the deauther to AP discovery events 34 | scanr.subscribe(deauther.check_threat) 35 | 36 | # Start scanning for networks 37 | scanr.scan() 38 | -------------------------------------------------------------------------------- /twinhunter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frozenjava/TwinHunter/49bf0f315d47b574d0ed2afe4df7c286256bd748/twinhunter/__init__.py -------------------------------------------------------------------------------- /twinhunter/accesspoint.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass 5 | class AccessPoint: 6 | bssid: str 7 | essid: str 8 | -------------------------------------------------------------------------------- /twinhunter/deauther.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | import typing 3 | 4 | import scapy.all as scapy 5 | from twinhunter.accesspoint import AccessPoint 6 | 7 | 8 | class Deauther(object): 9 | def __init__(self, interface: str, trusted_ap: AccessPoint): 10 | self._interface = interface 11 | self._trusted_ap = trusted_ap 12 | self._deauth_threads: typing.List[Thread] = list() 13 | 14 | def check_threat(self, ap: AccessPoint): 15 | """ 16 | Compare ESSIDs and BSSIDs of access point `ap` with access point `_trusted_ap`. 17 | If the ESSIDS match and the BSSIDS do not match then assume its an evil twin and start sending deauth packets. 18 | """ 19 | if ap.essid == self._trusted_ap.essid and ap.bssid.lower() != self._trusted_ap.bssid.lower(): 20 | print("FOUND EVIL TWIN!!! BSSID: {0!s} | ESSID: {1!s}".format(ap.bssid, ap.essid)) 21 | thread = Thread(target=self.deuath, args=(ap,)) 22 | self._deauth_threads.append(thread) 23 | thread.start() 24 | 25 | def deuath(self, ap: AccessPoint): 26 | """ 27 | Sends deauth packets to a given access point. 28 | """ 29 | pkt = ( 30 | scapy.RadioTap() 31 | / scapy.Dot11(addr1="FF:FF:FF:FF:FF:FF", addr2=ap.bssid, addr3=ap.bssid) 32 | / scapy.Dot11Deauth(reason=7) 33 | ) 34 | while True: 35 | scapy.sendp(pkt, iface=self._interface) 36 | -------------------------------------------------------------------------------- /twinhunter/scanner.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | import scapy.all as scapy 3 | from twinhunter.accesspoint import AccessPoint 4 | 5 | 6 | class Scanner(object): 7 | def __init__(self, interface: str): 8 | self._interface: str = interface 9 | self._ap_list: List[AccessPoint] = list() 10 | self._discovery_callbacks: list = list() 11 | 12 | def _packet_handler(self, pkt): 13 | """ 14 | Handle packets picked up by scapy and determine if they are actual 802.11 beacons. 15 | If they are, check if its a new Access Point we haven't seen and add it to the list of known APs. 16 | Also, when a new AP is discovered any observers will be called and passed the new AccessPoint object. 17 | """ 18 | if pkt.haslayer(scapy.Dot11Elt) and pkt.type == 0 and pkt.subtype == 8: 19 | ap = AccessPoint(bssid=pkt.addr2, essid=pkt.info.decode("utf-8")) 20 | if ap not in self._ap_list: 21 | self._ap_list.append(ap) 22 | 23 | # Call all observers 24 | for callback in self._discovery_callbacks: 25 | callback(ap) 26 | 27 | def subscribe(self, callback): 28 | """ 29 | Subscribe to the access point discovery events. 30 | Callbacks must take 1 argument of type AccessPoint 31 | """ 32 | self._discovery_callbacks.append(callback) 33 | 34 | def scan(self): 35 | """ 36 | Start scanning for wireless networks. 37 | """ 38 | scapy.sniff(iface=self._interface, prn=self._packet_handler, store=0) 39 | --------------------------------------------------------------------------------