Path: blob/master/modules/auxiliary/client/iec104/iec104.rb
19715 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Auxiliary6#7# this module sends IEC104 commands8#910include Msf::Exploit::Remote::Tcp1112def initialize(info = {})13super(14update_info(15info,16'Name' => 'IEC104 Client Utility',17'Description' => %q{18This module allows sending 104 commands.19},20'Author' => [21'Michael John <mjohn.info[at]gmail.com>'22],23'License' => MSF_LICENSE,24'Actions' => [25['SEND_COMMAND', { 'Description' => 'Send command to device' }]26],27'DefaultAction' => 'SEND_COMMAND',28'Notes' => {29'Stability' => [CRASH_SAFE],30'SideEffects' => [],31'Reliability' => []32}33)34)3536register_options(37[38Opt::RPORT(2404),39OptInt.new('ORIGINATOR_ADDRESS', [true, 'Originator Address', 0]),40OptInt.new('ASDU_ADDRESS', [true, 'Common Address of ASDU', 1]),41OptInt.new('COMMAND_ADDRESS', [true, 'Command Address / IOA Address', 0]),42OptInt.new('COMMAND_TYPE', [true, 'Command Type', 100]),43OptInt.new('COMMAND_VALUE', [true, 'Command Value', 20])44]45)46end4748# sends the frame data over tcp connection and returns received string49# using sock.get is causing quite some delay, but script needs to process responses from 104 server50def send_frame(data)51sock.put(data)52sock.get(-1, sock.def_read_timeout)53rescue StandardError => e54print_error('Error:' + e.message)55end5657# ACPI formats:58# TESTFR_CON = '\x83\x00\x00\x00'59# TESTFR_ACT = '\x43\x00\x00\x00'60# STOPDT_CON = '\x23\x00\x00\x00'61# STOPDT_ACT = '\x13\x00\x00\x00'62# STARTDT_CON = '\x0b\x00\x00\x00'63# STARTDT_ACT = '\x07\x00\x00\x00'6465# creates and STARTDT Activation frame -> answer should be a STARTDT confirmation66def startcon67apci_data = "\x68"68apci_data << "\x04"69apci_data << "\x07"70apci_data << "\x00"71apci_data << "\x00"72apci_data << "\x00"73apci_data74end7576# creates and STOPDT Activation frame -> answer should be a STOPDT confirmation77def stopcon78apci_data = "\x68"79apci_data << "\x04"80apci_data << "\x13"81apci_data << "\x00"82apci_data << "\x00"83apci_data << "\x00"84apci_data85end8687# creates the acpi header of a 104 message88def make_apci(asdu_data)89apci_data = "\x68"90apci_data << [asdu_data.size + 4].pack('c') # size byte91apci_data << String([@tx].pack('v'))92apci_data << String([@rx].pack('v'))93@rx += 294@tx += 295apci_data << asdu_data96apci_data97end9899# parses the header of a 104 message100def parse_headers(response_data)101if !response_data[0].eql?("\x04") && !response_data[1].eql?("\x01")102@rx = + (response_data[2].unpack('H*').first + response_data[1].unpack('H*').first).to_i(16)103print_good(' TX: ' + response_data[4].unpack('H*').first + response_data[3].unpack('H*').first + \104' RX: ' + response_data[2].unpack('H*').first + response_data[1].unpack('H*').first)105end106if response_data[7].eql?("\x07")107print_good(' CauseTx: ' + response_data[7].unpack('H*').first + ' (Activation Confirmation)')108elsif response_data[7].eql?("\x0a")109print_good(' CauseTx: ' + response_data[7].unpack('H*').first + ' (Termination Activation)')110elsif response_data[7].eql?("\x14")111print_good(' CauseTx: ' + response_data[7].unpack('H*').first + ' (Inrogen)')112elsif response_data[7].eql?("\x0b")113print_good(' CauseTx: ' + response_data[7].unpack('H*').first + ' (Feedback by distant command / Retrem)')114elsif response_data[7].eql?("\x03")115print_good(' CauseTx: ' + response_data[7].unpack('H*').first + ' (Spontaneous)')116elsif response_data[7].eql?("\x04")117print_good(' CauseTx: ' + response_data[7].unpack('H*').first + ' (Initialized)')118elsif response_data[7].eql?("\x05")119print_good(' CauseTx: ' + response_data[7].unpack('H*').first + ' (Interrogation)')120elsif response_data[7].eql?("\x06")121print_good(' CauseTx: ' + response_data[7].unpack('H*').first + ' (Activiation)')122123# 104 error messages124elsif response_data[7].eql?("\x2c")125print_error(' CauseTx: ' + response_data[7].unpack('H*').first + ' (Type Identification Unknown)')126elsif response_data[7].eql?("\x2d")127print_error(' CauseTx: ' + response_data[7].unpack('H*').first + ' (Cause Unknown)')128elsif response_data[7].eql?("\x2e")129print_error(' CauseTx: ' + response_data[7].unpack('H*').first + ' (ASDU Address Unknown)')130elsif response_data[7].eql?("\x2f")131print_error(' CauseTx: ' + response_data[7].unpack('H*').first + ' (IOA Address Unknown)')132elsif response_data[7].eql?("\x6e")133print_error(' CauseTx: ' + response_data[7].unpack('H*').first + ' (Unknown Comm Address ASDU)')134end135end136137##############################################################################################################138# following functions parse different 104 ASDU messages and prints it content, not all messages of the standard are currently implemented139##############################################################################################################140def parse_m_sp_na_1(response_data)141sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000 # this bit determines the object addressing structure142response_data = response_data[11..] # cut out acpi data143if sq_bit.eql?(0b10000000)144ioa = response_data[0..3] # extract ioa value145response_data = response_data[3..] # cut ioa from message146i = 0147while response_data.length >= 1148print_good(' IOA: ' + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \149' SIQ: 0x' + response_data[0].unpack('H*').first)150response_data = response_data[1..]151i += 1152end153else154while response_data.length >= 4155ioa = response_data[0..3] # extract ioa156print_good(' IOA: ' + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \157' SIQ: 0x' + response_data[3].unpack('H*').first)158response_data = response_data[4..]159end160end161end162163def parse_m_me_nb_1(response_data)164sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000165response_data = response_data[11..] # cut out acpi data166if sq_bit.eql?(0b10000000)167ioa = response_data[0..3]168response_data = response_data[3..]169i = 0170while response_data.length >= 3171print_good(' IOA: ' + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \172' Value: 0x' + response_data[0..1].unpack('H*').first + ' QDS: 0x' + response_data[2].unpack('H*').first)173response_data = response_data[3..]174i += 1175end176else177while response_data.length >= 6178ioa = response_data[0..5]179print_good(' IOA: ' + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \180' Value: 0x' + response_data[3..4].unpack('H*').first + ' QDS: 0x' + + response_data[5].unpack('H*').first)181response_data = response_data[6..]182end183end184end185186def parse_c_sc_na_1(response_data)187sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000188response_data = response_data[11..] # cut out acpi data189if sq_bit.eql?(0b10000000)190ioa = response_data[0..3]191response_data = response_data[3..]192i = 0193while response_data.length >= 1194print_good(' IOA: ' + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \195' DIQ: 0x' + response_data[0].unpack('H*').first)196response_data = response_data[1..]197i += 1198end199else200while response_data.length >= 4201ioa = response_data[0..3]202print_good(' IOA: ' + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \203' DIQ: 0x' + response_data[3].unpack('H*').first)204response_data = response_data[4..]205end206end207end208209def parse_m_dp_na_1(response_data)210sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000211response_data = response_data[11..] # cut out acpi data212if sq_bit.eql?(0b10000000)213ioa = response_data[0..3]214response_data = response_data[3..]215i = 0216while response_data.length >= 1217print_good(' IOA: ' + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \218' SIQ: 0x' + response_data[0].unpack('H*').first)219response_data = response_data[1..]220i += 1221end222else223while response_data.length >= 4224ioa = response_data[0..3]225print_good(' IOA: ' + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \226' SIQ: 0x' + response_data[3].unpack('H*').first)227response_data = response_data[4..]228end229end230end231232def parse_m_st_na_1(response_data)233sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000234response_data = response_data[11..] # cut out acpi data235if sq_bit.eql?(0b10000000)236ioa = response_data[0..3]237response_data = response_data[3..]238i = 0239while response_data.length >= 2240print_good(' IOA: ' + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \241' VTI: 0x' + response_data[0].unpack('H*').first + ' QDS: 0x' + response_data[1].unpack('H*').first)242response_data = response_data[2..]243i += 1244end245else246while response_data.length >= 5247ioa = response_data[0..4]248print_good(' IOA: ' + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \249' VTI: 0x' + response_data[3].unpack('H*').first + ' QDS: 0x' + response_data[4].unpack('H*').first)250response_data = response_data[5..]251end252end253end254255def parse_m_dp_tb_1(response_data)256sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000257response_data = response_data[11..] # cut out acpi data258if sq_bit.eql?(0b10000000)259ioa = response_data[0..3]260response_data = response_data[3..]261i = 0262while response_data.length >= 8263print_good(' IOA: ' + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \264' DIQ: 0x' + response_data[0].unpack('H*').first)265print_cp56time2a(response_data[1..7])266response_data = response_data[8..]267i += 1268end269else270while response_data.length >= 11271ioa = response_data[0..10]272print_good(' IOA: ' + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \273' DIQ: 0x' + response_data[3].unpack('H*').first)274print_cp56time2a(response_data[4..10])275response_data = response_data[11..]276end277end278end279280def parse_m_sp_tb_1(response_data)281sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000282response_data = response_data[11..] # cut out acpi data283if sq_bit.eql?(0b10000000)284ioa = response_data[0..3]285response_data = response_data[3..]286i = 0287while response_data.length >= 8288print_good(' IOA: ' + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \289' SIQ: 0x' + response_data[0].unpack('H*').first)290print_cp56time2a(response_data[1..7])291response_data = response_data[8..]292i += 1293end294else295while response_data.length >= 11296ioa = response_data[0..10]297print_good(' IOA: ' + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \298' SIQ: 0x' + response_data[3].unpack('H*').first)299print_cp56time2a(response_data[4..10])300response_data = response_data[11..]301end302end303end304305def parse_c_dc_na_1(response_data)306sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000307response_data = response_data[11..] # cut out acpi data308if sq_bit.eql?(0b10000000)309ioa = response_data[0..3]310response_data = response_data[3..]311i = 0312while response_data.length >= 1313print_good(' IOA: ' + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \314' DCO: 0x' + response_data[0].unpack('H*').first)315response_data = response_data[1..]316i += 1317end318else319while response_data.length >= 4320ioa = response_data[0..3]321print_good(' IOA: ' + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \322' DCO: 0x' + response_data[3].unpack('H*').first)323response_data = response_data[4..]324end325end326end327328def parse_m_me_na_1(response_data)329sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000330response_data = response_data[11..] # cut out acpi data331if sq_bit.eql?(0b10000000)332ioa = response_data[0..3]333response_data = response_data[3..]334i = 0335while response_data.length >= 3336print_good(' IOA: ' + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \337' Value: 0x' + response_data[0..1].unpack('H*').first + ' QDS: 0x' + response_data[2].unpack('H*').first)338response_data = response_data[3..]339i += 1340end341else342while response_data.length >= 6343ioa = response_data[0..3]344print_good(' IOA: ' + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \345' Value: 0x' + ioa[3..4].unpack('H*').first + ' QDS: 0x' + response_data[5].unpack('H*').first)346response_data = response_data[6..]347end348end349end350351def parse_m_me_nc_1(response_data)352sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000353response_data = response_data[11..] # cut out acpi data354if sq_bit.eql?(0b10000000)355ioa = response_data[0..3]356response_data = response_data[3..]357i = 0358while response_data.length >= 5359print_good(' IOA: ' + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \360' Value: 0x' + response_data[0..3].unpack('H*').first + ' QDS: 0x' + response_data[4].unpack('H*').first)361response_data = response_data[5..]362i += 1363end364else365while response_data.length >= 8366ioa = response_data[0..3]367print_good(' IOA: ' + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \368' Value: 0x' + response_data[3..6].unpack('H*').first + ' QDS: 0x' + response_data[7].unpack('H*').first)369response_data = response_data[8..]370end371end372end373374def parse_m_it_na_1(response_data)375sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000376response_data = response_data[11..] # cut out acpi data377if sq_bit.eql?(0b10000000)378response_data = response_data[11..]379ioa = response_data[0..3]380i = 0381while response_data.length >= 5382print_good(' IOA: ' + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \383' Value: 0x' + response_data[0..3].unpack('H*').first + ' QDS: 0x' + response_data[4].unpack('H*').first)384response_data = response_data[5..]385i += 1386end387else388while response_data.length >= 8389ioa = response_data[0..3]390print_good(' IOA: ' + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \391' Value: 0x' + response_data[3..6].unpack('H*').first + ' QDS: 0x' + response_data[7].unpack('H*').first)392response_data = response_data[8..]393end394end395end396397def parse_m_bo_na_1(response_data)398sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000399response_data = response_data[11..] # cut out acpi data400if sq_bit.eql?(0b10000000)401ioa = response_data[0..3]402response_data = response_data[3..]403i = 0404while response_data.length >= 5405print_good(' IOA: ' + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \406' Value: 0x' + response_data[0..3].unpack('H*').first + ' QDS: 0x' + response_data[4].unpack('H*').first)407response_data = response_data[5..]408i += 1409end410else411while response_data.length >= 8412ioa = response_data[0..3]413print_good(' IOA: ' + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \414' Value: 0x' + response_data[3..6].unpack('H*').first + ' QDS: 0x' + response_data[7].unpack('H*').first)415response_data = response_data[8..]416end417end418end419420# function to parses time format used in IEC 104421# function ported to ruby from: https://github.com/Ebolon/iec104422def print_cp56time2a(buf)423us = ((Integer(buf[1].unpack('c').first) & 0xFF) << 8) | (Integer(buf[0].unpack('c').first) & 0xFF)424second = Integer(us) / 1000425us %= 1000426minute = Integer(buf[2].unpack('c').first) & 0x3F427hour = Integer(buf[3].unpack('c').first) & 0x1F428day = Integer(buf[4].unpack('c').first) & 0x1F429month = (Integer(buf[5].unpack('c').first) & 0x0F) - 1430year = (Integer(buf[6].unpack('c').first) & 0x7F) + 2000431print_good(' Timestamp: ' + String(year) + '-' + String(format('%02d', month)) + '-' + String(format('%02d', day)) + ' ' + \432String(format('%02d', hour)) + ':' + String(format('%02d', minute)) + ':' + String(format('%02d', second)) + '.' + String(us))433end434435##############################################################################################################436# END of individual parse functions section437##############################################################################################################438439# parses the 104 response string of a message440def parse_response(response)441response_elements = response.split("\x68")442response_elements.shift443response_elements.each do |response_element|444if response_element[5].eql?("\x64")445print_good(' Parsing response: Interrogation command (C_IC_NA_1)')446parse_headers(response_element)447elsif response_element[5].eql?("\x01")448print_good(' Parsing response: Single point information (M_SP_NA_1)')449parse_headers(response_element)450parse_m_sp_na_1(response_element)451elsif response_element[5].eql?("\x0b")452print_good(' Parsing response: Measured value, scaled value (M_ME_NB_1)')453parse_headers(response_element)454parse_m_me_nb_1(response_element)455elsif response_element[5].eql?("\x2d")456print_good(' Parsing response: Single command (C_SC_NA_1)')457parse_headers(response_element)458parse_c_sc_na_1(response_element)459elsif response_element[5].eql?("\x03")460print_good(' Parsing response: Double point information (M_DP_NA_1)')461parse_headers(response_element)462parse_m_dp_na_1(response_element)463elsif response_element[5].eql?("\x05")464print_good(' Parsing response: Step position information (M_ST_NA_1)')465parse_headers(response_element)466parse_m_st_na_1(response_element)467elsif response_element[5].eql?("\x1f")468print_good(' Parsing response: Double point information with time (M_DP_TB_1)')469parse_headers(response_element)470parse_m_dp_tb_1(response_element)471elsif response_element[5].eql?("\x2e")472print_good(' Parsing response: Double command (C_DC_NA_1)')473parse_headers(response_element)474parse_c_dc_na_1(response_element)475elsif response_element[5].eql?("\x1e")476print_good(' Parsing response: Single point information with time (M_SP_TB_1)')477parse_headers(response_element)478parse_m_sp_tb_1(response_element)479elsif response_element[5].eql?("\x09")480print_good(' Parsing response: Measured value, normalized value (M_ME_NA_1)')481parse_headers(response_element)482parse_m_me_na_1(response_element)483elsif response_element[5].eql?("\x0d")484print_good(' Parsing response: Measured value, short floating point value (M_ME_NC_1)')485parse_headers(response_element)486parse_m_me_nc_1(response_element)487elsif response_element[5].eql?("\x0f")488print_good(' Parsing response: Integrated total without time tag (M_IT_NA_1)')489parse_headers(response_element)490parse_m_it_na_1(response_element)491elsif response_element[5].eql?("\x07")492print_good(' Parsing response: Bitstring of 32 bits without time tag. (M_BO_NA_1)')493parse_headers(response_element)494parse_m_bo_na_1(response_element)495496elsif response_element[5].eql?("\x46")497print_good('Received end of initialisation confirmation')498parse_headers(response_element)499elsif response_element[0].eql?("\x04") && response_element[1].eql?("\x01") && response_element[2].eql?("\x00")500print_good('Received S-Frame')501parse_headers(response_element)502elsif response_element[0].eql?("\x04") && response_element[1].eql?("\x0b") && response_element[2].eql?("\x00") && response_element[3].eql?("\x00")503print_good('Received STARTDT_ACT')504elsif response_element[0].eql?("\x04") && response_element[1].eql?("\x23") && response_element[2].eql?("\x00") && response_element[3].eql?("\x00")505print_good('Received STOPDT_ACT')506elsif response_element[0].eql?("\x04") && response_element[1].eql?("\x43") && response_element[2].eql?("\x00") && response_element[3].eql?("\x00")507print_good('Received TESTFR_ACT')508else509print_status('Received unknown message')510parse_headers(response_element)511print_status(response_element.unpack('H*').first)512end513# Uncomment for print received data514# print_good("DEBUG: " + response_element.unpack('H*').first)515end516end517518# sends 104 command with configure datastore options519# default values are for a general interrogation command520# for example a switching command would be:521# COMMAND_TYPE => 46 // double command without time522# COMMAND_ADDRESS => 100 // any IOA address that should be switched523# COMMAND_VALUE => 6 // switching off with short pulse524# use value 5 to switch on with short pulse525#526# Structure of 104 message:527# 1byte command type528# 1byte num ix -> 1 (one item send)529# 1byte cause of transmission -> 6 (activation)530# 1byte originator address531# 2byte common adsu address532# 3byte command address533# 1byte command value534def func_send_command535print_status('Sending 104 command')536537asdu = [datastore['COMMAND_TYPE']].pack('c') # type of command538asdu << "\x01" # num ix -> only one item is send539asdu << "\x06" # cause of transmission = activation, 6540asdu << [datastore['ORIGINATOR_ADDRESS']].pack('c') # sets originator address of client541asdu << String([Integer(datastore['ASDU_ADDRESS'])].pack('v')) # sets the common address of ADSU542asdu << String([Integer(datastore['COMMAND_ADDRESS'])].pack('V'))[0..2] # sets the IOA address, todo: check command address fits in the 3byte address field543asdu << [datastore['COMMAND_VALUE']].pack('c') # sets the value of the command544545# Uncomment for debugging546# print_status("Sending: " + make_apci(asdu).unpack('H*').first)547response = send_frame(make_apci(asdu))548549if response.nil?550print_error('No answer')551else552parse_response(response)553end554print_status('operation ended')555end556557def run558@rx = 0559@tx = 0560begin561connect562rescue StandardError => e563print_error('Error:' + e.message)564return565end566567# send STARTDT_CON to activate connection568response = send_frame(startcon)569if response.nil?570print_error('Could not connect to 104 service')571return572end573574parse_response(response)575576# send the 104 command577case action.name578when 'SEND_COMMAND'579func_send_command580else581print_error('Invalid ACTION')582end583584# send STOPDT_CON to terminate connection585response = send_frame(stopcon)586if response.nil?587print_error('Terminating Connection')588return589end590591print_status('Terminating Connection')592parse_response(response)593594begin595disconnect596rescue StandardError => e597print_error('Error:' + e.message)598end599end600end601602603