From 0d1bdbab671752aa579d7b4e10f4cf04d69a49a4 Mon Sep 17 00:00:00 2001 From: Dmitry Novikov Date: Sun, 14 Apr 2024 01:04:03 +0700 Subject: [PATCH] Implemented reduction of impact caused by zip-bomb exploit (#994) * Implemented reduction of impact caused by zip-bomb exploit Added network security CVars: - sv_net_incoming_decompression (0-1) Enables or disables incoming data decompression - sv_net_incoming_decompression_max_ratio (0.0 - 100.0) Sets max allowed ratio between compressed and decompressed data. (A ratio close to 90 indicates large uncompressed data with low entropy) - sv_net_incoming_decompression_max_size (16-65536) Adjusts max size of output data after decompression. Added CVar sv_net_incoming_decompression_punish for ban * Fix missing reg CVar sv_net_incoming_decompression_punish --- README.md | 4 ++ rehlds/engine/net_chan.cpp | 99 +++++++++++++++++++++++++++++++++----- 2 files changed, 91 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index d6a0b38..b824c15 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,10 @@ This means that plugins that do binary code analysis (Orpheu for example) probab
  • sv_rehlds_local_gametime <1|0> // A feature of local gametime which decrease "lags" if you run same map for a long time. Default: 0
  • sv_use_entity_file // Use custom entity file for a map. Path to an entity file will be "maps/[map name].ent". 0 - use original entities. 1 - use .ent files from maps directory. 2 - use .ent files from maps directory and create new .ent file if not exist.
  • sv_usercmd_custom_random_seed // When enabled server will populate an additional random seed independent of the client. Default: 0 +
  • sv_net_incoming_decompression <1|0> // When enabled server will decompress of incoming compressed file transfer payloads. Default: 1 +
  • sv_net_incoming_decompression_max_ratio <0|100> // Sets the max allowed ratio between compressed and uncompressed data for file transfer. (A ratio close to 90 indicates large uncompressed data with low entropy) Default: 80.0 +
  • sv_net_incoming_decompression_max_size <16|65536> // Sets the max allowed size for decompressed file transfer data. Default: 65536 bytes +
  • sv_net_incoming_decompression_punish // Time in minutes for which the player will be banned for malformed/abnormal bzip2 fragments (0 - Permanent, use a negative number for a kick). Default: -1
  • sv_tags <comma-delimited string list of tags> // Sets a string defining the "gametags" for this server, this is optional, but if it is set it allows users/scripts to filter in the matchmaking/server-browser interfaces based on the value. Default: "" diff --git a/rehlds/engine/net_chan.cpp b/rehlds/engine/net_chan.cpp index 5e51a8c..71fc0f6 100644 --- a/rehlds/engine/net_chan.cpp +++ b/rehlds/engine/net_chan.cpp @@ -36,6 +36,12 @@ cvar_t net_showpackets = { "net_showpackets", "0", 0, 0.0f, nullptr}; cvar_t net_showdrop = { "net_showdrop", "0", 0, 0.0f, nullptr}; cvar_t net_drawslider = { "net_drawslider", "0", 0, 0.0f, nullptr}; cvar_t net_chokeloopback = { "net_chokeloop", "0", 0, 0.0f, nullptr}; + +cvar_t sv_net_incoming_decompression = { "sv_net_incoming_decompression", "1", 0, 1.0f, nullptr }; +cvar_t sv_net_incoming_decompression_max_ratio = { "sv_net_incoming_decompression_max_ratio", "75.0", 0, 75.0f, nullptr }; +cvar_t sv_net_incoming_decompression_max_size = { "sv_net_incoming_decompression_max_size", "65536", 0, 65536.0f, nullptr }; +cvar_t sv_net_incoming_decompression_punish = { "sv_net_incoming_decompression_punish", "-1", 0, -1.0f, NULL }; + cvar_t sv_filetransfercompression = { "sv_filetransfercompression", "1", 0, 0.0f, nullptr}; cvar_t sv_filetransfermaxsize = { "sv_filetransfermaxsize", "10485760", 0, 0.0f, nullptr}; @@ -1433,6 +1439,9 @@ qboolean Netchan_CopyNormalFragments(netchan_t *chan) p = chan->incomingbufs[FRAG_NORMAL_STREAM]; + chan->incomingbufs[FRAG_NORMAL_STREAM] = nullptr; + chan->incomingready[FRAG_NORMAL_STREAM] = FALSE; + SZ_Clear(&net_message); MSG_BeginReading(); @@ -1470,27 +1479,87 @@ qboolean Netchan_CopyNormalFragments(netchan_t *chan) } SZ_Clear(&net_message); - - chan->incomingbufs[FRAG_NORMAL_STREAM] = nullptr; - chan->incomingready[FRAG_NORMAL_STREAM] = FALSE; - return FALSE; } #endif // REHLDS_FIXES + qboolean success = TRUE; + if (*(uint32 *)net_message.data == MAKEID('B', 'Z', '2', '\0')) { + // Determine whether decompression of compressed data is allowed +#ifdef REHLDS_FIXES + if (!sv_net_incoming_decompression.value) + { + if (chan->player_slot == 0) + { + Con_DPrintf("Incoming compressed data disallowed from\n"); + return FALSE; + } + // compressed data is expected only after requesting resource list + else if (host_client->m_sendrescount == 0) + { + Con_DPrintf("%s:Incoming compressed data disallowed from %s\n", NET_AdrToString(chan->remote_address), host_client->name); + return FALSE; + } + } +#endif + char uncompressed[65536]; - unsigned int uncompressedSize = 65536; - BZ2_bzBuffToBuffDecompress(uncompressed, &uncompressedSize, (char*)net_message.data + 4, net_message.cursize - 4, 1, 0); - Q_memcpy(net_message.data, uncompressed, uncompressedSize); - net_message.cursize = uncompressedSize; + unsigned int uncompressedSize = clamp((int)sv_net_incoming_decompression_max_size.value, 16, 65536); // valid range (16 - 65536) bytes + unsigned int compressedSize = net_message.cursize - 4; + + // Decompress net buffer data + if (success && (BZ2_bzBuffToBuffDecompress(uncompressed, &uncompressedSize, (char *)net_message.data + 4, compressedSize, 1, 0) == BZ_OK)) + { +#ifdef REHLDS_FIXES + // Check for an abnormal size ratio between compressed and uncompressed data + if (sv_net_incoming_decompression_max_ratio.value > 0 && compressedSize < uncompressedSize) + { + float ratio = ((float)(uncompressedSize - compressedSize) / uncompressedSize) * 100.0f; + if (ratio >= sv_net_incoming_decompression_max_ratio.value) + { + if (chan->player_slot == 0) + Con_DPrintf("Incoming abnormal uncompressed size with ratio %.2f\n", ratio); + else + Con_DPrintf("%s:Incoming abnormal uncompressed size with ratio %.2f from %s\n", NET_AdrToString(chan->remote_address), ratio, host_client->name); + + success = FALSE; + } + } +#endif + + // Copy uncompressed data back to the net buffer + Q_memcpy(net_message.data, uncompressed, uncompressedSize); + net_message.cursize = uncompressedSize; + } + else + { + // malformed data or compressed data exceeding sv_net_incoming_decompression_max_size + success = FALSE; + } + + // Drop client if decompression was unsuccessful + if (!success) + { + if ((chan->player_slot - 1) == host_client - g_psvs.clients) + { +#ifdef REHLDS_FIXES + if (sv_net_incoming_decompression_punish.value >= 0) + { + Con_DPrintf("%s:Banned for malformed/abnormal bzip2 fragments from %s\n", NET_AdrToString(chan->remote_address), host_client->name); + Cbuf_AddText(va("addip %.1f %s\n", sv_net_incoming_decompression_punish.value, NET_BaseAdrToString(chan->remote_address))); + } +#endif + + SV_DropClient(host_client, FALSE, "Malformed/abnormal compressed data"); + } + + SZ_Clear(&net_message); + } } - chan->incomingbufs[FRAG_NORMAL_STREAM] = nullptr; - chan->incomingready[FRAG_NORMAL_STREAM] = FALSE; - - return TRUE; + return success; } qboolean Netchan_CopyFileFragments(netchan_t *chan) @@ -1826,6 +1895,12 @@ void Netchan_Init(void) Cvar_RegisterVariable(&net_chokeloopback); Cvar_RegisterVariable(&net_drawslider); Cvar_RegisterVariable(&sv_filetransfercompression); +#ifdef REHLDS_FIXES + Cvar_RegisterVariable(&sv_net_incoming_decompression); + Cvar_RegisterVariable(&sv_net_incoming_decompression_max_ratio); + Cvar_RegisterVariable(&sv_net_incoming_decompression_max_size); + Cvar_RegisterVariable(&sv_net_incoming_decompression_punish); +#endif Cvar_RegisterVariable(&sv_filetransfermaxsize); }