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), 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 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 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 = int_from_str_base(args.src) rcv_arb_id = int_from_str_base(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 = [0x01, 0x00] # 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 foo(arb_id): """ Performs some example operations, such as sending and receiving CAN messages. :param arb_id: The arbitration id to use when sending message 1 """ # Define a callback function which will handle incoming messages def example_response_handler(msg): print("Callback handler: Incoming message!") print(msg) # Examples of how to filter data if msg.data[0] > 0x0F: print("First byte is not very small ({0})".format(msg.data[0])) if msg.arbitration_id < 0x10: print("Low arbitration ID on this one ({0})".format( msg.arbitration_id)) print("---") print("Setting up") # Create an instance of CanActions, which we can use to send and receive messages. with CanActions(arb_id) as can_wrap: # EXAMPLE 1 - SEND MESSAGE WITH CALLBACK # Define message contents message1 = [0x11, 0x22, 0x33, 0x44] # Number of seconds for callback handler to be active callback_handler_duration = 3 print("Sending message 1 and adding callback function") # Send the message on the CAN bus (using default arbitration ID) and register a callback # handler for incoming messages can_wrap.send_single_message_with_callback(message1, example_response_handler) print("Letting callback handler be active for {0} seconds".format( callback_handler_duration)) # Wait for three seconds before closing the CanActions instance. This means we have three # seconds to handle incoming messages through the callback function. sleep(callback_handler_duration) # Manually remove the callback function. This is only needed since we want to proceed # without keeping the callback handler in the next example - otherwise it would be # automatically removed once can_wrap is closed. can_wrap.clear_listeners() print("Removed callback handler") # EXAMPLE 2 - SEND MESSAGE TO CUSTOM ARBITRATION ID # Define message contents message2 = [0x55, 0x66, 0x77, 0x88, 0x99, 0xAA] # Define custom arbitration ID my_arb_id = 0x123 print("Sending message 2") # Send message on the CAN bus using the custom arbitration ID can_wrap.send(message2, my_arb_id) # When we reach here, can_wrap has been closed print("Done!")
def send_messages(messages, delay_between_messages): """ Sends a list of messages separated by a given delay. :param messages: List of messages, where a message has the format (arb_id, [data_byte]) :param delay_between_messages: Delay in seconds between each transmitted message """ with CanActions() as can_wrap: for arb_id, message_data in messages: print(" Arb_id: 0x{0:03x}, data: {1}".format(arb_id, ["{0:02x}".format(a) for a in message_data])) can_wrap.send(message_data, arb_id) sleep(delay_between_messages)
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 = int_from_str_base(args.min) max_id = int_from_str_base(args.max) no_stop = args.nostop 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), stdout.flush() def response_analyser(msg): # Catch both ok and negative response if len(msg.data) >= 2 and msg.data[1] in [0x50, 0x7F]: Diagnostics.found = True print( "\nFound diagnostics at arbitration ID 0x{0:04x}, " "reply at 0x{1:04x}".format(arb_id, msg.arbitration_id)) if no_stop == False: 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]) can_wrap.bruteforce_arbitration_id(message, response_analyser_wrapper, min_id=min_id, max_id=max_id, callback_end=discovery_finished)
def directive_send(arb_id, payload, response_handler): """ Sends a cansend directive. :param arb_id: The destination arbitration id. :param payload: The payload to be sent. :param response_handler: The callback handler that needs to be called when a response message is received. """ arb_id = "0x" + arb_id send_msg = payload_to_str_base(payload) with CanActions(int_from_str_base(arb_id)) as can_wrap: # Send the message on the CAN bus and register a callback # handler for incoming messages can_wrap.send_single_message_with_callback(list_int_from_str_base(send_msg), response_handler) # Letting callback handler be active for CALLBACK_HANDLER_DURATION seconds sleep(CALLBACK_HANDLER_DURATION)
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() 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:03x}, data: {1}".format(msg.arb_id, ["{0:02x}".format(a) for a in msg.data])) can_wrap.send(msg.data, msg.arb_id) if not loop: break loop_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): if 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 Scanning... {0} seconds left, {1} found".format( i - 1, len(ids_to_blacklist)), 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, 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:03x} {1} hits".format(arb_id, hits)) else: print("No arbitration IDs were detected.")
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 = int_from_str_base(args.src) rcv_arb_id = int_from_str_base(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, 0x0, 0x0]) 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: can_wrap.send([0x01, 0x04]) print("Cleared DTCs and reset MIL") else: print("Fetching Diagnostic Trouble Codes") can_wrap.send_single_message_with_callback([0x01, 0x03], decode_dtc_pkt) time.sleep(0.5) print("Fetching Pending Diagnostic Trouble Codes") can_wrap.send_single_message_with_callback([0x01, 0x07], 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 = int_from_str_base(args.src) rcv_arb_id = int_from_str_base(args.dst) service_id = int_from_str_base(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]) else: # Fine, but I don't want the remaining data can_wrap.send([0x32]) 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]) 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 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 = int_from_str_base(args.min) max_id = int_from_str_base(args.max) no_stop = args.nostop blacklist = [int_from_str_base(b) for b in 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): if 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 Scanning... {0} 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]) can_wrap.bruteforce_arbitration_id(message, response_analyser_wrapper, min_id=min_id, max_id=max_id, callback_end=discovery_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) 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), 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", 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", # 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")