ZecOps identified and reproduced an Out-Of-Bounds Write vulnerability that can be triggered by opening a malformed PDF. This vulnerability reminded us of the FORCEDENTRY vulnerability exploited by NSO/Pegasus according to the CitizenLabs blog.
As a brief background: ZecOps have analyzed several devices of Al-Jazeera journalists in the summer 2020 and automatically and successfully found compromised devices without relying on any IOC. These attacks were later attributed to NSO / Pegasus.
ZecOps Mobile EDR and Mobile XDR are available here.
Noteworthy, although these two vulnerabilities are different – they are close enough and worth a deeper read.
- We reported this vulnerability on September 1st, 2020 – iOS 14 beta was vulnerable at the time.
- The vulnerability was patched on September 14th, 2020 – iOS 14 beta release.
- Apple contacted us on October 20, 2020 – claiming that the bug was already fixed – (“We were unable to reproduce this issue using any current version of iOS 14. Are you able to reproduce this issue using any version of iOS 14? If so, we would appreciate any additional information you can provide us, such as an updated proof-of-concept.”).
No CVE was assigned.
It is possible that NSO noticed this incremental bug fix, and dived deeper into CoreGraphics.
Earlier last year, we obtained a PDF file that cannot be previewed on iOS. The PDF sample crashes previewUI with segmentation fault, meaning that a memory corruption was triggered by the PDF.
Open the PDF previewUI flashes and shows nothing:
The important question is: how do we find out the source of the memory corruption?
The MacOS preview works fine, no crash. Meaning that it’s the iOS library that might have an issue. We confirmed the assumption with the iPhone Simulator, since the crash happened on the iPhone Simulator.
It’s great news since Simulator on MacOS provides better debug tools than iOS. However, having debug capability is not enough since the process crashes only when the corrupted memory is being used, which is AFTER the actual memory corruption.
We need to find a way to trigger the crash right at the point the memory corruption happens.
“Guard Malloc is a special version of the malloc library that replaces the standard library during debugging. Guard Malloc uses several techniques to try and crash your application at the specific point where a memory error occurs. For example, it places separate memory allocations on different virtual memory pages and then deletes the entire page when the memory is freed. Subsequent attempts to access the deallocated memory cause an immediate memory exception rather than a blind access into memory that might now hold other data.”
Environment Variables Injection
In this case we cannot simply add an environment variable with the command line since the previewUI launches on clicking the PDF which does not launch from the terminal, we need to inject libgmalloc before the launch.
The process “launchd_sim” launches Simulator XPC services with a trampoline process called “xpcproxy_sim”. The “xpcproxy_sim” launches target processes with a posix_spawn system call, which gives us an opportunity to inject environment variables into the target process, in this case “com.apple.quicklook.extension.previewUI”.
The following lldb command “process attach –name xpcproxy_sim –waitfor” allows us to attach xpcproxy_sim then set a breakpoint on posix_spawn once it’s launched.
Once the posix_spawn breakpoint is hit, we are able to read the original environment variables by reading the address stored in the $r9 register.
By a few simple lldb expressions, we are able to overwrite one of the environment variables into “DYLD_INSERT_LIBRARIES=/usr/lib/libgmalloc.dylib”, injection complete.
Continuing execution, the process crashed almost right away.
Analyzing the Crash
Finally we got the Malloc Guard working as expected, the previewUI crashes right at the memmove function that triggers the memory corruption.
After libgmalloc injection we have the following backtrace that shows an Out-Of-Bounds write occurs in “CGDataProviderDirectGetBytesAtPositionInternal”.
Thread 3 Crashed:: Dispatch queue: PDFKit.PDFTilePool.workQueue 0 libsystem_platform.dylib 0x0000000106afc867 _platform_memmove$VARIANT$Nehalem + 71 1 com.apple.CoreGraphics 0x0000000101b44a98 CGDataProviderDirectGetBytesAtPositionInternal + 179 2 com.apple.CoreGraphics 0x0000000101d125ab provider_for_destination_get_bytes_at_position_inner + 562 3 com.apple.CoreGraphics 0x0000000101b44b09 CGDataProviderDirectGetBytesAtPositionInternal + 292 4 com.apple.CoreGraphics 0x0000000101c6c60c provider_with_softmask_get_bytes_at_position_inner + 611 5 com.apple.CoreGraphics 0x0000000101b44b09 CGDataProviderDirectGetBytesAtPositionInternal + 292 6 com.apple.CoreGraphics 0x0000000101dad19a get_chunks_direct + 242 7 com.apple.CoreGraphics 0x0000000101c58875 img_raw_read + 1470 8 com.apple.CoreGraphics 0x0000000101c65611 img_data_lock + 10985 9 com.apple.CoreGraphics 0x0000000101c6102f CGSImageDataLock + 1674 10 com.apple.CoreGraphics 0x0000000101a2479e ripc_AcquireRIPImageData + 875 11 com.apple.CoreGraphics 0x0000000101c8399d ripc_DrawImage + 2237 12 com.apple.CoreGraphics 0x0000000101c68d6f CGContextDrawImageWithOptions + 1112 13 com.apple.CoreGraphics 0x0000000101ab7c94 CGPDFDrawingContextDrawImage + 752
With the same method, we can take one step further, with the MallocStackLogging flag libgmalloc provides, we can track the function call stack at the time of each allocation.
After setting the “MallocStackLoggingNoCompact=1”, we got the following backtrace showing that the allocation was inside CGDataProviderCreateWithSoftMaskAndMatte.
ALLOC 0x6000ec9f9ff0-0x6000ec9f9fff [size=16]: 0x7fff51c07b77 (libsystem_pthread.dylib) start_wqthread | 0x7fff51c08a3d (libsystem_pthread.dylib) _pthread_wqthread | 0x7fff519f40c4 (libdispatch.dylib) _dispatch_workloop_worker_thread | 0x7fff519ea044 (libdispatch.dylib) _dispatch_lane_invoke | 0x7fff519e9753 (libdispatch.dylib) _dispatch_lane_serial_drain | 0x7fff519e38cb (libdispatch.dylib) _dispatch_client_callout | 0x7fff519e2951 (libdispatch.dylib) _dispatch_call_block_and_release | 0x7fff2a9df04d (com.apple.PDFKit) __71-[PDFPageBackgroundManager forceUpdateActivePageIndex:withMaxDuration:]_block_invoke | 0x7fff2a9dfe76 (com.apple.PDFKit) -[PDFPageBackgroundManager _drawPageImage:forQuality:] | 0x7fff2aa23b85 (com.apple.PDFKit) -[PDFPage imageOfSize:forBox:withOptions:] | 0x7fff2aa23e1e (com.apple.PDFKit) -[PDFPage _newCGImageWithBox:bitmapSize:scale:offset:backgroundColor:withRotation:withAntialiasing:withAnnotations:withBookmark:withDelegate:] | 0x7fff2aa22a40 (com.apple.PDFKit) -[PDFPage _drawWithBox:inContext:withRotation:isThumbnail:withAnnotations:withBookmark:withDelegate:] | 0x7fff240bdfe0 (com.apple.CoreGraphics) CGContextDrawPDFPage | 0x7fff240bdac4 (com.apple.CoreGraphics) CGContextDrawPDFPageWithDrawingCallbacks | 0x7fff244bb0b1 (com.apple.CoreGraphics) CGPDFScannerScan | 0x7fff244bab02 (com.apple.CoreGraphics) pdf_scanner_handle_xname | 0x7fff2421e73c (com.apple.CoreGraphics) op_Do | 0x7fff2414dc94 (com.apple.CoreGraphics) CGPDFDrawingContextDrawImage | 0x7fff242fed6f (com.apple.CoreGraphics) CGContextDrawImageWithOptions | 0x7fff2431999d (com.apple.CoreGraphics) ripc_DrawImage | 0x7fff240ba79e (com.apple.CoreGraphics) ripc_AcquireRIPImageData | 0x7fff242f6fe8 (com.apple.CoreGraphics) CGSImageDataLock | 0x7fff242f758b (com.apple.CoreGraphics) img_image | 0x7fff24301fe2 (com.apple.CoreGraphics) CGDataProviderCreateWithSoftMaskAndMatte | 0x7fff51bddad8 (libsystem_malloc.dylib) calloc | 0x7fff51bdd426 (libsystem_malloc.dylib) malloc_zone_calloc
The OOB-Write vulnerability happens in the function “CGDataProviderDirectGetBytesAtPositionInternal” of CoreGraphics library, the allocation of the target memory was inside the function “CGDataProviderCreateWithSoftMaskAndMatte“.
It allocates 16 bytes of memory if the “bits_per_pixel” equals or less than 1 byte, which is less than copy length.
We came out with a minimum PoC and reported to Apple on September 1st 2020, the issue was fixed on the iOS 14 release. We will release this POC soon.