1
mirror of https://github.com/rapid7/metasploit-framework synced 2024-10-29 18:07:27 +01:00

Land #10386, Add IEC104 client module

This commit is contained in:
Brent Cook 2018-08-04 07:43:15 -05:00
commit 78f66986e9
No known key found for this signature in database
GPG Key ID: 1FFAA0B24B708F96
2 changed files with 716 additions and 0 deletions

View File

@ -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) >
```

View File

@ -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