├── README.md └── rolljam.py /README.md: -------------------------------------------------------------------------------- 1 | # "Rolljam" attack simulation 2 | 3 | This Python code simulates what is being called a "Rolljam" attack. In this attack, the attacker steals a rolling code and replays it to gain access to a locked vehicle. 4 | 5 | To prevent the Rolljam attack, I store a list of codes which have been "rolled" through, and do not trust them, as they could have been stolen by the attacker. 6 | 7 | # Info 8 | 9 | Here is a Wired article about the Rolljam attack: http://www.wired.com/2015/08/hackers-tiny-device-unlocks-cars-opens-garages/ 10 | 11 | # How-To 12 | 13 | To run the code: 14 | 15 | python rolljam.py 16 | 17 | And observe the output. -------------------------------------------------------------------------------- /rolljam.py: -------------------------------------------------------------------------------- 1 | # ********************************************************************************** 2 | # Wes Calvert, 2015 3 | # ********************************************************************************** 4 | # License 5 | # ********************************************************************************** 6 | # This program is free software; you can redistribute it 7 | # and/or modify it under the terms of the GNU General 8 | # Public License as published by the Free Software 9 | # Foundation; either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will 13 | # be useful, but WITHOUT ANY WARRANTY; without even the 14 | # implied warranty of MERCHANTABILITY or FITNESS FOR A 15 | # PARTICULAR PURPOSE. See the GNU General Public 16 | # License for more details. 17 | # 18 | # You should have received a copy of the GNU General 19 | # Public License along with this program; if not, write 20 | # to the Free Software Foundation, Inc., 21 | # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 22 | # 23 | # Licence can be viewed at 24 | # http:#www.fsf.org/licenses/gpl.txt 25 | # 26 | # Please maintain this license information along with authorship 27 | # and copyright notices in any redistribution of this code 28 | # ********************************************************************************** 29 | 30 | import socket 31 | from Crypto.Cipher import AES 32 | from Crypto import Random 33 | import thread 34 | import time 35 | from multiprocessing import Process 36 | from hashlib import sha1 37 | 38 | def OTP(salt, n=0, digits=8): 39 | while True: 40 | hash = sha1(str(salt) + repr(n)).hexdigest() 41 | yield hash[-digits:] 42 | n += 1 43 | 44 | class Server(object): 45 | 46 | key = "123456789ABCDEFG" #None 47 | mode = AES.MODE_CFB 48 | clients = [] 49 | 50 | def __init__(self, address, port): 51 | self.address = address 52 | self.port = port 53 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 54 | self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 55 | self.socket.bind((self.address, self.port)) 56 | self.socket.listen(500) 57 | 58 | def serve(self): 59 | while True: 60 | clientsock, address = self.socket.accept() 61 | thread.start_new_thread(self.handler, (clientsock, address)) 62 | 63 | def handler(self, clientsock, address): 64 | while True: 65 | temp = clientsock.recv(100) 66 | if self.key is not None: 67 | iv = temp[:16] 68 | data = temp[16:] 69 | try: 70 | decryptor = AES.new(self.key, self.mode, IV=iv) 71 | except ValueError: 72 | # After the tests are finished running, I'm getting "IV must be 16 bytes". 73 | # There should not be any more data received though, so I don't know what is going on. 74 | return 75 | self.data = decryptor.decrypt(data) 76 | else: 77 | self.data = temp 78 | 79 | # Remove extra spaces and break message into chunks. 80 | self.data = self.data.strip() 81 | received_uid = self.data[:7] 82 | received_code = self.data[7:-4] 83 | message = self.data[-4:] 84 | known_client = False 85 | for client in self.clients: 86 | if client.uid == received_uid: 87 | known_client = True 88 | if received_code == client.last_code: 89 | print "Code Replayed! Client: {0} with received code: {1}".format(client.uid, client.last_code) 90 | break 91 | if received_code in client.invalid_codes: 92 | print "Possible Rolljam! Rejecting message from client {0} with code {1}".format(client.uid, client.last_code) 93 | break 94 | loops = 0 95 | client.next_code = client.generator.next() 96 | while received_code != client.next_code: 97 | loops += 1 98 | client.invalid_codes.append(client.next_code) 99 | client.next_code = client.generator.next() 100 | client.last_code = client.next_code 101 | print "Code accepted after {0} retries. Messsage: '{1}' from client '{2}' with received code: {3}, generated code: {4}".format( 102 | loops, message, received_uid, received_code, client.last_code) 103 | if not known_client: 104 | print "Unknown UID detected: {0}".format(received_uid) 105 | 106 | def SetEncryptionKey(self, key): 107 | if len(key) != 16: 108 | raise Exception("Key must be exactly 16 bytes!") 109 | self.key = key 110 | 111 | class Client(object): 112 | 113 | key = "123456789ABCDEFG" #None 114 | mode = AES.MODE_CFB 115 | 116 | def __init__(self, address, port, uid, seed): 117 | self.address = address 118 | self.port = port 119 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 120 | self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 121 | self.socket.connect((self.address, self.port)) 122 | self.uid = uid 123 | self.seed = seed 124 | self.generator = OTP(salt=seed) 125 | self.last_code = None 126 | self.next_code = self.generator.next() 127 | # List of invalid codes is located in the client object for simulation purposes only. 128 | # In the real world, the server device would maintain this list for each client. 129 | self.invalid_codes = [] 130 | 131 | def send(self, buffer_in, transmit=True): 132 | buffer = "{}{}{}".format(self.uid, self.generator.next(), buffer_in) 133 | clear_text = buffer 134 | if self.key is not None: 135 | while (len(buffer)+16) % 16 != 0: 136 | buffer += " " 137 | iv = Random.new().read(16) 138 | encryptor = AES.new(self.key, self.mode, IV=iv) 139 | buffer = iv + encryptor.encrypt(buffer) 140 | if transmit: 141 | self.socket.sendall(buffer) 142 | return buffer, clear_text # return the encrypted and clear text data for use in tests 143 | 144 | # This is a helper method to run the server object in another process. 145 | def run_server(server): 146 | for i in range(0,10): 147 | uid = "device%d" % i 148 | c = Client("localhost",10000,uid,1234) 149 | server.clients.append(c) 150 | try: 151 | server.serve() 152 | except KeyboardInterrupt: 153 | print "Shutting down..." 154 | 155 | # Simulate a few messages which were not received by the server. 156 | # The server should increment the rolling code twice when it receives the last message. 157 | def basic_test(): 158 | print "Beginning basic test..." 159 | c = Client("localhost",10000,"device0","1234") 160 | c.send("LOCK") 161 | c.send("UNLK", transmit=False) 162 | c.send("UNLK", transmit=False) 163 | c.send("UNLK") 164 | time.sleep(1) 165 | print "Basic test finished.\n" 166 | 167 | # "Naive" replay attack - just send the same encrypted data again. 168 | def naive_replay(): 169 | print "Beginning naive replay..." 170 | victim = Client("localhost",10000,"device1","1234") 171 | attacker = Client("localhost",10000,None,None) 172 | encrypted, clear = victim.send("UNLK") 173 | attacker.socket.sendall(encrypted) 174 | time.sleep(1) 175 | print "Naive replay finished.\n\n" 176 | 177 | # Rolljam attack - steal key and attempt to use it later. 178 | def rolljam(): 179 | print "Beginning rolljam..." 180 | victim = Client("localhost",10000,"device2","1234") 181 | attacker = Client("localhost",10000,None,None) 182 | victim.send("LOCK") 183 | encrypted, clear = victim.send("UNLK", transmit=False) 184 | victim.send("UNLK") 185 | attacker.socket.sendall(encrypted) 186 | time.sleep(1) 187 | print "Rolljam finished.\n\n" 188 | 189 | def main(): 190 | s = Server("localhost",10000) 191 | p = Process(target=run_server, args=(s,)) 192 | p.start() 193 | print "Server started, waiting a bit..." 194 | time.sleep(1) 195 | basic_test() 196 | naive_replay() 197 | rolljam() 198 | try: 199 | p.join() 200 | except KeyboardInterrupt: 201 | pass 202 | 203 | if __name__ == "__main__": 204 | main() --------------------------------------------------------------------------------