swf: grow decompression buffer on demand

Previously, SWF decompression guessed the final output size from FileLength,
allocated the whole buffer, and then decompressed once. A malicious SWF could
claim a 50MB output and force the per-thread inspection buffer to allocate and
retain 64 MiB before the compressed data was even checked.

Change the logic:
allocate 4 KiB, decompress, grow only when the buffer is full, and continue.
The buffer grows geometrically up to the configured output limit,
and the final inspection length is set to the number of bytes actually produced.

Also, for zlib, Z_OK does not by itself mean that decompression is complete.
It should be checked together with avail_out and avail_in,
grow the buffer when output space is exhausted, and continue the same inflate stream.

Ticket: #8829
(cherry picked from commit ddc6d29fd1)
pull/16211/head
Giuseppe Longo 2 months ago committed by Jason Ish
parent d8270028d7
commit eda28fab8d

@ -19,12 +19,13 @@
use lzma_rs::decompress::{Options, Stream};
use lzma_rs::error::Error;
use std::io::{Cursor, Write};
use std::io::{Cursor, ErrorKind, Write};
/// Propagate lzma crate errors
#[repr(C)]
pub enum LzmaStatus {
LzmaOk,
LzmaOutputFull,
LzmaIoError,
LzmaHeaderTooShortError,
LzmaError,
@ -35,6 +36,7 @@ pub enum LzmaStatus {
impl From<Error> for LzmaStatus {
fn from(e: Error) -> LzmaStatus {
match e {
Error::IoError(e) if e.kind() == ErrorKind::WriteZero => LzmaStatus::LzmaOutputFull,
Error::IoError(_) => LzmaStatus::LzmaIoError,
Error::HeaderTooShort(_) => LzmaStatus::LzmaHeaderTooShortError,
Error::LzmaError(e) => {
@ -50,8 +52,12 @@ impl From<Error> for LzmaStatus {
}
impl From<std::io::Error> for LzmaStatus {
fn from(_e: std::io::Error) -> LzmaStatus {
LzmaStatus::LzmaIoError
fn from(e: std::io::Error) -> LzmaStatus {
if e.kind() == ErrorKind::WriteZero {
LzmaStatus::LzmaOutputFull
} else {
LzmaStatus::LzmaIoError
}
}
}
@ -85,3 +91,4 @@ pub unsafe extern "C" fn lzma_decompress(
Err(e) => e.into(),
}
}

@ -28,6 +28,162 @@
#include "../stream-tcp.h"
#include "../detect.h"
#include "../detect-isdataat.h"
#include "../detect-engine-inspect-buffer.h"
#include "../util-file-decompression.h"
#include "../util-file-swf-decompression.h"
#include "../app-layer-events.h"
#include "../app-layer-htp.h"
#include <zlib.h>
static uint8_t *DetectFiledataSwfCreateCws(const uint8_t *plain, uint32_t plain_len,
uint32_t file_len, int compression_level, uint32_t *cws_len)
{
uLongf compressed_len = compressBound(plain_len);
uint8_t *cws = SCMalloc(SWF_HEADER_LEN + compressed_len);
if (cws == NULL)
return NULL;
cws[0] = 'C';
cws[1] = 'W';
cws[2] = 'S';
cws[3] = 0x06;
cws[4] = (uint8_t)file_len;
cws[5] = (uint8_t)(file_len >> 8);
cws[6] = (uint8_t)(file_len >> 16);
cws[7] = (uint8_t)(file_len >> 24);
int r = compress2(cws + SWF_HEADER_LEN, &compressed_len, plain, plain_len, compression_level);
if (r != Z_OK || compressed_len > UINT32_MAX - SWF_HEADER_LEN) {
SCFree(cws);
return NULL;
}
*cws_len = SWF_HEADER_LEN + (uint32_t)compressed_len;
return cws;
}
/**
* \test A malicious SWF whose header claims a huge decompressed size but
* carries only a few bytes of body must not drive a large allocation
* of the inspection buffer.
*/
static int DetectFiledataSwfDecompressAllocTest01(void)
{
uint8_t buffer[] = { 'C', 'W', 'S', 0x06, 0x80, 0xF0, 0xFA, 0x02, 0x00, 0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07 };
DetectEngineThreadCtx *det_ctx = SCCalloc(1, sizeof(*det_ctx));
FAIL_IF_NULL(det_ctx);
InspectionBuffer out_buffer;
memset(&out_buffer, 0, sizeof(out_buffer));
int r = FileSwfDecompression(buffer, (uint32_t)sizeof(buffer), det_ctx, &out_buffer,
HTTP_SWF_COMPRESSION_ZLIB, 0, 0);
FAIL_IF(r != 0);
FAIL_IF(out_buffer.size > SWF_DECOMPRESS_INITIAL_BUFFER_LEN);
InspectionBufferFree(&out_buffer);
AppLayerDecoderEventsFreeEvents(&det_ctx->decoder_events);
SCFree(det_ctx);
PASS;
}
/**
* \test A valid, small CWS with a forged large FileLength must only retain the
* buffer needed for its actual output.
*/
static int DetectFiledataSwfDecompressAllocTest02(void)
{
const uint8_t plain[] = "small compressed SWF body";
const uint32_t file_len = MAX_SWF_DECOMPRESSED_LEN;
uint32_t cws_len = 0;
uint8_t *cws = DetectFiledataSwfCreateCws(
plain, sizeof(plain), file_len, Z_BEST_COMPRESSION, &cws_len);
FAIL_IF_NULL(cws);
DetectEngineThreadCtx *det_ctx = SCCalloc(1, sizeof(*det_ctx));
FAIL_IF_NULL(det_ctx);
InspectionBuffer out_buffer = { 0 };
int r = FileSwfDecompression(
cws, cws_len, det_ctx, &out_buffer, HTTP_SWF_COMPRESSION_ZLIB, 0, 0);
FAIL_IF(r != 1);
FAIL_IF(out_buffer.size > SWF_DECOMPRESS_INITIAL_BUFFER_LEN);
FAIL_IF(out_buffer.len != SWF_HEADER_LEN + sizeof(plain));
FAIL_IF(memcmp(out_buffer.buf + 4, cws + 4, 4) != 0);
FAIL_IF(memcmp(out_buffer.buf + SWF_HEADER_LEN, plain, sizeof(plain)) != 0);
InspectionBufferFree(&out_buffer);
AppLayerDecoderEventsFreeEvents(&det_ctx->decoder_events);
SCFree(det_ctx);
SCFree(cws);
PASS;
}
/**
* \test A weakly compressed CWS must not reserve the maximum possible zlib
* expansion in the per-thread inspection buffer.
*/
static int DetectFiledataSwfDecompressAllocTest03(void)
{
const uint32_t plain_len = 48 * 1024;
uint8_t *plain = SCMalloc(plain_len);
FAIL_IF_NULL(plain);
for (uint32_t i = 0; i < plain_len; i++)
plain[i] = (uint8_t)i;
uint32_t cws_len = 0;
uint8_t *cws = DetectFiledataSwfCreateCws(
plain, plain_len, plain_len + SWF_HEADER_LEN, Z_NO_COMPRESSION, &cws_len);
FAIL_IF_NULL(cws);
DetectEngineThreadCtx *det_ctx = SCCalloc(1, sizeof(*det_ctx));
FAIL_IF_NULL(det_ctx);
InspectionBuffer out_buffer = { 0 };
int r = FileSwfDecompression(
cws, cws_len, det_ctx, &out_buffer, HTTP_SWF_COMPRESSION_ZLIB, 0, 0);
FAIL_IF(r != 1);
FAIL_IF(out_buffer.size > 64 * 1024);
FAIL_IF(out_buffer.len != SWF_HEADER_LEN + plain_len);
FAIL_IF(memcmp(out_buffer.buf + SWF_HEADER_LEN, plain, plain_len) != 0);
InspectionBufferFree(&out_buffer);
AppLayerDecoderEventsFreeEvents(&det_ctx->decoder_events);
SCFree(det_ctx);
SCFree(cws);
SCFree(plain);
PASS;
}
/**
* \test A ZWS with a forged large FileLength and invalid LZMA properties must
* fail without growing the inspection buffer.
*/
static int DetectFiledataSwfDecompressAllocTest04(void)
{
uint8_t buffer[] = { 'Z', 'W', 'S', 0x0D, 0x80, 0xF0, 0xFA, 0x02, 0x01, 0x00, 0x00, 0x00, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0x00 };
DetectEngineThreadCtx *det_ctx = SCCalloc(1, sizeof(*det_ctx));
FAIL_IF_NULL(det_ctx);
InspectionBuffer out_buffer = { 0 };
int r = FileSwfDecompression(buffer, (uint32_t)sizeof(buffer), det_ctx, &out_buffer,
HTTP_SWF_COMPRESSION_LZMA, 0, 0);
FAIL_IF(r != 0);
FAIL_IF(out_buffer.size > SWF_DECOMPRESS_INITIAL_BUFFER_LEN);
InspectionBufferFree(&out_buffer);
AppLayerDecoderEventsFreeEvents(&det_ctx->decoder_events);
SCFree(det_ctx);
PASS;
}
static int DetectEngineSMTPFiledataTest02(void)
{
@ -63,6 +219,14 @@ static int DetectFiledataParseTest04(void)
void DetectFiledataRegisterTests(void)
{
UtRegisterTest(
"DetectFiledataSwfDecompressAllocTest01", DetectFiledataSwfDecompressAllocTest01);
UtRegisterTest(
"DetectFiledataSwfDecompressAllocTest02", DetectFiledataSwfDecompressAllocTest02);
UtRegisterTest(
"DetectFiledataSwfDecompressAllocTest03", DetectFiledataSwfDecompressAllocTest03);
UtRegisterTest(
"DetectFiledataSwfDecompressAllocTest04", DetectFiledataSwfDecompressAllocTest04);
UtRegisterTest("DetectEngineSMTPFiledataTest02", DetectEngineSMTPFiledataTest02);
UtRegisterTest("DetectFiledataParseTest04", DetectFiledataParseTest04);
}

@ -34,6 +34,7 @@
#include "util-file-swf-decompression.h"
#include "util-misc.h"
#include "util-print.h"
#include "util-validate.h"
#define SWF_ZLIB_MIN_VERSION 0x06
#define SWF_LZMA_MIN_VERSION 0x0D
@ -59,8 +60,7 @@ int FileIsSwfFile(const uint8_t *buffer, uint32_t buffer_len)
*
* \param buffer compressed buffer
* \param buffer_len compressed buffer length
* \param decompressed_buffer buffer that store decompressed data
* \param decompressed_buffer_len decompressed data length
* \param out_buffer inspection buffer that stores decompressed data
* \param swf_type decompression algorithm to use
* \param decompress_depth how much decompressed data we want to store
* \param compress_depth how much compressed data we want to decompress
@ -124,23 +124,16 @@ int FileSwfDecompression(const uint8_t *buffer, uint32_t buffer_len,
decompressed_swf_len = MIN_SWF_LEN;
}
/* if decompress_depth is 0, keep the flash file length */
uint32_t decompressed_data_len = decompressed_swf_len;
// only restrict the decompressed_data_len, a too big decompress_depth does not make us more
// than we need
if (decompress_depth > 0 && decompress_depth < decompressed_data_len)
decompressed_data_len = decompress_depth;
uint32_t decompressed_data_limit = decompressed_swf_len;
if (decompress_depth > 0 && decompress_depth < decompressed_data_limit)
decompressed_data_limit = decompress_depth;
// cannot overflow as decompressed_swf_len maxed out at MAX_SWF_DECOMPRESSED_LEN 50000000
decompressed_data_len += 8;
/* make sure the inspection buffer has enough space */
SCInspectionBufferCheckAndExpand(out_buffer, decompressed_data_len);
if (out_buffer->size < decompressed_data_len) {
uint32_t initial_buffer_len =
MIN(SWF_DECOMPRESS_INITIAL_BUFFER_LEN, decompressed_data_limit + SWF_HEADER_LEN);
if (SCInspectionBufferCheckAndExpand(out_buffer, initial_buffer_len) == NULL) {
DetectEngineSetEvent(det_ctx, FILE_DECODER_EVENT_NO_MEM);
return 0;
}
out_buffer->len = decompressed_data_len;
/*
* FWS format
@ -152,7 +145,8 @@ int FileSwfDecompression(const uint8_t *buffer, uint32_t buffer_len,
out_buffer->buf[2] = 'S';
out_buffer->buf[3] = swf_version;
memcpy(out_buffer->buf + 4, &decompressed_swf_len, 4);
memset(out_buffer->buf + 8, 0, decompressed_data_len - 8);
uint32_t decompressed_data_produced = 0;
if ((swf_type == HTTP_SWF_COMPRESSION_ZLIB || swf_type == HTTP_SWF_COMPRESSION_BOTH) &&
compression_type == FILE_SWF_ZLIB_COMPRESSION)
@ -160,9 +154,8 @@ int FileSwfDecompression(const uint8_t *buffer, uint32_t buffer_len,
/* the first 8 bytes represents the fws header, see 'FWS format' above.
* data will start from 8th bytes
*/
r = FileSwfZlibDecompression(det_ctx,
(uint8_t *)buffer + offset, compressed_data_len,
out_buffer->buf + 8, out_buffer->len - 8);
r = FileSwfZlibDecompression(det_ctx, buffer + offset, compressed_data_len, out_buffer,
decompressed_data_limit, &decompressed_data_produced);
if (r == 0)
goto error;
@ -177,6 +170,7 @@ int FileSwfDecompression(const uint8_t *buffer, uint32_t buffer_len,
compressed_data_len += 13;
uint8_t *compressed_data = SCCalloc(1, compressed_data_len);
if (compressed_data == NULL) {
DetectEngineSetEvent(det_ctx, FILE_DECODER_EVENT_NO_MEM);
goto error;
}
/* put lzma properties */
@ -189,9 +183,8 @@ int FileSwfDecompression(const uint8_t *buffer, uint32_t buffer_len,
/* the first 8 bytes represents the fws header, see 'FWS format' above.
* data will start from 8th bytes
*/
r = FileSwfLzmaDecompression(det_ctx,
compressed_data, compressed_data_len,
out_buffer->buf + 8, out_buffer->len - 8);
r = FileSwfLzmaDecompression(det_ctx, compressed_data, compressed_data_len, out_buffer,
decompressed_data_limit, &decompressed_data_produced);
SCFree(compressed_data);
if (r == 0)
goto error;
@ -199,6 +192,9 @@ int FileSwfDecompression(const uint8_t *buffer, uint32_t buffer_len,
goto error;
}
DEBUG_VALIDATE_BUG_ON(decompressed_data_produced > decompressed_data_limit);
out_buffer->len = SWF_HEADER_LEN + decompressed_data_produced;
/* all went well so switch the buffer's inspect pointer/size
* to use the new data. */
out_buffer->inspect = out_buffer->buf;

@ -37,7 +37,6 @@
#include <zlib.h>
#define MAX_SWF_DECOMPRESSED_LEN 50000000
/*
* Return uncompressed file length
* in little-endian order
@ -75,16 +74,49 @@ uint8_t FileGetSwfVersion(const uint8_t *buffer, const uint32_t buffer_len)
return 0;
}
static bool FileSwfDecompressionBufferGrow(DetectEngineThreadCtx *det_ctx,
InspectionBuffer *out_buffer, uint32_t decompressed_data_limit)
{
const uint32_t maximum_buffer_len = decompressed_data_limit + SWF_HEADER_LEN;
uint32_t requested_len = out_buffer->size;
if (requested_len >= maximum_buffer_len)
return true;
if (requested_len == 0) {
requested_len = MIN(SWF_DECOMPRESS_INITIAL_BUFFER_LEN, maximum_buffer_len);
} else if (requested_len > maximum_buffer_len / 2) {
requested_len = maximum_buffer_len;
} else {
requested_len *= 2;
}
if (SCInspectionBufferCheckAndExpand(out_buffer, requested_len) == NULL) {
DetectEngineSetEvent(det_ctx, FILE_DECODER_EVENT_NO_MEM);
return false;
}
return true;
}
static uint32_t FileSwfDecompressionOutputCapacity(
const InspectionBuffer *out_buffer, uint32_t decompressed_data_limit)
{
if (out_buffer->size <= SWF_HEADER_LEN)
return 0;
return MIN(out_buffer->size - SWF_HEADER_LEN, decompressed_data_limit);
}
/* CWS format */
/*
* | 4 bytes | 4 bytes | n bytes |
* | 'CWS' + version | script len | compressed data |
*/
int FileSwfZlibDecompression(DetectEngineThreadCtx *det_ctx,
uint8_t *compressed_data, uint32_t compressed_data_len,
uint8_t *decompressed_data, uint32_t decompressed_data_len)
int FileSwfZlibDecompression(DetectEngineThreadCtx *det_ctx, const uint8_t *compressed_data,
uint32_t compressed_data_len, InspectionBuffer *out_buffer,
uint32_t decompressed_data_limit, uint32_t *decompressed_data_produced)
{
int ret = 1;
*decompressed_data_produced = 0;
z_stream infstream;
memset(&infstream, 0, sizeof(infstream));
infstream.zalloc = Z_NULL;
@ -93,8 +125,6 @@ int FileSwfZlibDecompression(DetectEngineThreadCtx *det_ctx,
infstream.avail_in = (uInt)compressed_data_len;
infstream.next_in = (Bytef *)compressed_data;
infstream.avail_out = (uInt)decompressed_data_len;
infstream.next_out = (Bytef *)decompressed_data;
int result = inflateInit(&infstream);
if (result != Z_OK) {
@ -102,28 +132,41 @@ int FileSwfZlibDecompression(DetectEngineThreadCtx *det_ctx,
return 0;
}
result = inflate(&infstream, Z_NO_FLUSH);
switch(result) {
case Z_STREAM_END:
while (true) {
uint32_t output_capacity =
FileSwfDecompressionOutputCapacity(out_buffer, decompressed_data_limit);
DEBUG_VALIDATE_BUG_ON(infstream.total_out > output_capacity);
infstream.avail_out = (uInt)(output_capacity - infstream.total_out);
infstream.next_out = out_buffer->buf + SWF_HEADER_LEN + infstream.total_out;
result = inflate(&infstream, Z_NO_FLUSH);
*decompressed_data_produced = (uint32_t)infstream.total_out;
if (result == Z_STREAM_END || *decompressed_data_produced == decompressed_data_limit)
break;
case Z_OK:
if (result == Z_OK && infstream.avail_in == 0)
break;
case Z_DATA_ERROR:
if (result == Z_OK && infstream.avail_out == 0) {
if (!FileSwfDecompressionBufferGrow(det_ctx, out_buffer, decompressed_data_limit)) {
ret = 0;
break;
}
continue;
}
if (result == Z_DATA_ERROR) {
DetectEngineSetEvent(det_ctx, FILE_DECODER_EVENT_Z_DATA_ERROR);
ret = 0;
break;
case Z_STREAM_ERROR:
} else if (result == Z_STREAM_ERROR) {
DetectEngineSetEvent(det_ctx, FILE_DECODER_EVENT_Z_STREAM_ERROR);
ret = 0;
break;
case Z_BUF_ERROR:
} else if (result == Z_BUF_ERROR) {
DetectEngineSetEvent(det_ctx, FILE_DECODER_EVENT_Z_BUF_ERROR);
ret = 0;
break;
default:
} else {
DetectEngineSetEvent(det_ctx, FILE_DECODER_EVENT_Z_UNKNOWN_ERROR);
ret = 0;
break;
}
ret = 0;
break;
}
inflateEnd(&infstream);
@ -135,48 +178,56 @@ int FileSwfZlibDecompression(DetectEngineThreadCtx *det_ctx,
* | 4 bytes | 4 bytes | 4 bytes | 5 bytes | n bytes | 6 bytes |
* | 'ZWS' + version | script len | compressed len | LZMA props | LZMA data | LZMA end marker |
*/
int FileSwfLzmaDecompression(DetectEngineThreadCtx *det_ctx,
uint8_t *compressed_data, uint32_t compressed_data_len,
uint8_t *decompressed_data, uint32_t decompressed_data_len)
int FileSwfLzmaDecompression(DetectEngineThreadCtx *det_ctx, const uint8_t *compressed_data,
uint32_t compressed_data_len, InspectionBuffer *out_buffer,
uint32_t decompressed_data_limit, uint32_t *decompressed_data_produced)
{
int ret = 0;
*decompressed_data_produced = 0;
size_t inprocessed = compressed_data_len;
size_t outprocessed = decompressed_data_len;
while (true) {
size_t inprocessed = compressed_data_len;
size_t outprocessed =
FileSwfDecompressionOutputCapacity(out_buffer, decompressed_data_limit);
ret = lzma_decompress(compressed_data, &inprocessed, decompressed_data, &outprocessed,
MAX_SWF_DECOMPRESSED_LEN);
ret = lzma_decompress(compressed_data, &inprocessed, out_buffer->buf + SWF_HEADER_LEN,
&outprocessed, MAX_SWF_DECOMPRESSED_LEN);
switch(ret) {
case LzmaOk:
if (ret == LzmaOk) {
*decompressed_data_produced = (uint32_t)outprocessed;
ret = 1;
break;
case LzmaIoError:
}
if (ret == LzmaOutputFull) {
*decompressed_data_produced = (uint32_t)outprocessed;
if (*decompressed_data_produced == decompressed_data_limit) {
ret = 1;
break;
}
if (!FileSwfDecompressionBufferGrow(det_ctx, out_buffer, decompressed_data_limit)) {
ret = 0;
break;
}
continue;
}
if (ret == LzmaIoError) {
DetectEngineSetEvent(det_ctx, FILE_DECODER_EVENT_LZMA_IO_ERROR);
ret = 0;
break;
case LzmaHeaderTooShortError:
} else if (ret == LzmaHeaderTooShortError) {
DetectEngineSetEvent(det_ctx, FILE_DECODER_EVENT_LZMA_HEADER_TOO_SHORT_ERROR);
ret = 0;
break;
case LzmaError:
} else if (ret == LzmaError) {
DetectEngineSetEvent(det_ctx, FILE_DECODER_EVENT_LZMA_DECODER_ERROR);
ret = 0;
break;
case LzmaMemoryError:
} else if (ret == LzmaMemoryError) {
DetectEngineSetEvent(det_ctx, FILE_DECODER_EVENT_LZMA_MEMLIMIT_ERROR);
ret = 0;
break;
case LzmaXzError:
} else if (ret == LzmaXzError) {
/* We should not see XZ compressed SWF files */
DEBUG_VALIDATE_BUG_ON(ret == LzmaXzError);
DetectEngineSetEvent(det_ctx, FILE_DECODER_EVENT_LZMA_XZ_ERROR);
ret = 0;
break;
default:
} else {
DetectEngineSetEvent(det_ctx, FILE_DECODER_EVENT_LZMA_UNKNOWN_ERROR);
ret = 0;
break;
}
ret = 0;
break;
}
return ret;

@ -28,15 +28,20 @@
/* If we don't have the decompressed data len,
* we use a default value.
*/
#define MIN_SWF_LEN 2920
#define MIN_SWF_LEN 2920
#define MAX_SWF_DECOMPRESSED_LEN 50000000
#define SWF_HEADER_LEN 8
#define SWF_DECOMPRESS_INITIAL_BUFFER_LEN 4096
uint8_t FileGetSwfVersion(const uint8_t *buffer, const uint32_t buffer_len);
uint32_t FileGetSwfDecompressedLen(const uint8_t *buffer, uint32_t buffer_len);
int FileSwfZlibDecompression(DetectEngineThreadCtx *det_ctx,
uint8_t *compressed_data, uint32_t compressed_data_len,
uint8_t *decompressed_data, uint32_t decompressed_data_len);
int FileSwfLzmaDecompression(DetectEngineThreadCtx *det_ctx,
uint8_t *compressed_data, uint32_t compressed_data_len,
uint8_t *decompressed_data, uint32_t decompressed_data_len);
int FileSwfZlibDecompression(DetectEngineThreadCtx *det_ctx, const uint8_t *compressed_data,
uint32_t compressed_data_len, InspectionBuffer *out_buffer,
uint32_t decompressed_data_limit, uint32_t *decompressed_data_produced);
int FileSwfLzmaDecompression(DetectEngineThreadCtx *det_ctx, const uint8_t *compressed_data,
uint32_t compressed_data_len, InspectionBuffer *out_buffer,
uint32_t decompressed_data_limit, uint32_t *decompressed_data_produced);
#endif /* SURICATA_UTIL_FILE_SWF_DECOMPRESSION_H */

Loading…
Cancel
Save