Talos Vulnerability Report

TALOS-2019-0815

NitroPDF jpeg2000 yTsiz Remote Code Execution Vulnerability

October 9, 2019
CVE Number

CVE-2019-5046

Summary

A specifically crafted jpeg2000 file embedded in a PDF file can lead to a heap corruption when opening a PDF document in NitroPDF 12.12.1.522. With careful memory manipulation, this can lead to arbitrary code execution. In order to trigger this vulnerability, the victim would need to open the malicious file.

Tested Versions

NitroPDF 12.12.1.522

Product URLs

https://www.gonitro.com/

CVSSv3 Score

8.8 - CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

CWE

CWE-122: Heap Based Buffer Overflow

Details

An potential remote code execution vulnerability exists in the PDF parsing functionality of Nitro Pro. A specially crafted PDF file can cause a vulnerability resulting in potential code execution.

While parsing an embedded jpeg2000 file with an unusually large xSiz and yTsiz, an integer overflow can happen while calculating the size of memory required for each pixel. Due to an integer overflow, an undersized chunk of memory is allocated and is then written to in a loop. Since the memory chunk is undersized, the process will write outside of its bounds and overwrite adjacent memory.

The memory allocation happens at npdf!CosCryptGetVersion+0x8a7c via VirtualAlloc. The size argument for VirtualAlloc is calculated in the calling function:

Breakpoint 3 hit
npdf!CosCryptGetVersion+0x9c9d:
00007fff`b208081d 03c2            add     eax,edx
0:000> t
npdf!CosCryptGetVersion+0x9c9f:
00007fff`b208081f c1f803          sar     eax,3
0:000> t
npdf!CosCryptGetVersion+0x9ca2:
00007fff`b2080822 448bf8          mov     r15d,eax
0:000> t
npdf!CosCryptGetVersion+0x9ca5:
00007fff`b2080825 488b8da0070000  mov     rcx,qword ptr [rbp+7A0h] ss:0000007c`8b9f5ca0=0000007c8b9f5cb8
0:000> t
npdf!CosCryptGetVersion+0x9cac:
00007fff`b208082c 8b510c          mov     edx,dword ptr [rcx+0Ch] ds:0000007c`8b9f5cc4=02000000
0:000> ?edx
Evaluate expression: 0 = 00000000`00000000
0:000> t
npdf!CosCryptGetVersion+0x9caf:
00007fff`b208082f 488b8de00f0000  mov     rcx,qword ptr [rbp+0FE0h] ss:0000007c`8b9f64e0=0000007c8b9f64f8
0:000> ?edx
Evaluate expression: 33554432 = 00000000`02000000                   [1]
0:000> t
npdf!CosCryptGetVersion+0x9cb6:
00007fff`b2080836 0faf11          imul    edx,dword ptr [rcx] ds:0000007c`8b9f64f8=0000002c
0:000> ?edx
Evaluate expression: 33554432 = 00000000`02000000                   [2]
0:000> t
npdf!CosCryptGetVersion+0x9cb9:
00007fff`b2080839 0faf5708        imul    edx,dword ptr [rdi+8] ds:0000007c`8b9f75e8=00000003
0:000> ?edx
Evaluate expression: 1476395008 = 00000000`58000000                 [3]
0:000> t
npdf!CosCryptGetVersion+0x9cbd:
00007fff`b208083d 0fafd0          imul    edx,eax
0:000> ?edx
Evaluate expression: 134217728 = 00000000`08000000                  [4]

In the above output, we can see the size is being calculated. At [1], edx first contains the value corresponding to xSiz value from the sample that triggers this vulnerability. In this case , it is 0x02000000. At [2], this value will be multiplied by 0x2c which is the value of yTsiz, a tile height value. At [3], an already large value is multiplied by 3 which leads to an integer overflow, as only edx is used as a size parameter. If we continue execution to the VirtualAlloc call, we can see this being the case:

0:000> bp npdf!CosCryptGetVersion+0x8a7c
0:000> g
Breakpoint 4 hit
npdf!CosCryptGetVersion+0x8a7c:
00007fff`b207f5fc ff1596212f00    call    qword ptr [npdf!nitro::digital_signature::signature_verifier::GetSignerCertificate+0x1754a8 (00007fff`b2371798)] ds:00007fff`b2371798={KERNEL32!VirtualAllocStub (00007fff`dc9e93c0)}
0:000> u
npdf!CosCryptGetVersion+0x8a7c:
00007fff`b207f5fc ff1596212f00    call    qword ptr [npdf!nitro::digital_signature::signature_verifier::GetSignerCertificate+0x1754a8 (00007fff`b2371798)]
00007fff`b207f602 488bf0          mov     rsi,rax
00007fff`b207f605 4885c0          test    rax,rax
00007fff`b207f608 753e            jne     npdf!CosCryptGetVersion+0x8ac8 (00007fff`b207f648)
00007fff`b207f60a 488d05dfea3300  lea     rax,[npdf!CAPFileSpecifier::`vftable'+0x49748 (00007fff`b23be0f0)]
00007fff`b207f611 4889442430      mov     qword ptr [rsp+30h],rax
00007fff`b207f616 448d4601        lea     r8d,[rsi+1]
00007fff`b207f61a 488d542430      lea     rdx,[rsp+30h]
0:000> ?rdx
Evaluate expression: 134217728 = 00000000`08000000
0:000> p
npdf!CosCryptGetVersion+0x8a82:
00007fff`b207f602 488bf0          mov     rsi,rax
0:000> ?rax
Evaluate expression: 1848504483840 = 000001ae`63740000              [5]

At [5] we see the pointer to allocated memory chunk in rax.

If we continue the execution, the heap overflow happens in function at npdf!nitro::notifications::notification_manager::FindNotification+0x753c0. Indeed, if we break at the beginning of this function, we see the above allocated buffer as first argument:

Breakpoint 5 hit
npdf!nitro::notifications::notification_manager::FindNotification+0x753c0:
00007fff`b2119ee0 48895c2408      mov     qword ptr [rsp+8],rbx ss:0000007c`8b9f5250=000001ae61efaf28
0:000> ?rcx
Evaluate expression: 1848504483840 = 000001ae`63740000

This function basically writes byte by byte values for a single component. First time it is called, it starts right at the beginning of our chunk. Second time it starts at offset 1, doing the second component. Third time for offset two, doing the third component:

Breakpoint 5 hit
npdf!nitro::notifications::notification_manager::FindNotification+0x753c0:
00007fff`b2119ee0 48895c2408      mov     qword ptr [rsp+8],rbx ss:0000007c`8b9f5250=000001ae61efaf28
0:000> ?rcx
Evaluate expression: 1848504483841 = 000001ae`63740001
0:000> g
Breakpoint 5 hit
npdf!nitro::notifications::notification_manager::FindNotification+0x753c0:
00007fff`b2119ee0 48895c2408      mov     qword ptr [rsp+8],rbx ss:0000007c`8b9f5250=000001ae61efaf28
0:000> ?rcx
Evaluate expression: 1848504483842 = 000001ae`63740002

When it's called once more, it's using the second third of the chunk, starting at offset 0x06000000, which is still well within the allocated chunk. The actual heap overflow and memory corruption happens when the function is called the 7th time, this time with offset of 0x0c000000, which is now more than the initially allocated buffer, and actually points to a next memory chunk which in this case happens to be allocated via heap:

Breakpoint 5 hit
npdf!nitro::notifications::notification_manager::FindNotification+0x753c0:
00007fff`b2119ee0 48895c2408      mov     qword ptr [rsp+8],rbx ss:0000007c`8b9f5250=000001ae61efaf28
0:000> ?rcx
Evaluate expression: 1848705810432 = 000001ae`6f740000
0:000> ?rcx-000001ae`63740002+2
Evaluate expression: 201326592 = 00000000`0c000000
0:000> !heap -p -a rcx
    address 000001ae6f740000 found in
    _DPH_HEAP_ROOT @ 1ae2d751000
    in busy allocation (  DPH_HEAP_BLOCK:         UserAddr         UserSize -         VirtAddr         VirtSize)
                             1ae62757000:      1ae6b750d80          438627f -      1ae6b750000          4388000
    00007fffdcc8f4bf ntdll!RtlDebugAllocateHeap+0x000000000000003f
    00007fffdcc3b530 ntdll!RtlpAllocateHeap+0x000000000008f760
    00007fffdcba9725 ntdll!RtlpAllocateHeapInternal+0x00000000000005e5
    00007fffc8a86a57 MSVCR120!malloc+0x000000000000005b [f:\dd\vctools\crt\crtw32\heap\malloc.c @ 92]
    00007fffb20eb7c1 npdf!nitro::notifications::notification_manager::FindNotification+0x0000000000046ca1
    00007fffb2139cc1 npdf!nitro::notifications::notification_manager::FindNotification+0x00000000000951a1
    00007fffb2139064 npdf!nitro::notifications::notification_manager::FindNotification+0x0000000000094544
    00007fffb2139432 npdf!nitro::notifications::notification_manager::FindNotification+0x0000000000094912
    00007fffb21183e2 npdf!nitro::notifications::notification_manager::FindNotification+0x00000000000738c2
    00007fffb21193e7 npdf!nitro::notifications::notification_manager::FindNotification+0x00000000000748c7
    00007fffb2081e88 npdf!CosCryptGetVersion+0x000000000000b308
    00007fffb2080c4b npdf!CosCryptGetVersion+0x000000000000a0cb
    00007fffb2081a77 npdf!CosCryptGetVersion+0x000000000000aef7
    00007fffb20837be npdf!CosCryptGetVersion+0x000000000000cc3e

Since the first argument of the function already points outside the original buffer and into an adjacent one, memory corruption is already happening, but it will only crash once it reaches the end of this adjacent chunk:

(666c.62d8): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
npdf!nitro::notifications::notification_manager::FindNotification+0x757db:
00007fff`b211a2fb 41880a          mov     byte ptr [r10],cl ds:000001ae`6fad7002=??
0:000> ?r10
Evaluate expression: 1848709574658 = 000001ae`6fad7002
0:000> dd r10
000001ae`6fad7002  ???????? ???????? ???????? ????????
000001ae`6fad7012  ???????? ???????? ???????? ????????
000001ae`6fad7022  ???????? ???????? ???????? ????????
000001ae`6fad7032  ???????? ???????? ???????? ????????
000001ae`6fad7042  ???????? ???????? ???????? ????????
000001ae`6fad7052  ???????? ???????? ???????? ????????
000001ae`6fad7062  ???????? ???????? ???????? ????????
000001ae`6fad7072  ???????? ???????? ???????? ????????
0:000> k 5
 # Child-SP          RetAddr           Call Site
00 0000007c`8b9f5240 00007fff`b2119131 npdf!nitro::notifications::notification_manager::FindNotification+0x757db
01 0000007c`8b9f5250 00007fff`b211944a npdf!nitro::notifications::notification_manager::FindNotification+0x74611
02 0000007c`8b9f52c0 00007fff`b2081e88 npdf!nitro::notifications::notification_manager::FindNotification+0x7492a
03 0000007c`8b9f53a0 00007fff`b2080c4b npdf!CosCryptGetVersion+0xb308
04 0000007c`8b9f5400 00007fff`b2081a77 npdf!CosCryptGetVersion+0xa0cb

The crash eventually happens in the following code that loops:

.text:00000001804BA2F8 loc_1804BA2F8:
.text:00000001804BA2F8 dec     r8d
.text:00000001804BA2FB mov     [r10], cl       
.text:00000001804BA2FE add     rdx, 4
.text:00000001804BA302 add     r10, r14
.text:00000001804BA305 test    r8d, r8d
.text:00000001804BA308 jg      short loc_1804BA2E1

In the above code, r10 holds the pointer into our overflown memory. Pointer in r10 is incremented by 3 each time the loop is executed, and the loop is guarded by value in r8d which decrements by 1. Initial value of r8d comes directly from xTsiz value in the crashing testcase. This leads to a controlled out of bounds overwrite on the heap.

By carefully controlling parameters in siz block, a precise integer overflow can be triggered allocating a memory region of precise size. By controlling the content of the image this overflow can be abused to overwrite adjacent heap memory which can ultimately lead to arbitrary code execution.

Timeline

2019-05-07 - Vendor disclosure
2019-07-02 - 60 day follow up
2019-07-29 - 2nd follow up (90 days approaching notice)
2019-08-06 - 3rd follow up
2019-08-07 - Vendor acknowledged & advised prior emails went to spam folder; Talos issued copy of report
2019-09-03 - Talos granted disclosure extension to 2019-09-10
2019-09-05 - Vendor advised issues will be addressed in a future release (timeline unknown)
2019-10-09 - Public Disclosure

Credit

Discovered by Aleksandar Nikolic of Cisco Talos.