Talos Vulnerability Report

TALOS-2019-0882

LEADTOOLS libltdic.so DICOM LDicomNet::receive information disclosure vulnerability

December 10, 2019
CVE Number

CVE-2019-5090

Summary

An exploitable information disclosure vulnerability exists in the DICOM packet-parsing functionality of LEADTOOLS libltdic.so, version 20.0.2019.3.15. A specially crafted packet can cause an out-of-bounds read, resulting in information disclosure. An attacker can send a packet to trigger this vulnerability.

Tested Versions

LEADTOOLS libltdic.so 20.0.2019.3.15

Product URLs

https://www.leadtools.com/

CVSSv3 Score

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

CWE

CWE-125: Out-of-bounds Read

Details

LEADTOOLS, according to the website, "is a collection of comprehensive toolkits to integrate document, medical, multimedia, and imaging technologies into desktop, server, tablet, and mobile applications." It offers prebuilt and portable libraries with an SDK for most platforms (Windows, Linux, Android, etc), that are all geared toward building applications for medical systems.

For the purposes of this post, we will be discussing LEADTOOLS' DICOM network protocol code, which is mainly used for transferring and viewing medical image information remotely.

When dealing with the LEADTOOLS DICOM code, the main function that actually leads to network library code is L_LTDIC_API L_INT L_DicomListen(hNet, pszHostAddress, nHostPort, nNbPeers). This function will take an initialized piece of memory (hNet), an IP address and port to listen on (pszHostAddress/nHostPort), a max amount of peers to listen to (nNbPeers), and then spin up a network server that implements the actual network logic. For dealing with certain predefined function types, callbacks are created and then assigned to predefined hooks within the hNet object, for example, when looking through the SDK's example code:

void OnReceiveAssociateRequest(HDICOMNET dicomNet, HDICOMPDU dicomPDU, void *userData)
void OnReceiveCEchoRequest(HDICOMNET dicomNet, L_UCHAR presentationID, L_UINT16 messageID, L_TCHAR *className, void *userData)
[..]

Since the listed functions are, in the end, up to the developers to implement, they are not really valid targets, but in order to get to these callbacks, there is still a bit of code connecting the packet read to the callback hooks found in the libltdic.so library.

Since LEADTOOLS' middleware libraries are not open-source, we must examine the disassembly of a singular function to proceed. After passing the first six bytes of the packet, which are the opcode and size of the input, we end up hitting a jumptable of the opcodes inside LDicomNet::Receive(int flag?,uchar opcode,uchar *buffer,uint buffer_len), which is:

PDU_UNKNOWN    [0x00] Unknown data sent.
PDU_ASSOCIATE_REQUEST  [0x01] Associate Request message sent
PDU_ASSOCIATE_ACCEPT   [0x02] Associate Accept message sent
PDU_ASSOCIATE_REJECT   [0x03] Associate Reject message sent
PDU_DATA_TRANSFER  [0x04] Data transfer made.
PDU_RELEASE_REQUEST    [0x05] Release Request message sent
PDU_RELEASE_RESPONSE   [0x06] Release Response message sent
PDU_ABORT  [0x07] Abort message sent.

The main codeflow for most DICOM functionality is under the PDU_DATA_TRANSFER opcode, which, assuming our provided size in bytes[2:6] is big enough, will cause us to hit the LDicomFile::Write(void *pBuffer, uint nLength) function, where by nLength is the provided bytes at offset[6:10], and pBuffer points to the rest of our bytes at offset[11]. The function will then allocate or reallocate a buffer of size nLength and proceed to read in that many bytes from our packet buffer into the new allocation.

The issue lies in the fact that there is no correspondence or checking between the length of our provided packet size at offset[2:6] and the provided file size at offset[6:10]. Thus, when populating the file from our buffer, the following code is run:

.text:00007F9484AE67A9 loc_7F9484AE67A9:                       ; CODE XREF: LDicomFile::Write(void *,uint)+207↓j
.text:00007F9484AE67A9                                         ; LDicomFile::Write(void *,uint)+274↓j
.text:00007F9484AE67A9                 lea     rdi, [rax+rdx]
.text:00007F9484AE67AD                 mov     rsi, src_buff  //[1]
.text:00007F9484AE67B0                 mov     rdx, rbp       //[2]
.text:00007F9484AE67B3                 call    memcpy_0

If the size of our packet buffer [1] is smaller than our declared file size [2], the memcpy will easily read an arbitrary amount out of bounds of our packet into the heap and be stored into the LDicomFile's m_pBuffer field.
From here it's very implementation dependent as to what to do with this out of bounds data, but in most cases, it should suffice to send a L_DicomSendCStoreRequest request to store the out of bounds data in the Dicom Datastore, and then a L_DicomSendCGetRequest to read it out. In cases where this functionality is not included, this vulnerability could still be used to potentially hit unmapped memory and cause a segfault for a DOS.

Crash Information

==54658==ERROR:AddressSanitizer: heap-buffer-overflow on address 0x60b000414bd4 at pc 0x7f4e39ccef7f bp 0x7f4e3353bc50 sp 0x7f4e3353b400
READof size 1048664 at 0x60b000414bd4 thread T1
   #0 0x7f4e39ccef7e  (/usr/lib/x86_64-linux-gnu/libasan.so.3+0x5cf7e)
   #1 0x7f4e38f9471e  (//boop/Leadtools/Bin/Lib/x64/libltdic.so.20+0x23671e)
   #2 0x7f4e38f877b7 in LDicomFile::Write(void*, unsigned int) (/boop/Leadtools/Bin/Lib/x64/libltdic.so.20+0x2297b7)
   #3 0x7f4e38f59399 in LDicomNet::Receive(int, unsigned char, unsigned char*, unsigned int) (/boop/Leadtools/Bin/Lib/x64/libltdic.so.20+0x1fb399)
   #4 0x7f4e38f59f88  (/boop/Leadtools/Bin/Lib/x64/libltdic.so.20+0x1fbf88)
   #5 0x7f4e3737e4a3 in start_thread (/lib/x86_64-linux-gnu/libpthread.so.0+0x74a3)
   #6 0x7f4e37a88d0e in __clone (/lib/x86_64-linux-gnu/libc.so.6+0xe8d0e)

0x60b000414bd4is located 0 bytes to the right of 100-byte region [0x60b000414b70,0x60b000414bd4)
allocatedby thread T1 here:
   #0 0x7f4e39d34090 in realloc (/usr/lib/x86_64-linux-gnu/libasan.so.3+0xc2090)
   #1 0x7f4e38f59f20  (/boop/Leadtools/Bin/Lib/x64/libltdic.so.20+0x1fbf20)

ThreadT1 created by T0 here:
   #0 0x7f4e39ca2f59 in __interceptor_pthread_create (/usr/lib/x86_64-linux-gnu/libasan.so.3+0x30f59)
   #1 0x7f4e38f5572c in LDicomNet::Listen(char*, unsigned int, int, int) (//boop/Leadtools/Bin/Lib/x64/libltdic.so.20+0x1f772c)

SUMMARY:AddressSanitizer: heap-buffer-overflow (/usr/lib/x86_64-linux-gnu/libasan.so.3+0x5cf7e)
Shadowbytes around the buggy address:
 0x0c168007a920: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
 0x0c168007a930: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
 0x0c168007a940: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
 0x0c168007a950: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
 0x0c168007a960: fa fa fa fa fa fa fa fa fa fa fa fa fa fa 00 00
=>0x0c168007a970:00 00 00 00 00 00 00 00 00 00[04]fa fa fa fa fa
 0x0c168007a980: fa fa fa fa 00 00 00 00 00 00 00 00 00 00 00 00
 0x0c168007a990: 00 fa fa fa fa fa fa fa fa fa 00 00 00 00 00 00
 0x0c168007a9a0: 00 00 00 00 00 00 00 fa fa fa fa fa fa fa fa fa
 0x0c168007a9b0: fd fd fd fd fd fd fd fd fd fd fd fd fd fa fa fa
 0x0c168007a9c0: fa fa fa fa fa fa fd fd fd fd fd fd fd fd fd fd
Shadowbyte legend (one shadow byte represents 8 application bytes):
 Addressable:           00
 Heap left redzone:     fa
==54658==ABORTING


0x00007ffff6acf0e3in ?? () from /lib/x86_64-linux-gnu/libc.so.6
-------------------------------[registers ]----
$rax  : 0x00007ffff7ec2018 -> 0x0000000000000000
$rbx  : 0x00007ffff0003260 -> 0x0000000000000000
$rcx  : 0x00007ffff0004e8c -> 0x0000000400000000
$rdx  : 0x0000000000100058
$rsp  : 0x00007ffff4cbbcd8 -> 0x00007ffff732871f ->  leave
$rbp  : 0x00007ffff4cbbd00 -> 0x0000000000100058 ("X"?)
$rsi  : 0x00007ffff0004e8c -> 0x0000000400000000
$rdi  : 0x00007ffff7ec2018 -> 0x0000000000000000
$rip  : 0x00007ffff6acf0e3 ->  movups xmm8, XMMWORD PTR [rsi+rdx*1-0x10]
$r8   : 0xffffffffffffffff
$r9   : 0x0000000000000000
$r10  : 0x000000000000024e
$r11  : 0x00007ffff7b583e0 -> <WinGlobalLock+0> lea rax, [rdi-0x1]
$r12  : 0x00007ffff0004e8c -> 0x0000000400000000
$r13  : 0x0000000000000006
$r14  : 0x0000000000000001
$r15  : 0x00007ffff0004e8b -> 0x0000040000000003
$eflags:[carry parity ADJUST zero sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
-----------------------------------[stack ]----
0x00007ffff4cbbcd8|+0x00:0x00007ffff732871f ->  leave  <-$rsp
0x00007ffff4cbbce0|+0x08:0x00007ffff0003260 -> 0x0000000000000000
0x00007ffff4cbbce8|+0x10:0x0000000000100058 ("X"?)
0x00007ffff4cbbcf0|+0x18:0x00007ffff0004e8c -> 0x0000000400000000
0x00007ffff4cbbcf8|+0x20:0x00007ffff7ec2018 -> 0x0000000000000000
0x00007ffff4cbbd00|+0x28:0x0000000000100058 ("X"?)     <-$rbp
0x00007ffff4cbbd08|+0x30:0x00007ffff731b7b8 -> <LDicomFile::Write(void*,+0> add QWORD PTR [rbx+0x120], rbp
0x00007ffff4cbbd10|+0x38:0x000000000010005a ("Z"?)
-------------------------------[boop ]----
  0x7ffff6acf0d2                  and    eax, ebx
  0x7ffff6acf0d4                  movups xmm4, XMMWORD PTR [rsi]
  0x7ffff6acf0d7                  movups xmm5, XMMWORD PTR [rsi+0x10]
  0x7ffff6acf0db                  movups xmm6, XMMWORD PTR [rsi+0x20]
  0x7ffff6acf0df                  movups xmm7, XMMWORD PTR [rsi+0x30]
->0x7ffff6acf0e3                 movups xmm8, XMMWORD PTR [rsi+rdx*1-0x10]
  0x7ffff6acf0e9                  lea    r11, [rdi+rdx*1-0x10]
  0x7ffff6acf0ee                  lea    rcx, [rsi+rdx*1-0x10]
  0x7ffff6acf0f3                  mov    r9, r11
  0x7ffff6acf0f6                  mov    r8, r11
  0x7ffff6acf0f9                  and    r8, 0xf
-----------------------------------[trace ]----
#0 0x00007ffff6acf0e3 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
#1 0x00007ffff732871f in ?? () from /boop/Leadtools/Bin/Lib/x64/libltdic.so.20
#2 0x00007ffff731b7b8 in LDicomFile::Write(void*, unsigned int) () from /boop/Leadtools/Bin/Lib/x64/libltdic.so.20
#3 0x00007ffff72ed39a in LDicomNet::Receive(int, unsigned char, unsigned char*, unsigned int) () from /boop/Leadtools/Bin/Lib/x64/libltdic.so.20
#4 0x00007ffff72edf89 in ?? () from /boop/Leadtools/Bin/Lib/x64/libltdic.so.20
------------------------------------------------

Timeline

2019-08-08 - Vendor Disclosure
2019-09-06 - 30 day follow up with vendor re: case number assigned
2019-09-09 - Vendor acknowledged issue under review
2019-10-21 - 60+ day follow up
2019-12-05 - Vendor patched
2019-12-10 - Public Release

Credit

Discovered by Lilith [-_-]zzzZZZ of Cisco Talos.