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.
suricata/scripts/dnp3-gen/dnp3-gen.py

741 lines
22 KiB
Python

#! /usr/bin/env python
#
# 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.
# This script generates DNP3 related source code based on definitions
# of DNP3 objects (currently the object structs).
from __future__ import print_function
import sys
import re
import yaml
import jinja2
IN_PLACE_START = "/* START GENERATED CODE */"
IN_PLACE_END = "/* END GENERATED CODE */"
util_lua_dnp3_objects_c_template = """/* 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.
*/
/**
* DO NOT EDIT. THIS FILE IS AUTO-GENERATED.
*
* Generated by command:
* {{command_line}}
*/
#include "suricata-common.h"
#include "app-layer-dnp3.h"
#include "app-layer-dnp3-objects.h"
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
#include "util-lua.h"
#include "util-lua-dnp3-objects.h"
/**
* \\brief Push an object point item onto the stack.
*/
void DNP3PushPoint(lua_State *luastate, DNP3Object *object,
DNP3Point *point)
{
switch (DNP3_OBJECT_CODE(object->group, object->variation)) {
{% for object in objects %}
case DNP3_OBJECT_CODE({{object.group}}, {{object.variation}}): {
DNP3ObjectG{{object.group}}V{{object.variation}} *data = point->data;
{% for field in object.fields %}
{% if is_integer_type(field.type) %}
lua_pushliteral(luastate, "{{field.name}}");
lua_pushinteger(luastate, data->{{field.name}});
lua_settable(luastate, -3);
{% elif field["type"] in ["flt32", "flt64"] %}
lua_pushliteral(luastate, "{{field.name}}");
lua_pushnumber(luastate, data->{{field.name}});
lua_settable(luastate, -3);
{% elif field["type"] == "chararray" %}
lua_pushliteral(luastate, "{{field.name}}");
LuaPushStringBuffer(luastate, (uint8_t *)data->{{field.name}},
strlen(data->{{field.name}}));
lua_settable(luastate, -3);
{% elif field["type"] == "vstr4" %}
lua_pushliteral(luastate, "{{field.name}}");
LuaPushStringBuffer(luastate, (uint8_t *)data->{{field.name}},
strlen(data->{{field.name}}));
lua_settable(luastate, -3);
{% elif field.type == "bytearray" %}
lua_pushliteral(luastate, "{{field.name}}");
lua_pushlstring(luastate, (const char *)data->{{field.name}},
data->{{field.len_field}});
lua_settable(luastate, -3);
{% elif field.type == "bstr8" %}
{% for field in field.fields %}
lua_pushliteral(luastate, "{{field.name}}");
lua_pushinteger(luastate, data->{{field.name}});
lua_settable(luastate, -3);
{% endfor %}
{% else %}
{{ raise("Unhandled datatype: %s" % (field.type)) }}
{% endif %}
{% endfor %}
break;
}
{% endfor %}
default:
break;
}
}
"""
output_json_dnp3_objects_template = """/* 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.
*/
/**
* DO NOT EDIT. THIS FILE IS AUTO-GENERATED.
*
* Generated by command:
* {{command_line}}
*/
#include "suricata-common.h"
#include "app-layer-dnp3.h"
#include "app-layer-dnp3-objects.h"
#include "output-json-dnp3-objects.h"
#include "output-json.h"
// clang-format off
void OutputJsonDNP3SetItem(JsonBuilder *js, DNP3Object *object,
DNP3Point *point)
{
switch (DNP3_OBJECT_CODE(object->group, object->variation)) {
{% for object in objects %}
case DNP3_OBJECT_CODE({{object.group}}, {{object.variation}}): {
DNP3ObjectG{{object.group}}V{{object.variation}} *data = point->data;
{% for field in object.fields %}
{% if is_integer_type(field.type) %}
jb_set_uint(js, "{{field.name}}", data->{{field.name}});
{% elif field.type in ["flt32", "flt64"] %}
jb_set_float(js, "{{field.name}}", data->{{field.name}});
{% elif field.type == "bytearray" %}
jb_set_base64(js, "data->{{field.name}}", data->{{field.name}}, data->{{field.len_field}});
{% elif field.type == "vstr4" %}
jb_set_string(js, "data->{{field.name}}", data->{{field.name}});
{% elif field.type == "chararray" %}
if (data->{{field.len_field}} > 0) {
jb_set_string_from_bytes(
js, "{{field.name}}", (const uint8_t *)data->{{field.name}}, data->{{field.len_field}});
} else {
jb_set_string(js, "{{field.name}}", "");
}
{% elif field.type == "bstr8" %}
{% for field in field.fields %}
jb_set_uint(js, "{{field.name}}", data->{{field.name}});
{% endfor %}
{% else %}
{{ raise("Unhandled datatype: %s" % (field.type)) }}
{% endif %}
{% endfor %}
break;
}
{% endfor %}
default:
SCLogDebug("Unknown object: %d:%d", object->group,
object->variation);
break;
}
}
// clang-format on
"""
def has_freeable_types(fields):
freeable_types = [
"bytearray",
]
for field in fields:
if field["type"] in freeable_types:
return True
return False
def is_integer_type(datatype):
integer_types = [
"uint64",
"uint32",
"uint24",
"uint16",
"uint8",
"int64",
"int32",
"int16",
"int8",
"dnp3time",
]
return datatype in integer_types
def to_type(datatype):
type_map = {
"uint8": "uint8_t",
}
if datatype in type_map:
return type_map[datatype]
else:
raise Exception("Unknown datatype: %s" % (datatype))
def generate(template, filename, context):
print("Generating %s." % (filename))
try:
env = jinja2.Environment(trim_blocks=True)
output = env.from_string(template).render(context)
with open(filename, "w") as fileobj:
fileobj.write(output)
except Exception as err:
print("Failed to generate %s: %s" % (filename, err))
sys.exit(1)
def raise_helper(msg):
raise Exception(msg)
def gen_object_structs(context):
""" Generate structs for all the define DNP3 objects. """
template = """
/* Code generated by:
* {{command_line}}
*/
{% for object in objects %}
typedef struct DNP3ObjectG{{object.group}}V{{object.variation}}_ {
{% for field in object.fields %}
{% if field.type == "bstr8" %}
{% for field in field.fields %}
uint8_t {{field.name}}:{{field.width}};
{% endfor %}
{% else %}
{% if field.type == "int16" %}
int16_t {{field.name}};
{% elif field.type == "int32" %}
int32_t {{field.name}};
{% elif field.type == "uint8" %}
uint8_t {{field.name}};
{% elif field.type == "uint16" %}
uint16_t {{field.name}};
{% elif field.type == "uint24" %}
uint32_t {{field.name}};
{% elif field.type == "uint32" %}
uint32_t {{field.name}};
{% elif field.type == "uint64" %}
uint64_t {{field.name}};
{% elif field.type == "flt32" %}
float {{field.name}};
{% elif field.type == "flt64" %}
double {{field.name}};
{% elif field.type == "dnp3time" %}
uint64_t {{field.name}};
{% elif field.type == "bytearray" %}
uint8_t *{{field.name}};
{% elif field.type == "vstr4" %}
char {{field.name}}[5];
{% elif field.type == "chararray" %}
char {{field.name}}[{{field.size}}];
{% else %}
{{ raise("Unknown datatype type '%s' for object %d:%d" % (
field.type, object.group, object.variation)) }}
{% endif %}
{% endif %}
{% endfor %}
{% if object.extra_fields %}
{% for field in object.extra_fields %}
{% if field.type == "uint8" %}
uint8_t {{field.name}};
{% elif field.type == "uint16" %}
uint16_t {{field.name}};
{% elif field.type == "uint32" %}
uint32_t {{field.name}};
{% else %}
{{ raise("Unknown datatype: %s" % (field.type)) }}
{% endif %}
{% endfor %}
{% endif %}
} DNP3ObjectG{{object.group}}V{{object.variation}};
{% endfor %}
"""
filename = "src/app-layer-dnp3-objects.h"
try:
env = jinja2.Environment(trim_blocks=True)
code = env.from_string(template).render(context)
content = open(filename).read()
content = re.sub(
"(%s).*(%s)" % (re.escape(IN_PLACE_START), re.escape(IN_PLACE_END)),
r"\1%s\2" % (code), content, 1, re.M | re.DOTALL)
open(filename, "w").write(content)
print("Updated %s." % (filename))
except Exception as err:
print("Failed to update %s: %s" % (filename, err), file=sys.stderr)
sys.exit(1)
def gen_object_decoders(context):
""" Generate decoders for all defined DNP3 objects. """
template = """
/* Code generated by:
* {{command_line}}
*/
{% for object in objects %}
{% if object.packed %}
static int DNP3DecodeObjectG{{object.group}}V{{object.variation}}(const uint8_t **buf, uint32_t *len,
uint8_t prefix_code, uint32_t start, uint32_t count,
DNP3PointList *points)
{
DNP3ObjectG{{object.group}}V{{object.variation}} *object = NULL;
uint32_t bytes = (count / 8) + 1;
uint32_t prefix = 0;
uint32_t point_index = start;
if (!DNP3ReadPrefix(buf, len, prefix_code, &prefix)) {
goto error;
}
for (uint32_t i = 0; i < bytes; i++) {
uint8_t octet;
if (!DNP3ReadUint8(buf, len, &octet)) {
goto error;
}
for (int j = 0; j < 8 && count; j = j + {{object.fields[0].width}}) {
object = SCCalloc(1, sizeof(*object));
if (unlikely(object == NULL)) {
goto error;
}
{% if object.fields[0].width == 1 %}
object->{{object.fields[0].name}} = (octet >> j) & 0x1;
{% elif object.fields[0].width == 2 %}
object->{{object.fields[0].name}} = (octet >> j) & 0x3;
{% else %}
#error "Unhandled field width: {{object.fields[0].width}}"
{% endif %}
if (!DNP3AddPoint(points, object, point_index, prefix_code, prefix)) {
goto error;
}
object = NULL;
count--;
point_index++;
}
}
return 1;
error:
if (object != NULL) {
SCFree(object);
}
return 0;
}
{% else %}
static int DNP3DecodeObjectG{{object.group}}V{{object.variation}}(const uint8_t **buf, uint32_t *len,
uint8_t prefix_code, uint32_t start, uint32_t count,
DNP3PointList *points)
{
DNP3ObjectG{{object.group}}V{{object.variation}} *object = NULL;
uint32_t prefix = 0;
uint32_t point_index = start;
{% if object._track_offset %}
uint32_t offset;
{% endif %}
{% if object.constraints %}
{% for (key, val) in object.constraints.items() %}
{% if key == "require_size_prefix" %}
if (!DNP3PrefixIsSize(prefix_code)) {
goto error;
}
{% elif key == "require_prefix_code" %}
if (prefix_code != {{val}}) {
goto error;
}
{% else %}
{{ raise("Unhandled constraint: %s" % (key)) }}
{% endif %}
{% endfor %}
{% endif %}
if (*len < count/8) {
goto error;
}
while (count--) {
object = SCCalloc(1, sizeof(*object));
if (unlikely(object == NULL)) {
goto error;
}
if (!DNP3ReadPrefix(buf, len, prefix_code, &prefix)) {
goto error;
}
{% if object._track_offset %}
offset = *len;
{% endif %}
{% for field in object.fields %}
{% if field.type == "int16" %}
if (!DNP3ReadUint16(buf, len, (uint16_t *)&object->{{field.name}})) {
goto error;
}
{% elif field.type == "int32" %}
if (!DNP3ReadUint32(buf, len, (uint32_t *)&object->{{field.name}})) {
goto error;
}
{% elif field.type == "uint8" %}
if (!DNP3ReadUint8(buf, len, &object->{{field.name}})) {
goto error;
}
{% elif field.type == "uint16" %}
if (!DNP3ReadUint16(buf, len, &object->{{field.name}})) {
goto error;
}
{% elif field.type == "uint24" %}
if (!DNP3ReadUint24(buf, len, &object->{{field.name}})) {
goto error;
}
{% elif field.type == "uint32" %}
if (!DNP3ReadUint32(buf, len, &object->{{field.name}})) {
goto error;
}
{% elif field.type == "uint64" %}
if (!DNP3ReadUint64(buf, len, &object->{{field.name}})) {
goto error;
}
{% elif field.type == "flt32" %}
if (!DNP3ReadFloat32(buf, len, &object->{{field.name}})) {
goto error;
}
{% elif field.type == "flt64" %}
if (!DNP3ReadFloat64(buf, len, &object->{{field.name}})) {
goto error;
}
{% elif field.type == "dnp3time" %}
if (!DNP3ReadUint48(buf, len, &object->{{field.name}})) {
goto error;
}
{% elif field.type == "vstr4" %}
if (*len < 4) {
goto error;
}
memcpy(object->{{field.name}}, *buf, 4);
object->{{field.name}}[4] = '\\\\0';
*buf += 4;
*len -= 4;
{% elif field.type == "bytearray" %}
{% if field.len_from_prefix %}
if (prefix < (offset - *len)) {
goto error;
}
object->{{field.len_field}} = (uint16_t) (prefix - (offset - *len));
{% endif %}
if (object->{{field.len_field}} > 0) {
if (*len < object->{{field.len_field}}) {
/* Not enough data. */
goto error;
}
object->{{field.name}} = SCCalloc(1, object->{{field.len_field}});
if (unlikely(object->{{field.name}} == NULL)) {
goto error;
}
memcpy(object->{{field.name}}, *buf, object->{{field.len_field}});
*buf += object->{{field.len_field}};
*len -= object->{{field.len_field}};
}
{% elif field.type == "chararray" %}
{% if field.len_from_prefix %}
if (prefix - (offset - *len) >= {{field.size}} || prefix < (offset - *len)) {
goto error;
}
{% if field.size == 255 %}
object->{{field.len_field}} = (uint8_t) (prefix - (offset - *len));
{% else %}
object->{{field.len_field}} = (uint16_t) (prefix - (offset - *len));
{% endif %}
{% endif %}
if (object->{{field.len_field}} > 0) {
if (*len < object->{{field.len_field}}) {
/* Not enough data. */
goto error;
}
memcpy(object->{{field.name}}, *buf, object->{{field.len_field}});
*buf += object->{{field.len_field}};
*len -= object->{{field.len_field}};
}
object->{{field.name}}[object->{{field.len_field}}] = '\\\\0';
{% elif field.type == "bstr8" %}
{
uint8_t octet;
if (!DNP3ReadUint8(buf, len, &octet)) {
goto error;
}
{% set ns = namespace(shift=0) %}
{% for field in field.fields %}
{% if field.width == 1 %}
object->{{field.name}} = (octet >> {{ns.shift}}) & 0x1;
{% elif field.width == 2 %}
object->{{field.name}} = (octet >> {{ns.shift}}) & 0x3;
{% elif field.width == 4 %}
object->{{field.name}} = (octet >> {{ns.shift}}) & 0xf;
{% elif field.width == 7 %}
object->{{field.name}} = (octet >> {{ns.shift}}) & 0x7f;
{% else %}
{{ raise("Unhandled width of %d." % (field.width)) }}
{% endif %}
{% set ns.shift = ns.shift + field.width %}
{% endfor %}
}
{% else %}
{{ raise("Unhandled datatype '%s' for object %d:%d." % (field.type,
object.group, object.variation)) }}
{% endif %}
{% endfor %}
if (!DNP3AddPoint(points, object, point_index, prefix_code, prefix)) {
goto error;
}
object = NULL;
point_index++;
}
return 1;
error:
if (object != NULL) {
{% for field in object.fields %}
{% if field.type == "bytearray" %}
if (object->{{field.name}} != NULL) {
SCFree(object->{{field.name}});
}
{% endif %}
{% endfor %}
SCFree(object);
}
return 0;
}
{% endif %}
{% endfor %}
void DNP3FreeObjectPoint(int group, int variation, void *point)
{
switch(DNP3_OBJECT_CODE(group, variation)) {
{% for object in objects %}
{% if f_has_freeable_types(object.fields) %}
case DNP3_OBJECT_CODE({{object.group}}, {{object.variation}}): {
DNP3ObjectG{{object.group}}V{{object.variation}} *object = (DNP3ObjectG{{object.group}}V{{object.variation}} *) point;
{% for field in object.fields %}
{% if field.type == "bytearray" %}
if (object->{{field.name}} != NULL) {
SCFree(object->{{field.name}});
}
{% endif %}
{% endfor %}
break;
}
{% endif %}
{% endfor %}
default:
break;
}
SCFree(point);
}
/**
* \\\\brief Decode a DNP3 object.
*
* \\\\retval 0 on success. On failure a positive integer corresponding
* to a DNP3 application layer event will be returned.
*/
int DNP3DecodeObject(int group, int variation, const uint8_t **buf,
uint32_t *len, uint8_t prefix_code, uint32_t start,
uint32_t count, DNP3PointList *points)
{
int rc = 0;
switch (DNP3_OBJECT_CODE(group, variation)) {
{% for object in objects %}
case DNP3_OBJECT_CODE({{object.group}}, {{object.variation}}):
rc = DNP3DecodeObjectG{{object.group}}V{{object.variation}}(buf, len, prefix_code, start, count,
points);
break;
{% endfor %}
default:
return DNP3_DECODER_EVENT_UNKNOWN_OBJECT;
}
return rc ? 0 : DNP3_DECODER_EVENT_MALFORMED;
}
"""
try:
filename = "src/app-layer-dnp3-objects.c"
env = jinja2.Environment(trim_blocks=True, lstrip_blocks=True)
code = env.from_string(template).render(context)
content = open(filename).read()
content = re.sub(
"(%s).*(%s)" % (re.escape(IN_PLACE_START), re.escape(IN_PLACE_END)),
r"\1%s\n\2" % (code), content, 1, re.M | re.DOTALL)
open(filename, "w").write(content)
print("Updated %s." % (filename))
except Exception as err:
print("Failed to update %s: %s" % (filename, err), file=sys.stderr)
sys.exit(1)
def preprocess_object(obj):
valid_keys = [
"group",
"variation",
"constraints",
"extra_fields",
"fields",
"packed",
]
valid_field_keys = [
"type",
"name",
"width",
"len_from_prefix",
"len_field",
"fields",
"size",
]
if "unimplemented" in obj:
print("Object not implemented: %s:%s: %s" % (
str(obj["group"]), str(obj["variation"]), obj["unimplemented"]))
return None
for key, val in obj.items():
if key not in valid_keys:
print("Invalid key '%s' in object %d:%d" % (
key, obj["group"], obj["variation"]), file=sys.stderr)
sys.exit(1)
for field in obj["fields"]:
for key in field.keys():
if key not in valid_field_keys:
print("Invalid key '%s' in object %d:%d" % (
key, obj["group"], obj["variation"]), file=sys.stderr)
sys.exit(1)
if "len_from_prefix" in field and field["len_from_prefix"]:
obj["_track_offset"] = True
break
if field["type"] == "bstr8":
width = 0
for subfield in field["fields"]:
width += int(subfield["width"])
assert(width == 8)
return obj
def main():
# Require Jinja2 2.10 or greater.
jv = jinja2.__version__.split(".")
if int(jv[0]) < 2 or (int(jv[0]) == 2 and int(jv[1]) < 10):
print("error: jinja2 v2.10 or great required")
return 1
definitions = yaml.load(open("scripts/dnp3-gen/dnp3-objects.yaml"))
print("Loaded %s objects." % (len(definitions["objects"])))
definitions["objects"] = map(preprocess_object, definitions["objects"])
# Filter out unimplemented objects.
definitions["objects"] = [
obj for obj in definitions["objects"] if obj != None]
context = {
"raise": raise_helper,
"objects": definitions["objects"],
"is_integer_type": is_integer_type,
"f_to_type": to_type,
"f_has_freeable_types": has_freeable_types,
"command_line": " ".join(sys.argv),
}
gen_object_structs(context)
gen_object_decoders(context)
generate(util_lua_dnp3_objects_c_template,
"src/util-lua-dnp3-objects.c",
context)
generate(output_json_dnp3_objects_template,
"src/output-json-dnp3-objects.c",
context)
if __name__ == "__main__":
sys.exit(main())