zgrab2/modules/modbus/modbus.go
Benjamin Wireman cfd48de9d6
Fix panics in Modbus scanner (#265)
* Check message and body length in in GetModbusResponse()

* add timeout to waiting when PSQL container to start

https://github.com/zmap/zgrab2/pull/265
2020-05-29 13:07:47 -04:00

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