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.
LEADTOOLS libltdic.so 20.0.2019.3.15
9.8 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CWE-190: Integer Overflow or Wraparound
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 // .text:00000000001FBEAD cmp rax, 0FFFFFFFFFFFFFFFFh .text:00000000001FBEB1 jz loc_1FC120
The first call to
_read at  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] ;  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 ;  mov edx, 0D9Ch add eax, 6 ;  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 , we dereference the pointer to where the six bytes landed and read them into
$eax eventually at . At , 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.
==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 fafa 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:
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
Discovered by Lilith [-_-] of Cisco Talos.