Talos Vulnerability Report

TALOS-2019-0877

LEADTOOLS libltdic.so DICOM receive code execution vulnerability

December 10, 2019
CVE Number

CVE-2019-5085

Summary

An exploitable code execution 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 integer overflow, resulting in heap corruption. 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.8 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

CWE

CWE-190: Integer Overflow or Wraparound

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 writeup, 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 found in the libltdic.so library.

Since LEADTOOLS' middleware libraries are not opensource, we must examine the disassembly of a singular function to proceed. The main handler of the server is essentially a big epoll loop, listening to see if there's any new clients connecting to the server socket (as one might expect). For purposes of discussion, this function will generically be named ServerLoop for the rest of this writeup.

After being spawned in a pthread by the L_DicomListen function, the ServerLoop function starts reading network bytes at the following disassembly:

ServerLoop+493↓j
.text:00000000001FBE8F       mov     eax, r15d
.text:00000000001FBE92       mov     dword ptr [rbx+3154h], 6
.text:00000000001FBE9C       mov     edi, [rbp+4]                ; fd
.text:00000000001FBE9F       sub     eax, edx                     
.text:00000000001FBEA1       cmp     eax, ecx                     
.text:00000000001FBEA3       cmovbe  ecx, eax
.text:00000000001FBEA6       mov     edx, ecx                     
.text:00000000001FBEA8       call    _read        //[1] 
.text:00000000001FBEAD       cmp     rax, 0FFFFFFFFFFFFFFFFh  
.text:00000000001FBEB1       jz      loc_1FC120

The first call to _read at [1] will read six bytes or less from the socket into the buffer pointed to by [$rbx+3158h]. The structure at $rbx+0x3150 seems to be a struct for holding both the size of the read and the destination:

<(^_^)> x/4gx $rbx+0x3150
0x62700000da50: 0x0000000600000000      0x0000602000339f70
0x62700000da60: 0x0000000000000000      0x0000000000000000


<(^_^)> info reg rdi rsi rdx
rdi            0x8                 0x8
rsi            0x602000339f70      0x602000339f70
rdx            0x6                 0x6

Then, after the read:

<(^_^)> x/2gx 0x0000602000339f70
0x602000339f70: 0x0000000100000001     0x0000000000000000

Which corresponds to the first six bytes of the packet that we sent:

packet_list = [
('outbound', bytearray(b'\x01\x00\x00\x00\x01\x00\x00\x01\x00\x00LEAD_SERVER  [...]

After this more processing occurs to make sure that we actually ended up reading six bytes, but even more interestingly, after this, an allocation occurs:

mov     rdi, [rbx+3158h]          ; [1]  
movzx   eax, byte ptr [rdi+4] 
movzx   edx, byte ptr [rdi+3] 
shl     eax, 8
shl     edx, 10h
or      eax, edx        
movzx   edx, byte ptr [rdi+5]
or      eax, edx        
movzx   edx, byte ptr [rdi+2] 
shl     edx, 18h
or      eax, edx                  ; [2]
mov     edx, 0D9Ch
add     eax, 6                    ; [3] 
mov     [rbx+3154h], eax
mov     esi, eax
lea     rax, L_LocalRealloc_0 ; 
call    qword ptr [rax] ; L_LocalRealloc 
test    rax, rax
mov     [rbx+3158h], rax
jz      loc_1FC170

At [1], we dereference the pointer to where the six bytes landed and read them into $eax eventually at [2]. At [3], we add 6 to the total and then plug this into a call to realloc([rbx+0x3158],size), where size is our $rax+0x6. As one might guess, this situation leads to a potential integer overflow in which we can cause our chunk to shrink down to a very small size. While this in itself is an issue and we could stop here, another read occurs immediately after that is the original size we passed in the first six bytes, resulting in a completely arbitrary heap based overflow.

Crash Information

==14477==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000339e16 at pc 0x7f6a07820965 bp 0x7f6a0103bdd0 sp 0x7f6a0103b580
WRITE of size 380 at 0x602000339e16 thread T1
    #0 0x7f6a07820964 in read (/usr/lib/x86_64-linux-gnu/libasan.so.3+0x48964)
    #1 0x7f6a06abff5c  (/Leadtools/Bin/Lib/x64/libltdic.so.20+0x1fbf5c)
    #2 0x7f6a04ee44a3 in start_thread (/lib/x86_64-linux-gnu/libpthread.so.0+0x74a3)
    #3 0x7f6a055eed0e in __clone (/lib/x86_64-linux-gnu/libc.so.6+0xe8d0e)

0x602000339e16 is located 1 bytes to the right of 5-byte region [0x602000339e10,0x602000339e15)
allocated by thread T1 here:
    #0 0x7f6a0789a090 in realloc (/usr/lib/x86_64-linux-gnu/libasan.so.3+0xc2090)
    #1 0x7f6a06abff20  (/Leadtools/Bin/Lib/x64/libltdic.so.20+0x1fbf20)

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

SUMMARY: AddressSanitizer: heap-buffer-overflow (/usr/lib/x86_64-linux-gnu/libasan.so.3+0x48964) in read
Shadow bytes around the buggy address:
  0x0c048005f370: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c048005f380: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c048005f390: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c048005f3a0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c048005f3b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x0c048005f3c0: fa fa[05]fa fa fa 00 07 fa fa fd fd fa fa fd fd
  0x0c048005f3d0: fa fa 00 00 fa fa 00 00 fa fa fd fa fa fa fd fd
  0x0c048005f3e0: fa fa fd fd fa fa 00 00 fa fa 00 00 fa fa fd fa
  0x0c048005f3f0: fa fa 00 00 fa fa 00 07 fa fa 00 07 fa fa 00 07
  0x0c048005f400: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c048005f410: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Heap right redzone:      fb
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack partial redzone:   f4
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:

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 [-_-] of Cisco Talos.