Talos Vulnerability Report

TALOS-2019-0972

Accusoft ImageGear TIFF TIF_read_stripdata code execution vulnerability

February 5, 2020
CVE Number

CVE-2019-5187

Summary

An exploitable out-of-bounds write vulnerability exists in the TIFreadstripdata function of the igcore19d.dll library of Accusoft ImageGear 19.5.0. A specially crafted TIFF file file can cause an out-of-bounds write, resulting in a remote code execution. An attacker needs to provide a malformed file to the victim to trigger the vulnerability.

Tested Versions

Accusoft ImageGear 19.5.0

Product URLs

https://www.accusoft.com/products/imagegear/overview/

CVSSv3 Score

9.8 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

CWE

CWE-787: Out-of-bounds Write

Details

The ImageGear library is a document imaging developer toolkit providing all kinds of functionality related to image conversion, creation, editing, annotation, etc. It supports more than 100 formats, including many image formats, DICOM, PDF, Microsoft Office and others.

There is a vulnerability in the TIF_read_stripdata function. A specially crafted TIFF file can lead to an out-of-bounds write which can result in remote code execution.

Trying to load a malformed TIFF file via IG_load_file function we end up in the following situation:

(168.21ec): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=0b193f00 ebx=ffffffff ecx=0b131ff8 edx=00000006 esi=00000008 edi=00000008
eip=0f3c40d2 esp=008ff168 ebp=008ff184 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010246
igCore19d!IG_mpi_page_set+0x8d42:
0f3c40d2 88040e          mov     byte ptr [esi+ecx],al      ds:0023:0b132000=??
0:000> kb
 # ChildEBP RetAddr  Args to Child              
WARNING: Stack unwind information not available. Following frames may be wrong.
00 008ff184 0f37239d 00000001 00000000 00000001 igCore19d!IG_mpi_page_set+0x8d42
01 008ff1a8 0f4cafb2 00000001 00000000 00000001 igCore19d!IG_thread_image_unlock+0x40bd
02 008ff1dc 0f4c6623 008ff2a8 07c34d68 0b12dff8 igCore19d!IG_mpi_page_set+0x10fc22
03 008ff228 0f4c61aa 008ff814 1000001f 00000000 igCore19d!IG_mpi_page_set+0x10b293
04 008ff250 0f4cb193 008ff814 1000001f 008ff2a8 igCore19d!IG_mpi_page_set+0x10ae1a
05 008ff278 0f4c525b 008ff814 1000001f 07c34d68 igCore19d!IG_mpi_page_set+0x10fe03
06 008ff78c 0f3907c9 008ff814 07c34d68 00000001 igCore19d!IG_mpi_page_set+0x109ecb
07 008ff7c4 0f3cfb97 00000000 07c34d68 008ff814 igCore19d!IG_image_savelist_get+0xb29
08 008ffa40 0f3cf4f9 00000000 054dbfa8 00000001 igCore19d!IG_mpi_page_set+0x14807
09 008ffa60 0f366007 00000000 054dbfa8 00000001 igCore19d!IG_mpi_page_set+0x14169
0a 008ffa80 012659ac 054dbfa8 008ffb6c 008ffb90 igCore19d!IG_load_file+0x47
0b 008ffb80 012661a7 054dbfa8 008ffcb4 00000021 simple_exe_141!fuzzme+0x3c [d:\sampleimagegear\fuzzme.cpp @ 62] 
0c 008ffd4c 01266cbe 00000005 05488f60 0537af58 simple_exe_141!main+0x2d7 [d:\sampleimagegear\fuzzme.cpp @ 141] 
0d 008ffd60 01266b27 ab4ddaa0 012615e1 012615e1 simple_exe_141!invoke_main+0x1e [d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 78] 
0e 008ffdbc 012669bd 008ffdcc 01266d38 008ffddc simple_exe_141!__scrt_common_main_seh+0x157 [d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288] 
0f 008ffdc4 01266d38 008ffddc 76e4e529 00627000 simple_exe_141!__scrt_common_main+0xd [d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 331] 
10 008ffdcc 76e4e529 00627000 76e4e510 008ffe38 simple_exe_141!mainCRTStartup+0x8 [d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\startup\exe_main.cpp @ 17] 
11 008ffddc 77a49ed1 00627000 46e93324 00000000 KERNEL32!BaseThreadInitThunk+0x19
12 008ffe38 77a49ea5 ffffffff 77a93884 00000000 ntdll!__RtlUserThreadStart+0x2b
13 008ffe48 00000000 012615e1 00627000 00000000 ntdll!_RtlUserThreadStart+0x1b

As we can see, an out-of-bounds operation occurred.
In order to reach the path to this function some conditions are required:

  • tiff tag value for compression must be set to 1
  • tiff tag planar value must be 2

The pseudo-code of this vulnerable function looks like this:

LINE1  char __cdecl TIF_read_stripdata(int SamplesPerPixel, int offsetStartStripData, int bitspersample, int imagewidth, void *buffer_for_strip_data, void *dstBuffer, int dstBufferSize)
LINE2  {
LINE3    int _bits_per_sample; // ecx
LINE4    int v8; // eax
LINE5    int index; // esi
LINE6    int _SamplesPerPixel; // edi
LINE7    int byte_index; // ebx
LINE8    int max_num_bytes; // edx
LINE9    int nb_bits; // edi
LINE10   unsigned __int8 byte; // ah
LINE11   int bit_position; // edx
LINE12   char bit_value; // al
LINE13   unsigned __int8 v17; // al
LINE14   int v18; // ebx
LINE15   _BYTE *v19; // esi
LINE16   int v20; // edx
LINE17   char v21; // cl
LINE18   int v22; // ecx
LINE19   int v23; // esi
LINE20   int v24; // edx
LINE21   __int16 v25; // dx
LINE22   int round_bits_per_sample; // eax
LINE23   int _size; // ecx
LINE24   int v28; // eax
LINE25   int v29; // edx
LINE26   int *v30; // ecx
LINE27   int v31; // edx
LINE28   _WORD *v32; // ecx
LINE29   int v33; // edx
LINE30   _BYTE *v34; // ecx
LINE31   int _max_column_size; // [esp+0h] [ebp-10h]
LINE32   int _max_offseta; // [esp+0h] [ebp-10h]
LINE33   int v38; // [esp+4h] [ebp-Ch]
LINE34   int v39; // [esp+4h] [ebp-Ch]
LINE35   int current_byte_index; // [esp+8h] [ebp-8h]
LINE36   int _component_size; // [esp+Ch] [ebp-4h]
LINE37   int a3a; // [esp+20h] [ebp+10h]
LINE38   unsigned __int8 a3_3; // [esp+23h] [ebp+13h]
LINE39 
LINE40   _bits_per_sample = bitspersample;
LINE41   LOBYTE(v8) = bitspersample - 1;
LINE42   switch ( bitspersample )
LINE43   {
LINE44     case 1:
LINE45     case 2:
LINE46     case 4:
LINE47       index = offsetStartStripData;
LINE48       _SamplesPerPixel = SamplesPerPixel;
LINE49       _component_size = 8 / bitspersample;
LINE50       byte_index = 0;
LINE51       current_byte_index = 0;
LINE52       max_num_bytes = imagewidth / (8 / bitspersample);//                                [5]
LINE53       _max_column_size = max_num_bytes;
LINE54       v38 = (1 << bitspersample) - 1;
LINE55       if ( max_num_bytes > 0 )
LINE56       {
LINE57         nb_bits = 8 / bitspersample;
LINE58         do                                      //                                       [1]
LINE59         {
LINE60           byte = *((_BYTE *)buffer_for_strip_data + byte_index);//                       [6]
LINE61           if ( nb_bits > 0 )
LINE62           {
LINE63             bit_position = _bits_per_sample * (nb_bits - 1);
LINE64             do                                  //                                       [2]
LINE65             {
LINE66               bit_value = byte >> bit_position; //                                       [3]
LINE67               bit_position -= _bits_per_sample;
LINE68               *((_BYTE *)dstBuffer + index) = v38 & bit_value;//                         [7]
LINE69               index += SamplesPerPixel;
LINE70               --nb_bits;
LINE71             }
LINE72             while ( nb_bits );
LINE73             byte_index = current_byte_index;
LINE74             _bits_per_sample = bitspersample;
LINE75             nb_bits = 8 / bitspersample;
LINE76             max_num_bytes = _max_column_size;
LINE77           }
LINE78           current_byte_index = ++byte_index;
LINE79         }
LINE80         while ( byte_index < max_num_bytes );   //                                       [4]
LINE81         _SamplesPerPixel = SamplesPerPixel;
LINE82       }
LINE83    [...]
LINE84       break;
LINE85     case 8:
LINE86     case 16:
LINE87     case 32:
LINE88    [...]
LINE89       break;
LINE90  [...]
LINE91   }
LINE92   return v8;
LINE93 }
LINE94 

In this algorithm we can observe a function TIF_read_stripdata whose objective is to store stripe data into dstBuffer, using two nested loops. The first one at [1] for processing the bytes and the second one at [2] for processing bits [3].

The range for the first one is delimited by max_num_bytes at [4], derived directly from the imagewidth, which is read from the file via the tag "ImageWidth", divided by the value of bitspersample which is also read from file [5].

Bytes are read from file in [6] at offset derived from the tag "StripOffsets" in the file. Each byte is split into bits to be stored into dstBuffer at [7].
The out-of-bounds is happening while the bits values are stored into dstBuffer.
For the function TIF_read_stripdata, we can directly control almost all its parameters by manipulating the tag values directly from the file: imagewidth , buffer_for_strip_data, bitspersample, SamplesPerPixel and somehow the size of dstBuffer.

Since dstBuffer is allocated dynamically before this function call, we need to see how the size of it is computed.
The size for dstBuffer can be computed in two ways, depending of the bitspersample value, as show by the pseudo-code of the function compute_size_for_strip_buffer:

LINE1  unsigned int __stdcall compute_size_for_strip_buffer(int a1)
LINE2  {
LINE3    unsigned int result; // eax
LINE4  
LINE5    if ( getbitspersamplevalue((tags_data_struct *)a1) == 1 )                                  
LINE6      result = compute_size_based_imagewidth((tags_data_struct *)a1);                          [8]
LINE7    else
LINE8      result = compute_size_based_imagewidth_bits_per_sample((tags_data_struct *)a1);          [9]
LINE9    return result;
LINE10 }

We can observe that function compute_size_based_imagewidth is called when the returned value by getbitspersamplevalue is equal to 1, otherwise function compute_size_based_imagewidth_bits_per_sample is called.
Next, let's see the pseudo-code of function compute_size_based_imagewidth:

LINE1  unsigned int __stdcall compute_size_based_imagewidth(tags_data_struct *a1)
LINE2  {
LINE3    int v2; // [esp+0h] [ebp-24h]
LINE4    int *v3; // [esp+14h] [ebp-10h]
LINE5    int v4; // [esp+20h] [ebp-4h]
LINE6  
LINE7    v3 = &v2;
LINE8    v4 = 0;
LINE9    if ( a1->field_18 != 1 )
LINE10     wrapper_throw_exception(-401, 0, 0, 0, "..\\..\\..\\..\\Common\\Core\\c_DIB.cpp", 587);
LINE11   return ((a1->imagewidth + 31) >> 3) & 0xFFFFFFFC;//                                        [10]
LINE12                                                  
LINE13 }

The compute_size_based_imagewidth is straightforward and is computing the size only considering the value of a1->imagewidth which corresponds to the "ImageWidth" tag value. In [10] we can summarize the formula to compute the size to:

size = ((imagewidth  + 31) / 8) & 0xFFFFFFFC

The second path taken at [9] for function compute_size_based_imagewidth_bits_per_sample is leading to a subroutine named sub_F869320:

LINE1  unsigned int __thiscall sub_F869320(_DWORD *this)
LINE2  {
LINE3    return ((CONTAINING_RECORD(this, buffer_struct, field_0)->imagewidth
LINE4           * CONTAINING_RECORD(this, tags_data_struct, field_0)->samplePerPixel
LINE5           * CONTAINING_RECORD(this, tags_data_struct, field_0)->round_bits_per_sample         [11]
LINE6           + 31) >> 3) & 0xFFFFFFFC;
LINE7  }

We can summarize the formula of the computed size in this case by the following:

size = (((imagewidth * samplePerPixel * round_bits_per_sample) + 31) / 8) & 0xFFFFFFFC

We can easily notice the presence of samplePerPixel and round_bits_per_sample in the formula at [11].
Note however that round_bits_per_sample is not the exact value extracted from the "BitsPerSample" tag, but rather a rounded value which is computed by the function round_max_bits_per_sample.
By looking at pseudo-code we can see the value corresponding to the "BitsPerSample" tag, which is rounded to the upper value at [12].

LINE1  int __cdecl round_max_bits_per_sample(int bitsperSample)
LINE2  {
LINE3    int index; // eax
LINE4    int list_bit_per_sample_value[] = {32, 16,8}
LINE5    index = 3;
LINE6    while ( bitsperSample > list_bit_per_sample_value[--index] )                                   [12]
LINE7    {
LINE8      if ( !index )
LINE9        wrapper_throw_exception(
LINE10         -1,
LINE11         "Unsupported depth specified.",
LINE12         bitsperSample,
LINE13         0,
LINE14         "..\\..\\..\\..\\Common\\Core\\Channel.cpp",
LINE15         125);
LINE16   }
LINE17   return list_bit_per_sample_value[index];
LINE18 }

The big difference between the two functions compute_size_based_imagewidth and compute_size_based_imagewidth_bits_per_sample is that in the former the value of the "BitsPerSample" tag is not rounded to 8, leading to the allocation of a buffer which is too small.

Crash Information

0:000> !analyze -v
*******************************************************************************
*                                                                             *
*                        Exception Analysis                                   *
*                                                                             *
*******************************************************************************


KEY_VALUES_STRING: 1

    Key  : AV.Fault
    Value: Write

    Key  : Analysis.CPU.Sec
    Value: 2

    Key  : Analysis.DebugAnalysisProvider.CPP
    Value: Create: 8007007e on DESKTOP-QA0AMQQ

    Key  : Analysis.DebugData
    Value: CreateObject

    Key  : Analysis.DebugModel
    Value: CreateObject

    Key  : Analysis.Elapsed.Sec
    Value: 27

    Key  : Analysis.Memory.CommitPeak.Mb
    Value: 93

    Key  : Analysis.System
    Value: CreateObject

    Key  : Timeline.OS.Boot.DeltaSec
    Value: 81148

    Key  : Timeline.Process.Start.DeltaSec
    Value: 193


NTGLOBALFLAG:  2100000

APPLICATION_VERIFIER_FLAGS:  0

APPLICATION_VERIFIER_LOADED: 1

EXCEPTION_RECORD:  (.exr -1)
ExceptionAddress: 0f3c40d2 (igCore19d!IG_mpi_page_set+0x00008d42)
   ExceptionCode: c0000005 (Access violation)
  ExceptionFlags: 00000000
NumberParameters: 2
   Parameter[0]: 00000001
   Parameter[1]: 0b132000
Attempt to write to address 0b132000

FAULTING_THREAD:  000021ec

PROCESS_NAME:  simple.exe_141.exe

WRITE_ADDRESS:  0b132000 

ERROR_CODE: (NTSTATUS) 0xc0000005 - L'instruction   0x%p emploie l'adresse m moire 0x%p. L' tat de la m moire ne peut pas  tre %s.

EXCEPTION_CODE_STR:  c0000005

EXCEPTION_PARAMETER1:  00000001

EXCEPTION_PARAMETER2:  0b132000

STACK_TEXT:  
WARNING: Stack unwind information not available. Following frames may be wrong.
008ff184 0f37239d 00000001 00000000 00000001 igCore19d!IG_mpi_page_set+0x8d42
008ff1a8 0f4cafb2 00000001 00000000 00000001 igCore19d!IG_thread_image_unlock+0x40bd
008ff1dc 0f4c6623 008ff2a8 07c34d68 0b12dff8 igCore19d!IG_mpi_page_set+0x10fc22
008ff228 0f4c61aa 008ff814 1000001f 00000000 igCore19d!IG_mpi_page_set+0x10b293
008ff250 0f4cb193 008ff814 1000001f 008ff2a8 igCore19d!IG_mpi_page_set+0x10ae1a
008ff278 0f4c525b 008ff814 1000001f 07c34d68 igCore19d!IG_mpi_page_set+0x10fe03
008ff78c 0f3907c9 008ff814 07c34d68 00000001 igCore19d!IG_mpi_page_set+0x109ecb
008ff7c4 0f3cfb97 00000000 07c34d68 008ff814 igCore19d!IG_image_savelist_get+0xb29
008ffa40 0f3cf4f9 00000000 054dbfa8 00000001 igCore19d!IG_mpi_page_set+0x14807
008ffa60 0f366007 00000000 054dbfa8 00000001 igCore19d!IG_mpi_page_set+0x14169
008ffa80 012659ac 054dbfa8 008ffb6c 008ffb90 igCore19d!IG_load_file+0x47
008ffb80 012661a7 054dbfa8 008ffcb4 00000021 simple_exe_141!fuzzme+0x3c
008ffd4c 01266cbe 00000005 05488f60 0537af58 simple_exe_141!main+0x2d7
008ffd60 01266b27 ab4ddaa0 012615e1 012615e1 simple_exe_141!invoke_main+0x1e
008ffdbc 012669bd 008ffdcc 01266d38 008ffddc simple_exe_141!__scrt_common_main_seh+0x157
008ffdc4 01266d38 008ffddc 76e4e529 00627000 simple_exe_141!__scrt_common_main+0xd
008ffdcc 76e4e529 00627000 76e4e510 008ffe38 simple_exe_141!mainCRTStartup+0x8
008ffddc 77a49ed1 00627000 46e93324 00000000 KERNEL32!BaseThreadInitThunk+0x19
008ffe38 77a49ea5 ffffffff 77a93884 00000000 ntdll!__RtlUserThreadStart+0x2b
008ffe48 00000000 012615e1 00627000 00000000 ntdll!_RtlUserThreadStart+0x1b


STACK_COMMAND:  ~0s ; .cxr ; kb

SYMBOL_NAME:  igCore19d!IG_mpi_page_set+8d42

MODULE_NAME: igCore19d

IMAGE_NAME:  igCore19d.dll

FAILURE_BUCKET_ID:  INVALID_POINTER_WRITE_AVRF_c0000005_igCore19d.dll!IG_mpi_page_set

OS_VERSION:  10.0.17763.1

BUILDLAB_STR:  rs5_release

OSPLATFORM_TYPE:  x86

OSNAME:  Windows 10

FAILURE_ID_HASH:  {39ff52ad-9054-81fd-3e4d-ef5d82e4b2c1}

Followup:     MachineOwner
---------

Timeline

2019-12-17 - Vendor Disclosure
2020-01-28 - Vendor announced fixed for the Linux platform, in ImageGear for C & C++ v18.7
2020-02-05 - Vendor released fixed for Windows
2020-02-05 - Public Release

Credit

Discovered by Emmanuel Tacheau of Cisco Talos.