1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
|
local shortport = require "shortport"
local comm = require "comm"
local stdnse = require "stdnse"
local string = require "string"
local match = require "match"
description = [[
Attempts to identify IEC 60870-5-104 ICS protocol.
After probing with a TESTFR (test frame) message, a STARTDT (start data
transfer) message is sent and general interrogation is used to gather the list
of information object addresses stored.
]]
---
-- @output
-- | iec-identify:
-- | ASDU address: 105
-- |_ Information objects: 30
--
author = {"Aleksandr Timorin", "Daniel Miller"}
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"discovery", "intrusive"}
portrule = shortport.port_or_service(2404, "iec-104", "tcp")
local function get_asdu(socket)
local status, data = socket:receive_buf(match.numbytes(2), true)
if not status then
return nil, data
end
if data:byte(1) ~= 0x68 then
return nil, "Not IEC-104"
end
local len = data:byte(2)
status, data = socket:receive_buf(match.numbytes(len), true)
if not status then
return nil, data
end
local apcitype = data:byte(1)
return apcitype, data
end
action = function(host, port)
local output = stdnse.output_table()
local socket, err = comm.opencon(host, port)
if not socket then
stdnse.debug1("Connect error: %s", err)
return nil
end
-- send TESTFR ACT command
-- Test frame, like "ping"
local TESTFR = "\x68\x04\x43\0\0\0"
local status, err = socket:send( TESTFR )
if not status then
stdnse.debug1("Failed to send: %s", err)
return nil
end
-- receive TESTFR answer
local apcitype, recv = get_asdu(socket)
if not apcitype then
stdnse.debug1("protocol error: %s", recv)
return nil
end
if apcitype ~= 0x83 then
stdnse.print_debug(1, "Not IEC-104. TESTFR response: %#x", apcitype)
return nil
end
-- send STARTDT ACT command
local STARTDT = "\x68\x04\x07\0\0\0"
status, err = socket:send( STARTDT )
if not status then
stdnse.debug1("Failed to send: %s", err)
return nil
end
-- receive STARTDT answer
apcitype, recv = get_asdu(socket)
if not apcitype then
stdnse.debug1("protocol error: %s", recv)
return nil
end
if apcitype ~= 0x0b then
stdnse.debug1("STARTDT ACT did not receive STARTDT CON: %#x", apcitype)
return nil
end
-- May also receive ME_EI_NA_1 (End of initialization), so check for that in the buffer after sending the next part
-- send C_IC_NA_1 command
-- type: 0x64, C_IC_NA_1,
-- numix: 1
-- TNCause: 6, Act
-- Originator address; 0
-- ASDU address: 0xffff
-- Information object address: 0
-- QOI: 0x14 (20), Station interrogation (global)
local C_IC_NA_1_broadcast = "\x68\x0e\0\0\0\0\x64\x01\x06\0\xff\xff\0\0\0\x14"
status, err = socket:send( C_IC_NA_1_broadcast )
if not status then
stdnse.debug1("Failed to send: %s", err)
return nil
end
local asdu_address
local ioas = 0
-- Have to draw the line somewhere.
local limit = 10
while limit > 0 do
limit = limit - 1
apcitype, recv = get_asdu(socket)
if not apcitype then
stdnse.debug1("Error in C_IC_NA_1: %s", recv)
break
end
if apcitype & 0x01 == 0 then -- Type I, numbered information transfer
-- skip 2 bytes Tx, 2 bytes Rx
local typeid = recv:byte(5)
if typeid == 70 then
-- ME_EI_NA_1, End of Initialization. Skip.
else
local numix = recv:byte(6) & 0x7f
local cause = recv:byte(7) & 0x3f
asdu_address = string.unpack("<I2", recv, 9)
stdnse.debug2("Got asdu=%d, type %d, cause %d, numix %d.", asdu_address, typeid, cause, numix)
if typeid == 100 then
-- C_IC_NA_1
if cause == 7 then
-- ActCon. Skip.
elseif cause == 10 then
-- ActTerm. The end!
break
else
-- TODO: do something!
end
else
if cause >= 20 and cause <= 36 then
-- Inrogen, response to general interrogation
ioas = ioas + numix
end
end
end
end
end
socket:close()
if asdu_address then
output["ASDU address"] = asdu_address
output["Information objects"] = ioas
else
output = "IEC-104 endpoint did not respond to C_IC_NA_1 request"
end
return output
end
|