Repository: secdev/scapy

Issue:
ISOTPScan does not work with extended IDs
When using scapy for CAN packages the ISOTP funcionallity does not work when using extended frames. tools\automotive\isotpscanner.py does have --extended option but it does not work.

The following command:
`isotpscanner.py --extended --interface=vector --channel=0 --bitrate=500000 --start 0x17ffFF00 --end 0x17ffffff`
crashes.

Scapy verison is the recent. Commit: 3dfbfc66d89e8a2432a2b857fef3eff94f1b5691
Python 3.7
Windows 10
Vector VN1610 as CAN interface

Output:
  File "C:\Python37\lib\site-packages\scapy\contrib\isotp.py", line 1939, in ISOTPScan
    sniff_time=sniff_time, verbose=verbose)
  File "C:\Python37\lib\site-packages\scapy\contrib\isotp.py", line 1871, in scan_extended
    scan_block_size))
  File "C:\Python37\lib\site-packages\scapy\contrib\isotp.py", line 1702, in send_multiple_ext
    sock.send(packet)
  File "C:\Python37\lib\site-packages\scapy\contrib\cansocket_python_can.py", line 69, in send
    raise ex
  File "C:\Python37\lib\site-packages\scapy\contrib\cansocket_python_can.py", line 67, in send
    return self.iface.send(msg)
  File "C:\Python37\lib\site-packages\python_can-2.2.0-py3.7.egg\can\interfaces\vector\canlib.py", line 336, in send
  File "C:\Python37\lib\site-packages\python_can-2.2.0-py3.7.egg\can\interfaces\vector\vxlapi.py", line 157, in check_status
can.interfaces.vector.exceptions.VectorError: xlCanTransmit failed (XL_ERR_WRONG_PARAMETER)



Discussion:
Hi Ita1pu, thanks for your bug report. 
The flag `extended` means ISOTP extended addressing. This is a addressing scheme, where the first data byte of a CAN frame is used for addressing. We didn't support extended CAN-Identifiers. We didn't implemented this feature, because we thought, the search space might be to big. From your example, I see that the search space would be small enough to scan it in a decent time. 

I would be happy, if you could make a PR which adds the support of extended addressing
Thanks for the quick response and explaination. I will make a PR. 
https://github.com/secdev/scapy/issues/2297#issuecomment-545374424_
The flag `extended` is required in the current version of isotpscanner. For a scan with extended CAN Identifiers, I would recommend a new flag. Maybe `extended_id` like in python-can

Resolving patch:
diff --git a/scapy/contrib/isotp.py b/scapy/contrib/isotp.py
--- a/scapy/contrib/isotp.py
+++ b/scapy/contrib/isotp.py
@@ -1702,11 +1702,12 @@ def send_multiple_ext(sock, ext_id, packet, number_of_packets):
         sock.send(packet)
 
 
-def get_isotp_packet(identifier=0x0, extended=False):
+def get_isotp_packet(identifier=0x0, extended=False, extended_can_id=False):
     """ Craft ISO TP packet
     Args:
         identifier: identifier of crafted packet
         extended: boolean if packet uses extended address
+        extended_can_id: boolean if CAN should use extended Ids
     """
 
     if extended:
@@ -1716,6 +1717,8 @@ def get_isotp_packet(identifier=0x0, extended=False):
     else:
         pkt = ISOTPHeader() / ISOTP_FF()
         pkt.data = b'\x00\x00\x00\x00\x00\x00'
+    if extended_can_id:
+        pkt.flags = "extended"
 
     pkt.identifier = identifier
     pkt.message_size = 100
@@ -1776,7 +1779,7 @@ def get_isotp_fc(id_value, id_list, noise_ids, extended, packet,
     and not in noise_ids
     append it to id_list
     """
-    if packet.flags:
+    if packet.flags and packet.flags != "extended":
         return
 
     if noise_ids is not None and packet.identifier in noise_ids:
@@ -1805,7 +1808,7 @@ def get_isotp_fc(id_value, id_list, noise_ids, extended, packet,
 
 
 def scan(sock, scan_range=range(0x800), noise_ids=None, sniff_time=0.1,
-         verbose=False):
+         extended_can_id=False, verbose=False):
     """Scan and return dictionary of detections
 
     Args:
@@ -1816,6 +1819,7 @@ def scan(sock, scan_range=range(0x800), noise_ids=None, sniff_time=0.1,
                        received during scan
             sniff_time: time the scan waits for isotp flow control responses
                         after sending a first frame
+            extended_can_id: Send extended can frames
             verbose: displays information during scan
 
     ISOTP-Scan - NO extended IDs
@@ -1829,13 +1833,14 @@ def scan(sock, scan_range=range(0x800), noise_ids=None, sniff_time=0.1,
                                                 verbose),
                    timeout=sniff_time,
                    started_callback=lambda: sock.send(
-                       get_isotp_packet(value)))
+                       get_isotp_packet(value, False, extended_can_id)))
     return return_values
 
 
 def scan_extended(sock, scan_range=range(0x800), scan_block_size=100,
-                  noise_ids=None, sniff_time=0.1, verbose=False):
-    """Scan with extended addresses and return dictionary of detections
+                  noise_ids=None, sniff_time=0.1, extended_can_id=False,
+                  verbose=False):
+    """Scan with ISOTP extended addresses and return dictionary of detections
 
     Args:
             sock: socket for can interface
@@ -1846,6 +1851,7 @@ def scan_extended(sock, scan_range=range(0x800), scan_block_size=100,
                        received during scan
             sniff_time: time the scan waits for isotp flow control responses
                         after sending a first frame
+            extended_can_id: Send extended can frames
             verbose: displays information during scan
 
     If an answer-packet found -> slow scan with
@@ -1858,15 +1864,16 @@ def scan_extended(sock, scan_range=range(0x800), scan_block_size=100,
     scan_block_size = scan_block_size or 1
 
     for value in scan_range:
-        pkt = get_isotp_packet(value, extended=True)
+        pkt = get_isotp_packet(value, extended=True,
+                               extended_can_id=extended_can_id)
         id_list = []
 
-        for extended_id in range(0, 256, scan_block_size):
-            sock.sniff(prn=lambda p: get_isotp_fc(extended_id, id_list,
+        for ext_isotp_id in range(0, 256, scan_block_size):
+            sock.sniff(prn=lambda p: get_isotp_fc(ext_isotp_id, id_list,
                                                   noise_ids, True, p,
                                                   verbose),
                        timeout=sniff_time * 3,
-                       started_callback=send_multiple_ext(sock, extended_id,
+                       started_callback=send_multiple_ext(sock, ext_isotp_id,
                                                           pkt,
                                                           scan_block_size))
             # sleep to prevent flooding
@@ -1874,9 +1881,9 @@ def scan_extended(sock, scan_range=range(0x800), scan_block_size=100,
 
         # remove duplicate IDs
         id_list = list(set(id_list))
-        for extended_id in id_list:
-            for ext_id in range(extended_id, min(extended_id +
-                                                 scan_block_size, 256)):
+        for ext_isotp_id in id_list:
+            for ext_id in range(ext_isotp_id, min(ext_isotp_id +
+                                                  scan_block_size, 256)):
                 pkt.extended_address = ext_id
                 full_id = (value << 8) + ext_id
                 sock.sniff(prn=lambda pkt: get_isotp_fc(full_id,
@@ -1893,6 +1900,7 @@ def ISOTPScan(sock, scan_range=range(0x7ff + 1), extended_addressing=False,
               sniff_time=0.1,
               output_format=None,
               can_interface="can0",
+              extended_can_id=False,
               verbose=False):
 
     """Scan for ISOTP Sockets on a bus and return findings
@@ -1910,6 +1918,7 @@ def ISOTPScan(sock, scan_range=range(0x7ff + 1), extended_addressing=False,
                        results (text, code or sockets). Provide a string
                        e.g. "text". Default is "socket".
         can_interface: interface used to create the returned code/sockets
+        extended_can_id: Use Extended CAN-Frames
         verbose: displays information during scan
 
     Scan for ISOTP Sockets in the defined range and returns found sockets
@@ -1925,6 +1934,7 @@ def ISOTPScan(sock, scan_range=range(0x7ff + 1), extended_addressing=False,
         print("Filtering background noise...")
 
     # Send dummy packet. In most cases, this triggers activity on the bus.
+
     dummy_pkt = CAN(identifier=0x123,
                     data=b'\xaa\xbb\xcc\xdd\xee\xff\xaa\xbb')
 
@@ -1936,34 +1946,41 @@ def ISOTPScan(sock, scan_range=range(0x7ff + 1), extended_addressing=False,
 
     if extended_addressing:
         found_packets = scan_extended(sock, scan_range, noise_ids=noise_ids,
-                                      sniff_time=sniff_time, verbose=verbose)
+                                      sniff_time=sniff_time,
+                                      extended_can_id=extended_can_id,
+                                      verbose=verbose)
     else:
         found_packets = scan(sock, scan_range, noise_ids=noise_ids,
-                             sniff_time=sniff_time, verbose=verbose)
+                             sniff_time=sniff_time,
+                             extended_can_id=extended_can_id,
+                             verbose=verbose)
 
     filter_periodic_packets(found_packets, verbose)
 
     if output_format == "text":
-        return generate_text_output(found_packets)
+        return generate_text_output(found_packets, extended_addressing)
     if output_format == "code":
-        return generate_code_output(found_packets, can_interface)
-    return generate_isotp_list(found_packets, can_interface)
+        return generate_code_output(found_packets, can_interface,
+                                    extended_addressing)
+    return generate_isotp_list(found_packets, can_interface,
+                               extended_addressing)
 
 
-def generate_text_output(found_packets):
+def generate_text_output(found_packets, extended_addressing=False):
     """Generate a human readable output from the result of the `scan` or
         the `scan_extended` function.
 
         Args:
                 found_packets: result of the `scan` or `scan_extended` function
+                extended_addressing: print results from a scan with ISOTP
+                                     extended addressing
     """
     if not found_packets:
         return "No packets found."
 
     text = "\nFound %s ISOTP-FlowControl Packet(s):" % len(found_packets)
     for pack in found_packets:
-        extended_id = pack > 0x7ff
-        if extended_id:
+        if extended_addressing:
             send_id = pack // 256
             send_ext = pack - (send_id * 256)
             ext_id = hex(orb(found_packets[pack][0].data[0]))
@@ -1993,7 +2010,8 @@ def generate_text_output(found_packets):
     return text
 
 
-def generate_code_output(found_packets, can_interface):
+def generate_code_output(found_packets, can_interface,
+                         extended_addressing=False):
     """Generate a copy&past-able output from the result of the `scan` or
         the `scan_extended` function.
 
@@ -2001,6 +2019,8 @@ def generate_code_output(found_packets, can_interface):
                 found_packets: result of the `scan` or `scan_extended` function
                 can_interface: description string for a CAN interface to be
                                 used for the creation of the output.
+                extended_addressing: print results from a scan with ISOTP
+                                     extended addressing
     """
     result = ""
     if not found_packets:
@@ -2012,8 +2032,7 @@ def generate_code_output(found_packets, can_interface):
              "load_contrib('isotp')\n\n" % PYTHON_CAN
 
     for pack in found_packets:
-        extended_id = pack > 0x7ff
-        if extended_id:
+        if extended_addressing:
             send_id = pack // 256
             send_ext = pack - (send_id * 256)
             ext_id = orb(found_packets[pack][0].data[0])
@@ -2035,7 +2054,8 @@ def generate_code_output(found_packets, can_interface):
     return header + result
 
 
-def generate_isotp_list(found_packets, can_interface):
+def generate_isotp_list(found_packets, can_interface,
+                        extended_addressing=False):
     """Generate a list of ISOTPSocket objects from the result of the `scan` or
         the `scan_extended` function.
 
@@ -2043,16 +2063,17 @@ def generate_isotp_list(found_packets, can_interface):
             found_packets: result of the `scan` or `scan_extended` function
             can_interface: description string for a CAN interface to be
                             used for the creation of the output.
+            extended_addressing: print results from a scan with ISOTP
+                                     extended addressing
     """
     socket_list = []
     for pack in found_packets:
-        extended_id = pack > 0x7ff
         pkt = found_packets[pack][0]
 
         dest_id = pkt.identifier
         pad = True if pkt.length == 8 else False
 
-        if extended_id:
+        if extended_addressing:
             source_id = pack >> 8
             source_ext = int(pack - (source_id * 256))
             dest_ext = orb(pkt.data[0])
diff --git a/scapy/tools/automotive/isotpscanner.py b/scapy/tools/automotive/isotpscanner.py
--- a/scapy/tools/automotive/isotpscanner.py
+++ b/scapy/tools/automotive/isotpscanner.py
@@ -53,8 +53,11 @@ def usage():
                           Duration in milliseconds a sniff is waiting for a
                           flow-control response.
     -x, --extended        Scan with ISOTP extended addressing.
+                          This has nothing to do with extended CAN identifiers
     -C, --piso            Print 'Copy&Paste'-ready ISOTPSockets.
     -v, --verbose         Display information during scan.\n
+    -w, --wide            Enable scanning spaces greater than 0x800
+        --extended_can_id Use extended CAN identifiers
     Example of use:\n
     Python2 or Windows:
     python2 -m scapy.tools.automotive.isotpscanner --interface=pcan --channel=PCAN_USBBUS1 --bitrate=250000 --start 0 --end 100
@@ -69,6 +72,8 @@ def main():
     extended = False
     piso = False
     verbose = False
+    wide_option = False
+    extended_can_id = False
     sniff_time = 100
     noise_listen_time = 2
     start = None
@@ -79,9 +84,10 @@ def main():
 
     options = getopt.getopt(
         sys.argv[1:],
-        'vxCt:n:i:c:b:s:e:h',
+        'vxCt:n:i:c:b:s:e:h:w',
         ['verbose', 'noise_listen_time=', 'sniff_time=', 'interface=', 'piso',
-         'channel=', 'bitrate=', 'start=', 'end=', 'help', 'extended'])
+         'channel=', 'bitrate=', 'start=', 'end=', 'help', 'extended', 'wide',
+         'extended_can_id'])
 
     try:
         for opt, arg in options[0]:
@@ -108,6 +114,10 @@ def main():
                 start = int(arg, 16)
             elif opt in ('-e', '--end'):
                 end = int(arg, 16)
+            elif opt in ('-w', '--wide'):
+                wide_option = True
+            elif opt in '--extended_can_id':
+                extended_can_id = True
     except getopt.GetoptError as msg:
         usage()
         print("ERROR:", msg, file=sys.stderr)
@@ -121,8 +131,19 @@ def main():
         print("\nPlease provide all required arguments.\n", file=sys.stderr)
         sys.exit(-1)
 
-    if end >= 0x800:
-        print("end must be < 0x800.", file=sys.stderr)
+    if end >= 2**29:
+        print("end must be < " + hex(2**29), file=sys.stderr)
+        sys.exit(-1)
+    elif not extended_can_id and end >= 0x800:
+        print("Standard IDs must be < 0x800.\n"
+              "Use --extended_can_id option for extended CAN range.",
+              file=sys.stderr)
+        sys.exit(-1)
+
+    if end - start > 0x800 and not wide_option:
+        print("Scanning big address spaces takes a lot of time.\n"
+              "Please use --wide option in order to scan identifiers > 0x800",
+              file=sys.stderr)
         sys.exit(-1)
 
     if end < start:
@@ -170,6 +191,7 @@ def main():
                        sniff_time=float(sniff_time) / 1000,
                        output_format="code" if piso else "text",
                        can_interface=interface_string,
+                       extended_can_id=extended_can_id,
                        verbose=verbose)
 
     print("Scan: \n%s" % result)


Test patch:
diff --git a/test/contrib/isotp.uts b/test/contrib/isotp.uts
--- a/test/contrib/isotp.uts
+++ b/test/contrib/isotp.uts
@@ -14,6 +14,7 @@ import threading, time, six, subprocess
 from six.moves.queue import Queue
 from subprocess import call
 from io import BytesIO
+from scapy.contrib.isotp import get_isotp_packet
 
 
 
@@ -353,6 +354,17 @@ assert(p.type == 0)
 assert(p.message_size == 4)
 assert(p.data == b'abcd')
 
+= Construct ISOTP_packet with extended can frame
+p = get_isotp_packet(identifier=0x1234, extended=False, extended_can_id=True)
+print(p)
+assert (p.identifier == 0x1234)
+assert (p.flags == "extended")
+
+= Construct ISOTPEA_Packet with extended can frame
+p = get_isotp_packet(identifier=0x1234, extended=True, extended_can_id=True)
+print(p)
+assert (p.identifier == 0x1234)
+assert (p.flags == "extended")
 
 + ISOTP fragment and defragment checks
 
diff --git a/test/contrib/isotpscan.uts b/test/contrib/isotpscan.uts
--- a/test/contrib/isotpscan.uts
+++ b/test/contrib/isotpscan.uts
@@ -251,6 +251,46 @@ assert "0x702" in result
 assert "0x703" in result
 assert "No Padding" in result
 
+= Test ISOTPScan(output_format=text) extended_can_id
+done = False
+
+drain_bus(iface0)
+def isotpserver(i):
+    with ISOTPSocket(new_can_socket0(), sid=0x1ffff700+i, did=0x1ffff600+i) as isotpsock1:
+        isotpsock1.sniff(timeout=100 ,count=1)
+
+def make_noise(pkt):
+    global done
+    while not done:
+        new_can_socket0().send(pkt)
+        time.sleep(0.1)
+
+pkt = CAN(identifier=0x1ffff701, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')
+thread_noise = threading.Thread(target=make_noise, args=(pkt))
+thread_noise.start()
+
+thread1 = threading.Thread(target=isotpserver, args=[2])
+thread2 = threading.Thread(target=isotpserver, args=[3])
+thread1.start()
+thread2.start()
+time.sleep(0.1)
+result = ISOTPScan(new_can_socket0(), range(0x1ffff5ff, 0x1ffff604+1), output_format="text", noise_listen_time=1, sniff_time=0.1, extended_can_id=True)
+assert 0 == subprocess.call(['cansend', 'vcan0', '1ffff601#01aa'])
+assert 0 == subprocess.call(['cansend', 'vcan0', '1ffff602#01aa'])
+assert 0 == subprocess.call(['cansend', 'vcan0', '1ffff603#01aa'])
+thread1.join()
+thread2.join()
+done = True
+thread_noise.join()
+print(result)
+text = "\nFound 2 ISOTP-FlowControl Packet(s):"
+assert text in result
+assert "0x1ffff602" in result
+assert "0x1ffff603" in result
+assert "0x1ffff702" in result
+assert "0x1ffff703" in result
+assert "No Padding" in result
+
 
 = Test ISOTPScan(output_format=code)
 done = False
@@ -330,6 +370,44 @@ assert s1 in result
 assert s2 in result
 
 
+= Test extended ISOTPScan(output_format=code) extended_can_id
+drain_bus(iface0)
+
+def isotpserver(i):
+    with ISOTPSocket(new_can_socket0(), sid=0x1ffff700+i, did=0x1ffff600+i, extended_addr=0x11, extended_rx_addr=0x22) as s:
+        s.sniff(timeout=100 ,count=1)
+
+def make_noise(pkt):
+    s = new_can_socket0()
+    for _ in range(20):
+        s.send(pkt)
+        time.sleep(0.1)
+
+pkt = CAN(identifier=0x1ffff701, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')
+thread_noise = threading.Thread(target=make_noise, args=pkt)
+thread_noise.start()
+
+thread1 = threading.Thread(target=isotpserver, args=[2])
+thread2 = threading.Thread(target=isotpserver, args=[3])
+thread1.start()
+thread2.start()
+time.sleep(0.1)
+
+result = ISOTPScan(new_can_socket0(), range(0x1ffff5ff, 0x1ffff604+1), extended_can_id=True, extended_addressing=True, sniff_time=0.1, noise_listen_time=1, output_format="code")
+
+assert 0 == subprocess.call(['cansend', 'vcan0', '1ffff602#2201aa'])
+assert 0 == subprocess.call(['cansend', 'vcan0', '1ffff603#2201aa'])
+thread1.join()
+thread2.join()
+thread_noise.join()
+
+s1 = "ISOTPSocket(can0, sid=0x1ffff602, did=0x1ffff702, padding=False, extended_addr=0x22, extended_rx_addr=0x11, basecls=ISOTP)"
+s2 = "ISOTPSocket(can0, sid=0x1ffff603, did=0x1ffff703, padding=False, extended_addr=0x22, extended_rx_addr=0x11, basecls=ISOTP)"
+print(result)
+assert s1 in result
+assert s2 in result
+
+
 = Test ISOTPScan(output_format=None)
 drain_bus(iface0)
 def isotpserver(i):