Analysis of snort rule structure

In the previous article, I briefly introduced the rule fields in comparison with snort's rules

Next, let's look at the data structure of rule headers and rule options

/* Chain list of rule head matching functions*/
typedef struct _RuleFpList
{
    /* context data for this test */
    /* The rule header is not used at present */
    void *context;

    /* rule check function pointer */
    /* Function type pointer and parameter of rule header matching: data package, rule header object and next object to be matched: in this way, the whole rule header matching can be realized by traversing the chain list within the function*/
    int (*RuleHeadFunc)(Packet *, struct _RuleTreeNode *, struct _RuleFpList *, int);

    /* pointer to the next rule function node */
    struct _RuleFpList *next;
} RuleFpList;

/*
 * Rule head
 */
typedef struct _RuleTreeNode
{
    /*The matching function of rule header is a single chain table*/
    RuleFpList *rule_func; /* match functions.. (Bidirectional etc.. ) */

    /* Regular header count, will be de duplicated*/
    int head_node_number;

    /* For example, the rule type is the example of the previous article: rule type alert*/
    RuleType type;

    /* Source IP group in rule header*/
    IpAddrSet *sip;
    /* Objective group IP*/
    IpAddrSet *dip;

    /* Protocol type: tcp udp */
    int proto;

    /* Source port group*/
    PortObject * src_portobject;
    /* Destination port rule*/
    PortObject * dst_portobject;

    /* Tags: packet direction, port type: any, etc*/
    uint32_t flags;     /* control flags */

    /* stuff for dynamic rules activation/deactivation */
    /* Use with Dynamic rules*/
    int active_flag;
    int activation_counter;
    int countdown;
    ActivateListNode *activate_list;

#if 0
    struct _RuleTreeNode *right;  /* ptr to the next RTN in the list */

    /** list of rule options to associate with this rule node */
    OptTreeNode *down;   
#endif

    /**points to global parent RTN list (Drop/Alert) which contains this 
     * RTN.
     */
    /*The header alert of the rule header chain of a certain type to which the rule belongs*/
    struct _ListHead *listhead;

    /**reference count from otn. Multiple OTNs can reference this RTN with the same
     * policy.
     */
    /* The reference count of rule options. Many rule options are subordinate to one rule header*/
    unsigned int otnRefCount;

} RuleTreeNode;


/* Rule chain header */
typedef struct _ListHead
{
    struct _OutputFuncNode *LogList;
    struct _OutputFuncNode *AlertList;
    struct _RuleListNode *ruleListNode;
} ListHead;

/* Rule linked list, which links all kinds of rule chain headers ListHead into a linked list to facilitate the detection of whether the rules are legal, such as name="snort". If there is no such action rule, the program reports an error*/
typedef struct _RuleListNode
{
    ListHead *RuleList;         /* The rule list associated with this node */
    RuleType mode;              /* the rule mode */
    int rval;                   /* 0 == no detection, 1 == detection event */
    int evalIndex;              /* eval index for this rule set */
    char *name;                 /* name of this rule list (for debugging)  */
    struct _RuleListNode *next; /* the next RuleListNode */
} RuleListNode;

As mentioned earlier, there is a lot of redundancy in the rule header. Next, it analyzes how snort can be de duplicated.

When snort parses the rule header, in the ProcessHeadNode function, call findHeadNode to check whether the same rule header exists before querying,

If RTN - > otnrefcount + + exists, increase the reference count. Otherwise, create a new rule header object and set various matching callback functions.

Analysis flow of rule header in the form of source code

static void ParseRule(SnortConfig *sc, SnortPolicy *p, char *args,
                      RuleType rule_type, ListHead *list)
{
...
    /* If there is no rule header, use the default rule header, TCP any any - > any any*/
    if (*args == '(')
    {
        test_rtn.flags |= ANY_DST_PORT;
        test_rtn.flags |= ANY_SRC_PORT;
        test_rtn.flags |= ANY_DST_IP;
        test_rtn.flags |= ANY_SRC_IP;
        test_rtn.flags |= BIDIRECTIONAL;
        test_rtn.type = rule_type;
        protocol = IPPROTO_TCP;

        roptions = args;

        DEBUG_WRAP(DebugMessage(DEBUG_CONFIGRULES, "Preprocessor Rule detected\n"););
    }    else
    {
        /* proto ip port dir ip port r*/
        /* Call the tool function mpspit to segment and read the rule content. The rule header is separated by spaces or '\ t', so "\ T" is passed in. The rule header has 7 items, but there is no action such as alert, so there are 6 items left in the rule header, along with the segmentation rule option. Therefore, pass in 7, num_toks returns the number of actual segmentation. The last parameter is escape character. Some rules are very long, and use '\ \' to enter Line wrap*/
        toks = mSplit(args, " \t", 7, &num_toks, '\\');

        /* A rule might not have rule options */
        if (num_toks < 6)
        {
            ParseError("Bad rule in rules file: %s", args);
        }
        /* Normal processing, toks[6]: rule options*/
        if (num_toks == 7)
            roptions = toks[6];

        /* The rule action is parsed before it enters the function*/
        test_rtn.type = rule_type;

       ...

        /* Set the rule protocol - fatal errors if protocol not found */
        /* Example of previous article: alert TCP $home & net any - > $external & net $HTTP & Ports */
        /* First rule agreement */
        protocol = GetRuleProtocol(toks[0]);
        test_rtn.proto = protocol;
        ...
        /* When the source IP group is resolved, the value of $home? Net will be obtained from the IP? Vartable linked list in the targeted? Policies policy object of the snort? Config object*/
        ProcessIP(sc, toks[1], &test_rtn, SRC, 0);
        ...
        /*Resolving the source IP group will find the PortObject object object from the hash table in portVarTable in the targeted policies policy object in the snort config object*/
        if (ParsePortList(&test_rtn, portVarTable, nonamePortVarTable,
                          toks[2], protocol, 0 /* =src port */ ))
        {
            ParseError("Bad source port: '%s'", toks[2]);
        }

        /* Packet direction analysis of rule header, determine format first*/
        if ((strcmp(toks[3], RULE_DIR_OPT__DIRECTIONAL) != 0) &&
            (strcmp(toks[3], RULE_DIR_OPT__BIDIRECTIONAL) != 0))
        {
            ParseError("Illegal direction specifier: %s", toks[3]);
        }

        /* New in version 1.3: support for bidirectional rules
         * This checks the rule "direction" token and sets the bidirectional
         * flag if the token = '<>' */
        /* Default "- >", otherwise "< >*/
        if (strcmp(toks[3], RULE_DIR_OPT__BIDIRECTIONAL) == 0)
        {
            DEBUG_WRAP(DebugMessage(DEBUG_CONFIGRULES,"Bidirectional rule!\n"););
            test_rtn.flags |= BIDIRECTIONAL;
        }
        ...
       /* Set the chain header of the rule action to which the rule header belongs*/
       test_rtn.listhead = list;
       /* Internally, it will query whether there is a matching callback function between the rule header and the setting rule header*/
       rtn = ProcessHeadNode(sc, &test_rtn, list);

    ...

    }
}

 

Source code analysis of rule header callback function

static RuleTreeNode * ProcessHeadNode(SnortConfig *sc, RuleTreeNode *test_node,
                                      ListHead *list)
{
    /* Find out if there is redundancy*/
    RuleTreeNode *rtn = findHeadNode(sc, test_node, getParserPolicy(sc));
    /* No, create directly, and copy in depth*/
    if (rtn == NULL)
    {
        DEBUG_WRAP(DebugMessage(DEBUG_CONFIGRULES,"Building New Chain head node\n"););

        rtn = (RuleTreeNode *)SnortAlloc(sizeof(RuleTreeNode));

        rtn->otnRefCount++;

        /* copy the prototype header info into the new header block */
        XferHeader(test_node, rtn);

        head_count++;
        rtn->head_node_number = head_count;

        /* initialize the function list for the new RTN */
        /* Set various callback functions*/
        SetupRTNFuncList(rtn);

        /* add link to parent listhead */
        rtn->listhead = list;
...
    }
    else
    {
        /*Increase reference count exists*/
        rtn->otnRefCount++;
        FreeRuleTreeNode(test_node);
    }

    return rtn;
}

 

static void SetupRTNFuncList(RuleTreeNode * rtn)
{
    /* If the rule headers are bidirectional, directly set the callback function CheckBidirectional*/
    if(rtn->flags & BIDIRECTIONAL)
    {
        DEBUG_WRAP(DebugMessage(DEBUG_CONFIGRULES,"CheckBidirectional->\n"););
        AddRuleFuncToList(CheckBidirectional, rtn);
    }
    /* Set callback functions one by one according to rule options*/
    else
    {
        
        /* Attach the proper port checking function to the function list */
        /*
         * the in-line "if's" check to see if the "any" or "not" flags have
         * been set so the PortToFunc call can determine which port testing
         * function to attach to the list
         */
        PortToFunc(rtn, (rtn->flags & ANY_DST_PORT ? 1 : 0),
                   (rtn->flags & EXCEPT_DST_PORT ? 1 : 0), DST);

        /* as above */
        PortToFunc(rtn, (rtn->flags & ANY_SRC_PORT ? 1 : 0),
                   (rtn->flags & EXCEPT_SRC_PORT ? 1 : 0), SRC);

        /* link in the proper IP address detection function */
        AddrToFunc(rtn, SRC);

        /* last verse, same as the first (but for dest IP) ;) */
        AddrToFunc(rtn, DST);
    }

    /* tack the end (success) function to the list */
    /* snort The callback function is managed in a chain way, and each function can succeed only when it returns 1. Therefore, a successful end function is set at the end of the list to end the rule matching, which is cleverly designed*/
    AddRuleFuncToList(RuleListEnd, rtn);
}

/* The end function returns 1 directly without doing anything. It checks whether the rule header matches successfully and directly determines whether the return value is 1 */
int RuleListEnd(Packet *p, struct _RuleTreeNode *rtn_idx,
        RuleFpList *fp_list, int check_ports)
{
    return 1;
}

/* insert rfunc matching callback function to the end of linked list*/
void AddRuleFuncToList(int (*rfunc) (Packet *, struct _RuleTreeNode *, struct _RuleFpList *, int), RuleTreeNode * rtn)
{
    RuleFpList *idx;

    DEBUG_WRAP(DebugMessage(DEBUG_CONFIGRULES,"Adding new rule to list\n"););

    idx = rtn->rule_func;
    if(idx == NULL)
    {
        rtn->rule_func = (RuleFpList *)SnortAlloc(sizeof(RuleFpList));

        rtn->rule_func->RuleHeadFunc = rfunc;
    }
    else
    {
        while(idx->next != NULL)
            idx = idx->next;

        idx->next = (RuleFpList *)SnortAlloc(sizeof(RuleFpList));
        idx = idx->next;
        idx->RuleHeadFunc = rfunc;
    }
}

 

The source code analysis of rule header analysis is finished here.

Take a look at how to call the callback function to match

/*
 *Rule header detection function
 * rtn :  Rule head
 * p : data packet
 * check_ports : Detection mark 
 */
int fpEvalRTN(RuleTreeNode *rtn, Packet *p, int check_ports)
{

...
    /* Failed to determine whether the return value is 1, not 1*/
    if(!rtn->rule_func->RuleHeadFunc(p, rtn, rtn->rule_func, check_ports))
    {
        DEBUG_WRAP(DebugMessage(DEBUG_DETECT,
                    "   => Header check failed, checking next node\n"););
        DEBUG_WRAP(DebugMessage(DEBUG_DETECT,
                    "   => returned from next node check\n"););
        PREPROC_PROFILE_END(ruleRTNEvalPerfStats);
        return 0;
    }

...
    /* Successfully returned 1*/
    return 1;

}

 

Posted on Mon, 02 Dec 2019 14:28:42 -0500 by donkeychoker