[ebtables] Add string filter to ebtables

Message ID 20180226215827.7521-1-bernie.harris@alliedtelesis.co.nz
State Changes Requested
Delegated to: Pablo Neira
Headers show
Series
  • [ebtables] Add string filter to ebtables
Related show

Commit Message

Bernie Harris Feb. 26, 2018, 9:58 p.m.
This patch is part of a proposal to add a string filter to
ebtables, which would be similar to the string filter in
iptables.

Like iptables, the ebtables filter uses the xt_string module,
however some modifications have been made for this to work
correctly.

Currently ebtables assumes that the revision number of all match
modules is 0. The xt_string module doesn't register a match with
revision 0 so the solution is to modify ebtables to allow
extensions to specify a revision number, similar to iptables.
This gets passed down to the kernel, which is then able to find
the match module correctly.

Signed-off-by: Bernie Harris <bernie.harris@alliedtelesis.co.nz>
---
 ebtables.8              |  20 +++
 extensions/Makefile     |   2 +-
 extensions/ebt_string.c | 319 ++++++++++++++++++++++++++++++++++++++++++++++++
 include/ebtables.h      |   5 +-
 include/ebtables_u.h    |   1 +
 libebtc.c               |   6 +-
 6 files changed, 350 insertions(+), 3 deletions(-)
 create mode 100644 extensions/ebt_string.c

Patch

diff --git a/ebtables.8 b/ebtables.8
index 81d1cf6..e3290fe 100644
--- a/ebtables.8
+++ b/ebtables.8
@@ -810,6 +810,26 @@  The hello time timer (0-65535) range.
 .TP
 .BR "--stp-forward-delay " "[!] [\fIdelay\fP][:\fIdelay\fP]"
 The forward delay timer (0-65535) range.
+.SS string
+This module matches on a given string using some pattern matching strategy.
+.TP
+.BR "--string-algo " "\fIalgorithm\fP"
+The pattern matching strategy. (bm = Boyer-Moore, kmp = Knuth-Pratt-Morris)
+.TP
+.BR "--string-from " "\fIoffset\fP"
+The lowest offset from which a match can start. (default: 0)
+.TP
+.BR "--string-to " "\fIoffset\fP"
+The highest offset from which a match can start. (default: size of frame)
+.TP
+.BR "--string " "[!] \fIpattern\fP"
+Matches the given pattern.
+.TP
+.BR "--string-hex " "[!] \fIpattern\fP"
+Matches the given pattern in hex notation, e.g. '|0D 0A|', '|0D0A|', 'www|09|netfilter|03|org|00|'
+.TP
+.BR "--string-icase"
+Ignore case when searching.
 .SS vlan
 Specify 802.1Q Tag Control Information fields.
 The protocol must be specified as
diff --git a/extensions/Makefile b/extensions/Makefile
index b3548e8..60a70a2 100644
--- a/extensions/Makefile
+++ b/extensions/Makefile
@@ -1,7 +1,7 @@ 
 #! /usr/bin/make
 
 EXT_FUNC+=802_3 nat arp arpreply ip ip6 standard log redirect vlan mark_m mark \
-          pkttype stp among limit ulog nflog
+          pkttype stp among limit ulog nflog string
 EXT_TABLES+=filter nat broute
 EXT_OBJS+=$(foreach T,$(EXT_FUNC), extensions/ebt_$(T).o)
 EXT_OBJS+=$(foreach T,$(EXT_TABLES), extensions/ebtable_$(T).o)
diff --git a/extensions/ebt_string.c b/extensions/ebt_string.c
new file mode 100644
index 0000000..793f5df
--- /dev/null
+++ b/extensions/ebt_string.c
@@ -0,0 +1,319 @@ 
+/* ebt_string
+ *
+ * Author:
+ * Bernie Harris <bernie.harris@alliedtelesis.co.nz>
+ *
+ * February, 2018
+ *
+ * Based on:
+ *  libxt_string.c, Copyright (C) 2000 Emmanuel Roger  <winfield@freegates.be>
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+#include <getopt.h>
+#include <netdb.h>
+#include <ctype.h>
+#include "../include/ebtables_u.h"
+#include <linux/if_packet.h>
+#include <linux/netfilter/xt_string.h>
+
+#define STRING_FROM  '1'
+#define STRING_TO    '2'
+#define STRING_ALGO  '3'
+#define STRING_ICASE '4'
+#define STRING       '5'
+#define STRING_HEX   '6'
+#define OPT_STRING_FROM  (1 << 0)
+#define OPT_STRING_TO    (1 << 1)
+#define OPT_STRING_ALGO  (1 << 2)
+#define OPT_STRING_ICASE (1 << 3)
+#define OPT_STRING       (1 << 4)
+#define OPT_STRING_HEX   (1 << 5)
+
+static const struct option opts[] =
+{
+	{ "string-from"             , required_argument, 0, STRING_FROM },
+	{ "string-to"               , required_argument, 0, STRING_TO },
+	{ "string-algo"             , required_argument, 0, STRING_ALGO },
+	{ "string-icase"            , no_argument,       0, STRING_ICASE },
+	{ "string"                  , required_argument, 0, STRING },
+	{ "string-hex"              , required_argument, 0, STRING_HEX },
+	{ 0 }
+};
+
+static void print_help()
+{
+	printf(
+"string options:\n"
+"--string-from offset    : Offset to start searching from (default: 0)\n"
+"--string-to   offset    : Offset to stop searching (default: packet size)\n"
+"--string-algo algorithm : Algorithm (bm = Boyer-Moore, kmp = Knuth-Pratt-Morris)\n"
+"--string-icase          : Ignore case when searching\n"
+"--string     [!] string : Match a string in a packet\n"
+"--string-hex [!] string : Match a hex string in a packet, e.g. |0D 0A|, |0D0A|, netfilter|03|org\n");
+}
+
+static void init(struct ebt_entry_match *match)
+{
+	struct xt_string_info *info = (struct xt_string_info *)match->data;
+
+	info->to_offset = UINT16_MAX;
+}
+
+static void parse_string(const char *s, struct xt_string_info *info)
+{
+	/* xt_string does not need \0 at the end of the pattern */
+	if (strlen(s) <= XT_STRING_MAX_PATTERN_SIZE) {
+		strncpy(info->pattern, s, XT_STRING_MAX_PATTERN_SIZE);
+		info->patlen = strnlen(s, XT_STRING_MAX_PATTERN_SIZE);
+		return;
+	}
+	ebt_print_error2("STRING too long \"%s\"", s);
+}
+
+static void parse_hex_string(const char *s, struct xt_string_info *info)
+{
+	int i=0, slen, sindex=0, schar;
+	short hex_f = 0, literal_f = 0;
+	char hextmp[3];
+
+	slen = strlen(s);
+
+	if (slen == 0) {
+		ebt_print_error2("STRING must contain at least one char");
+	}
+
+	while (i < slen) {
+		if (s[i] == '\\' && !hex_f) {
+			literal_f = 1;
+		} else if (s[i] == '\\') {
+			ebt_print_error2("Cannot include literals in hex data");
+		} else if (s[i] == '|') {
+			if (hex_f)
+				hex_f = 0;
+			else {
+				hex_f = 1;
+				/* get past any initial whitespace just after the '|' */
+				while (s[i+1] == ' ')
+					i++;
+			}
+			if (i+1 >= slen)
+				break;
+			else
+				i++;  /* advance to the next character */
+		}
+
+		if (literal_f) {
+			if (i+1 >= slen) {
+				ebt_print_error2("Bad literal placement at end of string");
+			}
+			info->pattern[sindex] = s[i+1];
+			i += 2;  /* skip over literal char */
+			literal_f = 0;
+		} else if (hex_f) {
+			if (i+1 >= slen) {
+				ebt_print_error2("Odd number of hex digits");
+			}
+			if (i+2 >= slen) {
+				/* must end with a "|" */
+				ebt_print_error2("Invalid hex block");
+			}
+			if (! isxdigit(s[i])) /* check for valid hex char */
+				ebt_print_error2("Invalid hex char '%c'", s[i]);
+			if (! isxdigit(s[i+1])) /* check for valid hex char */
+				ebt_print_error2("Invalid hex char '%c'", s[i+1]);
+			hextmp[0] = s[i];
+			hextmp[1] = s[i+1];
+			hextmp[2] = '\0';
+			if (! sscanf(hextmp, "%x", &schar))
+				ebt_print_error2("Invalid hex char `%c'", s[i]);
+			info->pattern[sindex] = (char) schar;
+			if (s[i+2] == ' ')
+				i += 3;  /* spaces included in the hex block */
+			else
+				i += 2;
+		} else {  /* the char is not part of hex data, so just copy */
+			info->pattern[sindex] = s[i];
+			i++;
+		}
+		if (sindex > XT_STRING_MAX_PATTERN_SIZE)
+			ebt_print_error2("STRING too long \"%s\"", s);
+		sindex++;
+	}
+	info->patlen = sindex;
+}
+
+static int parse(int c, char **argv, int argc, const struct ebt_u_entry *entry,
+		 unsigned int *flags, struct ebt_entry_match **match)
+{
+	struct xt_string_info *info = (struct xt_string_info *)(*match)->data;
+	int i;
+	int input_string_length = 0;
+	char buf[3] = { 0 };
+
+	switch (c) {
+	case STRING_FROM:
+		ebt_check_option2(flags, OPT_STRING_FROM);
+		if (ebt_check_inverse2(optarg))
+			ebt_print_error2("Unexpected `!' after --string-from");
+		info->from_offset = (__u16)strtoul(optarg, NULL, 10);
+		break;
+	case STRING_TO:
+		ebt_check_option2(flags, OPT_STRING_TO);
+		if (ebt_check_inverse2(optarg))
+			ebt_print_error2("Unexpected `!' after --string-to");
+		info->to_offset = (__u16)strtoul(optarg, NULL, 10);
+		break;
+	case STRING_ALGO:
+		ebt_check_option2(flags, OPT_STRING_ALGO);
+		if (ebt_check_inverse2(optarg))
+			ebt_print_error2("Unexpected `!' after --string-algo");
+		strncpy(info->algo, optarg, XT_STRING_MAX_ALGO_NAME_SIZE);
+		break;
+	case STRING_ICASE:
+		ebt_check_option2(flags, OPT_STRING_ICASE);
+		if (ebt_check_inverse2(optarg))
+			ebt_print_error2("Unexpected `!' after --string-icase");
+		info->u.v1.flags |= XT_STRING_FLAG_IGNORECASE;
+		break;
+	case STRING:
+		ebt_check_option2(flags, OPT_STRING);
+		parse_string(optarg, info);
+		if (ebt_check_inverse2(optarg)) {
+			info->u.v1.flags |= XT_STRING_FLAG_INVERT;
+		}
+		break;
+	case STRING_HEX:
+		ebt_check_option2(flags, OPT_STRING_HEX);
+		parse_hex_string(optarg, info);
+		if (ebt_check_inverse2(optarg)) {
+			info->u.v1.flags |= XT_STRING_FLAG_INVERT;
+		}
+		break;
+	default:
+		return 0;
+	}
+	return 1;
+}
+
+static void final_check(const struct ebt_u_entry *entry,
+			const struct ebt_entry_match *match, const char *name,
+			unsigned int hookmask, unsigned int time)
+{
+	struct xt_string_info *info = (struct xt_string_info *)match->data;
+
+	if (info->to_offset < info->from_offset) {
+		ebt_print_error2("'to' offset should not be less than 'from' "
+				 "offset");
+	}
+}
+
+/* Test to see if the string contains non-printable chars or quotes */
+static unsigned short int is_hex_string(const char *str,
+					const unsigned short int len)
+{
+	unsigned int i;
+	for (i=0; i < len; i++) {
+		if (! isprint(str[i])) {
+			/* string contains at least one non-printable char */
+			return 1;
+		}
+	}
+	/* use hex output if the last char is a "\" */
+	if (str[len-1] == '\\')
+		return 1;
+	return 0;
+}
+
+/* Print string with "|" chars included as one would pass to --string-hex */
+static void print_hex_string(const char *str, const unsigned short int len)
+{
+	unsigned int i;
+	/* start hex block */
+	printf("\"|");
+	for (i=0; i < len; i++)
+		printf("%02x", (unsigned char)str[i]);
+	/* close hex block */
+	printf("|\" ");
+}
+
+static void print_string(const char *str, const unsigned short int len)
+{
+	unsigned int i;
+	printf("\"");
+	for (i=0; i < len; i++) {
+		if (str[i] == '\"' || str[i] == '\\')
+			putchar('\\');
+		printf("%c", (unsigned char) str[i]);
+	}
+	printf("\" ");  /* closing quote */
+}
+
+static void print(const struct ebt_u_entry *entry,
+		  const struct ebt_entry_match *match)
+{
+	const struct xt_string_info *info =
+		(const struct xt_string_info *) match->data;
+	int invert = info->u.v1.flags & XT_STRING_FLAG_INVERT;
+
+	if (is_hex_string(info->pattern, info->patlen)) {
+		printf("--string-hex %s", invert ? "! " : "");
+		print_hex_string(info->pattern, info->patlen);
+	} else {
+		printf("--string %s", invert ? "! " : "");
+		print_string(info->pattern, info->patlen);
+	}
+	printf("--string-algo %s ", info->algo);
+	if (info->from_offset != 0)
+		printf("--string-from %u ", info->from_offset);
+	if (info->to_offset != 0)
+		printf("--string-to %u ", info->to_offset);
+	if (info->u.v1.flags & XT_STRING_FLAG_IGNORECASE)
+		printf("--string-icase ");
+}
+
+static int compare(const struct ebt_entry_match *m1,
+		   const struct ebt_entry_match *m2)
+{
+	const struct xt_string_info *info1 =
+		(const struct xt_string_info *) m1->data;
+	const struct xt_string_info *info2 =
+		(const struct xt_string_info *) m2->data;
+
+	if (info1->from_offset != info2->from_offset)
+		return 0;
+	if (info1->to_offset != info2->to_offset)
+		return 0;
+	if (info1->u.v1.flags != info2->u.v1.flags)
+		return 0;
+	if (info1->patlen != info2->patlen)
+		return 0;
+	if (strncmp (info1->algo, info2->algo, XT_STRING_MAX_ALGO_NAME_SIZE) != 0)
+		return 0;
+	if (strncmp (info1->pattern, info2->pattern, info1->patlen) != 0)
+		return 0;
+
+	return 1;
+}
+
+static struct ebt_u_match string_match =
+{
+	.name		= "string",
+	.revision	= 1,
+	.size		= sizeof(struct xt_string_info),
+	.help		= print_help,
+	.init		= init,
+	.parse		= parse,
+	.final_check	= final_check,
+	.print		= print,
+	.compare	= compare,
+	.extra_ops	= opts,
+};
+
+void _init(void)
+{
+	ebt_register_match(&string_match);
+}
diff --git a/include/ebtables.h b/include/ebtables.h
index 8f520c6..35719f6 100644
--- a/include/ebtables.h
+++ b/include/ebtables.h
@@ -113,7 +113,10 @@  struct ebt_entries {
 struct ebt_entry_match
 {
 	union {
-		char name[EBT_FUNCTION_MAXNAMELEN];
+		struct {
+			char name[EBT_FUNCTION_MAXNAMELEN];
+			uint8_t revision;
+		};
 		struct ebt_match *match;
 	} u;
 	/* size of data */
diff --git a/include/ebtables_u.h b/include/ebtables_u.h
index 35a5bcc..c6ca6f1 100644
--- a/include/ebtables_u.h
+++ b/include/ebtables_u.h
@@ -144,6 +144,7 @@  struct ebt_u_entry
 struct ebt_u_match
 {
 	char name[EBT_FUNCTION_MAXNAMELEN];
+	uint8_t revision;
 	/* size of the real match data */
 	unsigned int size;
 	void (*help)(void);
diff --git a/libebtc.c b/libebtc.c
index d474248..92fd764 100644
--- a/libebtc.c
+++ b/libebtc.c
@@ -272,6 +272,7 @@  void ebt_reinit_extensions()
 			if (!m->m)
 				ebt_print_memory();
 			strcpy(m->m->u.name, m->name);
+			m->m->u.revision = m->revision;
 			m->m->match_size = EBT_ALIGN(m->size);
 			m->used = 0;
 		}
@@ -550,8 +551,10 @@  int ebt_check_rule_exists(struct ebt_u_replace *replace,
 		while (m_l) {
 			m = (struct ebt_u_match *)(m_l->m);
 			m_l2 = u_e->m_list;
-			while (m_l2 && strcmp(m_l2->m->u.name, m->m->u.name))
+			while (m_l2 && (strcmp(m_l2->m->u.name, m->m->u.name) ||
+			       m_l2->m->u.revision != m->m->u.revision)) {
 				m_l2 = m_l2->next;
+			}
 			if (!m_l2 || !m->compare(m->m, m_l2->m))
 				goto letscontinue;
 			j++;
@@ -1209,6 +1212,7 @@  void ebt_register_match(struct ebt_u_match *m)
 	if (!m->m)
 		ebt_print_memory();
 	strcpy(m->m->u.name, m->name);
+	m->m->u.revision = m->revision;
 	m->m->match_size = EBT_ALIGN(m->size);
 	m->init(m->m);