├── .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 | [](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 | 
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 |
--------------------------------------------------------------------------------