CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/tools/hardware/elm327_relay.rb
Views: 1904
1
#!/usr/bin/env ruby
2
3
##
4
# This module requires Metasploit: https://metasploit.com/download
5
# Current source: https://github.com/rapid7/metasploit-framework
6
##
7
8
#
9
# ELM327 and STN1100 MCU interface to the Metasploit HWBridge
10
#
11
12
#
13
# This module requires a connected ELM327 or STN1100 is connected to
14
# the machines serial. Sets up a basic RESTful web server to communicate
15
#
16
# Requires MSF and the serialport gem to be installed.
17
# - `gem install serialport`
18
# - or, if using rvm: `rvm gemset install serialport`
19
#
20
21
### Non-typical gem ###
22
begin
23
require 'serialport'
24
rescue LoadError => e
25
gem = e.message.split.last
26
abort "#{gem} gem is not installed. Please install with `gem install #{gem}' or, if using rvm, `rvm gemset install #{gem}' and try again."
27
end
28
29
#
30
# Load our MSF API
31
#
32
33
msfbase = __FILE__
34
while File.symlink?(msfbase)
35
msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase))
36
end
37
$:.unshift(File.expand_path(File.join(File.dirname(msfbase), '..', '..', 'lib')))
38
require 'msfenv'
39
require 'rex'
40
require 'optparse'
41
42
# Prints with [*] that represents the message is a status
43
#
44
# @param msg [String] The message to print
45
# @return [void]
46
def print_status(msg='')
47
$stdout.puts "[*] #{msg}"
48
end
49
50
# Prints with [-] that represents the message is an error
51
#
52
# @param msg [String] The message to print
53
# @return [void]
54
def print_error(msg='')
55
$stdout.puts "[-] #{msg}"
56
end
57
58
# Base ELM327 Class for the Relay
59
module ELM327HWBridgeRelay
60
61
class ELM327Relay < Msf::Auxiliary
62
63
include Msf::Exploit::Remote::HttpServer::HTML
64
65
# @!attribute serial_port
66
# @return [String] The serial port device name
67
attr_accessor :serial_port
68
69
# @!attribute serial_baud
70
# @return [Integer] Baud rate of serial device
71
attr_accessor :serial_baud
72
73
# @!attribute serial_bits
74
# @return [Integer] Number of serial data bits
75
attr_accessor :serial_bits
76
77
# @!attribute serial_stop_bits
78
# @return [Integer] Stop bit
79
attr_accessor :serial_stop_bits
80
81
# @!attribute server_port
82
# @return [Integer] HTTP Relay server port
83
attr_accessor :server_port
84
85
def initialize(info={})
86
# Set some defaults
87
self.serial_port = "/dev/ttyUSB0"
88
self.serial_baud = 115200
89
begin
90
@opts = OptsConsole.parse(ARGV)
91
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
92
print_error("#{e.message} (please see -h)")
93
exit
94
end
95
96
if @opts.has_key? :server_port
97
self.server_port = @opts[:server_port]
98
else
99
self.server_port = 8080
100
end
101
102
super(update_info(info,
103
'Name' => 'ELM327/STN1100 HWBridge Relay Server',
104
'Description' => %q{
105
This module sets up a web server to bridge communications between
106
Metasploit and the EML327 or STN1100 chipset.
107
},
108
'Author' => [ 'Craig Smith' ],
109
'License' => MSF_LICENSE,
110
'Actions' =>
111
[
112
[ 'WebServer' ]
113
],
114
'PassiveActions' =>
115
[
116
'WebServer'
117
],
118
'DefaultAction' => 'WebServer',
119
'DefaultOptions' =>
120
{
121
'SRVPORT' => self.server_port,
122
'URIPATH' => "/"
123
}))
124
self.serial_port = @opts[:serial] if @opts.has_key? :serial
125
self.serial_baud = @opts[:baud].to_i if @opts.has_key? :baud
126
self.serial_bits = 8
127
self.serial_stop_bits = 1
128
@operational_status = 0
129
@ser = nil # Serial Interface
130
@device_name = ""
131
@packets_sent = 0
132
@last_sent = 0
133
@starttime = Time.now()
134
@supported_buses = [ { "bus_name" => "can0" } ]
135
end
136
137
# Sends a serial command to the ELM327. Automatically appends \r\n
138
#
139
# @param cmd [String] Serial AT command for ELM327
140
# @return [String] Response between command and '>' prompt
141
def send_cmd(cmd)
142
@ser.write(cmd + "\r\n")
143
resp = @ser.readline(">")
144
resp = resp[0, resp.length - 2]
145
resp.chomp!
146
resp
147
end
148
149
# Connects to the ELM327, resets parameters, gets device version and sets up general comms.
150
# Serial params are set via command options or during initialization
151
#
152
# @return [SerialPort] SerialPort object for communications. Also available as @ser
153
def connect_to_device()
154
begin
155
@ser = SerialPort.new(self.serial_port, self.serial_baud, self.serial_bits, self.serial_stop_bits, SerialPort::NONE)
156
rescue
157
$stdout.puts "Unable to connect to serial port. See -h for help"
158
exit -2
159
end
160
resp = send_cmd("ATZ") # Turn off ECHO
161
if resp =~ /ELM327/
162
send_cmd("ATE0") # Turn off ECHO
163
send_cmd("ATL0") # Disable linefeeds
164
@device_name = send_cmd("ATI")
165
send_cmd("ATH1") # Show Headers
166
@operational_status = 1
167
$stdout.puts("Connected. Relay is up and running...")
168
else
169
$stdout.puts("Connected but invalid ELM response: #{resp.inspect}")
170
@operational_status = 2
171
# Down the road we may make a way to re-init via the hwbridge but for now just exit
172
$stdout.puts("The device may not have been fully initialized, try reconnecting")
173
exit(-1)
174
end
175
@ser
176
end
177
178
# HWBridge Status call
179
#
180
# @return [Hash] Status return hash
181
def get_status()
182
status = Hash.new
183
status["operational"] = @operational_status
184
status["hw_specialty"] = { "automotive" => true }
185
status["hw_capabilities"] = { "can" => true}
186
status["last_10_errors"] = @last_errors # NOTE: no support for this yet
187
status["api_version"] = "0.0.1"
188
status["fw_version"] = "not supported"
189
status["hw_version"] = "not supported"
190
status["device_name"] = @device_name
191
status
192
end
193
194
# HWBridge Statistics Call
195
#
196
# @return [Hash] Statistics return hash
197
def get_statistics()
198
stats = Hash.new
199
stats["uptime"] = Time.now - @starttime
200
stats["packet_stats"] = @packets_sent
201
stats["last_request"] = @last_sent
202
volt = send_cmd("ATRV")
203
stats["voltage"] = volt.gsub(/V/,'')
204
stats
205
end
206
207
# HWBRidge DateTime Call
208
#
209
# @return [Hash] System DateTime Hash
210
def get_datetime()
211
{ "sytem_datetime" => Time.now() }
212
end
213
214
# HWBridge Timezone Call
215
#
216
# @return [Hash] System Timezone as String
217
def get_timezone()
218
{ "system_timezone" => Time.now.getlocal.zone }
219
end
220
221
# Returns supported buses. Can0 is always available
222
# TODO: Use custom methods to force non-standard buses such as kline
223
#
224
# @return [Hash] Hash of supported_buses
225
def get_supported_buses()
226
@supported_buses
227
end
228
229
# Sends CAN packet
230
#
231
# @param id [String] ID as a hex string
232
# @param data [String] String of HEX bytes to send
233
# @return [Hash] Success Hash
234
def cansend(id, data)
235
result = {}
236
result["success"] = false
237
id = "%03X" % id.to_i(16)
238
resp = send_cmd("ATSH#{id}")
239
if resp == "OK"
240
send_cmd("ATR0") # Disable response checks
241
send_cmd("ATCAF0") # Turn off ISO-TP formatting
242
else
243
return result
244
end
245
if data.scan(/../).size > 8
246
$stdout.puts("Error: Data size > 8 bytes")
247
return result
248
end
249
send_cmd(data)
250
@packets_sent += 1
251
@last_sent = Time.now()
252
if resp == "CAN ERROR"
253
result["success"] = false
254
return result
255
end
256
result["success"] = true
257
result
258
end
259
260
# Sends ISO-TP Packets
261
#
262
# @param srcid [String] Sender ID as hex string
263
# @param dstid [String] Responder ID as hex string
264
# @param data [String] Hex String of data to send
265
# @param timeout [Integer] Millisecond timeout, currently not implemented
266
# @param maxpkts [Integer] Max number of packets in response, currently not implemented
267
def isotpsend_and_wait(srcid, dstid, data, timeout, maxpkts)
268
result = {}
269
result["success"] = false
270
srcid = "%03X" % srcid.to_i(16)
271
dstid = "%03X" % dstid.to_i(16)
272
send_cmd("ATCAF1") # Turn on ISO-TP formatting
273
send_cmd("ATR1") # Turn on responses
274
send_cmd("ATSH#{srcid}") # Src Header
275
send_cmd("ATCRA#{dstid}") # Resp Header
276
send_cmd("ATCFC1"). # Enable flow control
277
resp = send_cmd(data)
278
@packets_sent += 1
279
@last_sent = Time.now()
280
if resp == "CAN ERROR"
281
result["success"] = false
282
return result
283
end
284
result["Packets"] = []
285
resp.split(/\r/).each do |line|
286
pkt = {}
287
if line=~/^(\w+) (.+)/
288
pkt["ID"] = $1
289
pkt["DATA"] = $2.split
290
end
291
result["Packets"] << pkt
292
end
293
result["success"] = true
294
result
295
end
296
297
# Generic Not supported call
298
#
299
# @return [Hash] Status not supported
300
def not_supported()
301
{ "status" => "not supported" }
302
end
303
304
# Handles incoming URI requests and calls their respective API functions
305
#
306
# @param cli [Socket] Socket for the browser
307
# @param request [Rex::Proto::Http::Request] HTTP Request sent by the browser
308
def on_request_uri(cli, request)
309
if request.uri =~ /status$/i
310
send_response_html(cli, get_status().to_json(), { 'Content-Type' => 'application/json' })
311
elsif request.uri =~ /statistics$/i
312
send_response_html(cli, get_stats().to_json(), { 'Content-Type' => 'applicaiton/json' })
313
elsif request.uri =~/settings\/datetime$/i
314
send_response_html(cli, get_datetime().to_json(), { 'Content-Type' => 'application/json' })
315
elsif request.uri =~/settings\/timezone$/i
316
send_response_html(cli, get_timezone().to_json(), { 'Content-Type' => 'application/json' })
317
# elsif request.uri =~/custom_methods$/i
318
# send_response_html(cli, get_custom_methods().to_json(), { 'Content-Type' => 'application/json' })
319
elsif request.uri =~/automotive/i
320
if request.uri =~/automotive\/supported_buses/i
321
send_response_html(cli, get_supported_buses().to_json(), { 'Content-Type' => 'application/json' })
322
elsif request.uri =~/automotive\/can0\/cansend/
323
params = CGI.parse(URI(request.uri).query)
324
if params.has_key? "id" and params.has_key? "data"
325
send_response_html(cli, cansend(params["id"][0], params["data"][0]).to_json(), { 'Content-Type' => 'application/json' })
326
else
327
send_response_html(cli, not_supported().to_json(), { 'Content-Type' => 'application/json' })
328
end
329
elsif request.uri =~/automotive\/can0\/isotpsend_and_wait/
330
params = CGI.parse(URI(request.uri).query)
331
if params.has_key? "srcid" and params.has_key? "dstid" and params.has_key? "data"
332
timeout = 1500
333
maxpkts = 3
334
timeout = params["timeout"][0] if params.has_key? "timeout"
335
maxpkts = params["maxpkts"][0] if params.has_key? "maxpkts"
336
send_response_html(cli, isotpsend_and_wait(params["srcid"][0], params["dstid"][0], params["data"][0], timeout, maxpkts).to_json(), { 'Content-Type' => 'application/json' })
337
else
338
send_response_html(cli, not_supported().to_json(), { 'Content-Type' => 'application/json' })
339
end
340
else
341
send_response_html(cli, not_supported().to_json(), { 'Content-Type' => 'application/json' })
342
end
343
else
344
send_response_html(cli, not_supported().to_json(), { 'Content-Type' => 'application/json' })
345
end
346
end
347
348
# Main run operation. Connects to device then runs the server
349
def run
350
connect_to_device()
351
exploit()
352
end
353
354
end
355
356
# This class parses the user-supplied options (inputs)
357
class OptsConsole
358
359
DEFAULT_BAUD = 115200
360
DEFAULT_SERIAL = "/dev/ttyUSB0"
361
362
# Returns the normalized user inputs
363
#
364
# @param args [Array] This should be Ruby's ARGV
365
# @raise [OptionParser::MissingArgument] Missing arguments
366
# @return [Hash] The normalized options
367
def self.parse(args)
368
parser, options = get_parsed_options
369
370
# Now let's parse it
371
# This may raise OptionParser::InvalidOption
372
parser.parse!(args)
373
374
options
375
end
376
377
# Returns the parsed options from ARGV
378
#
379
# raise [OptionParser::InvalidOption] Invalid option found
380
# @return [OptionParser, Hash] The OptionParser object and an hash containing the options
381
def self.get_parsed_options
382
options = {}
383
parser = OptionParser.new do |opt|
384
opt.banner = "Usage: #{__FILE__} [options]"
385
opt.separator ''
386
opt.separator 'Specific options:'
387
388
opt.on('-b', '--baud <serial_baud>',
389
"(Optional) Sets the baud speed for the serial device (Default=#{DEFAULT_BAUD})") do |v|
390
options[:baud] = v
391
end
392
393
opt.on('-s', '--serial <serial_device>',
394
"(Optional) Sets the serial device (Default=#{DEFAULT_SERIAL})") do |v|
395
options[:serial] = v
396
end
397
398
opt.on('-p', '--port <server_port>',
399
"(Optional) Sets the listening HTTP server port (Default=8080)") do |v|
400
options[:server_port] = v
401
end
402
403
opt.on_tail('-h', '--help', 'Show this message') do
404
$stdout.puts opt
405
exit
406
end
407
end
408
return parser, options
409
end
410
end
411
end
412
413
414
415
#
416
# Main
417
#
418
if __FILE__ == $PROGRAM_NAME
419
begin
420
bridge = ELM327HWBridgeRelay::ELM327Relay.new
421
bridge.run
422
rescue Interrupt
423
$stdout.puts("Shutting down")
424
end
425
end
426
427
428