CVE-2018-4039
An exploitable out-of-bounds write vulnerability exists in the PNG implementation of Atlantis Word Processor, version 3.2.7.2. This can allow an attacker to corrupt memory, which can result in code execution under the context of the application. An attacker must convince a victim to open a specially crafted document in order to trigger this vulnerability.
Atlantis Word Processor 3.2.7.1, 3.2.7.2
https://www.atlantiswordprocessor.com/en/
8.8 - CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CWE-129: Improper Validation of Array Index
Atlantis Word Processor is a traditional word processor that provides a number of useful features for a variety of users. The software is fully compatible with other word processors, such as Microsoft Office Word 2007, and even has a similar interface to Microsoft Word. Atlantis also has the ability to encrypt document files and fully customize the interface. This application is written in Delphi and contains the majority of its capabilities within a single relocatable binary.
When opening up a document that follows the open document format specification, the application will first fingerprint it in order to determine the correct file format parser which is performed by the following code. This code will first fetch the current TDoc
object and then check one of its fields that represents the current file format enumeration. When open document format is selected, this field will have the value “3,” which results in the execution of case 3. At [1], the application will then execute a function that fingerprints and continues parsing of the document.
awp+0x1b3139:
005b3139 8b45e8 mov eax,dword ptr [ebp-18h] // TDoc
005b313c 8b80dc000000 mov eax,dword ptr [eax+0DCh] // TDoc.fileFormatEnumeration
005b3142 83f805 cmp eax,5
005b3145 776a ja awp+0x1b31b1 (005b31b1)
005b3147 ff24854e315b00 jmp dword ptr awp+0x1b314e (005b314e)[eax*4]
...
awp+0x1b3193:
005b3193 55 push ebp // Case 3
005b3194 e8fbc0ffff call awp+0x1af294 (005af294) // [1]
005b3199 59 pop ecx
005b319a 8885d7f8ffff mov byte ptr [ebp-729h],al
005b31a0 eb1c jmp awp+0x1b31be (005b31be)
After determining the document type, the application will begin to parse it. When an image type is detected during parsing, the following function will be executed. This function will determine the image type and then construct an object based on this. When a PNG image is determined, the application will execute the branch at [2]. Immediately afterward at [3], the application will construct a TPNGImage
object. Once the object has been constructed, the application will make a dynamic call at [4].
awp+0x179bcb:
00579bcb 53 push ebx
00579bcc 56 push esi
00579bcd 57 push edi
00579bce 8bda mov ebx,edx
00579bd0 8bf0 mov esi,eax
00579bd2 3a5e56 cmp bl,byte ptr [esi+56h]
00579bd5 0f84fb000000 je awp+0x179cd6 (00579cd6)
00579bdb 33c0 xor eax,eax
00579bdd 55 push ebp
00579bde 68cc9c5700 push offset awp+0x179ccc (00579ccc)
00579be3 64ff30 push dword ptr fs:[eax]
00579be6 648920 mov dword ptr fs:[eax],esp
00579be9 84db test bl,bl
00579beb 7452 je awp+0x179c3f (00579c3f)
...
00579c3f 8a4608 mov al,byte ptr [esi+8] // Image Type
00579c42 2c01 sub al,1
00579c44 7204 jb awp+0x179c4a (00579c4a)
00579c46 742d je awp+0x179c75 (00579c75) // [2]
00579c48 eb48 jmp awp+0x179c92 (00579c92)
...
00579c75 b201 mov dl,1
00579c77 a1cc6b4300 mov eax,dword ptr [awp+0x36bcc (00436bcc)]
00579c7c e82f99e9ff call awp+0x135b0 (004135b0) // [3] Construct TPNGImage
00579c81 8bf8 mov edi,eax
00579c83 897e48 mov dword ptr [esi+48h],edi
00579c86 8b5610 mov edx,dword ptr [esi+10h]
00579c89 8bc7 mov eax,edi
00579c8b 8b08 mov ecx,dword ptr [eax]
00579c8d ff5138 call dword ptr [ecx+38h] // [4] Process PNG Image
00579c90 eb1b jmp awp+0x179cad (00579cad)
The dynamic function that was called will execute the following code. This function is named TIBXSQLVAR::LoadFromFile
and is used by each image type. This is a wrapper which will construct an instance of a TFileStream
at [5]. This TFileStream
object is then used to read from the image file when processing it. Immediately afterward, the application will then call a dynamic function to read the image from the TFileStream
. The function _TPNGImage_LoadFromStream
is called.
awp+0x11894:
00411894 55 push ebp
00411895 8bec mov ebp,esp
00411897 51 push ecx
00411898 53 push ebx
00411899 8bd8 mov ebx,eax
0041189b 6a20 push 20h
0041189d 8bca mov ecx,edx
0041189f a1d06b4000 mov eax,dword ptr [awp+0x6bd0 (00406bd0)]
004118a4 b201 mov dl,1
004118a6 e8b97dffff call awp+0x9664 (00409664) // [5] Construct a TFileStream
004118ab 8945fc mov dword ptr [ebp-4],eax
004118ae 33c0 xor eax,eax
004118b0 55 push ebp
004118b1 68dc184100 push offset awp+0x118dc (004118dc)
004118b6 64ff30 push dword ptr fs:[eax]
004118b9 648920 mov dword ptr fs:[eax],esp
004118bc 8b55fc mov edx,dword ptr [ebp-4]
004118bf 8bc3 mov eax,ebx
004118c1 8b08 mov ecx,dword ptr [eax]
004118c3 ff5140 call dword ptr [ecx+40h] // [6]
004118c6 33c0 xor eax,eax
Inside the _TPNGImage_LoadFromStream
function at 0x438d44, the following code will proceed to construct any required objects and then read each chunk into a list. To store each chunk, at [7] the application will construct a TChunkList
object and then store it onto the stack. Following this, the application will read eight bytes for the PNG image signature and then compare it at [8]. Once the signature is verified, the application will then enter a loop at [9] that will construct each chunk type and append it to the current chunk list. Afterward at [10], the application will make sure to initialize the TChunkIHDR
and TChunkIDAT
objects. Once these objects have been successfully constructed, the application will then enter another loop that calls the function at [11] for each IDAT
chunk that was parsed.
awp+0x38d44:
00438d44 55 push ebp
00438d45 8bec mov ebp,esp
00438d47 83c4f4 add esp,0FFFFFFF4h
....
00438d52 a1806b4300 mov eax,dword ptr [awp+0x36b80 (00436b80)]
00438d57 e8d8feffff call awp+0x38c34 (00438c34) // [7] Construct TChunkList
00438d5c 8945fc mov dword ptr [ebp-4],eax
...
00438d6d 8d55f4 lea edx,[ebp-0Ch]
00438d70 b908000000 mov ecx,8
00438d75 8bc3 mov eax,ebx
00438d77 8b30 mov esi,dword ptr [eax]
00438d79 ff5604 call dword ptr [esi+4] // Read 8 bytes from header
...
00438d7c ba1ca26600 mov edx,offset awp+0x26a21c (0066a21c)
00438d81 8d45f4 lea eax,[ebp-0Ch]
00438d84 b908000000 mov ecx,8
00438d89 e84a27fdff call awp+0xb4d8 (0040b4d8) // [8] @CompareMem
00438d8e 84c0 test al,al
00438d90 7505 jne awp+0x38d97 (00438d97)
...
awp+0x38d97:
00438d97 8bd3 mov edx,ebx
00438d99 8b45fc mov eax,dword ptr [ebp-4] // ChunkList
00438d9c e85ffdffff call awp+0x38b00 (00438b00) // [9] Construct and append chunk
00438da1 84c0 test al,al
00438da3 7414 je awp+0x38db9 (00438db9)
00438da5 8bc3 mov eax,ebx
00438da7 e81c07fdff call awp+0x94c8 (004094c8) // TStream::GetPosition
00438dac 8bf0 mov esi,eax
00438dae 8bc3 mov eax,ebx
00438db0 e82f07fdff call awp+0x94e4 (004094e4) // TStream::GetSize
00438db5 3bf0 cmp esi,eax
00438db7 7cde jl awp+0x38d97 (00438d97)
...
00438db9 8b45fc mov eax,dword ptr [ebp-4] // [10] ChunkList
00438dbc 8b5808 mov ebx,dword ptr [eax+8]
00438dbf 837b0400 cmp dword ptr [ebx+4],0
00438dc3 7427 je awp+0x38dec (00438dec)
00438dc5 8bc3 mov eax,ebx
00438dc7 e87cf0fcff call awp+0x7e48 (00407e48) // Return first index of TList
00438dcc 8b15806a4300 mov edx,dword ptr [awp+0x36a80 (00436a80)]
00438dd2 e8f99bfcff call awp+0x29d0 (004029d0) // Construct TChunkIHDR
00438dd7 84c0 test al,al
00438dd9 7411 je awp+0x38dec (00438dec)
00438ddb 8b15d86a4300 mov edx,dword ptr [awp+0x36ad8 (00436ad8)]
00438de1 8b45fc mov eax,dword ptr [ebp-4]
00438de4 e883fcffff call awp+0x38a6c (00438a6c) // Construct TChunkIDAT
00438de9 40 inc eax
00438dea 7505 jne awp+0x38df1 (00438df1)
...
awp+0x38e02:
00438e02 8b45fc mov eax,dword ptr [ebp-4] // Chunk list
00438e05 8b4008 mov eax,dword ptr [eax+8]
00438e08 8bd6 mov edx,esi
00438e0a e8fdeefcff call awp+0x7d0c (00407d0c) // Return element from chunk list
00438e0f 8b10 mov edx,dword ptr [eax]
00438e11 ff5204 call dword ptr [edx+4] // [11] TChunkIDAT
00438e14 46 inc esi
00438e15 4b dec ebx
00438e16 75ea jne awp+0x38e02 (00438e02)
Upon calling the method belonging to TChunkIDAT
, the function at 0x4379cc is then executed by the application. This function will construct a number of objects, one of which is the TZDecompressionStream
at [12]. This object is then used to decompress the data belonging to an IDAT
chunk. At [13], the application will then call a method belonging to TZDecompressionStream
to decompress the contents of the data pointed to by the current stream. This function will simply enter a loop that reads data from the IDAT
chunk within the file and then pass each chunk to the Inflate
function call at [14] to decompress.
awp+0x379cc:
004379cc 53 push ebx
004379cd 56 push esi
004379ce 57 push edi
004379cf 55 push ebp
004379d0 81c4c0fbffff add esp,0FFFFFBC0h
004379d6 8bf0 mov esi,eax
...
004379ea 8b4e08 mov ecx,dword ptr [esi+8]
004379ed b201 mov dl,1
004379ef a1f41e4300 mov eax,dword ptr [awp+0x31ef4 (00431ef4)]
004379f4 e883ecffff call awp+0x3667c (0043667c) // [12] Construct TZDecompressionStream
004379f9 89442420 mov dword ptr [esp+20h],eax
...
awp+0x37ee4:
00437ee4 8d4f01 lea ecx,[edi+1] // Loop
00437ee7 33c0 xor eax,eax
00437ee9 8ac3 mov al,bl
00437eeb 8b1484 mov edx,dword ptr [esp+eax*4]
00437eee 8b442420 mov eax,dword ptr [esp+20h] // TZDecompressionStream
00437ef2 8b28 mov ebp,dword ptr [eax]
00437ef4 ff5504 call dword ptr [ebp+4] // [13] \
...
00437f4e 80f301 xor bl,1
00437f51 ff442410 inc dword ptr [esp+10h]
00437f55 ff4c2428 dec dword ptr [esp+28h]
00437f59 7589 jne awp+0x37ee4 (00437ee4)
00437f5b eb05 jmp awp+0x37f62 (00437f62)
\
awp+0x366e8:
004366e8 53 push ebx
004366e9 56 push esi
004366ea 57 push edi
004366eb 51 push ecx
004366ec 890c24 mov dword ptr [esp],ecx
...
awp+0x36715:
00436715 837b1000 cmp dword ptr [ebx+10h],0
00436719 755a jne awp+0x36775 (00436775)
...
awp+0x36764:
00436764 8d4330 lea eax,[ebx+30h]
00436767 89430c mov dword ptr [ebx+0Ch],eax
0043676a 8b4304 mov eax,dword ptr [ebx+4]
0043676d e8562dfdff call awp+0x94c8 (004094c8) // TStream::GetPosition
00436772 894308 mov dword ptr [ebx+8],eax
00436775 8d430c lea eax,[ebx+0Ch]
00436778 33d2 xor edx,edx
0043677a e87df7ffff call awp+0x35efc (00435efc) // [14] Call to Inflate
0043677f 85c0 test eax,eax
00436781 7d05 jge awp+0x36788 (00436788)
...
00436788 837b1c00 cmp dword ptr [ebx+1Ch],0
0043678c 7f87 jg awp+0x36715 (00436715)
The Inflate
function at 0x435efc is used by the application to decompress data out of the PNG image. This function essentially is a large state machine that will process input data from the stream, and output as much data as possible before returning. At [15], a structure containing the current state is read from a property belonging to the TZDecompressionStream
object. From this structure, the first byte contains the current mode which is then used to select a case in order to handle processing the input that is to be decompressed. When handling case 7 (CODELENS), this state is then passed to the function call at [16].
awp+0x35efc:
00435efc 53 push ebx
00435efd 56 push esi
00435efe 57 push edi
00435eff 55 push ebp
00435f00 8bfa mov edi,edx
00435f02 8bd8 mov ebx,eax
...
00435f2c 8b4318 mov eax,dword ptr [ebx+18h] // [15] Inflate State
00435f2f 33d2 xor edx,edx
00435f31 8a10 mov dl,byte ptr [eax] // Current mode
00435f33 83fa0d cmp edx,0Dh
00435f36 0f8721030000 ja awp+0x3625d (0043625d)
00435f3c ff2495435f4300 jmp dword ptr awp+0x35f43 (00435f43)[edx*4]
...
00435f7b 8bd3 mov edx,ebx // Case 7
00435f7d 8b4014 mov eax,dword ptr [eax+14h]
00435f80 8bce mov ecx,esi
00435f82 e869f2ffff call awp+0x351f0 (004351f0) // [16]
00435f87 8bf0 mov esi,eax
00435f89 83fefd cmp esi,0FFFFFFFDh
00435f8c 750d jne awp+0x35f9b (00435f9b)
00435f8e 8b4318 mov eax,dword ptr [ebx+18h]
00435f91 c6000d mov byte ptr [eax],0Dh
00435f94 33d2 xor edx,edx
00435f96 895004 mov dword ptr [eax+4],edx
00435f99 eb91 jmp awp+0x35f2c (00435f2c)
The following function call is used to process the different states that are available under the CODELENS
case and is used specifically to build the different code and distance tables. Thus, this function is simply a large select statement that will branch based on the current state. At [16], this state will be fetched, and then used to determine the target address to branch to. When handling case 3, the application will allocate memory for the code lengths at [17]. During decoding, this array will be initialized with the different code lengths that were processed out of the input data. This implementation assumes that each of the code lengths that are written to this array are less than or equal to the value of 15. At [18] is the implementation of Case 5 which is used to build the code lengths table. This will take the current number of codes and adjust them before executing the block of code at [19]. The code at [19] will then set the initial lenbits
and distbits
before passing it to the actual function at [20] for processing.
awp+0x351f0:
004351f0 53 push ebx
004351f1 56 push esi
004351f2 57 push edi
004351f3 55 push ebp
004351f4 83c4d0 add esp,0FFFFFFD0h
004351f7 890c24 mov dword ptr [esp],ecx
...
00435235 33c0 xor eax,eax
00435237 8a03 mov al,byte ptr [ebx] // [16]
00435239 83f809 cmp eax,9
0043523c 0f87380b0000 ja awp+0x35d7a (00435d7a)
00435242 ff248549524300 jmp dword ptr awp+0x35249 (00435249)[eax*4]
...
awp+0x35777:
00435777 8b442404 mov eax,dword ptr [esp+4] // Length
0043577b c1e002 shl eax,2
0043577e e8e9c6fcff call awp+0x1e6c (00401e6c) // [17] System.GetMemory
00435783 89430c mov dword ptr [ebx+0Ch],eax
...
awp+0x358d0:
004358d0 8b4304 mov eax,dword ptr [ebx+4] // [18] Case 5
004358d3 89442404 mov dword ptr [esp+4],eax // Length
004358d7 8b442404 mov eax,dword ptr [esp+4] // Length
004358db 83e01f and eax,1Fh
004358de 0502010000 add eax,102h
004358e3 8b542404 mov edx,dword ptr [esp+4] // Length
004358e7 c1ea05 shr edx,5
004358ea 83e21f and edx,1Fh
004358ed 03c2 add eax,edx
004358ef 3b4308 cmp eax,dword ptr [ebx+8]
004358f2 0f8e15020000 jle awp+0x35b0d (00435b0d)
...
awp+0x35b0d:
00435b0d 33c0 xor eax,eax
00435b0f 894314 mov dword ptr [ebx+14h],eax // [19]
00435b12 c744241009000000 mov dword ptr [esp+10h],9 // lenbits
00435b1a c744241406000000 mov dword ptr [esp+14h],6 // distbits
00435b22 8b4304 mov eax,dword ptr [ebx+4]
00435b25 89442404 mov dword ptr [esp+4],eax // length
00435b29 68feffff1f push 1FFFFFFEh
00435b2e 8d442414 lea eax,[esp+14h]
00435b32 50 push eax // pointer to lenbits
00435b33 8d44241c lea eax,[esp+1Ch]
00435b37 50 push eax // pointer to distbits
00435b38 8d442434 lea eax,[esp+34h]
00435b3c 50 push eax
00435b3d 8d44243c lea eax,[esp+3Ch]
00435b41 50 push eax
00435b42 8b4324 mov eax,dword ptr [ebx+24h]
00435b45 50 push eax
00435b46 68feffff0f push 0FFFFFFEh
00435b4b 57 push edi
00435b4c 8b4b0c mov ecx,dword ptr [ebx+0Ch] // codelens array
00435b4f 8b542424 mov edx,dword ptr [esp+24h] // length
00435b53 c1ea05 shr edx,5
00435b56 83e21f and edx,1Fh
00435b59 42 inc edx
00435b5a 8b442424 mov eax,dword ptr [esp+24h] // length
00435b5e 83e01f and eax,1Fh
00435b61 0501010000 add eax,101h
00435b66 e805f4ffff call awp+0x34f70 (00434f70) // [20]
00435b6b 89442404 mov dword ptr [esp+4],eax
First, the function will store the pointers to the codelens
array and the dists
array. Next at [21], the application will allocate space for a work area when building the code table. This work area has enough space for 288 elements. This buffer will then be passed to the function at [21] along with some of the other required parameters in order to build the code length table using the input that was processed from the file. One of these required parameters is the codelens
array. As mentioned previously, this implementation later assumes that none of these codes are larger than 15, despite the fact that there are no constraints on these values.
awp+0x34f70:
00434f70 55 push ebp
00434f71 8bec mov ebp,esp
00434f73 83c4f4 add esp,0FFFFFFF4h
00434f76 53 push ebx
00434f77 56 push esi
00434f78 57 push edi
00434f79 894df8 mov dword ptr [ebp-8],ecx // codelens array
00434f7c 8955fc mov dword ptr [ebp-4],edx // dists array
00434f7f 8bf8 mov edi,eax
00434f81 33c0 xor eax,eax
00434f83 8945f4 mov dword ptr [ebp-0Ch],eax
00434f86 b880040000 mov eax,480h // 288 elements
00434f8b e8dccefcff call awp+0x1e6c (00401e6c) // [21] System.GetMemory
00434f90 8bf0 mov esi,eax
...
awp+0x34f92:
00434f92 6801010000 push 101h // extra codes
00434f97 6830a06600 push offset awp+0x26a030 (0066a030) // lengthc
00434f9c 6a1e push 1Eh // length of lengthc
00434f9e 68aca06600 push offset awp+0x26a0ac (0066a0ac) // lengtheb
00434fa3 6a1e push 1Eh // length of lengtheb
00434fa5 8b4518 mov eax,dword ptr [ebp+18h]
00434fa8 50 push eax
00434fa9 8b4520 mov eax,dword ptr [ebp+20h] // pointer to lenbits
00434fac 50 push eax
00434fad 8b4510 mov eax,dword ptr [ebp+10h]
00434fb0 50 push eax
00434fb1 8b450c mov eax,dword ptr [ebp+0Ch] // value of 0xffffffe
00434fb4 50 push eax
00434fb5 8d45f4 lea eax,[ebp-0Ch] // pointer to codelens array
00434fb8 50 push eax
00434fb9 56 push esi
00434fba 68feffff1f push 1FFFFFFEh
00434fbf 8b45f8 mov eax,dword ptr [ebp-8] // codelens array
00434fc2 8bcf mov ecx,edi
00434fc4 8b5524 mov edx,dword ptr [ebp+24h] // value of 0x1ffffffe
00434fc7 e824fbffff call awp+0x34af0 (00434af0) // [22] inflate_table
00434fcc 8bd8 mov ebx,eax
The inflate_table
function at 0x434af0 is then responsible for containing our vulnerability. First, this function copies the current number of codes and the lens
array into variables on the stack. Once this has been done, at [23], a 64-byte array will be initialized to contain the different counts. Afterward, the application will then update this array using the current code lengths that were passed to it via the %eax
register. Due to the application not checking that these code lengths are larger than 15, the loop at [24] can potentially write out of bounds of this 64-byte array. If any of the code lengths are larger than 15, then the instruction at [25] will write outside the bounds of its target array. This can cause memory corruption, which can lead to code execution under the context of the application.
awp+0x34af0:
00434af0 55 push ebp
00434af1 8bec mov ebp,esp
00434af3 81c408ffffff add esp,0FFFFFF08h
00434af9 53 push ebx
00434afa 56 push esi
00434afb 57 push edi
00434afc 894df4 mov dword ptr [ebp-0Ch],ecx // codes
00434aff 8955f8 mov dword ptr [ebp-8],edx
00434b02 8945fc mov dword ptr [ebp-4],eax // lens
...
00434b05 8d4584 lea eax,[ebp-7Ch] // count array
00434b08 ba40000000 mov edx,40h
00434b0d e87218fdff call awp+0x6384 (00406384) // [23] bzero
...
00434b12 8b4df4 mov ecx,dword ptr [ebp-0Ch] // codes
00434b15 49 dec ecx
00434b16 85c9 test ecx,ecx
00434b18 7210 jb awp+0x34b2a (00434b2a)
00434b1a 41 inc ecx
00434b1b 8b55fc mov edx,dword ptr [ebp-4] // lens
...
awp+0x34b1e: // [24] loop to update count table
00434b1e 8b02 mov eax,dword ptr [edx] // lens[index]
00434b20 ff448584 inc dword ptr [ebp+eax*4-7Ch] // [25] count[%eax]++
00434b24 83c204 add edx,4
00434b27 49 dec ecx
00434b28 75f4 jne awp+0x34b1e (00434b1e)
eax=0b28dc10 ebx=0b2183d0 ecx=0000011e edx=0b295258 esi=0b295258 edi=0000011e
eip=00434b20 esp=0018e490 ebp=0018e594 iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010206
awp+0x34b20:
00434b20 ff448584 inc dword ptr [ebp+eax*4-7Ch] ss:002b:2cbc5558=????????
0:000> u .
awp+0x34b20:
00434b20 ff448584 inc dword ptr [ebp+eax*4-7Ch]
00434b24 83c204 add edx,4
00434b27 49 dec ecx
00434b28 75f4 jne awp+0x34b1e (00434b1e)
0:000> ? poi(@ebp-c)
Evaluate expression: 286 = 0000011e
0:000> dc poi(@ebp-4)
0b295258 0b28dc10 00002da8 00000006 00000005 ..(..-..........
0b295268 00000005 00000006 00000008 00000005 ................
0b295278 00000006 00000006 00000006 00000005 ................
0b295288 00000005 00000006 00000007 00000006 ................
0b295298 00000005 00000004 00000006 00000006 ................
0b2952a8 00000006 00000006 00000005 00000006 ................
0b2952b8 00000009 00000007 00000007 00000006 ................
0b2952c8 00000005 00000006 00000007 00000005 ................
0:000> dc @ebp-7c
0018e518 00000000 00000000 00000000 00000000 ................
0018e528 00000000 00000000 00000000 00000000 ................
0018e538 00000000 00000000 00000000 00000000 ................
0018e548 00000000 00000000 00000000 00000000 ................
To use the proof of concept, simply open up or preview the document in the target application. The application should crash at the address specified due to memory corruption.
2018-11-16 - Vendor Disclosure
2018-11-20 - Vendor Patched; Public Release
Discovered by a member of Cisco Talos.