summaryrefslogtreecommitdiffstats
path: root/scripts/iec-identify.nse
blob: 81bcd61a7aefdafec53e37d7e42037b28982f2d7 (plain)
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