esp32_bluetooth_classic_sni.../libs/scapy/contrib/automotive/ecu.py
Matheus Eduardo Garbelini 86890704fd initial commit
todo: add documentation & wireshark dissector
2021-08-31 19:51:03 +08:00

343 lines
15 KiB
Python
Executable file

#! /usr/bin/env python
# This file is part of Scapy
# See http://www.secdev.org/projects/scapy for more information
# Copyright (C) Nils Weiss <nils@we155.de>
# This program is published under a GPLv2 license
# scapy.contrib.description = Helper class for tracking ECU states (ECU)
# scapy.contrib.status = loads
import time
import random
from collections import defaultdict
from scapy.packet import Raw, Packet
from scapy.plist import PacketList
from scapy.error import Scapy_Exception
from scapy.sessions import DefaultSession
from scapy.ansmachine import AnsweringMachine
__all__ = ["ECU", "ECUResponse", "ECUSession", "ECU_am"]
class ECU(object):
"""A ECU object can be used to
- track the states of an ECU.
- to log all modification to an ECU
- to extract supported responses of a real ECU
Usage:
>>> print("This ecu logs, tracks and creates supported responses")
>>> my_virtual_ecu = ECU()
>>> my_virtual_ecu.update(PacketList([...]))
>>> my_virtual_ecu.supported_responses
>>> print("Another ecu just tracks")
>>> my_tracking_ecu = ECU(logging=False, store_supported_responses=False) # noqa: E501
>>> my_tracking_ecu.update(PacketList([...]))
>>> print("Another ecu just logs all modifications to it")
>>> my_logging_ecu = ECU(verbose=False, store_supported_responses=False) # noqa: E501
>>> my_logging_ecu.update(PacketList([...]))
>>> my_logging_ecu.log
>>> print("Another ecu just creates supported responses")
>>> my_response_ecu = ECU(verbose=False, logging=False)
>>> my_response_ecu.update(PacketList([...]))
>>> my_response_ecu.supported_responses
"""
def __init__(self, init_session=None, init_security_level=None,
init_communication_control=None, logging=True, verbose=True,
store_supported_responses=True):
"""
Initialize an ECU object
:param init_session: An initial session
:param init_security_level: An initial security level
:param init_communication_control: An initial communication control
setting
:param logging: Turn logging on or off. Default is on.
:param verbose: Turn tracking on or off. Default is on.
:param store_supported_responses: Turn creation of supported responses
on or off. Default is on.
"""
self.current_session = init_session or 1
self.current_security_level = init_security_level or 0
self.communication_control = init_communication_control or 0
self.verbose = verbose
self.logging = logging
self.store_supported_responses = store_supported_responses
self.log = defaultdict(list)
self._supported_responses = list()
self._unanswered_packets = PacketList()
def reset(self):
self.current_session = 1
self.current_security_level = 0
self.communication_control = 0
def update(self, p):
if isinstance(p, PacketList):
for pkt in p:
self._update(pkt)
elif not isinstance(p, Packet):
raise Scapy_Exception("Provide a Packet object for an update")
else:
self._update(p)
def _update(self, pkt):
if self.verbose:
print(repr(self), repr(pkt))
if self.store_supported_responses:
self._update_supported_responses(pkt)
if self.logging:
self._update_log(pkt)
self._update_internal_state(pkt)
def _update_log(self, pkt):
for l in pkt.layers():
if hasattr(l, "get_log"):
log_key, log_value = l.get_log(pkt)
self.log[log_key].append((pkt.time, log_value))
def _update_internal_state(self, pkt):
for l in pkt.layers():
if hasattr(l, "modifies_ecu_state"):
l.modifies_ecu_state(pkt, self)
def _update_supported_responses(self, pkt):
self._unanswered_packets += PacketList([pkt])
answered, unanswered = self._unanswered_packets.sr()
for _, resp in answered:
ecu_resp = ECUResponse(session=self.current_session,
security_level=self.current_security_level,
responses=resp)
if ecu_resp not in self._supported_responses:
if self.verbose:
print("[+] ", repr(ecu_resp))
self._supported_responses.append(ecu_resp)
else:
if self.verbose:
print("[-] ", repr(ecu_resp))
self._unanswered_packets = unanswered
@property
def supported_responses(self):
# This sorts responses in the following order:
# 1. Positive responses first
# 2. Lower ServiceID first
# 3. Longer (more specific) responses first
self._supported_responses.sort(
key=lambda x: (x.responses[0].service == 0x7f,
x.responses[0].service,
0xffffffff - len(x.responses[0])))
return self._supported_responses
@property
def unanswered_packets(self):
return self._unanswered_packets
def __repr__(self):
return "ses: %03d sec: %03d cc: %d" % (self.current_session,
self.current_security_level,
self.communication_control)
class ECUSession(DefaultSession):
"""Tracks modification to an ECU 'on-the-flow'.
Usage:
>>> sniff(session=ECUSession)
"""
def __init__(self, *args, **kwargs):
DefaultSession.__init__(self, *args, **kwargs)
self.ecu = ECU(init_session=kwargs.pop("init_session", None),
init_security_level=kwargs.pop("init_security_level", None), # noqa: E501
init_communication_control=kwargs.pop("init_communication_control", None), # noqa: E501
logging=kwargs.pop("logging", True),
verbose=kwargs.pop("verbose", True),
store_supported_responses=kwargs.pop("store_supported_responses", True)) # noqa: E501
def on_packet_received(self, pkt):
if not pkt:
return
if isinstance(pkt, list):
for p in pkt:
ECUSession.on_packet_received(self, p)
return
self.ecu.update(pkt)
DefaultSession.on_packet_received(self, pkt)
class ECUResponse:
"""Encapsulates a response and the according ECU state.
A list of this objects can be used to configure a ECU Answering Machine.
This is useful, if you want to clone the behaviour of a real ECU on a bus.
Usage:
>>> print("Generates a ECUResponse which answers on UDS()/UDS_RDBI(identifiers=[2]) if ECU is in session 2 and has security_level 2") # noqa: E501
>>> ECUResponse(session=2, security_level=2, responses=UDS()/UDS_RDBIPR(dataIdentifier=2)/Raw(b"deadbeef1")) # noqa: E501
>>> print("Further examples")
>>> ECUResponse(session=range(3,5), security_level=[3,4], responses=UDS()/UDS_RDBIPR(dataIdentifier=3)/Raw(b"deadbeef2")) # noqa: E501
>>> ECUResponse(session=[5,6,7], security_level=range(5,7), responses=UDS()/UDS_RDBIPR(dataIdentifier=5)/Raw(b"deadbeef3")) # noqa: E501
>>> ECUResponse(session=lambda x: 8 < x <= 10, security_level=lambda x: x > 10, responses=UDS()/UDS_RDBIPR(dataIdentifier=9)/Raw(b"deadbeef4")) # noqa: E501
"""
def __init__(self, session=1, security_level=0,
responses=Raw(b"\x7f\x10"),
answers=None):
"""
Initialize an ECUResponse capsule
:param session: Defines the session in which this response is valid.
A integer, a callable or any iterable object can be
provided.
:param security_level: Defines the security_level in which this
response is valid. A integer, a callable or any
iterable object can be provided.
:param responses: A Packet or a list of Packet objects. By default the
last packet is asked if it answers a incoming packet.
This allows to send for example
`requestCorrectlyReceived-ResponsePending` packets.
:param answers: Optional argument to provide a custom answer here:
`lambda resp, req: return resp.answers(req)`
This allows the modification of a response depending
on a request. Custom SecurityAccess mechanisms can
be implemented in this way or generic NegativeResponse
messages which answers to everything can be realized
in this way.
"""
self.__session = session \
if hasattr(session, "__iter__") or callable(session) else [session]
self.__security_level = security_level \
if hasattr(security_level, "__iter__") or callable(security_level)\
else [security_level]
if isinstance(responses, PacketList):
self.responses = responses
elif isinstance(responses, Packet):
self.responses = PacketList([responses])
elif hasattr(responses, "__iter__"):
self.responses = PacketList(responses)
else:
self.responses = PacketList([responses])
self.__custom_answers = answers
def in_correct_session(self, current_session):
if callable(self.__session):
return self.__session(current_session)
else:
return current_session in self.__session
def has_security_access(self, current_security_level):
if callable(self.__security_level):
return self.__security_level(current_security_level)
else:
return current_security_level in self.__security_level
def answers(self, other):
if self.__custom_answers is not None:
return self.__custom_answers(self.responses[-1], other)
else:
return self.responses[-1].answers(other)
def __repr__(self):
return "session=%s, security_level=%s, responses=%s" % \
(self.__session, self.__security_level,
[resp.summary() for resp in self.responses])
def __eq__(self, other):
return \
self.__class__ == other.__class__ and \
self.__session == other.__session and \
self.__security_level == other.__security_level and \
len(self.responses) == len(other.responses) and \
all(bytes(x) == bytes(y) for x, y in zip(self.responses,
other.responses))
def __ne__(self, other):
# Python 2.7 compat
return not self == other
__hash__ = None
class ECU_am(AnsweringMachine):
"""AnsweringMachine which emulates the basic behaviour of a real world ECU.
Provide a list of ``ECUResponse`` objects to configure the behaviour of this
AnsweringMachine.
:param supported_responses: List of ``ECUResponse`` objects to define
the behaviour. The default response is
``generalReject``.
:param main_socket: Defines the object of the socket to send
and receive packets.
:param broadcast_socket: Defines the object of the broadcast socket.
Listen-only, responds with the main_socket.
`None` to disable broadcast capabilities.
:param basecls: Provide a basecls of the used protocol
Usage:
>>> resp = ECUResponse(session=range(0,255), security_level=0, responses=UDS() / UDS_NR(negativeResponseCode=0x7f, requestServiceId=0x10)) # noqa: E501
>>> sock = ISOTPSocket(can_iface, sid=0x700, did=0x600, basecls=UDS) # noqa: E501
>>> answering_machine = ECU_am(supported_responses=[resp], main_socket=sock, basecls=UDS) # noqa: E501
>>> sim = threading.Thread(target=answering_machine, kwargs={'count': 4, 'timeout':5}) # noqa: E501
>>> sim.start()
"""
function_name = "ECU_am"
sniff_options_list = ["store", "opened_socket", "count", "filter", "prn", "stop_filter", "timeout"] # noqa: E501
def parse_options(self, supported_responses=None,
main_socket=None, broadcast_socket=None, basecls=Raw,
timeout=None):
self.main_socket = main_socket
self.sockets = [self.main_socket]
if broadcast_socket is not None:
self.sockets.append(broadcast_socket)
self.ecu_state = ECU(logging=False, verbose=False,
store_supported_responses=False)
self.basecls = basecls
self.supported_responses = supported_responses
self.sniff_options["timeout"] = timeout
self.sniff_options["opened_socket"] = self.sockets
def is_request(self, req):
return req.__class__ == self.basecls
def print_reply(self, req, reply):
print("%s ==> %s" % (req.summary(), [res.summary() for res in reply]))
def make_reply(self, req):
if self.supported_responses is not None:
for resp in self.supported_responses:
if not isinstance(resp, ECUResponse):
raise Scapy_Exception("Unsupported type for response. "
"Please use `ECUResponse` objects. ")
if not resp.in_correct_session(self.ecu_state.current_session):
continue
if not resp.has_security_access(
self.ecu_state.current_security_level):
continue
if not resp.answers(req):
continue
for r in resp.responses:
for l in r.layers():
if hasattr(l, "modifies_ecu_state"):
l.modifies_ecu_state(r, self.ecu_state)
return resp.responses
return PacketList([self.basecls(b"\x7f" + bytes(req)[0:1] + b"\x10")])
def send_reply(self, reply):
for p in reply:
if len(reply) > 1:
time.sleep(random.uniform(0.01, 0.5))
self.main_socket.send(p)