diff options
Diffstat (limited to 'src/app-layer-dnp3.c')
-rw-r--r-- | src/app-layer-dnp3.c | 2655 |
1 files changed, 2655 insertions, 0 deletions
diff --git a/src/app-layer-dnp3.c b/src/app-layer-dnp3.c new file mode 100644 index 0000000..9501b9f --- /dev/null +++ b/src/app-layer-dnp3.c @@ -0,0 +1,2655 @@ +/* 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. + */ + +/** + * \file + * + * DNP3 protocol implementation + */ + +#include "suricata-common.h" +#include "suricata.h" +#include "stream.h" +#include "util-byte.h" +#include "util-unittest.h" +#include "util-hashlist.h" + +#include "util-print.h" +#include "util-spm-bs.h" +#include "util-enum.h" + +#include "app-layer.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" + +/* 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}, +}; + +/* 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) +{ +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + return 1; +#endif + 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; +} + +/* Some DNP3 servers start with a banner. */ +#define DNP3_BANNER "DNP3" + +/** + * \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 *)DNP3_BANNER, strlen(DNP3_BANNER)) != NULL; +} + +/** + * \brief DNP3 probing parser. + */ +static uint16_t DNP3ProbingParser(Flow *f, uint8_t direction, + const uint8_t *input, uint32_t len, + uint8_t *rdir) +{ + const DNP3LinkHeader *const hdr = (const DNP3LinkHeader *)input; + const bool toserver = (direction & STREAM_TOSERVER) != 0; + + /* May be a banner. */ + if (DNP3ContainsBanner(input, len)) { + SCLogDebug("Packet contains a DNP3 banner."); + bool is_banner = true; + // magic 0x100 = 256 seems good enough + for (uint32_t i = 0; i < len && i < 0x100; i++) { + if (!isprint(input[i])) { + is_banner = false; + break; + } + } + if (is_banner) { + if (toserver) { + *rdir = STREAM_TOCLIENT; + } + return ALPROTO_DNP3; + } + } + + /* 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; + } + + /* 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; + } + + // Test compatibility between direction and dnp3.ctl.direction + if ((DNP3_LINK_DIR(hdr->control) != 0) != toserver) { + *rdir = toserver ? STREAM_TOCLIENT : STREAM_TOSERVER; + } + SCLogDebug("Detected DNP3."); + return ALPROTO_DNP3; +} + +/** + * \brief Calculate 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 *orig_state, AppProto proto_orig) +{ + 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->tx_data.events, event); + dnp3->events++; + } + else { + SCLogWarning("Failed to set event, state or tx pointer was NULL."); + } +} + +/** + * \brief Set a DNP3 application layer event on a transaction. + */ +static void DNP3SetEventTx(DNP3Transaction *tx, uint8_t event) +{ + AppLayerDecoderEventsSetEventRaw(&tx->tx_data.events, event); + tx->dnp3->events++; +} + +/** + * \brief Allocation a DNP3 transaction. + */ +static DNP3Transaction *DNP3TxAlloc(DNP3State *dnp3, bool request) +{ + 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; + tx->is_request = request; + if (tx->is_request) { + tx->tx_data.detect_flags_tc |= APP_LAYER_TX_SKIP_INSPECT_FLAG; + } else { + tx->tx_data.detect_flags_ts |= APP_LAYER_TX_SKIP_INSPECT_FLAG; + } + TAILQ_INIT(&tx->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, uint8_t direction) +{ + if (direction == STREAM_TOSERVER) { + 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); +} + +/** + * \brief 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->lh.src == lh->src && ttx->lh.dst == lh->dst && ttx->is_request && !ttx->done && + NEXT_TH_SEQNO(DNP3_TH_SEQ(ttx->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->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, true); + if (unlikely(tx == NULL)) { + return; + } + tx->lh = *lh; + tx->th = th; + tx->ah = *ah; + } + + if (!DNP3ReassembleApplicationLayer(input + sizeof(DNP3LinkHeader), + input_len - sizeof(DNP3LinkHeader), &tx->buffer, &tx->buffer_len)) { + + /* Malformed, set event and mark as done. */ + DNP3SetEvent(dnp3, DNP3_DECODER_EVENT_MALFORMED); + tx->done = 1; + return; + } + + /* If this is not the final segment, just return. */ + if (!DNP3_TH_FIN(th)) { + return; + } + + tx->done = 1; + + if (DNP3DecodeApplicationObjects(tx, tx->buffer + sizeof(DNP3ApplicationHeader), + tx->buffer_len - sizeof(DNP3ApplicationHeader), &tx->objects)) { + tx->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->lh.src == lh->src && ttx->lh.dst == lh->dst && !ttx->is_request && + !ttx->done && NEXT_TH_SEQNO(DNP3_TH_SEQ(ttx->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->th = th; + } + else { + ah = (DNP3ApplicationHeader *)(input + offset); + offset += sizeof(DNP3ApplicationHeader); + iin = (DNP3InternalInd *)(input + offset); + + tx = DNP3TxAlloc(dnp3, false); + if (unlikely(tx == NULL)) { + return; + } + tx->lh = *lh; + tx->th = th; + tx->ah = *ah; + tx->iin = *iin; + } + + BUG_ON(tx->is_request); + + if (!DNP3ReassembleApplicationLayer(input + sizeof(DNP3LinkHeader), + input_len - sizeof(DNP3LinkHeader), &tx->buffer, &tx->buffer_len)) { + DNP3SetEvent(dnp3, DNP3_DECODER_EVENT_MALFORMED); + return; + } + + if (!DNP3_TH_FIN(th)) { + return; + } + + tx->done = 1; + + offset = sizeof(DNP3ApplicationHeader) + sizeof(DNP3InternalInd); + if (DNP3DecodeApplicationObjects( + tx, tx->buffer + offset, tx->buffer_len - offset, &tx->objects)) { + tx->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, STREAM_TOSERVER)) { + 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 AppLayerResult DNP3ParseRequest(Flow *f, void *state, AppLayerParserState *pstate, + StreamSlice stream_slice, void *local_data) +{ + SCEnter(); + DNP3State *dnp3 = (DNP3State *)state; + DNP3Buffer *buffer = &dnp3->request_buffer; + int processed = 0; + + const uint8_t *input = StreamSliceGetData(&stream_slice); + uint32_t input_len = StreamSliceGetDataLen(&stream_slice); + + if (input_len == 0) { + SCReturnStruct(APP_LAYER_OK); + } + + if (buffer->len) { + if (!DNP3BufferAdd(buffer, input, input_len)) { + 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)) { + goto error; + } + } + } + + SCReturnStruct(APP_LAYER_OK); + +error: + /* Reset the buffer. */ + DNP3BufferReset(buffer); + SCReturnStruct(APP_LAYER_ERROR); +} + +/** + * \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, STREAM_TOCLIENT)) { + 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 AppLayerResult DNP3ParseResponse(Flow *f, void *state, AppLayerParserState *pstate, + StreamSlice stream_slice, void *local_data) +{ + SCEnter(); + + DNP3State *dnp3 = (DNP3State *)state; + DNP3Buffer *buffer = &dnp3->response_buffer; + int processed; + + const uint8_t *input = StreamSliceGetData(&stream_slice); + uint32_t input_len = StreamSliceGetDataLen(&stream_slice); + + if (buffer->len) { + if (!DNP3BufferAdd(buffer, input, input_len)) { + 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)) { + goto error; + } + } + } + +done: + SCReturnStruct(APP_LAYER_OK); + +error: + /* An error occurred while processing DNP3 frames. Dump the + * buffer as we can't be assured that they are valid anymore. */ + DNP3BufferReset(buffer); + SCReturnStruct(APP_LAYER_ERROR); +} + +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->buffer != NULL) { + SCFree(tx->buffer); + } + + AppLayerDecoderEventsFreeEvents(&tx->tx_data.events); + + if (tx->tx_data.de_state != NULL) { + DetectEngineStateFree(tx->tx_data.de_state); + } + + DNP3TxFreeObjectList(&tx->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->tx_data.events != NULL) { + if (tx->tx_data.events->cnt <= dnp3->events) { + dnp3->events -= tx->tx_data.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 (dnp3tx->complete) + retval = 1; + + SCReturnInt(retval); +} + +/** + * \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("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 int DNP3StateGetEventInfoById(int event_id, const char **event_name, + AppLayerEventType *event_type) +{ + *event_name = SCMapEnumValueToName(event_id, dnp3_decoder_event_table); + if (*event_name == NULL) { + SCLogError("Event \"%d\" not present in " + "the DNP3 enum event map table.", + event_id); + return -1; + } + + *event_type = APP_LAYER_EVENT_TYPE_TRANSACTION; + + return 0; +} + +static AppLayerTxData *DNP3GetTxData(void *vtx) +{ + DNP3Transaction *tx = (DNP3Transaction *)vtx; + return &tx->tx_data; +} + +static AppLayerStateData *DNP3GetStateData(void *vstate) +{ + DNP3State *state = (DNP3State *)vstate; + return &state->state_data; +} + +/** + * \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 AppLayerGetTxIterTuple DNP3GetTxIterator(const uint8_t ipproto, const AppProto alproto, + void *alstate, uint64_t min_tx_id, uint64_t max_tx_id, AppLayerGetTxIterState *state) +{ + DNP3State *dnp_state = (DNP3State *)alstate; + AppLayerGetTxIterTuple no_tuple = { NULL, 0, false }; + if (dnp_state) { + DNP3Transaction *tx_ptr; + if (state->un.ptr == NULL) { + tx_ptr = TAILQ_FIRST(&dnp_state->tx_list); + } else { + tx_ptr = (DNP3Transaction *)state->un.ptr; + } + if (tx_ptr) { + while (tx_ptr->tx_num < min_tx_id + 1) { + tx_ptr = TAILQ_NEXT(tx_ptr, next); + if (!tx_ptr) { + return no_tuple; + } + } + if (tx_ptr->tx_num >= max_tx_id + 1) { + return no_tuple; + } + state->un.ptr = TAILQ_NEXT(tx_ptr, next); + AppLayerGetTxIterTuple tuple = { + .tx_ptr = tx_ptr, + .tx_id = tx_ptr->tx_num - 1, + .has_next = (state->un.ptr != NULL), + }; + return tuple; + } + } + return no_tuple; +} + +/** + * \brief Register the DNP3 application protocol parser. + */ +void RegisterDNP3Parsers(void) +{ + SCEnter(); + + const char *proto_name = "dnp3"; + + if (AppLayerProtoDetectConfProtoDetectionEnabledDefault("tcp", proto_name, false)) { + AppLayerProtoDetectRegisterProtocol(ALPROTO_DNP3, proto_name); + + if (RunmodeIsUnittests()) { + AppLayerProtoDetectPPRegister(IPPROTO_TCP, DNP3_DEFAULT_PORT, + ALPROTO_DNP3, 0, sizeof(DNP3LinkHeader), STREAM_TOSERVER, + DNP3ProbingParser, DNP3ProbingParser); + } + else { + if (!AppLayerProtoDetectPPParseConfPorts("tcp", IPPROTO_TCP, + proto_name, ALPROTO_DNP3, 0, sizeof(DNP3LinkHeader), + DNP3ProbingParser, DNP3ProbingParser)) { + return; + } + } + + } 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); + + AppLayerParserRegisterGetTx(IPPROTO_TCP, ALPROTO_DNP3, DNP3GetTx); + AppLayerParserRegisterGetTxIterator(IPPROTO_TCP, ALPROTO_DNP3, DNP3GetTxIterator); + AppLayerParserRegisterGetTxCnt(IPPROTO_TCP, ALPROTO_DNP3, DNP3GetTxCnt); + AppLayerParserRegisterTxFreeFunc(IPPROTO_TCP, ALPROTO_DNP3, + DNP3StateTxFree); + + AppLayerParserRegisterGetStateProgressFunc(IPPROTO_TCP, ALPROTO_DNP3, + DNP3GetAlstateProgress); + AppLayerParserRegisterStateProgressCompletionStatus(ALPROTO_DNP3, 1, 1); + + AppLayerParserRegisterGetEventInfo(IPPROTO_TCP, ALPROTO_DNP3, + DNP3StateGetEventInfo); + AppLayerParserRegisterGetEventInfoById(IPPROTO_TCP, ALPROTO_DNP3, + DNP3StateGetEventInfoById); + + AppLayerParserRegisterTxDataFunc(IPPROTO_TCP, ALPROTO_DNP3, + DNP3GetTxData); + AppLayerParserRegisterStateDataFunc(IPPROTO_TCP, ALPROTO_DNP3, DNP3GetStateData); + } + 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)); + +#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + /* 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)); +#endif + + 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))); + +#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + /* 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))); +#endif + + /* 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)); + +#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + /* Alter a byte in the header. */ + request[4] = 0; + FAIL_IF(DNP3CheckLinkHeaderCRC(header)); +#endif + + 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) - 1, + &rdir) != ALPROTO_DNP3); + FAIL_IF(rdir != STREAM_TOCLIENT); + + 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->buffer == NULL); + FAIL_IF(tx->buffer_len != 20); + FAIL_IF(tx->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); + DNP3Transaction *tx0 = DNP3GetTx(state, 1); + FAIL_IF(tx0 == NULL); + FAIL_IF(tx0 == tx); + FAIL_IF(!tx0->done); + FAIL_IF(tx0->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->done); + FAIL_IF(tx->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); + + /* Confirms are ignored currently. With the move to + unidirectional transactions it might be easy to support these + now. */ + DNP3Transaction *resptx = DNP3GetTx(state, 1); + FAIL_IF(resptx); + + AppLayerParserThreadCtxFree(alp_tctx); + StreamTcpFreeConfig(true); + FLOW_DESTROY(&flow); + PASS; +} + +/** + * \test Test flood state. + * + * Note that flood state needs to revisited with the modification to a + * unidirectional protocol. + */ +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->buffer == NULL); + FAIL_IF(tx->buffer_len != 20); + FAIL_IF(tx->ah.function_code != DNP3_APP_FC_DIR_OPERATE); + FAIL_IF_NOT(tx->done); + FAIL_IF_NOT(DNP3GetAlstateProgress(tx, STREAM_TOSERVER)); + + 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_NOT(DNP3GetAlstateProgress(tx, STREAM_TOSERVER)); + + /* 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)); + + 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->buffer == NULL); + FAIL_IF(tx->buffer_len != 20); + FAIL_IF(tx->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); + tx = DNP3GetTx(state, 1); + FAIL_IF_NOT_NULL(tx); + + /* 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); + + /* There should now be a response transaction. */ + tx = DNP3GetTx(state, 1); + FAIL_IF_NULL(tx); + FAIL_IF(tx->buffer == NULL); + FAIL_IF(tx->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(NULL, ALPROTO_UNKNOWN); + int pdus = DNP3HandleRequestLinkLayer(dnp3state, pkt, sizeof(pkt)); + FAIL_IF(pdus < 1); + DNP3Transaction *dnp3tx = DNP3GetTx(dnp3state, 0); + FAIL_IF_NULL(dnp3tx); + FAIL_IF(!dnp3tx->is_request); + FAIL_IF(TAILQ_EMPTY(&dnp3tx->objects)); + DNP3Object *object = TAILQ_FIRST(&dnp3tx->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(NULL, ALPROTO_UNKNOWN); + 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->is_request); + DNP3Object *obj = TAILQ_FIRST(&tx->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(NULL, ALPROTO_UNKNOWN); + FAIL_IF_NULL(dnp3state); + int bytes = DNP3HandleRequestLinkLayer(dnp3state, pkt, sizeof(pkt)); + FAIL_IF(bytes != sizeof(pkt)); + + DNP3StateFree(dnp3state); + PASS; +} + +/** +* \brief Test that an alert is raised on incorrect data. +*/ +static int DNP3ParserIncorrectUserData(void) +{ + uint8_t packet_bytes[] = { + 0x05, 0x64, 0x08, 0xc4, 0x03, 0x00, 0x04, 0x00, + 0xbf, 0xe9, 0xc1, 0xc1, 0x82, 0xc5, 0xee + }; + + 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); + + int r = AppLayerParserParse(NULL, alp_tctx, &flow, ALPROTO_DNP3, + STREAM_TOCLIENT, packet_bytes, sizeof(packet_bytes)); + + FAIL_IF(r == 0); + + AppLayerParserThreadCtxFree(alp_tctx); + StreamTcpFreeConfig(true); + FLOW_DESTROY(&flow); + 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); + UtRegisterTest("DNP3ParserIncorrectUserData", DNP3ParserIncorrectUserData); +#endif +} |