mirror of https://github.com/OISF/suricata
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2666 lines
76 KiB
C
2666 lines
76 KiB
C
/* Copyright (C) 2015 Open Information Security Foundation
|
|
*
|
|
* You can copy, redistribute or modify this Program under the terms of
|
|
* the GNU General Public License version 2 as published by the Free
|
|
* Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* version 2 along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
* 02110-1301, USA.
|
|
*/
|
|
|
|
#include "suricata-common.h"
|
|
#include "stream.h"
|
|
#include "util-byte.h"
|
|
#include "util-unittest.h"
|
|
#include "util-hashlist.h"
|
|
|
|
#include "util-print.h"
|
|
|
|
#include "app-layer-protos.h"
|
|
#include "app-layer-parser.h"
|
|
#include "app-layer-detect-proto.h"
|
|
|
|
#include "app-layer-dnp3.h"
|
|
#include "app-layer-dnp3-objects.h"
|
|
|
|
/* For hexdump(). */
|
|
#include "app-layer-dcerpc-common.h"
|
|
|
|
/* Default number of unreplied requests to be considered a flood. */
|
|
#define DNP3_DEFAULT_REQ_FLOOD_COUNT 500
|
|
|
|
#define DNP3_DEFAULT_PORT "20000"
|
|
|
|
/* Expected values for the start bytes. */
|
|
#define DNP3_START_BYTE0 0x05
|
|
#define DNP3_START_BYTE1 0x64
|
|
|
|
/* Minimum length for a DNP3 frame. */
|
|
#define DNP3_MIN_LEN 5
|
|
|
|
/* Length of each CRC. */
|
|
#define DNP3_CRC_LEN 2
|
|
|
|
/* DNP3 block size. After the link header a CRC is inserted after
|
|
* after 16 bytes of data. */
|
|
#define DNP3_BLOCK_SIZE 16
|
|
|
|
/* Maximum transport layer sequence number. */
|
|
#define DNP3_MAX_TRAN_SEQNO 64
|
|
|
|
/* Maximum application layer sequence number. */
|
|
#define DNP3_MAX_APP_SEQNO 16
|
|
|
|
/* The number of bytes in the header that are counted as part of the
|
|
* header length field. */
|
|
#define DNP3_LINK_HDR_LEN 5
|
|
|
|
/* Link function codes. */
|
|
enum {
|
|
DNP3_LINK_FC_CONFIRMED_USER_DATA = 3,
|
|
DNP3_LINK_FC_UNCONFIRMED_USER_DATA
|
|
};
|
|
|
|
/* Reserved addresses. */
|
|
#define DNP3_RESERVED_ADDR_MIN 0xfff0
|
|
#define DNP3_RESERVED_ADDR_MAX 0xfffb
|
|
|
|
/* Source addresses must be < 0xfff0. */
|
|
#define DNP3_SRC_ADDR_MAX 0xfff0
|
|
|
|
#define DNP3_OBJ_TIME_SIZE 6 /* AKA UINT48. */
|
|
#define DNP3_OBJ_G12_V1_SIZE 11
|
|
#define DNP3_OBJ_G12_V2_SIZE 11
|
|
#define DNP3_OBJ_G12_V3_SIZE 1
|
|
|
|
/* Extract the prefix code from the object qualifier. */
|
|
#define DNP3_OBJ_PREFIX(x) ((x >> 4) & 0x7)
|
|
|
|
/* Extract the range code from the object qualifier. */
|
|
#define DNP3_OBJ_RANGE(x) (x & 0xf)
|
|
|
|
/* Decoder event map. */
|
|
SCEnumCharMap dnp3_decoder_event_table[] = {
|
|
{"FLOODED", DNP3_DECODER_EVENT_FLOODED},
|
|
{"LEN_TOO_SMALL", DNP3_DECODER_EVENT_LEN_TOO_SMALL},
|
|
{"BAD_LINK_CRC", DNP3_DECODER_EVENT_BAD_LINK_CRC},
|
|
{"BAD_TRANSPORT_CRC", DNP3_DECODER_EVENT_BAD_TRANSPORT_CRC},
|
|
{"MALFORMED", DNP3_DECODER_EVENT_MALFORMED},
|
|
{"UNKNOWN_OBJECT", DNP3_DECODER_EVENT_UNKNOWN_OBJECT},
|
|
{NULL, -1},
|
|
};
|
|
|
|
/* Some DNP3 servers start with a banner. */
|
|
static const char banner[] = "DNP3";
|
|
|
|
/* Calculate the next transport sequence number. */
|
|
#define NEXT_TH_SEQNO(current) ((current + 1) % DNP3_MAX_TRAN_SEQNO)
|
|
|
|
/* Calculate the next application sequence number. */
|
|
#define NEXT_APP_SEQNO(current) ((current + 1) % DNP3_MAX_APP_SEQNO)
|
|
|
|
/* CRC table generated by pycrc - http://github.com/tpircher/pycrc.
|
|
* - Polynomial: 0x3d65. */
|
|
static const uint16_t crc_table[256] = {
|
|
0x0000, 0x365e, 0x6cbc, 0x5ae2, 0xd978, 0xef26, 0xb5c4, 0x839a,
|
|
0xff89, 0xc9d7, 0x9335, 0xa56b, 0x26f1, 0x10af, 0x4a4d, 0x7c13,
|
|
0xb26b, 0x8435, 0xded7, 0xe889, 0x6b13, 0x5d4d, 0x07af, 0x31f1,
|
|
0x4de2, 0x7bbc, 0x215e, 0x1700, 0x949a, 0xa2c4, 0xf826, 0xce78,
|
|
0x29af, 0x1ff1, 0x4513, 0x734d, 0xf0d7, 0xc689, 0x9c6b, 0xaa35,
|
|
0xd626, 0xe078, 0xba9a, 0x8cc4, 0x0f5e, 0x3900, 0x63e2, 0x55bc,
|
|
0x9bc4, 0xad9a, 0xf778, 0xc126, 0x42bc, 0x74e2, 0x2e00, 0x185e,
|
|
0x644d, 0x5213, 0x08f1, 0x3eaf, 0xbd35, 0x8b6b, 0xd189, 0xe7d7,
|
|
0x535e, 0x6500, 0x3fe2, 0x09bc, 0x8a26, 0xbc78, 0xe69a, 0xd0c4,
|
|
0xacd7, 0x9a89, 0xc06b, 0xf635, 0x75af, 0x43f1, 0x1913, 0x2f4d,
|
|
0xe135, 0xd76b, 0x8d89, 0xbbd7, 0x384d, 0x0e13, 0x54f1, 0x62af,
|
|
0x1ebc, 0x28e2, 0x7200, 0x445e, 0xc7c4, 0xf19a, 0xab78, 0x9d26,
|
|
0x7af1, 0x4caf, 0x164d, 0x2013, 0xa389, 0x95d7, 0xcf35, 0xf96b,
|
|
0x8578, 0xb326, 0xe9c4, 0xdf9a, 0x5c00, 0x6a5e, 0x30bc, 0x06e2,
|
|
0xc89a, 0xfec4, 0xa426, 0x9278, 0x11e2, 0x27bc, 0x7d5e, 0x4b00,
|
|
0x3713, 0x014d, 0x5baf, 0x6df1, 0xee6b, 0xd835, 0x82d7, 0xb489,
|
|
0xa6bc, 0x90e2, 0xca00, 0xfc5e, 0x7fc4, 0x499a, 0x1378, 0x2526,
|
|
0x5935, 0x6f6b, 0x3589, 0x03d7, 0x804d, 0xb613, 0xecf1, 0xdaaf,
|
|
0x14d7, 0x2289, 0x786b, 0x4e35, 0xcdaf, 0xfbf1, 0xa113, 0x974d,
|
|
0xeb5e, 0xdd00, 0x87e2, 0xb1bc, 0x3226, 0x0478, 0x5e9a, 0x68c4,
|
|
0x8f13, 0xb94d, 0xe3af, 0xd5f1, 0x566b, 0x6035, 0x3ad7, 0x0c89,
|
|
0x709a, 0x46c4, 0x1c26, 0x2a78, 0xa9e2, 0x9fbc, 0xc55e, 0xf300,
|
|
0x3d78, 0x0b26, 0x51c4, 0x679a, 0xe400, 0xd25e, 0x88bc, 0xbee2,
|
|
0xc2f1, 0xf4af, 0xae4d, 0x9813, 0x1b89, 0x2dd7, 0x7735, 0x416b,
|
|
0xf5e2, 0xc3bc, 0x995e, 0xaf00, 0x2c9a, 0x1ac4, 0x4026, 0x7678,
|
|
0x0a6b, 0x3c35, 0x66d7, 0x5089, 0xd313, 0xe54d, 0xbfaf, 0x89f1,
|
|
0x4789, 0x71d7, 0x2b35, 0x1d6b, 0x9ef1, 0xa8af, 0xf24d, 0xc413,
|
|
0xb800, 0x8e5e, 0xd4bc, 0xe2e2, 0x6178, 0x5726, 0x0dc4, 0x3b9a,
|
|
0xdc4d, 0xea13, 0xb0f1, 0x86af, 0x0535, 0x336b, 0x6989, 0x5fd7,
|
|
0x23c4, 0x159a, 0x4f78, 0x7926, 0xfabc, 0xcce2, 0x9600, 0xa05e,
|
|
0x6e26, 0x5878, 0x029a, 0x34c4, 0xb75e, 0x8100, 0xdbe2, 0xedbc,
|
|
0x91af, 0xa7f1, 0xfd13, 0xcb4d, 0x48d7, 0x7e89, 0x246b, 0x1235
|
|
};
|
|
|
|
/**
|
|
* \brief Compute the CRC for a buffer.
|
|
*
|
|
* \param buf Buffer to create CRC from.
|
|
* \param len Length of buffer (number of bytes to use for CRC).
|
|
|
|
*/
|
|
static uint16_t DNP3ComputeCRC(const uint8_t *buf, uint32_t len)
|
|
{
|
|
const uint8_t *byte = buf;
|
|
uint16_t crc = 0;
|
|
int idx;
|
|
|
|
while (len--) {
|
|
idx = (crc ^ *byte) & 0xff;
|
|
crc = (crc_table[idx] ^ (crc >> 8)) & 0xffff;
|
|
byte++;
|
|
}
|
|
|
|
return ~crc & 0xffff;
|
|
}
|
|
|
|
/**
|
|
* \brief Check the CRC of a block.
|
|
*
|
|
* \param block The block of data with CRC to be checked.
|
|
* \param len The size of the data block.
|
|
*
|
|
* \retval 1 if CRC is OK, otherwise 0.
|
|
*/
|
|
static int DNP3CheckCRC(const uint8_t *block, uint32_t len)
|
|
{
|
|
uint32_t crc_offset;
|
|
uint16_t crc;
|
|
|
|
/* Need at least one byte plus the CRC. */
|
|
if (len < DNP3_CRC_LEN + 1) {
|
|
return 0;
|
|
}
|
|
|
|
crc_offset = len - DNP3_CRC_LEN;
|
|
crc = DNP3ComputeCRC(block, len - DNP3_CRC_LEN);
|
|
if (((crc & 0xff) == block[crc_offset]) &&
|
|
((crc >> 8) == block[crc_offset + 1])) {
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* \brief Check the CRC of the link header.
|
|
*
|
|
* \param header Point to the link header.
|
|
*
|
|
* \retval 1 if header CRC is OK, otherwise 0.
|
|
*/
|
|
static int DNP3CheckLinkHeaderCRC(const DNP3LinkHeader *header)
|
|
{
|
|
return DNP3CheckCRC((uint8_t *)header, sizeof(DNP3LinkHeader));
|
|
}
|
|
|
|
/**
|
|
* \brief Check user data CRCs.
|
|
*
|
|
* \param data Pointer to user data.
|
|
* \param len Length of user data.
|
|
*
|
|
* \retval 1 if CRCs are OK, otherwise 0.
|
|
*/
|
|
static int DNP3CheckUserDataCRCs(const uint8_t *data, uint32_t len)
|
|
{
|
|
uint32_t offset = 0;
|
|
uint32_t block_size;
|
|
|
|
while (offset < len) {
|
|
if (len - offset >= DNP3_BLOCK_SIZE + DNP3_CRC_LEN) {
|
|
block_size = DNP3_BLOCK_SIZE + DNP3_CRC_LEN;
|
|
}
|
|
else {
|
|
block_size = len - offset;
|
|
}
|
|
|
|
if (!DNP3CheckCRC(data + offset, block_size)) {
|
|
/* Once failed, may as well return immediately. */
|
|
return 0;
|
|
}
|
|
|
|
offset += block_size;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* \brief Check the DNP3 frame start bytes.
|
|
*
|
|
* \retval 1 if valid, 0 if not.
|
|
*/
|
|
static int DNP3CheckStartBytes(const DNP3LinkHeader *header)
|
|
{
|
|
return header->start_byte0 == DNP3_START_BYTE0 &&
|
|
header->start_byte1 == DNP3_START_BYTE1;
|
|
}
|
|
|
|
/**
|
|
* \brief Check if a frame contains a banner.
|
|
*
|
|
* Some servers (outstations) appear to send back a banner that fails
|
|
* the normal frame checks. So first check for a banner.
|
|
*
|
|
* \retval 1 if a banner is found, 0 if not.
|
|
*/
|
|
static int DNP3ContainsBanner(const uint8_t *input, uint32_t len)
|
|
{
|
|
return BasicSearch(input, len, (uint8_t *)banner, strlen(banner)) != NULL;
|
|
}
|
|
|
|
/**
|
|
* \brief DNP3 probing parser.
|
|
*/
|
|
static uint16_t DNP3ProbingParser(Flow *f, uint8_t direction,
|
|
uint8_t *input, uint32_t len,
|
|
uint8_t *rdir)
|
|
{
|
|
DNP3LinkHeader *hdr = (DNP3LinkHeader *)input;
|
|
|
|
/* Check that we have the minimum amount of bytes. */
|
|
if (len < sizeof(DNP3LinkHeader)) {
|
|
SCLogDebug("Length too small to be a DNP3 header.");
|
|
return ALPROTO_UNKNOWN;
|
|
}
|
|
|
|
/* May be a banner. */
|
|
if (DNP3ContainsBanner(input, len)) {
|
|
SCLogDebug("Packet contains a DNP3 banner.");
|
|
goto end;
|
|
}
|
|
|
|
/* Verify start value (from AN2013-004b). */
|
|
if (!DNP3CheckStartBytes(hdr)) {
|
|
SCLogDebug("Invalid start bytes.");
|
|
return ALPROTO_FAILED;
|
|
}
|
|
|
|
/* Verify minimum length. */
|
|
if (hdr->len < DNP3_MIN_LEN) {
|
|
SCLogDebug("Packet too small to be a valid DNP3 fragment.");
|
|
return ALPROTO_FAILED;
|
|
}
|
|
|
|
end:
|
|
SCLogDebug("Detected DNP3.");
|
|
return ALPROTO_DNP3;
|
|
}
|
|
|
|
/**
|
|
* \brief Caculate the length of the transport layer with CRCs removed.
|
|
*
|
|
* \param input_len The length of the transport layer buffer.
|
|
*
|
|
* \retval The length of the buffer after CRCs are removed.
|
|
*/
|
|
static int DNP3CalculateTransportLengthWithoutCRCs(uint32_t input_len)
|
|
{
|
|
/* Too small. */
|
|
if (input_len < DNP3_CRC_LEN) {
|
|
return -1;
|
|
}
|
|
|
|
/* Get the number of complete blocks. */
|
|
int blocks = input_len / (DNP3_BLOCK_SIZE + DNP3_CRC_LEN);
|
|
|
|
/* And the number of bytes in the last block. */
|
|
int rem = input_len - (blocks * (DNP3_BLOCK_SIZE + DNP3_CRC_LEN));
|
|
|
|
if (rem) {
|
|
if (rem < DNP3_CRC_LEN) {
|
|
return -1;
|
|
}
|
|
return (blocks * DNP3_BLOCK_SIZE) + (rem - DNP3_CRC_LEN);
|
|
}
|
|
else {
|
|
return (blocks * DNP3_BLOCK_SIZE);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* \brief Reassemble the application layer by stripping the CRCs.
|
|
*
|
|
* Remove the CRCs from the user data blocks. The output is the user
|
|
* data with the CRCs removed as well as the transport header removed,
|
|
* but the input data still needs to include the transport header as
|
|
* its part of the first user data block.
|
|
*
|
|
* If the output length passed in is non-null, the new input data will
|
|
* be appended, and the output length pointer incremented as needed.
|
|
*
|
|
* \param input Input buffer starting at the transport header (which
|
|
* will be removed from the output).
|
|
* \param input_len Length of the input buffer.
|
|
* \param output Pointer to output buffer (may be realloc'd).
|
|
* \param output_len Pointer to output length.
|
|
*
|
|
* \retval 1 if reassembly was successful, otherwise 0.
|
|
*/
|
|
static int DNP3ReassembleApplicationLayer(const uint8_t *input,
|
|
uint32_t input_len, uint8_t **output, uint32_t *output_len)
|
|
{
|
|
int len = DNP3CalculateTransportLengthWithoutCRCs(input_len);
|
|
|
|
if (len <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
/* Remove one byte for the transport header and make sure we have
|
|
* at least one byte of user data. */
|
|
if (--len < 1) {
|
|
return 0;
|
|
}
|
|
|
|
if (*output == NULL) {
|
|
*output = SCCalloc(1, len);
|
|
if (unlikely(*output == NULL)) {
|
|
return 0;
|
|
}
|
|
}
|
|
else {
|
|
uint8_t *ptr = SCRealloc(*output, (size_t)(*output_len + len));
|
|
if (unlikely(ptr == NULL)) {
|
|
return 0;
|
|
}
|
|
*output = ptr;
|
|
}
|
|
|
|
int offset = 0, block_size;
|
|
while ((uint32_t)offset < input_len) {
|
|
if (input_len - offset > DNP3_BLOCK_SIZE + DNP3_CRC_LEN) {
|
|
block_size = DNP3_BLOCK_SIZE + DNP3_CRC_LEN;
|
|
}
|
|
else {
|
|
block_size = input_len - offset;
|
|
}
|
|
|
|
/* If handling the first block (offset is 0), trim off the
|
|
* first byte which is the transport header, and not part of
|
|
* the application data. */
|
|
if (offset == 0) {
|
|
offset++;
|
|
block_size--;
|
|
}
|
|
|
|
/* Need at least 3 bytes to continue. One for application
|
|
* data, and 2 for the CRC. If not, return failure for
|
|
* malformed frame. */
|
|
if (block_size < DNP3_CRC_LEN + 1) {
|
|
SCLogDebug("Not enough data to continue.");
|
|
return 0;
|
|
}
|
|
|
|
/* Make sure there is enough space to write into. */
|
|
if (block_size - DNP3_CRC_LEN > len) {
|
|
SCLogDebug("Not enough data to continue.");
|
|
return 0;
|
|
}
|
|
|
|
memcpy(*output + *output_len, input + offset,
|
|
block_size - DNP3_CRC_LEN);
|
|
*output_len += block_size - DNP3_CRC_LEN;
|
|
offset += block_size;
|
|
len -= block_size - DNP3_CRC_LEN;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* \brief Allocate a DNP3 state object.
|
|
*
|
|
* The DNP3 state object represents a single DNP3 TCP session.
|
|
*/
|
|
static void *DNP3StateAlloc(void)
|
|
{
|
|
SCEnter();
|
|
DNP3State *dnp3;
|
|
|
|
dnp3 = (DNP3State *)SCCalloc(1, sizeof(DNP3State));
|
|
if (unlikely(dnp3 == NULL)) {
|
|
return NULL;
|
|
}
|
|
TAILQ_INIT(&dnp3->tx_list);
|
|
|
|
SCReturnPtr(dnp3, "void");
|
|
}
|
|
|
|
/**
|
|
* \brief Set a DNP3 application layer event.
|
|
*
|
|
* Sets an event on the current transaction object.
|
|
*/
|
|
static void DNP3SetEvent(DNP3State *dnp3, uint8_t event)
|
|
{
|
|
if (dnp3 && dnp3->curr) {
|
|
AppLayerDecoderEventsSetEventRaw(&dnp3->curr->decoder_events, event);
|
|
dnp3->events++;
|
|
}
|
|
else {
|
|
SCLogWarning(SC_ERR_ALPARSER,
|
|
"Fail set set event, state or txn was NULL.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* \brief Set a DNP3 application layer event on a transaction.
|
|
*/
|
|
static void DNP3SetEventTx(DNP3Transaction *tx, uint8_t event)
|
|
{
|
|
AppLayerDecoderEventsSetEventRaw(&tx->decoder_events, event);
|
|
tx->dnp3->events++;
|
|
}
|
|
|
|
/**
|
|
* \brief Allocation a DNP3 transaction.
|
|
*/
|
|
static DNP3Transaction *DNP3TxAlloc(DNP3State *dnp3)
|
|
{
|
|
DNP3Transaction *tx = SCCalloc(1, sizeof(DNP3Transaction));
|
|
if (unlikely(tx == NULL)) {
|
|
return NULL;
|
|
}
|
|
dnp3->transaction_max++;
|
|
dnp3->unreplied++;
|
|
dnp3->curr = tx;
|
|
tx->dnp3 = dnp3;
|
|
tx->tx_num = dnp3->transaction_max;
|
|
TAILQ_INIT(&tx->request_objects);
|
|
TAILQ_INIT(&tx->response_objects);
|
|
TAILQ_INSERT_TAIL(&dnp3->tx_list, tx, next);
|
|
|
|
/* Check for flood state. */
|
|
if (dnp3->unreplied > DNP3_DEFAULT_REQ_FLOOD_COUNT) {
|
|
DNP3SetEvent(dnp3, DNP3_DECODER_EVENT_FLOODED);
|
|
dnp3->flooded = 1;
|
|
}
|
|
|
|
return tx;
|
|
}
|
|
|
|
/**
|
|
* \brief Calculate the length of a link frame with CRCs.
|
|
*
|
|
* This is required as the length parameter in the DNP3 header does not
|
|
* include the added CRCs.
|
|
*
|
|
* \param length The length from the DNP3 link header.
|
|
*
|
|
* \retval The length of the frame with CRCs included or 0 if the length isn't
|
|
* long enough to be a valid DNP3 frame.
|
|
*/
|
|
static uint32_t DNP3CalculateLinkLength(uint8_t length)
|
|
{
|
|
uint32_t frame_len = 0;
|
|
int rem;
|
|
|
|
/* Fail early if the length is less than the minimum size. */
|
|
if (length < DNP3_LINK_HDR_LEN) {
|
|
return 0;
|
|
}
|
|
|
|
/* Subtract the 5 bytes of the header that are included in the
|
|
* length. */
|
|
length -= DNP3_LINK_HDR_LEN;
|
|
|
|
rem = length % DNP3_BLOCK_SIZE;
|
|
frame_len = (length / DNP3_BLOCK_SIZE) * (DNP3_BLOCK_SIZE + DNP3_CRC_LEN);
|
|
if (rem) {
|
|
frame_len += rem + DNP3_CRC_LEN;
|
|
}
|
|
|
|
return frame_len + sizeof(DNP3LinkHeader);
|
|
}
|
|
|
|
/**
|
|
* \brief Check if the link function code specifies user data.
|
|
*
|
|
* \param header Point to link header.
|
|
*
|
|
* \retval 1 if frame contains user data, otherwise 0.
|
|
*/
|
|
static int DNP3IsUserData(const DNP3LinkHeader *header)
|
|
{
|
|
switch (DNP3_LINK_FC(header->control)) {
|
|
case DNP3_LINK_FC_CONFIRMED_USER_DATA:
|
|
case DNP3_LINK_FC_UNCONFIRMED_USER_DATA:
|
|
return 1;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* \brief Check if the frame has user data.
|
|
*
|
|
* Check if the DNP3 frame actually has user data by checking if data
|
|
* exists after the headers.
|
|
*
|
|
* \retval 1 if user data exists, otherwise 0.
|
|
*/
|
|
static int DNP3HasUserData(const DNP3LinkHeader *header)
|
|
{
|
|
if (DNP3_LINK_DIR(header->control)) {
|
|
return header->len >= DNP3_LINK_HDR_LEN + sizeof(DNP3TransportHeader) +
|
|
sizeof(DNP3ApplicationHeader);
|
|
}
|
|
else {
|
|
return header->len >= DNP3_LINK_HDR_LEN + sizeof(DNP3TransportHeader) +
|
|
sizeof(DNP3ApplicationHeader) + sizeof(DNP3InternalInd);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* \brief Reset a DNP3Buffer.
|
|
*/
|
|
static void DNP3BufferReset(DNP3Buffer *buffer)
|
|
{
|
|
buffer->offset = 0;
|
|
buffer->len = 0;
|
|
}
|
|
|
|
/**
|
|
* \brief Add data to a DNP3 buffer, enlarging the buffer if required.
|
|
*
|
|
* \param buffer Buffer to add data data.
|
|
* \param data Data to be added to buffer.
|
|
* \param len Size of data to be added to buffer.
|
|
*
|
|
* \param 1 if data was added successful, otherwise 0.
|
|
*/
|
|
static int DNP3BufferAdd(DNP3Buffer *buffer, const uint8_t *data, uint32_t len)
|
|
{
|
|
if (buffer->size == 0) {
|
|
buffer->buffer = SCCalloc(1, len);
|
|
if (unlikely(buffer->buffer == NULL)) {
|
|
return 0;
|
|
}
|
|
buffer->size = len;
|
|
}
|
|
else if (buffer->len + len > buffer->size) {
|
|
uint8_t *tmp = SCRealloc(buffer->buffer, buffer->len + len);
|
|
if (unlikely(tmp == NULL)) {
|
|
return 0;
|
|
}
|
|
buffer->buffer = tmp;
|
|
buffer->size = buffer->len + len;
|
|
}
|
|
memcpy(buffer->buffer + buffer->len, data, len);
|
|
buffer->len += len;
|
|
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* \brief Trim a DNP3 buffer.
|
|
*
|
|
* Trimming a buffer moves the data in the buffer up to the front of
|
|
* the buffer freeing up room at the end for more incoming data.
|
|
*
|
|
* \param buffer The buffer to trim.
|
|
*/
|
|
static void DNP3BufferTrim(DNP3Buffer *buffer)
|
|
{
|
|
if (buffer->offset == buffer->len) {
|
|
DNP3BufferReset(buffer);
|
|
}
|
|
else if (buffer->offset > 0) {
|
|
memmove(buffer->buffer, buffer->buffer + buffer->offset,
|
|
buffer->len - buffer->offset);
|
|
buffer->len = buffer->len - buffer->offset;
|
|
buffer->offset = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* \brief Free a DNP3 object.
|
|
*/
|
|
static void DNP3ObjectFree(DNP3Object *object)
|
|
{
|
|
if (object->points != NULL) {
|
|
DNP3FreeObjectPointList(object->group, object->variation,
|
|
object->points);
|
|
}
|
|
SCFree(object);
|
|
}
|
|
|
|
/**
|
|
* \breif Allocate a DNP3 object.
|
|
*/
|
|
static DNP3Object *DNP3ObjectAlloc(void)
|
|
{
|
|
DNP3Object *object = SCCalloc(1, sizeof(*object));
|
|
if (unlikely(object == NULL)) {
|
|
return NULL;
|
|
}
|
|
object->points = DNP3PointListAlloc();
|
|
if (object->points == NULL) {
|
|
DNP3ObjectFree(object);
|
|
return NULL;
|
|
}
|
|
return object;
|
|
}
|
|
|
|
/**
|
|
* \brief Decode DNP3 application objects.
|
|
*
|
|
* This function decoded known DNP3 application objects. As the
|
|
* protocol isn't self describing, we can only decode the buffer while
|
|
* the application objects are known. As soon as an unknown
|
|
* group/variation is hit, we must stop processing.
|
|
*
|
|
* \param buf the input buffer
|
|
* \param len length of the input buffer
|
|
* \param objects pointer to list where decoded objects will be stored.
|
|
*
|
|
* \retval 1 if all objects decoded, 0 if all objects could not be decoded (
|
|
* unknown group/variations)
|
|
*/
|
|
static int DNP3DecodeApplicationObjects(DNP3Transaction *tx, const uint8_t *buf,
|
|
uint32_t len, DNP3ObjectList *objects)
|
|
{
|
|
int retval = 0;
|
|
|
|
if (buf == NULL || len == 0) {
|
|
return 1;
|
|
}
|
|
|
|
while (len) {
|
|
uint32_t offset = 0;
|
|
|
|
if (len < sizeof(DNP3ObjHeader)) {
|
|
goto done;
|
|
}
|
|
DNP3ObjHeader *header = (DNP3ObjHeader *)buf;
|
|
offset += sizeof(DNP3ObjHeader);
|
|
|
|
DNP3Object *object = DNP3ObjectAlloc();
|
|
if (unlikely(object == NULL)) {
|
|
goto done;
|
|
}
|
|
TAILQ_INSERT_TAIL(objects, object, next);
|
|
|
|
object->group = header->group;
|
|
object->variation = header->variation;
|
|
object->qualifier = header->qualifier;
|
|
object->prefix_code = DNP3_OBJ_PREFIX(header->qualifier);
|
|
object->range_code = DNP3_OBJ_RANGE(header->qualifier);
|
|
|
|
/* IEEE 1815-2012, Table 4-5. */
|
|
switch (object->range_code) {
|
|
case 0x00:
|
|
case 0x03: {
|
|
/* 1 octet start and stop indexes OR 1 octet start and
|
|
* stop virtual addresses. */
|
|
if (offset + (sizeof(uint8_t) * 2) > len) {
|
|
/* Not enough data. */
|
|
SCLogDebug("Not enough data.");
|
|
goto not_enough_data;
|
|
}
|
|
object->start = buf[offset++];
|
|
object->stop = buf[offset++];
|
|
object->count = object->stop - object->start + 1;
|
|
break;
|
|
}
|
|
case 0x01:
|
|
case 0x04: {
|
|
/* 2 octet start and stop indexes OR 2 octect start
|
|
* and stop virtual addresses. */
|
|
if (offset + (sizeof(uint16_t) * 2) > len) {
|
|
/* Not enough data. */
|
|
SCLogDebug("Not enough data.");
|
|
goto not_enough_data;
|
|
}
|
|
object->start = DNP3_SWAP16(*(uint16_t *)(buf + offset));
|
|
offset += sizeof(uint16_t);
|
|
object->stop = DNP3_SWAP16(*(uint16_t *)(buf + offset));
|
|
offset += sizeof(uint16_t);
|
|
object->count = object->stop - object->start + 1;
|
|
break;
|
|
}
|
|
case 0x02:
|
|
case 0x05: {
|
|
/* 4 octet start and stop indexes OR 4 octect start
|
|
* and stop virtual addresses. */
|
|
if (offset + (sizeof(uint32_t) * 2) > len) {
|
|
/* Not enough data. */
|
|
SCLogDebug("Not enough data.");
|
|
goto not_enough_data;
|
|
}
|
|
object->start = DNP3_SWAP32(*(uint32_t *)(buf + offset));
|
|
offset += sizeof(uint32_t);
|
|
object->stop = DNP3_SWAP32(*(uint32_t *)(buf + offset));
|
|
offset += sizeof(uint32_t);
|
|
object->count = object->stop - object->start + 1;
|
|
break;
|
|
}
|
|
case 0x06:
|
|
/* No range field. */
|
|
object->count = 0;
|
|
break;
|
|
case 0x07:
|
|
/* 1 octet count of objects. */
|
|
if (offset + sizeof(uint8_t) > len) {
|
|
SCLogDebug("Not enough data.");
|
|
goto not_enough_data;
|
|
}
|
|
object->count = buf[offset];
|
|
offset += sizeof(uint8_t);
|
|
break;
|
|
case 0x08: {
|
|
/* 2 octet count of objects. */
|
|
if (offset + sizeof(uint16_t) > len) {
|
|
SCLogDebug("Not enough data.");
|
|
goto not_enough_data;
|
|
}
|
|
object->count = DNP3_SWAP16(*(uint16_t *)(buf + offset));
|
|
offset += sizeof(uint16_t);
|
|
break;
|
|
}
|
|
case 0x09: {
|
|
/* 4 octet count of objects. */
|
|
if (offset + sizeof(uint32_t) > len) {
|
|
SCLogDebug("Not enough data.");
|
|
goto not_enough_data;
|
|
}
|
|
object->count = DNP3_SWAP32(*(uint32_t *)(buf + offset));
|
|
offset += sizeof(uint32_t);
|
|
break;
|
|
}
|
|
case 0x0b: {
|
|
if (offset + sizeof(uint8_t) > len) {
|
|
/* Not enough data. */
|
|
SCLogDebug("Not enough data.");
|
|
goto not_enough_data;
|
|
}
|
|
object->count = *(uint8_t *)(buf + offset);
|
|
offset += sizeof(uint8_t);
|
|
break;
|
|
}
|
|
default:
|
|
SCLogDebug("Range code 0x%02x is reserved.",
|
|
object->range_code);
|
|
goto done;
|
|
}
|
|
|
|
buf += offset;
|
|
len -= offset;
|
|
|
|
if (object->variation == 0 || object->count == 0) {
|
|
goto next;
|
|
}
|
|
|
|
int event = DNP3DecodeObject(header->group, header->variation, &buf,
|
|
&len, object->prefix_code, object->start, object->count,
|
|
object->points);
|
|
if (event) {
|
|
DNP3SetEventTx(tx, DNP3_DECODER_EVENT_UNKNOWN_OBJECT);
|
|
goto done;
|
|
}
|
|
|
|
next:
|
|
continue;
|
|
}
|
|
|
|
/* All objects were decoded. */
|
|
retval = 1;
|
|
|
|
not_enough_data:
|
|
done:
|
|
return retval;
|
|
}
|
|
|
|
/**
|
|
* \brief Handle DNP3 request user data.
|
|
*
|
|
* \param dnp3 the current DNP3State
|
|
* \param input pointer to the DNP3 frame (starting with link header)
|
|
* \param input_len length of the input frame
|
|
*/
|
|
static void DNP3HandleUserDataRequest(DNP3State *dnp3, const uint8_t *input,
|
|
uint32_t input_len)
|
|
{
|
|
DNP3LinkHeader *lh;
|
|
DNP3TransportHeader th;
|
|
DNP3ApplicationHeader *ah;
|
|
DNP3Transaction *tx = NULL, *ttx;
|
|
|
|
lh = (DNP3LinkHeader *)input;
|
|
|
|
if (!DNP3CheckUserDataCRCs(input + sizeof(DNP3LinkHeader),
|
|
input_len - sizeof(DNP3LinkHeader))) {
|
|
return;
|
|
}
|
|
|
|
th = input[sizeof(DNP3LinkHeader)];
|
|
|
|
if (!DNP3_TH_FIR(th)) {
|
|
TAILQ_FOREACH(ttx, &dnp3->tx_list, next) {
|
|
if (ttx->request_lh.src == lh->src &&
|
|
ttx->request_lh.dst == lh->dst &&
|
|
ttx->has_request &&
|
|
!ttx->request_done &&
|
|
NEXT_TH_SEQNO(DNP3_TH_SEQ(ttx->request_th)) == DNP3_TH_SEQ(th))
|
|
{
|
|
tx = ttx;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (tx == NULL) {
|
|
return;
|
|
}
|
|
|
|
/* Update the saved transport header so subsequent segments
|
|
* will be matched to this sequence number. */
|
|
tx->response_th = th;
|
|
}
|
|
else {
|
|
ah = (DNP3ApplicationHeader *)(input + sizeof(DNP3LinkHeader) +
|
|
sizeof(DNP3TransportHeader));
|
|
|
|
/* Ignore confirms - for now. */
|
|
if (ah->function_code == DNP3_APP_FC_CONFIRM) {
|
|
return;
|
|
}
|
|
|
|
/* Create a transaction. */
|
|
tx = DNP3TxAlloc(dnp3);
|
|
if (unlikely(tx == NULL)) {
|
|
return;
|
|
}
|
|
tx->request_lh = *lh;
|
|
tx->request_th = th;
|
|
tx->request_ah = *ah;
|
|
tx->has_request = 1;
|
|
|
|
}
|
|
|
|
if (!DNP3ReassembleApplicationLayer(input + sizeof(DNP3LinkHeader),
|
|
input_len - sizeof(DNP3LinkHeader),
|
|
&tx->request_buffer, &tx->request_buffer_len)) {
|
|
|
|
/* Malformed, set event and mark as done. */
|
|
DNP3SetEvent(dnp3, DNP3_DECODER_EVENT_MALFORMED);
|
|
tx->request_done = 1;
|
|
return;
|
|
}
|
|
|
|
/* If this is not the final segment, just return. */
|
|
if (!DNP3_TH_FIN(th)) {
|
|
return;
|
|
}
|
|
|
|
tx->request_done = 1;
|
|
|
|
/* Some function codes do not expect a reply. */
|
|
switch (tx->request_ah.function_code) {
|
|
case DNP3_APP_FC_CONFIRM:
|
|
case DNP3_APP_FC_DIR_OPERATE_NR:
|
|
case DNP3_APP_FC_FREEZE_NR:
|
|
case DNP3_APP_FC_FREEZE_CLEAR_NR:
|
|
case DNP3_APP_FC_FREEZE_AT_TIME_NR:
|
|
case DNP3_APP_FC_AUTH_REQ_NR:
|
|
tx->response_done = 1;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (DNP3DecodeApplicationObjects(
|
|
tx, tx->request_buffer + sizeof(DNP3ApplicationHeader),
|
|
tx->request_buffer_len - sizeof(DNP3ApplicationHeader),
|
|
&tx->request_objects)) {
|
|
tx->request_complete = 1;
|
|
}
|
|
}
|
|
|
|
static void DNP3HandleUserDataResponse(DNP3State *dnp3, const uint8_t *input,
|
|
uint32_t input_len)
|
|
{
|
|
DNP3LinkHeader *lh;
|
|
DNP3TransportHeader th;
|
|
DNP3ApplicationHeader *ah;
|
|
DNP3InternalInd *iin;
|
|
DNP3Transaction *tx = NULL, *ttx;
|
|
uint32_t offset = 0;
|
|
|
|
lh = (DNP3LinkHeader *)input;
|
|
offset += sizeof(DNP3LinkHeader);
|
|
|
|
if (!DNP3CheckUserDataCRCs(input + offset, input_len - offset)) {
|
|
return;
|
|
}
|
|
|
|
th = input[offset++];
|
|
|
|
if (!DNP3_TH_FIR(th)) {
|
|
TAILQ_FOREACH(ttx, &dnp3->tx_list, next) {
|
|
if (ttx->response_lh.src == lh->src &&
|
|
ttx->response_lh.dst == lh->dst &&
|
|
ttx->has_response && !ttx->response_done &&
|
|
NEXT_TH_SEQNO(DNP3_TH_SEQ(ttx->response_th)) == DNP3_TH_SEQ(th))
|
|
{
|
|
tx = ttx;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (tx == NULL) {
|
|
return;
|
|
}
|
|
|
|
/* Replace the transport header in the transaction with this
|
|
* one in case there are more frames. */
|
|
tx->response_th = th;
|
|
}
|
|
else {
|
|
ah = (DNP3ApplicationHeader *)(input + offset);
|
|
offset += sizeof(DNP3ApplicationHeader);
|
|
iin = (DNP3InternalInd *)(input + offset);
|
|
|
|
if (ah->function_code == DNP3_APP_FC_UNSOLICITED_RESP) {
|
|
tx = DNP3TxAlloc(dnp3);
|
|
if (unlikely(tx == NULL)) {
|
|
return;
|
|
}
|
|
|
|
/* There is no request associated with an unsolicited
|
|
* response, so mark the request done as far as
|
|
* transaction state handling is concerned. */
|
|
tx->request_done = 1;
|
|
}
|
|
else {
|
|
/* Find transaction. */
|
|
TAILQ_FOREACH(ttx, &dnp3->tx_list, next) {
|
|
if (ttx->has_request &&
|
|
ttx->request_done &&
|
|
ttx->request_lh.src == lh->dst &&
|
|
ttx->request_lh.dst == lh->src &&
|
|
!ttx->has_response &&
|
|
!ttx->response_done &&
|
|
DNP3_APP_SEQ(ttx->request_ah.control) == DNP3_APP_SEQ(ah->control)) {
|
|
tx = ttx;
|
|
break;
|
|
}
|
|
}
|
|
if (tx == NULL) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
tx->has_response = 1;
|
|
tx->response_lh = *lh;
|
|
tx->response_th = th;
|
|
tx->response_ah = *ah;
|
|
tx->response_iin = *iin;
|
|
}
|
|
|
|
if (!DNP3ReassembleApplicationLayer(input + sizeof(DNP3LinkHeader),
|
|
input_len - sizeof(DNP3LinkHeader),
|
|
&tx->response_buffer, &tx->response_buffer_len)) {
|
|
DNP3SetEvent(dnp3, DNP3_DECODER_EVENT_MALFORMED);
|
|
return;
|
|
}
|
|
|
|
if (!DNP3_TH_FIN(th)) {
|
|
return;
|
|
}
|
|
|
|
tx->response_done = 1;
|
|
|
|
offset = sizeof(DNP3ApplicationHeader) + sizeof(DNP3InternalInd);
|
|
if (DNP3DecodeApplicationObjects(tx, tx->response_buffer + offset,
|
|
tx->response_buffer_len - offset,
|
|
&tx->response_objects)) {
|
|
tx->response_complete = 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* \brief Decode the DNP3 request link layer.
|
|
*
|
|
* \retval number of bytes processed or -1 if the data stream does not look
|
|
* like DNP3.
|
|
*/
|
|
static int DNP3HandleRequestLinkLayer(DNP3State *dnp3, const uint8_t *input,
|
|
uint32_t input_len)
|
|
{
|
|
SCEnter();
|
|
uint32_t processed = 0;
|
|
|
|
while (input_len) {
|
|
|
|
/* Need at least enough bytes for a DNP3 header. */
|
|
if (input_len < sizeof(DNP3LinkHeader)) {
|
|
break;
|
|
}
|
|
|
|
DNP3LinkHeader *header = (DNP3LinkHeader *)input;
|
|
|
|
if (!DNP3CheckStartBytes(header)) {
|
|
goto error;
|
|
}
|
|
|
|
if (!DNP3CheckLinkHeaderCRC(header)) {
|
|
DNP3SetEvent(dnp3, DNP3_DECODER_EVENT_BAD_LINK_CRC);
|
|
goto error;
|
|
}
|
|
|
|
uint32_t frame_len = DNP3CalculateLinkLength(header->len);
|
|
if (frame_len == 0) {
|
|
DNP3SetEvent(dnp3, DNP3_DECODER_EVENT_LEN_TOO_SMALL);
|
|
goto error;
|
|
}
|
|
if (input_len < frame_len) {
|
|
/* Insufficient data, just break - will wait for more data. */
|
|
break;
|
|
}
|
|
|
|
/* Ignore non-user data for now. */
|
|
if (!DNP3IsUserData(header)) {
|
|
goto next;
|
|
}
|
|
|
|
/* Make sure the header length is large enough for transport and
|
|
* application headers. */
|
|
if (!DNP3HasUserData(header)) {
|
|
DNP3SetEvent(dnp3, DNP3_DECODER_EVENT_LEN_TOO_SMALL);
|
|
goto next;
|
|
}
|
|
|
|
if (!DNP3CheckUserDataCRCs(input + sizeof(DNP3LinkHeader),
|
|
frame_len - sizeof(DNP3LinkHeader))) {
|
|
DNP3SetEvent(dnp3, DNP3_DECODER_EVENT_BAD_TRANSPORT_CRC);
|
|
goto next;
|
|
}
|
|
|
|
DNP3HandleUserDataRequest(dnp3, input, frame_len);
|
|
|
|
next:
|
|
/* Advance the input buffer. */
|
|
input += frame_len;
|
|
input_len -= frame_len;
|
|
processed += frame_len;
|
|
}
|
|
|
|
SCReturnInt(processed);
|
|
error:
|
|
/* Error out. Should only happen if this doesn't look like a DNP3
|
|
* frame. */
|
|
SCReturnInt(-1);
|
|
}
|
|
|
|
/**
|
|
* \brief Handle incoming request data.
|
|
*
|
|
* The actual request PDU parsing is done in
|
|
* DNP3HandleRequestLinkLayer. This function takes care of buffering TCP
|
|
* date if a segment does not contain a complete frame (or contains
|
|
* multiple frames, but not the complete final frame).
|
|
*/
|
|
static int DNP3ParseRequest(Flow *f, void *state, AppLayerParserState *pstate,
|
|
uint8_t *input, uint32_t input_len, void *local_data,
|
|
const uint8_t flags)
|
|
{
|
|
SCEnter();
|
|
DNP3State *dnp3 = (DNP3State *)state;
|
|
DNP3Buffer *buffer = &dnp3->request_buffer;
|
|
int processed = 0;
|
|
|
|
if (input_len == 0) {
|
|
SCReturnInt(1);
|
|
}
|
|
|
|
if (buffer->len) {
|
|
if (!DNP3BufferAdd(buffer, input, input_len)) {
|
|
SCLogError(SC_ERR_MEM_ALLOC, "Failed to allocate memory to buffer "
|
|
"DNP3 request data");
|
|
goto error;
|
|
}
|
|
processed = DNP3HandleRequestLinkLayer(dnp3,
|
|
buffer->buffer + buffer->offset,
|
|
buffer->len - buffer->offset);
|
|
if (processed < 0) {
|
|
goto error;
|
|
}
|
|
buffer->offset += processed;
|
|
DNP3BufferTrim(buffer);
|
|
}
|
|
else {
|
|
processed = DNP3HandleRequestLinkLayer(dnp3, input, input_len);
|
|
if (processed < 0) {
|
|
SCLogDebug("Failed to process request link layer.");
|
|
goto error;
|
|
}
|
|
|
|
input += processed;
|
|
input_len -= processed;
|
|
|
|
/* Not all data was processed, buffer it. */
|
|
if (input_len) {
|
|
if (!DNP3BufferAdd(buffer, input, input_len)) {
|
|
SCLogError(SC_ERR_MEM_ALLOC,
|
|
"Failed to allocate memory to buffer DNP3 request data");
|
|
goto error;
|
|
}
|
|
}
|
|
}
|
|
|
|
SCReturnInt(1);
|
|
|
|
error:
|
|
/* Reset the buffer. */
|
|
DNP3BufferReset(buffer);
|
|
SCReturnInt(-1);
|
|
}
|
|
|
|
/**
|
|
* \brief Decode the DNP3 response link layer.
|
|
*
|
|
* \retval number of bytes processed or -1 if the data stream does not
|
|
* like look DNP3.
|
|
*/
|
|
static int DNP3HandleResponseLinkLayer(DNP3State *dnp3, const uint8_t *input,
|
|
uint32_t input_len)
|
|
{
|
|
SCEnter();
|
|
uint32_t processed = 0;
|
|
|
|
while (input_len) {
|
|
|
|
/* Need at least enough bytes for a DNP3 header. */
|
|
if (input_len < sizeof(DNP3LinkHeader)) {
|
|
break;
|
|
}
|
|
|
|
DNP3LinkHeader *header = (DNP3LinkHeader *)input;
|
|
|
|
if (!DNP3CheckStartBytes(header)) {
|
|
goto error;
|
|
}
|
|
|
|
if (!DNP3CheckLinkHeaderCRC(header)) {
|
|
DNP3SetEvent(dnp3, DNP3_DECODER_EVENT_BAD_LINK_CRC);
|
|
goto error;
|
|
}
|
|
|
|
/* Calculate the number of bytes needed to for this frame. */
|
|
uint32_t frame_len = DNP3CalculateLinkLength(header->len);
|
|
if (frame_len == 0) {
|
|
DNP3SetEvent(dnp3, DNP3_DECODER_EVENT_LEN_TOO_SMALL);
|
|
goto error;
|
|
}
|
|
if (input_len < frame_len) {
|
|
/* Insufficient data, just break - will wait for more data. */
|
|
break;
|
|
}
|
|
|
|
/* Only handle user data frames for now. */
|
|
if (!DNP3IsUserData(header)) {
|
|
goto next;
|
|
}
|
|
|
|
/* Make sure the header length is large enough for transport and
|
|
* application headers. */
|
|
if (!DNP3HasUserData(header)) {
|
|
DNP3SetEvent(dnp3, DNP3_DECODER_EVENT_LEN_TOO_SMALL);
|
|
goto error;
|
|
}
|
|
|
|
if (!DNP3CheckUserDataCRCs(input + sizeof(DNP3LinkHeader),
|
|
frame_len - sizeof(DNP3LinkHeader))) {
|
|
DNP3SetEvent(dnp3, DNP3_DECODER_EVENT_BAD_TRANSPORT_CRC);
|
|
goto next;
|
|
}
|
|
|
|
DNP3HandleUserDataResponse(dnp3, input, frame_len);
|
|
|
|
next:
|
|
/* Advance the input buffer. */
|
|
input += frame_len;
|
|
input_len -= frame_len;
|
|
processed += frame_len;
|
|
}
|
|
|
|
SCReturnInt(processed);
|
|
error:
|
|
/* Error out. Should only happen if the data stream no longer
|
|
* looks like DNP3. */
|
|
SCReturnInt(-1);
|
|
}
|
|
|
|
/**
|
|
* \brief Parse incoming data.
|
|
*
|
|
* This is the entry function for DNP3 application layer data. Its
|
|
* main responsibility is buffering incoming data that cannot be
|
|
* processed.
|
|
*
|
|
* See DNP3ParseResponsePDUs for DNP3 frame handling.
|
|
*/
|
|
static int DNP3ParseResponse(Flow *f, void *state, AppLayerParserState *pstate,
|
|
uint8_t *input, uint32_t input_len, void *local_data,
|
|
const uint8_t flags)
|
|
{
|
|
SCEnter();
|
|
DNP3State *dnp3 = (DNP3State *)state;
|
|
DNP3Buffer *buffer = &dnp3->response_buffer;
|
|
int processed;
|
|
|
|
if (buffer->len) {
|
|
if (!DNP3BufferAdd(buffer, input, input_len)) {
|
|
SCLogError(SC_ERR_MEM_ALLOC, "Failed to allocate memory to buffer "
|
|
"DNP3 response data");
|
|
goto error;
|
|
}
|
|
processed = DNP3HandleResponseLinkLayer(dnp3,
|
|
buffer->buffer + buffer->offset,
|
|
buffer->len - buffer->offset);
|
|
if (processed < 0) {
|
|
goto error;
|
|
}
|
|
buffer->offset += processed;
|
|
DNP3BufferTrim(buffer);
|
|
}
|
|
else {
|
|
|
|
/* Check if this is a banner, ignore if it is. */
|
|
if (DNP3ContainsBanner(input, input_len)) {
|
|
goto done;
|
|
}
|
|
|
|
processed = DNP3HandleResponseLinkLayer(dnp3, input, input_len);
|
|
if (processed < 0) {
|
|
goto error;
|
|
}
|
|
input += processed;
|
|
input_len -= processed;
|
|
|
|
/* Not all data was processed, buffer it. */
|
|
if (input_len) {
|
|
if (!DNP3BufferAdd(buffer, input, input_len)) {
|
|
SCLogError(SC_ERR_MEM_ALLOC,
|
|
"Failed to allocate memory to buffer DNP3 response data");
|
|
goto error;
|
|
}
|
|
}
|
|
}
|
|
|
|
done:
|
|
SCReturnInt(1);
|
|
|
|
error:
|
|
/* An error occurred while processing DNP3 frames. Dump the
|
|
* buffer as we can't be assured that they are valid anymore. */
|
|
DNP3BufferReset(buffer);
|
|
SCReturnInt(-1);
|
|
}
|
|
|
|
static AppLayerDecoderEvents *DNP3GetEvents(void *state, uint64_t tx_id)
|
|
{
|
|
DNP3State *dnp3 = state;
|
|
DNP3Transaction *tx;
|
|
uint64_t tx_num = tx_id + 1;
|
|
|
|
if (dnp3->curr && dnp3->curr->tx_num == tx_num) {
|
|
return dnp3->curr->decoder_events;
|
|
}
|
|
|
|
TAILQ_FOREACH(tx, &dnp3->tx_list, next) {
|
|
if (tx->tx_num == tx_num) {
|
|
return tx->decoder_events;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static void *DNP3GetTx(void *alstate, uint64_t tx_id)
|
|
{
|
|
SCEnter();
|
|
DNP3State *dnp3 = (DNP3State *)alstate;
|
|
DNP3Transaction *tx = NULL;
|
|
uint64_t tx_num = tx_id + 1;
|
|
|
|
if (dnp3->curr && dnp3->curr->tx_num == (tx_num)) {
|
|
SCReturnPtr(dnp3->curr, "void");
|
|
}
|
|
|
|
TAILQ_FOREACH(tx, &dnp3->tx_list, next) {
|
|
if (tx_num != tx->tx_num) {
|
|
continue;
|
|
}
|
|
SCReturnPtr(tx, "void");
|
|
}
|
|
|
|
SCReturnPtr(NULL, "void");
|
|
}
|
|
|
|
static uint64_t DNP3GetTxCnt(void *state)
|
|
{
|
|
SCEnter();
|
|
uint64_t count = ((uint64_t)((DNP3State *)state)->transaction_max);
|
|
SCReturnUInt(count);
|
|
}
|
|
|
|
/**
|
|
* \brief Free all the objects in a DNP3ObjectList.
|
|
*/
|
|
static void DNP3TxFreeObjectList(DNP3ObjectList *objects)
|
|
{
|
|
DNP3Object *object;
|
|
|
|
while ((object = TAILQ_FIRST(objects)) != NULL) {
|
|
TAILQ_REMOVE(objects, object, next);
|
|
DNP3ObjectFree(object);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* \brief Free a DNP3 transaction.
|
|
*/
|
|
static void DNP3TxFree(DNP3Transaction *tx)
|
|
{
|
|
SCEnter();
|
|
|
|
if (tx->request_buffer != NULL) {
|
|
SCFree(tx->request_buffer);
|
|
}
|
|
|
|
if (tx->response_buffer != NULL) {
|
|
SCFree(tx->response_buffer);
|
|
}
|
|
|
|
AppLayerDecoderEventsFreeEvents(&tx->decoder_events);
|
|
|
|
if (tx->de_state != NULL) {
|
|
DetectEngineStateFree(tx->de_state);
|
|
}
|
|
|
|
DNP3TxFreeObjectList(&tx->request_objects);
|
|
DNP3TxFreeObjectList(&tx->response_objects);
|
|
|
|
SCFree(tx);
|
|
SCReturn;
|
|
}
|
|
|
|
/**
|
|
* \brief Free a transaction by ID on a specific DNP3 state.
|
|
*
|
|
* This function is called by the app-layer to free a transaction on a
|
|
* specific DNP3 state object.
|
|
*/
|
|
static void DNP3StateTxFree(void *state, uint64_t tx_id)
|
|
{
|
|
SCEnter();
|
|
DNP3State *dnp3 = state;
|
|
DNP3Transaction *tx = NULL, *ttx;
|
|
uint64_t tx_num = tx_id + 1;
|
|
|
|
TAILQ_FOREACH_SAFE(tx, &dnp3->tx_list, next, ttx) {
|
|
|
|
if (tx->tx_num != tx_num) {
|
|
continue;
|
|
}
|
|
|
|
if (tx == dnp3->curr) {
|
|
dnp3->curr = NULL;
|
|
}
|
|
|
|
if (tx->decoder_events != NULL) {
|
|
if (tx->decoder_events->cnt <= dnp3->events) {
|
|
dnp3->events -= tx->decoder_events->cnt;
|
|
}
|
|
else {
|
|
dnp3->events = 0;
|
|
}
|
|
}
|
|
dnp3->unreplied--;
|
|
|
|
/* Check flood state. */
|
|
if (dnp3->flooded && dnp3->unreplied < DNP3_DEFAULT_REQ_FLOOD_COUNT) {
|
|
dnp3->flooded = 0;
|
|
}
|
|
|
|
TAILQ_REMOVE(&dnp3->tx_list, tx, next);
|
|
DNP3TxFree(tx);
|
|
break;
|
|
}
|
|
|
|
SCReturn;
|
|
}
|
|
|
|
/**
|
|
* \brief Free a DNP3 state.
|
|
*/
|
|
static void DNP3StateFree(void *state)
|
|
{
|
|
SCEnter();
|
|
DNP3State *dnp3 = state;
|
|
DNP3Transaction *tx;
|
|
if (state != NULL) {
|
|
while ((tx = TAILQ_FIRST(&dnp3->tx_list)) != NULL) {
|
|
TAILQ_REMOVE(&dnp3->tx_list, tx, next);
|
|
DNP3TxFree(tx);
|
|
}
|
|
if (dnp3->request_buffer.buffer != NULL) {
|
|
SCFree(dnp3->request_buffer.buffer);
|
|
}
|
|
if (dnp3->response_buffer.buffer != NULL) {
|
|
SCFree(dnp3->response_buffer.buffer);
|
|
}
|
|
SCFree(dnp3);
|
|
}
|
|
SCReturn;
|
|
}
|
|
|
|
/**
|
|
* \brief Called by the app-layer to get the state progress.
|
|
*/
|
|
static int DNP3GetAlstateProgress(void *tx, uint8_t direction)
|
|
{
|
|
DNP3Transaction *dnp3tx = (DNP3Transaction *)tx;
|
|
DNP3State *dnp3 = dnp3tx->dnp3;
|
|
int retval = 0;
|
|
|
|
/* If flooded, "ack" old transactions. */
|
|
if (dnp3->flooded && (dnp3->transaction_max -
|
|
dnp3tx->tx_num >= DNP3_DEFAULT_REQ_FLOOD_COUNT)) {
|
|
SCLogDebug("flooded: returning tx as done.");
|
|
SCReturnInt(1);
|
|
}
|
|
|
|
if (direction & STREAM_TOCLIENT && dnp3tx->response_done) {
|
|
retval = 1;
|
|
}
|
|
else if (direction & STREAM_TOSERVER && dnp3tx->request_done) {
|
|
retval = 1;
|
|
}
|
|
|
|
SCReturnInt(retval);
|
|
}
|
|
|
|
/**
|
|
* \brief App-layer support.
|
|
*/
|
|
static int DNP3GetAlstateProgressCompletionStatus(uint8_t direction)
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* \brief App-layer support.
|
|
*/
|
|
static int DNP3StateGetEventInfo(const char *event_name, int *event_id,
|
|
AppLayerEventType *event_type)
|
|
{
|
|
*event_id = SCMapEnumNameToValue(event_name, dnp3_decoder_event_table);
|
|
if (*event_id == -1) {
|
|
SCLogError(SC_ERR_INVALID_ENUM_MAP, "Event \"%s\" not present in "
|
|
"the DNP3 enum event map table.", event_name);
|
|
return -1;
|
|
}
|
|
|
|
*event_type = APP_LAYER_EVENT_TYPE_TRANSACTION;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* \brief App-layer support.
|
|
*/
|
|
static DetectEngineState *DNP3GetTxDetectState(void *vtx)
|
|
{
|
|
DNP3Transaction *tx = vtx;
|
|
return tx->de_state;
|
|
}
|
|
|
|
/**
|
|
* \brief App-layer support.
|
|
*/
|
|
static int DNP3SetTxDetectState(void *vtx, DetectEngineState *s)
|
|
{
|
|
DNP3Transaction *tx = vtx;
|
|
tx->de_state = s;
|
|
return 0;
|
|
}
|
|
|
|
static void DNP3SetTxLogged(void *alstate, void *vtx, LoggerId logged)
|
|
{
|
|
DNP3Transaction *tx = (DNP3Transaction *)vtx;
|
|
tx->logged = logged;
|
|
}
|
|
|
|
static LoggerId DNP3GetTxLogged(void *alstate, void *vtx)
|
|
{
|
|
DNP3Transaction *tx = (DNP3Transaction *)vtx;
|
|
return tx->logged;
|
|
}
|
|
|
|
/**
|
|
* \brief Check if the prefix code is a size prefix.
|
|
*
|
|
* \retval 1 if the prefix_code specifies a size prefix, 0 if not.
|
|
*/
|
|
int DNP3PrefixIsSize(uint8_t prefix_code)
|
|
{
|
|
switch (prefix_code) {
|
|
case 0x04:
|
|
case 0x05:
|
|
case 0x06:
|
|
return 1;
|
|
break;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
static uint64_t DNP3GetTxDetectFlags(void *vtx, uint8_t dir)
|
|
{
|
|
DNP3Transaction *tx = (DNP3Transaction *)vtx;
|
|
if (dir & STREAM_TOSERVER) {
|
|
return tx->detect_flags_ts;
|
|
} else {
|
|
return tx->detect_flags_tc;
|
|
}
|
|
}
|
|
|
|
static void DNP3SetTxDetectFlags(void *vtx, uint8_t dir, uint64_t detect_flags)
|
|
{
|
|
DNP3Transaction *tx = (DNP3Transaction *)vtx;
|
|
if (dir & STREAM_TOSERVER) {
|
|
tx->detect_flags_ts = detect_flags;
|
|
} else {
|
|
tx->detect_flags_tc = detect_flags;
|
|
}
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* \brief Register the DNP3 application protocol parser.
|
|
*/
|
|
void RegisterDNP3Parsers(void)
|
|
{
|
|
SCEnter();
|
|
|
|
const char *proto_name = "dnp3";
|
|
|
|
if (AppLayerProtoDetectConfProtoDetectionEnabled("tcp", proto_name))
|
|
{
|
|
AppLayerProtoDetectRegisterProtocol(ALPROTO_DNP3, proto_name);
|
|
|
|
if (RunmodeIsUnittests()) {
|
|
AppLayerProtoDetectPPRegister(IPPROTO_TCP, DNP3_DEFAULT_PORT,
|
|
ALPROTO_DNP3, 0, sizeof(DNP3LinkHeader), STREAM_TOSERVER,
|
|
DNP3ProbingParser, NULL);
|
|
}
|
|
else {
|
|
if (!AppLayerProtoDetectPPParseConfPorts("tcp", IPPROTO_TCP,
|
|
proto_name, ALPROTO_DNP3, 0, sizeof(DNP3LinkHeader),
|
|
DNP3ProbingParser, NULL)) {
|
|
#ifndef AFLFUZZ_APPLAYER
|
|
return;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
}
|
|
else {
|
|
SCLogConfig("Protocol detection and parser disabled for DNP3.");
|
|
SCReturn;
|
|
}
|
|
|
|
if (AppLayerParserConfParserEnabled("tcp", proto_name))
|
|
{
|
|
SCLogConfig("Registering DNP3/tcp parsers.");
|
|
|
|
AppLayerParserRegisterParser(IPPROTO_TCP, ALPROTO_DNP3, STREAM_TOSERVER,
|
|
DNP3ParseRequest);
|
|
AppLayerParserRegisterParser(IPPROTO_TCP, ALPROTO_DNP3, STREAM_TOCLIENT,
|
|
DNP3ParseResponse);
|
|
|
|
AppLayerParserRegisterStateFuncs(IPPROTO_TCP, ALPROTO_DNP3,
|
|
DNP3StateAlloc, DNP3StateFree);
|
|
|
|
AppLayerParserRegisterGetEventsFunc(IPPROTO_TCP, ALPROTO_DNP3,
|
|
DNP3GetEvents);
|
|
AppLayerParserRegisterDetectStateFuncs(IPPROTO_TCP, ALPROTO_DNP3,
|
|
DNP3GetTxDetectState, DNP3SetTxDetectState);
|
|
AppLayerParserRegisterDetectFlagsFuncs(IPPROTO_TCP, ALPROTO_DNP3,
|
|
DNP3GetTxDetectFlags, DNP3SetTxDetectFlags);
|
|
|
|
AppLayerParserRegisterGetTx(IPPROTO_TCP, ALPROTO_DNP3, DNP3GetTx);
|
|
AppLayerParserRegisterGetTxCnt(IPPROTO_TCP, ALPROTO_DNP3, DNP3GetTxCnt);
|
|
AppLayerParserRegisterTxFreeFunc(IPPROTO_TCP, ALPROTO_DNP3,
|
|
DNP3StateTxFree);
|
|
|
|
AppLayerParserRegisterGetStateProgressFunc(IPPROTO_TCP, ALPROTO_DNP3,
|
|
DNP3GetAlstateProgress);
|
|
AppLayerParserRegisterGetStateProgressCompletionStatus(ALPROTO_DNP3,
|
|
DNP3GetAlstateProgressCompletionStatus);
|
|
|
|
AppLayerParserRegisterGetEventInfo(IPPROTO_TCP, ALPROTO_DNP3,
|
|
DNP3StateGetEventInfo);
|
|
|
|
AppLayerParserRegisterLoggerFuncs(IPPROTO_TCP, ALPROTO_DNP3,
|
|
DNP3GetTxLogged, DNP3SetTxLogged);
|
|
}
|
|
else {
|
|
SCLogConfig("Parser disabled for protocol %s. "
|
|
"Protocol detection still on.", proto_name);
|
|
}
|
|
|
|
#ifdef UNITTESTS
|
|
AppLayerParserRegisterProtocolUnittests(IPPROTO_TCP, ALPROTO_DNP3,
|
|
DNP3ParserRegisterTests);
|
|
#endif
|
|
|
|
SCReturn;
|
|
}
|
|
|
|
#ifdef UNITTESTS
|
|
|
|
#include "flow-util.h"
|
|
#include "stream-tcp.h"
|
|
|
|
/**
|
|
* \brief Utility function to fix CRCs when mangling a frame.
|
|
*/
|
|
static void DNP3FixCrc(uint8_t *data, uint32_t len)
|
|
{
|
|
uint32_t block_size;
|
|
|
|
while (len) {
|
|
if (len >= DNP3_BLOCK_SIZE + DNP3_CRC_LEN) {
|
|
block_size = DNP3_BLOCK_SIZE;
|
|
} else {
|
|
block_size = len - DNP3_CRC_LEN;
|
|
}
|
|
uint16_t crc = DNP3ComputeCRC(data, block_size);
|
|
data[block_size + 1] = (crc >> 8) & 0xff;
|
|
data[block_size] = crc & 0xff;
|
|
data += block_size + DNP3_CRC_LEN;
|
|
len -= block_size + DNP3_CRC_LEN;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* \test Test CRC checking on partial and full blocks.
|
|
*/
|
|
static int DNP3ParserTestCheckCRC(void)
|
|
{
|
|
uint8_t request[] = {
|
|
/* DNP3 start. */
|
|
0x05, 0x64, 0x1a, 0xc4, 0x02, 0x00, 0x01, 0x00,
|
|
0xa5, 0xe9,
|
|
|
|
/* Transport header. */
|
|
0xff,
|
|
|
|
/* Application layer - segment 1. */
|
|
0xc9, 0x05, 0x0c, 0x01, 0x28, 0x01, 0x00, 0x00,
|
|
0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x72,
|
|
0xef,
|
|
|
|
/* Application layer - segment 2. */
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff
|
|
};
|
|
|
|
/* Check link header CRC. */
|
|
FAIL_IF(!DNP3CheckCRC(request, sizeof(DNP3LinkHeader)));
|
|
|
|
/* Check first application layer segment. */
|
|
FAIL_IF(!DNP3CheckCRC(request + sizeof(DNP3LinkHeader),
|
|
DNP3_BLOCK_SIZE + DNP3_CRC_LEN));
|
|
|
|
/* Change a byte in link header, should fail now. */
|
|
request[2]++;
|
|
FAIL_IF(DNP3CheckCRC(request, sizeof(DNP3LinkHeader)));
|
|
|
|
/* Change a byte in the first application segment, should fail
|
|
* now. */
|
|
request[sizeof(DNP3LinkHeader) + 3]++;
|
|
FAIL_IF(DNP3CheckCRC(request + sizeof(DNP3LinkHeader),
|
|
DNP3_BLOCK_SIZE + DNP3_CRC_LEN));
|
|
|
|
PASS;
|
|
}
|
|
|
|
/**
|
|
* \test Test validation of all CRCs in user data.
|
|
*/
|
|
static int DNP3CheckUserDataCRCsTest(void)
|
|
{
|
|
/* Multi-block data with valid CRCs. */
|
|
uint8_t data_valid[] = {
|
|
0xff, 0xc9, 0x05, 0x0c,
|
|
0x01, 0x28, 0x01, 0x00,
|
|
0x00, 0x00, 0x01, 0x01,
|
|
0x01, 0x00, 0x00, 0x00,
|
|
0x72, 0xef, /* CRC. */
|
|
|
|
0xff, 0xc9, 0x05, 0x0c,
|
|
0x01, 0x28, 0x01, 0x00,
|
|
0x00, 0x00, 0x01, 0x01,
|
|
0x01, 0x00, 0x00, 0x00,
|
|
0x72, 0xef, /* CRC. */
|
|
|
|
0xff, 0xc9, 0x05, 0x0c,
|
|
0x01, 0x28, 0x01, 0x00,
|
|
0x00, 0x00, 0x01, 0x01,
|
|
0x01, 0x00, 0x00, 0x00,
|
|
0x72, 0xef, /* CRC. */
|
|
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00,
|
|
0xff, 0xff, /* CRC. */
|
|
};
|
|
FAIL_IF(!DNP3CheckUserDataCRCs(data_valid, sizeof(data_valid)));
|
|
|
|
/* Multi-block data with one non-crc byte altered. */
|
|
uint8_t data_invalid[] = {
|
|
0xff, 0xc9, 0x05, 0x0c,
|
|
0x01, 0x28, 0x01, 0x00,
|
|
0x00, 0x00, 0x01, 0x01,
|
|
0x01, 0x00, 0x00, 0x00,
|
|
0x72, 0xef, /* CRC. */
|
|
|
|
0xff, 0xc9, 0x05, 0x0c,
|
|
0x01, 0x28, 0x01, 0x00,
|
|
0x00, 0x00, 0x01, 0x01,
|
|
0x01, 0x00, 0x00, 0x00,
|
|
0x72, 0xef, /* CRC. */
|
|
|
|
0xff, 0xc9, 0x05, 0x0c,
|
|
0x01, 0x28, 0x01, 0x00,
|
|
0x00, 0x00, 0x01, 0x01,
|
|
0x01, 0x00, 0x00, 0x00,
|
|
0x72, 0xef, /* CRC. */
|
|
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x01, /* Invalid byte. */
|
|
0xff, 0xff, /* CRC. */
|
|
};
|
|
FAIL_IF(DNP3CheckUserDataCRCs(data_invalid, sizeof(data_invalid)));
|
|
|
|
/* 1 byte - need at least 3. */
|
|
uint8_t one_byte_nocrc[] = { 0x01 };
|
|
FAIL_IF(DNP3CheckUserDataCRCs(one_byte_nocrc, sizeof(one_byte_nocrc)));
|
|
|
|
/* 2 bytes - need at least 3. */
|
|
uint8_t two_byte_nocrc[] = { 0x01, 0x02 };
|
|
FAIL_IF(DNP3CheckUserDataCRCs(two_byte_nocrc, sizeof(two_byte_nocrc)));
|
|
|
|
/* 3 bytes, valid CRC. */
|
|
uint8_t three_bytes_good_crc[] = { 0x00, 0x00, 0x00 };
|
|
*(uint16_t *)(three_bytes_good_crc + 1) = DNP3ComputeCRC(
|
|
three_bytes_good_crc, 1);
|
|
FAIL_IF(!DNP3CheckUserDataCRCs(three_bytes_good_crc,
|
|
sizeof(three_bytes_good_crc)));
|
|
|
|
PASS;
|
|
}
|
|
|
|
/**
|
|
* \test Test the link layer length calculation.
|
|
*
|
|
* Test the calculation that converts the link provided in the DNP3
|
|
* header to the actual length of the frame. That is the length with
|
|
* CRCs as the length in the header does not include CRCs.
|
|
*/
|
|
static int DNP3CalculateLinkLengthTest(void)
|
|
{
|
|
/* These are invalid. */
|
|
FAIL_IF(DNP3CalculateLinkLength(0) != 0);
|
|
FAIL_IF(DNP3CalculateLinkLength(1) != 0);
|
|
FAIL_IF(DNP3CalculateLinkLength(2) != 0);
|
|
FAIL_IF(DNP3CalculateLinkLength(3) != 0);
|
|
FAIL_IF(DNP3CalculateLinkLength(4) != 0);
|
|
|
|
/* This is the minimum size. */
|
|
FAIL_IF(DNP3CalculateLinkLength(5) != 10);
|
|
|
|
/* 1 full user data blocks of data. */
|
|
FAIL_IF(DNP3CalculateLinkLength(21) != 28);
|
|
|
|
/* 2 full user data blocks of data. */
|
|
FAIL_IF(DNP3CalculateLinkLength(37) != 46);
|
|
|
|
/* 2 full user data blocks, plus one more byte. */
|
|
/* 2 full user data blocks of data. */
|
|
FAIL_IF(DNP3CalculateLinkLength(38) != 49);
|
|
|
|
/* The maximum size. */
|
|
FAIL_IF(DNP3CalculateLinkLength(255) != 292);
|
|
|
|
PASS;
|
|
}
|
|
|
|
/**
|
|
* \test The conversion of length with CRCs to the length without
|
|
* CRCs.
|
|
*/
|
|
static int DNP3CalculateTransportLengthWithoutCRCsTest(void)
|
|
{
|
|
FAIL_IF(DNP3CalculateTransportLengthWithoutCRCs(0) != -1);
|
|
FAIL_IF(DNP3CalculateTransportLengthWithoutCRCs(1) != -1);
|
|
FAIL_IF(DNP3CalculateTransportLengthWithoutCRCs(2) != 0);
|
|
FAIL_IF(DNP3CalculateTransportLengthWithoutCRCs(3) != 1);
|
|
FAIL_IF(DNP3CalculateTransportLengthWithoutCRCs(16) != 14);
|
|
FAIL_IF(DNP3CalculateTransportLengthWithoutCRCs(17) != 15);
|
|
FAIL_IF(DNP3CalculateTransportLengthWithoutCRCs(18) != 16);
|
|
|
|
/* 19 bytes is not enough for a second block. */
|
|
FAIL_IF(DNP3CalculateTransportLengthWithoutCRCs(19) != -1);
|
|
|
|
/* 20 bytes really isn't enough either, but is large enough to
|
|
* satisfy the CRC on the second block. */
|
|
FAIL_IF(DNP3CalculateTransportLengthWithoutCRCs(20) != 16);
|
|
|
|
FAIL_IF(DNP3CalculateTransportLengthWithoutCRCs(21) != 17);
|
|
|
|
PASS;
|
|
}
|
|
|
|
/**
|
|
* \test Test the validation of the link header CRC.
|
|
*/
|
|
static int DNP3ParserCheckLinkHeaderCRC(void)
|
|
{
|
|
/* DNP3 frame with valid headers and CRCs. */
|
|
uint8_t request[] = {
|
|
/* DNP3 start. */
|
|
0x05, 0x64, 0x1a, 0xc4, 0x02, 0x00, 0x01, 0x00,
|
|
0xa5, 0xe9,
|
|
|
|
/* Transport header. */
|
|
0xff,
|
|
|
|
/* Application layer. */
|
|
0xc9, 0x05, 0x0c, 0x01, 0x28, 0x01, 0x00, 0x00,
|
|
0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x72,
|
|
0xef, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff
|
|
};
|
|
|
|
DNP3LinkHeader *header = (DNP3LinkHeader *)request;
|
|
FAIL_IF(!DNP3CheckLinkHeaderCRC(header));
|
|
|
|
/* Alter a byte in the header. */
|
|
request[4] = 0;
|
|
FAIL_IF(DNP3CheckLinkHeaderCRC(header));
|
|
|
|
PASS;
|
|
}
|
|
|
|
/**
|
|
* \test Test removal of CRCs from user data.
|
|
*/
|
|
static int DNP3ReassembleApplicationLayerTest01(void)
|
|
{
|
|
uint32_t reassembled_len = 0;
|
|
uint8_t *output = NULL;
|
|
|
|
uint8_t payload[] = {
|
|
|
|
0xff, 0xc9, 0x05, 0x0c,
|
|
0x01, 0x28, 0x01, 0x00,
|
|
0x00, 0x00, 0x01, 0x01,
|
|
0x01, 0x00, 0x00, 0x00,
|
|
0x72, 0xef, /* CRC. */
|
|
|
|
0xff, 0xc9, 0x05, 0x0c,
|
|
0x01, 0x28, 0x01, 0x00,
|
|
0x00, 0x00, 0x01, 0x01,
|
|
0x01, 0x00, 0x00, 0x00,
|
|
0x72, 0xef, /* CRC. */
|
|
|
|
0xff, 0xc9, 0x05, 0x0c,
|
|
0x01, 0x28, 0x01, 0x00,
|
|
0x00, 0x00, 0x01, 0x01,
|
|
0x01, 0x00, 0x00, 0x00,
|
|
0x72, 0xef, /* CRC. */
|
|
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00,
|
|
0xff, 0xff, /* CRC. */
|
|
};
|
|
|
|
uint8_t expected[] = {
|
|
0xc9, 0x05, 0x0c,
|
|
0x01, 0x28, 0x01, 0x00,
|
|
0x00, 0x00, 0x01, 0x01,
|
|
0x01, 0x00, 0x00, 0x00,
|
|
/* CRC removed. */
|
|
0xff, 0xc9, 0x05, 0x0c,
|
|
0x01, 0x28, 0x01, 0x00,
|
|
0x00, 0x00, 0x01, 0x01,
|
|
0x01, 0x00, 0x00, 0x00,
|
|
/* CRC removed. */
|
|
0xff, 0xc9, 0x05, 0x0c,
|
|
0x01, 0x28, 0x01, 0x00,
|
|
0x00, 0x00, 0x01, 0x01,
|
|
0x01, 0x00, 0x00, 0x00,
|
|
/* CRC removed. */
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00
|
|
/* CRC removed. */
|
|
};
|
|
|
|
/* Valid frame. */
|
|
FAIL_IF(!DNP3ReassembleApplicationLayer(payload,
|
|
sizeof(payload), &output, &reassembled_len));
|
|
FAIL_IF(output == NULL);
|
|
FAIL_IF(reassembled_len != sizeof(expected));
|
|
FAIL_IF(memcmp(expected, output, reassembled_len));
|
|
SCFree(output);
|
|
|
|
/* 1 byte, invalid. */
|
|
reassembled_len = 0;
|
|
output = NULL;
|
|
FAIL_IF(DNP3ReassembleApplicationLayer(payload, 1, &output,
|
|
&reassembled_len));
|
|
FAIL_IF(output != NULL);
|
|
FAIL_IF(reassembled_len != 0);
|
|
|
|
/* 2 bytes, invalid. */
|
|
reassembled_len = 0;
|
|
output = NULL;
|
|
FAIL_IF(DNP3ReassembleApplicationLayer(payload, 2, &output,
|
|
&reassembled_len));
|
|
FAIL_IF(output != NULL);
|
|
FAIL_IF(reassembled_len != 0);
|
|
|
|
/* 3 bytes, minimum - but that would only be the transport header
|
|
* which isn't included in the output. */
|
|
reassembled_len = 0;
|
|
output = NULL;
|
|
FAIL_IF(DNP3ReassembleApplicationLayer(payload, 3, &output,
|
|
&reassembled_len));
|
|
FAIL_IF(output != NULL);
|
|
FAIL_IF(reassembled_len != 0);
|
|
|
|
/* 4 bytes is the minimum to get any reassembled data. */
|
|
reassembled_len = 0;
|
|
output = NULL;
|
|
FAIL_IF(!DNP3ReassembleApplicationLayer(payload, 4, &output,
|
|
&reassembled_len));
|
|
FAIL_IF(output == NULL);
|
|
FAIL_IF(reassembled_len != 1);
|
|
|
|
/* Last block too short (by 1 byte) for data + CRC. */
|
|
uint8_t short_payload1[] = {
|
|
|
|
0xff, 0xc9, 0x05, 0x0c,
|
|
0x01, 0x28, 0x01, 0x00,
|
|
0x00, 0x00, 0x01, 0x01,
|
|
0x01, 0x00, 0x00, 0x00,
|
|
0x72, 0xef, /* CRC. */
|
|
|
|
0xff, 0xc9, 0x05, 0x0c,
|
|
0x01, 0x28, 0x01, 0x00,
|
|
0x00, 0x00, 0x01, 0x01,
|
|
0x01, 0x00, 0x00, 0x00,
|
|
0x72, 0xef, /* CRC. */
|
|
|
|
0xff, 0xc9, 0x05, 0x0c,
|
|
0x01, 0x28, 0x01, 0x00,
|
|
0x00, 0x00, 0x01, 0x01,
|
|
0x01, 0x00, 0x00, 0x00,
|
|
0x72, 0xef, /* CRC. */
|
|
|
|
0x00, 0x00
|
|
};
|
|
reassembled_len = 0;
|
|
FAIL_IF(DNP3ReassembleApplicationLayer(short_payload1,
|
|
sizeof(short_payload1), &output, &reassembled_len));
|
|
|
|
/* Last block too short (by 2 bytes) for data + CRC. */
|
|
uint8_t short_payload2[] = {
|
|
|
|
0xff, 0xc9, 0x05, 0x0c,
|
|
0x01, 0x28, 0x01, 0x00,
|
|
0x00, 0x00, 0x01, 0x01,
|
|
0x01, 0x00, 0x00, 0x00,
|
|
0x72, 0xef, /* CRC. */
|
|
|
|
0xff, 0xc9, 0x05, 0x0c,
|
|
0x01, 0x28, 0x01, 0x00,
|
|
0x00, 0x00, 0x01, 0x01,
|
|
0x01, 0x00, 0x00, 0x00,
|
|
0x72, 0xef, /* CRC. */
|
|
|
|
0xff, 0xc9, 0x05, 0x0c,
|
|
0x01, 0x28, 0x01, 0x00,
|
|
0x00, 0x00, 0x01, 0x01,
|
|
0x01, 0x00, 0x00, 0x00,
|
|
0x72, 0xef, /* CRC. */
|
|
|
|
0x00,
|
|
};
|
|
reassembled_len = 0;
|
|
FAIL_IF(DNP3ReassembleApplicationLayer(short_payload2,
|
|
sizeof(short_payload2), &output, &reassembled_len));
|
|
|
|
PASS;
|
|
}
|
|
|
|
/**
|
|
* \test Test the probing parser.
|
|
*/
|
|
static int DNP3ProbingParserTest(void)
|
|
{
|
|
uint8_t pkt[] = {
|
|
0x05, 0x64, 0x05, 0xc9, 0x03, 0x00, 0x04, 0x00,
|
|
0xbd, 0x71
|
|
};
|
|
uint8_t rdir = 0;
|
|
|
|
/* Valid frame. */
|
|
FAIL_IF(DNP3ProbingParser(NULL, STREAM_TOSERVER, pkt, sizeof(pkt), &rdir) != ALPROTO_DNP3);
|
|
|
|
/* Send too little bytes. */
|
|
FAIL_IF(DNP3ProbingParser(NULL, STREAM_TOSERVER, pkt, sizeof(DNP3LinkHeader) - 1, &rdir) != ALPROTO_UNKNOWN);
|
|
|
|
/* Bad start bytes. */
|
|
pkt[0] = 0x06;
|
|
FAIL_IF(DNP3ProbingParser(NULL, STREAM_TOSERVER, pkt, sizeof(pkt), &rdir) != ALPROTO_FAILED);
|
|
|
|
/* Restore start byte. */
|
|
pkt[0] = 0x05;
|
|
|
|
/* Set the length to a value less than the minimum length of 5. */
|
|
pkt[2] = 0x03;
|
|
FAIL_IF(DNP3ProbingParser(NULL, STREAM_TOSERVER, pkt, sizeof(pkt), &rdir) != ALPROTO_FAILED);
|
|
|
|
/* Send a banner. */
|
|
char mybanner[] = "Welcome to DNP3 SCADA.";
|
|
FAIL_IF(DNP3ProbingParser(NULL, STREAM_TOSERVER, (uint8_t *)mybanner, sizeof(mybanner), &rdir) != ALPROTO_DNP3);
|
|
|
|
PASS;
|
|
}
|
|
|
|
/**
|
|
* \test Test a basic request/response.
|
|
*/
|
|
static int DNP3ParserTestRequestResponse(void)
|
|
{
|
|
DNP3State *state = NULL;
|
|
|
|
uint8_t request[] = {
|
|
/* DNP3 start. */
|
|
0x05, 0x64, 0x1a, 0xc4, 0x02, 0x00, 0x01, 0x00,
|
|
0xa5, 0xe9,
|
|
|
|
/* Transport header. */
|
|
0xff,
|
|
|
|
/* Application layer. */
|
|
0xc9, 0x05, 0x0c, 0x01, 0x28, 0x01, 0x00, 0x00,
|
|
0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x72,
|
|
0xef, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff
|
|
};
|
|
|
|
uint8_t response[] = {
|
|
/* DNP3 start. */
|
|
0x05, 0x64, 0x1c, 0x44, 0x01, 0x00, 0x02, 0x00,
|
|
0xe2, 0x59,
|
|
|
|
/* Transport header. */
|
|
0xc3,
|
|
|
|
/* Application layer. */
|
|
0xc9, 0x81, 0x00, 0x00, 0x0c, 0x01, 0x28, 0x01,
|
|
0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x7a,
|
|
0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0xff, 0xff
|
|
};
|
|
|
|
AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
Flow flow;
|
|
TcpSession ssn;
|
|
|
|
memset(&flow, 0, sizeof(flow));
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
flow.protoctx = (void *)&ssn;
|
|
flow.proto = IPPROTO_TCP;
|
|
flow.alproto = ALPROTO_DNP3;
|
|
|
|
StreamTcpInitConfig(TRUE);
|
|
|
|
SCMutexLock(&flow.m);
|
|
FAIL_IF(AppLayerParserParse(NULL, alp_tctx, &flow, ALPROTO_DNP3,
|
|
STREAM_TOSERVER, request, sizeof(request)));
|
|
SCMutexUnlock(&flow.m);
|
|
|
|
state = flow.alstate;
|
|
FAIL_IF(state == NULL);
|
|
|
|
DNP3Transaction *tx = DNP3GetTx(state, 0);
|
|
FAIL_IF(tx == NULL);
|
|
FAIL_IF(tx->tx_num != 1);
|
|
FAIL_IF(tx != state->curr);
|
|
FAIL_IF(tx->request_buffer == NULL);
|
|
FAIL_IF(tx->request_buffer_len != 20);
|
|
FAIL_IF(tx->request_ah.function_code != DNP3_APP_FC_DIR_OPERATE);
|
|
|
|
SCMutexLock(&flow.m);
|
|
FAIL_IF(AppLayerParserParse(NULL, alp_tctx, &flow, ALPROTO_DNP3,
|
|
STREAM_TOCLIENT, response, sizeof(response)));
|
|
SCMutexUnlock(&flow.m);
|
|
FAIL_IF(DNP3GetTx(state, 0) != tx);
|
|
FAIL_IF(!tx->response_done);
|
|
FAIL_IF(tx->response_buffer == NULL);
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
StreamTcpFreeConfig(TRUE);
|
|
FLOW_DESTROY(&flow);
|
|
PASS;
|
|
}
|
|
|
|
/**
|
|
* \test Test an unsolicited response from an outstation.
|
|
*
|
|
* This is kind of like a request initiated from the "server".
|
|
*/
|
|
static int DNP3ParserTestUnsolicitedResponseConfirm(void)
|
|
{
|
|
DNP3State *state = NULL;
|
|
|
|
/* Unsolicited response with confirm bit set. */
|
|
uint8_t response[] = {
|
|
0x05, 0x64, 0x16, 0x44, 0x01, 0x00, 0x02, 0x00,
|
|
0x89, 0xe5, 0xc4, 0xfa, 0x82, 0x00, 0x00, 0x02,
|
|
0x02, 0x17, 0x01, 0x01, 0x81, 0xa7, 0x75, 0xd8,
|
|
0x32, 0x4c, 0x81, 0x3e, 0x01, 0xa1, 0xc9
|
|
};
|
|
|
|
/* Confirm. */
|
|
uint8_t confirm[] = {
|
|
0x05, 0x64, 0x08, 0xc4, 0x02, 0x00,
|
|
0x01, 0x00, 0xd3, 0xb7, 0xc0, 0xda, 0x00, 0x6a,
|
|
0x3d
|
|
};
|
|
|
|
AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
Flow flow;
|
|
TcpSession ssn;
|
|
|
|
memset(&flow, 0, sizeof(flow));
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
flow.protoctx = (void *)&ssn;
|
|
flow.proto = IPPROTO_TCP;
|
|
flow.alproto = ALPROTO_DNP3;
|
|
|
|
StreamTcpInitConfig(TRUE);
|
|
|
|
SCMutexLock(&flow.m);
|
|
FAIL_IF(AppLayerParserParse(NULL, alp_tctx, &flow, ALPROTO_DNP3,
|
|
STREAM_TOCLIENT, response, sizeof(response)));
|
|
SCMutexUnlock(&flow.m);
|
|
|
|
state = flow.alstate;
|
|
FAIL_IF(state == NULL);
|
|
|
|
DNP3Transaction *tx = DNP3GetTx(state, 0);
|
|
FAIL_IF(tx == NULL);
|
|
FAIL_IF(tx->tx_num != 1);
|
|
FAIL_IF(tx != state->curr);
|
|
FAIL_IF(tx->request_buffer != NULL);
|
|
FAIL_IF(tx->response_buffer == NULL);
|
|
FAIL_IF(!tx->response_done);
|
|
FAIL_IF(tx->response_ah.function_code != DNP3_APP_FC_UNSOLICITED_RESP);
|
|
|
|
SCMutexLock(&flow.m);
|
|
FAIL_IF(AppLayerParserParse(NULL, alp_tctx, &flow, ALPROTO_DNP3,
|
|
STREAM_TOSERVER, confirm, sizeof(confirm)));
|
|
SCMutexUnlock(&flow.m);
|
|
FAIL_IF(DNP3GetTx(state, 0) != tx);
|
|
FAIL_IF(!tx->response_done);
|
|
FAIL_IF(tx->response_buffer == NULL);
|
|
/* FAIL_IF(tx->iin1 != 0 || tx->iin2 != 0); */
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
StreamTcpFreeConfig(TRUE);
|
|
FLOW_DESTROY(&flow);
|
|
PASS;
|
|
}
|
|
|
|
/**
|
|
* \test Test flood state.
|
|
*/
|
|
static int DNP3ParserTestFlooded(void)
|
|
{
|
|
DNP3State *state = NULL;
|
|
|
|
uint8_t request[] = {
|
|
/* DNP3 start. */
|
|
0x05, 0x64, 0x1a, 0xc4, 0x02, 0x00, 0x01, 0x00,
|
|
0xa5, 0xe9,
|
|
|
|
/* Transport header. */
|
|
0xff,
|
|
|
|
/* Application layer. */
|
|
0xc9, 0x05, 0x0c, 0x01, 0x28, 0x01, 0x00, 0x00,
|
|
0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x72,
|
|
0xef, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff
|
|
};
|
|
|
|
AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
Flow flow;
|
|
TcpSession ssn;
|
|
|
|
memset(&flow, 0, sizeof(flow));
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
flow.protoctx = (void *)&ssn;
|
|
flow.proto = IPPROTO_TCP;
|
|
flow.alproto = ALPROTO_DNP3;
|
|
|
|
StreamTcpInitConfig(TRUE);
|
|
|
|
SCMutexLock(&flow.m);
|
|
FAIL_IF(AppLayerParserParse(NULL, alp_tctx, &flow, ALPROTO_DNP3,
|
|
STREAM_TOSERVER, request, sizeof(request)));
|
|
SCMutexUnlock(&flow.m);
|
|
|
|
state = flow.alstate;
|
|
FAIL_IF(state == NULL);
|
|
|
|
DNP3Transaction *tx = DNP3GetTx(state, 0);
|
|
FAIL_IF(tx == NULL);
|
|
FAIL_IF(tx->tx_num != 1);
|
|
FAIL_IF(tx != state->curr);
|
|
FAIL_IF(tx->request_buffer == NULL);
|
|
FAIL_IF(tx->request_buffer_len != 20);
|
|
/* FAIL_IF(tx->app_function_code != DNP3_APP_FC_DIR_OPERATE); */
|
|
FAIL_IF(tx->response_done);
|
|
|
|
for (int i = 0; i < DNP3_DEFAULT_REQ_FLOOD_COUNT - 1; i++) {
|
|
SCMutexLock(&flow.m);
|
|
FAIL_IF(AppLayerParserParse(NULL, alp_tctx, &flow, ALPROTO_DNP3,
|
|
STREAM_TOSERVER, request, sizeof(request)));
|
|
SCMutexUnlock(&flow.m);
|
|
}
|
|
FAIL_IF(state->flooded);
|
|
FAIL_IF(DNP3GetAlstateProgress(tx, 0));
|
|
|
|
/* One more request should trip us into flooded state. */
|
|
SCMutexLock(&flow.m);
|
|
FAIL_IF(AppLayerParserParse(NULL, alp_tctx, &flow, ALPROTO_DNP3,
|
|
STREAM_TOSERVER, request, sizeof(request)));
|
|
SCMutexUnlock(&flow.m);
|
|
FAIL_IF(!state->flooded);
|
|
|
|
/* Progress for the oldest tx should return 1. */
|
|
FAIL_IF(!DNP3GetAlstateProgress(tx, 0));
|
|
|
|
/* But progress for the current state should still return 0. */
|
|
FAIL_IF(DNP3GetAlstateProgress(state->curr, 0));
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
StreamTcpFreeConfig(TRUE);
|
|
FLOW_DESTROY(&flow);
|
|
PASS;
|
|
}
|
|
|
|
/**
|
|
* \test Test parsing of partial frames.
|
|
*
|
|
* As DNP3 operates over TCP, it is possible that a partial DNP3 frame
|
|
* is received. Test that the partial frame will be buffered until the
|
|
* remainder is seen.
|
|
*/
|
|
static int DNP3ParserTestPartialFrame(void)
|
|
{
|
|
DNP3State *state = NULL;
|
|
DNP3Transaction *tx;
|
|
int r;
|
|
|
|
uint8_t request_partial1[] = {
|
|
/* DNP3 start. */
|
|
0x05, 0x64, 0x1a, 0xc4, 0x02, 0x00, 0x01, 0x00,
|
|
0xa5, 0xe9,
|
|
|
|
/* Transport header. */
|
|
0xff,
|
|
|
|
/* Application layer. */
|
|
0xc9, 0x05, 0x0c, 0x01, 0x28, 0x01, 0x00, 0x00,
|
|
};
|
|
|
|
uint8_t request_partial2[] = {
|
|
/* Remainder of application layer. */
|
|
0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x72,
|
|
0xef, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff
|
|
};
|
|
|
|
uint8_t response_partial1[] = {
|
|
/* DNP3 start. */
|
|
0x05, 0x64, 0x1c, 0x44, 0x01, 0x00, 0x02, 0x00,
|
|
0xe2, 0x59,
|
|
|
|
/* Transport header. */
|
|
0xc3,
|
|
|
|
/* Application layer. */
|
|
0xc9, 0x81, 0x00, 0x00, 0x0c, 0x01, 0x28, 0x01,
|
|
};
|
|
|
|
uint8_t response_partial2[] = {
|
|
0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x7a,
|
|
0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0xff, 0xff
|
|
};
|
|
|
|
/* Boiler plate for app layer setup. */
|
|
AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
Flow flow;
|
|
TcpSession ssn;
|
|
memset(&flow, 0, sizeof(flow));
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
flow.protoctx = (void *)&ssn;
|
|
flow.proto = IPPROTO_TCP;
|
|
flow.alproto = ALPROTO_DNP3;
|
|
StreamTcpInitConfig(TRUE);
|
|
|
|
/* Pass in the first partial frame. */
|
|
|
|
SCMutexLock(&flow.m);
|
|
r = AppLayerParserParse(NULL, alp_tctx, &flow, ALPROTO_DNP3,
|
|
STREAM_TOSERVER, request_partial1, sizeof(request_partial1));
|
|
SCMutexUnlock(&flow.m);
|
|
FAIL_IF(r != 0);
|
|
|
|
/* Frame should just be buffered, but not yet processed. */
|
|
state = flow.alstate;
|
|
FAIL_IF(state == NULL);
|
|
FAIL_IF(state->request_buffer.len != sizeof(request_partial1));
|
|
FAIL_IF(state->request_buffer.offset != 0);
|
|
FAIL_IF(memcmp(state->request_buffer.buffer, request_partial1,
|
|
sizeof(request_partial1)));
|
|
|
|
/* There should not be a transaction yet. */
|
|
FAIL_IF(state->transaction_max != 0);
|
|
FAIL_IF(DNP3GetTx(state, 0) != NULL);
|
|
|
|
/* Send the second partial. */
|
|
SCMutexLock(&flow.m);
|
|
r = AppLayerParserParse(NULL, alp_tctx, &flow, ALPROTO_DNP3,
|
|
STREAM_TOSERVER, request_partial2, sizeof(request_partial2));
|
|
SCMutexUnlock(&flow.m);
|
|
FAIL_IF(r != 0);
|
|
|
|
/* The second partial completed the frame, the buffer should now
|
|
* be clear. */
|
|
FAIL_IF(state->request_buffer.len != 0);
|
|
FAIL_IF(state->request_buffer.offset != 0);
|
|
|
|
/* Should now have a complete transaction. */
|
|
tx = DNP3GetTx(state, 0);
|
|
FAIL_IF(tx == NULL);
|
|
FAIL_IF(tx->tx_num != 1);
|
|
FAIL_IF(tx != state->curr);
|
|
FAIL_IF(tx->request_buffer == NULL);
|
|
FAIL_IF(tx->request_buffer_len != 20);
|
|
FAIL_IF(tx->request_ah.function_code != DNP3_APP_FC_DIR_OPERATE);
|
|
|
|
/* Send partial response. */
|
|
SCMutexLock(&flow.m);
|
|
r = AppLayerParserParse(NULL, alp_tctx, &flow, ALPROTO_DNP3,
|
|
STREAM_TOCLIENT, response_partial1, sizeof(response_partial1));
|
|
SCMutexUnlock(&flow.m);
|
|
FAIL_IF(r != 0);
|
|
FAIL_IF(state->response_buffer.len != sizeof(response_partial1));
|
|
FAIL_IF(state->response_buffer.offset != 0);
|
|
FAIL_IF(tx->response_done);
|
|
|
|
/* Send rest of response. */
|
|
SCMutexLock(&flow.m);
|
|
r = AppLayerParserParse(NULL, alp_tctx, &flow, ALPROTO_DNP3,
|
|
STREAM_TOCLIENT, response_partial2, sizeof(response_partial2));
|
|
SCMutexUnlock(&flow.m);
|
|
FAIL_IF(r != 0);
|
|
|
|
/* Buffer should now be empty. */
|
|
FAIL_IF(state->response_buffer.len != 0);
|
|
FAIL_IF(state->response_buffer.offset != 0);
|
|
|
|
/* Transaction should be replied to now. */
|
|
FAIL_IF(!tx->response_done);
|
|
FAIL_IF(tx->response_buffer == NULL);
|
|
FAIL_IF(tx->response_buffer_len == 0);
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
StreamTcpFreeConfig(TRUE);
|
|
FLOW_DESTROY(&flow);
|
|
PASS;
|
|
}
|
|
|
|
/**
|
|
* \test Test multiple DNP3 frames in one TCP read.
|
|
*/
|
|
static int DNP3ParserTestMultiFrame(void)
|
|
{
|
|
DNP3State *state = NULL;
|
|
|
|
/* Unsolicited response 1. */
|
|
uint8_t unsol_response1[] = {
|
|
0x05, 0x64, 0x16, 0x44, 0x01, 0x00, 0x02, 0x00,
|
|
0x89, 0xe5, 0xc4, 0xfa, 0x82, 0x00, 0x00, 0x02,
|
|
0x02, 0x17, 0x01, 0x01, 0x81, 0xa7, 0x75, 0xd8,
|
|
0x32, 0x4c, 0x81, 0x3e, 0x01, 0xa1, 0xc9,
|
|
};
|
|
|
|
/* Unsolicited response 2. */
|
|
uint8_t unsol_response2[] = {
|
|
0x05, 0x64, 0x16, 0x44, 0x01, 0x00, 0x02, 0x00,
|
|
0x89, 0xe5, 0xc5, 0xfb, 0x82, 0x00, 0x00, 0x02,
|
|
0x02, 0x17, 0x01, 0x0c, 0x01, 0xd8, 0x75, 0xd8,
|
|
0x32, 0x4c, 0xc9, 0x3c, 0x01, 0xa1, 0xc9,
|
|
};
|
|
|
|
uint8_t combined[sizeof(unsol_response1) + sizeof(unsol_response2)];
|
|
memcpy(combined, unsol_response1, sizeof(unsol_response1));
|
|
memcpy(combined + sizeof(unsol_response1), unsol_response2,
|
|
sizeof(unsol_response2));
|
|
|
|
/* Setup. */
|
|
AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
Flow flow;
|
|
TcpSession ssn;
|
|
int r;
|
|
memset(&flow, 0, sizeof(flow));
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
flow.protoctx = (void *)&ssn;
|
|
flow.proto = IPPROTO_TCP;
|
|
flow.alproto = ALPROTO_DNP3;
|
|
StreamTcpInitConfig(TRUE);
|
|
|
|
SCMutexLock(&flow.m);
|
|
r = AppLayerParserParse(NULL, alp_tctx, &flow, ALPROTO_DNP3,
|
|
STREAM_TOCLIENT, combined, sizeof(combined));
|
|
SCMutexUnlock(&flow.m);
|
|
FAIL_IF(r != 0);
|
|
|
|
state = flow.alstate;
|
|
FAIL_IF(state == NULL);
|
|
FAIL_IF(state->transaction_max != 2);
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
StreamTcpFreeConfig(TRUE);
|
|
FLOW_DESTROY(&flow);
|
|
PASS;
|
|
}
|
|
|
|
/**
|
|
* \test Test the parsing of a request PDU.
|
|
*
|
|
* The PDU under test contains a single read request object:
|
|
* - Group: 1
|
|
* - Variation: 0
|
|
* - Count: 0
|
|
*/
|
|
static int DNP3ParserTestParsePDU01(void)
|
|
{
|
|
/* Frame to be tested. This frame is a DNP3 request with one read
|
|
* request data object, group 1, variation 0. */
|
|
const uint8_t pkt[] = {
|
|
0x05, 0x64,
|
|
0x0b, 0xc4, 0x17, 0x00, 0xef, 0xff, 0xc4, 0x8f,
|
|
0xe1, 0xc8, 0x01, 0x01, 0x00, 0x06, 0x77, 0x6e
|
|
};
|
|
|
|
DNP3State *dnp3state = DNP3StateAlloc();
|
|
int pdus = DNP3HandleRequestLinkLayer(dnp3state, pkt, sizeof(pkt));
|
|
FAIL_IF(pdus < 1);
|
|
DNP3Transaction *dnp3tx = DNP3GetTx(dnp3state, 0);
|
|
FAIL_IF_NULL(dnp3tx);
|
|
FAIL_IF(!dnp3tx->has_request);
|
|
FAIL_IF(TAILQ_EMPTY(&dnp3tx->request_objects));
|
|
DNP3Object *object = TAILQ_FIRST(&dnp3tx->request_objects);
|
|
FAIL_IF(object->group != 1 || object->variation != 0);
|
|
FAIL_IF(object->count != 0);
|
|
|
|
DNP3StateFree(dnp3state);
|
|
PASS;
|
|
}
|
|
|
|
/**
|
|
* \test Test the decode of a DNP3 fragment with a single 70:3 object.
|
|
*/
|
|
static int DNP3ParserDecodeG70V3Test(void)
|
|
{
|
|
const uint8_t pkt[] = {
|
|
0x05, 0x64,
|
|
0x63, 0xc4, 0x04, 0x00, 0x03, 0x00, 0xc7, 0xee,
|
|
0xc7, 0xc9, 0x1b, 0x46, 0x03, 0x5b, 0x01, 0x55,
|
|
0x00, 0x1a, 0x00, 0x3b, 0x00, 0x00, 0x00, 0x00,
|
|
0x9e, 0xc7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0xff, 0xff, 0x00, 0x1e, 0x00, 0x43,
|
|
0x3a, 0x2f, 0x74, 0x65, 0x6d, 0x70, 0x2f, 0x44,
|
|
0x4e, 0x50, 0x44, 0x65, 0x67, 0x7d, 0x76, 0x69,
|
|
0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
|
|
0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x93, 0x0c,
|
|
0x6e, 0x20, 0x77, 0x72, 0x69, 0x74, 0x74, 0x65,
|
|
0x6e, 0x20, 0x74, 0x6f, 0x20, 0x52, 0x65, 0x6d,
|
|
0x35, 0x20, 0x6f, 0x74, 0x65, 0x20, 0x44, 0x65,
|
|
0x76, 0x69, 0x63, 0x65, 0x2e, 0x78, 0x6d, 0x6c,
|
|
0xc4, 0x8b
|
|
};
|
|
|
|
DNP3State *dnp3state = DNP3StateAlloc();
|
|
FAIL_IF_NULL(dnp3state);
|
|
int bytes = DNP3HandleRequestLinkLayer(dnp3state, pkt, sizeof(pkt));
|
|
FAIL_IF(bytes != sizeof(pkt));
|
|
DNP3Transaction *tx = DNP3GetTx(dnp3state, 0);
|
|
FAIL_IF_NULL(tx);
|
|
FAIL_IF_NOT(tx->has_request);
|
|
DNP3Object *obj = TAILQ_FIRST(&tx->request_objects);
|
|
FAIL_IF_NULL(obj);
|
|
FAIL_IF_NOT(obj->group == 70);
|
|
FAIL_IF_NOT(obj->variation == 3);
|
|
FAIL_IF_NOT(obj->prefix_code == 0x5);
|
|
FAIL_IF_NOT(obj->range_code == 0xb);
|
|
FAIL_IF_NOT(obj->count == 1);
|
|
DNP3Point *point = TAILQ_FIRST(obj->points);
|
|
FAIL_IF_NULL(point);
|
|
FAIL_IF_NOT(point->prefix == 85);
|
|
FAIL_IF_NOT(point->size == 85);
|
|
FAIL_IF_NULL(point->data);
|
|
DNP3ObjectG70V3 *data = point->data;
|
|
FAIL_IF_NOT(strcmp(
|
|
data->filename,
|
|
"C:/temp/DNPDeviceConfiguration written to Remote Device.xml") == 0);
|
|
DNP3StateFree(dnp3state);
|
|
PASS;
|
|
}
|
|
|
|
/**
|
|
* \brief Test that an alert is raised on an unknown object.
|
|
*/
|
|
static int DNP3ParserUnknownEventAlertTest(void)
|
|
{
|
|
/* Valid DNP3 frame with 70:3 object. */
|
|
uint8_t pkt[] = {
|
|
0x05, 0x64, 0x63, 0xc4, 0x04, 0x00, 0x03, 0x00,
|
|
0xc7, 0xee,
|
|
|
|
0xc7, 0xc9, 0x1b,
|
|
|
|
/* Object and variation. Originally 70:3, now 70:99, an
|
|
* unknown object. */
|
|
0x46, 0x63,
|
|
|
|
0x5b, 0x01, 0x55,
|
|
0x00, 0x1a, 0x00, 0x3b, 0x00, 0x00, 0x00, 0x00,
|
|
0x9e, 0xc7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0xff, 0xff, 0x00, 0x1e, 0x00, 0x43,
|
|
0x3a, 0x2f, 0x74, 0x65, 0x6d, 0x70, 0x2f, 0x44,
|
|
0x4e, 0x50, 0x44, 0x65, 0x67, 0x7d, 0x76, 0x69,
|
|
0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
|
|
0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x93, 0x0c,
|
|
0x6e, 0x20, 0x77, 0x72, 0x69, 0x74, 0x74, 0x65,
|
|
0x6e, 0x20, 0x74, 0x6f, 0x20, 0x52, 0x65, 0x6d,
|
|
0x35, 0x20, 0x6f, 0x74, 0x65, 0x20, 0x44, 0x65,
|
|
0x76, 0x69, 0x63, 0x65, 0x2e, 0x78, 0x6d, 0x6c,
|
|
0xc4, 0x8b
|
|
};
|
|
|
|
DNP3FixCrc(pkt + 10, sizeof(pkt) - 10);
|
|
|
|
DNP3State *dnp3state = DNP3StateAlloc();
|
|
FAIL_IF_NULL(dnp3state);
|
|
int bytes = DNP3HandleRequestLinkLayer(dnp3state, pkt, sizeof(pkt));
|
|
FAIL_IF(bytes != sizeof(pkt));
|
|
|
|
DNP3StateFree(dnp3state);
|
|
PASS;
|
|
}
|
|
|
|
#endif
|
|
|
|
void DNP3ParserRegisterTests(void)
|
|
{
|
|
#ifdef UNITTESTS
|
|
UtRegisterTest("DNP3ParserTestCheckCRC", DNP3ParserTestCheckCRC);
|
|
UtRegisterTest("DNP3ParserCheckLinkHeaderCRC",
|
|
DNP3ParserCheckLinkHeaderCRC);
|
|
UtRegisterTest("DNP3CheckUserDataCRCsTest", DNP3CheckUserDataCRCsTest);
|
|
UtRegisterTest("DNP3CalculateLinkLengthTest", DNP3CalculateLinkLengthTest);
|
|
UtRegisterTest("DNP3CalculateTransportLengthWithoutCRCsTest",
|
|
DNP3CalculateTransportLengthWithoutCRCsTest);
|
|
UtRegisterTest("DNP3ReassembleApplicationLayerTest01",
|
|
DNP3ReassembleApplicationLayerTest01);
|
|
UtRegisterTest("DNP3ProbingParserTest", DNP3ProbingParserTest);
|
|
UtRegisterTest("DNP3ParserTestRequestResponse",
|
|
DNP3ParserTestRequestResponse);
|
|
UtRegisterTest("DNP3ParserTestUnsolicitedResponseConfirm",
|
|
DNP3ParserTestUnsolicitedResponseConfirm);
|
|
UtRegisterTest("DNP3ParserTestPartialFrame", DNP3ParserTestPartialFrame);
|
|
UtRegisterTest("DNP3ParserTestMultiFrame", DNP3ParserTestMultiFrame);
|
|
UtRegisterTest("DNP3ParserTestFlooded", DNP3ParserTestFlooded);
|
|
UtRegisterTest("DNP3ParserTestParsePDU01", DNP3ParserTestParsePDU01);
|
|
UtRegisterTest("DNP3ParserDecodeG70V3Test", DNP3ParserDecodeG70V3Test);
|
|
UtRegisterTest("DNP3ParserUnknownEventAlertTest",
|
|
DNP3ParserUnknownEventAlertTest);
|
|
#endif
|
|
}
|