Land #10386, Add IEC104 client module
commit
78f66986e9
|
@ -0,0 +1,120 @@
|
|||
# IEC104 Client for Metasploit
|
||||
|
||||
## Definition
|
||||
|
||||
IEC 60870 part 5 is one of the IEC 60870 set of standards which define systems used for telecontrol (supervisory control and data acquisition) in electrical engineering and power system automation applications. Part 5 provides a communication profile for sending basic telecontrol messages between two systems, which uses permanent directly connected data circuits between the systems. The IEC Technical Committee 57 (Working Group 03) have developed a protocol standard for telecontrol, teleprotection, and associated telecommunications for electric power systems. The result of this work is IEC 60870-5. Five documents specify the base IEC 60870-5:
|
||||
|
||||
IEC 60870-5-104 (IEC 104) protocol is an extension of IEC 101 protocol with the changes in transport, network, link & physical layer services to suit the complete network access. The standard uses an open TCP/IP interface to network to have connectivity to the LAN (Local Area Network) and routers with different facility (ISDN, X.25, Frame relay etc.) can be used to connect to the WAN (Wide Area Network). Application layer of IEC 104 is preserved same as that of IEC 101 with some of the data types and facilities not used. There are two separate link layers defined in the standard, which is suitable for data transfer over Ethernet & serial line (PPP - Point-to-Point Protocol). The control field data of IEC104 contains various types of mechanisms for effective handling of network data synchronization.
|
||||
|
||||
## Install
|
||||
Create a new folder for the IEC104 module, place script in new folder
|
||||
|
||||
```
|
||||
mkdir -p $HOME/.msf4/modules/auxiliary/client/iec104
|
||||
cp iec104.rb $HOME/.msf4/modules/auxiliary/client/iec104/
|
||||
```
|
||||
|
||||
## Usage
|
||||
### Selection of module in msfconsole
|
||||
```
|
||||
msf > use auxiliary/client/iec104/iec104
|
||||
```
|
||||
|
||||
### Show module options
|
||||
```
|
||||
msf auxiliary(client/iec104/iec104) > show options
|
||||
|
||||
Module options (auxiliary/client/iec104/iec104):
|
||||
|
||||
Name Current Setting Required Description
|
||||
---- --------------- -------- -----------
|
||||
ASDU_ADDRESS 1 yes Common Address of ASDU
|
||||
COMMAND_ADDRESS 0 yes Command Address / IOA Address
|
||||
COMMAND_TYPE 100 yes Command Type
|
||||
COMMAND_VALUE 20 yes Command Value
|
||||
ORIGINATOR_ADDRESS 0 yes Originator Address
|
||||
RHOST yes The target address
|
||||
RPORT 2404 yes The target port (TCP)
|
||||
|
||||
|
||||
Auxiliary action:
|
||||
|
||||
Name Description
|
||||
---- -----------
|
||||
SEND_COMMAND Send command to device
|
||||
```
|
||||
|
||||
### Usage Examples
|
||||
Example of sending IEC104 general interrogation command
|
||||
This is using thde default setting for command type, address and value, this is connecting to an local IEC104 server simulator for demo purposes
|
||||
```
|
||||
msf auxiliary(client/iec104/iec104) > set rhost 127.0.0.1
|
||||
rhost => 127.0.0.1
|
||||
msf auxiliary(client/iec104/iec104) > run
|
||||
|
||||
[+] 127.0.0.1:2404 - Recieved STARTDT_ACT
|
||||
[*] 127.0.0.1:2404 - Sending 104 command
|
||||
[+] 127.0.0.1:2404 - Parsing response: Interrogation command (C_IC_NA_1)
|
||||
[+] 127.0.0.1:2404 - TX: 0002 RX: 0000
|
||||
[+] 127.0.0.1:2404 - CauseTx: 07 (Activation Confirmation)
|
||||
[+] 127.0.0.1:2404 - Parsing response: Single point information (M_SP_NA_1)
|
||||
[+] 127.0.0.1:2404 - TX: 0002 RX: 0002
|
||||
[+] 127.0.0.1:2404 - CauseTx: 14 (Inrogen)
|
||||
[+] 127.0.0.1:2404 - IOA: 1 SIQ: 0x00
|
||||
[+] 127.0.0.1:2404 - IOA: 2 SIQ: 0x00
|
||||
[+] 127.0.0.1:2404 - IOA: 3 SIQ: 0x01
|
||||
[+] 127.0.0.1:2404 - IOA: 4 SIQ: 0x00
|
||||
[+] 127.0.0.1:2404 - Parsing response: Single point information (M_SP_NA_1)
|
||||
[+] 127.0.0.1:2404 - TX: 0002 RX: 0004
|
||||
[+] 127.0.0.1:2404 - CauseTx: 14 (Inrogen)
|
||||
[+] 127.0.0.1:2404 - IOA: 7 SIQ: 0x00
|
||||
[+] 127.0.0.1:2404 - Parsing response: Double point information (M_DP_NA_1)
|
||||
[+] 127.0.0.1:2404 - TX: 0002 RX: 0006
|
||||
[+] 127.0.0.1:2404 - CauseTx: 14 (Inrogen)
|
||||
[+] 127.0.0.1:2404 - IOA: 6 SIQ: 0x02
|
||||
[+] 127.0.0.1:2404 - Parsing response: Interrogation command (C_IC_NA_1)
|
||||
[+] 127.0.0.1:2404 - TX: 0002 RX: 0008
|
||||
[+] 127.0.0.1:2404 - CauseTx: 0a (Termination Activation)
|
||||
[*] 127.0.0.1:2404 - operation ended
|
||||
[*] 127.0.0.1:2404 - Terminating Connection
|
||||
[+] 127.0.0.1:2404 - Recieved STOPDT_ACT
|
||||
[*] Auxiliary module execution completed
|
||||
msf auxiliary(client/iec104/iec104) >
|
||||
```
|
||||
|
||||
Example sending switching command
|
||||
IOA address to be switched is "5", the command type is a double command "46", command is for switching off without time value "5"
|
||||
Using local IEC 104 server simulator
|
||||
|
||||
```
|
||||
msf auxiliary(client/iec104/iec104) > set rhost 127.0.0.1
|
||||
rhost => 127.0.0.1
|
||||
msf auxiliary(client/iec104/iec104) > set command_address 5
|
||||
command_address => 5
|
||||
msf auxiliary(client/iec104/iec104) > set command_type 46
|
||||
command_type => 46
|
||||
msf auxiliary(client/iec104/iec104) > set command_value 5
|
||||
command_value => 5
|
||||
msf auxiliary(client/iec104/iec104) > run
|
||||
|
||||
[+] 127.0.0.1:2404 - Recieved STARTDT_ACT
|
||||
[*] 127.0.0.1:2404 - Sending 104 command
|
||||
[+] 127.0.0.1:2404 - Parsing response: Double command (C_DC_NA_1)
|
||||
[+] 127.0.0.1:2404 - TX: 0002 RX: 0000
|
||||
[+] 127.0.0.1:2404 - CauseTx: 07 (Activation Confirmation)
|
||||
[+] 127.0.0.1:2404 - IOA: 5 DCO: 0x05
|
||||
[+] 127.0.0.1:2404 - Parsing response: Single point information with time (M_SP_TB_1)
|
||||
[+] 127.0.0.1:2404 - TX: 0002 RX: 0002
|
||||
[+] 127.0.0.1:2404 - CauseTx: 03 (Spontaneous)
|
||||
[+] 127.0.0.1:2404 - IOA: 3 SIQ: 0x00
|
||||
[+] 127.0.0.1:2404 - Timestamp: 2018-03-30 21:39:52.930
|
||||
[+] 127.0.0.1:2404 - Parsing response: Double command (C_DC_NA_1)
|
||||
[+] 127.0.0.1:2404 - TX: 0002 RX: 0004
|
||||
[+] 127.0.0.1:2404 - CauseTx: 0a (Termination Activation)
|
||||
[+] 127.0.0.1:2404 - IOA: 5 DCO: 0x05
|
||||
[*] 127.0.0.1:2404 - operation ended
|
||||
[*] 127.0.0.1:2404 - Terminating Connection
|
||||
[+] 127.0.0.1:2404 - Recieved STOPDT_ACT
|
||||
[*] Auxiliary module execution completed
|
||||
msf auxiliary(client/iec104/iec104) >
|
||||
```
|
|
@ -0,0 +1,596 @@
|
|||
##
|
||||
# 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
|
Loading…
Reference in New Issue