CVE-2019-5090
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.
LEADTOOLS libltdic.so 20.0.2019.3.15
9.1 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:H
CWE-125: Out-of-bounds Read
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.
==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
------------------------------------------------
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 [-_-]zzzZZZ of Cisco Talos.