A quick writeup on afcd disassembly/decompilation using jtool

Jonathan Levin,, 2/11/2018


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)

1. Basic analysis

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:
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:

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: (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 true true[0]:

The entitlements of[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.

2. Decompilation

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:

Chimera:libexec morpheus$ cat /tmp/afcd.ARM64.DDB86EE3-1DE4-3266-A8F2-C2CBDA62D7BB 0x1000023e4:_func_1000023e4 0x1000028f0:_func_1000028f0 0x100002a30:_func_100002a30 0x100002e8c:_func_100002e8c 0x100002f34:_func_100002f34

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):

Chimera:libexec morpheus$ export JCOLOR=1
Chimera:libexec morpheus$ jtool --jtooldir /tmp -d main afcd | less -R
Opened companion File: ./afcd.ARM64.DDB86EE3-1DE4-3266-A8F2-C2CBDA62D7BB
Disassembling from file offset 0x23e4, Address 0x1000023e4 to next function, mmapped 0x121641000
; //
; // function #1
; //
1000023e4 STP X28, X27, [SP, #-80]! ; // ; // ; *(SP + 0xffffffffffffffb0) = 0x0
1000023e8 STP X24, X23, [SP, #16] ; // ; // ; *(SP + 0x10) = 0x0
1000023ec STP X22, X21, [SP, #32] ; // ; // ; *(SP + 0x20) = 0x0
1000023f0 STP X20, X19, [SP, #48] ; // ; // ; *(SP + 0x30) = 0x0
1000023f4 STP X29, X30, [SP, #64] ; // ; // ; *(SP + 0x40) = 0x0
1000023f8 ADD X29, SP, #64 ; R29 = SP+0x40
1000023fc SUB SP, SP, 1232 ; // ; SP -= 0x4d0 (stack frame)
100002400 MOV X19, X1 ; // ; --X19 = X1 = ARG1
100002404 MOV X20, X0 ; // ; --X20 = X0 = ARG0
100002408 NOP ; // ;
10000240c LDR X8, #7164 ; // ; // ;.. X8 = *(100004008) = -libSystem.B.dylib::___stack_chk_guard-
100002410 LDR X8, [X8, #0] ; // ; // ;; R8 = *(libSystem.B.dylib::___stack_chk_guard)
100002414 STUR X8, X29, #-72 ; // ; // ; SP + 0x4c8 = X8 libSystem.B.dylib::___stack_chk_guard
100002418 ADD X0, SP, #200 ; R0 = SP+0xc8
10000241c ORR W1, WZR, #0x400 ; // ; ->R1 = 0x400
100002420 BL libSystem.B.dylib::_bzero ; 0x10000310c
R0 = libSystem.B.dylib::_bzero(SP + 0xc8,1024);
100002424 BL ; // libafc.dylib::_AFCPlatformInitialize ; 0x100003004
100002428 ADR X1, #4738 "afcd starting"; // ; ->R1 = 0x1000036aa
10000242c NOP ; // ;
100002430 ORR W0, WZR, #0x4 ; // ; ->R0 = 0x4
100002434 BL ; // libafc.dylib::_AFCLog ; 0x100002ff8
1000028c8 STR X8, [SP, #176] ; // ; // ; *(SP + 0xb0) = 0x1000028f0
1000028cc ADR X8, #6660 ; // ; ->R8 = 0x1000042d0
1000028d0 NOP ; // ;
1000028d4 STR X8, [SP, #184] ; // ; // ; *(SP + 0xb8) = 0x1000042d0
1000028d8 STRB W31, [X31, #192] ; // ; // ; *(SP + 0xc0) = 0x0
1000028dc ADD X1, SP, #160 ; R1 = SP+0xa0
1000028e0 BL ; // libSystem.B.dylib::_xpc_connection_set_event_handler ; 0x1000032a4
1000028e4 LDR X0, [X21, #912] ; // ; // ; -R0 = *(R21 + 912) = .. *(0x100004390, no sym) = 0x0 ... ?..
1000028e8 BL ; // libSystem.B.dylib::_xpc_connection_resume ; 0x100003298
1000028ec BL ; // libSystem.B.dylib::_dispatch_main ; 0x100003154

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:


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:


So far - past the standard prolog (which sets up the stack and __stack_chk_guard) we have:

   bzero(SP + 0xc8,1024);
   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 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 (if the -r is not found), the strcmp can't pick up both and decompiles assuming (the closest code path). If the string actually IS, 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 /private/var/mobile/Library/Logs/CrashReporter, if we got -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 /Library/Logs/DiagnosticReports can also be served, in a MultiUser configuration, which is why we need 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);
	AFCLog(1, "kqueue(): %m");

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");
	servedDir = realpath(servedDir, NULL);
	if (!servedDir)
	    int err =  *(__error());
	     if (err !=  ENOENT) {
		  AFCLog(0x1, "realpath(%s) failure: %s");
	     int rc = mkdir ("/private/var/mobile/Library/Logs/CrashReporter", 0755);
	     if (rc < 0)
		   int err = *(__error());
		   AFCLog(0x1, "could not create directory %s (%d)",
#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
		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()...
	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);
	// 2634
	void *sbExt = sandbox_extension_issue_file ("",

	 if (!sbExt) {
		// 26cc
	        _AFCLog(0x1,"sandbox_extension_issue_file failed: %d", errno );
	// 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);

	uint64_trc1 = sandbox_extension_consume(sbExt);
	if (rc1 < 0) {
		_AFCLog(0x1,"sandbox_extension_consume failed (%d): %s", errno, sbExt );
		_AFCLog(0x1,"failed to load sandbox" );

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 Sandbox.kext, and since the profile doesn't know ahead of time which directory is to be "served", 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:
		// or - 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");
		// 28a8

		// Jtool automatically follows blocks, and gets you the block function
		// Take that **, IDA/Hopper :-)
		xpc_connection_set_event_handler(afcdMach, _func_100028f0);

	// 28ec
	dispatch_main(); // never returns.

The rest is pretty simple. But if you want AFCD2, patching is simple:

To turn AFCD into AFCD2 simply patch out the 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 a.ent. Do this on ANOTHER binary, call it 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 jtooling. 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 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.

* -Note to "_____ can do this too" flamers: Yes, I know you can probably do that with IDA, and no, I don't care. Or at least, won't care until they come out with a CLI interface. And yeah, you can probably do that with some other third party tool, radare or what not. But I didn't write that tool. I wrote jtool.
** - If you are literally thinking about taking that, it's a simple trick : The function is at block + 0x10. See MOXiI Volume I, Chapter 8.