etc~ scripts and random things |
| git clone https://github.com/djnnvx/etc.git |
| log | files | refs | archive |
doip.py (8932B)
1 #!/usr/bin/env python3 2 """ 3 Minimal DoIP / UDS client (ISO 13400, ISO 14229). 4 5 ./doip.py discover 6 ./doip.py info 7 ./doip.py send 22 F1 90 8 ./doip.py scan 9 ./doip.py shell 10 11 In shell: type hex to send UDS, .ta <addr> to retarget an ECU behind the 12 gateway, .scan to enumerate them, .info, .q to quit. 13 Raw hex is raw. 0x11 resets, 0x2E writes, 0x31 runs routines. 14 """ 15 import argparse, socket, struct, sys 16 17 V = 0x03 18 ACT = {0x00: 'unknown source address', 0x01: 'all sockets active', 0x02: 'SA differs from activated', 19 0x03: 'SA registered elsewhere', 0x04: 'missing authentication', 0x05: 'rejected confirmation', 20 0x06: 'unsupported activation type', 0x07: 'encrypted link required', 21 0x10: 'activated', 0x11: 'confirmation required'} 22 NRC = {0x10: 'generalReject', 0x11: 'serviceNotSupported', 0x12: 'subFunctionNotSupported', 23 0x13: 'incorrectMessageLengthOrInvalidFormat', 0x14: 'responseTooLong', 0x21: 'busyRepeatRequest', 24 0x22: 'conditionsNotCorrect', 0x24: 'requestSequenceError', 0x25: 'noResponseFromSubnetComponent', 25 0x31: 'requestOutOfRange', 0x33: 'securityAccessDenied', 0x34: 'authenticationRequired', 26 0x35: 'invalidKey', 0x36: 'exceedNumberOfAttempts', 0x37: 'requiredTimeDelayNotExpired', 27 0x70: 'uploadDownloadNotAccepted', 0x72: 'generalProgrammingFailure', 0x78: 'responsePending', 28 0x7E: 'subFunctionNotSupportedInActiveSession', 0x7F: 'serviceNotSupportedInActiveSession'} 29 DIDS = {0xF190: 'VIN', 0xF191: 'vehicleManufacturerECUHardwareNumber', 0xF193: 'hardwareVersion', 30 0xF194: 'softwareIdentification', 0xF195: 'softwareVersion', 0xF197: 'systemNameOrEngineType', 31 0xF18C: 'ECUSerialNumber', 0xF1A0: 'vehicleManufacturerSpecific', 0xF186: 'activeDiagnosticSession'} 32 33 34 def hdr(pt, pl=b''): 35 return struct.pack('>BBHI', V, 0xFF ^ V, pt, len(pl)) + pl 36 37 38 def rd(sock): 39 h = sock.recv(8) 40 if len(h) < 8: 41 return None, b'' 42 _, _, pt, ln = struct.unpack('>BBHI', h) 43 b = b'' 44 while len(b) < ln: 45 c = sock.recv(ln - len(b)) 46 if not c: 47 break 48 b += c 49 return pt, b 50 51 52 def udp(target, ptype, timeout=3): 53 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 54 s.settimeout(timeout) 55 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 56 s.sendto(hdr(ptype), (target, 13400)) 57 try: 58 d, a = s.recvfrom(4096) 59 except socket.timeout: 60 return None, b'', None 61 _, _, pt, ln = struct.unpack('>BBHI', d[:8]) 62 return pt, d[8:8 + ln], a[0] 63 64 65 def discover(bcast): 66 pt, b, ip = udp(bcast, 0x0001) 67 if pt != 0x0004: 68 print('no vehicle announcement'); return None 69 print(f'entity : {ip}') 70 print(f'VIN : {b[0:17].decode("ascii", "replace")}') 71 print(f'logical address : 0x{int.from_bytes(b[17:19], "big"):04X}') 72 print(f'EID / GID : {b[19:25].hex(":")} / {b[25:31].hex(":")}') 73 fa = b[31] 74 print(f'further action : 0x{fa:02X}' + (' (routing activation required)' if fa else ' (none)')) 75 return ip 76 77 78 def info(target): 79 for pt, name in ((0x4001, 'entity status'), (0x4003, 'power mode')): 80 rt, b, _ = udp(target, pt) 81 print(f'{name:15} : {b.hex() if b else "(no reply)"}') 82 if rt == 0x4002 and len(b) >= 7: 83 print(f' node_type=0x{b[0]:02X} max_sockets={b[1]} open={b[2]} ' 84 f'max_data={int.from_bytes(b[3:7], "big")}') 85 86 87 class Conn: 88 def __init__(self, target, sa, timeout): 89 self.sa, self.timeout = sa, timeout 90 self.s = socket.create_connection((target, 13400), timeout) 91 self.s.settimeout(timeout) 92 self.s.sendall(hdr(0x0005, struct.pack('>HB', sa, 0) + b'\x00\x00\x00\x00')) 93 pt, b = rd(self.s) 94 if pt != 0x0006 or len(b) < 5: 95 raise SystemExit(f'routing activation failed: ptype=0x{pt:04x} {b.hex()}') 96 ta, ea, code = struct.unpack('>HHB', b[:5]) 97 print(f'[+] routing activation: tester=0x{ta:04X} entity=0x{ea:04X} code=0x{code:02X} ({ACT.get(code, "?")})') 98 if code != 0x10: 99 raise SystemExit('not activated') 100 101 def uds(self, ta, payload, quiet=False): 102 self.s.sendall(hdr(0x8001, struct.pack('>HH', self.sa, ta) + payload)) 103 while True: 104 try: 105 pt, b = rd(self.s) 106 except socket.timeout: 107 return None 108 if pt == 0x0007: # AliveCheckRequest 109 self.s.sendall(hdr(0x0008, struct.pack('>H', self.sa))) 110 continue 111 if pt == 0x8002: 112 continue 113 if pt == 0x8003: 114 if not quiet: 115 print(f' diag NACK 0x{b[4]:02X}' if len(b) > 4 else f' diag NACK {b.hex()}') 116 return None 117 if pt != 0x8001: 118 if not quiet: 119 print(f' ptype=0x{pt:04x} {b.hex()}') 120 return None 121 r = b[4:] 122 if len(r) >= 3 and r[0] == 0x7F and r[2] == 0x78: 123 continue 124 return r 125 126 127 def show(r): 128 if r is None: 129 print(' (no response)') 130 elif len(r) >= 3 and r[0] == 0x7F: 131 print(f' NEGATIVE svc=0x{r[1]:02X} nrc=0x{r[2]:02X} {NRC.get(r[2], "?")}') 132 else: 133 txt = ''.join(chr(c) if 32 <= c < 127 else '.' for c in r) 134 print(f' {r.hex()}\n "{txt}"') 135 136 137 def scan(c, lo=0x0000, hi=0x0FFF): 138 """Pipelined: blast a window of TesterPresent then drain.""" 139 print(f'[*] scanning 0x{lo:04X}-0x{hi:04X} with 3E00 (TesterPresent)') 140 found = set() 141 for base in range(lo, hi + 1, 64): 142 for ta in range(base, min(base + 64, hi + 1)): 143 c.s.sendall(hdr(0x8001, struct.pack('>HH', c.sa, ta) + b'\x3e\x00')) 144 c.s.settimeout(0.6) 145 while True: 146 try: 147 pt, b = rd(c.s) 148 except socket.timeout: 149 break 150 if pt == 0x8001 and len(b) >= 5: 151 src, r = int.from_bytes(b[0:2], 'big'), b[4:] 152 if r[:1] == b'\x7e': 153 found.add((src, 'positive')) 154 elif r[:1] == b'\x7f': 155 found.add((src, f'nrc=0x{r[2]:02X} {NRC.get(r[2], "?")}')) 156 c.s.settimeout(c.timeout) 157 print(f'[+] {len(found)} responding logical addresses:') 158 for a, k in sorted(found): 159 print(f' 0x{a:04X} {k}') 160 return [a for a, _ in sorted(found)] 161 162 163 def ident(c, addrs): 164 for ta in addrs: 165 print(f'\n=== ECU 0x{ta:04X}') 166 for d, n in DIDS.items(): 167 r = c.uds(ta, b'\x22' + struct.pack('>H', d), quiet=True) 168 if r and r[0] == 0x62: 169 v = r[3:] 170 t = ''.join(chr(x) if 32 <= x < 127 else '.' for x in v) 171 print(f' {d:04X} {n:34} "{t[:48]}"') 172 173 174 def main(): 175 a = argparse.ArgumentParser() 176 a.add_argument('cmd', choices=['discover', 'info', 'send', 'scan', 'ident', 'shell']) 177 a.add_argument('data', nargs='*') 178 a.add_argument('-t', '--target', default='127.0.0.1') 179 a.add_argument('-b', '--broadcast', default='169.254.255.255') 180 a.add_argument('-s', '--sa', default='0x0EF4') 181 a.add_argument('-d', '--ta', default='0x0010') 182 a.add_argument('--timeout', type=float, default=4.0) 183 o = a.parse_args() 184 sa, ta = int(o.sa, 0), int(o.ta, 0) 185 186 if o.cmd == 'discover': 187 discover(o.broadcast); return 188 if o.cmd == 'info': 189 info(o.target); return 190 191 c = Conn(o.target, sa, o.timeout) 192 if o.cmd == 'send': 193 show(c.uds(ta, bytes.fromhex(''.join(o.data)))) 194 elif o.cmd == 'scan': 195 scan(c) 196 elif o.cmd == 'ident': 197 ident(c, [int(x, 0) for x in o.data] or scan(c)) 198 else: 199 print('hex to send, .ta <addr>, .scan, .ident, .info, .dids, .q') 200 while True: 201 try: 202 line = input(f'0x{ta:04X}> ').strip() 203 except (EOFError, KeyboardInterrupt): 204 break 205 if not line: 206 continue 207 if line in ('.q', '.quit'): 208 break 209 if line.startswith('.ta'): 210 ta = int(line.split()[1], 0); continue 211 if line == '.scan': 212 scan(c); continue 213 if line == '.ident': 214 ident(c, [ta]); continue 215 if line == '.info': 216 info(o.target); continue 217 if line == '.dids': 218 for d, n in DIDS.items(): 219 r = c.uds(ta, b'\x22' + struct.pack('>H', d), quiet=True) 220 if r and r[0] == 0x62: 221 v = r[3:] 222 txt = ''.join(chr(x) if 32 <= x < 127 else '.' for x in v) 223 print(f' {d:04X} {n:42} {txt}') 224 continue 225 try: 226 show(c.uds(ta, bytes.fromhex(line.replace(' ', '')))) 227 except ValueError: 228 print(' not hex') 229 230 231 if __name__ == '__main__': 232 sys.exit(main())