├── .gitignore ├── README.md ├── configs.py ├── docs └── img │ ├── sentrygun-alert.png │ ├── sentrygun-blank.png │ ├── sentrygun-grid.png │ └── sentrygun-new.png ├── network_tools.py ├── pip.req ├── sentrygun.py ├── sg-calibrator.py ├── sniffer.py └── whitelist.txt /.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 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sentrygun 2 | 3 | This is the github repo for the sentrygun sensor component. For sentrygun-server, please see https://github.com/s0lst1c3/sentrygun-server 4 | 5 | Sentrygun is an open source toolkit for detecting and responding to evil twin and karma attacks. It is capable of identifying evil twin attacks using whitelisting and listening for anomalies in signal strength. It is capable of detecting karma attacks by deliberately sending out probe requests for randomized ESSIDs then comparing the responses. 6 | 7 | A sentrygun installation consists of an array of sensors arranged in a grid that communicate with a command and control server. The sensor units analyze wireless traffic to detect nearby evil twin and karma attacks, and report results back to the server. When an evil twin or karma attack is detected, an alert is displayed in sentrygun's web frontend. Network administrators can then take steps to locate the attack, or use sentrygun to launch counterattacks against the offending rogue AP. 8 | 9 | Want to contribute to sentrygun? Make a pull request, or contact labs@gdssecurity.com. 10 | 11 | # Key Features 12 | 13 | - Capable of detecting karma attacks based on probe request/response patterns 14 | - Capable of detecting evil twin attacks through the use of whitelist crossreferencing 15 | - Capable of detecting evil twin attacks by identifying anomalies in signal strength 16 | - Assists network administrators in determining physical location of rogue AP attack 17 | - Capable of launching counterattacks against rogue access points 18 | 19 | # Upcoming Features 20 | 21 | - machine learning based approach to evil twin detection 22 | - identification of behaviors typically used by rogue APs to evade detection 23 | - identification of rogue APs through the use of WiFi canaries 24 | 25 | # Full Setup 26 | 27 | These are the full installation instructions for a rogue AP detection system using sentrygun and sentrygun-server. For instructions on how to use sentrygun after completing steps 1 through 5, please see the "Usage" section below. 28 | 29 | ## Step 1 - sentrygun sensor setup 30 | 31 | A sentrygun sensor can be built using any device that meets the following requirements: 32 | 33 | - supports a modern 64 bit Linux operating system 34 | - has an ethernet adapter 35 | - can power an external wireless adapter such as the TP-Link TL-WN722N. 36 | 37 | A good candidate for building a sentrygun sensor is the Raspberry Pi microcomputer. 38 | 39 | Additionally, each sentrygun sensor requires an external dual band wireless adapter capable 40 | of packet injection and monitor mode. 41 | 42 | To build a sentrygun sensor unit: 43 | 44 | 1. install linux operating system on device 45 | 2. download sentrygun using the following command 46 | 47 | git clone https://github.com/s0lst1c3/sentrygun.git 48 | 49 | 3. in the newly created sentrygun directory, install python dependencies using pip 50 | 51 | pip install -r pip.req 52 | 53 | 4. connect external wireless adapter to the device 54 | 55 | ## Step 2 - sentrygun-server setup 56 | 57 | The machine running sentrygun's server component can be anything from a laptop to a rackmount machine. Any machine can be used so long as it meets the following requirements: 58 | 59 | - is provisioned with a modern 64 bit Linux operating system 60 | - is capable of running sentrygun's software dependencies 61 | - is capable of connecting to an ethernet network 62 | 63 | To setup sentrygun-server on your machine of choice, first install the following software dependencies: 64 | 65 | dnf install gcc redhat-rpm-config python-devel autossh redis 66 | 67 | Then install the python dependencies enumerated in the pip.req file included with the project: 68 | 69 | pip install -r pip.req 70 | 71 | ## Step 3 - network setup 72 | 73 | sentrygun sensors should be arranged in a grid across the area that they are responsible for protecting. For example, to add rogue AP protection to a warehouse: 74 | 75 | ![alt tag](https://raw.githubusercontent.com/s0lst1c3/sentrygun/docs/docs/img/sentrygun-grid.png) 76 | 77 | The sensors should be connected to the machine running sentrygun-server over a phsyical network connection. Preferably, this connection should occur over an ethernet connection only accessible to network administrators (i.e. management network). 78 | 79 | ## Step 4 - Calibrate sentrygun sensors 80 | 81 | sentrygun sensor devices must be calibrated against your wireless network if evil twin detection is to be enabled. To calibrate the clients, populate the whitelist.txt file on each of your sensor devices with the bssid and essid of each access point on your network. The access points should be listed in whitelist.txt using the following format. 82 | 83 | # essid bssid 84 | gdslabs ff:ff:ff:aa:aa:aa 85 | gdslabs 00:11:22:33:44:00 86 | gdslabs 00:11:22:33:44:55 87 | 88 | # and so on and so forth 89 | 90 | Next, run sentrygun's calibration routine using the provided sg-calibrator.py script. The syntax for using sg-calibrator.py is as follows: 91 | 92 | python sg-calibrator.py -i IFACE 93 | 94 | Substitute IFACE with the name of the device's external wireless interface. The specified wireless interface should be in monitor mode. 95 | 96 | The sg-calibrator.py script will collect packets from your wireless access points over a period of time. It will then calculate the mean tx value across all packets collected. Finally, sg-calibrator.py will set a low and high bound equal to plus or minus N times the maximum deviation from the mean tx, where N is the value specified in configs.py. 97 | 98 | It is imperative that sg-calibrator.py is run in a physically secure environment to prevent statistical poisoning attacks. Sentrygun's evil twin detection relies on detecting tx values that fall outside of an expected threshold. An attacker could render this functionality useless by broadcasting their own tx values during the calibration phase. 99 | 100 | Once all devices have been calibrated, we can proceed to step 4 to initialize the system. 101 | 102 | ## Step 5 - Run System 103 | 104 | To run the sentrygun system, first start the sentrygun-server instance by issuing the following command on the CnC machine: 105 | 106 | python run.py 107 | 108 | sentrygun-server accepts the following command line arguments. 109 | 110 | - --port - specifies the port on which sentrygun-server should listen (defaults to 80) 111 | - --host - specifies the address at which sentrygun-server should listen (defaults to 0.0.0.0 if --tunnels flag is not used. Defaults to 127.0.0.1 if --tunnels flag is used). 112 | - --debug - Run in debug mode (not recommended for production environments) 113 | - --expire - Sets the number of seconds that alerts should remain active before they are automatically dismissed. To disable alert expiration, set this to 0 (default). 114 | - --tunnels - Creates ssh tunnels from localhost:PORT on sentrygun-server to localhost:PORT on a list of sentrygun clients, where PORT is the port at which sentrygun-server listens on. When this flag is used, sentrygun-server will always listen on localhost regardless of whether the --host is used. Use this option when running sentrygun on a hostle network (you should assume that you are). sentrysun clients should be specified with the format user@host:port. 115 | 116 | Once the server is running, start up each of the sentrygun sensor instances using the following syntax: 117 | 118 | python sentrygun.py 119 | 120 | sentrygun.py accepts the following command line arguments: 121 | 122 | - -a SERVER_ADDRESS - connect to sentrygun-server CnC at address SERVER_ADDRESS 123 | - -p SERVER_PORT - connect to sentrygun-server CnC on port SERVER_PORT 124 | - --evil-twin - enable evil-twin detection 125 | - --karma - enable karma attack detection 126 | - -i IFACE - substitute IFACE with name of external wireless interface (should be in monitor mode) 127 | 128 | For example, to enable evil-twin and karma detection with server located at example.com:4444 use the following command: 129 | 130 | python sentrygun.py -i wlan1 -p 4444 -a example.com --evil-twin --karma 131 | 132 | In the above example, the wireless interface is named wlan1. 133 | 134 | # Usage Instructions 135 | 136 | Once the system is up and running, navigate to the address and port at which the sentrygun-server instance is running. For example, if we started sentrygun-server at 192.168.1.39:4444 in step 4 of the setup instructions, we would navigate to the following address in our browser: 137 | 138 | http://192.168.1.39:4444 139 | 140 | You should be presented with a blank screen labeled "SentryGun Dashboard". There will also be an expandable toolbar on the left with links to features that have not been implemented yet. 141 | 142 | ![alt tag](https://raw.githubusercontent.com/s0lst1c3/sentrygun/docs/docs/img/sentrygun-blank.png) 143 | 144 | When a rogue access point attack is detected by one or more sentrygun sensor devices, an alert will appear instantly on the dashboard as shown below. 145 | 146 | ![alt tag](https://raw.githubusercontent.com/s0lst1c3/sentrygun/docs/docs/img/sentrygun-new.png) 147 | 148 | Each alert corresponds to an active Rogue AP attack against your network. Clicking an alert will reveal a list of all devices currently detecting the attack, as shown below. 149 | 150 | ![alt tag](https://raw.githubusercontent.com/s0lst1c3/sentrygun/docs/docs/img/sentrygun-alert.png) 151 | 152 | The list of alerting devices is sorted by distance and signal strength. This allows you to begin locating the source of the attack, as the rogue AP is closest to the sensor displayed at the top of the list. 153 | 154 | Located at the bottom of each alert list is a toolbar that can be used to dismiss alerts as well as launch counterattacks against the rogue access points. The available actions shown in the toolbar are as follows: 155 | 156 | - Locate - Displays a heat map to help locate the source of the attack (not implemented yet) 157 | - Deauth - Causes all sensor devices to flood the offending rogue AP with deauth packets 158 | - Napalm - Physically degrade offending rogue AP using flooding attack (not implemented yet) 159 | - Dismiss - Manually dismiss the alert and cease all counterattacks against offending rogue AP. 160 | 161 | To perform one of these actions: 162 | 163 | 1. click the action by name in the toolbar 164 | 2. click submit 165 | -------------------------------------------------------------------------------- /configs.py: -------------------------------------------------------------------------------- 1 | DEVICE_NAME = 'Conference Room' 2 | 3 | N_TIMES_MAX_DEV = 2 4 | 5 | CALIBRATION_TX_COUNT = 15 6 | 7 | ESSID_LEN = 24 8 | SERVER_ENDPOINT = 'alert/add' 9 | 10 | THRESHOLD = 2 11 | BURST_COUNT = 10 12 | THREAT_MITIGATION_ENABLED = True 13 | ALERTS_ENABLED = True 14 | 15 | # alert settings -------------------------------------------------------------- 16 | ALERT_RECIPIENTS = [] 17 | 18 | SMTP_SERVER = '' 19 | SMTP_PORT = 587 20 | SMTP_USER = '' 21 | SMTP_PASS = '' 22 | DEFAULT_SUBJECT = 'Sentrygun Alert' 23 | -------------------------------------------------------------------------------- /docs/img/sentrygun-alert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s0lst1c3/sentrygun/8b03e969a89909ea6a84f49271a60a5a72d9ade9/docs/img/sentrygun-alert.png -------------------------------------------------------------------------------- /docs/img/sentrygun-blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s0lst1c3/sentrygun/8b03e969a89909ea6a84f49271a60a5a72d9ade9/docs/img/sentrygun-blank.png -------------------------------------------------------------------------------- /docs/img/sentrygun-grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s0lst1c3/sentrygun/8b03e969a89909ea6a84f49271a60a5a72d9ade9/docs/img/sentrygun-grid.png -------------------------------------------------------------------------------- /docs/img/sentrygun-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s0lst1c3/sentrygun/8b03e969a89909ea6a84f49271a60a5a72d9ade9/docs/img/sentrygun-new.png -------------------------------------------------------------------------------- /network_tools.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from subprocess import check_output 4 | 5 | def traceroute(hostname): 6 | 7 | out = check_output(['traceroute', hostname]) 8 | count = 0 9 | for line in out.split('\n')[1:]: 10 | if line: 11 | count += 1 12 | return count 13 | 14 | def wifi_connect(iface, essid): 15 | 16 | os.system('ifconfig %s down' % iface) 17 | os.system('iwconfig %s essid %s' % (iface, essid)) 18 | os.system('ifconfig %s up' % iface) 19 | 20 | os.system('dhclient %s' % iface) 21 | -------------------------------------------------------------------------------- /pip.req: -------------------------------------------------------------------------------- 1 | requests >= 2.4.2 2 | -------------------------------------------------------------------------------- /sentrygun.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # sentrygun 3 | # Gabriel 'solstice' Ryan 4 | # gabriel@solstice.me 5 | # v0.0.1 6 | 7 | import random 8 | import requests 9 | import string 10 | import time 11 | import json 12 | import mmh3 13 | import os 14 | import sys 15 | import cPickle 16 | 17 | from multiprocessing import Queue, Process 18 | from collections import deque 19 | from configs import * 20 | from argparse import ArgumentParser 21 | from socketIO_client import SocketIO, BaseNamespace 22 | from network_tools import traceroute, wifi_connect 23 | 24 | shitlist = Queue() 25 | deauth_list = Queue() 26 | napalm_list = Queue() 27 | 28 | def alert_factory(location=None, 29 | bssid=None, 30 | channel=None, 31 | essid=None, 32 | tx=None, 33 | intent=None): 34 | 35 | # all arguments are required 36 | assert not any([ 37 | location is None, 38 | bssid is None, 39 | channel is None, 40 | essid is None, 41 | tx is None, 42 | intent is None, 43 | ]) 44 | 45 | # return dict from arguments 46 | _id = str(mmh3.hash(''.join([ bssid, str(channel), intent]))) 47 | 48 | return { 49 | 50 | 'id' : _id, 51 | 'location' : location, 52 | 'bssid' : bssid, 53 | 'channel' : channel, 54 | 'tx' : tx, 55 | 'essid' : essid, 56 | 'intent' : intent, 57 | 'timestamp' : time.time(), 58 | } 59 | 60 | def run_canary(iface, essid): 61 | 62 | wifi_connect(iface, essid) 63 | 64 | last_hops = traceroute('google.com') 65 | try: 66 | while True: 67 | 68 | time.sleep(5) 69 | hops = traceroute('google.com') 70 | if hops != last_hops: 71 | alert = alert_factory(location=DEVICE_NAME, 72 | bssid='not applicable', 73 | channel=0, 74 | intent='canary tripped!', 75 | essid=essid) 76 | shitlist.put(alert) 77 | 78 | except KeyboardInterrupt: 79 | 80 | os.system('ifconfig %s down' % iface) 81 | 82 | def rand_essid(): 83 | return ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in xrange(ESSID_LEN)) 84 | 85 | def deauth(bssid, client='ff:ff:ff:ff:ff:ff'): 86 | 87 | 88 | while True: 89 | print '[deauth worker] running deauth attack against', bssid 90 | time.sleep(1) 91 | 92 | #pckt = Dot11(addr1=client, addr2=bssid, addr3=bssid) / Dot11Deauth() 93 | #while True: 94 | 95 | # for i in range(64): 96 | # if i == 0: 97 | # print '[Sentrygun] Deauthing ', bssid 98 | # send(pckt) 99 | 100 | def napalm(bssid, client='ff:ff:ff:ff:ff:ff'): 101 | 102 | 103 | while True: 104 | print '[deauth] running napalm attack against', bssid 105 | time.sleep(1) 106 | 107 | # code to connect 100k clients goes here 108 | 109 | class PunisherNamespace(BaseNamespace): 110 | 111 | def on_napalm_target(self, *args): 112 | 113 | alert = args[0] 114 | 115 | napalm_list.put(alert) 116 | 117 | if 'dismiss' in alert: 118 | 119 | print '[punisher] ceasing any existing napalm attacks against', json.dumps(alert, indent=4, sort_keys=True) 120 | 121 | else: 122 | 123 | print '[punisher] initiating napalm attack against', json.dumps(alert, indent=4, sort_keys=True) 124 | 125 | def on_deauth_target(self, *args): 126 | 127 | alert = args[0] 128 | 129 | deauth_list.put(alert) 130 | 131 | if 'dismiss' in alert: 132 | 133 | print '[punisher] ceasing any existing deauth attack against', json.dumps(alert, indent=4, sort_keys=True) 134 | 135 | else: 136 | 137 | print '[punisher] initiating deauth attack against', json.dumps(alert, indent=4, sort_keys=True) 138 | 139 | def deauth_scheduler(): 140 | 141 | try: 142 | 143 | deauth_treatments = {} 144 | while True: 145 | 146 | alert = deauth_list.get() 147 | _id = alert['id'] 148 | 149 | if 'dismiss' in alert: 150 | 151 | if _id not in deauth_treatments: 152 | continue 153 | deauth_treatments[_id].terminate() 154 | del deauth_treatments[_id] 155 | 156 | elif _id in deauth_treatments: 157 | continue 158 | 159 | else: 160 | bssid = alert['bssid'] 161 | 162 | print '[deauth_scheduler] received new target:', bssid 163 | 164 | deauth_treatments[_id] = Process(target=deauth, args=(bssid,)) 165 | deauth_treatments[_id].daemon = True 166 | deauth_treatments[_id].start() 167 | 168 | except KeyboardInterrupt: 169 | pass 170 | 171 | def napalm_scheduler(): 172 | 173 | try: 174 | 175 | 176 | napalm_treatments = {} 177 | while True: 178 | 179 | alert = napalm_list.get() 180 | _id = alert['id'] 181 | 182 | if 'dismiss' in alert: 183 | 184 | if _id not in napalm_treatments: 185 | continue 186 | napalm_treatments[_id].terminate() 187 | del napalm_treatments[_id] 188 | 189 | elif _id in napalm_treatments: 190 | continue 191 | 192 | else: 193 | bssid = alert['bssid'] 194 | 195 | print '[napalm_scheduler] received new target:', bssid 196 | 197 | napalm_treatments[_id] = Process(target=napalm, args=(bssid,)) 198 | napalm_treatments[_id].daemon = True 199 | napalm_treatments[_id].start() 200 | 201 | except KeyboardInterrupt: 202 | pass 203 | 204 | 205 | def punisher(configs): 206 | 207 | socket = SocketIO(configs['server_addr'], configs['server_port']) 208 | punisher_ns = socket.define(PunisherNamespace, '/punisher') 209 | 210 | try: 211 | socket.wait() 212 | 213 | except KeyboardInterrupt: 214 | 215 | pass 216 | 217 | def listener(configs): 218 | 219 | server_uri = 'http://%s:%d/%s' %\ 220 | (configs['server_addr'], configs['server_port'], SERVER_ENDPOINT) 221 | 222 | try: 223 | 224 | while True: 225 | 226 | alert = shitlist.get() 227 | 228 | print '[alert] %s attack: notifying server at %s' %\ 229 | (alert['intent'], server_uri) 230 | response = requests.post(server_uri, json=alert) 231 | print '[alert] server at %s acknowledged notification with status code %d' % (server_uri, response.status_code) 232 | 233 | except KeyboardInterrupt: 234 | pass 235 | 236 | def detect_rogue_ap_attacks(): 237 | 238 | import sniffer 239 | 240 | responding_aps = {} 241 | try: 242 | 243 | probe_responses = sniffer.response_sniffer(interface) 244 | 245 | for response in probe_responses: 246 | 247 | ssid = response['essid'] 248 | bssid = response['addr3'].lower() 249 | channel = response['channel'] 250 | tx = response['tx'] 251 | 252 | print '[probe Response]', ssid, bssid, response['tx'], channel 253 | 254 | if configs['evil_twin'] and ssid in whitelist: 255 | 256 | if bssid not in whitelist[ssid]: 257 | 258 | print '[anomaly] %s has ssid: %s but not in whitelist' % (bssid, ssid) 259 | 260 | alert = alert_factory(location=DEVICE_NAME, 261 | bssid=bssid, 262 | channel=response['channel'], 263 | intent='evil twin - whitelist', 264 | tx=response['tx'], 265 | essid=ssid) 266 | shitlist.put(alert) 267 | 268 | else: 269 | 270 | ap = calibration_table['ssids'][ssid]['bssids'][bssid] 271 | 272 | upper_bound = ap['upper_bound'] 273 | lower_bound = ap['lower_bound'] 274 | 275 | if tx > upper_bound or tx < lower_bound: 276 | 277 | print '[anomaly] Illegal tx varation: %s ' % bssid 278 | alert = alert_factory(location=DEVICE_NAME, 279 | bssid=bssid, 280 | channel=response['channel'], 281 | intent='evil twin - tx', 282 | tx=response['tx'], 283 | essid=ssid) 284 | 285 | shitlist.put(alert) 286 | 287 | elif configs['karma']: 288 | 289 | if bssid in responding_aps: 290 | 291 | responding_aps[bssid].add(ssid) 292 | 293 | else: 294 | 295 | responding_aps[bssid] = set([]) 296 | responding_aps[bssid].add(ssid) 297 | 298 | if len(responding_aps[bssid]) > 1: 299 | 300 | print '[anomaly] %s has sent probe responses for %d SSIDs' % (bssid, len(responding_aps[bssid])) 301 | 302 | alert = alert_factory(location=DEVICE_NAME, 303 | bssid=bssid, 304 | channel=response['channel'], 305 | intent='karma', 306 | tx=response['tx'], 307 | essid=ssid) 308 | shitlist.put(alert) 309 | 310 | except KeyboardInterrupt: 311 | pass 312 | 313 | def channel_hopper(): 314 | 315 | import sniffer 316 | while True: 317 | 318 | # channel hop from main process 319 | for channel in xrange(1, 14): 320 | 321 | if configs['karma']: 322 | 323 | for i in xrange(THRESHOLD): 324 | next_essid = rand_essid() 325 | sniffer.send_probe_requests(interface=interface, ssid=next_essid) 326 | 327 | print '[channel hopper] Switching to channel', channel 328 | os.system('iwconfig %s channel %d' % (configs['iface'], channel)) 329 | time.sleep(6) 330 | 331 | def set_configs(): 332 | 333 | parser = ArgumentParser() 334 | 335 | parser.add_argument('-i', 336 | dest='iface', 337 | required=True, 338 | type=str, 339 | help='Specify network interface to use') 340 | 341 | parser.add_argument('-a', 342 | dest='server_addr', 343 | required=True, 344 | type=str, 345 | help='Send data to server at this address') 346 | 347 | parser.add_argument('-p', 348 | dest='server_port', 349 | required=False, 350 | default=80, 351 | type=int, 352 | help='Send data to server listening on this port') 353 | 354 | parser.add_argument('--evil-twin', 355 | dest='evil_twin', 356 | action='store_true', 357 | help='detect evil twin attacks') 358 | 359 | parser.add_argument('--karma', 360 | dest='karma', 361 | action='store_true', 362 | help='detect karma attacks') 363 | 364 | parser.add_argument('--canary', 365 | dest='canary', 366 | type=str, 367 | default='', 368 | required=False, 369 | help='Use canary to detect network drops (must specify essid and dedicated interface in the form essid:interface )') 370 | 371 | 372 | return parser.parse_args().__dict__ 373 | 374 | 375 | 376 | if __name__ == '__main__': 377 | 378 | print ''' 379 | 380 | _______ _______ _ _________ _______ _______ _ 381 | ( ____ \( ____ \( ( /|\__ __/( ____ )|\ /|( ____ \|\ /|( ( /| 382 | | ( \/| ( \/| \ ( | ) ( | ( )|( \ / )| ( \/| ) ( || \ ( | 383 | | (_____ | (__ | \ | | | | | (____)| \ (_) / | | | | | || \ | | 384 | (_____ )| __) | (\ \) | | | | __) \ / | | ____ | | | || (\ \) | 385 | ) || ( | | \ | | | | (\ ( ) ( | | \_ )| | | || | \ | 386 | /\____) || (____/\| ) \ | | | | ) \ \__ | | | (___) || (___) || ) \ | 387 | \_______)(_______/|/ )_) )_( |/ \__/ \_/ (_______)(_______)|/ )_) 388 | 389 | 390 | Gabriel Ryan 391 | 392 | ''' 393 | 394 | configs = set_configs() 395 | interface = configs['iface'] 396 | 397 | if configs['evil_twin']: 398 | 399 | try: 400 | 401 | with open(r'calibration_table.pickle', 'rb') as fd: 402 | 403 | calibration_table = cPickle.load(fd) 404 | 405 | except IOError: 406 | 407 | print '[error] calibration_table.pickle not found' 408 | print '[error] please run sg-calibrator.py before running sentrygun with --evil-twin flag' 409 | sys.exit() 410 | 411 | try: 412 | 413 | with open(r'whitelist.pickle', 'rb') as fd: 414 | 415 | whitelist = cPickle.load(fd) 416 | 417 | except IOError: 418 | 419 | print '[error] whitelist.pickle not found' 420 | print '[error] please run sg-calibrator.py before running sentrygun with --evil-twin flag' 421 | sys.exit() 422 | 423 | 424 | daemons = [] 425 | try: 426 | 427 | daemons.append(Process(target=detect_rogue_ap_attacks, args=())) 428 | 429 | if configs['canary']: 430 | canary_configs = configs['canary'].split(':') 431 | canary_essid = canary_configs[0] 432 | canary_iface = canary_configs[1] 433 | daemons.append(Process(target=run_canary, args=(canary_iface, canary_essid,))) 434 | 435 | daemons.append(Process(target=listener, args=(configs,))) 436 | daemons.append(Process(target=punisher, args=(configs,))) 437 | daemons.append(Process(target=deauth_scheduler, args=())) 438 | daemons.append(Process(target=napalm_scheduler, args=())) 439 | daemons.append(Process(target=channel_hopper, args=())) 440 | 441 | for d in daemons: 442 | d.start() 443 | 444 | except KeyboardInterrupt: 445 | 446 | for d in running_daemons: 447 | 448 | d.terminate() 449 | d.join() 450 | -------------------------------------------------------------------------------- /sg-calibrator.py: -------------------------------------------------------------------------------- 1 | import sniffer 2 | import json 3 | import os 4 | import cPickle 5 | import time 6 | 7 | from argparse import ArgumentParser 8 | from multiprocessing import Process 9 | from configs import * 10 | 11 | def channel_hopper(): 12 | 13 | import sniffer 14 | while True: 15 | 16 | for channel in xrange(1, 14): 17 | 18 | print '[channel hopper] Switching to channel', channel 19 | os.system('iwconfig %s channel %d' % (configs['iface'], channel)) 20 | time.sleep(6) 21 | 22 | def set_configs(): 23 | 24 | parser = ArgumentParser() 25 | 26 | parser.add_argument('-i', 27 | dest='iface', 28 | required=True, 29 | type=str, 30 | help='Specify network interface to use') 31 | 32 | return parser.parse_args().__dict__ 33 | 34 | if __name__ == '__main__': 35 | 36 | configs = set_configs() 37 | interface = configs['iface'] 38 | 39 | whitelist = {} 40 | calibration_table = { 41 | 'len' : 0, 42 | 'calibrated_count' : 0, 43 | 'calibrated' : False, 44 | 'ssids' : {}, 45 | } 46 | 47 | hopper = Process(target=channel_hopper, args=()) 48 | hopper.daemon = True 49 | hopper.start() 50 | 51 | with open('whitelist.txt') as fd: 52 | 53 | for line in fd: 54 | 55 | line = line.split() 56 | ssid = line[0] 57 | bssid = line[1].lower() 58 | 59 | if ssid in whitelist: 60 | whitelist[ssid].add(bssid) 61 | calibration_table['ssids'][ssid]['bssids'][bssid] = { 62 | 'calibrated' : False, 63 | 'packets' : [], 64 | 'len' : 0, 65 | } 66 | calibration_table['ssids'][ssid]['len'] += 1 67 | 68 | else: 69 | whitelist[ssid] = set() 70 | whitelist[ssid].add(bssid) 71 | calibration_table['ssids'][ssid] = { 72 | 'bssids' : { 73 | bssid : { 74 | 'calibrated' : False, 75 | 'packets' : [], 76 | 'len' : 0, 77 | }, 78 | }, 79 | 'calibrated_count' : 0, 80 | 'calibrated' : False, 81 | 'len' : 1, 82 | } 83 | calibration_table['len'] += 1 84 | 85 | probe_responses = sniffer.response_sniffer(interface) 86 | 87 | print json.dumps(calibration_table, indent=4, sort_keys=True) 88 | 89 | for response in probe_responses: 90 | 91 | ssid = response['essid'] 92 | bssid = response['addr3'].lower() 93 | tx = response['tx'] 94 | 95 | print '[probe_response]', ssid, bssid, tx 96 | 97 | ''' so ugly ''' 98 | if ssid in calibration_table['ssids']: 99 | 100 | if bssid in calibration_table['ssids'][ssid]['bssids']: 101 | 102 | if not calibration_table['ssids'][ssid]['calibrated']: 103 | 104 | if not calibration_table['ssids'][ssid]['bssids'][bssid]['calibrated']: 105 | 106 | calibration_table['ssids'][ssid]['bssids'][bssid]['packets'].append(tx) 107 | calibration_table['ssids'][ssid]['bssids'][bssid]['len'] += 1 108 | 109 | if calibration_table['ssids'][ssid]['bssids'][bssid]['len'] >= CALIBRATION_TX_COUNT: 110 | 111 | calibration_table['ssids'][ssid]['bssids'][bssid]['calibrated'] = True 112 | 113 | calibration_table['ssids'][ssid]['calibrated_count'] += 1 114 | if calibration_table['ssids'][ssid]['calibrated_count'] >= calibration_table['ssids'][ssid]['len']: 115 | 116 | calibration_table['ssids'][ssid]['calibrated'] = True 117 | calibration_table['calibrated_count'] += 1 118 | 119 | if calibration_table['calibrated_count'] >= calibration_table['len']: 120 | 121 | calibration_table['calibrated'] = True 122 | break 123 | 124 | print json.dumps(calibration_table, indent=4, sort_keys=True) 125 | 126 | ssids = calibration_table['ssids'] 127 | for s in ssids: 128 | 129 | for bssid in ssids[s]['bssids']: 130 | 131 | tx_list = ssids[s]['bssids'][bssid]['packets'] 132 | tx_list_len = ssids[s]['bssids'][bssid]['len'] 133 | 134 | mean = sum(tx_list) / tx_list_len 135 | max_dev = max(abs(el - mean) for el in tx_list) 136 | 137 | upper_bound = mean + (max_dev * N_TIMES_MAX_DEV) 138 | lower_bound = mean - (max_dev * N_TIMES_MAX_DEV) 139 | 140 | ssids[s]['bssids'][bssid]['mean'] = mean 141 | ssids[s]['bssids'][bssid]['max_dev'] = max_dev 142 | ssids[s]['bssids'][bssid]['upper_bound'] = upper_bound 143 | ssids[s]['bssids'][bssid]['lower_bound'] = lower_bound 144 | 145 | with open(r'calibration_table.pickle', 'wb') as fd: 146 | 147 | cPickle.dump(calibration_table, fd) 148 | 149 | with open(r'whitelist.pickle', 'wb') as fd: 150 | 151 | cPickle.dump(whitelist, fd) 152 | 153 | print json.dumps(calibration_table, indent=4, sort_keys=True) 154 | 155 | hopper.terminate() 156 | -------------------------------------------------------------------------------- /sniffer.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | 4 | # set scapy loglevel to ERROR before importing scapy 5 | logging.getLogger("scapy.runtime").setLevel(logging.ERROR) 6 | from scapy.all import * 7 | 8 | from configs import BURST_COUNT 9 | 10 | PROBE_REQUEST = 0x4 11 | PROBE_RESPONSE = 0x5 12 | 13 | def is_valid_probe_response(packet): 14 | 15 | if not packet.haslayer(Dot11): 16 | return None 17 | 18 | if packet.subtype != PROBE_RESPONSE: 19 | return None 20 | 21 | #return extract_valid_probe_data(packet) 22 | return packet 23 | 24 | def extract_probe_data(packet): 25 | 26 | return { 27 | 28 | 'addr2' : packet.addr2, 29 | 'addr1' : packet.addr1, 30 | 'addr3' : packet.addr3, 31 | 'essid' : packet[Dot11Elt].info, 32 | 'channel' : int(ord(packet[Dot11Elt:3].info)), 33 | 'tx' : -(256-ord(packet.notdecoded[-6:-5])), 34 | 'len' : packet.len, 35 | 'timestamp' : time.time(), 36 | } 37 | 38 | def response_sniffer(interface): 39 | 40 | while True: 41 | pkt = sniff(lfilter=is_valid_probe_response, iface=interface, count=1, store=1, timeout=10) 42 | 43 | if len(pkt) > 0: 44 | try: 45 | yield extract_probe_data(pkt[0]) 46 | except IndexError: 47 | continue 48 | 49 | def ProbeReq(count=BURST_COUNT,ssid='',dst='ff:ff:ff:ff:ff:ff', interface=None): 50 | 51 | param = Dot11ProbeReq() 52 | essid = Dot11Elt(ID='SSID',info=ssid) 53 | rates = Dot11Elt(ID='Rates',info='\x03\x12\x96\x18\x24\x30\x48\x60') 54 | dsset = Dot11Elt(ID='DSset',info='\x01') 55 | pkt = RadioTap()\ 56 | /Dot11(type=0,subtype=4,addr1=dst,addr2='00:11:22:33:44:55',addr3='00:11:22:33:44:00')\ 57 | /param/essid/rates/dsset 58 | 59 | print '[karma honeypot] sending 802.11 probe request: SSID=[%s], count=%d' % (ssid,count) 60 | sendp(pkt,iface=interface,count=count,inter=0.1,verbose=0) 61 | 62 | def send_probe_requests(interface=None, ssid=None): 63 | 64 | ProbeReq(ssid=ssid, interface=interface) 65 | 66 | -------------------------------------------------------------------------------- /whitelist.txt: -------------------------------------------------------------------------------- 1 | starbucks ff:ff:ff:aa:aa:aa 2 | --------------------------------------------------------------------------------