CVE-2019-5187
An exploitable out-of-bounds write vulnerability exists in the TIF_read_stripdata 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.
Accusoft ImageGear 19.5.0
https://www.accusoft.com/products/imagegear/overview/
9.8 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CWE-787: Out-of-bounds Write
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:
1
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.
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
---------
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
Discovered by Emmanuel Tacheau of Cisco Talos.