How to fuzz SOME/IP protocol - How did I get my name on the BMW Hall of Fame (Part 1)


2023-03-02





In 2022, as part of the BMW Private Bug Bounty program, I reported several security vulnerabilities in applications using the SOME/IP protocol in the head units of BMW vehicle called MGU22. As a result, I was honored to be included in the BMW Recognition of Security Exports by the BMW automotive group. In this article, I’ll introduce you to the SOME/IP protocol and share how I fuzzed SOME/IP applications to find vulnerabilities.


What is SOME/IP Protocol?

Traditionally, the main communication method used in in-vehicle networks in the automotive industry has been signal-based CAN communication. CAN remains a widely used protocol within vehicles even today. However, in recent times, alongside CAN communication, there is a growing trend toward the use of Ethernet-based communication methods. One of the Ethernet-based in-vehicle protocols in the AUTOSAR standard is SOME/IP (Scalable Service-Oriented MiddlewarE over IP), which is a service-oriented protocol. BMW Group uses the vsomeip open-source implementation of the SOME/IP protocol.

As briefly mentioned in my previous post, Tesla also has vsomeip installed in its systems, but it is unclear whether Tesla uses the SOME/IP protocol in its vehicles.


The figure below is a SOME/IP-based communication scheme for an in-vehicle network.

Diagnostic Cable for Tesla ToolBox

SOME/IP messages can use either TCP or UDP protocols. Additionally, service interfaces are distinguished by port number, so multiple instances of the same interface cannot be provided on the same port.

Messaging types are supported in four ways, as shown in the figure below.

Diagnostic Cable for Tesla ToolBox

Diagnostic Cable for Tesla ToolBox

Diagnostic Cable for Tesla ToolBox

Diagnostic Cable for Tesla ToolBox

The difference between Request/Response and Fire & Forget is whether there is a response message. Events is a messaging type where the server calls back when an event occurs.

For more information about subscribe, see the SOME/IP-SD (SOME/IP Service Discovery) protocol. We will explain it in more detail in part 2, but let’s first learn about the SOME/IP packet structure.


SOME/IP Packet Structure

First, in order to send a normal SOME/IP message, you need to know the SOME/IP header structure. The figure below is the SOME/IP header format taken from the standard document.

Diagnostic Cable for Tesla ToolBox

Now, let’s take a look at each field of the SOME/IP header structure.

  • Message ID
    • Each service has a unique Service ID.
    • Each service can have multiple methods, and each method has its own ID.

Diagnostic Cable for Tesla ToolBox

  • Length
    • The length from the Request ID to the end of the payload.
  • Request ID
    • The Client ID allows an ECU to differentiate calls from multiple clients to the same method.
    • The Session ID allows to distinguish sequential messages or requests originating from the same sender from each other.

Diagnostic Cable for Tesla ToolBox

  • Protocol Version
    • It usually has a fixed value.
  • Interface Version
    • It usually has a fixed value.
  • Message Type The Message Type field is different depending on the four messaging types mentioned above. For example, in the case of event messages, the message type is 0x2 notification, and the sender does not expect a response.

Diagnostic Cable for Tesla ToolBox

  • Return Code A normal message generally has a return code of 0x00 (E_OK).

Diagnostic Cable for Tesla ToolBox

Diagnostic Cable for Tesla ToolBox

Let’s take an example. Most recent head units have Bluetooth functionality, which allows smartphones to connect to vehicles. In this case, let’s assume that there is a service called DeviceManager that manages devices connected to the vehicle. DeviceManager can have a variety of methods, such as connecting the vehicle to the passenger’s smartphone, checking the connection status, switching the connection to another device, and disconnecting the device. Additionally, it is possible that other controllers in the vehicle may want to receive device connection status in event format, as well as request/response format data requests to the head unit. In this way, a service can often be composed of a variety of methods and support a variety of messaging types.


Payload Structure

SOME/IP payloads are serialized before transmission, and the receiving side receives the data through the deserialization process. Therefore, random fuzzing, which sends random bytes to the payload, is unlikely to deliver the desired data to the application layer.

Diagnostic Cable for Tesla ToolBox


In order to perform structured fuzzing, you need to understand the variable types and serialization of the payload. In the actual case of BMW’s MGU22, it uses SOME/IP related libraries of COVESA, and the implementation of each data type is in the code of the following link.


Here is a table showing some of the data types that make up the payload.

Data Type Byte Count
UInt8 1
UInt16 2
UInt32 4
UInt64 8
Bool 1
Enum 1, 2
Utf8 Depends on the length of the string
Utf16 Depends on the length of the string
Array Depends on the length of the string
Map Depends on the length of the string
Struct 0

The following examples are code implemented in Python for each data type, and the return value is a hex string.

def _uint8(p):
    return "{:02X}".format(p)

def _uint16(p):
    return "{:04X}".format(p)

def _uint32(p):
    return "{:08X}".format(p)

def _uint64(p):
    return "{:016X}".format(p)

def _bool(p):
    return "{:02X}".format(p)

def _enum(p, w=False):
    if w:
        return "{:04X}".format(p)
    else:
        return "{:02X}".format(p)

def _utf8(p):
    data = "EFBBBF"
    data += p.encode("utf-8").hex()
    data += "00"
    return "{:08X}".format(len(data)//2) + data
        
def _utf16(p):
    data = "FEFF"
    data += p.encode("utf-16-be").hex()
    data += "0000"
    return "{:08X}".format(len(data)//2) + data

def _array(p):
    return "{:08X}".format(len(p)//2) + p

def _map(k, v):
    data = k + v
    return "{:08X}".format(len(data)//2) + data

For example, the UTF-16 character sequence “TEST” can be represented as follows.

print(_utf16("TEST"))

# the length of a string 0000000C
# actual string content "FEFF" + "0054004500530054" + "0000"
# return value 0000000CFEFF00540045005300540000

Also, the payload is composed of serialized data of multiple types Serialized data is simply a sequence of data types. If a data type requires a length, the length is prefixed to the data. For example, let’s consider the following data types serialized

Struct
{
  UInt32,
  Array
  {
    Struct
    {
      Utf8,
      Utf8
    },
  },
  Map
  {
    Enum,
    Utf8
  },
  Bool
}

The following data types are serialized in the following order: Struct - UInt32 - Array - Struct - Utf8 - Utf8 - Map - Enum - Utf8 - Bool.

Struct is ignored in the serialization process. There are three main data types inside Struct: Array, Map, and Bool.

The Array contains Struct, Utf8, and Utf8 types. Here, Struct is also ignored, and in Python code, it becomes _array(_utf8("str1") + _utf8("str2")).

In the case of Map, it can also be represented as _map(_enum(data1), _utf8("str3")).

Therefore, the entire payload can be represented in code as follows.

payload =  _uint32(data1)
payload += _array(_utf8("str1") + _utf8("str2"))
payload += _map(_enum(data2) + _utf8("str3"))
payload += _bool(data3)

In conclusion, the code to send a serialized SOME/IP message is as follows

import socket 
import datetime


class SomeIP:
    def __init__(self, service_id, method_id, client_id, session_id, protocol_version, interface_version, message_type, return_code):
        self.service_id = service_id    
        self.method_id = method_id
        self.client_id = client_id
        self.session_id = session_id
        self.protocol_version = protocol_version
        self.interface_version = interface_version
        self.message_type = message_type
        self.return_code = return_code
        
    def assemble_packet(self, payload):
        self.header =  "{:04X}".format(self.service_id)
        self.header += "{:04X}".format(self.method_id) 
        self.header += "{:08X}".format(len(payload) + 8)
        self.header += "{:04X}".format(self.client_id)
        self.header += "{:04X}".format(self.session_id) 
        self.header += "{:02X}".format(self.protocol_version)
        self.header += "{:02X}".format(self.interface_version)
        self.header += "{:02X}".format(self.message_type)
        self.header += "{:02X}".format(self.return_code)
        self.packet = bytes.fromhex(self.header) + payload
    

def main():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.connect(("160.48.199.99", 32504))
    sock.settimeout(0.1)

    def _uint8(p):
        return "{:02X}".format(p)
    def _uint16(p):
        return "{:04X}".format(p)
    def _uint32(p):
        return "{:08X}".format(p)
    def _uint64(p):
        return "{:016X}".format(p)
    def _bool(p):
        return "{:02X}".format(p)
    def _enum(p, w=False):
        if w:
            return "{:04X}".format(p)
        else:
            return "{:02X}".format(p)

    def _utf8(p):
        data = "EFBBBF"
        data += p.encode("utf-8").hex()
        data += "00"
        return "{:08X}".format(len(data)//2) + data
            
    def _utf16(p):
        data = "FEFF"
        data += p.encode("utf-16-be").hex()
        data += "0000"
        return "{:08X}".format(len(data)//2) + data

    def _array(p):
        return "{:08X}".format(len(p)//2) + p

    def _map(k, v):
        data = k + v
        return "{:08X}".format(len(data)//2) + data

    def someip_func(data1, str1, str2, data2, str3, data3) -> SomeIP:
        someip = SomeIP(45246, 1, 0, 0, 1, 1, 0, 0)
        payload =  _uint32(data1)
        payload =  _array(_utf8(str1) + _utf8(str2))
        payload += _map(_enum(data2), _utf8(str3))
        payload += _bool(data3)
        someip.assemble_packet(bytes.fromhex(payload))
        return someip

    def send_and_recv(someip):
        try:
            sock.send(someip.packet)
            print(f"{datetime.datetime.now()}: {someip.packet.hex()}")
            res = sock.recv(1024)
            print(f"Recv: {res.hex()}")
        except socket.timeout:
            pass


    send_and_recv(someip_func(1, "aaa", "bbb", 2, "ccc", 3))


if __name__ == "__main__":
    main()

The code above is for sending a serialized message to the IP address 160.48.199.99 on port 32504. The service ID is 45246 and the method ID is 1. The messaging mode is request/response.

In this case, the session ID and client ID can be set to 0, and the protocol version is fixed to 1. The interface version usually has a value between 0 and 3.

The result of the above code is encoded as bytes and sent with the following values in hex.


b0be00010000003300000000010100000000001600000007efbbbf6161610000000007efbbbf626262000000000c0200000007efbbbf6363630003


We are now ready to send SOME/IP messages. However, we do not know the IP address and port to send the message to, the protocol type (TCP or UDP), the Service ID and Method ID, the message type (Request/Response or Event), or the data types that are serialized in the payload. If we fuzz by sending random data, the coverage will be very low. Therefore, we need to learn about SOME/IP-SD protocol to know each element.

SOME/IP-SD stands for SOME/IP Service Discovery Protocol. It is a message used to find service instances, detect whether service instances are running, and implement publish/subscribe processing.

In Part 2, we will learn about how the SOME/IP-SD works, connect a PC to the vehicle network, analyze SOME/IP-SD packet structure.




Reference

   vsomeip-in-10-minutes:    https://github.com/COVESA/vsomeip/wiki/vsomeip-in-10-minutes#prep