From: Maya Dagon (mdagon) Date: Tue, 17 Oct 2023 13:07:38 +0000 (+0000) Subject: Pull request #4049: http_inspect: response to 0.9 isn't necessarily 0.9 X-Git-Tag: 3.1.73.0~4 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b5395360f50e82e08f4726129225ca836a679a04;p=thirdparty%2Fsnort3.git Pull request #4049: http_inspect: response to 0.9 isn't necessarily 0.9 Merge in SNORT/snort3 from ~MDAGON/snort3:zero_nine_res to master Squashed commit of the following: commit 5a1eb93b13c3a086c9c9baa4382853fecb5bb408 Author: maya dagon Date: Wed Oct 4 08:30:54 2023 -0400 http_inspect: response to 0.9 isn't necessarily 0.9 --- diff --git a/src/service_inspectors/http_inspect/http_cutter.cc b/src/service_inspectors/http_inspect/http_cutter.cc index 321f8bedd..0c0ea6060 100644 --- a/src/service_inspectors/http_inspect/http_cutter.cc +++ b/src/service_inspectors/http_inspect/http_cutter.cc @@ -31,6 +31,36 @@ 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; +} diff --git a/src/service_inspectors/http_inspect/http_cutter.h b/src/service_inspectors/http_inspect/http_cutter.h index e8160413a..99658b875 100644 --- a/src/service_inspectors/http_inspect/http_cutter.h +++ b/src/service_inspectors/http_inspect/http_cutter.h @@ -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: diff --git a/src/service_inspectors/http_inspect/http_stream_splitter.h b/src/service_inspectors/http_inspect/http_stream_splitter.h index d38fc9981..8a9b231ea 100644 --- a/src/service_inspectors/http_inspect/http_stream_splitter.h +++ b/src/service_inspectors/http_inspect/http_stream_splitter.h @@ -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; diff --git a/src/service_inspectors/http_inspect/http_stream_splitter_scan.cc b/src/service_inspectors/http_inspect/http_stream_splitter_scan.cc index e32d6309c..9b2a7edac 100644 --- a/src/service_inspectors/http_inspect/http_stream_splitter_scan.cc +++ b/src/service_inspectors/http_inspect/http_stream_splitter_scan.cc @@ -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); +} +