def tester_present(args): send_arb_id = args.src delay = args.delay suppress_positive_response = args.spr testerpresent_service_id = 0x3E if suppress_positive_response: sub_function = 0x80 else: sub_function = 0x00 message_data = [ 0x02, testerpresent_service_id, sub_function, 0x00, 0x00, 0x00, 0x00, 0x00 ] print("Sending TesterPresent to arbitration ID {0} (0x{0:02x})".format( send_arb_id)) print("\nPress Ctrl+C to stop\n") with CanActions(arb_id=send_arb_id) as can_wrap: counter = 1 while True: can_wrap.send(data=message_data) print("\rCounter:", counter, end="") stdout.flush() time.sleep(delay) counter += 1
def scan_arbitration_ids_to_blacklist(scan_duration): print("Scanning for arbitration IDs to blacklist (-autoblacklist)") ids_to_blacklist = set() def response_handler(msg): """ Blacklists the arbitration ID of a message if it could be misinterpreted as valid diagnostic response :param msg: can.Message instance to check """ if len(msg.data) > 1 and msg.data[1] in valid_responses: ids_to_blacklist.add(msg.arbitration_id) with CanActions() as can_actions: # Listen for matches can_actions.add_listener(response_handler) for i in range(scan_duration, 0, -1): print("\r{0:> 3} seconds left, {1} found".format( i - 1, len(ids_to_blacklist)), end="") stdout.flush() time.sleep(1) print("") can_actions.clear_listeners() # Add found matches to blacklist for arb_id in ids_to_blacklist: blacklist.append(arb_id)
def initiate_dump(handler, whitelist, separator_seconds, candump_format): """ Runs the 'handler' function on all incoming CAN messages. Filtering is controlled by the list 'args.whitelist' A separator is printed between messages if no messages have been handled in float 'args.separator_seconds' :param handler: function to call on all incoming messages :param whitelist: list of allowed arbitration IDs, or None to allow all :param separator_seconds: float seconds before printing a separator between messages, or None to never do this :param candump_format: bool indicating whether messages should be passed to 'handler' in candump str format """ if candump_format: format_func = msg_to_candump_format else: format_func = str separator_enabled = separator_seconds is not None last_message_timestamp = datetime.datetime.min messages_since_last_separator = 0 print("Dumping CAN traffic (press Ctrl+C to exit)".format(whitelist)) with CanActions(notifier_enabled=False) as can_wrap: for msg in can_wrap.bus: # Separator handling if separator_enabled and messages_since_last_separator > 0: if (datetime.datetime.now() - last_message_timestamp ).total_seconds() > separator_seconds: # Print separator handler( "--- Count: {0}".format(messages_since_last_separator)) messages_since_last_separator = 0 # Message handling if len(whitelist) == 0 or msg.arbitration_id in whitelist: handler(format_func(msg)) last_message_timestamp = datetime.datetime.now() messages_since_last_separator += 1
def start_listener(falling_sort): """ Counts messages per arbitration ID. Prints a list of IDs afterwards, sorted by number of hits. :param falling_sort: bool indicating whether results should be sorted in falling order """ found_arb_ids = Counter() try: # Listen for messages print("Running listener (press Ctrl+C to exit)") with CanActions(notifier_enabled=False) as can_wrap: for msg in can_wrap.bus: if msg.arbitration_id not in found_arb_ids: print( "\rLast ID: 0x{0:08x} ({1} unique arbitration IDs found)" .format(msg.arbitration_id, len(found_arb_ids) + 1), end=" ") stdout.flush() found_arb_ids[msg.arbitration_id] += 1 except KeyboardInterrupt: # Print results if len(found_arb_ids) > 0: print("\n\nDetected arbitration IDs:") for (arb_id, hits) in sorted(found_arb_ids.items(), key=lambda x: x[1], reverse=falling_sort): print("Arb id 0x{0:08x} {1} hits".format(arb_id, hits)) else: print("\nNo arbitration IDs were detected.")
def replay_fuzz(directives, show_requests, show_responses): """ Replay cansend directives from 'filename' :param directives: list of (int arb_id, list data) tuples :param show_requests: bool indicating whether requests should be printed to stdout :param show_responses: bool indicating whether responses should be printed to stdout """ # Define a callback function which will handle incoming messages def response_handler(msg): if msg.arbitration_id != arb_id or list(msg.data) != data: if not show_requests: # Print last sent request print("Sent: {0}".format(directive)) print(" Received: {0}".format( directive_str(msg.arbitration_id, msg.data))) arb_id = None data = None count = 0 with CanActions() as can_wrap: if show_responses: # Enable callback handler for incoming messages can_wrap.add_listener(response_handler) for arb_id, data in directives: count += 1 directive = directive_str(arb_id, data) can_wrap.send(data=data, arb_id=arb_id) if show_requests: print("Sending ({0}) {1}".format(count, directive)) sleep(DELAY_BETWEEN_MESSAGES) print("Replay finished")
def service_discovery(args): """ Scans for supported DCM services. Prints a list of all supported services afterwards. :param args: A namespace containing src and dst """ send_arb_id = args.src rcv_arb_id = args.dst with CanActions(arb_id=send_arb_id) as can_wrap: print("Starting DCM service discovery") supported_services = [] def response_analyser_wrapper(service_id): print("\rProbing service 0x{0:02x} ({1} found)".format( service_id, len(supported_services)), end="") stdout.flush() def response_analyser(m): # Skip incoming messages with wrong arbitration ID if m.arbitration_id != rcv_arb_id: return # Skip replies where service is not supported if m.data[3] == 0x11: return # Service supported - add to list supported_services.append(m.data[2]) return response_analyser def done(): print("\nDone!") # Message to bruteforce - [length, service id] msg = insert_message_length([0x00], pad=True) # Index of service id byte in message service_index = 1 try: # Initiate bruteforce can_wrap.bruteforce_data(msg, service_index, response_analyser_wrapper, callback_end=done) finally: # Clear listeners can_wrap.notifier.listeners = [] print("") # Print id and name of all found services for service in supported_services: service_name = DCM_SERVICE_NAMES.get(service, "Unknown service") print("Supported service 0x{0:02x}: {1}".format( service, service_name))
def xcp_command_discovery(args): """Attempts to call all XCP commands and lists which ones are supported.""" global connect_reply, command_reply send_arb_id = int_from_str_base(args.src) rcv_arb_id = int_from_str_base(args.dst) connect_message = [0xFF, 0, 0, 0, 0, 0, 0, 0] def connect_callback_handler(msg): global connect_reply if msg.arbitration_id == rcv_arb_id: connect_reply = True print("XCP command discovery\n") print("COMMAND{0}SUPPORTED".format(" " * 17)) print("-" * 33) with CanActions(send_arb_id) as can_wrap: # Bruteforce against list of commands (excluding connect) for cmd_code, cmd_desc in XCP_COMMAND_CODES[1:]: # Connect connect_reply = False can_wrap.send_single_message_with_callback( connect_message, connect_callback_handler) connect_timestamp = datetime.now() while not connect_reply and datetime.now( ) - connect_timestamp < timedelta(seconds=3): pass if not connect_reply: print("ERROR: Connect timeout") exit() # Build message for current command cmd_msg = [cmd_code, 0, 0, 0, 0, 0, 0, 0] # Callback handler for current command def callback_handler(msg): global command_reply if msg.arbitration_id == rcv_arb_id: print("{0:<23} {1}".format(cmd_desc, msg.data[0] != 0xFE)) command_reply = True command_reply = False # Send, wait for reply, clear listeners and move on can_wrap.send_single_message_with_callback( cmd_msg, callback=callback_handler) command_timestamp = datetime.now() while not command_reply and datetime.now( ) - command_timestamp < timedelta(seconds=3): pass if not command_reply: print("ERROR: Command timeout") exit() can_wrap.clear_listeners() print("\nDone!")
def send_messages(messages, loop): """ Sends a list of messages separated by a given delay. :param loop: bool indicating whether the message sequence should be looped (re-sent over and over) :param messages: List of messages, where a message has the format (arb_id, [data_byte]) """ with CanActions(notifier_enabled=False) as can_wrap: loop_counter = 0 while True: for i in range(len(messages)): msg = messages[i] if i != 0 or loop_counter != 0: sleep(msg.delay) print(" Arb_id: 0x{0:08x}, data: {1}".format(msg.arb_id, ["{0:02x}".format(a) for a in msg.data])) can_wrap.send(msg.data, msg.arb_id, msg.is_extended, msg.is_error, msg.is_remote) if not loop: break loop_counter += 1
def do_stuff(my_arbitration_id): """ Performs some example operations, such as sending and receiving CAN messages. :param my_arbitration_id: The default arbitration id to use when sending messages :type my_arbitration_id: int """ # The notifier should only be enabled when handling incoming traffic using callbacks use_notifier = False # Setup CanActions wrapper to use for receiving and sending messages with CanActions(arb_id=my_arbitration_id, notifier_enabled=use_notifier) as can_wrap: # Define message contents my_message = [0x11, 0x22, 0x33, 0x44] # Send message using the default arbitration ID for can_wrap can_wrap.send(data=my_message) # Send the same message again, but on a custom arbitration ID this time my_custom_arbitration_id = 0x123ABC can_wrap.send(data=my_message, arb_id=my_custom_arbitration_id) # Listen for incoming traffic for a while duration_seconds = 1.0 start_time = time.time() end_time = start_time + duration_seconds while time.time() < end_time: # Check if a message is available msg = can_wrap.bus.recv(0) if msg is None: # No message was available right now - continue listening loop continue # If we reach here, a message was received. Let's print it! print("Received a message on channel", msg.channel) print(" Arb ID: 0x{0:x} ({0})".format(msg.arbitration_id)) data_string = list_to_hex_str(msg.data, ".") print(" Data: ", data_string) # Module logic for message handling goes here if msg.arbitration_id < 0x10: print(" That was a low arbitration ID!") # When we reach here, can_wrap has been closed print("\nDone!")
def xcp_arbitration_id_discovery(args): """Scans for XCP support by brute forcing XCP connect messages against different arbitration IDs.""" global hit_counter min_id = int_from_str_base(args.min) max_id = int_from_str_base(args.max) hit_counter = 0 with CanActions() as can_wrap: print("Starting XCP discovery") def response_analyser_wrapper(arb_id): print("\rSending XCP connect to 0x{0:04x}".format(arb_id), end="") stdout.flush() def response_analyser(msg): global hit_counter # Handle positive response if msg.data[0] == 0xff and any(msg.data[1:]): hit_counter += 1 decode_connect_response(msg) print("Found XCP at arb ID 0x{0:04x}, reply at 0x{1:04x}". format(arb_id, msg.arbitration_id)) print("#" * 20) print("\n") # Handle negative response elif msg.data[0] == 0xfe: print( "\nFound XCP (with a bad reply) at arbitration ID 0x{0:03x}, reply at 0x{1:04x}" .format(arb_id, msg.arbitration_id)) decode_xcp_error(msg) return response_analyser def discovery_end(s): print("\r{0}: Found {1} possible matches.".format(s, hit_counter)) can_wrap.bruteforce_arbitration_id([0xff], response_analyser_wrapper, min_id=min_id, max_id=max_id, callback_end=discovery_end)
def initiate_dump(handler, args): """ Adds a CAN message handler which should add found data to found_arb_ids. Prints all of these afterwards, sorted by the number of hits. :param handler: Message handler function :param args: Argument namespace (reversed sorting applied if args.reverse) """ whitelist = [int_from_str_base(x) for x in args.whitelist] if args.candump_format: format_func = msg_to_candump_format else: format_func = str def whitelist_handling(msg): if len(whitelist) == 0 or msg.arbitration_id in whitelist: handler(format_func(msg)) print("Dumping CAN traffic (press Ctrl+C to exit)".format(whitelist)) with CanActions() as can_wrap: can_wrap.add_listener(whitelist_handling) while True: pass
def start_listener(handler, args): """ Adds a CAN message handler which should add found data to found_arb_ids. Prints all of these afterwards, sorted by the number of hits. :param handler: Message handler function :param args: Argument namespace (reversed sorting applied if args.reverse) """ try: print("Starting listener (press Ctrl+C to exit)") with CanActions() as can_wrap: can_wrap.add_listener(handler) while True: pass finally: if len(found_arb_ids) > 0: print("\n\nDetected arbitration IDs:") for (arb_id, hits) in sorted(found_arb_ids.items(), key=lambda x: x[1], reverse=args.reverse): print("Arb id 0x{0:08x} {1} hits".format(arb_id, hits)) else: print("No arbitration IDs were detected.")
def xcp_arbitration_id_discovery(args): """Scans for XCP support by brute forcing XCP connect messages against different arbitration IDs.""" global hit_counter min_id = args.min max_id = args.max blacklist = set(args.blacklist) blacklist_duration = args.autoblacklist hit_counter = 0 def is_valid_response(msg): """ Returns a bool indicating whether 'data' is a valid XCP response :param data: list of message data bytes :return: True if data is a valid XCP response, False otherwise """ data = msg.data return (len(data) > 1 and data[0] == 0xff and any(data[1:])) or \ (len(data) > 0 and data[0] == 0xfe) if blacklist_duration > 0: # Perform automatic blacklist scanning with CanActions(notifier_enabled=False) as blacklist_wrap: blacklist |= auto_blacklist(blacklist_wrap.bus, blacklist_duration, is_valid_response, True) with CanActions() as can_wrap: print("Starting XCP discovery") def response_analyser_wrapper(arb_id): print("\rSending XCP connect to 0x{0:04x}".format(arb_id), end="") stdout.flush() def response_analyser(msg): global hit_counter # Ignore blacklisted arbitration IDs if msg.arbitration_id in blacklist: return # Handle positive response if msg.data[0] == 0xff and any(msg.data[1:]): hit_counter += 1 decode_connect_response(msg) print("Found XCP at arb ID 0x{0:04x}, reply at 0x{1:04x}". format(arb_id, msg.arbitration_id)) print("#" * 20) print("\n") # Handle negative response elif msg.data[0] == 0xfe: print( "\nFound XCP (with a bad reply) at arbitration ID 0x{0:03x}, reply at 0x{1:04x}" .format(arb_id, msg.arbitration_id)) decode_xcp_error(msg) return response_analyser def discovery_end(s): print("\r{0}: Found {1} possible matches.".format(s, hit_counter)) can_wrap.bruteforce_arbitration_id([0xff], response_analyser_wrapper, min_id=min_id, max_id=max_id, callback_end=discovery_end)
def xcp_arbitration_id_discovery(args): """Scans for XCP support by brute forcing XCP connect messages against different arbitration IDs.""" global hit_counter min_id = int_from_str_base(args.min) max_id = int_from_str_base(args.max) blacklist = [int_from_str_base(b) for b in args.blacklist] hit_counter = 0 def is_valid_response(data): """ Returns a bool indicating whether 'data' is a valid XCP response :param data: list of message data bytes :return: True if data is a valid XCP response, False otherwise """ return (len(data) > 1 and data[0] == 0xff and any(data[1:])) or \ (len(data) > 0 and data[0] == 0xfe) def scan_arbitration_ids_to_blacklist(scan_duration): print("Scanning for arbitration IDs to blacklist (-autoblacklist)") ids_to_blacklist = set() def response_handler(msg): """ Blacklists the arbitration ID of a message if it could be misinterpreted as valid diagnostic response :param msg: can.Message instance to check """ if is_valid_response(msg.data): ids_to_blacklist.add(msg.arbitration_id) with CanActions() as can_actions: # Listen for matches can_actions.add_listener(response_handler) for i in range(scan_duration, 0, -1): print("\r{0:> 3} seconds left, {1} found".format( i - 1, len(ids_to_blacklist)), end="") stdout.flush() time.sleep(1) print("") can_actions.clear_listeners() # Add found matches to blacklist for arb_id in ids_to_blacklist: blacklist.append(arb_id) # Perform automatic blacklist scanning if args.autoblacklist > 0: scan_arbitration_ids_to_blacklist(args.autoblacklist) with CanActions() as can_wrap: print("Starting XCP discovery") def response_analyser_wrapper(arb_id): print("\rSending XCP connect to 0x{0:04x}".format(arb_id), end="") stdout.flush() def response_analyser(msg): global hit_counter # Ignore blacklisted arbitration IDs if msg.arbitration_id in blacklist: return # Handle positive response if msg.data[0] == 0xff and any(msg.data[1:]): hit_counter += 1 decode_connect_response(msg) print("Found XCP at arb ID 0x{0:04x}, reply at 0x{1:04x}". format(arb_id, msg.arbitration_id)) print("#" * 20) print("\n") # Handle negative response elif msg.data[0] == 0xfe: print( "\nFound XCP (with a bad reply) at arbitration ID 0x{0:03x}, reply at 0x{1:04x}" .format(arb_id, msg.arbitration_id)) decode_xcp_error(msg) return response_analyser def discovery_end(s): print("\r{0}: Found {1} possible matches.".format(s, hit_counter)) can_wrap.bruteforce_arbitration_id([0xff], response_analyser_wrapper, min_id=min_id, max_id=max_id, callback_end=discovery_end)
def dcm_dtc(args): """ Fetches and prints the Diagnostic Trouble Codes from a supported service (Mode $03) :param args: A namespace containing src, dst and clear """ send_arb_id = args.src rcv_arb_id = args.dst clear = args.clear big_data = [] big_data_size = 0 def dtc_type(x): return { 0: "P", 1: "C", 2: "B", 3: "U", }.get(x, "?") def decode_dtc(data): # Expects 2 bytes if len(data) != 2: return return dtc_type((data[0] & 0xC0) >> 6) + format( (data[0] & 0x30) >> 4) + format(data[0] & 0x0F, "01x") + format( data[1], "02x") def decode_dtc_pkt(msg): if msg.arbitration_id != rcv_arb_id: return return_packet = False # TODO: Are we sure that data byte 0 is 0x10, or should the check be against data[0] & 0x10 instead? if msg.data[0] == 0x10 and (msg.data[2] == 0x43 or msg.data[2] == 0x47): return_packet = True if (msg.data[0] & 0xF0) == 0x20: # We should probably set a state for this return_packet = True if msg.data[1] == 0x43: return_packet = True if msg.data[2] == 0x47: return_packet = True if not return_packet: return global big_data global big_data_size if big_data_size == 0 and (msg.data[1] == 0x43 or msg.data[1] == 0x47): # Single frame print("There are {0} DTCs".format(msg.data[2])) if msg.data[2] == 0: return if msg.data[0] > 2: print("DTC: {0}".format(decode_dtc(msg.data[3:5]))) if msg.data[0] > 4: print("DTC: {0}".format(decode_dtc(msg.data[5:6]))) if msg.data[0] > 6: print("DTC: {0}".format(decode_dtc(msg.data[7:9]))) if msg.data[0] == 0x10: # Multi Frame (First Frame) full_dlc = (msg.data[0] & 0x0F) + msg.data[1] print("There are {0} DTCs".format(msg.data[3])) print("DTC: {0}".format(decode_dtc(msg.data[4:6]))) print("DTC: {0}".format(decode_dtc(msg.data[6:8]))) can_wrap.send([0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) big_data_size = full_dlc - 6 if (msg.data[0] & 0xF0) == 0x20: # Consecutive if big_data_size > 8: big_data.extend(msg.data[1:]) big_data_size -= 7 else: big_data.extend(msg.data[1:big_data_size + 1]) big_data_size = 0 if big_data_size == 0: for i in range(0, len(big_data), 2): print("DTC: {0}".format(decode_dtc(big_data[i:i + 2]))) with CanActions(arb_id=send_arb_id) as can_wrap: if clear: msg = insert_message_length([0x04], pad=True) can_wrap.send(msg) print("Cleared DTCs and reset MIL") else: print("Fetching Diagnostic Trouble Codes") msg = insert_message_length([0x03], pad=True) can_wrap.send_single_message_with_callback(msg, decode_dtc_pkt) time.sleep(0.5) print("Fetching Pending Diagnostic Trouble Codes") msg = insert_message_length([0x07], pad=True) can_wrap.send_single_message_with_callback(msg, decode_dtc_pkt) time.sleep(1)
def subfunc_discovery(args): """ Scans for subfunctions of a given service. :param args: A namespace containing src, dst, service, show and i """ send_arb_id = args.src rcv_arb_id = args.dst service_id = args.service show_data = args.show bruteforce_indices = args.i # Sanity checks all_valid = True for i in bruteforce_indices: if not 2 <= i <= 7: print( "Invalid bruteforce index '{0}' - must be in range 2-7".format( i)) all_valid = False if not all_valid: return with CanActions(arb_id=send_arb_id) as can_wrap: found_sub_functions = [] print("Starting DCM sub-function discovery") def response_analyser_wrapper(data): print("\rProbing sub-function 0x{0:02x} data {1} (found: {2})". format(service_id, data, len(found_sub_functions)), end="") stdout.flush() def response_analyser(msg): if msg.arbitration_id != rcv_arb_id: return # Response queued - do not handle if msg.data[:4] == [0x03, 0x7f, service_id, 0x78]: can_wrap.current_delay = 1.0 return # Catch ok status elif msg.data[1] - 0x40 == service_id or \ (msg.data[1] == 0x7F and msg.data[3] not in [0x11, 0x12, 0x31, 0x78]): found_sub_functions.append((data, [msg])) elif msg.data[0] == 0x10: # If response takes up multiple frames can_wrap.current_delay = 1.0 found_sub_functions.append((data, [msg])) if show_data: # Cool, give me the rest can_wrap.send( [0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) else: # Fine, but I don't want the remaining data can_wrap.send( [0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) elif show_data and msg.data[0] & 0xF0 == 0x20: # Parts following a 0x30 in multiple frame response (keep waiting) can_wrap.current_delay = 1.0 found_sub_functions[-1][1].append(msg) else: # We got an answer - no reason to keep waiting can_wrap.current_delay = 0.0 return response_analyser def finished(s): print("\nDone: {0}".format(s)) try: # Message to bruteforce - [length, session control, default session] message = insert_message_length([service_id, 0x00, 0x00], pad=True) can_wrap.bruteforce_data_new(message, bruteforce_indices=bruteforce_indices, callback=response_analyser_wrapper, callback_done=finished) can_wrap.notifier.listeners = [] finally: # Print found functions if len(found_sub_functions) > 0: print("\n\nFound sub-functions for service 0x{0:02x} ({1}):\n". format( service_id, DCM_SERVICE_NAMES.get(service_id, "Unknown service"))) for (sub_function, msgs) in found_sub_functions: print("Sub-function {0}".format(" ".join(sub_function))) if show_data: for message in msgs: print(" {0}".format(message)) else: print("\n\nNo sub-functions were found")
def xcp_get_basic_information(args): send_arb_id = int_from_str_base(args.src) rcv_arb_id = int_from_str_base(args.dst) def callback_wrapper(callback): """ Adds handling of uninteresting or error messages to a callback function. :param callback: The callback function to run on successful messages :return: A callback function with extended message handling """ def c(msg): if msg.arbitration_id != rcv_arb_id: return if msg.data[0] == 0xfe: return if msg.data[0] == 0xff: callback(msg) else: print("Unexpected reply:\n{0}\n".format(msg)) return c class ProbeMessage: """Wrapper class for probe messages""" def __init__(self, message_data, callback): self.message_data = message_data self.callback = callback_wrapper(callback) def __str__(self): return "{0}".format( ["{0:02x}".format(a) for a in self.message_data]) # Callback handler for GetId messages def print_msg_as_text(msg): print("".join([chr(x) for x in msg.data[1:]])) def handle_get_id_reply(msg): can_wrap.send_single_message_with_callback( [0xf5, msg.data[4]], callback_wrapper(print_msg_as_text)) # Define probe messages probe_msgs = [ ProbeMessage([0xff], decode_connect_response), # Connect ProbeMessage([0xfb], decode_get_comm_mode_info_response), # GetCommMode ProbeMessage([0xfd], decode_get_status_response), # GetStatus ProbeMessage([0xfa, 0x00], handle_get_id_reply), # GetId ASCII text ProbeMessage( [0xfa, 0x01], handle_get_id_reply), # GetId ASAM-MC2 filename w/o path/ext ProbeMessage( [0xfa, 0x02], handle_get_id_reply), # GetId ASAM-MC2 filename with path/ext ProbeMessage([0xfa, 0x03], handle_get_id_reply), # GetId ASAM-MC2 URL ProbeMessage([0xfa, 0x04], handle_get_id_reply) ] # GetId ASAM-MC2 fileToUpload # Initiate probing with CanActions(arb_id=send_arb_id) as can_wrap: print("Probing for XCP info") for probe in probe_msgs: print("Sending probe message: {0}".format(probe)) can_wrap.send_single_message_with_callback(probe.message_data, probe.callback) time.sleep(2) print("Probing finished")
def xcp_memory_dump(args): """ Performs a memory dump to file or stdout via XCP. :param args: A namespace containing src, dst, start, length and f """ send_arb_id = int_from_str_base(args.src) rcv_arb_id = int_from_str_base(args.dst) start_address = int_from_str_base(args.start) length = int_from_str_base(args.length) dump_file = args.f # TODO Implement support for larger segments against ECUs which support this (e.g. 0xfc for test board) max_segment_size = 0x7 global byte_counter, bytes_left, dump_complete, segment_counter, timeout_start # Timeout timer dump_complete = False # Counters for data length byte_counter = 0 segment_counter = 0 def handle_upload_reply(msg): global byte_counter, bytes_left, dump_complete, timeout_start, segment_counter if msg.arbitration_id != rcv_arb_id: return if msg.data[0] == 0xfe: decode_xcp_error(msg) return if msg.data[0] == 0xff: # Reset timeout timer timeout_start = datetime.now() # Calculate end index of data to handle end_index = min(8, bytes_left + 1) if dump_file: with open(dump_file, "ab") as outfile: outfile.write(bytearray(msg.data[1:end_index])) else: print(" ".join( ["{0:02x}".format(j) for j in msg.data[1:end_index]])) # Update counters byte_counter += max_segment_size bytes_left -= max_segment_size if bytes_left < 1: if dump_file: print("\rDumping segment {0} ({1} b, 0 b left)".format( segment_counter, length), end="") print("Dump complete!") dump_complete = True elif byte_counter > max_segment_size - 1: # Dump another segment segment_counter += 1 if dump_file: # Print progress print("\rDumping segment {0} ({1} b, {2} b left)".format( segment_counter, ((segment_counter + 1) * max_segment_size + byte_counter), bytes_left), end="") stdout.flush() byte_counter = 0 can_wrap.send_single_message_with_callback( [0xf5, min(max_segment_size, bytes_left)], handle_upload_reply) def handle_set_mta_reply(msg): if msg.arbitration_id != rcv_arb_id: return if msg.data[0] == 0xfe: decode_xcp_error(msg) return if msg.data[0] == 0xff: print("Set MTA acked") print("Dumping data:") # Initiate dumping if dump_file: print("\rDumping segment 0", end="") can_wrap.send_single_message_with_callback( [0xf5, min(max_segment_size, bytes_left)], handle_upload_reply) else: print("Unexpected reply: {0}\n".format(msg)) def handle_connect_reply(msg): if msg.arbitration_id != rcv_arb_id: return if msg.data[0] == 0xfe: decode_xcp_error(msg) return if msg.data[0] == 0xff: print("Connected: Using", end=" ") # Check connect reply to see whether to reverse byte order for MTA msb_format = msg.data[2] & 1 if msb_format: print("Motorola format (MSB lower)") else: print("Intel format (LSB lower)") r.reverse() can_wrap.send_single_message_with_callback( [0xf6, 0x00, 0x00, 0x00, r[0], r[1], r[2], r[3]], handle_set_mta_reply) else: print("Unexpected connect reply: {0}\n".format(msg)) # Calculate address bytes (4 bytes, least significant first) r = [] n = start_address bytes_left = length # Calculate start address (r is automatically reversed after connect if needed) n &= 0xFFFFFFFF for i in range(4): r.append(n & 0xFF) n >>= 8 # Make sure dump_file can be opened if specified (clearing it if it already exists) if dump_file: try: with open(dump_file, "w") as _: pass except IOError as e: print("Error when opening dump file:\n\n{0}".format(e)) return # Initialize with CanActions(arb_id=send_arb_id) as can_wrap: print("Attempting XCP memory dump") # Connect and prepare for dump can_wrap.send_single_message_with_callback([0xff], handle_connect_reply) # Idle timeout handling timeout_start = datetime.now() while not dump_complete and datetime.now() - timeout_start < timedelta( seconds=3): pass if not dump_complete: print("\nERROR: Dump ended due to idle timeout")
def dcm_discovery(args): """ Scans for diagnostics support by sending session control against different arbitration IDs. :param: args: A namespace containing min and max """ min_id = args.min max_id = args.max no_stop = args.nostop blacklist = args.blacklist valid_responses = [0x50, 0x7F] def scan_arbitration_ids_to_blacklist(scan_duration): print("Scanning for arbitration IDs to blacklist (-autoblacklist)") ids_to_blacklist = set() def response_handler(msg): """ Blacklists the arbitration ID of a message if it could be misinterpreted as valid diagnostic response :param msg: can.Message instance to check """ if len(msg.data) > 1 and msg.data[1] in valid_responses: ids_to_blacklist.add(msg.arbitration_id) with CanActions() as can_actions: # Listen for matches can_actions.add_listener(response_handler) for i in range(scan_duration, 0, -1): print("\r{0:> 3} seconds left, {1} found".format( i - 1, len(ids_to_blacklist)), end="") stdout.flush() time.sleep(1) print("") can_actions.clear_listeners() # Add found matches to blacklist for arb_id in ids_to_blacklist: blacklist.append(arb_id) # Perform automatic blacklist scanning if args.autoblacklist > 0: scan_arbitration_ids_to_blacklist(args.autoblacklist) class Diagnostics: found = False with CanActions() as can_wrap: print("Starting diagnostics service discovery") def response_analyser_wrapper(arb_id): print("\rSending Diagnostic Session Control to 0x{0:04x}".format( arb_id), end="") stdout.flush() def response_analyser(msg): # Ignore blacklisted arbitration IDs if msg.arbitration_id in blacklist: return # Catch both ok and negative response if len(msg.data) >= 2 and msg.data[1] in valid_responses: Diagnostics.found = True print("\nFound diagnostics at arbitration ID 0x{0:04x}, " "reply at 0x{1:04x}".format(arb_id, msg.arbitration_id)) if not no_stop: can_wrap.bruteforce_stop() return response_analyser def discovery_finished(s): if Diagnostics.found: print("\n{0}".format(s)) else: print( "\nDiagnostics service could not be found: {0}".format(s)) # Message to bruteforce - [length, session control, default session] message = insert_message_length([0x10, 0x01], pad=True) can_wrap.bruteforce_arbitration_id(message, response_analyser_wrapper, min_id=min_id, max_id=max_id, callback_end=discovery_finished)
def identify_fuzz(all_composites, show_responses): """ Replays a list of composites causing an effect, prompting for input to help isolate the message causing the effect :param all_composites: list of composites :param show_responses: bool indicating whether responses should be printed to stdout :return: str directive if message is found, None otherwise """ def response_handler(msg): if msg.arbitration_id != arb_id or list(msg.data) != data: response_directive = directive_str(msg.arbitration_id, msg.data) print(" Received {0}".format(response_directive)) arb_id = None data = None composites = None directive = None repeat = False with CanActions() as can_wrap: try: # Send all messages in first round gen = split_lists(all_composites, 1) while True: print() if repeat: # Keep previous list of messages to send repeat = False else: # Get next list of messages to send composites = next(gen) if show_responses: # Enable callback handler for incoming messages can_wrap.add_listener(response_handler) # Send messages for index in range(len(composites)): composite = composites[index] arb_id = composite[0] data = composite[1] directive = directive_str(arb_id, data) can_wrap.send(data=data, arb_id=arb_id) print("Sending ({0}/{1}) {2}".format( index + 1, len(composites), directive)) sleep(DELAY_BETWEEN_MESSAGES) # Disable callback handler for incoming messages can_wrap.clear_listeners() # Get user input print("\nWas the desired effect observed?") valid_response = False while not valid_response: valid_response = True response = input( "(y)es | (n)o | (r)eplay | (q)uit: ").lower() if response == "y": if len(composites) == 1: # Single message found print("\nMatch found! Message causing effect: {0}". format(directive)) return directive else: # Split into even smaller lists of messages gen = split_lists(composites, REPLAY_NUMBER_OF_SUB_LISTS) elif response == "n": # Try next list of messages pass elif response == "r": # Repeat batch repeat = True elif response == "q": # Quit return None else: # Invalid choice - ask again print("Invalid choice") valid_response = False except StopIteration: # No more messages to try - give up print("\nNo match was found.") return None
def mutate_fuzz(initial_arb_id, initial_data, arb_id_bitmap, data_bitmap, filename=None, show_status=True, show_responses=False, seed=None): """ Performs mutation based fuzzing of selected nibbles of a given arbitration ID and data. Nibble selection is controlled by bool lists 'arb_id_bitmap' and 'data_bitmap'. :param initial_arb_id: list of nibbles (ints in interval 0x0-0xF, inclusive) :param initial_data: list of nibbles (ints in interval 0x0-0xF, inclusive) :param arb_id_bitmap: list of bool values, representing which nibbles of 'initial_arb_id' to bruteforce :param data_bitmap: list of bool values, representing which nibbles of 'initial_data' to bruteforce :param filename: file to write cansend directives to :param show_status: bool indicating whether current message and counter should be printed to stdout :param show_responses: bool indicating whether responses should be printed to stdout :param seed: use given seed instead of random seed """ # Seed handling set_seed(seed) def response_handler(msg): # Callback handler for printing incoming messages if msg.arbitration_id != arb_id or list(msg.data) != data: response_directive = directive_str(msg.arbitration_id, msg.data) print(" Received {0}".format(response_directive)) number_of_nibbles_to_fuzz_arb_id = sum(arb_id_bitmap) number_of_nibbles_to_fuzz_data = sum(data_bitmap) file_logging_enabled = filename is not None output_file = None # Set initial values - needed in case they are static data = None arb_id = None if number_of_nibbles_to_fuzz_data == 0: data = apply_fuzzed_data(initial_data, [], data_bitmap) if number_of_nibbles_to_fuzz_arb_id == 0: arb_id = int_from_byte_list( apply_fuzzed_data(initial_arb_id, [], arb_id_bitmap)) try: if file_logging_enabled: output_file = open(filename, "a") with CanActions() as can_wrap: if show_responses: can_wrap.add_listener(response_handler) message_count = 0 while True: if number_of_nibbles_to_fuzz_arb_id > 0: # Mutate arbitration ID fuzzed_nibbles_arb_id = [ random.randint(0, 0xF) for _ in range(number_of_nibbles_to_fuzz_arb_id) ] arb_id_bytes = apply_fuzzed_data(initial_arb_id, fuzzed_nibbles_arb_id, arb_id_bitmap) arb_id = int_from_byte_list(arb_id_bytes) if number_of_nibbles_to_fuzz_data > 0: # Mutate data fuzzed_nibbles_data = [ random.randint(0, 0xF) for _ in range(number_of_nibbles_to_fuzz_data) ] data = apply_fuzzed_data(initial_data, fuzzed_nibbles_data, data_bitmap) if show_status: print("\rSending {0:04x} # {1} ({2})".format( arb_id, list_to_hex_str(data, " "), message_count), end="") stdout.flush() can_wrap.send(data, arb_id) message_count += 1 # Log to file if file_logging_enabled: write_directive_to_file_handle(output_file, arb_id, data) sleep(DELAY_BETWEEN_MESSAGES) except KeyboardInterrupt: if show_status: print() finally: if output_file is not None: output_file.close()
def bruteforce_fuzz(arb_id, initial_data, data_bitmap, filename=None, start_index=0, show_progress=True, show_responses=True): """ Performs a brute force of selected data nibbles for a given arbitration ID. Nibble selection is controlled by bool list 'data_bitmap'. Example: bruteforce_fuzz(0x123, [0x1, 0x2, 0xA, 0xB], [True, False, False, True]) will cause the following messages to be sent: 0x123#02A0 0x123#02A1 0x123#02A2 (...) 0x123#02AF 0x123#12A0 0x123#12A1 (...) 0x123#F2AF :param arb_id: int arbitration ID :param initial_data: list of nibbles (ints in interval 0x0-0xF, inclusive) :param data_bitmap: list of bool values, representing which nibbles of 'initial_data' to bruteforce :param filename: file to write cansend directives to :param start_index: int index to start at (can be used to resume interrupted session) :param show_progress: bool indicating whether progress should be printed to stdout :param show_responses: bool indicating whether responses should be printed to stdout """ # Sanity checks if not 2 <= len(initial_data) <= 16: raise ValueError( "Invalid initial data: must be between 2 and 16 nibbles") if not len(initial_data) % 2 == 0: raise ValueError("Invalid initial data: must have an even length") if not len(initial_data) == len(data_bitmap): raise ValueError( "Initial data ({0}) and data bitmap ({1}) must have the same length" .format(len(initial_data), len(data_bitmap))) number_of_nibbles_to_bruteforce = sum(data_bitmap) end_index = 16**number_of_nibbles_to_bruteforce if not 0 <= start_index <= end_index: raise ValueError( "Invalid start index '{0}', current range is [0-{1}]".format( start_index, end_index)) def response_handler(msg): # Callback handler for printing incoming messages if msg.arbitration_id != arb_id or list(msg.data) != output_data: response_directive = directive_str(msg.arbitration_id, msg.data) print(" Received {0}".format(response_directive)) # Initialize fuzzed nibble generator nibble_values = range(0xF + 1) fuzz_data = product(nibble_values, repeat=number_of_nibbles_to_bruteforce) file_logging_enabled = filename is not None output_file = None output_data = [] try: if file_logging_enabled: output_file = open(filename, "a") with CanActions(arb_id=arb_id) as can_wrap: if show_progress: print("Starting at index {0} of {1}\n".format( start_index, end_index)) if show_responses: can_wrap.add_listener(response_handler) message_count = 0 # Traverse all outputs from fuzz generator for current_fuzzed_nibbles in fuzz_data: # Skip handling until start_index is met if message_count < start_index: message_count += 1 continue # Apply fuzzed data output_data = apply_fuzzed_data(initial_data, current_fuzzed_nibbles, data_bitmap) # Send message can_wrap.send(output_data) message_count += 1 if show_progress: print("\rCurrent: {0} Index: {1}".format( list_to_hex_str(output_data, " "), message_count), end="") stdout.flush() # Log to file if file_logging_enabled: write_directive_to_file_handle(output_file, arb_id, output_data) sleep(DELAY_BETWEEN_MESSAGES) if show_progress: print() finally: if output_file is not None: output_file.close() if show_progress: print("Brute force finished")
def random_fuzz(static_arb_id=None, static_data=None, filename=None, min_id=ARBITRATION_ID_MIN, max_id=ARBITRATION_ID_MAX, min_data_length=MIN_DATA_LENGTH, max_data_length=MAX_DATA_LENGTH, show_status=True, seed=None): """ A simple random fuzzer algorithm, which sends random or static data to random or static arbitration IDs :param static_arb_id: int representing static arbitration ID :param static_data: list of bytes representing static data :param filename: file to write cansend directives to :param min_id: minimum allowed arbitration ID :param max_id: maximum allowed arbitration ID :param min_data_length: minimum allowed data length :param max_data_length: maximum allowed data length :param show_status: bool indicating whether current message and counter should be printed to stdout :param seed: use given seed instead of random seed """ # Sanity checks if static_arb_id is not None and static_data is not None: raise ValueError( "Both static arbitration ID and static data cannot be set at the same time" ) if not 0 <= min_id < max_id <= ARBITRATION_ID_MAX: raise ValueError("Invalid value for min_id and/or max_id") if not MIN_DATA_LENGTH <= min_data_length <= max_data_length <= MAX_DATA_LENGTH: raise ValueError( "Invalid value for min_data_length ({0}) and/or max_data_length ({1})" .format(min_data_length, max_data_length)) if static_data is not None and len(static_data) > MAX_DATA_LENGTH: raise ValueError( "static_data ({0} bytes) must not be more than {1} bytes long". format(len(static_data), MAX_DATA_LENGTH)) # Seed handling set_seed(seed) # Define a callback function which will handle incoming messages def response_handler(msg): if msg.arbitration_id != arb_id or list(msg.data) != data: directive = directive_str(arb_id, data) print("\rDirective: {0} (message {1})".format( directive, message_count)) print(" Received message: {0}".format(msg)) arb_id = None data = None file_logging_enabled = filename is not None output_file = None try: if file_logging_enabled: output_file = open(filename, "a") with CanActions() as can_wrap: # Register callback handler for incoming messages can_wrap.add_listener(response_handler) message_count = 0 # Fuzzing logic while True: # Set arbitration ID if static_arb_id is None: # Use a random arbitration ID arb_id = get_random_arbitration_id(min_id, max_id) else: # Use the static arbitration ID arb_id = static_arb_id # Set data if static_data is None: data = get_random_data(min_data_length, max_data_length) else: data = static_data if show_status: print("\rMessages sent: {0}".format(message_count), end="") stdout.flush() # Send message can_wrap.send(data=data, arb_id=arb_id) message_count += 1 # Log to file if file_logging_enabled: write_directive_to_file_handle(output_file, arb_id, data) sleep(DELAY_BETWEEN_MESSAGES) except IOError as e: print("ERROR: {0}".format(e)) finally: if output_file is not None: output_file.close()