I had to upgrade recently from OSX 10.11.6 to OSX 10.13.3 to be able to run the latest Xcode. The upgrade has broken a few things and also brought a bunch of new system stuff — I found out about a dozen of new daemons because my Little Snitch was popping an alert quite often after the upgrade.
There were some questionable daemons trying to access the internets, for example keyboardservicesd — based on the name only, why would a keyboard-related daemon connect online? I didn’t want to get into the details so I just blocked almost all of them. But why stop at blocking the access? A better way is to actually stop these muddy services, that’s what I did and ended up seeing this “iCloud Drive may not work properly. Please check the iCloud preference pane.” alert every time I opened the Open/Save File dialog in any application — it showed up once per app launch.
That is very annoying, especially since I don’t care about this iCloud thing at all and I would gladly remove/disable it altogether in an official way, but guess what, Apple doesn’t provide that way (at least, I couldn’t find anything online). I was then searching for this message to figure out what is possible to do to get rid of it — nope, nothing. I had to brush up on my little reverse engineering skills to deal with it myself. The step-by-step story (and guide to repeat it) is below.
Here is the list of daemons that I’ve disabled before seeing this message:
I strongly suspect that disabling the bird daemon caused all this mess. The man page says:
bird is one of the system daemons backing the Documents in the Cloud feature.
Looking for the string
The basic way to find the piece of code that you need is to search for a string related to that code, in our case it’s the string about the iCloud. It can be either in the binary or in external resources. I’m using the ag command here — “The Silver Searcher” — a better grep.
$ ag -Q --search-binary 'iCloud Drive may not work properly. Please check the iCloud preference pane.' /System/ 2>/dev/null
Binary file /System/Library/CoreServices/Finder.app/Contents/Resources/English.lproj/Localizable.strings matches.
Binary file /System/Library/PrivateFrameworks/FinderKit.framework/Versions/A/Resources/English.lproj/Localizable.strings matches.
Not bad, we have something to start with. If you less either file, you’ll see they are stored in a binary plist format. We can convert it to something more readable:
$ plutil -convert xml1 -o - /System/Library/PrivateFrameworks/FinderKit.framework/Versions/A/Resources/English.lproj/Localizable.strings | grep -B1 'iCloud Drive may not work properly.' <key>A42</key>
<string>iCloud Drive may not work properly. Please check the iCloud preference pane.</string>
Whatever binary displaying the alert doesn’t contain the string itself, but it must have the localization key “A42” instead. My idea to find the library using it was to observe that TextEdit and Console applications display it, so there must be a common framework between them. The following command prints the frameworks that are used by the both apps:
Nothing. I wonder where this alert is coming from.
A primitive tester
To reproduce this behavior and be able to debug it, I created a new “Cocoa App” in Xcode, used Objective-C to have fewer levels of abstractions. I only added this function to ViewController.m to be able to open a file dialog using “Cmd+O”:
There are sandboxed applications on OSX and the system restricts their access to the file system, which means the file dialogs may behave differently. This documentation link confirms it:
The macOS security technology that interacts with the user to expand your sandbox is called Powerbox. Powerbox has no API. Your app uses Powerbox transparently when you use the NSOpenPanel and NSSavePanel classes.
So I disabled the “App Sandbox” checkbox on the app target’s Capabilities tab. Sure enough, I get the same error in my app.
Going back to the search results for the original string I tried:
I’ve never used the Hopper Disassembler, so it was a good time to brew cask install hopper-disassembler and run it. Open the FinderKit binary, select the 64-bit architecture and let it disassemble the code. I searched for “A42” in the left strings panel and found it here:
aA42:00000000003833c2db"A42",0; DATA XREF=cfstring_A42
Press “x” to jump to the single reference, press “x” again to jump to its single usage here:
__ZN4fstd23finder_callable_details15callable_holderIZL18ShowBRGenericErrorP8NSStringE3$_6vJEEclEv://fstd::finder_callable_details::callable_holder<ShowBRGenericError(NSString*)::$_6,void>::operator()()000000000025252epushrbp; Begin of try block…000000000025256dmovrdi,rbx; argument "cf" for method imp___stubs__CFRetain0000000000252570callimp___stubs__CFRetain; CFRetain0000000000252575leardx,qword[cfstring_A42]; End of try block started at 0x25252e, Begin of try block (catch block at 0x25267b), @"A42"000000000025257cmovrdi,r12; argument #1 for method __ZN7TString3SetEPK10__CFStringS2_000000000025257fmovrsi,r15; argument #2 for method __ZN7TString3SetEPK10__CFStringS2_0000000000252582call__ZN7TString3SetEPK10__CFStringS2_; TString::Set(__CFString const*, __CFString const*)0000000000252587movqword[rbp+var_D0],rbx; End of try block started at 0x252575000000000025258emovrdi,rbx; Begin of try block (catch block at 0x252679), argument "cf" for method imp___stubs__CFRetain0000000000252591callimp___stubs__CFRetain; CFRetain0000000000252596leardx,qword[cfstring_A41]; End of try block started at 0x25258e, Begin of try block (catch block at 0x252674), @"A41"000000000025259dleardi,qword[rbp+var_D0]; argument #1 for method __ZN7TString3SetEPK10__CFStringS2_00000000002525a4movrsi,r15; argument #2 for method __ZN7TString3SetEPK10__CFStringS2_00000000002525a7call__ZN7TString3SetEPK10__CFStringS2_; TString::Set(__CFString const*, __CFString const*)00000000002525acmovqword[rbp+var_C8],rbx; End of try block started at 0x25259600000000002525b3movrdi,rbx; Begin of try block (catch block at 0x25266f), argument "cf" for method imp___stubs__CFRetain00000000002525b6callimp___stubs__CFRetain; CFRetain00000000002525bbleardx,qword[cfstring_A38]; End of try block started at 0x2525b3, Begin of try block (catch block at 0x25266d), @"A38"00000000002525c2leardi,qword[rbp+var_C8]; argument #1 for method __ZN7TString3SetEPK10__CFStringS2_00000000002525c9movrsi,r15; argument #2 for method __ZN7TString3SetEPK10__CFStringS2_00000000002525cccall__ZN7TString3SetEPK10__CFStringS2_; TString::Set(__CFString const*, __CFString const*)00000000002525d1call__ZN7TString12KEmptyStringEv; TString::KEmptyString(), End of try block started at 0x2525bb, Begin of try block (catch block at 0x252691)00000000002525d6addr14,0x800000000002525daleardi,qword[rbp+var_C0]; argument #1 for method __ZN6TAlertC2ERK7TStringS2_S2_S2_S2_00000000002525e1learsi,qword[rbp+var_D8]; argument #2 for method __ZN6TAlertC2ERK7TStringS2_S2_S2_S2_00000000002525e8learcx,qword[rbp+var_D0]; argument #4 for method __ZN6TAlertC2ERK7TStringS2_S2_S2_S2_00000000002525eflear8,qword[rbp+var_C8]; argument #5 for method __ZN6TAlertC2ERK7TStringS2_S2_S2_S2_00000000002525f6movrdx,r14; argument #3 for method __ZN6TAlertC2ERK7TStringS2_S2_S2_S2_00000000002525f9movr9,rax00000000002525fccall__ZN6TAlertC2ERK7TStringS2_S2_S2_S2_; TAlert::TAlert(TString const&, TString const&, TString const&, TString const&, TString const&)0000000000252601leardi,qword[rbp+var_C8]; End of try block started at 0x2525d10000000000252608call__ZN4TRefIPK10__CFString20TRetainReleasePolicyIS2_EED2Ev; TRef<__CFString const*, TRetainReleasePolicy<__CFString const*> >::~TRef()000000000025260dleardi,qword[rbp+var_D0]0000000000252614call__ZN4TRefIPK10__CFString20TRetainReleasePolicyIS2_EED2Ev; TRef<__CFString const*, TRetainReleasePolicy<__CFString const*> >::~TRef()0000000000252619leardi,qword[rbp+var_D8]0000000000252620call__ZN4TRefIPK10__CFString20TRetainReleasePolicyIS2_EED2Ev; TRef<__CFString const*, TRetainReleasePolicy<__CFString const*> >::~TRef()0000000000252625leardi,qword[rbp+var_C0]; Begin of try block (catch block at 0x252680)000000000025262ccall__ZNK6TAlert15DisplayStdAlertEv; TAlert::DisplayStdAlert() const0000000000252631cmprax,0x10000000000252635jneloc_25263c0000000000252637call__ZN7TLaunch18OpenICloudPrefPaneEv; TLaunch::OpenICloudPrefPane()
This is clearly the right place! You can see the strings A41 and A38 nearby which are “Open iCloud Preferences…” and “OK” respectively, and the call to TAlert::DisplayStdAlert().
Figuring out the code path
I was interested in the conditions of when this function is called. Launch our test app, open the dialog, then pause the program.
Our function is located at address 0x0025252e in the binary (more specifically, in the x86_64 section of the binary since the FinderKit and all other system frameworks are fat binaries containing i386 and x86_64 architectures; we’ll come back to this below). The dump shows that the section for this function is FinderKit.__TEXT.__text which has the base address of 0x00007fff6918aeb0. Now we need to calculate the memory address of the function: 0x6918aeb0 - 0x00001eb0 + 0x0025252e = 0x693db52e. Verifying this:
Hmm, it’s not loaded yet. Apparently it’s loaded lazily when needed. We can put a breakpoint visually at the beginWithCompletionHandler: line, which is when the framework is already loaded (by the way. the memory address is the same between launches).
So it’s an objc_msgSend call, which also somehow overwrites the frame because it’s not in the stacktrace. I think it’s an optimization of the dynamic Objective-C runtime. Let’s restart the program, set the same breakpoint and also another one to figure out what message is sent:
(lldb)breakpointset-a0x7fff55bc01bfBreakpoint3:where=Foundation`__NSThreadPerformPerform+328,address=0x00007fff55bc01bf(lldb)breakpointcommandadd3Enteryourdebuggercommand(s).Type'DONE'toend.>p(void)printf("[%s,%s]\n",(char*)object_getClassName($arg1),$arg2)>continue>DONE[FI_TRunSoonHelper,performSelector:withObject:]p(void)printf("[%s,%s]\n",(char*)object_getClassName($arg1),$arg2)continueProcess69182resumingCommand#2 'continue' continued the target.
Not very helpful, only one call. I’m not that familiar with the ObjC’s inner workings and didn’t have much time to extract more information from it. So let’s move on.
Binary patching in memory
This is the prologue and epilogue of our function, you can clearly see the epilogue undoing the changes that the prologue does:
000000000025252epushrbp; Begin of try block000000000025252fmovrbp,rsp0000000000252532pushr150000000000252534pushr140000000000252536pushr120000000000252538pushrbx0000000000252539subrsp,0xc0…0000000000252658addrsp,0xc0000000000025265fpoprbx0000000000252660popr120000000000252662popr140000000000252664popr150000000000252666poprbp0000000000252667ret; endp
To disable the function, we can just replace the first instruction (push rbp) with ret. What’s annoying is that I couldn’t find an option to display the opcodes directly in the disassembly in Hopper, so we can see them in lldb again:
WTF?! It’s writable now, why can’t I change the byte? I don’t know the definite answer, but I suspect it’s because of the code signatures of the system frameworks or the fact that the files are not writable (however that shouldn’t matter). I failed to patch the binary in memory, let’s try a patched file.
In order not to mess with the system, I wanted to patch the framework and inject my version into my program to verify the patch works. As mentioned above, the framework contains two architectures:
$ lipo -info /System/Library/PrivateFrameworks/FinderKit.framework/FinderKit
Architectures in the fat file: /System/Library/PrivateFrameworks/FinderKit.framework/FinderKit are: x86_64 i386
I copied into my home directory to inject the local copy (still unpatched) into my program. Briefly, I tried setting the DYLD_LIBRARY_PATH, DYLD_FRAMEWORK_PATH, DYLD_INSERT_LIBRARIES (with and without DYLD_FORCE_FLAT_NAMESPACE=1) environment variables — didn’t work, according to the output of target modules dump sections FinderKit.
I even tried linking to FinderKit explicitly in my program, but it still failed to load my version. I moved on to disable all standard paths to make sure /System/Library/PrivateFrameworks is not in the search path by using these linker flags: -framework FinderKit -F $HOME/bin/ -Z -F /System/Library/Frameworks -L /usr/lib. Nope, didn’t work a single bit, the binary was still linked against the system framework:
$ otool -L …/Build/Products/Debug/testalert.app/Contents/MacOS/testalert
/System/Library/PrivateFrameworks/FinderKit.framework/Versions/A/FinderKit (compatibility version 1.0.0, current version 1054.2.4)
Why, oh why?! I failed miserably at this task.
Final binary patching for real
We need to extract the individual slices from the fat binary first:
$ cd ~/bin/FinderKit.framework/Versions/A
$ lipo -thin x86_64 -output FinderKit.x86_64 FinderKit
I used Hex Fiend to replace the byte 55 at offset 0x25252e with the byte c3. Repackaging and replacing the fat binary (NB: you need to disable SIP to overwrite the files in /System):
Oops, how could I forget about code signing?! Interestingly, I tried the same with TextEdit.app, which didn’t crash, the file dialog didn’t open either, and I saw these lines in the system log:
CODE SIGNING: process 85221[com.apple.appkit]: rejecting invalid page at address 0x10609d000 from offset 0x253000 in file "/System/Library/PrivateFrameworks/FinderKit.framework/Versions/A/FinderKit"(cs_mtime:1524345553.61230579 == mtime:1524345553.61230579)(signed:1 validated:1 tainted:1 nx:0 wpmapped:0 slid:0 dirty:0 depth:0)
While still inside the ~/bin/FinderKit.framework/Versions/A directory, resign the binary:
$ codesign -vvv .
.: invalid signature (code or signature have been modified)In architecture: x86_64
$ codesign -f -s - --timestamp=none .
.: replacing existing signature
$ sudo cp FinderKit /System/Library/PrivateFrameworks/FinderKit.framework/Versions/A/FinderKit
$ codesign -vv /System/Library/PrivateFrameworks/FinderKit.framework/
/System/Library/PrivateFrameworks/FinderKit.framework/: valid on disk
/System/Library/PrivateFrameworks/FinderKit.framework/: satisfies its Designated Requirement
Repeat the simple trick with Console.app and… crash! Let’s try again:
$ cd ../../..
$ codesign -f -s - --timestamp=none FinderKit.framework
FinderKit.framework: replacing existing signature
$ codesign -vvvv FinderKit.framework
FinderKit.framework: valid on disk
FinderKit.framework: satisfies its Designated Requirement
$ sudo rsync -av FinderKit.framework/ /System/Library/PrivateFrameworks/FinderKit.framework/
The file dialog finally works (it seems you need to copy the whole framework directory even though the only change is in the FinderKit.framework/Versions/A/FinderKit file). And without that annoying alert! Epic Success!
First, this works for all the apps I’ve tried so far — the alert is gone, I haven’t noticed any strange behavior. FinderKit is a dynamic framework only, so it’s impossible to compile it statically into an application, thus we don’t have to worry that the alert will appear in an app as long as the shared framework is patched.
Second, when I restart OSX or relaunch Finder.app, I still see this alert. That correlates with the search result of our target string in /System/Library/CoreServices/Finder.app as seen at the top of the post, and should be pretty easy to patch in the same way.
Third, the same as with using a custom sudo binary, an OS update is very likely to break the fix.
This process was definitely fun, I’ve learned some things and I’ve learned that some things don’t work as expected in OSX. And it was easier than I’d anticipated.