metasploit-framework/modules/auxiliary/client/iec104/iec104.rb

597 lines
25 KiB
Ruby

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
#
# this module sends IEC104 commands
#
include Msf::Exploit::Remote::Tcp
def initialize(info = {})
super(update_info(info,
'Name' => 'IEC104 Client Utility',
'Description' => %q(
This module allows sending 104 commands.
),
'Author' =>
[
'Michael John <mjohn.info[at]gmail.com>'
],
'License' => MSF_LICENSE,
'Actions' =>
[
['SEND_COMMAND', { 'Description' => 'Send command to device' }]
],
'DefaultAction' => 'SEND_COMMAND'))
register_options(
[
Opt::RPORT(2404),
OptInt.new('ORIGINATOR_ADDRESS', [true, "Originator Address", 0]),
OptInt.new('ASDU_ADDRESS', [true, "Common Address of ASDU", 1]),
OptInt.new('COMMAND_ADDRESS', [true, "Command Address / IOA Address", 0]),
OptInt.new('COMMAND_TYPE', [true, "Command Type", 100]),
OptInt.new('COMMAND_VALUE', [true, "Command Value", 20])
]
)
end
# sends the frame data over tcp connection and returns recieved string
# using sock.get is causing quite some delay, but scripte needs to process responses from 104 server
def send_frame(data)
begin
sock.put(data)
sock.get(-1, sock.def_read_timeout)
rescue StandardError => e
print_error("Error:" + e.message)
end
end
# ACPI formats:
# TESTFR_CON = '\x83\x00\x00\x00'
# TESTFR_ACT = '\x43\x00\x00\x00'
# STOPDT_CON = '\x23\x00\x00\x00'
# STOPDT_ACT = '\x13\x00\x00\x00'
# STARTDT_CON = '\x0b\x00\x00\x00'
# STARTDT_ACT = '\x07\x00\x00\x00'
# creates and STARTDT Activation frame -> answer should be a STARTDT confirmation
def startcon
apci_data = "\x68"
apci_data << "\x04"
apci_data << "\x07"
apci_data << "\x00"
apci_data << "\x00"
apci_data << "\x00"
apci_data
end
# creates and STOPDT Activation frame -> answer should be a STOPDT confirmation
def stopcon
apci_data = "\x68"
apci_data << "\x04"
apci_data << "\x13"
apci_data << "\x00"
apci_data << "\x00"
apci_data << "\x00"
apci_data
end
# creates the acpi header of a 104 message
def make_apci(asdu_data)
apci_data = "\x68"
apci_data << [asdu_data.size + 4].pack("c") # size byte
apci_data << String([$tx].pack('v'))
apci_data << String([$rx].pack('v'))
$rx = $rx + 2
$tx = $tx + 2
apci_data << asdu_data
apci_data
end
# parses the header of a 104 message
def parse_headers(response_data)
if !response_data[0].eql?("\x04") && !response_data[1].eql?("\x01")
$rx = + (response_data[2].unpack('H*').first + response_data[1].unpack('H*').first).to_i(16)
print_good(" TX: " + response_data[4].unpack('H*').first + response_data[3].unpack('H*').first + \
" RX: " + response_data[2].unpack('H*').first + response_data[1].unpack('H*').first)
end
if response_data[7].eql?("\x07")
print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Activation Confirmation)")
elsif response_data[7].eql?("\x0a")
print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Termination Activation)")
elsif response_data[7].eql?("\x14")
print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Inrogen)")
elsif response_data[7].eql?("\x0b")
print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Feedback by distant command / Retrem)")
elsif response_data[7].eql?("\x03")
print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Spontaneous)")
elsif response_data[7].eql?("\x04")
print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Initialized)")
elsif response_data[7].eql?("\x05")
print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Interrogation)")
elsif response_data[7].eql?("\x06")
print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Activiation)")
# 104 error messages
elsif response_data[7].eql?("\x2c")
print_error(" CauseTx: " + response_data[7].unpack('H*').first + " (Type Identification Unknown)")
elsif response_data[7].eql?("\x2d")
print_error(" CauseTx: " + response_data[7].unpack('H*').first + " (Cause Unknown)")
elsif response_data[7].eql?("\x2e")
print_error(" CauseTx: " + response_data[7].unpack('H*').first + " (ASDU Address Unknown)")
elsif response_data[7].eql?("\x2f")
print_error(" CauseTx: " + response_data[7].unpack('H*').first + " (IOA Address Unknown)")
elsif response_data[7].eql?("\x6e")
print_error(" CauseTx: " + response_data[7].unpack('H*').first + " (Unknown Comm Address ASDU)")
end
end
##############################################################################################################
# following functions parse different 104 ASDU messages and prints it content, not all messages of the standard are currently implemented
##############################################################################################################
def parse_m_sp_na_1(response_data)
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000 # this bit determines the object addressing structure
response_data = response_data[11..-1] # cut out acpi data
if sq_bit.eql?(0b10000000)
ioa = response_data[0..3] # extract ioa value
response_data = response_data[3..-1] # cut ioa from message
i = 0
while response_data.length >= 1
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
" SIQ: 0x" + response_data[0].unpack('H*').first)
response_data = response_data[1..-1]
i += 1
end
else
while response_data.length >= 4
ioa = response_data[0..3] # extract ioa
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
" SIQ: 0x" + response_data[3].unpack('H*').first)
response_data = response_data[4..-1]
end
end
end
def parse_m_me_nb_1(response_data)
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
response_data = response_data[11..-1] # cut out acpi data
if sq_bit.eql?(0b10000000)
ioa = response_data[0..3]
response_data = response_data[3..-1]
i = 0
while response_data.length >= 3
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
" Value: 0x" + response_data[0..1].unpack('H*').first + " QDS: 0x" + response_data[2].unpack('H*').first)
response_data = response_data[3..-1]
i += 1
end
else
while response_data.length >= 6
ioa = response_data[0..5]
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
" Value: 0x" + response_data[3..4].unpack('H*').first + " QDS: 0x" + + response_data[5].unpack('H*').first)
response_data = response_data[6..-1]
end
end
end
def parse_c_sc_na_1(response_data)
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
response_data = response_data[11..-1] # cut out acpi data
if sq_bit.eql?(0b10000000)
ioa = response_data[0..3]
response_data = response_data[3..-1]
i = 0
while response_data.length >= 1
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
" DIQ: 0x" + response_data[0].unpack('H*').first)
response_data = response_data[1..-1]
i += 1
end
else
while response_data.length >= 4
ioa = response_data[0..3]
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
" DIQ: 0x" + response_data[3].unpack('H*').first)
response_data = response_data[4..-1]
end
end
end
def parse_m_dp_na_1(response_data)
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
response_data = response_data[11..-1] # cut out acpi data
if sq_bit.eql?(0b10000000)
ioa = response_data[0..3]
response_data = response_data[3..-1]
i = 0
while response_data.length >= 1
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
" SIQ: 0x" + response_data[0].unpack('H*').first)
response_data = response_data[1..-1]
i += 1
end
else
while response_data.length >= 4
ioa = response_data[0..3]
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
" SIQ: 0x" + response_data[3].unpack('H*').first)
response_data = response_data[4..-1]
end
end
end
def parse_m_st_na_1(response_data)
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
response_data = response_data[11..-1] # cut out acpi data
if sq_bit.eql?(0b10000000)
ioa = response_data[0..3]
response_data = response_data[3..-1]
i = 0
while response_data.length >= 2
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
" VTI: 0x" + response_data[0].unpack('H*').first + " QDS: 0x" + response_data[1].unpack('H*').first)
response_data = response_data[2..-1]
i += 1
end
else
while response_data.length >= 5
ioa = response_data[0..4]
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
" VTI: 0x" + response_data[3].unpack('H*').first + " QDS: 0x" + response_data[4].unpack('H*').first)
response_data = response_data[5..-1]
end
end
end
def parse_m_dp_tb_1(response_data)
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
response_data = response_data[11..-1] # cut out acpi data
if sq_bit.eql?(0b10000000)
ioa = response_data[0..3]
response_data = response_data[3..-1]
i = 0
while response_data.length >= 8
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
" DIQ: 0x" + response_data[0].unpack('H*').first)
print_cp56time2a(response_data[1..7])
response_data = response_data[8..-1]
i += 1
end
else
while response_data.length >= 11
ioa = response_data[0..10]
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
" DIQ: 0x" + response_data[3].unpack('H*').first)
print_cp56time2a(response_data[4..10])
response_data = response_data[11..-1]
end
end
end
def parse_m_sp_tb_1(response_data)
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
response_data = response_data[11..-1] # cut out acpi data
if sq_bit.eql?(0b10000000)
ioa = response_data[0..3]
response_data = response_data[3..-1]
i = 0
while response_data.length >= 8
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
" SIQ: 0x" + response_data[0].unpack('H*').first)
print_cp56time2a(response_data[1..7])
response_data = response_data[8..-1]
i += 1
end
else
while response_data.length >= 11
ioa = response_data[0..10]
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
" SIQ: 0x" + response_data[3].unpack('H*').first)
print_cp56time2a(response_data[4..10])
response_data = response_data[11..-1]
end
end
end
def parse_c_dc_na_1(response_data)
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
response_data = response_data[11..-1] # cut out acpi data
if sq_bit.eql?(0b10000000)
ioa = response_data[0..3]
response_data = response_data[3..-1]
i = 0
while response_data.length >= 1
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
" DCO: 0x" + response_data[0].unpack('H*').first)
response_data = response_data[1..-1]
i += 1
end
else
while response_data.length >= 4
ioa = response_data[0..3]
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
" DCO: 0x" + response_data[3].unpack('H*').first)
response_data = response_data[4..-1]
end
end
end
def parse_m_me_na_1(response_data)
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
response_data = response_data[11..-1] # cut out acpi data
if sq_bit.eql?(0b10000000)
ioa = response_data[0..3]
response_data = response_data[3..-1]
i = 0
while response_data.length >= 3
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
" Value: 0x" + response_data[0..1].unpack('H*').first + " QDS: 0x" + response_data[2].unpack('H*').first)
response_data = response_data[3..-1]
i += 1
end
else
while response_data.length >= 6
ioa = response_data[0..3]
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
" Value: 0x" + ioa[3..4].unpack('H*').first + " QDS: 0x" + response_data[5].unpack('H*').first)
response_data = response_data[6..-1]
end
end
end
def parse_m_me_nc_1(response_data)
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
response_data = response_data[11..-1] # cut out acpi data
if sq_bit.eql?(0b10000000)
ioa = response_data[0..3]
response_data = response_data[3..-1]
i = 0
while response_data.length >= 5
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
" Value: 0x" + response_data[0..3].unpack('H*').first + " QDS: 0x" + response_data[4].unpack('H*').first)
response_data = response_data[5..-1]
i += 1
end
else
while response_data.length >= 8
ioa = response_data[0..3]
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
" Value: 0x" + response_data[3..6].unpack('H*').first + " QDS: 0x" + response_data[7].unpack('H*').first)
response_data = response_data[8..-1]
end
end
end
def parse_m_it_na_1(response_data)
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
response_data = response_data[11..-1] # cut out acpi data
if sq_bit.eql?(0b10000000)
response_data = response_data[11..-1]
ioa = response_data[0..3]
i = 0
while response_data.length >= 5
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
" Value: 0x" + response_data[0..3].unpack('H*').first + " QDS: 0x" + response_data[4].unpack('H*').first)
response_data = response_data[5..-1]
i += 1
end
else
while response_data.length >= 8
ioa = response_data[0..3]
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
" Value: 0x" + response_data[3..6].unpack('H*').first + " QDS: 0x" + response_data[7].unpack('H*').first)
response_data = response_data[8..-1]
end
end
end
def parse_m_bo_na_1(response_data)
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
response_data = response_data[11..-1] # cut out acpi data
if sq_bit.eql?(0b10000000)
ioa = response_data[0..3]
response_data = response_data[3..-1]
i = 0
while response_data.length >= 5
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
" Value: 0x" + response_data[0..3].unpack('H*').first + " QDS: 0x" + response_data[4].unpack('H*').first)
response_data = response_data[5..-1]
i += 1
end
else
while response_data.length >= 8
ioa = response_data[0..3]
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
" Value: 0x" + response_data[3..6].unpack('H*').first + " QDS: 0x" + response_data[7].unpack('H*').first)
response_data = response_data[8..-1]
end
end
end
# function to parses time format used in IEC 104
# function ported to ruby from: https://github.com/Ebolon/iec104
def print_cp56time2a(buf)
us = ((Integer(buf[1].unpack('c').first) & 0xFF) << 8) | (Integer(buf[0].unpack('c').first) & 0xFF)
second = Integer(us) / 1000
us = us % 1000
minute = Integer(buf[2].unpack('c').first) & 0x3F
hour = Integer(buf[3].unpack('c').first) & 0x1F
day = Integer(buf[4].unpack('c').first) & 0x1F
month = (Integer(buf[5].unpack('c').first) & 0x0F) - 1
year = (Integer(buf[6].unpack('c').first) & 0x7F) + 2000
print_good(" Timestamp: " + String(year) + "-" + String(format("%02d", month)) + "-" + String(format("%02d", day)) + " " + \
String(format("%02d", hour)) + ":" + String(format("%02d", minute)) + ":" + String(format("%02d", second)) + "." + String(us))
end
##############################################################################################################
# END of individual parse functions section
##############################################################################################################
# parses the 104 response string of a message
def parse_response(response)
response_elements = response.split("\x68")
response_elements.shift
response_elements.each do |response_element|
if response_element[5].eql?("\x64")
print_good(" Parsing response: Interrogation command (C_IC_NA_1)")
parse_headers(response_element)
elsif response_element[5].eql?("\x01")
print_good(" Parsing response: Single point information (M_SP_NA_1)")
parse_headers(response_element)
parse_m_sp_na_1(response_element)
elsif response_element[5].eql?("\x0b")
print_good(" Parsing response: Measured value, scaled value (M_ME_NB_1)")
parse_headers(response_element)
parse_m_me_nb_1(response_element)
elsif response_element[5].eql?("\x2d")
print_good(" Parsing response: Single command (C_SC_NA_1)")
parse_headers(response_element)
parse_c_sc_na_1(response_element)
elsif response_element[5].eql?("\x03")
print_good(" Parsing response: Double point information (M_DP_NA_1)")
parse_headers(response_element)
parse_m_dp_na_1(response_element)
elsif response_element[5].eql?("\x05")
print_good(" Parsing response: Step position information (M_ST_NA_1)")
parse_headers(response_element)
parse_m_st_na_1(response_element)
elsif response_element[5].eql?("\x1f")
print_good(" Parsing response: Double point information with time (M_DP_TB_1)")
parse_headers(response_element)
parse_m_dp_tb_1(response_element)
elsif response_element[5].eql?("\x2e")
print_good(" Parsing response: Double command (C_DC_NA_1)")
parse_headers(response_element)
parse_c_dc_na_1(response_element)
elsif response_element[5].eql?("\x1e")
print_good(" Parsing response: Single point information with time (M_SP_TB_1)")
parse_headers(response_element)
parse_m_sp_tb_1(response_element)
elsif response_element[5].eql?("\x09")
print_good(" Parsing response: Measured value, normalized value (M_ME_NA_1)")
parse_headers(response_element)
parse_m_me_na_1(response_element)
elsif response_element[5].eql?("\x0d")
print_good(" Parsing response: Measured value, short floating point value (M_ME_NC_1)")
parse_headers(response_element)
parse_m_me_nc_1(response_element)
elsif response_element[5].eql?("\x0f")
print_good(" Parsing response: Integrated total without time tag (M_IT_NA_1)")
parse_headers(response_element)
parse_m_it_na_1(response_element)
elsif response_element[5].eql?("\x07")
print_good(" Parsing response: Bitstring of 32 bits without time tag. (M_BO_NA_1)")
parse_headers(response_element)
parse_m_bo_na_1(response_element)
elsif response_element[5].eql?("\x46")
print_good("Recieved end of initialisation confirmation")
parse_headers(response_element)
elsif response_element[0].eql?("\x04") && response_element[1].eql?("\x01") && response_element[2].eql?("\x00")
print_good("Recieved S-Frame")
parse_headers(response_element)
elsif response_element[0].eql?("\x04") && response_element[1].eql?("\x0b") && response_element[2].eql?("\x00") && response_element[3].eql?("\x00")
print_good("Recieved STARTDT_ACT")
elsif response_element[0].eql?("\x04") && response_element[1].eql?("\x23") && response_element[2].eql?("\x00") && response_element[3].eql?("\x00")
print_good("Recieved STOPDT_ACT")
elsif response_element[0].eql?("\x04") && response_element[1].eql?("\x43") && response_element[2].eql?("\x00") && response_element[3].eql?("\x00")
print_good("Recieved TESTFR_ACT")
else
print_status("Recieved unknown message")
parse_headers(response_element)
print_status(response_element.unpack('H*').first)
end
# Uncomment for print recieved data
# print_good("DEBUG: " + response_element.unpack('H*').first)
end
end
# sends 104 command with configure datastore options
# default values are for a general interrogation command
# for example a switching command would be:
# COMMAND_TYPE => 46 // double command without time
# COMMAND_ADDRESS => 100 // any IOA address that should be switched
# COMMAND_VALUE => 6 // switching off with short pulse
# use value 5 to switch on with short pulse
#
# Structure of 104 message:
# 1byte command type
# 1byte num ix -> 1 (one item send)
# 1byte cause of transmission -> 6 (activation)
# 1byte originator address
# 2byte common adsu address
# 3byte command address
# 1byte command value
def func_send_command
print_status("Sending 104 command")
asdu = [datastore['COMMAND_TYPE']].pack("c") # type of command
asdu << "\x01" # num ix -> only one item is send
asdu << "\x06" # cause of transmission = activation, 6
asdu << [datastore['ORIGINATOR_ADDRESS']].pack("c") # sets originator address of client
asdu << String([Integer(datastore['ASDU_ADDRESS'])].pack('v')) # sets the common address of ADSU
asdu << String([Integer(datastore['COMMAND_ADDRESS'])].pack('V'))[0..2] # sets the IOA address, todo: check command address fits in the 3byte address field
asdu << [datastore['COMMAND_VALUE']].pack("c") # sets the value of the command
# Uncomment for debugging
# print_status("Sending: " + make_apci(asdu).unpack('H*').first)
response = send_frame(make_apci(asdu))
if response.nil?
print_error("No answer")
else
parse_response(response)
end
print_status("operation ended")
end
def run
$rx = 0
$tx = 0
begin
connect
rescue StandardError => e
print_error("Error:" + e.message)
return
end
# send STARTDT_CON to activate connection
response = send_frame(startcon)
if response.nil?
print_error("Could not connect to 104 service")
return
else
parse_response(response)
end
# send the 104 command
case action.name
when "SEND_COMMAND"
func_send_command
else
print_error("Invalid ACTION")
end
# send STOPDT_CON to terminate connection
response = send_frame(stopcon)
if response.nil?
print_error("Terminating Connection")
return
else
print_status("Terminating Connection")
parse_response(response)
end
begin
disconnect
rescue StandardError => e
print_error("Error:" + e.message)
end
end
end