Coverage Report

Created: 2026-01-02 06:13

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/wireshark/epan/dissectors/packet-snort-config.c
Line
Count
Source
1
/* packet-snort-config.c
2
 *
3
 * Copyright 2016, Martin Mathieson
4
 *
5
 * Wireshark - Network traffic analyzer
6
 * By Gerald Combs <gerald@wireshark.org>
7
 * Copyright 1998 Gerald Combs
8
 *
9
 * SPDX-License-Identifier: GPL-2.0-or-later
10
 */
11
12
0
#define WS_LOG_DOMAIN "packet-snort-config"
13
#include "config.h"
14
#include <wireshark.h>
15
16
#include <stdlib.h>
17
#include <string.h>
18
19
#include <wsutil/file_util.h>
20
#include <wsutil/strtoi.h>
21
#include <wsutil/report_message.h>
22
23
#include "packet-snort-config.h"
24
#include "ws_attributes.h"
25
26
/* Forward declaration */
27
static void parse_config_file(SnortConfig_t *snort_config, FILE *config_file_fd, const char *filename, const char *dirname, int recursion_level);
28
29
/* Skip white space from 'source', return pointer to first non-whitespace char */
30
static const char *skipWhiteSpace(const char *source, int *accumulated_offset)
31
0
{
32
0
    int offset = 0;
33
34
    /* Skip any leading whitespace */
35
0
    while ((source[offset] == ' ') || (source[offset] == '\t')) {
36
0
        offset++;
37
0
    }
38
39
0
    *accumulated_offset += offset;
40
0
    return source + offset;
41
0
}
42
43
/* Read a token from source, stop when get to end of string or delimiter. */
44
/* - source: input string
45
 * - delimiter:  char to stop at
46
 * - length: out param set to delimiter or end-of-string offset
47
 * - accumulated_Length: out param that gets length added to it
48
 * - copy: whether or an allocated string should be returned
49
 * - returns: requested string.  Returns from static buffer when copy is false */
50
static char* read_token(const char* source, char delimeter, int *length, int *accumulated_length, bool copy)
51
0
{
52
0
    static char static_buffer[1024];
53
0
    int offset = 0;
54
55
0
    const char *source_proper = skipWhiteSpace(source, accumulated_length);
56
57
0
    while (source_proper[offset] != '\0' && source_proper[offset] != delimeter) {
58
0
        offset++;
59
0
    }
60
61
0
    *length = offset;
62
0
    *accumulated_length += offset;
63
0
    if (copy) {
64
        /* Copy into new string */
65
0
        char *new_string = g_strndup(source_proper, offset+1);
66
0
        new_string[offset] = '\0';
67
0
        return new_string;
68
0
    }
69
0
    else {
70
        /* Return in static buffer */
71
0
        memcpy(&static_buffer, source_proper, offset);
72
0
        static_buffer[offset] = '\0';
73
0
        return static_buffer;
74
0
    }
75
0
}
76
77
/* Add a new content field to the rule */
78
static bool rule_add_content(Rule_t *rule, const char *content_string, bool negated)
79
0
{
80
0
    if (rule->number_contents < MAX_CONTENT_ENTRIES) {
81
0
        content_t *new_content = &(rule->contents[rule->number_contents++]);
82
0
        new_content->str = g_strdup(content_string);
83
0
        new_content->negation = negated;
84
0
        rule->last_added_content = new_content;
85
0
        return true;
86
0
    }
87
0
    return false;
88
0
}
89
90
/* Set the nocase property for a rule */
91
static void rule_set_content_nocase(Rule_t *rule)
92
0
{
93
0
    if (rule->last_added_content) {
94
0
        rule->last_added_content->nocase = true;
95
0
    }
96
0
}
97
98
/* Set the offset property of a content field */
99
static void rule_set_content_offset(Rule_t *rule, int value)
100
0
{
101
0
    if (rule->last_added_content) {
102
0
        rule->last_added_content->offset = value;
103
0
        rule->last_added_content->offset_set = true;
104
0
    }
105
0
}
106
107
/* Set the depth property of a content field */
108
static void rule_set_content_depth(Rule_t *rule, unsigned value)
109
0
{
110
0
    if (rule->last_added_content) {
111
0
        rule->last_added_content->depth = value;
112
0
    }
113
0
}
114
115
/* Set the distance property of a content field */
116
static void rule_set_content_distance(Rule_t *rule, int value)
117
0
{
118
0
    if (rule->last_added_content) {
119
0
        rule->last_added_content->distance = value;
120
0
        rule->last_added_content->distance_set = true;
121
0
    }
122
0
}
123
124
/* Set the distance property of a content field */
125
static void rule_set_content_within(Rule_t *rule, unsigned value)
126
0
{
127
0
    if (rule->last_added_content) {
128
        /* Assuming won't be 0... */
129
0
        rule->last_added_content->within = value;
130
0
    }
131
0
}
132
133
/* Set the fastpattern property of a content field */
134
static void rule_set_content_fast_pattern(Rule_t *rule)
135
0
{
136
0
    if (rule->last_added_content) {
137
0
        rule->last_added_content->fastpattern = true;
138
0
    }
139
0
}
140
141
/* Set the rawbytes property of a content field */
142
static void rule_set_content_rawbytes(Rule_t *rule)
143
0
{
144
0
    if (rule->last_added_content) {
145
0
        rule->last_added_content->rawbytes = true;
146
0
    }
147
0
}
148
149
/* Set the http_method property of a content field */
150
static void rule_set_content_http_method(Rule_t *rule)
151
0
{
152
0
    if (rule->last_added_content) {
153
0
        rule->last_added_content->http_method = true;
154
0
    }
155
0
}
156
157
/* Set the http_client property of a content field */
158
static void rule_set_content_http_client_body(Rule_t *rule)
159
0
{
160
0
    if (rule->last_added_content) {
161
0
        rule->last_added_content->http_client_body = true;
162
0
    }
163
0
}
164
165
/* Set the http_cookie property of a content field */
166
static void rule_set_content_http_cookie(Rule_t *rule)
167
0
{
168
0
    if (rule->last_added_content) {
169
0
        rule->last_added_content->http_cookie = true;
170
0
    }
171
0
}
172
173
/* Set the http_UserAgent property of a content field */
174
static void rule_set_content_http_user_agent(Rule_t *rule)
175
0
{
176
0
    if (rule->last_added_content) {
177
0
        rule->last_added_content->http_user_agent = true;
178
0
    }
179
0
}
180
181
/* Add a uricontent field to the rule */
182
static bool rule_add_uricontent(Rule_t *rule, const char *uricontent_string, bool negated)
183
0
{
184
0
    if (rule_add_content(rule, uricontent_string, negated)) {
185
0
        rule->last_added_content->content_type = UriContent;
186
0
        return true;
187
0
    }
188
0
    return false;
189
0
}
190
191
/* This content field now becomes a uricontent after seeing modifier  */
192
static void rule_set_http_uri(Rule_t *rule)
193
0
{
194
0
    if (rule->last_added_content != NULL) {
195
0
        rule->last_added_content->content_type = UriContent;
196
0
    }
197
0
}
198
199
/* Add a pcre field to the rule */
200
static bool rule_add_pcre(Rule_t *rule, const char *pcre_string)
201
0
{
202
0
    if (rule_add_content(rule, pcre_string, false)) {
203
0
        rule->last_added_content->content_type = Pcre;
204
0
        return true;
205
0
    }
206
0
    return false;
207
0
}
208
209
/* Set the rule's classtype field */
210
static bool rule_set_classtype(Rule_t *rule, const char *classtype)
211
0
{
212
0
    rule->classtype = g_strdup(classtype);
213
0
    return true;
214
0
}
215
216
/* Add a reference string to the rule */
217
static void rule_add_reference(Rule_t *rule, const char *reference_string)
218
0
{
219
0
    if (rule->number_references < MAX_REFERENCE_ENTRIES) {
220
0
        rule->references[rule->number_references++] = g_strdup(reference_string);
221
0
    }
222
0
}
223
224
/* Check to see if the ip 'field' corresponds to an entry in the ipvar dictionary.
225
 * If it is add entry to rule */
226
static void rule_check_ip_vars(SnortConfig_t *snort_config, Rule_t *rule, char *field)
227
0
{
228
0
    void *original_key = NULL;
229
0
    void *value = NULL;
230
231
    /* Make sure field+1 not NULL. */
232
0
    if (strlen(field) < 2) {
233
0
        return;
234
0
    }
235
236
    /* Make sure there is room for another entry */
237
0
    if (rule->relevant_vars.num_ip_vars >= MAX_RULE_IP_VARS) {
238
0
        return;
239
0
    }
240
241
    /* TODO: a loop re-looking up the answer until its not just another ipvar! */
242
0
    if (g_hash_table_lookup_extended(snort_config->ipvars, field+1, &original_key, &value)) {
243
244
0
        rule->relevant_vars.ip_vars[rule->relevant_vars.num_ip_vars].name = (char*)original_key;
245
0
        rule->relevant_vars.ip_vars[rule->relevant_vars.num_ip_vars].value = (char*)value;
246
247
0
        rule->relevant_vars.num_ip_vars++;
248
0
    }
249
0
}
250
251
/* Check to see if the port 'field' corresponds to an entry in the portvar dictionary.
252
 * If it is add entry to rule */
253
static void rule_check_port_vars(SnortConfig_t *snort_config, Rule_t *rule, char *field)
254
0
{
255
0
    void *original_key = NULL;
256
0
    void *value = NULL;
257
258
    /* Make sure field+1 not NULL. */
259
0
    if (strlen(field) < 2) {
260
0
        return;
261
0
    }
262
263
    /* Make sure there is room for another entry */
264
0
    if (rule->relevant_vars.num_port_vars >= MAX_RULE_PORT_VARS) {
265
0
        return;
266
0
    }
267
268
    /* TODO: a loop re-looking up the answer until its not just another portvar! */
269
0
    if (g_hash_table_lookup_extended(snort_config->portvars, field+1, &original_key, &value)) {
270
0
        rule->relevant_vars.port_vars[rule->relevant_vars.num_port_vars].name = (char*)original_key;
271
0
        rule->relevant_vars.port_vars[rule->relevant_vars.num_port_vars].value = (char*)value;
272
273
0
        rule->relevant_vars.num_port_vars++;
274
0
    }
275
0
}
276
277
/* Look over the IP addresses and ports, and work out which variables/values are being used */
278
void rule_set_relevant_vars(SnortConfig_t *snort_config, Rule_t *rule)
279
0
{
280
0
    int length;
281
0
    int accumulated_length = 0;
282
0
    char *field;
283
284
    /* No need to do this twice */
285
0
    if (rule->relevant_vars.relevant_vars_set) {
286
0
        return;
287
0
    }
288
289
    /* Walk tokens up to the options, and look up ones that are addresses or ports. */
290
291
    /* Skip "alert" */
292
0
    read_token(rule->rule_string+accumulated_length, ' ', &length, &accumulated_length, false);
293
294
    /* Skip protocol. */
295
0
    read_token(rule->rule_string+accumulated_length, ' ', &length, &accumulated_length, false);
296
297
    /* Read source address */
298
0
    field = read_token(rule->rule_string+accumulated_length, ' ', &length, &accumulated_length, false);
299
0
    rule_check_ip_vars(snort_config, rule, field);
300
301
    /* Read source port */
302
0
    field = read_token(rule->rule_string+accumulated_length, ' ', &length, &accumulated_length, false);
303
0
    rule_check_port_vars(snort_config, rule, field);
304
305
    /* Read direction */
306
0
    read_token(rule->rule_string+accumulated_length, ' ', &length, &accumulated_length, false);
307
308
    /* Dest address */
309
0
    field = read_token(rule->rule_string+accumulated_length, ' ', &length, &accumulated_length, false);
310
0
    rule_check_ip_vars(snort_config, rule, field);
311
312
    /* Dest port */
313
0
    field = read_token(rule->rule_string+accumulated_length, ' ', &length, &accumulated_length, false);
314
0
    rule_check_port_vars(snort_config, rule, field);
315
316
    /* Set flag so won't do again for this rule */
317
0
    rule->relevant_vars.relevant_vars_set = true;
318
0
}
319
320
321
typedef enum vartype_e { var, ipvar, portvar, unknownvar } vartype_e;
322
323
/* Look for a "var", "ipvar" or "portvar" entry in this line */
324
static bool parse_variables_line(SnortConfig_t *snort_config, const char *line)
325
0
{
326
0
    vartype_e var_type = unknownvar;
327
328
0
    const char*  variable_type;
329
0
    char *  variable_name;
330
0
    char *  value;
331
332
0
    int length;
333
0
    int accumulated_length = 0;
334
335
    /* Get variable type */
336
0
    variable_type = read_token(line, ' ', &length, &accumulated_length, false);
337
0
    if (variable_type == NULL) {
338
0
        return false;
339
0
    }
340
341
0
    if (strncmp(variable_type, "var", 3) == 0) {
342
0
        var_type = var;
343
0
    }
344
0
    else if (strncmp(variable_type, "ipvar", 5) == 0) {
345
0
        var_type = ipvar;
346
0
    }
347
0
    else if (strncmp(variable_type, "portvar", 7) == 0) {
348
0
        var_type = portvar;
349
0
    }
350
0
    else {
351
0
        return false;
352
0
    }
353
354
    /* Get variable name */
355
0
    variable_name = read_token(line+ accumulated_length, ' ', &length, &accumulated_length, true);
356
0
    if (variable_name == NULL) {
357
0
        return false;
358
0
    }
359
360
    /* Now value */
361
0
    value = read_token(line + accumulated_length, ' ', &length, &accumulated_length, true);
362
0
    if (value == NULL) {
363
0
        return false;
364
0
    }
365
366
    /* Add (name->value) to table according to variable type. */
367
0
    switch (var_type) {
368
0
        case var:
369
0
            if (strcmp(variable_name, "RULE_PATH") == 0) {
370
                /* This can be relative or absolute. */
371
0
                snort_config->rule_path = value;
372
0
                snort_config->rule_path_is_absolute = g_path_is_absolute(value);
373
0
                ws_debug("rule_path set to %s (is_absolute=%d)",
374
0
                                   snort_config->rule_path, snort_config->rule_path_is_absolute);
375
0
            }
376
0
            g_hash_table_insert(snort_config->vars, variable_name, value);
377
0
            break;
378
0
        case ipvar:
379
0
            g_hash_table_insert(snort_config->ipvars, variable_name, value);
380
0
            break;
381
0
        case portvar:
382
0
            g_hash_table_insert(snort_config->portvars, variable_name, value);
383
0
            break;
384
385
0
        default:
386
0
            return false;
387
0
    }
388
389
0
    return false;
390
0
}
391
392
/* Hash function for where key is a string. Just add up the value of each character and return that.. */
393
static unsigned string_hash(const void *key)
394
0
{
395
0
    unsigned total=0, n=0;
396
0
    const char *key_string = (const char *)key;
397
0
    char c = key_string[n];
398
399
0
    while (c != '\0') {
400
0
        total += (int)c;
401
0
        c = key_string[++n];
402
0
    }
403
0
    return total;
404
0
}
405
406
/* Comparison function for where key is a string. Simple comparison using strcmp() */
407
static gboolean string_equal(const void *a, const void *b)
408
0
{
409
0
    const char *stringa = (const char*)a;
410
0
    const char *stringb = (const char*)b;
411
412
0
    return (strcmp(stringa, stringb) == 0);
413
0
}
414
415
/* Process a line that configures a reference line (invariably from 'reference.config') */
416
static bool parse_references_prefix_file_line(SnortConfig_t *snort_config, const char *line)
417
0
{
418
0
    char *prefix_name, *prefix_value;
419
0
    int length=0, accumulated_length=0;
420
0
    int n;
421
422
0
    if (strncmp(line, "config reference: ", 18) != 0) {
423
0
        return false;
424
0
    }
425
426
    /* Read the prefix and value */
427
0
    const char *source = line+18;
428
0
    prefix_name = read_token(source, ' ', &length, &accumulated_length, true);
429
    /* Store all name chars in lower case. */
430
0
    for (n=0; prefix_name[n] != '\0'; n++) {
431
0
        prefix_name[n] = g_ascii_tolower(prefix_name[n]);
432
0
    }
433
434
0
    prefix_value = read_token(source+accumulated_length, ' ', &length, &accumulated_length, true);
435
436
    /* Add entry into table */
437
0
    g_hash_table_insert(snort_config->references_prefixes, prefix_name, prefix_value);
438
439
0
    return false;
440
0
}
441
442
/* Try to expand the reference using the prefixes stored in the config */
443
char *expand_reference(SnortConfig_t *snort_config, char *reference)
444
0
{
445
0
    static char expanded_reference[512];
446
0
    int length = (int)strlen(reference);
447
0
    int accumulated_length = 0;
448
449
    /* Extract up to ',', then substitute prefix! */
450
0
    ws_debug("expand_reference(%s)", reference);
451
0
    char *prefix = read_token(reference, ',', &length, &accumulated_length, false);
452
453
0
    if (*prefix != '\0') {
454
        /* Convert to lowercase before lookup */
455
0
        unsigned n;
456
0
        for (n=0; prefix[n] != '\0'; n++) {
457
0
            prefix[n] = g_ascii_tolower(prefix[n]);
458
0
        }
459
460
        /* Look up prefix in table. */
461
0
        const char* prefix_replacement;
462
0
        prefix_replacement = (char*)g_hash_table_lookup(snort_config->references_prefixes, prefix);
463
464
        /* Append prefix and remainder, and return!!!! */
465
0
        if (prefix_replacement) {
466
0
            snprintf(expanded_reference, 512, "%s%s", prefix_replacement, reference+length+1);
467
0
            return expanded_reference;
468
0
        }
469
0
        else {
470
            /* Just return the original reference */
471
0
            return reference;
472
0
        }
473
474
0
    }
475
0
    return "ERROR: Reference didn't contain prefix and ','!";
476
0
}
477
478
/* The rule has been matched with an alert, so update global config stats */
479
void rule_set_alert(SnortConfig_t *snort_config, Rule_t *rule,
480
                    unsigned *global_match_number,
481
                    unsigned *rule_match_number)
482
0
{
483
0
    snort_config->stat_alerts_detected++;
484
0
    *global_match_number = snort_config->stat_alerts_detected;
485
0
    if (rule != NULL) {
486
0
        *rule_match_number = ++rule->matches_seen;
487
0
    }
488
0
}
489
490
491
492
/* Delete an individual entry from a string table. */
493
static gboolean delete_string_entry(void *key,
494
                                    void *value,
495
                                    void *user_data _U_)
496
0
{
497
0
    char *key_string = (char*)key;
498
0
    char *value_string = (char*)value;
499
500
0
    g_free(key_string);
501
0
    g_free(value_string);
502
503
0
    return TRUE;
504
0
}
505
506
/* See if this is an include line, if it is open the file and call parse_config_file() */
507
// NOLINTNEXTLINE(misc-no-recursion)
508
static bool parse_include_file(SnortConfig_t *snort_config, const char *line, const char *config_directory, int recursion_level)
509
0
{
510
0
    int length;
511
0
    int accumulated_length = 0;
512
0
    char *include_filename;
513
514
    /* Look for "include " */
515
0
    const char *include_token = read_token(line, ' ', &length, &accumulated_length, false);
516
0
    if (strlen(include_token) == 0) {
517
0
        return false;
518
0
    }
519
0
    if (strncmp(include_token, "include", 7) != 0) {
520
0
        return false;
521
0
    }
522
523
    /* Read the filename */
524
0
    include_filename = read_token(line+accumulated_length, ' ', &length, &accumulated_length, false);
525
0
    if (*include_filename != '\0') {
526
0
        FILE *new_config_fd;
527
0
        char *substituted_filename;
528
0
        bool is_rule_file = false;
529
530
        /* May need to substitute variables into include path. */
531
0
        if (strncmp(include_filename, "$RULE_PATH", 10) == 0) {
532
            /* Write rule path variable value */
533
            /* Don't assume $RULE_PATH will end in a file separator */
534
0
            if (snort_config->rule_path_is_absolute) {
535
                /* Rule path is absolute, so it can go at start */
536
0
                substituted_filename = g_build_path(G_DIR_SEPARATOR_S,
537
0
                           snort_config->rule_path,
538
0
                           include_filename + 11,
539
0
                           NULL);
540
0
            }
541
0
            else {
542
                /* Rule path is relative to config directory, so it goes first */
543
0
                substituted_filename = g_build_path(G_DIR_SEPARATOR_S,
544
0
                           config_directory,
545
0
                           snort_config->rule_path,
546
0
                           include_filename + 11,
547
0
                           NULL);
548
0
            }
549
0
            is_rule_file = true;
550
0
        }
551
0
        else {
552
            /* No $RULE_PATH, just use directory and filename */
553
            /* But may not even need directory if included_folder is absolute! */
554
0
            if (!g_path_is_absolute(include_filename)) {
555
0
                substituted_filename = g_build_path(G_DIR_SEPARATOR_S,
556
0
                            config_directory, include_filename, NULL);
557
0
            }
558
0
            else {
559
0
                substituted_filename = g_strdup(include_filename);
560
0
            }
561
0
        }
562
563
        /* Try to open the file. */
564
0
        new_config_fd = ws_fopen(substituted_filename, "r");
565
0
        if (new_config_fd == NULL) {
566
0
            ws_debug("Failed to open config file %s", substituted_filename);
567
0
            report_failure("Snort dissector: Failed to open config file %s\n", substituted_filename);
568
0
            g_free(substituted_filename);
569
0
            return false;
570
0
        }
571
572
        /* Parse the file */
573
0
        if (is_rule_file) {
574
0
            snort_config->stat_rules_files++;
575
0
        }
576
0
        parse_config_file(snort_config, new_config_fd, substituted_filename, config_directory, recursion_level + 1);
577
0
        g_free(substituted_filename);
578
579
        /* Close the file */
580
0
        fclose(new_config_fd);
581
582
0
        return true;
583
0
    }
584
0
    return false;
585
0
}
586
587
/* Process an individual option - i.e. the elements found between '(' and ')' */
588
static void process_rule_option(Rule_t *rule, char *options, int option_start_offset, int options_end_offset, int colon_offset)
589
0
{
590
0
    static char name[1024], value[1024];
591
0
    name[0] = '\0';
592
0
    value[0] = '\0';
593
0
    int value_length = 0;
594
0
    int32_t value_i32 = 0;
595
0
    uint32_t value_u32 = 0;
596
0
    uint16_t value_u16 = 0;
597
0
    const char *endptr; // Just to ignore trailing whitespace
598
0
    int spaces_after_colon = 0;
599
600
0
    if (colon_offset != 0) {
601
        /* Name and value */
602
0
        (void) g_strlcpy(name, options+option_start_offset, colon_offset-option_start_offset);
603
0
        if (options[colon_offset] == ' ') {
604
0
            spaces_after_colon = 1;
605
0
        }
606
0
        (void) g_strlcpy(value, options+colon_offset+spaces_after_colon, options_end_offset-spaces_after_colon-colon_offset);
607
0
        value_length = (int)strlen(value);
608
0
    }
609
0
    else {
610
        /* Just name */
611
0
        (void) g_strlcpy(name, options+option_start_offset, options_end_offset-option_start_offset);
612
0
    }
613
614
    /* Think this is space at end of all options - don't compare with option names */
615
0
    if (name[0] == '\0') {
616
0
        return;
617
0
    }
618
619
    /* Process the rule options that we are interested in */
620
0
    if (strcmp(name, "msg") == 0) {
621
0
        rule->msg = g_strdup(value);
622
0
    }
623
0
    else if (strcmp(name, "sid") == 0) {
624
0
        if (!ws_strtou32(value, &endptr, &value_u32)) {
625
0
            ws_info("failed to parse %s argument", name);
626
0
            return;
627
0
        }
628
0
        rule->sid = value_u32;
629
0
    }
630
0
    else if (strcmp(name, "rev") == 0) {
631
0
        if (!ws_strtou32(value, &endptr, &value_u32)) {
632
0
            ws_info("failed to parse %s argument", name);
633
0
            return;
634
0
        }
635
0
        rule->rev = value_u32;
636
0
    }
637
0
    else if (strcmp(name, "content") == 0) {
638
0
        int value_start = 0;
639
640
0
        if (value_length < 3) {
641
0
            return;
642
0
        }
643
644
        /* Need to trim off " ", but first check for ! */
645
0
        if (value[0] == '!') {
646
0
            value_start = 1;
647
0
            if (value_length < 4) {
648
                /* i.e. also need quotes + at least one character */
649
0
                return;
650
0
            }
651
0
        }
652
653
0
        value[options_end_offset-colon_offset-spaces_after_colon-2] = '\0';
654
0
        rule_add_content(rule, value+value_start+1, value_start == 1);
655
0
    }
656
0
    else if (strcmp(name, "uricontent") == 0) {
657
0
        int value_start = 0;
658
659
0
        if (value_length < 3) {
660
0
            return;
661
0
        }
662
663
        /* Need to trim off " ", but first check for ! */
664
0
        if (value[0] == '!') {
665
0
            value_start = 1;
666
0
            if (value_length < 4) {
667
0
                return;
668
0
            }
669
0
        }
670
671
0
        value[options_end_offset-colon_offset-spaces_after_colon-2] = '\0';
672
0
        rule_add_uricontent(rule, value+value_start+1, value_start == 1);
673
0
    }
674
0
    else if (strcmp(name, "http_uri") == 0) {
675
0
        rule_set_http_uri(rule);
676
0
    }
677
0
    else if (strcmp(name, "pcre") == 0) {
678
0
        int value_start = 0;
679
680
        /* Need at least opening and closing / */
681
0
        if (value_length < 3) {
682
0
            return;
683
0
        }
684
685
        /* Not expecting negation (!)... */
686
687
0
        value[options_end_offset-colon_offset-spaces_after_colon-2] = '\0';
688
0
        rule_add_pcre(rule, value+value_start+1);
689
0
    }
690
0
    else if (strcmp(name, "nocase") == 0) {
691
0
        rule_set_content_nocase(rule);
692
0
    }
693
0
    else if (strcmp(name, "offset") == 0) {
694
        // Allows values from -65535 to 65535
695
0
        if (!ws_strtoi32(value, &endptr, &value_i32)) {
696
0
            ws_info("failed to parse %s argument", name);
697
0
            return;
698
0
        }
699
0
        rule_set_content_offset(rule, value_i32);
700
0
    }
701
0
    else if (strcmp(name, "depth") == 0) {
702
        // Max value is 65535
703
0
        if (!ws_strtou16(value, &endptr, &value_u16)) {
704
0
            ws_info("failed to parse %s argument", name);
705
0
            return;
706
0
        }
707
0
        rule_set_content_depth(rule, value_u16);
708
0
    }
709
0
    else if (strcmp(name, "within") == 0) {
710
        // Max value is 65535
711
0
        if (!ws_strtou16(value, &endptr, &value_u16)) {
712
0
            ws_info("failed to parse %s argument", name);
713
0
            return;
714
0
        }
715
0
        rule_set_content_within(rule, value_u16);
716
0
    }
717
0
    else if (strcmp(name, "distance") == 0) {
718
        // Allows values from -65535 to 65535
719
0
        if (!ws_strtoi32(value, &endptr, &value_i32)) {
720
0
            ws_info("failed to parse %s argument", name);
721
0
            return;
722
0
        }
723
0
        rule_set_content_distance(rule, value_i32);
724
0
    }
725
0
    else if (strcmp(name, "fast_pattern") == 0) {
726
0
        rule_set_content_fast_pattern(rule);
727
0
    }
728
0
    else if (strcmp(name, "http_method") == 0) {
729
0
        rule_set_content_http_method(rule);
730
0
    }
731
0
    else if (strcmp(name, "http_client_body") == 0) {
732
0
        rule_set_content_http_client_body(rule);
733
0
    }
734
0
    else if (strcmp(name, "http_cookie") == 0) {
735
0
        rule_set_content_http_cookie(rule);
736
0
    }
737
0
    else if (strcmp(name, "http_user_agent") == 0) {
738
0
        rule_set_content_http_user_agent(rule);
739
0
    }
740
0
    else if (strcmp(name, "rawbytes") == 0) {
741
0
        rule_set_content_rawbytes(rule);
742
0
    }
743
0
    else if (strcmp(name, "classtype") == 0) {
744
0
        rule_set_classtype(rule, value);
745
0
    }
746
0
    else if (strcmp(name, "reference") == 0) {
747
0
        rule_add_reference(rule, value);
748
0
    }
749
0
    else {
750
        /* Ignore an option we don't currently handle */
751
0
    }
752
0
}
753
754
/* Parse a Snort alert, return true if successful */
755
static bool parse_rule(SnortConfig_t *snort_config, char *line, const char *filename, int line_number, int line_length)
756
0
{
757
0
    const char* options_start;
758
0
    char *options;
759
0
    bool in_quotes = false;
760
0
    int options_start_index = 0, options_index = 0, colon_offset = 0;
761
0
    char c;
762
0
    int length = 0; /*  CID 1398227 (bogus - read_token() always sets it) */
763
0
    Rule_t *rule = NULL;
764
765
    /* Rule will begin with alert */
766
0
    if (strncmp(line, "alert ", 6) != 0) {
767
0
        return false;
768
0
    }
769
770
    /* Allocate the rule itself */
771
0
    rule = g_new(Rule_t, 1);
772
773
0
    ws_debug("looks like a rule: %s", line);
774
0
    memset(rule, 0, sizeof(Rule_t));
775
776
0
    rule->rule_string = g_strdup(line);
777
0
    rule->file = g_strdup(filename);
778
0
    rule->line_number = line_number;
779
780
    /* Next token is the protocol */
781
0
    rule->protocol = read_token(line+6, ' ', &length, &length, true);
782
783
    /* Find start of options. */
784
0
    options_start = strstr(line, "(");
785
0
    if (options_start == NULL) {
786
0
        ws_debug("start of options not found");
787
0
        g_free(rule);
788
0
        return false;
789
0
    }
790
0
    options_index = (int)(options_start-line) + 1;
791
792
    /* To make parsing simpler, replace final ')' with ';' */
793
0
    if (line[line_length-1] != ')') {
794
0
        g_free(rule);
795
0
        return false;
796
0
    }
797
0
    else {
798
0
        line[line_length-1] = ';';
799
0
    }
800
801
    /* Skip any spaces before next option */
802
0
    while (line[options_index] == ' ') options_index++;
803
804
    /* Now look for next ';', process one option at a time */
805
0
    options = &line[options_index];
806
0
    options_index = 0;
807
808
0
    while ((c = options[options_index++])) {
809
        /* Keep track of whether inside quotes */
810
0
        if (c == '"') {
811
0
            in_quotes = !in_quotes;
812
0
        }
813
        /* Ignore ';' while inside quotes */
814
0
        if (!in_quotes) {
815
0
            if (c == ':') {
816
0
                colon_offset = options_index;
817
0
            }
818
0
            if (c == ';') {
819
                /* End of current option - add to rule. */
820
0
                process_rule_option(rule, options, options_start_index, options_index, colon_offset);
821
822
                /* Skip any spaces before next option */
823
0
                while (options[options_index] == ' ') options_index++;
824
825
                /* Next rule will start here */
826
0
                options_start_index = options_index;
827
0
                colon_offset = 0;
828
0
                in_quotes = false;
829
0
            }
830
0
        }
831
0
    }
832
833
    /* Add rule to map of rules. */
834
0
    g_hash_table_insert(snort_config->rules, GUINT_TO_POINTER((unsigned)rule->sid), rule);
835
0
    ws_debug("Snort rule with SID=%u added to table", rule->sid);
836
837
0
    return true;
838
0
}
839
840
/* Delete an individual rule */
841
static gboolean delete_rule(void *    key _U_,
842
                            void *    value,
843
                            void *    user_data _U_)
844
0
{
845
0
    Rule_t *rule = (Rule_t*)value;
846
0
    unsigned int n;
847
848
    /* Delete strings on heap. */
849
0
    g_free(rule->rule_string);
850
0
    g_free(rule->file);
851
0
    g_free(rule->msg);
852
0
    g_free(rule->classtype);
853
0
    g_free(rule->protocol);
854
855
0
    for (n=0; n < rule->number_contents; n++) {
856
0
        g_free(rule->contents[n].str);
857
0
        g_free(rule->contents[n].translated_str);
858
0
    }
859
860
0
    for (n=0; n < rule->number_references; n++) {
861
0
        g_free(rule->references[n]);
862
0
    }
863
864
0
    ws_debug("Freeing rule at :%p", rule);
865
0
    g_free(rule);
866
0
    return TRUE;
867
0
}
868
869
870
/* Parse this file, adding details to snort_config. */
871
/* N.B. using recursion_level to limit stack depth. */
872
0
#define MAX_CONFIG_FILE_RECURSE_DEPTH 8
873
// NOLINTNEXTLINE(misc-no-recursion)
874
static void parse_config_file(SnortConfig_t *snort_config, FILE *config_file_fd,
875
                              const char *filename, const char *dirname, int recursion_level)
876
0
{
877
0
    #define MAX_LINE_LENGTH 4096
878
0
    char line[MAX_LINE_LENGTH];
879
0
    int  line_number = 0;
880
881
0
    ws_debug("parse_config_file(filename=%s, recursion_level=%d)", filename, recursion_level);
882
883
0
    if (recursion_level > MAX_CONFIG_FILE_RECURSE_DEPTH) {
884
0
        return;
885
0
    }
886
887
    /* Read each line of the file in turn, and see if we want any info from it. */
888
0
    while (fgets(line, MAX_LINE_LENGTH, config_file_fd)) {
889
890
0
        int line_length;
891
0
        ++line_number;
892
893
        /* Nothing interesting to parse */
894
0
        if ((line[0] == '\0') || (line[0] == '#')) {
895
0
            continue;
896
0
        }
897
898
        /* Trim newline from end */
899
0
        line_length = (int)strlen(line);
900
0
        while (line_length && ((line[line_length - 1] == '\n') || (line[line_length - 1] == '\r'))) {
901
0
            --line_length;
902
0
        }
903
0
        line[line_length] = '\0';
904
0
        if (line_length == 0) {
905
0
            continue;
906
0
        }
907
908
        /* Offer line to the various parsing functions.  Could optimise order.. */
909
0
        if (parse_variables_line(snort_config, line)) {
910
0
            continue;
911
0
        }
912
0
        if (parse_references_prefix_file_line(snort_config, line)) {
913
0
            continue;
914
0
        }
915
0
        if (parse_include_file(snort_config, line, dirname, recursion_level)) {
916
0
            continue;
917
0
        }
918
0
        if (parse_rule(snort_config, line, filename, line_number, line_length)) {
919
0
            snort_config->stat_rules++;
920
0
            continue;
921
0
        }
922
0
    }
923
0
}
924
925
926
927
/* Create the global ConfigParser */
928
void create_config(SnortConfig_t **snort_config, const char *snort_config_file)
929
0
{
930
0
    char* dirname;
931
0
    char* basename;
932
0
    FILE *config_file_fd;
933
934
0
    ws_debug("create_config (%s)", snort_config_file);
935
936
0
    *snort_config = g_new(SnortConfig_t, 1);
937
0
    memset(*snort_config, 0, sizeof(SnortConfig_t));
938
939
    /* Create rule table */
940
0
    (*snort_config)->rules = g_hash_table_new(g_direct_hash, g_direct_equal);
941
942
    /* Create reference prefix table */
943
0
    (*snort_config)->references_prefixes = g_hash_table_new(string_hash, string_equal);
944
945
    /* Vars tables */
946
0
    (*snort_config)->vars = g_hash_table_new(string_hash, string_equal);
947
0
    (*snort_config)->ipvars = g_hash_table_new(string_hash, string_equal);
948
0
    (*snort_config)->portvars = g_hash_table_new(string_hash, string_equal);
949
950
    /* Extract separate directory and filename. */
951
0
    dirname =  g_path_get_dirname(snort_config_file);
952
0
    basename =  g_path_get_basename(snort_config_file);
953
954
    /* Attempt to open the config file */
955
0
    config_file_fd = ws_fopen(snort_config_file, "r");
956
0
    if (config_file_fd == NULL) {
957
0
        ws_debug("Failed to open config file %s", snort_config_file);
958
0
        report_failure("Snort dissector: Failed to open config file %s\n", snort_config_file);
959
0
    }
960
0
    else {
961
        /* Start parsing from the top-level config file. */
962
0
        parse_config_file(*snort_config, config_file_fd, snort_config_file, dirname, 1 /* recursion level */);
963
0
        fclose(config_file_fd);
964
0
    }
965
966
0
    g_free(dirname);
967
0
    g_free(basename);
968
0
}
969
970
971
/* Delete the entire config */
972
void delete_config(SnortConfig_t **snort_config)
973
0
{
974
0
    ws_debug("delete_config()");
975
976
    /* Iterate over all rules, freeing each one! */
977
0
    g_hash_table_foreach_remove((*snort_config)->rules, delete_rule, NULL);
978
0
    g_hash_table_destroy((*snort_config)->rules);
979
980
    /* References table */
981
0
    g_hash_table_foreach_remove((*snort_config)->references_prefixes, delete_string_entry, NULL);
982
0
    g_hash_table_destroy((*snort_config)->references_prefixes);
983
984
    /* Free up variable tables */
985
0
    g_hash_table_foreach_remove((*snort_config)->vars, delete_string_entry, NULL);
986
0
    g_hash_table_destroy((*snort_config)->vars);
987
0
    g_hash_table_foreach_remove((*snort_config)->ipvars, delete_string_entry, NULL);
988
0
    g_hash_table_destroy((*snort_config)->ipvars);
989
0
    g_hash_table_foreach_remove((*snort_config)->portvars, delete_string_entry, NULL);
990
0
    g_hash_table_destroy((*snort_config)->portvars);
991
992
0
    g_free(*snort_config);
993
994
0
    *snort_config = NULL;
995
0
}
996
997
/* Look for a rule corresponding to the given SID */
998
Rule_t *get_rule(SnortConfig_t *snort_config, uint32_t sid)
999
0
{
1000
0
    if ((snort_config == NULL) || (snort_config->rules == NULL)) {
1001
0
        return NULL;
1002
0
    }
1003
0
    else {
1004
0
        return (Rule_t*)g_hash_table_lookup(snort_config->rules, GUINT_TO_POINTER(sid));
1005
0
    }
1006
0
}
1007
1008
/* Fetch some statistics. */
1009
void get_global_rule_stats(SnortConfig_t *snort_config, unsigned int sid,
1010
                           unsigned int *number_rules_files, unsigned int *number_rules,
1011
                           unsigned int *alerts_detected, unsigned int *this_rule_alerts_detected)
1012
0
{
1013
0
    *number_rules_files = snort_config->stat_rules_files;
1014
0
    *number_rules = snort_config->stat_rules;
1015
0
    *alerts_detected = snort_config->stat_alerts_detected;
1016
0
    const Rule_t *rule;
1017
1018
    /* Look up rule and get current/total matches */
1019
0
    rule = get_rule(snort_config, sid);
1020
0
    if (rule) {
1021
0
        *this_rule_alerts_detected = rule->matches_seen;
1022
0
    }
1023
0
    else {
1024
0
        *this_rule_alerts_detected = 0;
1025
0
    }
1026
0
}
1027
1028
/* Reset stats on individual rule */
1029
static void reset_rule_stats(void *    key _U_,
1030
                             void *    value,
1031
                             void *    user_data _U_)
1032
0
{
1033
0
    Rule_t *rule = (Rule_t*)value;
1034
0
    rule->matches_seen = 0;
1035
0
}
1036
1037
/* Reset stats on all rules */
1038
void reset_global_rule_stats(SnortConfig_t *snort_config)
1039
0
{
1040
    /* Reset global stats */
1041
0
    if (snort_config == NULL) {
1042
0
        return;
1043
0
    }
1044
0
    snort_config->stat_alerts_detected = 0;
1045
1046
    /* Iterate over all rules, resetting the stats of each */
1047
0
    g_hash_table_foreach(snort_config->rules, reset_rule_stats, NULL);
1048
0
}
1049
1050
1051
/*************************************************************************************/
1052
/* Dealing with content fields and trying to find where it matches within the packet */
1053
/* Parse content strings to interpret binary and escaped characters. Do this         */
1054
/* so we can look for in frame using memcmp().                                       */
1055
static unsigned char content_get_nibble_value(char c)
1056
0
{
1057
0
    static unsigned char values[256];
1058
0
    static bool values_set = false;
1059
1060
0
    if (!values_set) {
1061
        /* Set table once and for all */
1062
0
        unsigned ch;
1063
0
        for (ch='a'; ch <= 'f'; ch++) {
1064
0
            values[ch] = 0xa + (ch-'a');
1065
0
        }
1066
0
        for (ch='A'; ch <= 'F'; ch++) {
1067
0
            values[ch] = 0xa + (ch-'A');
1068
0
        }
1069
0
        for (ch='0'; ch <= '9'; ch++) {
1070
0
            values[ch] = (ch-'0');
1071
0
        }
1072
0
        values_set = true;
1073
0
    }
1074
1075
0
    return values[(unsigned char)c];
1076
0
}
1077
1078
/* Go through string, converting hex digits into uint8_t, and removing escape characters. */
1079
unsigned content_convert_to_binary(content_t *content)
1080
0
{
1081
0
    int output_idx = 0;
1082
0
    bool in_binary_mode = false;    /* Are we in a binary region of the string?   */
1083
0
    bool have_one_nibble = false;   /* Do we have the first nibble of the pair needed to make a byte? */
1084
0
    unsigned char one_nibble = 0;       /* Value of first nibble if we have it */
1085
0
    char c;
1086
0
    int n;
1087
0
    bool have_backslash = false;
1088
0
    static char binary_str[1024];
1089
1090
    /* Just return length if have previously translated in binary string. */
1091
0
    if (content->translated) {
1092
0
        return content->translated_length;
1093
0
    }
1094
1095
    /* Walk over each character, work out what needs to be written into output */
1096
0
    for (n=0; content->str[n] != '\0'; n++) {
1097
0
        c = content->str[n];
1098
0
        if (c == '|') {
1099
            /* Flip binary mode */
1100
0
            in_binary_mode = !in_binary_mode;
1101
0
            continue;
1102
0
        }
1103
1104
0
        if (!in_binary_mode) {
1105
            /* Not binary mode. Copying characters into output buffer, but watching out for escaped chars. */
1106
0
            if (!have_backslash) {
1107
0
                if (c == '\\') {
1108
                    /* Just note that we have a backslash */
1109
0
                    have_backslash = true;
1110
0
                    continue;
1111
0
                }
1112
0
                else {
1113
                    /* Just copy the character straight into output. */
1114
0
                    binary_str[output_idx++] = (unsigned char)c;
1115
0
                }
1116
0
            }
1117
0
            else {
1118
                /* Currently have a backslash. Reset flag. */
1119
0
                have_backslash = 0;
1120
                /* Just copy the character into output. Really, the only characters that should be escaped
1121
                   are ';' and  '\'  and '"' */
1122
0
                binary_str[output_idx++] = (unsigned char)c;
1123
0
            }
1124
0
        }
1125
0
        else {
1126
            /* Binary mode. Handle pairs of hex digits and translate into uint8_t */
1127
0
            if (c == ' ') {
1128
                /* Ignoring inside binary mode */
1129
0
                continue;
1130
0
            }
1131
0
            else {
1132
0
                unsigned char nibble = content_get_nibble_value(c);
1133
0
                if (!have_one_nibble) {
1134
                    /* Store first nibble of a pair */
1135
0
                    one_nibble = nibble;
1136
0
                    have_one_nibble = true;
1137
0
                }
1138
0
                else {
1139
                    /* Combine both nibbles into a byte */
1140
0
                    binary_str[output_idx++] = (one_nibble << 4) + nibble;
1141
                    /* Reset flag - looking for new pair of nibbles */
1142
0
                    have_one_nibble = false;
1143
0
                }
1144
0
            }
1145
0
        }
1146
0
    }
1147
1148
    /* Store result for next time. */
1149
0
    content->translated_str = (unsigned char*)g_malloc(output_idx+1);
1150
0
    memcpy(content->translated_str, binary_str, output_idx+1);
1151
0
    content->translated = true;
1152
0
    content->translated_length = output_idx;
1153
1154
0
    return output_idx;
1155
0
}
1156
1157
/* In order to use glib's regex library, need to trim
1158
  '/' delimiters and any modifiers from the end of the string */
1159
bool content_convert_pcre_for_regex(content_t *content)
1160
0
{
1161
0
    unsigned pcre_length, i, end_delimiter_offset = 0;
1162
1163
    /* Return if already converted */
1164
0
    if (content->translated_str) {
1165
0
        return true;
1166
0
    }
1167
1168
0
    pcre_length = (unsigned)strlen(content->str);
1169
1170
    /* Start with content->str */
1171
0
    if (pcre_length < 3) {
1172
        /* Can't be valid.  Expect /regex/[modifiers] */
1173
0
        return false;
1174
0
    }
1175
1176
0
    if (pcre_length >= 512) {
1177
        /* Have seen regex library crash on very long expressions
1178
         * (830 bytes) as seen in SID=2019326, REV=6 */
1179
0
        return false;
1180
0
    }
1181
1182
    /* Verify that string starts with / */
1183
0
    if (content->str[0] != '/') {
1184
0
        return false;
1185
0
    }
1186
1187
    /* Next, look for closing / near end of string */
1188
0
    for (i=pcre_length-1; i > 2; i--) {
1189
0
        if (content->str[i] == '/') {
1190
0
            end_delimiter_offset = i;
1191
0
            break;
1192
0
        }
1193
0
        else {
1194
0
            switch (content->str[i]) {
1195
0
                case 'i':
1196
0
                    content->pcre_case_insensitive = true;
1197
0
                    break;
1198
0
                case 's':
1199
0
                    content->pcre_dot_includes_newline = true;
1200
0
                    break;
1201
0
                case 'B':
1202
0
                    content->pcre_raw = true;
1203
0
                    break;
1204
0
                case 'm':
1205
0
                    content->pcre_multiline = true;
1206
0
                    break;
1207
1208
0
                default:
1209
                    /* TODO: handle other modifiers that will get seen? */
1210
                    /* N.B. 'U' (match in decoded URI buffers) can't be handled, so don't store in flag. */
1211
                    /* N.B. not sure if/how to handle 'R' (effectively distance:0) */
1212
0
                    ws_debug("Unhandled pcre modifier '%c'", content->str[i]);
1213
0
                    break;
1214
0
            }
1215
0
        }
1216
0
    }
1217
0
    if (end_delimiter_offset == 0) {
1218
        /* Didn't find it */
1219
0
        return false;
1220
0
    }
1221
1222
    /* Store result for next time. */
1223
0
    content->translated_str = (unsigned char*)g_malloc(end_delimiter_offset);
1224
0
    memcpy(content->translated_str, content->str+1, end_delimiter_offset - 1);
1225
0
    content->translated_str[end_delimiter_offset-1] = '\0';
1226
0
    content->translated = true;
1227
0
    content->translated_length = end_delimiter_offset - 1;
1228
1229
    return true;
1230
0
}
1231
1232
/*
1233
 * Editor modelines  -  https://www.wireshark.org/tools/modelines.html
1234
 *
1235
 * Local variables:
1236
 * c-basic-offset: 4
1237
 * tab-width: 8
1238
 * indent-tabs-mode: nil
1239
 * End:
1240
 *
1241
 * vi: set shiftwidth=4 tabstop=8 expandtab:
1242
 * :indentSize=4:tabSize=8:noTabs=true:
1243
 */