afcd
disassembly/decompilation using jtool
I got inadvertently tagged on Twitter to a discussion involving the Apple File Conduit daemon. This is a really simple daemon, which is used by Apple to move files in and out of the device, primarily the user's media files. Jailbreakers are familiar with afcd2, which is an unsandboxed version. I personally prefer ssh, but this daemon can interoperate with iFunBox, which makes it popular.
Having just concluded a great 5-day session of The Tg MacOS/iOS internals training where I work extensively with jtool
, people told me they didn't really have any idea it could do all it does. So it struck me that this simple daemon would be a great topic for an article - haven't written any in a long time - demonstrating some applied jtool
practices. So here it is. And this isn't complete, but gets you 60% of the way there (took about an hour to write, mostly formatting HTML ... :-P)
When approaching a binary, the first step is always jtool -l
(or, better, -l -v
). In this case the former suffices, and gives us this:
From which we get:
jtool
is actually pretty good with that - I just hate the language myself.__TEXT.__info_plist
). Extracting with jtool
we have:
Daemons with such embedded property lists aren't too common, and the embedded plist is used so as to provide the daemon with a CFBundleIdentifier
.
__RESTRICT.__restrict
. This section does nothing but alert dyld
to prune any and all DYLD_*
environment variables, so the daemon cannot be injected to during startup. As mentioned in MOXiI Vol. 1 Chapter 7, there is a select set of daemons (notably, the despicable amfid
) who use this method for restriction. It's worth noting that afcd
doesn't actually need it since it is an entitled binary.jtool -L
)
lockdownd
-framework
switch, or required since AFCConnectionSetIOTimeout
expects an IOTimeout
parameterCFDictionary
objects, etcjtool -S -v
) we find three dependencies here - _MKBUserTypeDeviceMode and _kMKBDeviceModeKey/MultiUser - used to detect "Multi User configurations", i.e. those on iPads with studentd
Next is an examination of entitlements. From the code signature we see we definitely have those:
The entitlements of com.apple.security.network.[client|server]
are what allow the daemon to connect to local ports (client
) and bind them (server
). The system-groups
gives it access to the OSAnalytics
mechanism.
With only about 750 statements of ARM64 assembly, the daemon is ridiculously simple - making it a crime to not just decompile it. We first force the creation of a companion file, using jtool -d --jtooldir /tmp afcd
:
The trick here is to specify the jtooldir
, which is a directory where you can put all your "companion files" into. A Jtool companion file gets generated with the name of the binary in question (here, afcd
), a suffix denoting the architecture (.ARM64
), so that you can match it to the right binary in a FAT case, and a UUID (here, DD....BB
, so that jtool
can get the right match in case you have multiple binaries. (The value is taken from the LC_UUID
load command).
The companion file created in this way is populated from the LC_FUNCTION_STARTS
, and any Objective-C methnames found. Since here we have none of the latter, we just get the functions:
So we have five functions. jtool
will provide an automatic alias of "main" for the entry point (determined from the LC_MAIN
or LC_UNIXTHREAD
, so we start disassembling there, turning on JCOLOR=1
, and using less -R
to not be overwhelmed with curses. The output is a bit different than what I'm presenting here, which was generated by appending --html
(which I used when embedding jtool
output in my books):
Note the highlighted lines - jtool
follows arguments, and automatically puts them into functions it recognizes. The common ones (e.g. bzero()
) are hard coded. But what about ones which are specific to the binary? Or ones I haven't hard-coded? The latter, you can ask me to add anytime via the forum. But the former, you have to add yourself. Observe above, we have libafc.dylib::_AFCLog
, called at 0x100002434
from 0x100002ff8. We see that AFCLog
takes two arguments - a number in X0, and a message (char *) in X1. So we add to the companion file the following line:
0x100002ff8:_AFCLog(xc)
Stating "x" for hexadecimal (we could have had "i" for integer, too), and "c" for character. We then re-run jtool. As if by magic, we get:
This is, by far, one of jtool
's best features, which is easy to use and not entirely matched by other disassembly tools*. You can do that on any symbol - whether an internal function or an external (as in this case, through DYLD imports). It's especially useful with error/logging/complainer functions. You can also add void functions with empty parentheses:
0x100003004:_AFCPlatformInitialize()
So far - past the standard prolog (which sets up the stack and __stack_chk_guard
) we have:
bzero(SP + 0xc8,1024); AFCPlatformInitialize(); AFCLog(0x4,"afcd starting"; );
And so, we continue. The function is relatively big, so I opted to embed more screenshots and also show the incremental disassembly by address (-d 0x...
and number of bytes (,...)
:
The above construct is something jtool
doesn't handle (yet), but it's fairly readable. It as a getopt_long
loop, which is a bit weird since it starts with a jump to ..2470
and then a branch back to ...2464
. The getopt
is for #114, which jtool
conveniently decodes as the ASCII lowercase 'r'. If we have an argument and it's not 'r', we jump to ..2780
, where we find:
So -r
is for crash reporter. Indeed, we see that the string com.apple.crashreportcopymobile
gets loaded to X23, #880
in the getopt
. It then gets compared in the strcmp
in 0x1000024ac
. Note, that because there is another code path which loads com.apple.afcd
(if the -r
is not found), the strcmp
can't pick up both and decompiles assuming com.apple.afcd
(the closest code path). If the string actually IS com.apple.afcd
, then "/private/var/mobile/Media" gets loaded to X19 and we jump to ...2590. Else, the flow diverts to 24c0, where X19 presumably loads to something else. So we disassemble a little before 2590, and see:
Yep. X19 can be -r
- which makes sense. Looking a bit further down, we see that this is the "served" directory. (If you look a bit earlier you'll see that MK*
(MobileKeyBag) APIs.
Decompiling the previous listing will yield:
int kq = kqueue(); if (kq == -1) { #if 0 1000027c0 ADR X1, #4078 "kqueue(): %m" ; ->R1 = 0x1000037ae 1000027c4 NOP ; 1000027c8 B 0x1000027e8 .. 1000027e8 ORR W0, WZR, #0x1 ; ->R0 = 0x1 1000027ec BL _AFCLog(xc) ; 0x100002ff8 ; ; _AFCLog(0x1,"realpath(%s) failure: %s"; ); 1000027f0 ORR W0, WZR, #0x1 ; ->R0 = 0x1 1000027f4 BL libSystem.B.dylib::_exit ; 0x100003178 ; ; libSystem.B.dylib::_exit(1); #endif AFCLog(1, "kqueue(): %m"); exit(1);
Again, note that jtool
can decompile, but when it comes to common code paths, it will only get the closest one (which in this case is the error on realpath
, which is initialized by code in ...27d0) so it's not 100%. That said, it's easy to figure out when the decompilation does make sense and when it doesn't.
Following the kqueue(1) is indeed a call to realpath(3)
, whose man page shows as:
char * realpath(const char *restrict file_name, char *restrict resolved_name);
so we have :
extern int * __error; char *servedDir; // a global - 0x100004378 int kq = kqueue(); if (kq == -1) { AFCLog(1, "kqueue(): %m"); exit(1); } servedDir = realpath(servedDir, NULL); if (!servedDir) { int err = *(__error()); if (err != ENOENT) { AFCLog(0x1, "realpath(%s) failure: %s"); exit(1); } int rc = mkdir ("/private/var/mobile/Library/Logs/CrashReporter", 0755); if (rc < 0) { int err = *(__error()); AFCLog(0x1, "could not create directory %s (%d)", "/private/var/mobile/Library/Logs/CrashReporter", err); exit(1); #if 0 1000026b4 BL libSystem.B.dylib::___error ; 0x1000030dc 1000026b8 LDR W8, [X0, #0] ; -R8 = *(R0 + 0) = .. *(0x0, no sym) = 0xfeedfacf ... (null)?.. 1000026bc STP X19, X8, [SP, #0] ; *(SP + 0x0) = 0x0 1000026c0 ADR X1, #4347 "could not create directory %s (%d)" ; ->R1 = 0x1000037bb 1000026c4 NOP ; 1000026c8 B 0x100002740 #endif } servedDir = strdup ("/private/var/mobile/Library/Logs/CrashReporter"); } AFCLog(0x4, "Serving directory %s", servedDir);
So now we get to the interesting part: Just what "serving a directory" entails. The disassembly continues:
Ignoring that ucky 0x52800a0d53201f (a bug, I know, I'll fix it some time), this decompiles to:
struct stat stBuf; rc = lstat (servedDir, &stBuf); if (rc < 0) { // note optimization on 31st bit) // 268c AFCLog(0x1, "could not stat directory %s: %d", servedDir, errno); // easier than *__error()... exit(1); } AFCLog(0x5, "directory %s mode is 0x%x", servedDir, stBuf.st_mode); if (stBuf.st_mode & 0xf000 != S_IFDIR) // 0040000 { // 26a4 AFCLog(0x1, "path %s is not a directory", servedDir); exit(1); } // 2634 void *sbExt = sandbox_extension_issue_file ("com.apple.afc.root", servedDir, 0); if (!sbExt) { // 26cc _AFCLog(0x1,"sandbox_extension_issue_file failed: %d", errno ); exit(1); } // 2650 char *errorbuf; // SP + 0xa0 rc = sandbox_init ("afcd", 2, &errorBuf); if (rc < 0) { // 26ec AFCLog(0x1, "Could not load afcd sandbox profile: %s", errorBuf); exit(1); } uint64_trc1 = sandbox_extension_consume(sbExt); if (rc1 < 0) { _AFCLog(0x1,"sandbox_extension_consume failed (%d): %s", errno, sbExt ); free(sbExt); _AFCLog(0x1,"failed to load sandbox" ); exit(1); }
The use of sandbox_init(1)
is interesting. As I explain in Volume III, this hails from the days of "seatbelt", when sandboxing was voluntary, and not based on the seatbelt-profiles
entitlement or (in iOS) the execution path (in iOS). There is a predefined profile for afcd
in the afcd
issues an extension on that dir (BEFORE entering the sandbox), and then confines itself, but allows that extension (by "consuming it" - again, this is in Chapter 8 of Volume III).
Keeping a positive outlook, and assuming the extension consume worked, we resume :
Decompilation is straightforward (look at the comments):
free (sbExt); /* dispatch_group_t */ dg /* 0x100004380, a global */ = dispatch_group_create(); /* dispatch_queue_create */ afcd_xpc_listener_queue /* 0x100004388, another global */ = dispatch_queue_create("afcd xpc listener", 0); dispatch_set_target_queue (dg, dispatch_get_global_queue (2,0)); AFCLog(0x4, "Ready to start"); /* xpc_connection_t */ afcdMach; // Another global, 0x100004390 if (afcdMach == XPC_CONNECTION_NULL) { // serviceName is a char * from way up in the first or second listing: // com.apple.afcd or com.apple.crashreportcopymobile) - global at 0x10004370 AFCLog ("Creating XPC Service %s", serviceName); afcdMach = xpc_connection_create_mach_service(serviceName, afcd_xpc_listener_queue, XPC_CONNECTION_MACH_SERVICE_LISTENER ); if (acfdMach == XPC_CONNECTION_NULL) { AFCLog(0x1, "Could not create XPC listener"); exit(1); } // 28a8 // Jtool automatically follows blocks, and gets you the block function // Take that **, IDA/Hopper :-) xpc_connection_set_event_handler(afcdMach, _func_100028f0); xpc_connection_resume(afcdMach); } // 28ec dispatch_main(); // never returns.
The rest is pretty simple. But if you want AFCD2, patching is simple:
sandbox_init
, and set servedDir to "/". This will violate the code signature, so resign with jtool --sign --inplace --ent a.ent
, providing the same entitlements (+ platform-application
) in afcd2
, and create a plist, and that's that.
Hope this serves both as a quick writeup on afcd
, as well as an example of applied jtool
ing. The unofficial man page is in the download page.
IF YOU FIND BUGS IN JTOOL, I WILL HAPPILY FIX THEM - JUST REPORT THEM OVER The NewOSXBook.com/forum Tools section. JTool is built around my use cases, so it may crash/behave weirdly outside that domain. That said, I've used it on virtually all of Apple's binaries - and it should work well. It's still far from perfect, but you can help. Bug reports / feature requests are very appreciated!
There's more where that came from - The next MOXiI training will be NYC, May 7th, 2018.
jtool
.