UDS (ISO 14229)
The diagnostic language an ECU speaks over CAN. One-byte service ids, a positive response that echoes the service plus 0x40, and a negative response whose reason code tells you exactly which gate refused you.
UDS (Unified Diagnostic Services, ISO 14229) is the request/response protocol a workshop tool speaks to an ECU. It is an application layer: it needs a transport underneath, which on a vehicle is almost always Wiki: iso-tp over Wiki: can-bus. Every request starts with a one-byte service id, every positive answer echoes that service id plus 0x40, and every refusal is three bytes that name the exact reason.
What it is
UDS defines the services a diagnostic client may ask an ECU to perform: read a record, write a record, reset, run a routine, download new software. It says nothing about how the bytes travel. On CAN the pairing is fixed by convention:
| Address | Meaning |
|---|---|
0x7E0 to 0x7E7 |
physical request, addressed to one specific ECU |
0x7E8 to 0x7EF |
the matching response ids (request id + 8) |
0x7DF |
functional request, broadcast to every diagnostic ECU on the bus |
A request is SID [parameters]. A positive response is SID+0x40 [data]. A negative response is always three bytes: 7F <SID> <NRC>.
The same protocol also runs over DoIP (diagnostics over IP, ISO 13400) on newer vehicles and over K-line on old ones. The service ids and reason codes are identical; only the transport changes.
The services you actually meet
| Service | SID | Positive | What it does |
|---|---|---|---|
| DiagnosticSessionControl | 0x10 |
0x50 |
Switch session: 01 default, 02 programming, 03 extended |
| ECUReset | 0x11 |
0x51 |
Reboot the ECU (01 hard reset, 03 soft reset) |
| ReadDataByIdentifier | 0x22 |
0x62 |
Read a record by its 2-byte DID |
| SecurityAccess | 0x27 |
0x67 |
The seed/key challenge that unlocks privileged functions |
| WriteDataByIdentifier | 0x2E |
0x6E |
Write a record by DID |
| RoutineControl | 0x31 |
0x71 |
Start, stop, or read the result of a built-in routine |
| RequestDownload | 0x34 |
0x74 |
Open a transfer to write memory or flash |
| TesterPresent | 0x3E |
0x7E |
Keepalive that stops the session timing out |
Most services take a subfunction as their first parameter byte. Bit 7 of that byte is the suppressPosRspMsgIndicationBit: sending 3E 80 instead of 3E 00 asks the ECU to stay silent on success, which is how a keepalive avoids flooding the bus. A non-default session dies after roughly five seconds (the S3 timer) unless a TesterPresent keeps it open, and that single fact explains most "it worked in the terminal but not in my script" reports.
SecurityAccess, the gate
0x27 runs as paired subfunctions: an odd one requests a seed, and the even one immediately above it sends the key back.
27 01 -> 67 01 <seed bytes> requestSeed, level 1
27 02 <key> -> 67 02 sendKey, level 1 granted
Higher levels (03/04, 05/06, ...) exist and usually guard more dangerous services. Two behaviours are worth knowing before you conclude anything from a capture: an ECU that is already unlocked answers requestSeed with an all-zero seed, and a wrong key consumes the seed, so you must request a fresh one for every attempt.
The key is computed from the seed by a fixed manufacturer transform. It is not a cryptographic protocol, it is a shared secret in the shape of a small function, and its whole security rests on that function staying inside the vendor. It rarely does: the transform lives in a diagnostic tool, in an ECU firmware image, or in a leaked specification, and recovering it from any of those is the real work. Once recovered, passing the gate is mechanical.
Negative response codes
The NRC is the most useful byte in the protocol because it distinguishes "not here" from "not yet".
| NRC | Name | What it really tells you |
|---|---|---|
0x11 |
serviceNotSupported | This ECU does not implement the service at all |
0x12 |
subFunctionNotSupported | Right service, wrong subfunction |
0x13 |
incorrectMessageLengthOrInvalidFormat | Your length is wrong, check the ISO-TP framing |
0x22 |
conditionsNotCorrect | Valid request, wrong state (no session, no seed requested) |
0x24 |
requestSequenceError | Right steps, wrong order |
0x31 |
requestOutOfRange | The DID or address does not exist |
0x33 |
securityAccessDenied | It exists, you are not unlocked |
0x35 |
invalidKey | Your key was wrong; the seed is now burned |
0x36 |
exceedNumberOfAttempts | Too many wrong keys, you are locked out |
0x37 |
requiredTimeDelayNotExpired | Locked out and still serving the delay |
0x78 |
responsePending | Working on it, extend your timeout |
0x7F |
serviceNotSupportedInActiveSession | Exists, but not in the session you are in |
0x78 is not an error. The ECU may repeat it many times while a slow operation runs, and a client that treats it as a failure will abort a request that was about to succeed.
How to work it
The raw path uses can-utils and reads exactly like the protocol:
sudo ip link set can0 up type can bitrate 500000
isotpsend -s 7E0 -d 7E8 can0 <<< "10 03" # enter the extended session
isotprecv -s 7E0 -d 7E8 can0 | xxd # in a second terminal, before you send
The library path is udsoncan on top of can-isotp, and it handles the response matching, the 0x78 waiting, and the NRC decoding for you:
import isotp, udsoncan
from udsoncan.connections import PythonIsoTpConnection
from udsoncan.client import Client
s = isotp.socket()
s.bind("can0", isotp.Address(rxid=0x7E8, txid=0x7E0))
with Client(PythonIsoTpConnection(s), request_timeout=2) as client:
client.change_session(0x03)
client.unlock_security_access(1) # needs config["security_algo"]
print(client.read_data_by_identifier(0xF190))
unlock_security_access only works once you give the client your recovered transform: set client.config["security_algo"] to a callable taking (level, seed, params) and returning the key bytes. Without it the library has nothing to send.
For discovery rather than exploitation, caringcaribou scans for responding ids and supported services (caringcaribou uds discovery, then uds services), which is the fastest way to find out which of the ids above are alive on a bus you do not have documentation for.
Pitfalls
- Physical versus functional. SecurityAccess and most reads must be addressed to the ECU on its physical id. Broadcasting them to
0x7DFoften gets silence, which reads like "no such service" and is not. - The session times out. Without TesterPresent you drop back to the default session mid-attack and every later request answers
0x7F. - A burned seed. After a wrong key, requesting the same seed again and replaying an old key pair proves nothing. Read the fresh seed every time.
- Lockout is real.
0x36and0x37mean the ECU is counting your failures, often across power cycles. Brute forcing a 4-byte key over UDS is not a plan. - Writes and routines are not reversible.
0x2E,0x31and0x34change a real vehicle. Bench harness only.