CVE-2017-2416 Remote code execution triggered by malformed GIF in ImageIO framework, affecting most iOS/macOS apps

ImageIO Available for: iPhone 5 and later, iPad 4th generation and later, iPod touch 6th generation and later Impact: Processing a maliciously crafted image may lead to arbitrary code execution Description: A memory corruption issue was addressed through improved input validation. CVE-2017-2416: flanker_hqd of KeenLab, Tencent

Abstract

(For Chinese version of this writeup see https://blog.flanker017.me/cve-2017-2416-gif-rce-chn/)

Recently I’ve switched my main research focus back from Apple stuff to Android and browsers. While I was auditing a custom image parsing library written by some ppls, I transferred the test case image manipulated by 010editor via a popular IM messenger, and all of a sudden, the app crashed. I investigated the crash and found it is a issue in ImageIO library, and can be automatically triggered in all kinds of iOS/macOS apps that receives GIF images, especially the ones for instant messaging, such as Signal, Telegram, Slack, iMessage etc and Email clients such as Mail, Outlook, Inbox, Gmail , etc and even financial apps that want to be an IM such as Alipay . All these apps will crash on receiving the malicious GIF.

I haven’t test Twitter, but should you find a way to post the malformed GIF online (which I think can be done by manipulated the post stream to bypass the frontend filtering, but I was too busy to try that), the client should also crash as well.

What make things worse is that many clients will automatically reload and reparse the image on open, triggering the vulnerability again and again, lead to infinite loop and eliminating the need for attacker to persistent – -b

DEMO video1

The first video demonstrates receiving malformed gif file via iMessage lead to crash

DEMO video2

the second video demonstrates persistence (user cannot open iMessage anymore…)

Crash trace

* thread #1: tid = 0x17570, 0x00007fff9557f1ab ImageIO`IIOReadPlugin::IIOReadPlugin(CGImagePlus*, unsigned int, unsigned int, long long, unsigned char) + 67, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT) frame #0: 0x00007fff9557f1ab ImageIO`IIOReadPlugin::IIOReadPlugin(CGImagePlus*, unsigned int, unsigned int, long long, unsigned char) + 67 ImageIO`IIOReadPlugin::IIOReadPlugin: -> 0x7fff9557f1ab <+67>: mov al, byte ptr [rdi + 0x40] 0x7fff9557f1ae <+70>: mov qword ptr [rbx + 0x20], rdi 0x7fff9557f1b2 <+74>: mov byte ptr [rbx + 0xc8], al 0x7fff9557f1b8 <+80>: xor eax, eax Thread 0 Crashed:: Dispatch queue: com.apple.main-thread 0 com.apple.ImageIO.framework 0x00007fffa144d1ab IIOReadPlugin::IIOReadPlugin(CGImagePlus*, unsigned int, unsigned int, long long, unsigned char) + 67 1 com.apple.ImageIO.framework 0x00007fffa14b8c93 GIFReadPlugin::InitProc(CGImagePlugin*, unsigned long, unsigned long) + 59 2 com.apple.ImageIO.framework 0x00007fffa14177da IIOImageSource::makeImagePlus(unsigned long, __CFDictionary const*) + 252 3 com.apple.ImageIO.framework 0x00007fffa141918b IIOImageSource::getPropertiesAtIndexInternal(unsigned long, __CFDictionary const*) + 57 4 com.apple.ImageIO.framework 0x00007fffa141911c IIOImageSource::copyPropertiesAtIndex(unsigned long, __CFDictionary const*) + 98 5 com.apple.ImageIO.framework 0x00007fffa13f03ca CGImageSourceCopyPropertiesAtIndex + 181 6 com.apple.AppKit 0x00007fff9cfdbcae +[NSBitmapImageRep _imagesWithData:hfsFileType:extension:zone:expandImageContentNow:includeAllReps:] + 543 7 com.apple.AppKit 0x00007fff9cfdba68 +[NSBitmapImageRep _imageRepsWithData:hfsFileType:extension:expandImageContentNow:] + 93 8 com.apple.AppKit 0x00007fff9d4bf08e -[NSImage _initWithData:fileType:hfsType:] + 479

Almost all image related functions on Apple platform calls down to [NSImage _initWithData:fileType:hfsType:] , and IIOImageSource dispatches image parsing to corresponding plugin based on signature detection (note: not based on file extension). This feature will be useful afterwards.

Sample file to test if you’re vulnerable

Test image sample:

Sample PNG Sample GIF

Grab an image file and change the width/height field to both negative short whose unsigned form value larger than 0xff00.

Drag it into /send to any macos/iOS application and if it crashes, you’re vulnerable.

Analysis

The root cause seems to be at GIFReadPlugin::init function, in the following decompiled snippet:

v32 = (signed __int16)width * (signed __int64)height; if ( v32 > filesize * 1100 * v29 ) { LOBYTE(aspectbyte) = 0; v15 = 0LL; if ( this->gapC0[8] ) { LOBYTE(aspectbyte) = 0; LogError( "init", 498, "malformed GIF file (%d x %d) - [canvasSize: %ld fileSize: %ld ratio: %d]

", (unsigned int)(signed __int16)width, (unsigned int)(height), // width >> 16 is height (signed __int16)width * (signed __int64)SHIWORD(width), filesize, v32 / filesize); v15 = 0LL; } goto LABEL_71; } __text:00000000000CC51F movsx rax, r9w __text:00000000000CC523 mov ecx, r9d __text:00000000000CC526 shr ecx, 10h __text:00000000000CC529 movsx rbx, cx __text:00000000000CC52D imul rbx, rax __text:00000000000CC531 imul rdx, r12, 44Ch __text:00000000000CC538 mov rax, rdx __text:00000000000CC53B imul rax, rsi __text:00000000000CC53F cmp rbx, rax

An attacker can craft an image of negative height and weight, thus bypassing the check comparing to file size, lead to following out-of-bound. As I have mentioned above, the dispatching is based on file signature rather than file extension. I noticed some applications’ web interfaces have check on the size of GIF images, preventing me from spreading this POC to mobile apps. However they do not have check on PNG extension, allowing me to upload the malformed GIF image in PNG extension, bypassing the check and crashes whoever receives it.

While this does make sense, after Apple releases the fix I checked the new ImageIO binary and found the fix actually goes another way. Recall the crash happens in IIOReadPlugin::IIOReadPlugin, in the following pseudo code at 10.11.2/3:

bool __fastcall IIOReadPlugin::IIOReadPlugin(IIOReadPlugin *a1, __int64 a2, int a3, int a4, __int64 a5, unsigned __int8 a6) { unsigned __int8 v6; // r14@1 IIOReadPlugin *this; // rbx@1 __int64 v8; // rax@1 __int64 sessionwrap; // rdi@1 IIOImageReadSession *session; // rax@2 IIOImageRead *v11; // rdi@2 __int64 v12; // rax@2 __int64 *v13; // rcx@5 __int64 v14; // rdx@5 bool result; // al@5 v6 = a6; this = a1; a1->vt = (__int64)off_1659D0; a1->field_8 = a2; v8 = *(_QWORD *)(a2 + 24); a1->field_10 = v8; a1->field_38 = a3; a1->field_3c = a4; a1->field_30 = a5; sessionwrap = *(_QWORD *)(v8 + 24); if ( sessionwrap ) { session = (IIOImageReadSession *)CGImageReadSessionGetSession(sessionwrap); //session is invalid this->session = session; v11 = (IIOImageRead *)session->imageread; //oob happens here and lead to crash LOBYTE(session) = v11->field_40; this->field_20 = (__int64)v11; this->field_c8 = (char)session; v12 = 0LL; if ( v11 ) v12 = IIOImageRead::getSize(v11); } else { this->field_20 = 0LL; this->session = 0LL; this->field_c8 = 1; v12 = 0LL; } And now apple changes the if-block in 10.12.4: a1->field_8 = cgimgplus; imageplus = CGImagePlusGetIPlus(cgimgplus); a1->field_10 = imageplus; a1->field_38 = v9; a1->field_3c = v8; a1->field_30 = v7; v12 = *(_QWORD *)(imageplus + 32); a1->field_18 = v12; imageread = *(IIOImageRead **)(v12 + 32); if ( imageread ) { v10->field_c8 = *((_BYTE *)imageread + 64); v10->field_20 = (__int64)imageread; v14 = IIOImageRead::getSize(imageread); } else { v10->field_c8 = 0; v10->field_20 = 0LL; v14 = 0LL; }

Removing the usage of IIOImageReadSession in this function. Is it better than fixing the size change? Dunno.

Custom fix?

For app developers who want to mitigate this issue for users staying at old versions, I suggest check for negative width and height before passing to NSImage.

I believe this vulnerability is introduced in iOS 10, so iOS 9/OSX 10.11 users are not affected (how many ppls are still using iOS9? Raise your hands). For iOS 10/macOS 10.12 users, please upgrade to 10.3/10.12.4 for the official fix.

Timeline