Project

General

Profile

Packet Inspection Module

Introduction

The simplest detection module type in Suricata is the packet inspection module. It inspects a single packet, most likely a packet header property, but in this example we will look at the payload.

The goal in this simple example is to register a keyword, parse it's options, set it up and make sure it's actually ran. Add tests and integrate it into the engine.

We're calling it "helloworld". It will take two numeric options. The first value will be inspected against the first byte of a payload, the second value against the last.

Syntax: helloworld:1,2;

Not very useful, but the purpose here is to demonstrate how to integrate it into the engine.

Header file

The header file servers two purposes. It declares the prototype of the registration function and it declares the per keyword instance context. This context will hold the rule option values, in this case 2 bytes.

This file will be called src/detect-helloworld.h

#ifndef _DETECT_HELLOWORLD_H
#define _DETECT_HELLOWORLD_H

typedef struct DetectHelloWorldData_ {
    uint8_t helloworld1;   /**< first value */
    uint8_t helloworld2;   /**< second value */
} DetectHelloWorldData;

void DetectHelloWorldRegister(void);

#endif  /* _DETECT_HELLOWORLD_H */

Main code

The detection logic of a module lives in the main c file, in our case src/detect-helloworld.c.

Headers to include

#include "suricata-common.h" 
#include "util-unittest.h" 

#include "detect-parse.h" 
#include "detect-engine.h" 

#include "detect-helloworld.h" 

Registration

The rule options are generally parsed using a pcre regex.

/**
 * \brief Regex for parsing our keyword options
 */
#define PARSE_REGEX  "^\\s*([0-9]+)?\\s*,s*([0-9]+)?\\s*$" 

static pcre *parse_regex;
static pcre_extra *parse_regex_study;

The registration registers 3 functions, Match, Setup and Free.

/* prototypes */
static int DetectHelloWorldMatch (ThreadVars *, DetectEngineThreadCtx *,
        Packet *, Signature *, SigMatch *);
static int DetectHelloWorldSetup (DetectEngineCtx *, Signature *, char *);
static void DetectHelloWorldFree (void *);
static void DetectHelloWorldRegisterTests (void);

/**
 * \brief Registration function for helloworld: keyword
 */
void DetectHelloWorldRegister(void) {
    sigmatch_table[DETECT_HELLOWORLD].name = "helloworld";
    sigmatch_table[DETECT_HELLOWORLD].desc = "<todo>";
    sigmatch_table[DETECT_HELLOWORLD].url = "<todo>";
    sigmatch_table[DETECT_HELLOWORLD].Match = DetectHelloWorldMatch;
    sigmatch_table[DETECT_HELLOWORLD].Setup = DetectHelloWorldSetup;
    sigmatch_table[DETECT_HELLOWORLD].Free = DetectHelloWorldFree;
    sigmatch_table[DETECT_HELLOWORLD].RegisterTests = DetectHelloWorldRegisterTests;

    const char *eb;
    int eo;
    int opts = 0;

    parse_regex = pcre_compile(PARSE_REGEX, opts, &eb, &eo, NULL);
    if (parse_regex == NULL) {
        SCLogError(SC_ERR_PCRE_COMPILE, "pcre compile of \"%s\" failed at offset %" PRId32 ": %s", PARSE_REGEX, eo, eb);
        goto error;
    }

    parse_regex_study = pcre_study(parse_regex, 0, &eb);
    if (eb != NULL) {
        SCLogError(SC_ERR_PCRE_STUDY, "pcre study failed: %s", eb);
        goto error;
    }
    return;

error:
    if (parse_regex != NULL)
        SCFree(parse_regex);
    if (parse_regex_study != NULL)
        SCFree(parse_regex_study);
    return;
}

The Match function

The Match function is called per Signature, per Packet, after pre-filtering. So only if the addresses, ports and content inspection all match, then this is called.

The module is guaranteed to exclusively inspect this packet, so as long as only the packet itself and the keyword's context are used, threading is of no concern here.

Note that the keyword's context should be considered read-only at this point, as multiple detect threads can read from it.

/**
 * \brief This function is used to match HELLOWORLD rule option on a packet
 *
 * \param t pointer to thread vars
 * \param det_ctx pointer to the pattern matcher thread
 * \param p pointer to the current packet
 * \param m pointer to the sigmatch with context that we will cast into DetectHelloWorldData
 *
 * \retval 0 no match
 * \retval 1 match
 */
int DetectHelloWorldMatch (ThreadVars *t, DetectEngineThreadCtx *det_ctx, Packet *p, Signature *s, SigMatch *m)
{
    int ret = 0;
    DetectHelloWorldData *helloworldd = (DetectHelloWorldData *) m->ctx;
#if 0
    if (PKT_IS_PSEUDOPKT(p)) {
        /* fake pkt */
    }

    if (PKT_IS_IPV4(p)) {
        /* ipv4 pkt */
    } else if (PKT_IS_IPV6(p)) {
        /* ipv6 pkt */
    } else {
        SCLogDebug("pcket is of not IPv4 or IPv6");
        return ret;
    }
#endif
    /* packet payload access */
    if (p->payload != NULL && p->payload_len > 0) {
        if (helloworldd->helloworld1 == p->payload[0] &&
            helloworldd->helloworld2 == p->payload[p->payload_len - 1])
        {
            ret = 1;
        }
    }

    return ret;
}

Setup and keyword parsing

Parsing

Rule keyword options are parsed using the PCRE above.

/**
 * \brief This function is used to parse helloworld options passed via helloworld: keyword
 *
 * \param helloworldstr Pointer to the user provided helloworld options
 *
 * \retval helloworldd pointer to DetectHelloWorldData on success
 * \retval NULL on failure
 */

DetectHelloWorldData *DetectHelloWorldParse (char *helloworldstr)
{
    DetectHelloWorldData *helloworldd = NULL;
    char *arg1 = NULL;
    char *arg2 = NULL;
#define MAX_SUBSTRINGS 30
    int ret = 0, res = 0;
    int ov[MAX_SUBSTRINGS];

    ret = pcre_exec(parse_regex, parse_regex_study, helloworldstr, strlen(helloworldstr), 0, 0, ov, MAX_SUBSTRINGS);
    if (ret != 3) {
        SCLogError(SC_ERR_PCRE_MATCH, "parse error, ret %" PRId32 "", ret);
        goto error;
    }
    const char *str_ptr;

    res = pcre_get_substring((char *) helloworldstr, ov, MAX_SUBSTRINGS, 1, &str_ptr);
    if (res < 0) {
        SCLogError(SC_ERR_PCRE_GET_SUBSTRING, "pcre_get_substring failed");
        goto error;
    }
    arg1 = (char *) str_ptr;
    SCLogDebug("Arg1 \"%s\"", arg1);

    if (ret >= 3) {
        res = pcre_get_substring((char *) helloworldstr, ov, MAX_SUBSTRINGS, 2, &str_ptr);
        if (res < 0) {
            SCLogError(SC_ERR_PCRE_GET_SUBSTRING, "pcre_get_substring failed");
            goto error;
        }
        arg2 = (char *) str_ptr;
        SCLogDebug("Arg2 \"%s\"", arg2);

    }

    helloworldd = SCMalloc(sizeof (DetectHelloWorldData));
    if (unlikely(helloworldd == NULL))
        goto error;
    helloworldd->helloworld1 = (uint8_t)atoi(arg1);
    helloworldd->helloworld2 = (uint8_t)atoi(arg2);

    SCFree(arg1);
    SCFree(arg2);
    return helloworldd;

error:
    if (helloworldd)
        SCFree(helloworldd);
    if (arg1)
        SCFree(arg1);
    if (arg2)
        SCFree(arg2);
    return NULL;
}

Setup

The Setup function creates a context for the keyword and attaches it to the Signature. It runs the parser above to setup the context.

Note it will be run if a signature uses the keyword. So while coding/testing, add a test rule:

alert tcp any any -> any any (msg:"helloworld 12"; helloworld:1,2; sid:1234567; rev:1;)

/**
 * \brief this function is used to get the parsed helloworld data into the current signature
 *
 * \param de_ctx pointer to the Detection Engine Context
 * \param s pointer to the Current Signature
 * \param helloworldstr pointer to the user provided helloworld options
 *
 * \retval 0 on Success
 * \retval -1 on Failure
 */
static int DetectHelloWorldSetup (DetectEngineCtx *de_ctx, Signature *s, char *helloworldstr)
{
    DetectHelloWorldData *helloworldd = NULL;
    SigMatch *sm = NULL;

    helloworldd = DetectHelloWorldParse(helloworldstr);
    if (helloworldd == NULL)
        goto error;

    sm = SigMatchAlloc();
    if (sm == NULL)
        goto error;

    sm->type = DETECT_HELLOWORLD;
    sm->ctx = (void *)helloworldd;

    SigMatchAppendSMToList(s, sm, DETECT_SM_LIST_MATCH);
    s->flags |= SIG_FLAG_REQUIRE_PACKET;

    return 0;

error:
    if (helloworldd != NULL)
        DetectHelloWorldFree(helloworldd);
    if (sm != NULL)
        SCFree(sm);
    return -1;
}

Free

The Free function is called when the engine cleans up the signature. This can happen when Suricata shuts down, in case a keyword after this on in the same signature returns a parse error or during live rule reload, when one detection engine is replaced by a new one.

/**
 * \brief this function will free memory associated with DetectHelloWorldData
 *
 * \param ptr pointer to DetectHelloWorldData
 */
void DetectHelloWorldFree(void *ptr) {
    DetectHelloWorldData *helloworldd = (DetectHelloWorldData *)ptr;
    SCFree(helloworldd);
}

Unittesting

Each module should have a number of unittests to test the various conditions for parsing, matching and such.

#ifdef UNITTESTS

/**
 * \test description of the test
 */

static int DetectHelloWorldParseTest01 (void) {
    DetectHelloWorldData *helloworldd = NULL;
    uint8_t res = 0;

    helloworldd = DetectHelloWorldParse("1,10");
    if (helloworldd != NULL) {
        if (helloworldd->helloworld1 == 1 && helloworldd->helloworld2 == 10)
            res = 1;

        DetectHelloWorldFree(helloworldd);
    }

    return res;
}

static int DetectHelloWorldSignatureTest01 (void) {
    uint8_t res = 0;

    DetectEngineCtx *de_ctx = DetectEngineCtxInit();
    if (de_ctx == NULL)
        goto end;

    Signature *sig = DetectEngineAppendSig(de_ctx, "alert ip any any -> any any (helloworld:1,10; sid:1; rev:1;)");
    if (sig == NULL) {
        printf("parsing signature failed: ");
        goto end;
    }

    /* if we get here, all conditions pass */
    res = 1;
end:
    if (de_ctx != NULL)
        DetectEngineCtxFree(de_ctx);
    return res;
}

#endif /* UNITTESTS */

/**
 * \brief this function registers unit tests for DetectHelloWorld
 */
void DetectHelloWorldRegisterTests(void) {
#ifdef UNITTESTS
    UtRegisterTest("DetectHelloWorldParseTest01",
            DetectHelloWorldParseTest01, 1);
    UtRegisterTest("DetectHelloWorldSignatureTest01",
            DetectHelloWorldSignatureTest01, 1);
#endif /* UNITTESTS */
}

Integrating into the rest of the engine

Build System

To tell the build system about the new module, edit src/Makefile.am and add the files to "suricata_SOURCES".

diff --git a/src/Makefile.am b/src/Makefile.am
index 0910e06..38a82e1 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -120,6 +120,7 @@ detect-fragbits.c detect-fragbits.h \
 detect-fragoffset.c detect-fragoffset.h \
 detect-ftpbounce.c detect-ftpbounce.h \
 detect-gid.c detect-gid.h \
+detect-helloworld.c detect-helloworld.h \
 detect-http-client-body.c detect-http-client-body.h \
 detect-http-cookie.c detect-http-cookie.h \
 detect-http-header.c detect-http-header.h \

Note that the list of files is ordered alphabetically by convention.

Keyword ID

In src/detect.c a unique ID for each rule keyword is registered.

diff --git a/src/detect.h b/src/detect.h
index 3ebcb8a..5dca075 100644
--- a/src/detect.h
+++ b/src/detect.h
@@ -1097,6 +1097,8 @@ enum {
     DETECT_LUAJIT,
     DETECT_IPREP,

+    DETECT_HELLOWORLD,
+
     /* make sure this stays last */
     DETECT_TBLSIZE,
 };

Calling the registration function

In src/detect.c the registration function should be added to "SigTableSetup". To make this work the header file needs to be included in the file as well.

diff --git a/src/detect.c b/src/detect.c
index c9224d6..16b7f91 100644
--- a/src/detect.c
+++ b/src/detect.c
@@ -144,6 +144,7 @@
 #include "detect-app-layer-event.h" 
 #include "detect-luajit.h" 
 #include "detect-iprep.h" 
+#include "detect-helloworld.h" 

 #include "util-rule-vars.h" 

@@ -4781,6 +4782,7 @@ void SigTableSetup(void) {
     DetectHttpUARegister();
     DetectLuajitRegister();
     DetectIPRepRegister();
+    DetectHelloWorldRegister();

     uint8_t i = 0;
     for (i = 0; i < DETECT_TBLSIZE; i++) {

make

Just typing "make" should now build your new module.

Make sure it compiles without compiler warnings, then test the unittests:

$ ./src/suricata -u -l tmp/ -UHelloWorld
[13959] 30/11/2012 -- 17:56:21 - (suricata.c:1260) <Info> (main) -- This is Suricata version 1.4dev (rev 2accda7)
Test DetectHelloWorldParseTest01                                  : pass
Test DetectHelloWorldSignatureTest01                              : pass
==== TEST RESULTS ====
PASSED: 2
FAILED: 0
======================