cfd48de9d6
* Check message and body length in in GetModbusResponse() * add timeout to waiting when PSQL container to start https://github.com/zmap/zgrab2/pull/265
414 lines
13 KiB
Go
414 lines
13 KiB
Go
package modbus
|
|
|
|
/*
|
|
* ZGrab Copyright 2015 Regents of the University of Michigan
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
* use this file except in compliance with the License. You may obtain a copy
|
|
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
* implied. See the License for the specific language governing
|
|
* permissions and limitations under the License.
|
|
*/
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
"errors"
|
|
)
|
|
|
|
// MEIResponse is the parsed data field from the 0x2B/0x0E response.
|
|
type MEIResponse struct {
|
|
// ConformityLevel specifies the confirmity level of the device and the type of supported access.
|
|
// Valid values include 0x01, 0x02, 0x03, for basic, regular, extended stream access, and
|
|
// 8x81, 0x82, 0x83 for basic, regular, extended stream/individual access.
|
|
ConformityLevel int `json:"conformity_level"`
|
|
|
|
// MoreFollows specifies whether more data follows. Strictly should be 00 or FF, but we take any nonzero number
|
|
// to mean that more data follows.
|
|
MoreFollows bool `json:"more_follows"`
|
|
|
|
// NextObjectID gives the next object ID if MoreFollows is set, otherwise it should be 0x00.
|
|
NextObjectID int `json:"next_object_id"`
|
|
|
|
// ObjectCount gives the number of items returned.
|
|
ObjectCount int `json:"object_count"`
|
|
|
|
// Objects is a set of object ID/value pairs returned by the server.
|
|
Objects MEIObjectSet `json:"objects,omitempty"`
|
|
}
|
|
|
|
// MEIObjectSet wraps the list of object ID/value pairs, encoding them as a dict of ID name -> value
|
|
type MEIObjectSet []MEIObject
|
|
|
|
// MarshalJSON encodes the object ID list as a map of { "obj.OID.Name()": obj.Value }
|
|
func (ms *MEIObjectSet) MarshalJSON() ([]byte, error) {
|
|
enc := make(map[string]string, len(*ms))
|
|
for _, obj := range *ms {
|
|
enc[obj.OID.Name()] = obj.Value
|
|
}
|
|
return json.Marshal(enc)
|
|
}
|
|
|
|
// MEIObject wraps the ID/value pair in the 0x2B/0x0E response.
|
|
type MEIObject struct {
|
|
// OID is the object identifier.
|
|
OID MEIObjectID
|
|
// Value is the value for that object identifier.
|
|
Value string
|
|
}
|
|
|
|
// MEIObjectID is the numeric identifier used by the server to identify different data.
|
|
type MEIObjectID int
|
|
|
|
const (
|
|
// OIDVendor identifies the vendor name. Mandatory ASCII String, category = basic.
|
|
OIDVendor MEIObjectID = 0
|
|
|
|
// OIDProductCode identifies the product code. Mandatory ASCII String, category = basic.
|
|
OIDProductCode MEIObjectID = 1
|
|
|
|
// OIDRevision identifies the MajorMinorRevision. Mandatory ASCII String, category = basic.
|
|
OIDRevision MEIObjectID = 2
|
|
|
|
// OIDVendorURL identifies the vendor URL. Optional ASCII String, category = regular.
|
|
OIDVendorURL MEIObjectID = 3
|
|
|
|
// OIDProductName identifies the product name. Optional ASCII String, category = regular.
|
|
OIDProductName MEIObjectID = 4
|
|
|
|
// OIDModelName identifies the model name. Optional ASCII String, category = regular.
|
|
OIDModelName MEIObjectID = 5
|
|
|
|
// OIDUserApplicationName identifies the user application name. Optional ASCII String, category = regular.
|
|
OIDUserApplicationName MEIObjectID = 6
|
|
)
|
|
|
|
var meiObjectNames = []string{
|
|
"vendor",
|
|
"product_code",
|
|
"revision",
|
|
"vendor_url",
|
|
"product_name",
|
|
"model_name",
|
|
"user_application_name",
|
|
}
|
|
|
|
// Name maps the object ID to its friendly name; if the ID is not on the list, it returns "oid_$(id)".
|
|
func (m *MEIObjectID) Name() string {
|
|
oid := int(*m)
|
|
var name string
|
|
if oid >= len(meiObjectNames) || oid < 0 {
|
|
name = "oid_" + strconv.Itoa(oid)
|
|
} else {
|
|
name = meiObjectNames[oid]
|
|
}
|
|
return name
|
|
}
|
|
|
|
// MarshalJSON encodes the identifier as its friendly name.
|
|
func (m *MEIObject) MarshalJSON() ([]byte, error) {
|
|
enc := make(map[string]interface{}, 1)
|
|
name := m.OID.Name()
|
|
enc[name] = m.Value
|
|
return json.Marshal(enc)
|
|
}
|
|
|
|
// ExceptionResponse wraps the exception returned by the server.
|
|
type ExceptionResponse struct {
|
|
// ExceptionFunction is the function to which the server is responding -- namely, the value of the FunctionCode in
|
|
// the response with the high bit masked off.
|
|
ExceptionFunction FunctionCode `json:"exception_function"`
|
|
|
|
// ExceptionType is the type code representing the exception.
|
|
// For details see e.g. section 7 of http://www.modbus.org/docs/Modbus_Application_Protocol_V1_1b.pdf
|
|
ExceptionType byte `json:"exception_type"`
|
|
}
|
|
|
|
// ModbusEvent is the response object. Either MEIResponse or ExceptionResponse will be set.
|
|
type ModbusEvent struct {
|
|
// Length is the data length the server claimed to return in the header. Should be len(Response) + 1 (the function
|
|
// code is included)
|
|
Length int `json:"length"`
|
|
|
|
// UnitID is the unit ID for which the server is responding. If request unit ID was nonzero, should match that.
|
|
UnitID int `json:"unit_id"`
|
|
|
|
// Function is the response function code. The high bit indicates the presence of an exception (set -> exception).
|
|
Function FunctionCode `json:"function_code"`
|
|
|
|
// Response is the response data (not including the function code).
|
|
Response []byte `json:"raw_response,omitempty"`
|
|
|
|
// MEIResponse is the parsed response; it is present if the response was decoded successfully and there was no
|
|
// exception.
|
|
MEIResponse *MEIResponse `json:"mei_response,omitempty"`
|
|
|
|
// ExceptionResponse is the parsed exception; it is present if the response was decoded successfully and there was
|
|
// an exception (i.e. the high bit of Function is set).
|
|
ExceptionResponse *ExceptionResponse `json:"exception_response,omitempty"`
|
|
|
|
// Raw is the full raw response from the server, including the header.
|
|
Raw []byte `json:"raw,omitempty"`
|
|
}
|
|
|
|
// IsException returns true if this response indicates an exception has occurred.
|
|
func (m *ModbusResponse) IsException() bool {
|
|
return (m.Function&0x80 != 0)
|
|
}
|
|
|
|
// getEvent does basic validation parsing, and on success, returns the event; on failure, it returns nil and the error,
|
|
// which should be interpreted as a protocol error / failed detection.
|
|
func (m *ModbusResponse) getEvent(strict bool) (*ModbusEvent, error) {
|
|
ret := &ModbusEvent{
|
|
Length: m.Length,
|
|
UnitID: m.UnitID,
|
|
Function: m.Function,
|
|
Response: m.Data,
|
|
Raw: m.Raw,
|
|
}
|
|
if m.IsException() {
|
|
ex, err := m.getExceptionResponse(strict)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ret.ExceptionResponse = ex
|
|
} else {
|
|
// TODO: This is only valid for 0x0E.
|
|
mei, err := m.getMEIResponse(strict)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ret.MEIResponse = mei
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
func (m *ModbusResponse) getExceptionResponse(strict bool) (*ExceptionResponse, error) {
|
|
exceptionFunction := m.Function & 0x7F
|
|
var exceptionType byte
|
|
if len(m.Data) > 0 {
|
|
exceptionType = m.Data[0]
|
|
} else if strict {
|
|
return nil, fmt.Errorf("Empty response body on error for function 0x%02x", exceptionFunction)
|
|
}
|
|
return &ExceptionResponse{
|
|
ExceptionFunction: exceptionFunction,
|
|
ExceptionType: exceptionType,
|
|
}, nil
|
|
}
|
|
|
|
func (m *ModbusResponse) getMEIResponse(strict bool) (*MEIResponse, error) {
|
|
if m.Function != FunctionCodeMEI {
|
|
return nil, fmt.Errorf("Invalid function code 0x%02x", m.Function)
|
|
}
|
|
if len(m.Data) < 6 {
|
|
return nil, fmt.Errorf("Response too short (%d bytes)", len(m.Data))
|
|
}
|
|
meiType := m.Data[0]
|
|
if meiType != 0x0E {
|
|
return nil, fmt.Errorf("Invalid response data (expected 0xee, got 0x%02x)", meiType)
|
|
}
|
|
// TODO: Allow different values here
|
|
readType := m.Data[1]
|
|
if readType != 1 {
|
|
return nil, fmt.Errorf("Invalid response data (expected 0x01, got 0x%02x)", readType)
|
|
}
|
|
conformityLevel := m.Data[2]
|
|
moreFollows := (m.Data[3] != 0)
|
|
if strict && m.Data[3] != 0x00 && m.Data[3] != 0xFF {
|
|
return nil, fmt.Errorf("Invalid response data (expected 0x00 or 0xff for MoreFollows, got 0x%02x)", m.Data[3])
|
|
}
|
|
nextObject := m.Data[4]
|
|
if strict && (!moreFollows && nextObject != 0) {
|
|
return nil, fmt.Errorf("For MoreFollows == 0x00, expected NextObjectID == 0x00 (got 0x%02x)", nextObject)
|
|
}
|
|
objectCount := m.Data[5]
|
|
objects := make([]MEIObject, objectCount)
|
|
it := 6
|
|
for idx := range objects {
|
|
n, obj := parseMEIObject(m.Data[it:])
|
|
it += n
|
|
if obj == nil {
|
|
break
|
|
}
|
|
objects[idx] = *obj
|
|
}
|
|
return &MEIResponse{
|
|
ConformityLevel: int(conformityLevel),
|
|
MoreFollows: moreFollows,
|
|
NextObjectID: int(nextObject),
|
|
ObjectCount: int(objectCount),
|
|
Objects: objects,
|
|
}, nil
|
|
}
|
|
|
|
func parseMEIObject(objectBytes []byte) (int, *MEIObject) {
|
|
length := len(objectBytes)
|
|
if length < 2 {
|
|
return length, nil
|
|
}
|
|
oid := objectBytes[0]
|
|
objLen := int(objectBytes[1])
|
|
if length < 2+objLen {
|
|
return length, nil
|
|
}
|
|
s := string(objectBytes[2 : 2+objLen])
|
|
obj := MEIObject{
|
|
OID: MEIObjectID(oid),
|
|
Value: s,
|
|
}
|
|
return 2 + objLen, &obj
|
|
}
|
|
|
|
// FunctionCode identifies the Modbus function being queried.
|
|
type FunctionCode byte
|
|
|
|
// ExceptionFunctionCode represents the function code corresponding to an exception -- that is, with the high bit set.
|
|
type ExceptionFunctionCode byte
|
|
|
|
// ExceptionCode represents the exception description codes.
|
|
type ExceptionCode byte
|
|
|
|
// ModbusRequest wraps the Modbus ApplicationDataUnit (ADU).
|
|
type ModbusRequest struct {
|
|
// UnitID is the target unit ID. 0 can get special treatment (ignored, invalid, or treated as broadcast).
|
|
UnitID int
|
|
|
|
// FunctionCode identifies the function for the server to execute.
|
|
Function FunctionCode
|
|
|
|
// Deta is the payload for the request, its format depends on the FunctionCode.
|
|
Data []byte
|
|
}
|
|
|
|
// MarshalRequest marshals the request for transport to the server.
|
|
func (c *Conn) MarshalRequest(r *ModbusRequest) (data []byte, err error) {
|
|
data = make([]byte, 7+1+len(r.Data))
|
|
// Request ID: default ZG
|
|
binary.BigEndian.PutUint16(data[0:2], c.scanner.config.RequestID)
|
|
// Protocol: must be 0
|
|
binary.BigEndian.PutUint16(data[2:4], 0)
|
|
msglen := len(r.Data) + 2 // unit ID and function
|
|
binary.BigEndian.PutUint16(data[4:6], uint16(msglen))
|
|
// leaving UnitID == 0 keeps the original zgrab behavior (which hangs on the modbus simulator -- "station #0 so no
|
|
// response allowed")
|
|
// similarly, if the identified unit is offline, it hangs with "Station ID <X> off-line, no response sent".
|
|
data[6] = byte(r.UnitID)
|
|
data[7] = byte(r.Function)
|
|
copy(data[8:], r.Data)
|
|
return
|
|
}
|
|
|
|
// ModbusResponse wraps the data returned by the server in response to the ModbusRequest.
|
|
type ModbusResponse struct {
|
|
// Length is the number of bytes the server says it will return.
|
|
Length int
|
|
|
|
// UnitID identifies the unit this response pertains to.
|
|
UnitID int
|
|
|
|
// Function is the function code returned by the server (which may have the high bit set to indicate an exception).
|
|
Function FunctionCode
|
|
|
|
// Data is payload, its format depends on the function code. Should be Length - 1 bytes.
|
|
Data []byte
|
|
|
|
// Raw is the actual data returned by the server, including the header.
|
|
Raw []byte
|
|
}
|
|
|
|
// GetModbusResponse reads the response from the server and does some minimal parsing.
|
|
func (c *Conn) GetModbusResponse() (*ModbusResponse, error) {
|
|
header := make([]byte, 7)
|
|
_, err := io.ReadFull(c.getUnderlyingConn(), header)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("modbus: could not get response: %s", err.Error())
|
|
}
|
|
|
|
// first 4 bytes should be known, verify them
|
|
requestID := binary.BigEndian.Uint16(header[0:2])
|
|
if requestID != c.scanner.config.RequestID {
|
|
return nil, fmt.Errorf("modbus: requestID did not match (got 0x%02x, expected 0x%02x)", requestID, c.scanner.config.RequestID)
|
|
}
|
|
protocolID := binary.BigEndian.Uint16(header[2:4])
|
|
if protocolID != 0 {
|
|
return nil, fmt.Errorf("modbus: protocol ID did not match (got 0x%02x, expected 0x00)", protocolID)
|
|
}
|
|
msglen := int(binary.BigEndian.Uint16(header[4:6]))
|
|
unitID := int(header[6])
|
|
|
|
// less than 1 because it's msglen - 1
|
|
if msglen < 1 {
|
|
return nil, errors.New("modbus: invalid message length")
|
|
}
|
|
|
|
body := make([]byte, msglen-1)
|
|
cnt := 0
|
|
// One of the bytes in length counts as part of the header
|
|
var readError error
|
|
for cnt < len(body) {
|
|
var n int
|
|
n, readError = c.getUnderlyingConn().Read(body[cnt:])
|
|
cnt += n
|
|
|
|
if readError != nil {
|
|
// Some servers return a message length of 09, but stop sending data after the two-byte error
|
|
body = body[:cnt]
|
|
break
|
|
}
|
|
}
|
|
|
|
if readError == io.EOF {
|
|
readError = nil
|
|
}
|
|
raw := make([]byte, len(header)+len(body))
|
|
copy(raw[0:7], header)
|
|
copy(raw[7:], body)
|
|
|
|
if len(body) < 1 {
|
|
return nil, readError
|
|
}
|
|
|
|
//TODO this really should be done by a more elegant unmarshaling function
|
|
return &ModbusResponse{
|
|
Length: msglen,
|
|
UnitID: unitID,
|
|
Function: FunctionCode(body[0]),
|
|
Data: body[1:],
|
|
Raw: raw,
|
|
}, readError
|
|
}
|
|
|
|
// FunctionCode strips the high bit off of the exception function code, to get the function for which the server is
|
|
// responding.
|
|
func (e ExceptionFunctionCode) FunctionCode() FunctionCode {
|
|
code := byte(e) & byte(0x7f)
|
|
return FunctionCode(code)
|
|
}
|
|
|
|
// ExceptionFunctionCode gets the code that the server would return in an exception to the given function code.
|
|
func (c FunctionCode) ExceptionFunctionCode() ExceptionFunctionCode {
|
|
code := byte(c) | byte(0x80)
|
|
return ExceptionFunctionCode(code)
|
|
}
|
|
|
|
// IsException checks if the given function code is an exception (i.e. its high bit is set).
|
|
func (c FunctionCode) IsException() bool {
|
|
return (byte(c) & 0x80) == 0x80
|
|
}
|
|
|
|
// ModbusFunctionEncapsulatedInterface identifies the MEI read function.
|
|
var ModbusFunctionEncapsulatedInterface = FunctionCode(0x2B)
|
|
|
|
const (
|
|
// FunctionCodeMEI identifies the MEI read function.
|
|
FunctionCodeMEI = FunctionCode(0x2B)
|
|
)
|