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:
Chimera:libexec morpheus$ jtool -l afcd LC 00: LC_SEGMENT_64 Mem: 0x000000000-0x100000000 __PAGEZERO LC 01: LC_SEGMENT_64 Mem: 0x100000000-0x100004000 __TEXT Mem: 0x1000023e4-0x100002f80 __TEXT.__text (Normal) Mem: 0x100002f80-0x1000032e0 __TEXT.__stubs (Symbol Stubs) Mem: 0x1000032e0-0x100003658 __TEXT.__stub_helper (Normal) Mem: 0x100003658-0x100003690 __TEXT.__const Mem: 0x100003690-0x100003cf5 __TEXT.__cstring (C-String Literals) Mem: 0x100003cf5-0x100003fb2 __TEXT.__info_plist Mem: 0x100003fb4-0x100003ffc __TEXT.__unwind_info LC 02: LC_SEGMENT_64 Mem: 0x100004000-0x100008000 __DATA Mem: 0x100004000-0x100004090 __DATA.__got (Non-Lazy Symbol Ptrs) Mem: 0x100004090-0x1000042d0 __DATA.__la_symbol_ptr (Lazy Symbol Ptrs) Mem: 0x1000042d0-0x100004330 __DATA.__const Mem: 0x100004330-0x100004370 __DATA.__data Mem: 0x100004370-0x100004398 __DATA.__bss (Zero Fill) LC 03: LC_SEGMENT_64 Mem: 0x100008000-0x100008000 __RESTRICT Mem: 0x100008000-0x100008000 __RESTRICT.__restrict LC 04: LC_SEGMENT_64 Mem: 0x100008000-0x100010000 __LINKEDIT LC 05: LC_DYLD_INFO LC 06: LC_SYMTAB Symbol table is at offset 0x8a28 (35368), 91 entries String table is at offset 0x9260 (37472), 1992 bytes LC 07: LC_DYSYMTAB 1 local symbols at index 0 1 external symbols at index 1 89 undefined symbols at index 2 No TOC No modtab 162 Indirect symbols at offset 0x8fd8 LC 08: LC_LOAD_DYLINKER /usr/lib/dyld LC 09: LC_UUID UUID: DDB86EE3-1DE4-3266-A8F2-C2CBDA62D7BB LC 10: LC_VERSION_MIN_IPHONEOS Minimum iOS version: 11.0.0 LC 11: LC_SOURCE_VERSION Source Version: 261.0.0.0.0 LC 12: LC_MAIN Entry Point: 0x23e4 (Mem: 0x1000023e4) LC 13: LC_LOAD_DYLIB /System/Library/PrivateFrameworks/MobileKeyBag.framework/MobileKeyBag LC 14: LC_LOAD_DYLIB /usr/lib/liblockdown.dylib LC 15: LC_LOAD_DYLIB /usr/lib/libafc.dylib LC 16: LC_LOAD_DYLIB /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation LC 17: LC_LOAD_DYLIB /System/Library/Frameworks/IOKit.framework/Versions/A/IOKit LC 18: LC_LOAD_DYLIB /usr/lib/libSystem.B.dylib LC 19: LC_FUNCTION_STARTS Offset: 35352, Size: 16 (0x8a18-0x8a28) LC 20: LC_DATA_IN_CODE Offset: 35368, Size: 0 (0x8a28-0x8a28) LC 21: LC_CODE_SIGNATURE Offset: 39472, Size: 1088 (0x9a30-0x9e70)
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:
Chimera:libexec morpheus$ jtool -e __TEXT.__info_plist afcd Requested section found at Offset 15605 Extracting __TEXT.__info_plist at 15605, 701 (2bd) bytes into afcd.__TEXT.__info_plist Chimera:libexec morpheus$ cat afcd.__TEXT.__info_plist <?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>CFBundleDevelopmentRegion</key> <string>English</string> <key>CFBundleExecutable</key> <string>afcd</string> <key>CFBundleIdentifier</key> <string>com.apple.afcd</string> <key>CFBundleInfoDictionaryVersion</key> <string>6.0</string> <key>CFBundleDisplayName</key> <string>afcd</string> <key>CFBundleSignature</key> <string>????</string> <key>CFBundleVersion</key> <string>1.0</string> <key>CFBundleName</key> <string>afcd</string> <key>CFBundleShortVersionString</key> <string>2.0</string> </dict> </plist>
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:
Chimera:libexec morpheus$ jtool --sig afcd
Blob at offset: 39472 (1088 bytes) is an embedded signature
Code Directory (583 bytes)
Version: 20400
Flags: adhoc (0x2)
CodeLimit: 0x9a30
Identifier: com.apple.afcd (0x58)
Executable Segment: Base 0x00000000 Limit: 0x00000000 Flags: 0x00000000
CDHash: 1d07099f400db41e1be68a26ce10513dbf2af4d21a61608813b7d81ac0ede415 (computed)
# of Hashes: 10 code + 5 special
Hashes @263 size: 32 Type: SHA-256
Empty requirement set (12 bytes)
Entitlements (423 bytes) (use --ent to view)
Blob Wrapper (8 bytes) (0x10000 is CMS (RFC3852) signature)
Chimera:libexec morpheus$ jtool --ent afcd | jlutil
com.apple.security.network.client: true
com.apple.security.network.server: true
com.apple.security.system-groups[0]: systemgroup.com.apple.osanalytics
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
:
Chimera:libexec morpheus$ jtool -d --jtooldir /tmp afcd > /dev/null
Creating new companion file
Dumping symbol cache to file
Opened companion File: /tmp/afcd.ARM64.DDB86EE3-1DE4-3266-A8F2-C2CBDA62D7BB
Warning: Unable to read symbols from companion file.. Continuing anyway
Disassembling from file offset 0x23e4, Address 0x1000023e4 , mmapped 0x12a2a2000
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
.