├── .gitignore ├── LICENSE.md ├── README.md ├── config.ini ├── data ├── someip_client_cf12fb22.pcapng ├── someip_client_offer_cf12fb22.pcapng ├── someip_client_request_cf12fb22.pcapng └── someip_fields.json ├── main.py ├── misc ├── flowchart.png ├── flowchart.xml ├── fuzzing.png ├── fuzzing.webm ├── notes.txt ├── send.py └── someip.py ├── requirements.txt └── someip_fuzzer ├── __init__.py ├── config.py ├── fuzzer.py ├── heartbeat.py ├── log.py ├── template.py └── types.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .venv 3 | **/__pycache__/ 4 | **/*.pyc 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2022 someip-protocol-fuzzer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # someip-protocol-fuzzer 2 | 3 | This repository features a proof-of-concept for a SOME/IP network protocol fuzzer. 4 | 5 | What it does is quite simple: It mutates user-defined protocol fields using radamsa. 6 | 7 | Implemented is a heartbeat mechanism which checks whether the target service on the other end is still responding. If not, fuzzing will be terminated. 8 | 9 | [![demo](https://raw.githubusercontent.com/cfanatic/someip-protocol-fuzzer/master/misc/fuzzing.png)](https://codefanatic.de/git/fuzzing.webm) 10 | 11 | ## Requirements 12 | 13 | It is recommended to use two VMs which run any version of GNU/Linux each. 14 | 15 | Furthermore, following software packages are required: 16 | 17 | | VM #1 - Target | VM #2 - Fuzzer | 18 | | ------------------ | ------------------ | 19 | | CMake 3.16.3 | Python 3.7.6 | 20 | | vsomeip 3.1.20 | scapy 2.4.5 | 21 | | boost 1.65.1 | radamsa 0.6 | 22 | 23 | ## Setup 24 | 25 | The instructions below describe how to configure the VMs for the target service and protocol fuzzer. The default configuration assumes that 192.168.0.18 is the IP address on VM #1, and 192.168.0.19 on VM #2. 26 | 27 | ### VM #1 - Target 28 | 29 | Clone [vsomeip](https://github.com/COVESA/vsomeip) and [vsomeip-fuzzing](https://github.com/cfanatic/vsomeip-fuzzing/tree/feature-demo-someip). Check out the branch `feature-demo-someip`, and follow the [setup instructions](https://github.com/cfanatic/vsomeip-fuzzing/tree/feature-demo-someip#setup). Call `make response` to build a SOME/IP service as the fuzzing target. 30 | 31 | Adjust the IP address in the [service configuration file](https://github.com/cfanatic/vsomeip-fuzzing/blob/feature-demo-someip/conf/vsomeip_response.json) accordingly. 32 | 33 | When you run `VSOMEIP_CONFIGURATION=../conf/vsomeip_response.json ./response`, the output must show something similar to: 34 | 35 | ```log 36 | 2022-02-18 14:59:28.396999 [info] Parsed vsomeip configuration in 0ms 37 | 2022-02-18 14:59:28.397593 [info] Using configuration file: "../conf/vsomeip_response.json". 38 | 2022-02-18 14:59:28.397751 [info] Initializing vsomeip application "!!SERVICE!!". 39 | 2022-02-18 14:59:28.398101 [info] Instantiating routing manager [Host]. 40 | 2022-02-18 14:59:28.398342 [info] create_local_server Routing endpoint at /tmp/vsomeip-0 41 | 2022-02-18 14:59:28.398740 [info] Service Discovery enabled. Trying to load module. 42 | 2022-02-18 14:59:28.400098 [info] Service Discovery module loaded. 43 | 2022-02-18 14:59:28.400516 [info] Application(!!SERVICE!!, 1212) is initialized (11, 100). 44 | 2022-02-18 14:59:28.400694 [info] Starting vsomeip application "!!SERVICE!!" (1212) 45 | 2022-02-18 14:59:28.401511 [info] main dispatch thread id from application: 1212 (!!SERVICE!!) 46 | 2022-02-18 14:59:28.403643 [info] io thread id from application: 1212 (!!SERVICE!!) 47 | 2022-02-18 14:59:28.402029 [info] shutdown thread id from application: 1212 (!!SERVICE!!) 48 | 2022-02-18 14:59:28.402721 [info] Watchdog is disabled! 49 | 2022-02-18 14:59:28.404079 [info] vSomeIP 3.1.20.3 | (default) 50 | 2022-02-18 14:59:28.403850 [info] OFFER(1212): [1234.5678:0.0] (true) 51 | 2022-02-18 14:59:28.404343 [info] Network interface "eth0" state changed: up 52 | 2022-02-18 14:59:28.406458 [info] Route "default route (0.0.0.0/0) if: eth0 gw: 192.168.0.1" 53 | 2022-02-18 14:59:28.407549 [debug] Joining to multicast group 224.224.224.245 from 192.168.0.18 54 | 2022-02-18 14:59:28.407767 [info] udp_server_endpoint_impl: SO_RCVBUF (Multicast) is: 212992 55 | 2022-02-18 14:59:28.411331 [info] SOME/IP routing ready. 56 | ``` 57 | 58 | ### VM #2 - Fuzzer 59 | 60 | Clone [someip-protocol-fuzzer](https://github.com/cfanatic/someip-protocol-fuzzer), and run following instructions: 61 | 62 | ```bash 63 | virtualenv -p python3 .venv 64 | source .venv/bin/activate 65 | pip3 install -r requirements.txt 66 | ``` 67 | 68 | Open the [fuzzer configuration file](https://github.com/cfanatic/someip-protocol-fuzzer/blob/master/config.ini), and adjust the IP address configuration fields accordingly. Same for the source and destination ports. You can find this out by analyzing SOME/IP traffic using Wireshark. 69 | 70 | Finally, install [radamsa](https://gitlab.com/akihe/radamsa). 71 | 72 | When you run `sudo python3 main.py`, the output must show something similar to: 73 | 74 | ```log 75 | 15:13:15 INFO: Fuzzing protocol layer 'SOMEIP' on protocol field 'load' 76 | 15:13:15 INFO: Heartbeat is started 77 | 15:13:15 INFO: Thread #0 is started 78 | 15:13:16 INFO: Sending: b'Hell\\nSril\\nSril\\nService!ervice!ervice!ervice!' 79 | 15:13:17 INFO: Sending: b'o SService!' 80 | 15:13:18 ERROR: No heartbeat found on SOME/IP service 81 | 15:13:21 INFO: Heartbeat is stopped 82 | 15:13:21 INFO: Thread #0 is stopped 83 | 15:13:21 INFO: Exiting main() 84 | ``` 85 | 86 | ## Configuration 87 | 88 | Define which SOME/IP protocol field the fuzzer shall mutate. Open the [protocol definition file](https://github.com/cfanatic/someip-protocol-fuzzer/blob/master/data/someip_fields.json), and set `"fuzzer": "radamsa"` for each field accordingly. 89 | 90 | The example below fuzzes the payload of a paket using *Hello Service!* as the initial seed: 91 | 92 | ```json 93 | "load": { 94 | "values": [ 95 | "48656c6c6f205365727669636521" 96 | ], 97 | "type": "StrField", 98 | "fuzzing": { 99 | "fuzzer": "radamsa" 100 | } 101 | } 102 | ``` 103 | 104 | The [protocol definition export](https://github.com/cfanatic/someip-protocol-fuzzer/blob/master/someip_fuzzer/template.py) based on a given Wireshark trace was inspired by the presentation from Timo Ramsauer on [Black-Box Live Protocol Fuzzing](https://media.ccc.de/v/eh19-149-black-box-live-protocol-fuzzing). 105 | 106 | ## Fuzzing 107 | 108 | Execute the service on VM #1 and the fuzzer on VM #2 as described above. 109 | 110 | Following process takes place, including a periodic heartbeat mechanism implemented as ping/pong exchange: 111 | 112 | ![flowchart](https://raw.githubusercontent.com/cfanatic/someip-protocol-fuzzer/master/misc/flowchart.png) 113 | 114 | ## Improvements 115 | 116 | - The current state is a black-box approach. In order to improve the attack potential in grey-box fashion, one would need to implement a state machine to pass the most trivial checks on the target service first. 117 | - It makes sense to call the heartbeat check after each input transmission. This way you can capture which particular mutated input is responsible for anomalies on the target service. 118 | -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | [Fuzzer] 2 | Interface = eth0 3 | Trace = data/someip_client_request_cf12fb22.pcapng 4 | Template = data/someip_fields.json 5 | Filter = ip host 192.168.0.18 or ip host 192.168.0.19 and ip proto \udp and not ip proto \igmp 6 | # Save current fuzzing value as next input for the next seed 7 | History = no 8 | # Select between [replay] and [live] 9 | Mode = replay 10 | # Select between [SOMEIP] and [SD] 11 | Layer = SOMEIP 12 | 13 | [Service] 14 | Host = 192.168.0.18 15 | Port = 30509 16 | 17 | [Client] 18 | Host = 192.168.0.19 19 | Port = 42574 20 | -------------------------------------------------------------------------------- /data/someip_client_cf12fb22.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfanatic/someip-protocol-fuzzer/5977f62580f02a95568c0715dd6bb2eb804d0a81/data/someip_client_cf12fb22.pcapng -------------------------------------------------------------------------------- /data/someip_client_offer_cf12fb22.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfanatic/someip-protocol-fuzzer/5977f62580f02a95568c0715dd6bb2eb804d0a81/data/someip_client_offer_cf12fb22.pcapng -------------------------------------------------------------------------------- /data/someip_client_request_cf12fb22.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfanatic/someip-protocol-fuzzer/5977f62580f02a95568c0715dd6bb2eb804d0a81/data/someip_client_request_cf12fb22.pcapng -------------------------------------------------------------------------------- /data/someip_fields.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "outgoing": true, 4 | "layer": "SOMEIP", 5 | "fields": { 6 | "srv_id": { 7 | "values": [ 8 | 4660 9 | ], 10 | "type": "XShortField", 11 | "fuzzing": { 12 | "fuzzer": null 13 | } 14 | }, 15 | "sub_id": { 16 | "values": [ 17 | 0 18 | ], 19 | "type": "BitEnumField", 20 | "fuzzing": { 21 | "fuzzer": null 22 | } 23 | }, 24 | "method_id": { 25 | "values": [ 26 | 1057 27 | ], 28 | "type": "ConditionalField", 29 | "fuzzing": { 30 | "fuzzer": null 31 | } 32 | }, 33 | "event_id": { 34 | "values": [ 35 | null 36 | ], 37 | "type": "ConditionalField", 38 | "fuzzing": { 39 | "fuzzer": null 40 | } 41 | }, 42 | "len": { 43 | "values": [ 44 | 22 45 | ], 46 | "type": "IntField", 47 | "fuzzing": { 48 | "fuzzer": null 49 | } 50 | }, 51 | "client_id": { 52 | "values": [ 53 | 4883 54 | ], 55 | "type": "XShortField", 56 | "fuzzing": { 57 | "fuzzer": null 58 | } 59 | }, 60 | "session_id": { 61 | "values": [ 62 | 1 63 | ], 64 | "type": "XShortField", 65 | "fuzzing": { 66 | "fuzzer": null 67 | } 68 | }, 69 | "proto_ver": { 70 | "values": [ 71 | 1 72 | ], 73 | "type": "XByteField", 74 | "fuzzing": { 75 | "fuzzer": null 76 | } 77 | }, 78 | "iface_ver": { 79 | "values": [ 80 | 0 81 | ], 82 | "type": "XByteField", 83 | "fuzzing": { 84 | "fuzzer": null 85 | } 86 | }, 87 | "msg_type": { 88 | "values": [ 89 | 0 90 | ], 91 | "type": "ByteEnumField", 92 | "fuzzing": { 93 | "fuzzer": null 94 | } 95 | }, 96 | "retcode": { 97 | "values": [ 98 | 0 99 | ], 100 | "type": "ByteEnumField", 101 | "fuzzing": { 102 | "fuzzer": null 103 | } 104 | }, 105 | "offset": { 106 | "values": [ 107 | null 108 | ], 109 | "type": "ConditionalField", 110 | "fuzzing": { 111 | "fuzzer": null 112 | } 113 | }, 114 | "res": { 115 | "values": [ 116 | null 117 | ], 118 | "type": "ConditionalField", 119 | "fuzzing": { 120 | "fuzzer": null 121 | } 122 | }, 123 | "more_seg": { 124 | "values": [ 125 | null 126 | ], 127 | "type": "ConditionalField", 128 | "fuzzing": { 129 | "fuzzer": null 130 | } 131 | }, 132 | "load": { 133 | "values": [ 134 | "48656c6c6f205365727669636521" 135 | ], 136 | "type": "StrField", 137 | "fuzzing": { 138 | "fuzzer": "radamsa" 139 | } 140 | } 141 | } 142 | } 143 | ] -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from someip_fuzzer.config import config 2 | from someip_fuzzer.fuzzer import Fuzzer 3 | from someip_fuzzer.heartbeat import Heartbeat 4 | from someip_fuzzer.log import log_info, log_error 5 | from someip_fuzzer.template import * 6 | from someip_fuzzer.types import * 7 | from queue import Queue 8 | import signal 9 | import time 10 | 11 | def generate_template(): 12 | generator = Template() 13 | packets = generator.read_capture() 14 | trace = generator.create_template(packets) 15 | generator.save_template(trace) 16 | log_info("Printing JSON dump") 17 | generator.print_template(trace) 18 | 19 | def import_template(): 20 | generator = Template() 21 | trace = generator.read_template() 22 | return trace 23 | 24 | def shutdown(signum, frame): 25 | raise ServiceShutdown("Caught signal %d" % signum) 26 | 27 | def main(): 28 | signal.signal(signal.SIGTERM, shutdown) 29 | signal.signal(signal.SIGINT, shutdown) 30 | 31 | excq = Queue() 32 | targets = [] 33 | threads = [] 34 | 35 | template = import_template() 36 | fields = template[(True, config["Fuzzer"]["Layer"])]["fields"].items() 37 | for fieldname, fieldvalues in fields: 38 | fuzzer = fieldvalues["fuzzing"]["fuzzer"] 39 | if fuzzer is not None: 40 | targets.append((fieldname, fuzzer)) 41 | log_info("Fuzzing protocol layer '{}' on protocol field '{}'".format(config["Fuzzer"]["Layer"], fieldname)) 42 | 43 | if config["Fuzzer"]["Mode"] == "replay": 44 | try: 45 | threads.append(Heartbeat(excq)) 46 | for i in range(len(targets)): 47 | threads.append(Fuzzer(i, excq, template, targets[i])) 48 | for t in threads: 49 | t.start() 50 | while True: 51 | if excq.qsize() != 0: 52 | raise excq.get() 53 | except (NoHostError, NoHeartbeatError, NoSudoError) as exc: 54 | log_error(exc) 55 | except ServiceShutdown as msg: 56 | log_info(msg) 57 | finally: 58 | for t in threads: 59 | t.shutdown.set() 60 | t.join() 61 | log_info("Exiting main()") 62 | elif config["Fuzzer"]["Mode"] == "live": 63 | pass 64 | 65 | if __name__ == "__main__": 66 | main() 67 | -------------------------------------------------------------------------------- /misc/flowchart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfanatic/someip-protocol-fuzzer/5977f62580f02a95568c0715dd6bb2eb804d0a81/misc/flowchart.png -------------------------------------------------------------------------------- /misc/flowchart.xml: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /misc/fuzzing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfanatic/someip-protocol-fuzzer/5977f62580f02a95568c0715dd6bb2eb804d0a81/misc/fuzzing.png -------------------------------------------------------------------------------- /misc/fuzzing.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfanatic/someip-protocol-fuzzer/5977f62580f02a95568c0715dd6bb2eb804d0a81/misc/fuzzing.webm -------------------------------------------------------------------------------- /misc/notes.txt: -------------------------------------------------------------------------------- 1 | Blackbox-Testing 2 | ---------------- 3 | - https://media.ccc.de/v/eh19-149-black-box-live-protocol-fuzzing 4 | - Bettercap can be used for man-in-the-middle attacks 5 | 6 | 7 | Test Generation 8 | --------------- 9 | - Idea #1: Send random bits to service application. This does not work, because we are required to establish a valid connection first. 10 | - Idea #2: Replace TCP/UDP-IP application layer stack with random data. This is a better approach, but we might not get past the initial if-statements which check the protocol version that is being used, for example. 11 | - Idea #3: Analyze protocol structure and fuzz particular protocol fields only. 12 | - Idea #4: Fuzz particular protocol fields and implement the communication logic. For example, for SOME/IP it is required to send an intial FIND SERVICE message in order to get the address and port for a particular service instance. That is a lot of work, because you need to analyze and implement the complete communication handshake. 13 | - Idea #5: Black-box live protocol fuzzing. Client sends messages cyclically, a fuzzer in the middle intercepts the pakets and manipulates their content and then forwards it to the service application. Advantages: 1. no need to worry about application logic, 2. the complete system interaction is under test. Disadvantages: 1. pakets need to be manipulated on-the-fly, 2. system components may not generate enough pakets. 14 | 15 | 16 | Scapy 17 | ----- 18 | - Basically Wireshark for Python 19 | - Can generate new packets 20 | - Can dissect existing packets 21 | - Can manipulate existing packets 22 | - Can recalculate the checksum 23 | 24 | 25 | Templates 26 | --------- 27 | - SOME/IP traffic needs to be recorded 28 | - Templates will be generated out of this traffic 29 | - The templates will contain a specific field for every protcol layer/field 30 | - The templates shall contain every protocol layer for data transmitted to the services, and data transmitted from the service 31 | - For every template field, you can specify which fuzzing technique should be applied 32 | - Modules are used to fuzz the fields, e.g. 1. Scapy offers a module for byte fields, 2. Radamsa can be used for string fields as a general purpose fuzzer (you can feed a pool of values into Radamsa, e.g. payload data that has been observed in the past) 33 | 34 | 35 | Radamsa 36 | ------- 37 | - In practice many programs fail in unique ways. Some common ways to catch obvious errors are to check the exit value, enable fatal signal printing in kernel and checking if something new turns up in dmesg, run a program under strace, gdb or valgrind and see if something interesting is caught, check if an error reporter process has been started after starting the program, etc. 38 | - The recommended use is $ radamsa -o output-%n.foo -n 100 samples/*.foo 39 | 40 | 41 | Scapy Commands 42 | -------------- 43 | s = sniff(iface="eth0", prn=lambda x: x.show(), count=10, filter="ip host 192.168.0.3 or ip host 192.168.0.4 and ip proto \\udp and not ip proto \\igmp") 44 | 45 | 46 | Wireshark Filter 47 | ---------------- 48 | !arp and !icmpv6 and ip.addr == 192.168.0.3 and ip.dst == 192.168.0.4 and someip2 and !icmp 49 | !arp and !icmpv6 and someip2 and !icmp and ip.addr==192.168.0.3 50 | 51 | 52 | TODO 53 | ---- 54 | - Implement SOME/IP-SD packet dissecting 55 | - Parse a trace with multiple SOME/IP frames 56 | - JSON template must respect the SOME/IP paket protocol structure 57 | -------------------------------------------------------------------------------- /misc/send.py: -------------------------------------------------------------------------------- 1 | # sudo python3 ./send.py 2 | 3 | from scapy.all import * 4 | 5 | #### Required for version 2.4.3 #### 6 | # load_contrib("automotive.someip") 7 | # load_contrib("automotive.someip_sd") 8 | 9 | # i = IP(src="192.168.0.4", dst="192.168.0.3") 10 | # u = UDP(sport=48664, dport=30509) 11 | # sip = SOMEIP() 12 | # sip.iface_ver = 0 13 | # sip.proto_ver = 1 14 | # sip.msg_type = "REQUEST" 15 | # sip.retcode = "E_OK" 16 | # sip.msg_id.srv_id = 0x1234 17 | # sip.msg_id.sub_id = 0x0 18 | # sip.msg_id.method_id=0x0421 19 | # sip.req_id.client_id = 0x1313 20 | # sip.req_id.session_id = 0x0010 21 | # sip.add_payload(Raw ("Hello!")) 22 | # p = i/u/sip 23 | # res = sr1(p) 24 | 25 | #### Required for version 2.4.3-dev699 (master) #### 26 | # load_contrib("automotive.someip") 27 | 28 | # i = IP(src="192.168.0.3", dst="192.168.0.4") 29 | # u = UDP(sport=48664, dport=30509) 30 | # sip = SOMEIP() 31 | # sip.iface_ver = 0 32 | # sip.proto_ver = 1 33 | # sip.msg_type = "REQUEST" 34 | # sip.retcode = "E_OK" 35 | # sip.srv_id = 0x1234 36 | # sip.sub_id = 0x0 37 | # sip.method_id=0x0421 38 | # sip.client_id = 0x1313 39 | # sip.session_id = 0x0010 40 | # sip.add_payload(Raw ("ping")) 41 | # p = i/u/sip 42 | # res = sr1(p) 43 | 44 | #### Required for version 2.4.5 using python 3.7.6, pip3 #### 45 | load_contrib("automotive.someip") 46 | 47 | i = IP(src="192.168.0.19", dst="192.168.0.18") 48 | u = UDP(sport=46334, dport=30509) 49 | sip = SOMEIP() 50 | sip.iface_ver = 0 51 | sip.proto_ver = 1 52 | sip.msg_type = "REQUEST" 53 | sip.retcode = "E_OK" 54 | sip.msg_id.srv_id = 0x1234 55 | sip.msg_id.sub_id = 0x0 56 | sip.msg_id.method_id=0x0421 57 | sip.req_id.client_id = 0x1313 58 | sip.req_id.session_id = 0x0010 59 | sip.add_payload(Raw ("ping")) 60 | p = i/u/sip 61 | res = sr1(p, retry=0, timeout=1, verbose=False) 62 | if res[Raw].load[-4:] == bytes("pong", "utf-8"): 63 | print("Pong received!") 64 | else: 65 | print("NO pong received!") 66 | -------------------------------------------------------------------------------- /misc/someip.py: -------------------------------------------------------------------------------- 1 | # scapy==2.4.3.dev699: 2 | # /usr/local/lib/python3.8/dist-packages/scapy/contrib/automotive/someip.py 3 | 4 | # MIT License 5 | 6 | # Copyright (c) 2018 Jose Amores 7 | 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | # This file is part of Scapy 27 | # See http://www.secdev.org/projects/scapy for more information 28 | # Copyright (C) Sebastian Baar 29 | # This program is published under a GPLv2 license 30 | 31 | # scapy.contrib.description = Scalable service-Oriented MiddlewarE/IP (SOME/IP) 32 | # scapy.contrib.status = loads 33 | 34 | import ctypes 35 | import collections 36 | import struct 37 | 38 | from scapy.layers.inet import TCP, UDP 39 | from scapy.layers.inet6 import IP6Field 40 | from scapy.compat import raw, orb 41 | from scapy.config import conf 42 | from scapy.modules.six.moves import range 43 | from scapy.packet import Packet, Raw, bind_top_down, bind_bottom_up 44 | from scapy.fields import XShortField, BitEnumField, ConditionalField, \ 45 | BitField, XBitField, IntField, XByteField, ByteEnumField, \ 46 | ShortField, X3BytesField, StrLenField, IPField, FieldLenField, \ 47 | PacketListField, XIntField 48 | 49 | 50 | class SOMEIP(Packet): 51 | """ SOME/IP Packet.""" 52 | 53 | PROTOCOL_VERSION = 0x01 54 | INTERFACE_VERSION = 0x01 55 | LEN_OFFSET = 0x08 56 | LEN_OFFSET_TP = 0x0c 57 | TYPE_REQUEST = 0x00 58 | TYPE_REQUEST_NO_RET = 0x01 59 | TYPE_NOTIFICATION = 0x02 60 | TYPE_REQUEST_ACK = 0x40 61 | TYPE_REQUEST_NORET_ACK = 0x41 62 | TYPE_NOTIFICATION_ACK = 0x42 63 | TYPE_RESPONSE = 0x80 64 | TYPE_ERROR = 0x81 65 | TYPE_RESPONSE_ACK = 0xc0 66 | TYPE_ERROR_ACK = 0xc1 67 | TYPE_TP_REQUEST = 0x20 68 | TYPE_TP_REQUEST_NO_RET = 0x21 69 | TYPE_TP_NOTIFICATION = 0x22 70 | TYPE_TP_RESPONSE = 0x23 71 | TYPE_TP_ERROR = 0x24 72 | RET_E_OK = 0x00 73 | RET_E_NOT_OK = 0x01 74 | RET_E_UNKNOWN_SERVICE = 0x02 75 | RET_E_UNKNOWN_METHOD = 0x03 76 | RET_E_NOT_READY = 0x04 77 | RET_E_NOT_REACHABLE = 0x05 78 | RET_E_TIMEOUT = 0x06 79 | RET_E_WRONG_PROTOCOL_V = 0x07 80 | RET_E_WRONG_INTERFACE_V = 0x08 81 | RET_E_MALFORMED_MSG = 0x09 82 | RET_E_WRONG_MESSAGE_TYPE = 0x0a 83 | 84 | _OVERALL_LEN_NOPAYLOAD = 16 85 | 86 | name = "SOME/IP" 87 | 88 | fields_desc = [ 89 | XShortField("srv_id", 0), 90 | BitEnumField("sub_id", 0, 1, {0: "METHOD_ID", 1: "EVENT_ID"}), 91 | ConditionalField(XBitField("method_id", 0, 15), 92 | lambda pkt: pkt.sub_id == 0), 93 | ConditionalField(XBitField("event_id", 0, 15), 94 | lambda pkt: pkt.sub_id == 1), 95 | IntField("len", None), 96 | XShortField("client_id", 0), 97 | XShortField("session_id", 0), 98 | XByteField("proto_ver", PROTOCOL_VERSION), 99 | XByteField("iface_ver", INTERFACE_VERSION), 100 | ByteEnumField("msg_type", TYPE_REQUEST, { 101 | TYPE_REQUEST: "REQUEST", 102 | TYPE_REQUEST_NO_RET: "REQUEST_NO_RETURN", 103 | TYPE_NOTIFICATION: "NOTIFICATION", 104 | TYPE_REQUEST_ACK: "REQUEST_ACK", 105 | TYPE_REQUEST_NORET_ACK: "REQUEST_NO_RETURN_ACK", 106 | TYPE_NOTIFICATION_ACK: "NOTIFICATION_ACK", 107 | TYPE_RESPONSE: "RESPONSE", 108 | TYPE_ERROR: "ERROR", 109 | TYPE_RESPONSE_ACK: "RESPONSE_ACK", 110 | TYPE_ERROR_ACK: "ERROR_ACK", 111 | TYPE_TP_REQUEST: "TP_REQUEST", 112 | TYPE_TP_REQUEST_NO_RET: "TP_REQUEST_NO_RETURN", 113 | TYPE_TP_NOTIFICATION: "TP_NOTIFICATION", 114 | TYPE_TP_RESPONSE: "TP_RESPONSE", 115 | TYPE_TP_ERROR: "TP_ERROR", 116 | }), 117 | ByteEnumField("retcode", 0, { 118 | RET_E_OK: "E_OK", 119 | RET_E_NOT_OK: "E_NOT_OK", 120 | RET_E_UNKNOWN_SERVICE: "E_UNKNOWN_SERVICE", 121 | RET_E_UNKNOWN_METHOD: "E_UNKNOWN_METHOD", 122 | RET_E_NOT_READY: "E_NOT_READY", 123 | RET_E_NOT_REACHABLE: "E_NOT_REACHABLE", 124 | RET_E_TIMEOUT: "E_TIMEOUT", 125 | RET_E_WRONG_PROTOCOL_V: "E_WRONG_PROTOCOL_VERSION", 126 | RET_E_WRONG_INTERFACE_V: "E_WRONG_INTERFACE_VERSION", 127 | RET_E_MALFORMED_MSG: "E_MALFORMED_MESSAGE", 128 | RET_E_WRONG_MESSAGE_TYPE: "E_WRONG_MESSAGE_TYPE", 129 | }), 130 | ConditionalField(BitField("offset", 0, 28), 131 | lambda pkt: SOMEIP._is_tp(pkt)), 132 | ConditionalField(BitField("res", 0, 3), 133 | lambda pkt: SOMEIP._is_tp(pkt)), 134 | ConditionalField(BitField("more_seg", 0, 1), 135 | lambda pkt: SOMEIP._is_tp(pkt)) 136 | ] 137 | 138 | def post_build(self, pkt, pay): 139 | length = self.len 140 | if length is None: 141 | if SOMEIP._is_tp(self): 142 | length = SOMEIP.LEN_OFFSET_TP + len(pay) 143 | else: 144 | length = SOMEIP.LEN_OFFSET + len(pay) 145 | 146 | pkt = pkt[:4] + struct.pack("!I", length) + pkt[8:] 147 | return pkt + pay 148 | 149 | def answers(self, other): 150 | if other.__class__ == self.__class__: 151 | if self.msg_type in [SOMEIP.TYPE_REQUEST_NO_RET, 152 | SOMEIP.TYPE_REQUEST_NORET_ACK, 153 | SOMEIP.TYPE_NOTIFICATION, 154 | SOMEIP.TYPE_TP_REQUEST_NO_RET, 155 | SOMEIP.TYPE_TP_NOTIFICATION]: 156 | return 0 157 | return self.payload.answers(other.payload) 158 | return 0 159 | 160 | @staticmethod 161 | def _is_tp(pkt): 162 | """Returns true if pkt is using SOMEIP-TP, else returns false.""" 163 | 164 | tp = [SOMEIP.TYPE_TP_REQUEST, SOMEIP.TYPE_TP_REQUEST_NO_RET, 165 | SOMEIP.TYPE_TP_NOTIFICATION, SOMEIP.TYPE_TP_RESPONSE, 166 | SOMEIP.TYPE_TP_ERROR] 167 | if isinstance(pkt, Packet): 168 | return pkt.msg_type in tp 169 | else: 170 | return pkt[15] in tp 171 | 172 | def fragment(self, fragsize=1392): 173 | """Fragment SOME/IP-TP""" 174 | fnb = 0 175 | fl = self 176 | lst = list() 177 | while fl.underlayer is not None: 178 | fnb += 1 179 | fl = fl.underlayer 180 | 181 | for p in fl: 182 | s = raw(p[fnb].payload) 183 | nb = (len(s) + fragsize) // fragsize 184 | for i in range(nb): 185 | q = p.copy() 186 | del q[fnb].payload 187 | q[fnb].len = SOMEIP.LEN_OFFSET_TP + \ 188 | len(s[i * fragsize:(i + 1) * fragsize]) 189 | q[fnb].more_seg = 1 190 | if i == nb - 1: 191 | q[fnb].more_seg = 0 192 | q[fnb].offset += i * fragsize // 16 193 | r = conf.raw_layer(load=s[i * fragsize:(i + 1) * fragsize]) 194 | r.overload_fields = p[fnb].payload.overload_fields.copy() 195 | q.add_payload(r) 196 | lst.append(q) 197 | 198 | return lst 199 | 200 | 201 | def _bind_someip_layers(): 202 | # arnd: this is required to bind SOME/IP-SD 203 | bind_top_down(UDP, SOMEIP, sport=30490, dport=30490) 204 | for i in range(15): 205 | bind_bottom_up(UDP, SOMEIP, sport=30490 + i) 206 | bind_bottom_up(TCP, SOMEIP, sport=30490 + i) 207 | bind_bottom_up(UDP, SOMEIP, dport=30490 + i) 208 | bind_bottom_up(TCP, SOMEIP, dport=30490 + i) 209 | 210 | # arnd: this is required to bind SOME/IP 211 | for i in range(15): 212 | bind_bottom_up(UDP, SOMEIP, sport=30509 + i) 213 | bind_bottom_up(TCP, SOMEIP, sport=30509 + i) 214 | bind_bottom_up(UDP, SOMEIP, dport=30509 + i) 215 | bind_bottom_up(TCP, SOMEIP, dport=30509 + i) 216 | 217 | _bind_someip_layers() 218 | 219 | 220 | class _SDPacketBase(Packet): 221 | """ base class to be used among all SD Packet definitions.""" 222 | def extract_padding(self, s): 223 | return "", s 224 | 225 | 226 | SDENTRY_TYPE_SRV_FINDSERVICE = 0x00 227 | SDENTRY_TYPE_SRV_OFFERSERVICE = 0x01 228 | SDENTRY_TYPE_SRV = (SDENTRY_TYPE_SRV_FINDSERVICE, 229 | SDENTRY_TYPE_SRV_OFFERSERVICE) 230 | SDENTRY_TYPE_EVTGRP_SUBSCRIBE = 0x06 231 | SDENTRY_TYPE_EVTGRP_SUBSCRIBE_ACK = 0x07 232 | SDENTRY_TYPE_EVTGRP = (SDENTRY_TYPE_EVTGRP_SUBSCRIBE, 233 | SDENTRY_TYPE_EVTGRP_SUBSCRIBE_ACK) 234 | SDENTRY_OVERALL_LEN = 16 235 | 236 | 237 | def _MAKE_SDENTRY_COMMON_FIELDS_DESC(type): 238 | return [ 239 | XByteField("type", type), 240 | XByteField("index_1", 0), 241 | XByteField("index_2", 0), 242 | XBitField("n_opt_1", 0, 4), 243 | XBitField("n_opt_2", 0, 4), 244 | XShortField("srv_id", 0), 245 | XShortField("inst_id", 0), 246 | XByteField("major_ver", 0), 247 | X3BytesField("ttl", 0) 248 | ] 249 | 250 | 251 | class SDEntry_Service(_SDPacketBase): 252 | name = "Service Entry" 253 | fields_desc = _MAKE_SDENTRY_COMMON_FIELDS_DESC( 254 | SDENTRY_TYPE_SRV_FINDSERVICE) 255 | fields_desc += [ 256 | XIntField("minor_ver", 0) 257 | ] 258 | 259 | 260 | class SDEntry_EventGroup(_SDPacketBase): 261 | name = "Eventgroup Entry" 262 | fields_desc = _MAKE_SDENTRY_COMMON_FIELDS_DESC( 263 | SDENTRY_TYPE_EVTGRP_SUBSCRIBE) 264 | fields_desc += [ 265 | XBitField("res", 0, 12), 266 | XBitField("cnt", 0, 4), 267 | XShortField("eventgroup_id", 0) 268 | ] 269 | 270 | 271 | def _sdentry_class(payload, **kargs): 272 | TYPE_PAYLOAD_I = 0 273 | pl_type = orb(payload[TYPE_PAYLOAD_I]) 274 | cls = None 275 | 276 | if pl_type in SDENTRY_TYPE_SRV: 277 | cls = SDEntry_Service 278 | elif pl_type in SDENTRY_TYPE_EVTGRP: 279 | cls = SDEntry_EventGroup 280 | 281 | return cls(payload, **kargs) 282 | 283 | 284 | def _sdoption_class(payload, **kargs): 285 | pl_type = orb(payload[2]) 286 | 287 | cls = { 288 | SDOPTION_CFG_TYPE: SDOption_Config, 289 | SDOPTION_LOADBALANCE_TYPE: SDOption_LoadBalance, 290 | SDOPTION_IP4_ENDPOINT_TYPE: SDOption_IP4_EndPoint, 291 | SDOPTION_IP4_MCAST_TYPE: SDOption_IP4_Multicast, 292 | SDOPTION_IP4_SDENDPOINT_TYPE: SDOption_IP4_SD_EndPoint, 293 | SDOPTION_IP6_ENDPOINT_TYPE: SDOption_IP6_EndPoint, 294 | SDOPTION_IP6_MCAST_TYPE: SDOption_IP6_Multicast, 295 | SDOPTION_IP6_SDENDPOINT_TYPE: SDOption_IP6_SD_EndPoint 296 | }.get(pl_type, Raw) 297 | 298 | return cls(payload, **kargs) 299 | 300 | 301 | # SD Option 302 | SDOPTION_CFG_TYPE = 0x01 303 | SDOPTION_LOADBALANCE_TYPE = 0x02 304 | SDOPTION_LOADBALANCE_LEN = 0x05 305 | SDOPTION_IP4_ENDPOINT_TYPE = 0x04 306 | SDOPTION_IP4_ENDPOINT_LEN = 0x0009 307 | SDOPTION_IP4_MCAST_TYPE = 0x14 308 | SDOPTION_IP4_MCAST_LEN = 0x0009 309 | SDOPTION_IP4_SDENDPOINT_TYPE = 0x24 310 | SDOPTION_IP4_SDENDPOINT_LEN = 0x0009 311 | SDOPTION_IP6_ENDPOINT_TYPE = 0x06 312 | SDOPTION_IP6_ENDPOINT_LEN = 0x0015 313 | SDOPTION_IP6_MCAST_TYPE = 0x16 314 | SDOPTION_IP6_MCAST_LEN = 0x0015 315 | SDOPTION_IP6_SDENDPOINT_TYPE = 0x26 316 | SDOPTION_IP6_SDENDPOINT_LEN = 0x0015 317 | 318 | 319 | def _MAKE_COMMON_SDOPTION_FIELDS_DESC(type, length=None): 320 | return [ 321 | ShortField("len", length), 322 | XByteField("type", type), 323 | XByteField("res_hdr", 0) 324 | ] 325 | 326 | 327 | def _MAKE_COMMON_IP_SDOPTION_FIELDS_DESC(): 328 | return [ 329 | XByteField("res_tail", 0), 330 | ByteEnumField("l4_proto", 0x11, {0x06: "TCP", 0x11: "UDP"}), 331 | ShortField("port", 0) 332 | ] 333 | 334 | 335 | class SDOption_Config(_SDPacketBase): 336 | name = "Config Option" 337 | fields_desc = _MAKE_COMMON_SDOPTION_FIELDS_DESC(SDOPTION_CFG_TYPE) + [ 338 | StrLenField("cfg_str", "\x00", length_from=lambda pkt: pkt.len - 1) 339 | ] 340 | 341 | def post_build(self, pkt, pay): 342 | if self.len is None: 343 | length = len(self.cfg_str) + 1 # res_hdr field takes 1 byte 344 | pkt = struct.pack("!H", length) + pkt[2:] 345 | return pkt + pay 346 | 347 | @staticmethod 348 | def make_string(data): 349 | # Build a valid null-terminated configuration string from a dict or a 350 | # list with key-value pairs. 351 | # 352 | # Example: 353 | # >>> SDOption_Config.make_string({ "hello": "world" }) 354 | # b'\x0bhello=world\x00' 355 | # 356 | # >>> SDOption_Config.make_string([ 357 | # ... ("x", "y"), 358 | # ... ("abc", "def"), 359 | # ... ("123", "456") 360 | # ... ]) 361 | # b'\x03x=y\x07abc=def\x07123=456\x00' 362 | 363 | if isinstance(data, dict): 364 | data = data.items() 365 | 366 | # combine entries 367 | data = ("{}={}".format(k, v) for k, v in data) 368 | # prepend length 369 | data = ("{}{}".format(chr(len(v)), v) for v in data) 370 | # concatenate 371 | data = "".join(data) 372 | data += "\x00" 373 | 374 | return data.encode("utf8") 375 | 376 | 377 | class SDOption_LoadBalance(_SDPacketBase): 378 | name = "LoadBalance Option" 379 | fields_desc = _MAKE_COMMON_SDOPTION_FIELDS_DESC( 380 | SDOPTION_LOADBALANCE_TYPE, SDOPTION_LOADBALANCE_LEN) 381 | fields_desc += [ 382 | ShortField("priority", 0), 383 | ShortField("weight", 0) 384 | ] 385 | 386 | 387 | class SDOption_IP4_EndPoint(_SDPacketBase): 388 | name = "IP4 EndPoint Option" 389 | fields_desc = _MAKE_COMMON_SDOPTION_FIELDS_DESC( 390 | SDOPTION_IP4_ENDPOINT_TYPE, SDOPTION_IP4_ENDPOINT_LEN) 391 | fields_desc += [ 392 | IPField("addr", "0.0.0.0"), 393 | ] + _MAKE_COMMON_IP_SDOPTION_FIELDS_DESC() 394 | 395 | 396 | class SDOption_IP4_Multicast(_SDPacketBase): 397 | name = "IP4 Multicast Option" 398 | fields_desc = _MAKE_COMMON_SDOPTION_FIELDS_DESC( 399 | SDOPTION_IP4_MCAST_TYPE, SDOPTION_IP4_MCAST_LEN) 400 | fields_desc += [ 401 | IPField("addr", "0.0.0.0"), 402 | ] + _MAKE_COMMON_IP_SDOPTION_FIELDS_DESC() 403 | 404 | 405 | class SDOption_IP4_SD_EndPoint(_SDPacketBase): 406 | name = "IP4 SDEndPoint Option" 407 | fields_desc = _MAKE_COMMON_SDOPTION_FIELDS_DESC( 408 | SDOPTION_IP4_SDENDPOINT_TYPE, SDOPTION_IP4_SDENDPOINT_LEN) 409 | fields_desc += [ 410 | IPField("addr", "0.0.0.0"), 411 | ] + _MAKE_COMMON_IP_SDOPTION_FIELDS_DESC() 412 | 413 | 414 | class SDOption_IP6_EndPoint(_SDPacketBase): 415 | name = "IP6 EndPoint Option" 416 | fields_desc = _MAKE_COMMON_SDOPTION_FIELDS_DESC( 417 | SDOPTION_IP6_ENDPOINT_TYPE, SDOPTION_IP6_ENDPOINT_LEN) 418 | fields_desc += [ 419 | IP6Field("addr", "::"), 420 | ] + _MAKE_COMMON_IP_SDOPTION_FIELDS_DESC() 421 | 422 | 423 | class SDOption_IP6_Multicast(_SDPacketBase): 424 | name = "IP6 Multicast Option" 425 | fields_desc = _MAKE_COMMON_SDOPTION_FIELDS_DESC( 426 | SDOPTION_IP6_MCAST_TYPE, SDOPTION_IP6_MCAST_LEN) 427 | fields_desc += [ 428 | IP6Field("addr", "::"), 429 | ] + _MAKE_COMMON_IP_SDOPTION_FIELDS_DESC() 430 | 431 | 432 | class SDOption_IP6_SD_EndPoint(_SDPacketBase): 433 | name = "IP6 SDEndPoint Option" 434 | fields_desc = _MAKE_COMMON_SDOPTION_FIELDS_DESC( 435 | SDOPTION_IP6_SDENDPOINT_TYPE, SDOPTION_IP6_SDENDPOINT_LEN) 436 | fields_desc += [ 437 | IP6Field("addr", "::"), 438 | ] + _MAKE_COMMON_IP_SDOPTION_FIELDS_DESC() 439 | 440 | 441 | ## 442 | # SD PACKAGE DEFINITION 443 | ## 444 | class SD(_SDPacketBase): 445 | """ 446 | SD Packet 447 | 448 | NOTE : when adding 'entries' or 'options', do not use list.append() 449 | method but create a new list 450 | e.g. : p = SD() 451 | p.option_array = [SDOption_Config(),SDOption_IP6_EndPoint()] 452 | """ 453 | SOMEIP_MSGID_SRVID = 0xffff 454 | SOMEIP_MSGID_SUBID = 0x1 455 | SOMEIP_MSGID_EVENTID = 0x100 456 | SOMEIP_CLIENT_ID = 0x0000 457 | SOMEIP_MINIMUM_SESSION_ID = 0x0001 458 | SOMEIP_PROTO_VER = 0x01 459 | SOMEIP_IFACE_VER = 0x01 460 | SOMEIP_MSG_TYPE = SOMEIP.TYPE_NOTIFICATION 461 | SOMEIP_RETCODE = SOMEIP.RET_E_OK 462 | 463 | _sdFlag = collections.namedtuple('Flag', 'mask offset') 464 | FLAGSDEF = { 465 | "REBOOT": _sdFlag(mask=0x80, offset=7), 466 | "UNICAST": _sdFlag(mask=0x40, offset=6) 467 | } 468 | 469 | name = "SD" 470 | fields_desc = [ 471 | XByteField("flags", 0), 472 | X3BytesField("res", 0), 473 | FieldLenField("len_entry_array", None, 474 | length_of="entry_array", fmt="!I"), 475 | PacketListField("entry_array", None, cls=_sdentry_class, 476 | length_from=lambda pkt: pkt.len_entry_array), 477 | FieldLenField("len_option_array", None, 478 | length_of="option_array", fmt="!I"), 479 | PacketListField("option_array", None, cls=_sdoption_class, 480 | length_from=lambda pkt: pkt.len_option_array) 481 | ] 482 | 483 | def get_flag(self, name): 484 | name = name.upper() 485 | if name in self.FLAGSDEF: 486 | return ((self.flags & self.FLAGSDEF[name].mask) >> 487 | self.FLAGSDEF[name].offset) 488 | else: 489 | return None 490 | 491 | def set_flag(self, name, value): 492 | name = name.upper() 493 | if name in self.FLAGSDEF: 494 | self.flags = (self.flags & 495 | (ctypes.c_ubyte(~self.FLAGSDEF[name].mask).value)) \ 496 | | ((value & 0x01) << self.FLAGSDEF[name].offset) 497 | 498 | def set_entryArray(self, entry_list): 499 | if isinstance(entry_list, list): 500 | self.entry_array = entry_list 501 | else: 502 | self.entry_array = [entry_list] 503 | 504 | def set_optionArray(self, option_list): 505 | if isinstance(option_list, list): 506 | self.option_array = option_list 507 | else: 508 | self.option_array = [option_list] 509 | 510 | 511 | bind_top_down(SOMEIP, SD, 512 | srv_id=SD.SOMEIP_MSGID_SRVID, 513 | sub_id=SD.SOMEIP_MSGID_SUBID, 514 | client_id=SD.SOMEIP_CLIENT_ID, 515 | session_id=SD.SOMEIP_MINIMUM_SESSION_ID, 516 | event_id=SD.SOMEIP_MSGID_EVENTID, 517 | proto_ver=SD.SOMEIP_PROTO_VER, 518 | iface_ver=SD.SOMEIP_IFACE_VER, 519 | msg_type=SD.SOMEIP_MSG_TYPE, 520 | retcode=SD.SOMEIP_RETCODE) 521 | 522 | bind_bottom_up(SOMEIP, SD, 523 | srv_id=SD.SOMEIP_MSGID_SRVID, 524 | sub_id=SD.SOMEIP_MSGID_SUBID, 525 | event_id=SD.SOMEIP_MSGID_EVENTID, 526 | proto_ver=SD.SOMEIP_PROTO_VER, 527 | iface_ver=SD.SOMEIP_IFACE_VER, 528 | msg_type=SD.SOMEIP_MSG_TYPE, 529 | retcode=SD.SOMEIP_RETCODE) 530 | 531 | # FIXME: Service Discovery messages shall be transported over UDP 532 | # (TR_SOMEIP_00248) 533 | # FIXME: The port 30490 (UDP and TCP as well) shall be only used for SOME/IP-SD 534 | # and not used for applications communicating over SOME/IP 535 | # (TR_SOMEIP_00020) 536 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | git+https://github.com/secdev/scapy.git@v2.4.5 2 | -------------------------------------------------------------------------------- /someip_fuzzer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfanatic/someip-protocol-fuzzer/5977f62580f02a95568c0715dd6bb2eb804d0a81/someip_fuzzer/__init__.py -------------------------------------------------------------------------------- /someip_fuzzer/config.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import json 3 | 4 | config = configparser.ConfigParser() 5 | config.read("config.ini") 6 | 7 | def read_json(s): 8 | global config 9 | config = json.loads(s) 10 | -------------------------------------------------------------------------------- /someip_fuzzer/fuzzer.py: -------------------------------------------------------------------------------- 1 | from scapy.all import * 2 | from someip_fuzzer.config import config 3 | from someip_fuzzer.log import log_info 4 | from someip_fuzzer.types import * 5 | from queue import Queue 6 | import binascii 7 | import random 8 | import threading 9 | import time 10 | import subprocess 11 | 12 | class Fuzzer(threading.Thread): 13 | 14 | def __init__(self, index, excq, template, targets): 15 | super().__init__() 16 | self.index = index 17 | self.excq = excq 18 | self.template = template 19 | self.targets = targets 20 | self.shutdown = threading.Event() 21 | 22 | def run(self): 23 | log_info("Thread #{} is started".format(self.index)) 24 | while not self.shutdown.is_set(): 25 | time.sleep(1) # this value must be set according to the available bandwidth 26 | value = self.prepare() 27 | self.send(value) 28 | log_info("Thread #{} is stopped".format(self.index)) 29 | 30 | def prepare(self): 31 | if self.shutdown.is_set(): 32 | return 33 | fields = self.template[(True, config["Fuzzer"]["Layer"])]["fields"] 34 | target = self.targets[0] 35 | index = random.choice(range(len(fields[target]["values"]))) 36 | value = fields[target]["values"][index] 37 | p = subprocess.Popen( 38 | ["radamsa"], 39 | stdin=subprocess.PIPE, 40 | stdout=subprocess.PIPE, 41 | stderr=subprocess.STDOUT 42 | ) 43 | if isinstance(value, str): 44 | value_convert = binascii.unhexlify(value) # convert hex -> 48656c6c6f205365727669636521 to ascii -> b'Hello Service!' 45 | else: 46 | value_convert = value 47 | value_fuzz = p.communicate(input=value_convert)[0] 48 | if config["Fuzzer"]["History"] == "yes": 49 | log_info("Saving current fuzzing value as next seed") 50 | fields[target]["values"][index] = value_fuzz 51 | return value_fuzz 52 | 53 | def send(self, value): 54 | log_info("Sending: {}".format(value)) 55 | i = IP(src=config["Client"]["Host"], dst=config["Service"]["Host"]) 56 | u = UDP(sport=config["Client"].getint("Port"), dport=config["Service"].getint("Port")) 57 | sip = SOMEIP() 58 | sip.iface_ver = 0 59 | sip.proto_ver = 1 60 | sip.msg_type = "REQUEST" 61 | sip.retcode = "E_OK" 62 | sip.msg_id.srv_id = 0x1234 63 | sip.msg_id.sub_id = 0x0 64 | sip.msg_id.method_id=0x0421 65 | sip.req_id.client_id = 0x1313 66 | sip.req_id.session_id = 0x0010 67 | sip.add_payload(Raw (value)) 68 | paket = i/u/sip 69 | res = sr1(paket, retry=0, timeout=1, verbose=False) 70 | -------------------------------------------------------------------------------- /someip_fuzzer/heartbeat.py: -------------------------------------------------------------------------------- 1 | from scapy.all import * 2 | from someip_fuzzer.config import config 3 | from someip_fuzzer.log import log_info 4 | from someip_fuzzer.types import * 5 | from queue import Queue 6 | import threading 7 | import time 8 | 9 | load_contrib("automotive.someip") 10 | 11 | class Heartbeat(threading.Thread): 12 | 13 | def __init__(self, excq): 14 | super().__init__() 15 | self.excq = excq 16 | self.shutdown = threading.Event() 17 | 18 | def run(self): 19 | log_info("Heartbeat is started") 20 | while not self.shutdown.is_set(): 21 | try: 22 | time.sleep(3) 23 | self.check() 24 | except PermissionError: 25 | self.excq.put(NoSudoError("Permission as sudo required to send SOME/IP pakets")) 26 | log_info("Heartbeat is stopped") 27 | 28 | def check(self): 29 | try: 30 | i = IP(src=config["Client"]["Host"], dst=config["Service"]["Host"]) 31 | u = UDP(sport=config["Client"].getint("Port"), dport=config["Service"].getint("Port")) 32 | sip = SOMEIP() 33 | sip.iface_ver = 0 34 | sip.proto_ver = 1 35 | sip.msg_type = "REQUEST" 36 | sip.retcode = "E_OK" 37 | sip.msg_id.srv_id = 0x1234 38 | sip.msg_id.sub_id = 0x0 39 | sip.msg_id.method_id=0x0421 40 | sip.req_id.client_id = 0x1313 41 | sip.req_id.session_id = 0x0010 42 | sip.add_payload(Raw ("ping")) 43 | paket = i/u/sip 44 | res = sr1(paket, retry=0, timeout=3, verbose=False) 45 | if res == None: 46 | raise NoHostError("No response received from SOME/IP host") 47 | if res[Raw].load[-4:] != bytes("pong", "utf-8"): 48 | raise NoHeartbeatError("No heartbeat found on SOME/IP service") 49 | except (NoHostError, NoHeartbeatError) as exc: 50 | self.excq.put(exc) 51 | -------------------------------------------------------------------------------- /someip_fuzzer/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import threading 3 | 4 | logger = logging.getLogger("someip_fuzzer") 5 | logger.setLevel(logging.DEBUG) 6 | 7 | # formatter = logging.Formatter("%(asctime)s - %(levelname)s: %(message)s", "%Y-%m-%d %H:%M:%S") 8 | formatter = logging.Formatter("%(asctime)s %(levelname)s: %(message)s", "%H:%M:%S") 9 | 10 | ch = logging.StreamHandler() 11 | ch.setLevel(logging.DEBUG) 12 | ch.setFormatter(formatter) 13 | 14 | logger.addHandler(ch) 15 | 16 | lock = threading.Lock() 17 | 18 | def log_debug(text): 19 | lock.acquire() 20 | logger.debug(text) 21 | lock.release() 22 | 23 | def log_info(text): 24 | lock.acquire() 25 | logger.info(text) 26 | lock.release() 27 | 28 | def log_warning(text): 29 | lock.acquire() 30 | logger.warning(text) 31 | lock.release() 32 | 33 | def log_error(text): 34 | lock.acquire() 35 | logger.error(text) 36 | lock.release() 37 | -------------------------------------------------------------------------------- /someip_fuzzer/template.py: -------------------------------------------------------------------------------- 1 | from scapy.all import * 2 | from someip_fuzzer.config import config 3 | from someip_fuzzer.log import log_info 4 | import binascii 5 | import json 6 | import pprint 7 | 8 | load_contrib("automotive.someip") 9 | 10 | class Template(): 11 | 12 | def read_capture(self): 13 | plist = sniff(filter=config["Fuzzer"]["Filter"], prn=self.log_packet, offline=config["Fuzzer"]["Trace"]) 14 | return plist 15 | 16 | @staticmethod 17 | def log_packet(packet): 18 | log_info(packet.summary()) 19 | 20 | def create_template(self, packets): 21 | template = {} 22 | while len(packets): 23 | packet = packets.pop(0) 24 | layer = [layer.name for layer in self.__count_layers(packet)] 25 | log_info(layer) 26 | payload = packet.getlayer(3) # gets SOME/IP layer and below 27 | outgoing = packet["UDP"].dport == int(config["Service"]["Port"]) 28 | self.__add_to_template(template, outgoing, payload) 29 | return template 30 | 31 | def save_template(self, template): 32 | template_json = [] 33 | for key, value in template.items(): 34 | template = { 35 | "outgoing": key[0], # true, false 36 | "layer": key[1], # SOMEIP, SOMEIP-SD, etc. 37 | "fields": value["fields"] # srv_id, sub_id, method_id, event_id, etc. 38 | } 39 | template_json.append(template) 40 | with open(config["Fuzzer"]["Template"], "w") as outfile: 41 | json.dump(template_json, outfile, indent = 4, cls=TemplateEncoder) 42 | 43 | def print_template(self, template): 44 | template_json = [] 45 | for key, value in template.items(): 46 | template = { 47 | "outgoing": key[0], 48 | "layer": key[1], 49 | "fields": value["fields"] 50 | } 51 | template_json.append(template) 52 | print(json.dumps(template_json, default=str, indent=4, sort_keys=False)) 53 | 54 | def read_template(self): 55 | with open(config["Fuzzer"]["Template"], "r") as infile: 56 | template_json = json.load(infile) 57 | template = {} 58 | for item in template_json: 59 | template[(item["outgoing"], item["layer"])] = {"fields": item["fields"]} 60 | return template 61 | 62 | def __add_to_template(self, template, outgoing, payload): 63 | key = (outgoing, type(payload).__name__) # example: (True, SOMEIP) 64 | if key not in template: 65 | template[key] = {} 66 | paket_layers = [layer.name for layer in self.__count_layers(payload)] 67 | template_layer = template[key] 68 | if "fields" not in template_layer: 69 | fields = {} 70 | for paket_layer in paket_layers: 71 | for name, value in payload.getlayer(paket_layer).fields.items(): 72 | fields[name] = { 73 | "values": set(), 74 | "type": type(payload[paket_layer].get_field(name)).__name__, # ugly, but we need to get Scapy data types 75 | "fuzzing": {"fuzzer": None}, 76 | } 77 | template_layer["fields"] = fields 78 | try: 79 | for name, value in payload.getlayer(paket_layer).fields.items(): 80 | template_layer["fields"][name]["values"].add(value) # add protocol layer field value, e.g. srv_id -> 4660 81 | except TypeError: 82 | print("Unhashable type") 83 | 84 | def __count_layers(self, packet): 85 | counter = 0 86 | while True: 87 | layer = packet.getlayer(counter) 88 | if layer is None: 89 | break 90 | yield layer 91 | counter += 1 92 | 93 | class TemplateEncoder(json.JSONEncoder): 94 | def default(self, obj): 95 | if isinstance(obj, set): 96 | return list(obj) 97 | if isinstance(obj, bytes): 98 | return binascii.hexlify(obj).decode("utf-8") 99 | return json.JSONEncoder.default(self, obj) 100 | -------------------------------------------------------------------------------- /someip_fuzzer/types.py: -------------------------------------------------------------------------------- 1 | class NoHostError(Exception): 2 | pass 3 | 4 | class NoHeartbeatError(Exception): 5 | pass 6 | 7 | class NoSudoError(Exception): 8 | pass 9 | 10 | class ServiceShutdown(Exception): 11 | pass 12 | --------------------------------------------------------------------------------