]> git.ipfire.org Git - thirdparty/snort3.git/commitdiff
Pull request #4049: http_inspect: response to 0.9 isn't necessarily 0.9
authorMaya Dagon (mdagon) <mdagon@cisco.com>
Tue, 17 Oct 2023 13:07:38 +0000 (13:07 +0000)
committerOleksii Shumeiko -X (oshumeik - SOFTSERVE INC at Cisco) <oshumeik@cisco.com>
Tue, 17 Oct 2023 13:07:38 +0000 (13:07 +0000)
Merge in SNORT/snort3 from ~MDAGON/snort3:zero_nine_res to master

Squashed commit of the following:

commit 5a1eb93b13c3a086c9c9baa4382853fecb5bb408
Author: maya dagon <mdagon@cisco.com>
Date:   Wed Oct 4 08:30:54 2023 -0400

    http_inspect: response to 0.9 isn't necessarily 0.9

src/service_inspectors/http_inspect/http_cutter.cc
src/service_inspectors/http_inspect/http_cutter.h
src/service_inspectors/http_inspect/http_stream_splitter.h
src/service_inspectors/http_inspect/http_stream_splitter_scan.cc

index 321f8bedd9a774bb3102cb2ade4b41052af1a4c4..0c0ea6060915366e4021ee7e4a8ac84f63e2ca18 100644 (file)
 using namespace HttpEnums;
 using namespace HttpCommon;
 
+bool HttpStartCutter::find_eol(uint8_t octet, uint32_t idx, HttpInfractions* infractions, HttpEventGen* events)
+{
+    if (octet == '\n')
+    {
+        num_crlf++;
+        if (num_crlf == 1)
+        {
+            // There was no CR before this
+            *infractions += INF_LF_WITHOUT_CR;
+            events->create_event(EVENT_LF_WITHOUT_CR);
+        }
+        num_flush = idx + 1;
+        return true;
+    }
+    if (num_crlf == 1)
+    {
+        // CR not followed by LF
+        *infractions += INF_CR_WITHOUT_LF;
+        events->create_event(EVENT_CR_WITHOUT_LF);
+        num_flush = idx;   // current octet not flushed
+        return true;
+    }
+    if (octet == '\r')
+    {
+        num_crlf = 1;
+    }
+
+    return false;
+}
+
 ScanResult HttpStartCutter::cut(const uint8_t* buffer, uint32_t length,
     HttpInfractions* infractions, HttpEventGen* events, uint32_t, bool, HXBodyState)
 {
@@ -87,29 +117,9 @@ ScanResult HttpStartCutter::cut(const uint8_t* buffer, uint32_t length,
                 break;
             }
         }
-        if (buffer[k] == '\n')
-        {
-            num_crlf++;
-            if (num_crlf == 1)
-            {
-                // There was no CR before this
-                *infractions += INF_LF_WITHOUT_CR;
-                events->create_event(EVENT_LF_WITHOUT_CR);
-            }
-            num_flush = k+1;
-            return SCAN_FOUND;
-        }
-        if (num_crlf == 1)
-        {   // CR not followed by LF
-            *infractions += INF_CR_WITHOUT_LF;
-            events->create_event(EVENT_CR_WITHOUT_LF);
-            num_flush = k;                      // current octet not flushed
+
+        if (find_eol(buffer[k], k, infractions, events))
             return SCAN_FOUND;
-        }
-        if (buffer[k] == '\r')
-        {
-            num_crlf = 1;
-        }
     }
     octets_seen += length;
     return SCAN_NOT_FOUND;
@@ -936,3 +946,46 @@ bool HttpBodyCutter::dangerous(const uint8_t* data, uint32_t length)
     return false;
 }
 
+uint8_t HttpZeroNineCutter::match[] = { 'H', 'T', 'T', 'P', '/' };
+
+HttpStartCutter::ValidationResult HttpZeroNineCutter::validate(uint8_t octet, HttpInfractions*, HttpEventGen*)
+{
+    if (octet != match[octets_checked])
+        return V_BAD;
+
+    if (++octets_checked >= match_size)
+        return V_GOOD;
+
+    return V_TBD;
+}
+
+// Lightweight version of the start cutter. Checking whether it is 0.9 response or more advanced
+// version's status line.
+ScanResult HttpZeroNineCutter::cut(const uint8_t* buffer, uint32_t length,
+    HttpInfractions* infractions, HttpEventGen* events, uint32_t, bool, HXBodyState)
+{
+    for (uint32_t k = 0; k < length; k++)
+    {
+        if (!validated)
+        {
+            // The purpose of validate() is to quickly and efficiently dispose of obviously wrong
+            // bindings. Passing is no guarantee that the connection is really HTTP, but failing
+            // makes it clear that it isn't.
+            switch (validate(buffer[k], infractions, events))
+            {
+            case V_GOOD:
+                validated = true;
+                break;
+            case V_BAD:
+                return SCAN_ABORT;
+            case V_TBD:
+                break;
+            }
+        }
+
+        if (find_eol(buffer[k], k, infractions, events))
+            return SCAN_FOUND;
+    }
+    octets_seen += length;
+    return SCAN_NOT_FOUND;
+}
index e8160413a75746fde0b9ed7794fded58c25a16e8..99658b87580418ebf75dcc7b97923206f176fd50 100644 (file)
@@ -65,11 +65,13 @@ public:
 
 protected:
     enum ValidationResult { V_GOOD, V_BAD, V_TBD };
+    bool validated = false;
+
+    bool find_eol(uint8_t octet, uint32_t ind, HttpInfractions* infractions, HttpEventGen* events);
 
 private:
     static const int MAX_LEADING_WHITESPACE = 20;
     virtual ValidationResult validate(uint8_t octet, HttpInfractions*, HttpEventGen*) = 0;
-    bool validated = false;
 };
 
 class HttpRequestCutter : public HttpStartCutter
@@ -87,6 +89,19 @@ private:
     ValidationResult validate(uint8_t octet, HttpInfractions*, HttpEventGen*) override;
 };
 
+class HttpZeroNineCutter : public HttpStartCutter
+{
+public:
+    HttpEnums::ScanResult cut(const uint8_t* buffer, uint32_t length,
+        HttpInfractions* infractions, HttpEventGen* events, uint32_t, bool, HttpCommon::HXBodyState) override;
+    static const int match_size = 5;
+    static uint8_t match[match_size];
+
+private:
+    uint32_t octets_checked = 0;
+    ValidationResult validate(uint8_t octet, HttpInfractions*, HttpEventGen*) override;
+};
+
 class HttpHeaderCutter : public HttpCutter
 {
 public:
index d38fc9981ab9d48ee8a5f00009a3d080101971d6..8a9b231ea95b0fe3f80ffc2c71a7bdf46717aa48 100644 (file)
@@ -66,6 +66,10 @@ private:
     void process_gzip_header(const uint8_t* data,
         uint32_t length, HttpFlowData* session_data) const;
     bool gzip_header_check_done(HttpFlowData* session_data) const;
+    StreamSplitter::Status handle_zero_nine(snort::Flow*, HttpFlowData*, const uint8_t* data,
+        uint32_t length, uint32_t* flush_offset, HttpCommon::SectionType&, HttpCutter*&);
+    StreamSplitter::Status call_cutter(snort::Flow*, HttpFlowData*, const uint8_t* data,
+        uint32_t length, uint32_t* flush_offset, HttpCommon::SectionType&);
 
     HttpInspect* const my_inspector;
     const HttpCommon::SourceId source_id;
index e32d6309c068a5d3213c27724a4932b4dff22bef..9b2a7edacd8d7d10ddc04f77de5173c3c68553cc 100644 (file)
@@ -72,6 +72,9 @@ HttpCutter* HttpStreamSplitter::get_cutter(SectionType type,
     case SEC_REQUEST:
         return (HttpCutter*)new HttpRequestCutter;
     case SEC_STATUS:
+        if (session_data->expected_trans_num[SRC_SERVER] == session_data->zero_nine_expected)
+            return (HttpCutter*)new HttpZeroNineCutter;
+
         return (HttpCutter*)new HttpStatusCutter;
     case SEC_HEADER:
     case SEC_TRAILER:
@@ -133,118 +136,53 @@ StreamSplitter::Status HttpStreamSplitter::scan(Packet* pkt, const uint8_t* data
     return scan(pkt->flow, data, length, flush_offset);
 }
 
-StreamSplitter::Status HttpStreamSplitter::scan(Flow* flow, const uint8_t* data, uint32_t length,
-    uint32_t* flush_offset)
+StreamSplitter::Status HttpStreamSplitter::handle_zero_nine(Flow* flow, HttpFlowData* session_data, const uint8_t* data, uint32_t length,
+    uint32_t* flush_offset, SectionType& type, HttpCutter*& cutter)
 {
-    Profile profile(HttpModule::get_profile_stats());
-
-    // This is the session state information we share with HttpInspect and store with stream. A
-    // session is defined by a TCP connection. Since scan() is the first to see a new TCP
-    // connection the new flow data object is created here.
-    HttpFlowData* session_data = HttpInspect::http_get_flow_data(flow);
-
-    if (session_data == nullptr)
-    {
-        HttpInspect::http_set_flow_data(flow, session_data = new HttpFlowData(flow, my_inspector->params));
-        HttpModule::increment_peg_counts(PEG_FLOW);
-    }
+    // Didn't match status line, this is a 0.9 response.
+    // Delete ZeroNineCutter, save the amount of bytes that should be resent to BodyOld
+    const uint32_t prev_scan_match = cutter->get_octets_seen();
+    delete cutter;
+    cutter = nullptr;
 
-#ifdef REG_TEST
-    if (HttpTestManager::use_test_input(HttpTestManager::IN_HTTP))
+    // 0.9 response is a body that runs to connection end with no headers.
+    // Processing this imaginary empty headers allows
+    // us to overcome this limitation and reuse the entire HTTP infrastructure.
+    session_data->version_id[source_id] = VERS_0_9;
+    session_data->status_code_num = 200;
+    HttpModule::increment_peg_counts(PEG_RESPONSE);
+    prepare_flush(session_data, nullptr, SEC_HEADER, 0, 0, 0, false, 0, 0);
+    my_inspector->process((const uint8_t*)"", 0, flow, SRC_SERVER, false, nullptr);
+    session_data->transaction[SRC_SERVER]->clear_section();
+    HttpContextData* hcd = (HttpContextData*)DetectionEngine::get_data(HttpContextData::ips_id);
+    assert(hcd != nullptr);
+    if (hcd == nullptr)
     {
-        // This block substitutes a completely new data buffer supplied by the test tool in place
-        // of the "real" data. It also rewrites the buffer length.
-        *flush_offset = length;
-        uint8_t* test_data = nullptr;
-        HttpTestManager::get_test_input_source()->scan(test_data, length, source_id,
-            session_data->seq_num);
-        if (length == 0)
-            return StreamSplitter::FLUSH;
-        data = test_data;
-    }
-#endif
-
-    SectionType& type = session_data->type_expected[source_id];
-    session_data->partial_flush[source_id] = false;
-
-    if (type == SEC_ABORT)
-        return status_value(StreamSplitter::ABORT);
-
-    if (length > MAX_OCTETS)
-    {
-        assert(false);
         type = SEC_ABORT;
         return status_value(StreamSplitter::ABORT);
     }
+    hcd->clear();
 
-#ifdef REG_TEST
-    if (HttpTestManager::use_test_output(HttpTestManager::IN_HTTP) &&
-        !HttpTestManager::use_test_input(HttpTestManager::IN_HTTP))
+    // Call BodyOldCutter
+    StreamSplitter::Status status;
+    if (prev_scan_match)
     {
-        fprintf(HttpTestManager::get_output_file(), "Scan from flow data %" PRIu64
-            " direction %d length %u client port %hu server port %hu\n", session_data->seq_num,
-            source_id, length, flow->client_port, flow->server_port);
-        fflush(HttpTestManager::get_output_file());
-        if (HttpTestManager::get_show_scan())
-        {
-            Field(length, data).print(HttpTestManager::get_output_file(), "Scan segment");
-        }
-    }
-#endif
-
-    if (session_data->tcp_close[source_id])
-    {
-        // assert(false); // FIXIT-L This currently happens. Add assert back when problem resolved.
-        type = SEC_ABORT;
-        return status_value(StreamSplitter::ABORT);
-    }
-
-    // If the last request was a CONNECT and we have not yet seen the response, this is early C2S
-    // traffic. If there has been a pipeline overflow or underflow we cannot match requests to
-    // responses, so there is no attempt to track early C2S traffic.
-    if ((source_id == SRC_CLIENT) && (type == SEC_REQUEST) && !session_data->for_httpx &&
-        session_data->last_request_was_connect)
-    {
-        const uint64_t last_request_trans_num = session_data->expected_trans_num[SRC_CLIENT] - 1;
-        const bool server_behind_connect =
-            (session_data->expected_trans_num[SRC_SERVER] < last_request_trans_num);
-        const bool server_expecting_connect_status =
-            ((session_data->expected_trans_num[SRC_SERVER] == last_request_trans_num)
-            && (session_data->type_expected[SRC_SERVER] == SEC_STATUS));
-        const bool pipeline_valid = !session_data->pipeline_overflow &&
-            !session_data->pipeline_underflow;
-
-        if ((server_behind_connect || server_expecting_connect_status) && pipeline_valid)
-        {
-            *session_data->get_infractions(source_id) += INF_EARLY_C2S_TRAFFIC_AFTER_CONNECT;
-            session_data->events[source_id]->create_event(EVENT_EARLY_C2S_TRAFFIC_AFTER_CONNECT);
-            session_data->last_connect_trans_w_early_traffic =
-                session_data->expected_trans_num[SRC_CLIENT] - 1;
-        }
-        session_data->last_request_was_connect = false;
+        assert(prev_scan_match < HttpZeroNineCutter::match_size);
+        uint8_t* buffer = new uint8_t[length + prev_scan_match];
+        memcpy (buffer, HttpZeroNineCutter::match, prev_scan_match);
+        memcpy (buffer + prev_scan_match, data, length);
+        status = call_cutter(flow, session_data, buffer, length + prev_scan_match, flush_offset, type);
+        delete[] buffer;
     }
+    else
+        status = call_cutter(flow, session_data, data, length, flush_offset, type);
 
-    HttpModule::increment_peg_counts(PEG_SCAN);
-
-    // Check for 0.9 response message
-    if ((type == SEC_STATUS) &&
-        (session_data->expected_trans_num[SRC_SERVER] == session_data->zero_nine_expected))
-    {
-        // 0.9 response is a body that runs to connection end with no headers.
-        // Processing this imaginary empty headers allows
-        // us to overcome this limitation and reuse the entire HTTP infrastructure.
-        session_data->version_id[source_id] = VERS_0_9;
-        session_data->status_code_num = 200;
-        HttpModule::increment_peg_counts(PEG_RESPONSE);
-        prepare_flush(session_data, nullptr, SEC_HEADER, 0, 0, 0, false, 0, 0);
-        my_inspector->process((const uint8_t*)"", 0, flow, SRC_SERVER, false, nullptr);
-        session_data->transaction[SRC_SERVER]->clear_section();
-        HttpContextData* hcd = (HttpContextData*)DetectionEngine::get_data(HttpContextData::ips_id);
-        assert(hcd != nullptr);
-        if (hcd != nullptr)
-            hcd->clear();
-    }
+    return status;
+}
 
+StreamSplitter::Status HttpStreamSplitter::call_cutter(Flow* flow, HttpFlowData* session_data, const uint8_t* data, uint32_t length,
+    uint32_t* flush_offset, SectionType& type)
+{
     HttpCutter*& cutter = session_data->cutter[source_id];
     if (cutter == nullptr)
     {
@@ -265,10 +203,12 @@ StreamSplitter::Status HttpStreamSplitter::scan(Flow* flow, const uint8_t* data,
         {
             *session_data->get_infractions(source_id) += INF_ENDLESS_HEADER;
             auto event = HttpEnums::EVENT_HEADERS_TOO_LONG;
+
             if (session_data ->type_expected[source_id] == HttpCommon::SEC_REQUEST)
                 event = HttpEnums::EVENT_REQ_TOO_LONG;
             else if (session_data ->type_expected[source_id] == HttpCommon::SEC_STATUS)
                 event = HttpEnums::EVENT_STAT_TOO_LONG;
+
             session_data->events[source_id]->create_event(event);
             session_data->events[source_id]->create_event(HttpEnums::EVENT_LOSS_OF_SYNC);
             type = SEC_ABORT;
@@ -290,6 +230,8 @@ StreamSplitter::Status HttpStreamSplitter::scan(Flow* flow, const uint8_t* data,
         // Wait patiently for more data
         return status_value(StreamSplitter::SEARCH);
     case SCAN_ABORT:
+        if (type == SEC_STATUS && session_data->expected_trans_num[SRC_SERVER] == session_data->zero_nine_expected)
+            return handle_zero_nine(flow, session_data, data, length, flush_offset, type, cutter);
         type = SEC_ABORT;
         delete cutter;
         cutter = nullptr;
@@ -332,3 +274,99 @@ StreamSplitter::Status HttpStreamSplitter::scan(Flow* flow, const uint8_t* data,
     }
 }
 
+StreamSplitter::Status HttpStreamSplitter::scan(Flow* flow, const uint8_t* data, uint32_t length,
+    uint32_t* flush_offset)
+{
+    Profile profile(HttpModule::get_profile_stats());
+
+    // This is the session state information we share with HttpInspect and store with stream. A
+    // session is defined by a TCP connection. Since scan() is the first to see a new TCP
+    // connection the new flow data object is created here.
+    HttpFlowData* session_data = HttpInspect::http_get_flow_data(flow);
+
+    if (session_data == nullptr)
+    {
+        HttpInspect::http_set_flow_data(flow, session_data = new HttpFlowData(flow, my_inspector->params));
+        HttpModule::increment_peg_counts(PEG_FLOW);
+    }
+
+#ifdef REG_TEST
+    if (HttpTestManager::use_test_input(HttpTestManager::IN_HTTP))
+    {
+        // This block substitutes a completely new data buffer supplied by the test tool in place
+        // of the "real" data. It also rewrites the buffer length.
+        *flush_offset = length;
+        uint8_t* test_data = nullptr;
+        HttpTestManager::get_test_input_source()->scan(test_data, length, source_id,
+            session_data->seq_num);
+        if (length == 0)
+            return StreamSplitter::FLUSH;
+        data = test_data;
+    }
+#endif
+
+    SectionType& type = session_data->type_expected[source_id];
+    session_data->partial_flush[source_id] = false;
+
+    if (type == SEC_ABORT)
+        return status_value(StreamSplitter::ABORT);
+
+    if (length > MAX_OCTETS)
+    {
+        assert(false);
+        type = SEC_ABORT;
+        return status_value(StreamSplitter::ABORT);
+    }
+
+#ifdef REG_TEST
+    if (HttpTestManager::use_test_output(HttpTestManager::IN_HTTP) &&
+        !HttpTestManager::use_test_input(HttpTestManager::IN_HTTP))
+    {
+        fprintf(HttpTestManager::get_output_file(), "Scan from flow data %" PRIu64
+            " direction %d length %u client port %hu server port %hu\n", session_data->seq_num,
+            source_id, length, flow->client_port, flow->server_port);
+        fflush(HttpTestManager::get_output_file());
+        if (HttpTestManager::get_show_scan())
+        {
+            Field(length, data).print(HttpTestManager::get_output_file(), "Scan segment");
+        }
+    }
+#endif
+
+    if (session_data->tcp_close[source_id])
+    {
+        // assert(false); // FIXIT-L This currently happens. Add assert back when problem resolved.
+        type = SEC_ABORT;
+        return status_value(StreamSplitter::ABORT);
+    }
+
+    // If the last request was a CONNECT and we have not yet seen the response, this is early C2S
+    // traffic. If there has been a pipeline overflow or underflow we cannot match requests to
+    // responses, so there is no attempt to track early C2S traffic.
+    if ((source_id == SRC_CLIENT) && (type == SEC_REQUEST) && !session_data->for_httpx &&
+        session_data->last_request_was_connect)
+    {
+        const uint64_t last_request_trans_num = session_data->expected_trans_num[SRC_CLIENT] - 1;
+        const bool server_behind_connect =
+            (session_data->expected_trans_num[SRC_SERVER] < last_request_trans_num);
+        const bool server_expecting_connect_status =
+            ((session_data->expected_trans_num[SRC_SERVER] == last_request_trans_num)
+            && (session_data->type_expected[SRC_SERVER] == SEC_STATUS));
+        const bool pipeline_valid = !session_data->pipeline_overflow &&
+            !session_data->pipeline_underflow;
+
+        if ((server_behind_connect || server_expecting_connect_status) && pipeline_valid)
+        {
+            *session_data->get_infractions(source_id) += INF_EARLY_C2S_TRAFFIC_AFTER_CONNECT;
+            session_data->events[source_id]->create_event(EVENT_EARLY_C2S_TRAFFIC_AFTER_CONNECT);
+            session_data->last_connect_trans_w_early_traffic =
+                session_data->expected_trans_num[SRC_CLIENT] - 1;
+        }
+        session_data->last_request_was_connect = false;
+    }
+
+    HttpModule::increment_peg_counts(PEG_SCAN);
+
+    return call_cutter(flow, session_data, data, length, flush_offset, type);
+}
+