├── .gitignore ├── LICENSE ├── README.MD ├── logging.cfg ├── nmea.db ├── nmeasnitch.cfg ├── nmeasnitch.py └── rate.sql /.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 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 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 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 ZX Security 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 | # nmeasnitch 2 | nmeasnitch is a tool to detect GPS spoofing by looking at the nmea sentences. This was presented at [Defcon 25 2017](https://www.defcon.org/html/defcon-25/dc-25-speakers.html#Robinson) 3 | 4 | ## Requirements 5 | 1. python3-serial 6 | 1. A GPS Device that will talk to [GPSd](http://www.catb.org/gpsd/) 7 | 1. GPSd installed 8 | 9 | ## Running 10 | 1. Configure options in nmeasnitch.cfg 11 | 1. Run nmeasnitch `./nmeasnitch.py` 12 | 1. Use rate.sql to see if the rates are changing. 13 | -------------------------------------------------------------------------------- /logging.cfg: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root 3 | 4 | [handlers] 5 | keys=consoleHandler, timedRotatingFileHandler 6 | 7 | [formatters] 8 | keys=simpleFormatter 9 | 10 | [logger_root] 11 | level=DEBUG 12 | handlers=consoleHandler, timedRotatingFileHandler 13 | 14 | [handler_timedRotatingFileHandler] 15 | class=handlers.TimedRotatingFileHandler 16 | level=DEBUG 17 | formatter=simpleFormatter 18 | args=('nmeasnitch.log', 'H', 1, 72) 19 | 20 | [handler_consoleHandler] 21 | class=StreamHandler 22 | level=WARNING 23 | formatter=simpleFormatter 24 | args=(sys.stdout,) 25 | 26 | [formatter_simpleFormatter] 27 | format=%(asctime)s - %(name)s - %(levelname)s - %(message)s 28 | datefmt= 29 | -------------------------------------------------------------------------------- /nmea.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zxsecurity/NMEAsnitch/ee25e6b1c275d9651c9de3b6ded1b63cabf68f34/nmea.db -------------------------------------------------------------------------------- /nmeasnitch.cfg: -------------------------------------------------------------------------------- 1 | [database] 2 | log_to_db = True 3 | db_filename = nmea.db 4 | 5 | [serial] 6 | serial_port = /dev/gps0 7 | -------------------------------------------------------------------------------- /nmeasnitch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding=utf-8 3 | 4 | """ 5 | nmeasnitch.py is tool that will try and detect if the GPS is being spoofed. 6 | 7 | command: 8 | nemasnitch.py 9 | 10 | The configuration of the checking is handled by nemasnitch.cfg and the logging configuarion by logging.cfg 11 | """ 12 | 13 | __author__ = 'Karit @nzkarit' 14 | __copyright__ = 'Copyright 2017 Karit' 15 | __license__ = 'MIT' 16 | __version__ = '0.1' 17 | 18 | import serial 19 | import sqlite3 20 | import configparser 21 | import logging 22 | import logging.config 23 | import datetime 24 | 25 | def read_serial(): 26 | while True: 27 | line = str(ser.readline()) 28 | ts = str(datetime.datetime.now()) 29 | tidied = line.strip("b'").strip("\\r\\n") 30 | 31 | if tidied[:1] == '$': 32 | if log_to_db: 33 | sentence_type = tidied.split(",")[0] 34 | c = conn.cursor() 35 | c.execute('INSERT INTO sentences (date, sentence, sentence_type) VALUES (?, ?, ?)', (ts, tidied, sentence_type)) 36 | conn.commit() 37 | c.close() 38 | logger.debug(tidied) 39 | 40 | def start_serial(): 41 | serial_port = cfg.get('serial' , 'serial_port') 42 | logger.debug('Starting Serial connection to %s'%(serial_port)) 43 | global ser 44 | ser = serial.Serial(serial_port, timeout=1) 45 | 46 | def close_serial(): 47 | logger.debug('Closing Serial connection') 48 | ser.close() 49 | 50 | def connect_to_db(): 51 | """ 52 | Connect to the sqlite database 53 | 54 | The configuartion for this is gpsnitch.cfg 55 | """ 56 | global log_to_db 57 | log_to_db = cfg.getboolean('database', 'log_to_db') 58 | if log_to_db: 59 | logger.debug('Connecting to DB') 60 | global conn 61 | filename = cfg.get('database' , 'db_filename') 62 | conn = sqlite3.connect(filename) 63 | logger.debug('Connected to DB') 64 | else: 65 | logger.debug('Not Logging to DB') 66 | 67 | def close_db(): 68 | if log_to_db: 69 | logger.debug('Closing DB') 70 | conn.close() 71 | else: 72 | logger.debug('Not Logging to DB, so nothing to close') 73 | 74 | def shut_down(): 75 | """ 76 | Closes connections 77 | """ 78 | close_db() 79 | close_serial() 80 | 81 | def start_script(): 82 | global cfg 83 | cfg = configparser.ConfigParser() 84 | cfg.read('nmeasnitch.cfg') 85 | 86 | global logger 87 | logging.config.fileConfig('logging.cfg') 88 | logger = logging.getLogger(__name__) 89 | logger.info('Starting nmeasnitch') 90 | 91 | start_serial() 92 | connect_to_db() 93 | 94 | try: 95 | read_serial() 96 | except KeyboardInterrupt: 97 | shut_down() 98 | except (OSError, IOError) as error: 99 | shut_down() 100 | sys.stderr.write('\rError--> {}'.format(error)) 101 | logger.error('Error--> {}'.format(error)) 102 | sys.exit(1) 103 | 104 | if __name__ == '__main__': 105 | start_script() 106 | -------------------------------------------------------------------------------- /rate.sql: -------------------------------------------------------------------------------- 1 | SELECT sentence_type, (number*1.0/time_period) as rate 2 | FROM 3 | (SELECT sentence_type, count(*) as number 4 | FROM sentences 5 | GROUP BY sentence_type), 6 | (SELECT MAX(strftime('%s',date))-MIN(strftime('%s',date)) as time_period 7 | FROM sentences) 8 | --------------------------------------------------------------------------------