├── .gitattributes ├── .gitignore ├── NMEAdesync.cfg ├── NMEAdesync.py ├── README.md └── logging.cfg /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.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 | local_settings.py 55 | 56 | # Flask instance folder 57 | instance/ 58 | 59 | # Scrapy stuff: 60 | .scrapy 61 | 62 | # Sphinx documentation 63 | docs/_build/ 64 | 65 | # PyBuilder 66 | target/ 67 | 68 | # IPython Notebook 69 | .ipynb_checkpoints 70 | 71 | # pyenv 72 | .python-version 73 | 74 | # celery beat schedule file 75 | celerybeat-schedule 76 | 77 | # dotenv 78 | .env 79 | 80 | # virtualenv 81 | venv/ 82 | ENV/ 83 | 84 | # Spyder project settings 85 | .spyderproject 86 | 87 | # Rope project settings 88 | .ropeproject 89 | 90 | # ========================= 91 | # Operating System Files 92 | # ========================= 93 | 94 | # OSX 95 | # ========================= 96 | 97 | .DS_Store 98 | .AppleDouble 99 | .LSOverride 100 | 101 | # Thumbnails 102 | ._* 103 | 104 | # Files that might appear in the root of a volume 105 | .DocumentRevisions-V100 106 | .fseventsd 107 | .Spotlight-V100 108 | .TemporaryItems 109 | .Trashes 110 | .VolumeIcon.icns 111 | 112 | # Directories potentially created on remote AFP share 113 | .AppleDB 114 | .AppleDesktop 115 | Network Trash Folder 116 | Temporary Items 117 | .apdisk 118 | 119 | # Windows 120 | # ========================= 121 | 122 | # Windows image file caches 123 | Thumbs.db 124 | ehthumbs.db 125 | 126 | # Folder config file 127 | Desktop.ini 128 | 129 | # Recycle Bin used on file shares 130 | $RECYCLE.BIN/ 131 | 132 | # Windows Installer files 133 | *.cab 134 | *.msi 135 | *.msm 136 | *.msp 137 | 138 | # Windows shortcuts 139 | *.lnk 140 | -------------------------------------------------------------------------------- /NMEAdesync.cfg: -------------------------------------------------------------------------------- 1 | [location] 2 | # The Location information for the transmission. Use decimal lat and long and metres for the altitude 3 | #deg and decminal minute 12 deg 34.56 min 4 | latitude = 1234.5678 5 | latitude_north_or_south = S 6 | #deg and decminal minute 123 deg 45.67 min 7 | longitude = 12345.6789 8 | longitude_west_or_east = E 9 | altitude = 123.4 10 | magnetic_variation = 012.3 11 | magnetic_variation_direction = E 12 | knots = 0.12 13 | true_heading = 123.45 14 | 15 | [time] 16 | #Whether to start from current time or set time 17 | start_with_current_time = False 18 | #The time to from YYYY-MM-DD HH:MM:SS 19 | start_time = 2017-02-19 04:15:00 20 | #For each iteration how much time to step by. Use a negative number to move time backwards 21 | step_time = 1 22 | #length of iteration. >0. How long the iteration should be. 0.5 means that time will move by step_time every 0.5seconds i.e. 2x normal. 2. measn that time will move step_time every 2 seconds i.e. half speed. 23 | iteration_time = 0.5 24 | 25 | 26 | [pps] 27 | #If should put out a PPS signal 28 | pps_enabled = True 29 | #What port the PPS output will be on 30 | pin = 25 31 | 32 | -------------------------------------------------------------------------------- /NMEAdesync.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding=utf-8 3 | 4 | 5 | """ 6 | NMEAdesync.py is a tool which through NMEA serial data will move time backwards on a NTPd server 7 | 8 | command: 9 | NMEAdesync.py 10 | 11 | To configure the running please edit NMEAdesync.cfg and the logging configuration in logging.cfg 12 | """ 13 | 14 | 15 | import sys 16 | import configparser 17 | import logging 18 | import logging.config 19 | from datetime import datetime, timedelta 20 | from time import sleep 21 | import threading 22 | #import RPi.GPIO as GPIO 23 | 24 | 25 | __author__ = 'Karit' 26 | __copyright__ = 'Copyright 2017 Karit' 27 | __license__ = 'MIT' 28 | __version__ = '0.1' 29 | 30 | def run_NMEAdesync(): 31 | 32 | if cfg.getboolean('pps', 'pps_enabled'): 33 | import RPi.GPIO as GPIO 34 | GPIO.setmode(GPIO.BCM) 35 | outputPin = cfg.getint('pps', 'pin') 36 | GPIO.setup(outputPin, GPIO.OUT) 37 | 38 | iterationTime = cfg.getfloat('time', 'iteration_time') 39 | tenthTterationTime = iterationTime/10 40 | 41 | if cfg.getboolean('time', 'start_with_current_time'): 42 | runningTime = datetime.now() 43 | else: 44 | runningTime = datetime.strptime(cfg.get('time', 'start_time'), '%Y-%m-%d %H:%M:%S') 45 | 46 | stepTime = cfg.getfloat('time', 'step_time') 47 | iterationTime = cfg.getfloat('time', 'iteration_time') 48 | 49 | while True: 50 | print(generate_gprmc_line(runningTime)) 51 | print(generate_gpgga_line(runningTime)) 52 | runningTime = runningTime + timedelta(seconds=stepTime) 53 | if cfg.getboolean('pps', 'pps_enabled'): 54 | GPIO.output(outputPin, GPIO.HIGH) 55 | sleep(tenthTterationTime) 56 | GPIO.output(outputPin, GPIO.LOW) 57 | sleep(tenthTterationTime*9) 58 | else: 59 | sleep(iterationTime) 60 | 61 | def generate_gpgga_line(lineTime): 62 | logger.debug('Running generate_gpgga_line with time: %s'%(lineTime)) 63 | 64 | messageType = 'GPGGA' 65 | time = lineTime.strftime('%H%M%S.000') 66 | latitude = cfg.get('location', 'latitude') 67 | northOrSouth = cfg.get('location', 'latitude_north_or_south') 68 | longitude = cfg.get('location', 'longitude') 69 | westOrEast = cfg.get('location', 'longitude_west_or_east') 70 | fixQuality = 1 71 | numberOfSatellites = 10 72 | hdop = 0.96 73 | altitude = cfg.getfloat('location', 'altitude') 74 | altitudeUnits = 'M' 75 | heightAboveWGS84 = cfg.getfloat('location', 'altitude') 76 | heightAboveWGS84Units = 'M' 77 | dgpsAge = '' 78 | dgpsStationID = '' 79 | 80 | stringToChecksum = '%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s' % (messageType,time, latitude, northOrSouth, longitude, westOrEast, fixQuality, numberOfSatellites, hdop, altitude, altitudeUnits, heightAboveWGS84, heightAboveWGS84Units, dgpsAge, dgpsStationID) 81 | checksum = nmea_checksum(stringToChecksum) 82 | 83 | nmeaOuput = '$%s*%s' % (stringToChecksum, checksum) 84 | 85 | logger.debug('Return from generate_gprmc_line with: %s'%(nmeaOuput)) 86 | return nmeaOuput 87 | 88 | def generate_gprmc_line(lineTime): 89 | logger.debug('Running generate_gprmc_line with time: %s'%(lineTime)) 90 | 91 | messageType = 'GPRMC' 92 | time = lineTime.strftime('%H%M%S.000') 93 | receiverWarning = 'A' 94 | latitude = cfg.get('location', 'latitude') 95 | northOrSouth = cfg.get('location', 'latitude_north_or_south') 96 | longitude = cfg.get('location', 'longitude') 97 | westOrEast = cfg.get('location', 'longitude_west_or_east') 98 | knots = cfg.get('location', 'knots') 99 | trueHeading = cfg.get('location', 'true_heading') 100 | date = lineTime.strftime('%d%m%y') 101 | magneticVariation = '' 102 | magneticVariationDirection = '' 103 | 104 | stringToChecksum = '%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,A' % (messageType,time, receiverWarning, latitude, northOrSouth, longitude, westOrEast, knots, trueHeading, date, magneticVariation, magneticVariationDirection) 105 | checksum = nmea_checksum(stringToChecksum) 106 | 107 | nmeaOuput = '$%s*%s' % (stringToChecksum, checksum) 108 | 109 | logger.debug('Return from generate_gprmc_line with: %s'%(nmeaOuput)) 110 | return nmeaOuput 111 | 112 | def nmea_checksum(stringToChecksum): 113 | """https://doschman.blogspot.co.nz/2013/01/calculating-nmea-sentence-checksums.html""" 114 | checksum = 0 115 | 116 | for c in stringToChecksum: 117 | checksum ^= ord(c) 118 | 119 | checksum = hex(checksum) 120 | checksum = checksum[2:] 121 | return (checksum) 122 | 123 | def shut_down(): 124 | """ 125 | Closes connections and threads 126 | """ 127 | logger.info('Keyboard interrupt received. Terminated by user. Good Bye.') 128 | sys.exit(1) 129 | 130 | def start_script(): 131 | global cfg 132 | cfg = configparser.ConfigParser() 133 | cfg.read('NMEAdesync.cfg') 134 | 135 | global logger 136 | logging.config.fileConfig('logging.cfg') 137 | logger = logging.getLogger(__name__) 138 | logger.info('Starting NMEAdesync') 139 | 140 | 141 | 142 | 143 | try: 144 | run_NMEAdesync() 145 | except KeyboardInterrupt: 146 | shut_down() 147 | except (OSError, IOError) as error: 148 | sys.stderr.write('\rError--> {}'.format(error)) 149 | logger.error('Error--> {}'.format(error)) 150 | sys.exit(1) 151 | 152 | if __name__ == '__main__': 153 | start_script() 154 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NMEAdesync 2 | NMEAdesync is a tool which will output NMEA sentences to stdout. Using [socat](http://www.dest-unreach.org/socat/) you can redirect this output to NTPd and move time. NMEAdesync will be first prensented during a conference talk at [BSidesCBR 2017](http://www.bsidesau.com.au/speakers.html#david). 3 | 4 | NMEAdesync will send NMEA senetences with a spoof time to NTPd and also a spoofed PPS 5 | 6 | ## Requirements 7 | NTPd using NMEA data over serial as the time, with PPS for accuarete timing. I set up a Pi using this [guide](https://frillip.com/raspberry-pi-stratum-1-ntp-server/). 8 | 9 | ## Running 10 | 1. Configure the options in NMEAdesync.cfg 11 | 1. Connect to the PPS wire to GPIO pint 25 12 | 1. sudo rm /dev/gps0 13 | 1. socat -d -d pty,raw,echo=0 "exec:/home/pi/NMEAdesync.py,pty,raw,echo=0" 14 | 1. Note the pts number as will need to use it in the next step 15 | 1. sudo ln -s /dev/pts/1 /dev/gps0 16 | 1. Notice the time has changed 17 | 1. Check pps sudo ppstest /dev/pps0 18 | -------------------------------------------------------------------------------- /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=('NMEAdesync.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 | --------------------------------------------------------------------------------