Before we dive into, please note:

That having been said, this bug is (was) remotely triggerable and on an affected device, will crash any iOS application that is processing remote messages (iMessage, Facebook Messenger, WhatsApp, etc.)!

Background

"Patrick I think China has hacked my iPhone"

Though I'd normally laugh off this rather implausible scenario, I'm working on my emotional intelligence (ha!) so patiently inquired as to why my Taiwanese friend thought this was the case.

She claimed that any time she typed the word Taiwan or worse, received a message with a Taiwanese flag (🇹🇼) it would crash the application on her (fully patched) iOS device.

I remained a little skeptical but as the following gif shows, this was exactly what was happening...consistently!

I quickly reverted to my rather immature self and sent her multiple Taiwanese flags causing her iMessage, Facebook Messenger, and WhatsApp apps to consistently crash. Oh the lolz 🤣

In this blog post, we'll illustrate how to analyze and track down the underlying cause of this remote iOS flaw (without requiring a jailbroken 'research' device).

The Crash

The crashing device was an iPhone 7, running iOS 11.3 (the latest version of iOS at the time):

Device: iPhone 7, iPhone9,1 (US)
iOS: 11.3 (15E216)
Language: English, followed by Chinese
Region: United States
Jailbroken: No

The following is an abridged crash report, from iMessenger (MobileSMS). It was pulled off the device (via Settings -> Privacy, Analytics, Analytics Data):

{"app_name":"MobileSMS","timestamp":"2018-04-18 22:27:25.13 -0700","app_version":"5.0",  
"slice_uuid":"feac9bde-20a2-37c2-86e0-119fb8b9b650","adam_id":0,"build_version":"1.0",  
"bundleID":"com.apple.MobileSMS","share_with_app_devs":false,"is_first_party":true,  
"bug_type":"109","os_version":"iPhone OS 11.3  
 (15E216)","incident_id":"9EE5610B-7A0C-4558-895F-CF876DEB6B07","name":"MobileSMS"}  
 
Incident Identifier: 9EE5610B-7A0C-4558-895F-CF876DEB6B07
CrashReporter Key:   69340bb1126c092b97b9af069f4f6f037466ee0c
Hardware Model:      iPhone9,1
Process:             MobileSMS [10417]
Path:                /Applications/MobileSMS.app/MobileSMS
Identifier:          com.apple.MobileSMS
Version:             1.0 (5.0)
Code Type:           ARM-64 (Native)
Role:                Foreground
Parent Process:      launchd [1]
Coalition:           com.apple.MobileSMS [2015]
 
 
Date/Time:           2018-04-18 22:27:24.9896 -0700
Launch Time:         2018-04-18 22:26:16.9044 -0700
OS Version:          iPhone OS 11.3 (15E216)
Baseband Version:    3.66.00
Report Version:      104
 
Exception Type:  EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000000
 
Termination Signal: Segmentation fault: 11
Termination Reason: Namespace SIGNAL, Code 0xb
Terminating Process: exc handler [0]
Triggered by Thread:  6
 
Filtered syslog:
None found
 
Thread 0 name:  Dispatch queue: com.apple.main-thread
Thread 0:
0   libsystem_kernel.dylib            0x00000001824b3e08 0x1824b3000 + 3592
1   libsystem_kernel.dylib            0x00000001824b3c80 0x1824b3000 + 3200
2   CoreFoundation                    0x00000001829f6e40 0x182909000 + 974400
3   CoreFoundation                    0x00000001829f4908 0x182909000 + 964872
4   CoreFoundation                    0x0000000182914da8 0x182909000 + 48552
5   GraphicsServices                  0x00000001848f7020 0x1848ec000 + 45088
6   UIKit                             0x000000018c8f578c 0x18c5d8000 + 3266444
7   MobileSMS                         0x0000000100e1867c 0x100df8000 + 132732
8   libdyld.dylib                     0x00000001823a5fc0 0x1823a5000 + 4032
 
....
 
Thread 6 name:  Dispatch queue: com.apple.ResponseKit
Thread 6 Crashed:
0   CoreFoundation                    0x0000000182922efc 0x182909000 + 106236
1   CoreEmoji                         0x00000001886b2354 0x1886a6000 + 50004
2   CoreEmoji                         0x00000001886b2354 0x1886a6000 + 50004
3   CoreEmoji                         0x00000001886b2c80 0x1886a6000 + 52352
4   CoreEmoji                         0x00000001886a8ebc 0x1886a6000 + 11964
5   ResponseKit                       0x00000001968754ac 0x19683d000 + 230572
6   ResponseKit                       0x0000000196872e9c 0x19683d000 + 220828
7   ResponseKit                       0x00000001968739b4 0x19683d000 + 223668
8   ResponseKit                       0x0000000196862e78 0x19683d000 + 155256
9   ResponseKit                       0x0000000196862c00 0x19683d000 + 154624
10  ResponseKit                       0x00000001968619f0 0x19683d000 + 150000
11  libdispatch.dylib                 0x0000000182340b24 0x18233f000 + 6948
12  libdispatch.dylib                 0x0000000182340ae4 0x18233f000 + 6884
13  libdispatch.dylib                 0x000000018234aa38 0x18233f000 + 47672
14  libdispatch.dylib                 0x000000018234b380 0x18233f000 + 50048
15  libdispatch.dylib                 0x000000018234bd4c 0x18233f000 + 52556
16  libdispatch.dylib                 0x000000018235411c 0x18233f000 + 86300
17  libsystem_pthread.dylib           0x0000000182673e70 0x182673000 + 3696
18  libsystem_pthread.dylib           0x0000000182673b08 0x182673000 + 2824
 
 
Thread 6 crashed with ARM Thread State (64-bit):
    x0: 0x0000000000000000   x1: 0x00000001add1ad38   x2: 0x0000000000000000   x3: 0x00000001ad364438
    x4: 0x0000000000000000   x5: 0x0000000000000001   x6: 0x0000000000000000   x7: 0x0000000000000000
    x8: 0x0000000000000000   x9: 0x00000001b4e15930  x10: 0x0000000ffffffff8  x11: 0x0000000000000040
   x12: 0xffffffffffffffff  x13: 0x0000000000000001  x14: 0x0000000000000000  x15: 0x00002d0000002d00
   x16: 0x0000000000000000  x17: 0x0000000000002d00  x18: 0x0000000000000000  x19: 0x0000000000000000
   x20: 0x00000001add1ad38  x21: 0x0000000000000000  x22: 0x0000000000000000  x23: 0x00000001c4864cc0
   x24: 0x00000001000404ef  x25: 0x0000000000050000  x26: 0x0000000103d059e4  x27: 0x0000000103d059e4
   x28: 0x0000000000000000   fp: 0x000000016f1a5b20   lr: 0x00000001886b2354
    sp: 0x000000016f1a5b00   pc: 0x0000000182922efc cpsr: 0x80000000
 
Binary Images:
0x100df8000 - 0x100e43fff MobileSMS arm64  <feac9bde20a237c286e0119fb8b9b650> /Applications/MobileSMS.app/MobileSMS
 
0x182909000 - 0x182c9ffff CoreFoundation arm64  <cf162b3ca2883453b2914300d4f19612> /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
0x1886a6000 - 0x1886b7fff CoreEmoji arm64  <6d18237f09d23ce6aa6abb287d7aa515> /System/Library/PrivateFrameworks/CoreEmoji.framework/CoreEmoji
0x19683d000 - 0x19693ffff ResponseKit arm64  <4f7abc9a8f803cb2bff0172b8c69f13e> /System/Library/PrivateFrameworks/ResponseKit.framework/ResponseKit

We'll walk thru this in more detail, but the tl;dr version is that a EXC_BAD_ACCESS (SIGSEGV) (Exception Subtype: KERN_INVALID_ADDRESS) occurred at address 0x0000000000000000.

Such crashes are usually indicative of a NULL-pointer dereference. Here, it appears that the fault was triggered when iOS was performing some type of emoji processing (which fits the trigger of a Taiwanese flag being received by iMessenger).

Other relevant information in the iOS crash report includes:

Crash Analyis

Our goal is to now track down the cause of the crash. That is say, why did the instruction at 0x0000000182922efc dereference a NULL-pointer?

We'll start by reversing the dynamic libraries that appear in the call stack, (ResponseKit, CoreEmoji, and CoreFoundation). Specifically we'll examine the code at the addresses within these dylibs that appear in the call stack (stack backtrace).

Since my friend's phone was not jailbroken, we can't just grab the dylib binaries from the device...we have to get them from elsewhere. Turns out the easiest is from the iOS 11.3 restore image. Such restore images contain the iOS system binaries, such as the dylibs we're after. We can grab the iOS 11.3 restore image (iPhone_4.7_P3_11.0_11.3_15E216_Restore.ipsw) from ipsw.me.

Once this file is downloaded, we can mount the 058-97716-127.dmg disk image via the hdiutil command:

$ hdiutil attach iPhone_4.7_P3_11.0_11.3_15E216_Restore/058-97716-127.dmg expected CRC32 $BDE79F12 /dev/disk2 GUID_partition_scheme
/dev/disk2s1 EFI
/dev/disk2s2 Apple_APFS
/dev/disk3 EF57347C-0000-11AA-AA11-0030654 /dev/disk3s1 41504653-0000-11AA-AA11-0030654 /Volumes/Emet15E216.D10D101D20D201OS

Since the dylibs we're after are embedded in the dyld shared cache (dyld_shared_cache_arm64) we have to extract them.

Using jtool this is fairly straight forward. Simply specify the name of the dylib to extract (e.g.CoreEmoji) and then the path to the shared cache:

jtool -e "CoreEmoji" /Volumes/Emet15E216.D10D101D20D201OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64
Extracting /System/Library/PrivateFrameworks/CoreEmoji.framework/CoreEmoji at 0x6b4e000 into dyld_shared_cache_arm64.CoreEmoji

The following dylibs (which were each referenced in the call stack of the crashing thread) were extracted:

dyld_shared_cache_arm64_CoreEmoji
version: 69.3.0
sha1: 20F6BECF7C76A3FEAFEB8D2321F593388A3CB9B6
 
dyld_shared_cache_arm64_CoreFoundation
version: 1452.23.0
sha1: AD3A226884BB3612694B9AB37DF18F42452D5139
 
dyld_shared_cache_arm64_ResponseKit
version 109.0.0
sha1: BDA7F1F329321C20539499EAF1C36693823CF60E

Unfortunately, we have to symbolicate these dylibs as the majority of their symbols have all been 'redacted':

$ nm dyld_shared_cache_arm64_ResponseKit
0000000194ce6f9c t <redacted>
0000000194ce701c t <redacted>
0000000194ce7090 t <redacted>
0000000194ce70c4 t <redacted>
0000000194ce71e4 t <redacted>
0000000194ce72e0 t <redacted>
0000000194ce72e8 t <redacted>
0000000194ce72f0 t <redacted>
0000000194ce735c t <redacted>
0000000194ce7444 t <redacted>
...

First though, let's rebase the extracted iOS dylibs so that their addresses (when viewed in a disassembler/decompiler) will match those in the crash report.

To rebase each extracted dylib, we use the base addresses found in the crash report (for example 0x1886a6000 for CoreEmoji). In Hopper, one can rebase a binary via the Modify -> Change File Base Address...:

Once the dylibs are rebased, we can tackle the symbolication (or lack thereof) issue.

I wasn't sure the 'best' way to do this, so I simply leveraged the macOS versions of each dylib. Specifically, I 'matched' the symbolicated x64 disassembly (macOS) to the unsymbolicated arm64 disassembly (iOS). Though somewhat of a manual process, this worked great!

For example, take the addresses 0x0000000196862c00 (from frame #9, in the call stack). Here's the full decompilation of the method (within the iOS ResponseKit dylib) which contains address 0x0000000196862c00:

//iOS ResponseKit
int <redacted>_194d0ab58(int arg0) {
    r25 = loc_19147d5e0(r2);
    r22 = loc_19147d5e0(r4);
    loc_19147d5e0(r5);
    r27 = *_RKMessageResponseDontOverrideLanguageID | r7;
    loc_19147d5e0(r6);
    loc_19147d5d8(arg0, 0x1b3e37900, r25, r3, r22, r5, &var_58, 0x0, r27);
    loc_19147d5e8();
    loc_19147d5dc(r26);
    loc_19147d5dc(r22);
    loc_19147d5dc(r25);
    loc_19147d5e0(var_58);
    loc_19147d5dc(r20);
    loc_19147d5dc(r21);
    r0 = loc_19147d7cc(r19);
    return r0;
}

If we decompile macOS's ResponseKit (/System/Library/PrivateFrameworks/ResponseKit.framework) we can find the "matching" x64 decompilation in the RKMessageResponseManager responsesForMessageImp:maximumResponses:forConversationHistory:forContext:withLanguage:options: method (note the reference to the RKMessageResponseDontOverrideLanguageID symbol):

/* @class RKMessageResponseManager */
-(void *)responsesForMessageImp:(void *)arg2 maximumResponses:(unsigned long long)arg3 forConversationHistory:(void *)arg4 forContext:(void *)arg5 withLanguage:(void *)arg6 options:(unsigned long long)arg7 {
    r14 = [arg2 retain];
    r15 = [arg4 retain];
    var_38 = [arg5 retain];
    var_30 = arg6;
    rbx = *_RKMessageResponseDontOverrideLanguageID;
    r13 = [arg6 retain];
    rax = [self responsesForMessageWithLanguageDetectionImp:r14 maximumResponses:arg3 forConversationHistory:r15 forContext:arg5 withLanguage:&var_30 inputModes:0x0 options:rbx | arg7];
    [var_38 release];
    [r15 release];
    [r14 release];
    r14 = [rax retain];
    rbx = [var_30 retain];
    [r13 release];
    [rbx release];
    rax = [r14 autorelease];
    return rax;
}

Now we know the int <redacted>_194d0ab58 method in the iOS ResponseKit dylib, is really the RKMessageResponseManager responsesForMessageImp:maximumResponses:forConversationHistory:forContext:withLanguage:options: method.

Once the dylibs are rebased and (manually) symbolicated, it makes understanding the bug a lot easier, as the method names are fairly descriptive as to their purpose(s).

We'll start by analyzing the addresses in the call stack of the crashing thread (thread #6) in order to uncover the root cause of this remote iOS bug.

Skipping over the (likely spurious) call into libdispatch.dylib we start at stack frame #10 and map each address to the symbolicated method (or block) it falls into:

#10  ResponseKit   0x00000001968619f0
-[RKMessageResponseManager responsesForMessage:maximumResponses:forContext:withLanguage:options:completionBlock:]
 
#9 ResponseKit  0x0000000196862c00
-[RKMessageResponseManager responsesForMessageImp:maximumResponses:forConversationHistory:forContext:withLanguage:options:]
 
#8 ResponseKit  0x0000000196862e78
-[RKMessageResponseManager responsesForMessageWithLanguageDetectionImp:maximumResponses:forConversationHistory:forContext:withLanguage:inputModes:options:]:
 
#7 ResponseKit  0x00000001968739b4
 +[RKMessageClassifier messageClassification:withLanguageIdentifier:conversationTurns:]:
 
#6 ResponseKit  0x0000000196872e9c
-[NSLinguisticTagger languageOfRange:withAdditionalContext:withPreferredLanguages:]
 
#5 ResponseKit  0x00000001968754ac
+[RKUtilities removeEmoji:]
 
#4 CoreEmoji  0x00000001886a8ebc
CEMStringContainsEmoji
 
#3 CoreEmoji  0x00000001886b2c80
unnamed subroutine
 
#2 CoreEmoji  0x00000001886b2354
unnamed subroutine
 
#1 CoreEmoji  0x00000001886b2354
unnamed subroutine
 
#0 CoreFoundation  0x0000000182922efc
CFStringCompare + 0x38

Ok so what's going on? Well looks like when a message is received, ResponseKit classifies the message, and (if some classification is true?) invokes the +[RKUtilities removeEmoji:] method. This method calls into the CoreEmoji dylib in order to apparently perform the actual removal of the emoji(s).

Why would iOS want to remove an emoji? We'll get to that shortly!

After calling into some unnamed subroutines, CoreEmoji invokes the CFStringCompare function, which crashed at an instruction found at address 0x0000000182922efc

The address 0x0000000182922efc is the address of the faulting instruction.
It is the final address in the call stack (i.e. frame #0), as well as in the pc (program counter) register in the "ARM Thread State" section of the crash report.

What is instruction within CFStringCompare at 0x0000000182922efc?

0000000180dcaefc         ldr        x8, [x21]

The ldr arm instruction loads a "program-relative or external address into a register" (arm). Here it's attempting to dereference and load the value from the x21 register into the x8 register.

Looking at the "ARM Thread State" section of the crash report, shows us that the x21 register is NULL (0x0000000000000000) at the time of the crash:

Thread 6 crashed with ARM Thread State (64-bit):
    x0: 0x0000000000000000   x1: 0x00000001add1ad38   x2: 0x0000000000000000   x3: 0x00000001ad364438
    x4: 0x0000000000000000   x5: 0x0000000000000001   x6: 0x0000000000000000   x7: 0x0000000000000000
    x8: 0x0000000000000000   x9: 0x00000001b4e15930  x10: 0x0000000ffffffff8  x11: 0x0000000000000040
   x12: 0xffffffffffffffff  x13: 0x0000000000000001  x14: 0x0000000000000000  x15: 0x00002d0000002d00
   x16: 0x0000000000000000  x17: 0x0000000000002d00  x18: 0x0000000000000000  x19: 0x0000000000000000
   x20: 0x00000001add1ad38  x21: 0x0000000000000000  x22: 0x0000000000000000  x23: 0x00000001c4864cc0
   x24: 0x00000001000404ef  x25: 0x0000000000050000  x26: 0x0000000103d059e4  x27: 0x0000000103d059e4
   x28: 0x0000000000000000   fp: 0x000000016f1a5b20   lr: 0x00000001886b2354
    sp: 0x000000016f1a5b00   pc: 0x0000000182922efc cpsr: 0x80000000

If one tries to dereference a NULL address (pointer), this will crash with a EXC_BAD_ACCESS(SIGSEGV)` ...which is the exact reason given in the crash report:

Exception Type:  EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000000

So what is the value in x21 supposed to be? ...well, obviously a valid address of something.

But what? Looking at the arm64 disassembly of the instructions (within the CFStringCompare function) that precede the faulting instruction, we can see it's the first parameter to passed in to CFStringCompare.

_CFStringCompare:
0000000182922ec4         stp        x22, x21, [sp, #-0x30]!                     
0000000182922ec8         stp        x20, x19, [sp, #0x10]
0000000182922ecc         stp        x29, x30, [sp, #0x20]
0000000182922ed0         add        x29, sp, #0x20
0000000182922ed4         mov        x19, x2
0000000182922ed8         mov        x20, x1
0000000182922edc         mov        x21, x0
0000000182922ee0         tbz        x21, 0x3f, loc_182922efc        ;take this
 
loc_182922ee4:
0000000182922ee4         adrp       x8, #0x1b3519000                            
0000000182922ee8         ldr        x1, [x8, #0x308]                            
0000000182922eec         mov        x0, x21
0000000182922ef0         bl         0x181c1c900
0000000182922ef4         mov        x3, x0
0000000182922ef8         b          loc_182922fd0
 
loc_182922efc:
0000000182922efc         ldr        x8, [x21]                       ; b00m, we crash as x21 is NULL

Maybe the decompilation is more illustrative:

_CFStringCompare
 
r21 = theString1;
if ((r21 & 0xffffffff80000000) != 0x0) {
     r3 = loc_181c1c900(r21, *0x1b3519308);
}
else
{
     r8 = *r21; //b00m, we crash as this is NULL
}

In the disassembly at 0x0000000182922edc we can see that the first parameter (passed in the x0 register) is moved into the x21 register:

mov        x21, x0

Following a test at 0x0000000182922ee0 (to detect if the pointer is 'tagged'), the code jumps to address 0x0000000182922efc where it dereferences the NULL x21 register, and thus crashes.

The check on the register x21 (implemented in this assembly instruction; tbz x21, 0x3f, loc_182922efc) is a check to see if the pointer is 'tagged'.
"Tagged pointers were introduced in iOS 7 and Mac OS X 10.7 for 64-bit architectures. A tagged pointer is a special pointer with data stored directly into the pointer instead of doing memory allocations. This has obvious performance advantages." blog.timac.org

The function definition for CFStringCompare is:

CFComparisonResult CFStringCompare(CFStringRef theString1, CFStringRef theString2, CFStringCompareFlags compareOptions);

The first parameter is a CFStringRef creatively named theString1. Since the crash is a dereference of this first parameter (x0, moved into x21), we now know that something is passing in a NULL value for the theString1 parameter!

Hooray, we've figured the immediate cause of the crash: a NULL string passed to CFStringCompare.

Let's look back up the call stack trace to find out why such a NULL value is being erroneously passed in!

Turns out we don't have to look far. Recall that CFStringCompare is invoked by an unnamed function in CoreEmoji.dylib (dyld_shared_cache_arm64_CoreEmoji) at address 0x00000001886b22ec

As the the extracted CoreEmoji binary (from the dyld shared cache) is not symbolicated, it's simpler just work off the disassembled code of this subroutine from macOS version of the dylib.

Below is the decompilation of both (to illustrate the code in the macOS and iOS versions are the same):

//iOS (arm64)
int <redacted>_186b5a2ec {
    var_10 = r20;
    stack[-24] = r19;
    r31 = r31 + 0xffffffffffffffe0;
    saved_fp = r29;
    stack[-8] = r30;
    if (*qword_1b1c9baf8 != -0x1) {
            dispatch_once(0x1b37f3af8, 0x1add1a6f8);
    }
    r20 = loc_182938048();
    r19 = loc_1829387c8();
    loc_1829111e8(r20);
    if (*(int8_t *)byte_1b1c9bb00 != 0x0) {
            r0 = 0x0;
    }
    else {
            r0 = loc_182922ec4(r19, 0x1add1ad38, 0x0);
            if (r0 != 0x0) {
                    if (CPU_FLAGS & NE) {
                            r0 = 0x1;
                    }
            }
    }
    return r0;
}
//macOS (x64)
int sub_b9fe() {
    if (*qword_128e8 != -1)
    {
            dispatch_once(qword_128e8, ^ {/* block implemented at sub_ba72 */ } });
    }
    rbx = CFLocaleCopyCurrent();
    r14 = CFLocaleGetValue(rbx, **_kCFLocaleCountryCode);
    CFRelease(rbx);
    if (*(int8_t *)byte_128f0 != 0x0) {
            rax = 0x0;
    }
    else {
            rax = CFStringCompare(r14, @"CN", 0x0);
            rax = rax != 0x0 ? 0x1 : 0x0;
    }
    return rax;
}

In the arm64 decompilation, the following line represents the call into CFStringCompare (which crashes):

r0 = loc_182922ec4(r19, 0x1add1ad38, 0x0);

The register r19 is the first parameter (theString1) that was NULL, and thus triggered the crash.

Looking up a few lines we can see r19 is set as the return value of a call to loc_1829387c8();

r19 = loc_1829387c8();

Thanks to the macOS symbolicated decompilation, we can see this is a call to CFLocaleGetValue().

Apple documents this function:

CFTypeRef CFLocaleGetValue(CFLocaleRef locale, CFLocaleKey key);
Returns the corresponding value for the given key of a locale’s key-value pair.

From the decompilation we can determine the locale is the return value from CFLocaleCopyCurrent(), while the key is the _kCFLocaleCountryCode.

Thus, the original source code likely looked something like this:

CFLocaleRef locale = CFLocaleCopyCurrent();
CFStringRef countryCode = CFLocaleGetValue (locale, kCFLocaleCountryCode);

Immediately after this code is a check against a boolean flag (byte_1b1c9bb00 on iOS, byte_128f0 on macOS).

Sticking with the symbolicated macOS dylib, we can find a cross-reference (x-ref) to this value to figure out where it's set (sub_ba72):

void sub_ba72(void * _block) {
    rbx = CFPreferencesCopyValue(@"Country", **_kCFPreferencesAnyApplication, **_kCFPreferencesAnyUser, **_kCFPreferencesCurrentHost);
    if (rbx != 0x0) {
            r14 = CFEqual(rbx, @"CN") != 0x0 ? 0x1 : 0x0;
            CFRelease(rbx);
    }
    else {
            r14 = 0x0;
    }
    *(int8_t *)byte_128f0 = r14;
    return;
}

This code (sub_ba72) determines the user's current 'Country' preference.

If it's not equal to China ("CN") the flag is set to 0x1 (true). If the country is China, or the CFPreferencesCopyValue() failed and returned NULL, the flag is set to 0x0 (false).

My friend's phone's region and language were not set to "CN" (a least in the UI) - so this flag should (AFAIK) be set to 0x1 (true):

...but since the code took the else clause (in turn calling CFStringCompare()), it would indicate that this flag would have to be 0x0.

//check some flag ('CN')
if (*(int8_t *)byte_1b1c9bb00 != 0x0) {
    r0 = 0x0;
}
 
//we take this path
else {
 
  //call to CFStringCompare() that crashes
  r0 = loc_182922ec4(r19, 0x1add1ad38, 0x0);
 
  ... 
 
}

One explanation is that the call to CFPreferencesCopyValue(@"Country" ...) failed for some reason which would also set the flag to 0x0. Or the code thought the phone's locale was set to "CN" for some (unknown) reason?

Regardless, CFStringCompare was called with a first parameter (register r19) set to NULL:

//call CFStringCompare()
// first parameter is NULL, and thus crashes
// second parameter is @"CN"
r0 = loc_182922ec4(r19, 0x1add1ad38, 0x0);

Again note, the r19 register can only be NULL if the call to CFLocaleGetValue() fails (i.e returns NULL).

One explanation is that the call to CFLocaleCopyCurrent returned NULL, which in turn would cause CFLocaleGetValue to also return NULL (which in turn would pass NULL to CFStringCompare() and thus crash).

If we look at other places in Apple's code such as their CFStringCompareWithOptionsAndLocale function, we can see here they check the return value of CFLocaleCopyCurrent():

locale = CFLocaleCopyCurrent();
langCode = ((NULL == locale) ? NULL : (const uint8_t *)_CFStrGetLanguageIdentifierForLocale(locale));

...this implies CFLocaleCopyCurrent() can indeed fail, and return NULL (and thus should be checked!)

Unfortunately at this point, my understanding of the situation comes to an end. That is to say, I'm not sure why under what conditions: CFLocaleGetValue(CFLocaleCopyCurrent(), kCFLocaleCountryCode) can return NULL....but it can, and this is not checked! And thus CFStringCompare() is invoked with NULL and the application comes crashing down!

Apple weights in here, noting:

Under certain conditions if a device’s language/locale settings were set incorrectly, i.e. missing regionCode, it can return NULL. To trigger this the device must be set in a unsupported region-less state.

The Fix

After two+ years of being unable to type 'Taiwan' or being remotely DOS'd anytime her phone received an Taiwanese flag emoji the fix (kudos to my friend Josh S. for the idea!), was simply to toogle the region from US, to China, then back to US.

I'm not 100% sure why (or how this fixed it), but I'm guessing it either set the 'Country' value to 'US' so the boolan flag (at byte_1b1c9bb00) was set now to 0x1, meaning CFStringCompare() was never called....or, that the calls to CFLocaleCopyCurrent()/CFLocaleGetValue() no longer returned NULL, meaning a valid string was passed to CFStringCompare().

Since I wasn't sure how many other iOS users were affected, I also reported the bug to Apple. They assigned it CVE-2018-4290 and patched it in iOS 11.4.1:

I haven't had a chance to reverse Apple's patch, but I suggested the following as a simple fix:

To avoid this crash the code should likely just check the result of the call to CFLocaleGetValue() and if its fails (i.e. returns NULL), skip the call to CFStringCompare():

CFLocaleRef locale = CFLocaleCopyCurrent();
CFStringRef countryCode = CFLocaleGetValue (locale, kCFLocaleCountryCode);
 
//fix!
// make sure to check this!!
if(NULL != countryCode)
{
     CFStringCompare(countryCode, @"CN", 0x0);
}
 
//otherwise handle case where `countryCode` is NULL
else
{
  .... 
}

The "China" Aspect

So far this blog has been a technical deep-dive which uncovered and explained the technical reason of a (remote) iOS crash. However, there still remains an unanswered but rather intriguing question: "What was this code trying to accomplish anyways?"

Recall that:

So much China! ...hrmm so what gives!

The (apparent) answer can be found on the emojipedia website which states:

This flag is hidden from the emoji keyboard on iOS devices where the region is set to China.
Chinese iPhones won't display this flag and will instead show a missing character tofu (☒).

A forum on the MacRumors website, titled, "Apple censors Taiwan flag on iPhones in China" discusses this in more details and provides proof, confirming that on "iPhones in China" the Taiwanese flag is indeed, not shown:

You can test this too. Just change your Region to China (Via System Prefences, General, Language & Region, Region). Taiwanese flags will now be rendered as blank white box with an 'X' thru it!

Does Apple really add code to iOS to appease the Chinese government? Of course! And when that code is buggy, their users suffer.

Though Apple loves to exude an aura of 'users first', the reality is they are first and foremost a corporation. As such their primary objective is always profit 🤑 #truth

Conclusion

In this blog post, we tracked down the cause of a remote iOS flaw.

Though its impact was limited to a denial of service (NULL-pointer dereference) it made for an interesting case study of analyzing iOS code.

...and if Apple hadn't tried to appease the Chinese government in the first place, there would be no bug!