]> git.ipfire.org Git - thirdparty/snort3.git/commitdiff
Pull request #3273: US 688507: http_inspect: rule option to compare numeric header...
authorTom Peters (thopeter) <thopeter@cisco.com>
Fri, 25 Feb 2022 18:23:22 +0000 (18:23 +0000)
committerTom Peters (thopeter) <thopeter@cisco.com>
Fri, 25 Feb 2022 18:23:22 +0000 (18:23 +0000)
Merge in SNORT/snort3 from ~MDAGON/snort3:numeric2 to master

Squashed commit of the following:

commit aafe16b64d6b9620cdb8869459072f86381da7e7
Author: Maya Dagon <mdagon@cisco.com>
Date:   Mon Feb 14 15:34:50 2022 -0500

    http_inspect: http_header_test, http_trailer_test rule options

doc/user/http_inspect.txt
src/service_inspectors/http_inspect/CMakeLists.txt
src/service_inspectors/http_inspect/http_api.cc
src/service_inspectors/http_inspect/http_enum.h
src/service_inspectors/http_inspect/http_msg_section.cc
src/service_inspectors/http_inspect/ips_http_num_hdrs.cc
src/service_inspectors/http_inspect/ips_http_param.cc
src/service_inspectors/http_inspect/ips_http_test.cc [new file with mode: 0644]
src/service_inspectors/http_inspect/ips_http_test.h [new file with mode: 0644]
src/service_inspectors/http_inspect/ips_http_version.cc

index 107e26029d5f58c8f210bb2c4df397e0a8ad6b6f..4c3f6176aebd55b8c5e60ecf48fea3ce9f3b4ae9 100755 (executable)
@@ -706,6 +706,13 @@ the general HTTP/1 format but contain version fields falsely claiming to be
 HTTP/2.0 or HTTP/0.9 will match "other" as described above. The http_version
 rule option is available to examine the actual bytes in the version field.
 
+===== http_header_test and http_trailer_test
+
+Rule options that perform various tests against a specific header and 
+trailer field, respectively. It can perform a range test, check whether the
+value is numeric or whether it is absent. Negative values are considered
+non-numeric. Values with more than 18 digits are considered non-numeric.
+
 ==== Timing issues and combining rule options
 
 HTTP inspector is stateful. That means it is aware of a bigger picture than
index 0a151aac056a8d6aa56069a29a2b7c00f8135f3d..c9069f2ce7a1a7a5cdec4a660861c2c6069ad2ed 100644 (file)
@@ -78,6 +78,8 @@ set (FILE_LIST
     ips_http_num_hdrs.h
     ips_http_param.cc
     ips_http_param.h
+    ips_http_test.cc
+    ips_http_test.h
     ips_http_version.cc
     ips_http_version.h
 )
index 22d814c87535842e1d2de93e5d542b23429e6152..604ed780ede0324e66f1589ab32d486fee14d155 100644 (file)
@@ -103,6 +103,7 @@ const InspectApi HttpApi::http_api =
 extern const BaseApi* ips_http_client_body;
 extern const BaseApi* ips_http_cookie;
 extern const BaseApi* ips_http_header;
+extern const BaseApi* ips_http_header_test;
 extern const BaseApi* ips_http_method;
 extern const BaseApi* ips_http_num_headers;
 extern const BaseApi* ips_http_num_trailers;
@@ -117,6 +118,7 @@ extern const BaseApi* ips_http_raw_uri;
 extern const BaseApi* ips_http_stat_code;
 extern const BaseApi* ips_http_stat_msg;
 extern const BaseApi* ips_http_trailer;
+extern const BaseApi* ips_http_trailer_test;
 extern const BaseApi* ips_http_true_ip;
 extern const BaseApi* ips_http_uri;
 extern const BaseApi* ips_http_version;
@@ -133,6 +135,7 @@ const BaseApi* sin_http[] =
     ips_http_client_body,
     ips_http_cookie,
     ips_http_header,
+    ips_http_header_test,
     ips_http_method,
     ips_http_num_headers,
     ips_http_num_trailers,
@@ -147,6 +150,7 @@ const BaseApi* sin_http[] =
     ips_http_stat_code,
     ips_http_stat_msg,
     ips_http_trailer,
+    ips_http_trailer_test,
     ips_http_true_ip,
     ips_http_uri,
     ips_http_version,
index 054ad68a8c6b84a3af6eff07043e17aa85bcc020..7e28b3fcdd191d73c4a1b529dcf0f0fdae12c54d 100755 (executable)
@@ -54,7 +54,7 @@ enum SectionType { SEC_DISCARD = -19, SEC_ABORT = -18, SEC__NOT_COMPUTE=-14, SEC
     SEC_BODY_OLD, SEC_BODY_H2 };
 
 // HTTP rule options.
-// Lower portion is message buffers available to clients.
+// Lower numbered portion is message buffers available to clients.
 // That part must remain synchronized with HttpApi::classic_buffer_names[]
 enum HTTP_RULE_OPT { HTTP_BUFFER_CLIENT_BODY = 1, HTTP_BUFFER_COOKIE, HTTP_BUFFER_HEADER,
     HTTP_BUFFER_METHOD, HTTP_BUFFER_PARAM, HTTP_BUFFER_RAW_BODY, HTTP_BUFFER_RAW_COOKIE,
@@ -62,7 +62,8 @@ enum HTTP_RULE_OPT { HTTP_BUFFER_CLIENT_BODY = 1, HTTP_BUFFER_COOKIE, HTTP_BUFFE
     HTTP_BUFFER_RAW_TRAILER, HTTP_BUFFER_RAW_URI, HTTP_BUFFER_STAT_CODE, HTTP_BUFFER_STAT_MSG,
     HTTP_BUFFER_TRAILER, HTTP_BUFFER_TRUE_IP, HTTP_BUFFER_URI, HTTP_BUFFER_VERSION,
     BUFFER_JS_DATA, BUFFER_VBA_DATA , HTTP__BUFFER_MAX = BUFFER_VBA_DATA,
-    HTTP_RANGE_NUM_HDRS, HTTP_RANGE_NUM_TRAILERS, HTTP_VERSION_MATCH, HTTP__MAX_RULE_OPTION };
+    HTTP_RANGE_NUM_HDRS, HTTP_RANGE_NUM_TRAILERS, HTTP_VERSION_MATCH,
+    HTTP_HEADER_TEST, HTTP_TRAILER_TEST, HTTP__MAX_RULE_OPTION };
 
 // Peg counts
 // This enum must remain synchronized with HttpModule::peg_names[] in http_tables.cc
index b12cbfff7a05f78a4a8ce6938f74a66d10c7f935..6f8c9f7b5a20bc9b9ac0d89c5bb9f025c994bdd3 100644 (file)
@@ -148,7 +148,7 @@ const Field& HttpMsgSection::classic_normalize(const Field& raw, Field& norm,
 
 const Field& HttpMsgSection::get_classic_buffer(unsigned id, uint64_t sub_id, uint64_t form)
 {
-    HttpBufferInfo buffer_info(id, sub_id, form);
+    const HttpBufferInfo buffer_info(id, sub_id, form);
 
     return get_classic_buffer(buffer_info);
 }
@@ -176,8 +176,10 @@ const Field& HttpMsgSection::get_classic_buffer(const HttpBufferInfo& buf)
       }
     case HTTP_BUFFER_HEADER:
     case HTTP_BUFFER_TRAILER:
+    case HTTP_HEADER_TEST:
+    case HTTP_TRAILER_TEST:
       {
-        HttpMsgHeadShared* const head = (buf.type == HTTP_BUFFER_HEADER) ?
+        HttpMsgHeadShared* const head = (buf.type == HTTP_BUFFER_HEADER || buf.type == HTTP_HEADER_TEST) ?
             (HttpMsgHeadShared*)header[buffer_side] : (HttpMsgHeadShared*)trailer[buffer_side];
         if (head == nullptr)
             return Field::FIELD_NULL;
index 545167d22417e7deb3879372817ce8bb905a5ae5..c3c4d5d0dab4cd77e1242f6bd2b6fededc5671b3 100644 (file)
@@ -82,7 +82,7 @@ uint32_t HttpNumHdrsIpsOption::hash() const
 bool HttpNumHdrsIpsOption::operator==(const IpsOption& ips) const
 {
     const HttpNumHdrsIpsOption& hio = static_cast<const HttpNumHdrsIpsOption&>(ips);
-    return HttpIpsOption::operator==(static_cast<const HttpIpsOption&>(ips)) &&
+    return HttpIpsOption::operator==(ips) &&
            range == hio.range;
 }
 
index 17bc5286b561e4f067bce78c96c01975345d039f..a864f8df2a8b4f556bf9dac5e4987e51e4a1a19c 100644 (file)
@@ -87,7 +87,7 @@ bool HttpParamIpsOption::operator==(const IpsOption& ips) const
 {
     const HttpParamIpsOption& hio = static_cast<const HttpParamIpsOption&>(ips);
 
-    return HttpIpsOption::operator==(static_cast<const HttpIpsOption&>(ips)) &&
+    return HttpIpsOption::operator==(ips) &&
            http_param == hio.http_param;
 }
 
diff --git a/src/service_inspectors/http_inspect/ips_http_test.cc b/src/service_inspectors/http_inspect/ips_http_test.cc
new file mode 100644 (file)
index 0000000..835054a
--- /dev/null
@@ -0,0 +1,302 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2022-2022 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// 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 along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+// ips_http_test.cc author Maya Dagon <mdagon@cisco.com>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "ips_http_test.h"
+
+#include "framework/cursor.h"
+#include "hash/hash_key_operations.h"
+#include "log/messages.h"
+#include "parser/parse_utils.h"
+#include "protocols/packet.h"
+
+#include "http_common.h"
+#include "http_enum.h"
+#include "http_inspect.h"
+
+using namespace snort;
+using namespace HttpCommon;
+using namespace HttpEnums;
+
+THREAD_LOCAL std::array<ProfileStats, TEST_PSI_MAX> HttpTestRuleOptModule::http_test_ps;
+
+const std::string hdr_test_range = "0:999999999999999999"; // max 18 digit uint
+
+bool HttpTestRuleOptModule::begin(const char*, int, SnortConfig*)
+{
+    HttpRuleOptModule::begin(nullptr, 0, nullptr);
+    check.init();
+    numeric = NV_UNDEFINED;
+    absent = false;
+    if (rule_opt_index == HTTP_HEADER_TEST)
+        inspect_section = IS_FLEX_HEADER;
+    else
+    {
+        inspect_section = IS_TRAILER;
+        is_trailer_opt = true;
+    }
+
+    return true;
+}
+
+bool HttpTestRuleOptModule::set(const char*, Value& v, SnortConfig*)
+{
+    if (v.is("check"))
+    {
+        return check.validate(v.get_string(), hdr_test_range.c_str());
+    }
+    else if (v.is("numeric"))
+    {
+        numeric = v.get_bool()? NV_TRUE : NV_FALSE;
+    }
+    else if (v.is("absent"))
+    {
+        absent = true;
+    }
+    else
+    {
+        return HttpRuleOptModule::set(nullptr, v, nullptr);
+    }
+
+    return true;
+}
+
+bool HttpTestRuleOptModule::end(const char*, int, SnortConfig*)
+{
+    if (sub_id == 0)
+        ParseError("Specify field name");
+
+    if (!absent && !check.is_set() && numeric == NV_UNDEFINED)
+        ParseError("check, numeric, or absent should be specified");
+
+    if ((absent && (check.is_set() || numeric != NV_UNDEFINED)) ||
+        (check.is_set() && numeric == NV_FALSE))
+        ParseWarning(WARN_RULES, "conflicting suboptions");
+
+    return HttpRuleOptModule::end(nullptr, 0, nullptr);
+}
+
+uint32_t HttpTestIpsOption::hash() const
+{
+    uint32_t a = HttpIpsOption::hash();
+    uint32_t b = check.hash();
+    uint32_t c = numeric;
+    mix(a,b,c);
+    c = absent ? 1 : 0;
+    mix(a,b,c);
+    finalize(a,b,c);
+    return c;
+}
+
+bool HttpTestIpsOption::operator==(const IpsOption& ips) const
+{
+    const HttpTestIpsOption& hio = static_cast<const HttpTestIpsOption&>(ips);
+    return HttpIpsOption::operator==(ips) &&
+           check == hio.check &&
+           numeric == hio.numeric &&
+           absent == hio.absent;
+}
+
+static int64_t get_decimal_num(enum NumericValue& is_numeric, const uint8_t* start, int32_t length)
+{
+    int64_t total = 0;
+    int32_t k = 0;
+    do
+    {
+        int value = start[k] - '0';
+        if ((value < 0) || (value > 9))
+        {
+            is_numeric = NV_FALSE;
+            return -1;
+        }
+        total = total*10 + value;
+    }
+    while (++k < length);
+
+    is_numeric = NV_TRUE;
+    return total;
+}
+
+IpsOption::EvalStatus HttpTestIpsOption::eval_header_test(const Field& http_buffer) const
+{
+    bool is_absent = false;
+    enum NumericValue is_numeric = NV_UNDEFINED;
+    int64_t num = 0;
+
+    const int32_t length = http_buffer.length();
+    if (length <= 0)
+        is_absent = true;
+    // Limit to 18 decimal digits, to fit comfortably into int64_t.
+    else if (length <= 18)
+        num = get_decimal_num(is_numeric, http_buffer.start(), length);
+    else
+        is_numeric = NV_FALSE;
+
+    const bool absent_passed = !absent || (absent && is_absent);
+    const bool numeric_passed = (numeric == NumericValue::NV_UNDEFINED) ||
+                                (is_numeric == numeric);
+    const bool range_passed = !check.is_set() || (is_numeric == NV_TRUE && check.eval(num));
+
+    return (absent_passed && numeric_passed && range_passed) ? MATCH : NO_MATCH;
+}
+
+
+IpsOption::EvalStatus HttpTestIpsOption::eval(Cursor&, Packet* p)
+{
+    RuleProfile profile(HttpTestRuleOptModule::http_test_ps[idx]);
+
+    const HttpInspect* const hi = eval_helper(p);
+    if (hi == nullptr)
+        return NO_MATCH;
+    
+    const Field& http_buffer = hi->http_get_buf(p, buffer_info);
+
+    return eval_header_test(http_buffer);
+}
+
+
+//-------------------------------------------------------------------------
+// http_header_test
+//-------------------------------------------------------------------------
+#undef IPS_OPT
+#define IPS_OPT "http_header_test"
+#undef IPS_HELP
+#define IPS_HELP "rule option to perform range check on specified header field, \
+check whether it is a number, or check if the field is absent"
+
+static const Parameter hdr_test_params[] =
+{
+    { "field", Parameter::PT_STRING, nullptr, nullptr,
+        "Header to perform check on. Header name is case insensitive." },
+    { "request", Parameter::PT_IMPLIED, nullptr, nullptr,
+        "match against the headers from the request message even when examining the response" },
+    { "with_header", Parameter::PT_IMPLIED, nullptr, nullptr,
+        "this rule is limited to examining HTTP message headers" },
+    { "with_body", Parameter::PT_IMPLIED, nullptr, nullptr,
+        "parts of this rule examine HTTP message body" },
+    { "with_trailer", Parameter::PT_IMPLIED, nullptr, nullptr,
+        "parts of this rule examine HTTP message trailers" },
+    { "check", Parameter::PT_INTERVAL, hdr_test_range.c_str(), nullptr,
+        "range check to perform on header value" },
+    { "numeric", Parameter::PT_BOOL, nullptr, nullptr,
+        "header value is a number" },
+    { "absent", Parameter::PT_IMPLIED, nullptr, nullptr,
+        "header is absent" },
+    { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr }
+};
+
+static Module* http_header_test_mod_ctor()
+{
+    return new HttpTestRuleOptModule(IPS_OPT, IPS_HELP, HTTP_HEADER_TEST, CAT_SET_OTHER,
+        TEST_PSI_HEADER_TEST, hdr_test_params);
+}
+
+static const IpsApi header_test_api =
+{
+    {
+        PT_IPS_OPTION,
+        sizeof(IpsApi),
+        IPSAPI_VERSION,
+        1,
+        API_RESERVED,
+        API_OPTIONS,
+        IPS_OPT,
+        IPS_HELP,
+        http_header_test_mod_ctor,
+        HttpTestRuleOptModule::mod_dtor
+    },
+    OPT_TYPE_DETECTION,
+    0, PROTO_BIT__TCP,
+    nullptr,
+    nullptr,
+    nullptr,
+    nullptr,
+    HttpTestIpsOption::opt_ctor,
+    HttpTestIpsOption::opt_dtor,
+    nullptr
+};
+
+//-------------------------------------------------------------------------
+// http_trailer_test
+//-------------------------------------------------------------------------
+static const Parameter trailer_test_params[] =
+{
+    { "field", Parameter::PT_STRING, nullptr, nullptr,
+        "Trailer to perform check on. Trailer name is case insensitive." },
+    { "request", Parameter::PT_IMPLIED, nullptr, nullptr,
+        "match against the trailers from the request message even when examining the response" },
+    { "with_header", Parameter::PT_IMPLIED, nullptr, nullptr,
+        "parts of this rule examine HTTP headers" },
+    { "with_body", Parameter::PT_IMPLIED, nullptr, nullptr,
+        "parts of this rule examine HTTP message body" },
+    { "check", Parameter::PT_INTERVAL, hdr_test_range.c_str(), nullptr,
+        "range check to perform on trailer value" },
+    { "numeric", Parameter::PT_BOOL, nullptr, nullptr,
+        "trailer value is a number" },
+    { "absent", Parameter::PT_IMPLIED, nullptr, nullptr,
+        "trailer is absent" },
+    { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr }
+};
+
+#undef IPS_OPT
+#define IPS_OPT "http_trailer_test"
+#undef IPS_HELP
+#define IPS_HELP "rule option to perform range check on specified trailer field, \
+check whether it is a number, or check if the field is absent"
+
+static Module* http_trailer_test_mod_ctor()
+{
+    return new HttpTestRuleOptModule(IPS_OPT, IPS_HELP, HTTP_TRAILER_TEST, CAT_SET_OTHER,
+        TEST_PSI_TRAILER_TEST, trailer_test_params);
+}
+
+static const IpsApi trailer_test_api =
+{
+    {
+        PT_IPS_OPTION,
+        sizeof(IpsApi),
+        IPSAPI_VERSION,
+        1,
+        API_RESERVED,
+        API_OPTIONS,
+        IPS_OPT,
+        IPS_HELP,
+        http_trailer_test_mod_ctor,
+        HttpTestRuleOptModule::mod_dtor
+    },
+    OPT_TYPE_DETECTION,
+    0, PROTO_BIT__TCP,
+    nullptr,
+    nullptr,
+    nullptr,
+    nullptr,
+    HttpTestIpsOption::opt_ctor,
+    HttpTestIpsOption::opt_dtor,
+    nullptr
+};
+
+//-------------------------------------------------------------------------
+// plugins
+//-------------------------------------------------------------------------
+
+const BaseApi* ips_http_header_test = &header_test_api.base;
+const BaseApi* ips_http_trailer_test = &trailer_test_api.base;
diff --git a/src/service_inspectors/http_inspect/ips_http_test.h b/src/service_inspectors/http_inspect/ips_http_test.h
new file mode 100644 (file)
index 0000000..681e47a
--- /dev/null
@@ -0,0 +1,85 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2022-2022 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// 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 along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+// ips_http_test.h author Maya Dagon <mdagon@cisco.com>
+
+#ifndef IPS_HTTP_TEST_H
+#define IPS_HTTP_TEST_H
+
+#include <array>
+
+#include "profiler/profiler.h"
+#include "framework/ips_option.h"
+#include "framework/module.h"
+#include "framework/range.h"
+
+#include "http_enum.h"
+#include "http_field.h"
+#include "ips_http.h"
+
+enum TestPsIdx { TEST_PSI_HEADER_TEST, TEST_PSI_TRAILER_TEST, TEST_PSI_MAX };
+
+enum NumericValue { NV_UNDEFINED, NV_TRUE, NV_FALSE };
+
+class HttpTestRuleOptModule : public HttpRuleOptModule
+{
+public:
+    HttpTestRuleOptModule(const char* key_, const char* help, HttpEnums::HTTP_RULE_OPT rule_opt_index_,
+        snort::CursorActionType cat_, TestPsIdx idx_, const snort::Parameter params[])
+        : HttpRuleOptModule(key_, help, rule_opt_index_, cat_, params), idx(idx_) {}
+
+    snort::ProfileStats* get_profile() const override
+    { return &http_test_ps[idx]; }
+  
+    static void mod_dtor(snort::Module* m) { delete m; }
+    bool begin(const char*, int, snort::SnortConfig*) override;
+    bool set(const char*, snort::Value&, snort::SnortConfig*) override;
+    bool end(const char*, int, snort::SnortConfig*) override;
+
+private:
+    friend class HttpTestIpsOption;
+    static THREAD_LOCAL std::array<snort::ProfileStats, TEST_PSI_MAX> http_test_ps;
+    const TestPsIdx idx;
+    snort::RangeCheck check;
+    enum NumericValue numeric;
+    bool absent;
+};
+
+class HttpTestIpsOption : public HttpIpsOption
+{
+public:
+    HttpTestIpsOption(const HttpTestRuleOptModule* cm) :
+        HttpIpsOption(cm, RULE_OPTION_TYPE_OTHER), idx(cm->idx), check(cm->check),
+        numeric(cm->numeric), absent(cm->absent) {}
+    EvalStatus eval(Cursor&, snort::Packet*) override;
+    uint32_t hash() const override;
+    bool operator==(const snort::IpsOption& ips) const override;
+    static IpsOption* opt_ctor(snort::Module* m, OptTreeNode*)
+        { return new HttpTestIpsOption((HttpTestRuleOptModule*)m); }
+    static void opt_dtor(snort::IpsOption* p) { delete p; }
+
+private:
+    const TestPsIdx idx;
+    const snort::RangeCheck check;
+    const enum NumericValue numeric;
+    const bool absent;
+
+    IpsOption::EvalStatus eval_header_test(const Field& http_buffer) const;
+};
+
+#endif
+
index 7a60c31e9371764a0240f2f685c9e1b4b772c9e8..90b2e3ffb376cb0dcb62a3df45411fa7213d3a09 100644 (file)
@@ -108,7 +108,7 @@ uint32_t HttpVersionIpsOption::hash() const
 bool HttpVersionIpsOption::operator==(const IpsOption& ips) const
 {
     const HttpVersionIpsOption& hio = static_cast<const HttpVersionIpsOption&>(ips);
-    return HttpIpsOption::operator==(static_cast<const HttpIpsOption&>(ips)) &&
+    return HttpIpsOption::operator==(ips) &&
            version_flags == hio.version_flags;
 }