[ Reverse Engineering UDS with JScan ]
Original walkthrough by jmccorm; page polish, schema, and cross-links by magikh0e. The technique is the useful part — using a known-good UDS client (JScan in this case) as a stimulus, capturing what it sends, and decoding the protocol byte-by-byte. Same approach works with any UDS-speaking tool: wiTECH, Autel, Launch X431, Topdon. JScan is just the example.
[ Why UDS ]
Many times you'll want to use the numbers you already find on the bus because they're there for free. You don't have to do anything special to make them update, they just do. But there is more advanced information and more advanced controls available if you're willing to do some work. That's UDS.
When you come across a tool that does something interesting — be it a Tazer, JScan, or a semi-professional vehicle diagnostic tool — you can often capture and replicate that behaviour. In this case we used JScan. Connect JScan up to your vehicle and confirm it's working before you start sniffing.
[ Setup ]
With a Raspberry Pi (or any Linux host with a CAN adapter), open two terminal windows. Both will receive data; you don't need to send anything yet, JScan handles the sending.
First window — ISO-TP reassembly:
isotpdump -s 620 -d 504 -c -ta -u any
Second window — raw CAN with a filter that catches most FCA module response IDs while suppressing the noisier broadcast traffic:
candump any,400:400 | egrep -v "can.*4[01234]. "
These two windows will let you see what's happening when you trigger actions in JScan.
In JScan, click Modules then Heat Ventilation and A/C. Click Live Data and select Battery Voltage - (V). This gives us the battery voltage as detected at the HVAC Module. After selecting just the one item, hit OK. Then press the green Play button in the lower-right corner. It should start streaming the live voltage data to your screen — "14.4", "14.5", "14.4"...
Now go back and look at the terminal windows. What do you see? The first window has nothing. The second one has a flood of traffic like this:
can0 783 [8] 03 22 D0 20 00 00 00 00 can0 503 [8] 05 62 D0 20 91 91 00 00 can0 783 [8] 03 22 D0 20 00 00 00 00 can0 503 [8] 05 62 D0 20 8F 8F 00 00
On the first window, you'll need to adjust some parameters because
the HVAC unit expects its commands to be sent with an ID of $783 and it
sends replies back on $503. Adjust the -s and -d
arguments accordingly:
isotpdump -s 783 -d 503 -c -ta -u any
[ Reading a value (Service 0x22) ]
Now let's break down the traffic we just saw. The tester-side request first:
can0 783 [8] 03 22 D0 20 00 00 00 00
03 "I have a message that's three bytes long." (PCI length)
22 "I want to read a vehicle parameter by identifier."
(UDS Service 0x22 -- ReadDataByIdentifier)
D0 20 "That particular identifier is $D020."
00 ... Padding out the 8-byte CAN frame.
And the HVAC module's reply:
can0 503 [8] 05 62 D0 20 91 91 00 00
05 "I have a reply that's five bytes long."
62 "Your command of 22 was successful."
(positive response = request_service + 0x40, so 22 + 40 = 62)
D0 20 "You requested identifier $D020."
91 91 "And its value is 91. Let me repeat that! 91."
(anything after those 5 meaningful bytes is padding)
$91 is the number 145 decimal. And that was also our voltage as shown on the screen by JScan. Or at least close enough — 14.5 volts. The module is reporting the voltage in tenths of a volt, encoded as the first useful byte of the response.
[ Activations (Service 0x2F) ]
You might also see things in JScan called Activations. These are usually the same shape as the read we just did, but instead of asking for what an identifier IS, you're telling the vehicle what an identifier should be SET to.
NOTE: Some Activations may work by Service 0x2E (WriteDataByIdentifier), which writes a given value into the provided identifier. These are often used to change permanent vehicle parameters in ECU NVRAM. Take care to use Service 0x22 to READ the data by identifier first, BEFORE you write — that way you can back out your changes if something goes wrong.
Let's go to the Body Control Module → Activations → Honk. Honking the horn uses Service 0x2F (IOControlByIdentifier), which is short-term takeover — safer to use than 0x2E because it doesn't write to NVRAM. We turn the horn on and off and see something close to this:
can1 620 [8] 02 10 03 00 00 00 00 00 can1 504 [8] 06 50 03 00 14 00 C8 03 can1 620 [8] 05 2F D0 AD 03 01 00 00 can1 504 [8] 04 6F D0 AD 03 00 C8 03 can1 620 [8] 05 2F D0 AD 03 00 00 00 can1 504 [8] 04 6F D0 AD 03 00 C8 03
Let me decode that, a little less verbosely than before.
Frame 1 — tester requests an Extended Diagnostic Session:
can1 620 [8] 02 10 03 00 00 00 00 00
02 "I have a message that's two bytes long."
10 "I want to control this diagnostic session."
(UDS Service 0x10 -- DiagnosticSessionControl)
03 "I want an extended session where more features are unlocked."
Frame 2 — BCM grants it:
can1 504 [8] 06 50 03 00 14 00 C8 03 06 "I have a reply that's six bytes long." 50 "Your command 10 was successful." (10 + 40 = 50) 03 "You wanted an extended session." 00 14 00 C8 Session-timing parameters (P2 / P2*). 03 Padding -- ignore.
The BCM just granted permission to unlock some slightly restricted diagnostic features, but only for a limited amount of time. This gives the tester permission to honk the horn (through software) and to do other disruptive things.
Frame 3 — tester turns the horn on:
can1 620 [8] 05 2F D0 AD 03 01 00 00
05 "I have a message that's five bytes long."
2F "I want input/output control of a device by identifier."
(UDS Service 0x2F -- IOControlByIdentifier)
D0 AD "It's the horn." (DID $D0AD)
03 "shortTermAdjustment" (control byte)
01 "Turn it on!"
Frame 4 — BCM confirms:
can1 504 [8] 04 6F D0 AD 03 00 C8 03 04 6F D0 AD 03 "Horn status was successfully changed." (2F + 40 = 6F)
If we're quick enough, we're still in our Extended Diagnostic Session. If not, we'd need to repeat the Session Control command. Happily only 0.01 seconds have passed, so we don't need to extend the session before unhonking:
Frame 5 — tester turns the horn off:
can1 620 [8] 05 2F D0 AD 03 00 00 00 05 "I have a message that's five bytes long." 2F "I want input/output control of a device by identifier." D0 AD "It's the horn." 03 "shortTermAdjustment" 00 "Turn it off already!"
Frame 6 — BCM confirms:
can1 504 [8] 04 6F D0 AD 03 00 C8 03 04 6F D0 AD 03 "Horn status was successfully changed."
[ Decoding the traffic — rules of thumb ]
From the walkthrough above, three rules carry across every UDS service you'll meet on this platform:
1. First byte of every CAN frame is the PCI length -- "this many UDS
bytes follow". Anything past that count is padding, ignore it.
2. Positive response service byte = request service byte + 0x40.
0x22 (ReadDataByIdentifier) -> 0x62
0x10 (DiagnosticSessionControl) -> 0x50
0x2F (IOControlByIdentifier) -> 0x6F
0x2E (WriteDataByIdentifier) -> 0x6E
0x3E (TesterPresent) -> 0x7E
3. Negative response service byte is always 0x7F, followed by the
original service byte and a one-byte Negative Response Code (NRC):
0x7F 22 33 request to read failed: securityAccessDenied
0x7F 2F 7E request to IOControl failed: subFunctionNotSupportedInActiveSession
0x7F 10 12 request to change session failed: subFunctionNotSupported
The horn's request/response pair ($620 / $504) is the same one the 3rd_brakelight.sh script uses — both actuators live on the BCM. The HVAC module sits on a different pair ($783 / $503). Different ECUs use different ID pairs; the request/response delta isn't a fixed constant on FCA platforms, so capture-then-decode (as above) is the reliable way to discover each pair.
[ Scripting it with isotpsend ]
The hand-built cansend approach is great for learning
— you see every byte, including the PCI length byte and the
trailing padding. For anything beyond a demo, though, you want
isotpsend: it handles ISO-TP framing for you so you can
just provide the UDS service bytes and stop padding by hand.
Here's a script ("3honk") that honks the horn three times via the same UDS flow we walked through above, but using isotpsend instead of cansend. We're almost certain to build on this pattern later.
#!/bin/bash # Wake up the CAN bus if needed (at the cost of 0.2 seconds) cansend can0 2D3#0700000000000000 sleep 0.2 # Enter Extended Diagnostic Session via Service 0x10 sub 0x03. echo "10 03" | isotpsend -s 620 -d 504 -p 00: -P a can1 for i in 1 2 3 do echo HONK echo "2F D0 AD 03 01" | isotpsend -s 620 -d 504 -p 00:00 -P a can1 sleep 0.05 echo UNHONK echo "2F D0 AD 03 00" | isotpsend -s 620 -d 504 -p 00:00 -P a can1 sleep 0.1 done
Same horn, same DID, same byte sequences — but no manual
8-byte padding, no PCI length byte to remember. isotpsend
prepends the PCI byte for you and pads short payloads to a full CAN
frame via -P a ("always pad") and -p 00: /
-p 00:00 ("pad with 0x00 in both directions").
Note: the script assumes the vehicle is already awake. If not, the
cansend wake frame at the top will knock it awake for you.
In another window, use either (or both) of these commands to watch what's happening at different levels of the messaging stack:
isotpdump -s 620 -d 504 -c -ta -u any candump any,400:400 | egrep -v "can.*4[01234]. "
This pattern — isotpsend on the request side, isotpdump on the receive side — is the right scaffolding for any UDS work that involves payloads longer than 7 bytes. Once your request needs an FF + CF + Flow-Control exchange, cansend stops being viable; isotpsend keeps working unchanged.
This script lives on-disk as 3honk.sh with the inline comments expanded. The companion horn.sh shows the same flow in fully-polished cansend form (SIGINT trap to silence the horn on exit, CLI flags for press/burst modes) for when you want the safety scaffolding more than the protocol clarity.
[ Next steps ]
Things can get more complex than this, particularly when there's a message longer than seven bytes (usually one of the module's replies). Multi-frame ISO-TP framing — First Frame + Consecutive Frames + Flow Control — is documented in detail at SniffingCanbus.txt.
The formalized version of the "five quick taps of the horn" recipe that jmccorm originally wrote is on this site as horn.sh — same byte sequences, wrapped in safety guards (SIGINT trap to silence the horn, returnControlToECU on exit, CLI flags for press vs burst modes). The same scaffold is reused by 3rd_brakelight.sh for the brake light — only the DID changes.
For Python-side UDS work — once you've decoded a few requests
manually and want to script larger workflows — reach for
udsoncan.
Higher-level method calls like
client.read_data_by_identifier(0xD020) instead of building
the 0x22 request byte-by-byte.
For automated recon (find live ECUs, sweep DIDs, dump DTCs), caringcaribou is the attack-toolkit layer built on top of udsoncan — useful when you want to know what modules a vehicle exposes before deciding which one to dig into manually.
On 2018+ FCA / Stellantis vehicles, UDS over the OBD-II port is gated by the Secure Gateway Module. The behind-the-glovebox 13-way connectors bypass it; the OBD-II port (without AutoAuth subscription) is effectively read-only. See the SGW guide for details on which UDS services pass through unauthenticated and which don't.
