Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/tools/hardware/elm327_relay.rb
Views: 11766
#!/usr/bin/env ruby12##3# This module requires Metasploit: https://metasploit.com/download4# Current source: https://github.com/rapid7/metasploit-framework5##67#8# ELM327 and STN1100 MCU interface to the Metasploit HWBridge9#1011#12# This module requires a connected ELM327 or STN1100 is connected to13# the machines serial. Sets up a basic RESTful web server to communicate14#15# Requires MSF and the serialport gem to be installed.16# - `gem install serialport`17# - or, if using rvm: `rvm gemset install serialport`18#1920### Non-typical gem ###21begin22require 'serialport'23rescue LoadError => e24gem = e.message.split.last25abort "#{gem} gem is not installed. Please install with `gem install #{gem}' or, if using rvm, `rvm gemset install #{gem}' and try again."26end2728#29# Load our MSF API30#3132msfbase = __FILE__33while File.symlink?(msfbase)34msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase))35end36$:.unshift(File.expand_path(File.join(File.dirname(msfbase), '..', '..', 'lib')))37require 'msfenv'38require 'rex'39require 'optparse'4041# Prints with [*] that represents the message is a status42#43# @param msg [String] The message to print44# @return [void]45def print_status(msg='')46$stdout.puts "[*] #{msg}"47end4849# Prints with [-] that represents the message is an error50#51# @param msg [String] The message to print52# @return [void]53def print_error(msg='')54$stdout.puts "[-] #{msg}"55end5657# Base ELM327 Class for the Relay58module ELM327HWBridgeRelay5960class ELM327Relay < Msf::Auxiliary6162include Msf::Exploit::Remote::HttpServer::HTML6364# @!attribute serial_port65# @return [String] The serial port device name66attr_accessor :serial_port6768# @!attribute serial_baud69# @return [Integer] Baud rate of serial device70attr_accessor :serial_baud7172# @!attribute serial_bits73# @return [Integer] Number of serial data bits74attr_accessor :serial_bits7576# @!attribute serial_stop_bits77# @return [Integer] Stop bit78attr_accessor :serial_stop_bits7980# @!attribute server_port81# @return [Integer] HTTP Relay server port82attr_accessor :server_port8384def initialize(info={})85# Set some defaults86self.serial_port = "/dev/ttyUSB0"87self.serial_baud = 11520088begin89@opts = OptsConsole.parse(ARGV)90rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e91print_error("#{e.message} (please see -h)")92exit93end9495if @opts.has_key? :server_port96self.server_port = @opts[:server_port]97else98self.server_port = 808099end100101super(update_info(info,102'Name' => 'ELM327/STN1100 HWBridge Relay Server',103'Description' => %q{104This module sets up a web server to bridge communications between105Metasploit and the EML327 or STN1100 chipset.106},107'Author' => [ 'Craig Smith' ],108'License' => MSF_LICENSE,109'Actions' =>110[111[ 'WebServer' ]112],113'PassiveActions' =>114[115'WebServer'116],117'DefaultAction' => 'WebServer',118'DefaultOptions' =>119{120'SRVPORT' => self.server_port,121'URIPATH' => "/"122}))123self.serial_port = @opts[:serial] if @opts.has_key? :serial124self.serial_baud = @opts[:baud].to_i if @opts.has_key? :baud125self.serial_bits = 8126self.serial_stop_bits = 1127@operational_status = 0128@ser = nil # Serial Interface129@device_name = ""130@packets_sent = 0131@last_sent = 0132@starttime = Time.now()133@supported_buses = [ { "bus_name" => "can0" } ]134end135136# Sends a serial command to the ELM327. Automatically appends \r\n137#138# @param cmd [String] Serial AT command for ELM327139# @return [String] Response between command and '>' prompt140def send_cmd(cmd)141@ser.write(cmd + "\r\n")142resp = @ser.readline(">")143resp = resp[0, resp.length - 2]144resp.chomp!145resp146end147148# Connects to the ELM327, resets parameters, gets device version and sets up general comms.149# Serial params are set via command options or during initialization150#151# @return [SerialPort] SerialPort object for communications. Also available as @ser152def connect_to_device()153begin154@ser = SerialPort.new(self.serial_port, self.serial_baud, self.serial_bits, self.serial_stop_bits, SerialPort::NONE)155rescue156$stdout.puts "Unable to connect to serial port. See -h for help"157exit -2158end159resp = send_cmd("ATZ") # Turn off ECHO160if resp =~ /ELM327/161send_cmd("ATE0") # Turn off ECHO162send_cmd("ATL0") # Disable linefeeds163@device_name = send_cmd("ATI")164send_cmd("ATH1") # Show Headers165@operational_status = 1166$stdout.puts("Connected. Relay is up and running...")167else168$stdout.puts("Connected but invalid ELM response: #{resp.inspect}")169@operational_status = 2170# Down the road we may make a way to re-init via the hwbridge but for now just exit171$stdout.puts("The device may not have been fully initialized, try reconnecting")172exit(-1)173end174@ser175end176177# HWBridge Status call178#179# @return [Hash] Status return hash180def get_status()181status = Hash.new182status["operational"] = @operational_status183status["hw_specialty"] = { "automotive" => true }184status["hw_capabilities"] = { "can" => true}185status["last_10_errors"] = @last_errors # NOTE: no support for this yet186status["api_version"] = "0.0.1"187status["fw_version"] = "not supported"188status["hw_version"] = "not supported"189status["device_name"] = @device_name190status191end192193# HWBridge Statistics Call194#195# @return [Hash] Statistics return hash196def get_statistics()197stats = Hash.new198stats["uptime"] = Time.now - @starttime199stats["packet_stats"] = @packets_sent200stats["last_request"] = @last_sent201volt = send_cmd("ATRV")202stats["voltage"] = volt.gsub(/V/,'')203stats204end205206# HWBRidge DateTime Call207#208# @return [Hash] System DateTime Hash209def get_datetime()210{ "sytem_datetime" => Time.now() }211end212213# HWBridge Timezone Call214#215# @return [Hash] System Timezone as String216def get_timezone()217{ "system_timezone" => Time.now.getlocal.zone }218end219220# Returns supported buses. Can0 is always available221# TODO: Use custom methods to force non-standard buses such as kline222#223# @return [Hash] Hash of supported_buses224def get_supported_buses()225@supported_buses226end227228# Sends CAN packet229#230# @param id [String] ID as a hex string231# @param data [String] String of HEX bytes to send232# @return [Hash] Success Hash233def cansend(id, data)234result = {}235result["success"] = false236id = "%03X" % id.to_i(16)237resp = send_cmd("ATSH#{id}")238if resp == "OK"239send_cmd("ATR0") # Disable response checks240send_cmd("ATCAF0") # Turn off ISO-TP formatting241else242return result243end244if data.scan(/../).size > 8245$stdout.puts("Error: Data size > 8 bytes")246return result247end248send_cmd(data)249@packets_sent += 1250@last_sent = Time.now()251if resp == "CAN ERROR"252result["success"] = false253return result254end255result["success"] = true256result257end258259# Sends ISO-TP Packets260#261# @param srcid [String] Sender ID as hex string262# @param dstid [String] Responder ID as hex string263# @param data [String] Hex String of data to send264# @param timeout [Integer] Millisecond timeout, currently not implemented265# @param maxpkts [Integer] Max number of packets in response, currently not implemented266def isotpsend_and_wait(srcid, dstid, data, timeout, maxpkts)267result = {}268result["success"] = false269srcid = "%03X" % srcid.to_i(16)270dstid = "%03X" % dstid.to_i(16)271send_cmd("ATCAF1") # Turn on ISO-TP formatting272send_cmd("ATR1") # Turn on responses273send_cmd("ATSH#{srcid}") # Src Header274send_cmd("ATCRA#{dstid}") # Resp Header275send_cmd("ATCFC1"). # Enable flow control276resp = send_cmd(data)277@packets_sent += 1278@last_sent = Time.now()279if resp == "CAN ERROR"280result["success"] = false281return result282end283result["Packets"] = []284resp.split(/\r/).each do |line|285pkt = {}286if line=~/^(\w+) (.+)/287pkt["ID"] = $1288pkt["DATA"] = $2.split289end290result["Packets"] << pkt291end292result["success"] = true293result294end295296# Generic Not supported call297#298# @return [Hash] Status not supported299def not_supported()300{ "status" => "not supported" }301end302303# Handles incoming URI requests and calls their respective API functions304#305# @param cli [Socket] Socket for the browser306# @param request [Rex::Proto::Http::Request] HTTP Request sent by the browser307def on_request_uri(cli, request)308if request.uri =~ /status$/i309send_response_html(cli, get_status().to_json(), { 'Content-Type' => 'application/json' })310elsif request.uri =~ /statistics$/i311send_response_html(cli, get_stats().to_json(), { 'Content-Type' => 'applicaiton/json' })312elsif request.uri =~/settings\/datetime$/i313send_response_html(cli, get_datetime().to_json(), { 'Content-Type' => 'application/json' })314elsif request.uri =~/settings\/timezone$/i315send_response_html(cli, get_timezone().to_json(), { 'Content-Type' => 'application/json' })316# elsif request.uri =~/custom_methods$/i317# send_response_html(cli, get_custom_methods().to_json(), { 'Content-Type' => 'application/json' })318elsif request.uri =~/automotive/i319if request.uri =~/automotive\/supported_buses/i320send_response_html(cli, get_supported_buses().to_json(), { 'Content-Type' => 'application/json' })321elsif request.uri =~/automotive\/can0\/cansend/322params = CGI.parse(URI(request.uri).query)323if params.has_key? "id" and params.has_key? "data"324send_response_html(cli, cansend(params["id"][0], params["data"][0]).to_json(), { 'Content-Type' => 'application/json' })325else326send_response_html(cli, not_supported().to_json(), { 'Content-Type' => 'application/json' })327end328elsif request.uri =~/automotive\/can0\/isotpsend_and_wait/329params = CGI.parse(URI(request.uri).query)330if params.has_key? "srcid" and params.has_key? "dstid" and params.has_key? "data"331timeout = 1500332maxpkts = 3333timeout = params["timeout"][0] if params.has_key? "timeout"334maxpkts = params["maxpkts"][0] if params.has_key? "maxpkts"335send_response_html(cli, isotpsend_and_wait(params["srcid"][0], params["dstid"][0], params["data"][0], timeout, maxpkts).to_json(), { 'Content-Type' => 'application/json' })336else337send_response_html(cli, not_supported().to_json(), { 'Content-Type' => 'application/json' })338end339else340send_response_html(cli, not_supported().to_json(), { 'Content-Type' => 'application/json' })341end342else343send_response_html(cli, not_supported().to_json(), { 'Content-Type' => 'application/json' })344end345end346347# Main run operation. Connects to device then runs the server348def run349connect_to_device()350exploit()351end352353end354355# This class parses the user-supplied options (inputs)356class OptsConsole357358DEFAULT_BAUD = 115200359DEFAULT_SERIAL = "/dev/ttyUSB0"360361# Returns the normalized user inputs362#363# @param args [Array] This should be Ruby's ARGV364# @raise [OptionParser::MissingArgument] Missing arguments365# @return [Hash] The normalized options366def self.parse(args)367parser, options = get_parsed_options368369# Now let's parse it370# This may raise OptionParser::InvalidOption371parser.parse!(args)372373options374end375376# Returns the parsed options from ARGV377#378# raise [OptionParser::InvalidOption] Invalid option found379# @return [OptionParser, Hash] The OptionParser object and an hash containing the options380def self.get_parsed_options381options = {}382parser = OptionParser.new do |opt|383opt.banner = "Usage: #{__FILE__} [options]"384opt.separator ''385opt.separator 'Specific options:'386387opt.on('-b', '--baud <serial_baud>',388"(Optional) Sets the baud speed for the serial device (Default=#{DEFAULT_BAUD})") do |v|389options[:baud] = v390end391392opt.on('-s', '--serial <serial_device>',393"(Optional) Sets the serial device (Default=#{DEFAULT_SERIAL})") do |v|394options[:serial] = v395end396397opt.on('-p', '--port <server_port>',398"(Optional) Sets the listening HTTP server port (Default=8080)") do |v|399options[:server_port] = v400end401402opt.on_tail('-h', '--help', 'Show this message') do403$stdout.puts opt404exit405end406end407return parser, options408end409end410end411412413414#415# Main416#417if __FILE__ == $PROGRAM_NAME418begin419bridge = ELM327HWBridgeRelay::ELM327Relay.new420bridge.run421rescue Interrupt422$stdout.puts("Shutting down")423end424end425426427428