Running iOS in QEMU to an interactive bash shell (1): tutorial

By Jonathan Afek (@JonathanAfek)
June 17, 2019


While wanting to do some iOS security research and inspired by the work done by zhuowei, I decided to try and get this emulation project further along the boot process. The goal was to get the system to boot without having to patch the kernel beforehand or during the boot process, have new modules that extend QEMU’s capabilities to execute arm64 XNU systems and, get an interactive bash shell. This post is the first post in a 2-post series, in which I will present instructions for executing iOS on QEMU and launching an interactive bash shell. In the second post, I will detail some of the research that was required in order to get there. For this project, the iOS version and device that were chosen are iOS 12.1 and iPhone 6s Plus, because this specific iOS 12 image comes with a lot of symbols exported in the kernel image compared to other iOS kernel images that are usually stripped of most symbols. This presented some more challenges, because it is a non-KTRR device that uses a secure monitor image, and that required changes in the stages of the solution already done by zhuowei. Another change is that I wanted this feature to be in external modules that could later be extended and used to create modules for other iOS devices and versions, instead of having the code inside the core QEMU code.

Current Status

The project is now available at qemu-aleph-git with the required scripts at qemu-scripts-aleph-git. The current status allows booting to user mode with a read only mounted ram disk, to which new executables and launchd items can be added (before boot), and those can use the dyld cache from the main disk image copied to the ram disk and communicate with the user via the emulated UART channel. Here is a demonstration of running an interactive bash shell with this project:

iOS bash shell in QEMU

This lets you execute whatever user mode process you want as root, with whatever entitlements you choose, and debug the process and/or the kernel with a kernel debugger:

iOS QEMU gdb kernel debugger

Current Limitations

  1. There is a long suspension where nothing happens for a few seconds before mounting the ram disk.
  2. It only works with a ram disk image which is mounted as read only, and is limited to 2GB in size.
  3. We can only communicate with the guest iOS over UART, and no other communication channels are currently available.
  4. No devices emulation: screen, touch, wifi, BT or anything else.
  5. Only a single emulated CPU is currently supported.


To start the process we first need to prepare a kernel image, a device tree, a static trust cache, and ram disk images. To get the images we first need to get the update file from Apple: iOS 12.1 update file. This is actually a zip file which we can extract:

Downloads jonathanafek$ unzip iPhone_5.5_12.1_16B92_Restore.ipsw
Archive:  iPhone_5.5_12.1_16B92_Restore.ipsw
   creating: Firmware/
  inflating: Restore.plist           
   creating: Firmware/usr/
   creating: Firmware/usr/local/
  inflating: BuildManifest.plist     
  inflating: Firmware/Mav10-7.21.00.Release.plist  
   creating: Firmware/all_flash/
  inflating: Firmware/all_flash/DeviceTree.n66ap.im4p.plist  
  inflating: Firmware/all_flash/LLB.n56.RELEASE.im4p  
  inflating: Firmware/all_flash/[email protected]~iphone.im4p  
  inflating: Firmware/all_flash/[email protected]~iphone.im4p  
  inflating: Firmware/all_flash/LLB.n66.RELEASE.im4p  
  inflating: Firmware/all_flash/sep-firmware.n56.RELEASE.im4p.plist  
  inflating: Firmware/all_flash/iBoot.n56.RELEASE.im4p.plist  
  inflating: Firmware/all_flash/[email protected]~iphone.im4p  
  inflating: Firmware/all_flash/iBoot.n66m.RELEASE.im4p  
  inflating: Firmware/all_flash/iBoot.n56.RELEASE.im4p  
  inflating: Firmware/all_flash/DeviceTree.n66ap.im4p  
  inflating: Firmware/all_flash/sep-firmware.n66m.RELEASE.im4p.plist  
  inflating: Firmware/all_flash/[email protected]~iphone.im4p  
  inflating: Firmware/all_flash/[email protected]~iphone-lightning.im4p  
   creating: Firmware/dfu/
  inflating: Firmware/dfu/iBSS.n56.RELEASE.im4p.plist  
  inflating: Firmware/all_flash/[email protected]~iphone-lightning.im4p  
  inflating: Firmware/all_flash/[email protected]~iphone.im4p  
  inflating: Firmware/dfu/iBEC.n66m.RELEASE.im4p.plist  
  inflating: Firmware/dfu/iBSS.n66.RELEASE.im4p  
  inflating: Firmware/048-32459-105.dmg.trustcache  
  inflating: Firmware/dfu/iBSS.n66m.RELEASE.im4p  
  inflating: Firmware/dfu/iBEC.n56.RELEASE.im4p.plist  
  inflating: Firmware/all_flash/sep-firmware.n56.RELEASE.im4p  
  inflating: Firmware/Mav13-5.21.00.Release.bbfw  
  inflating: Firmware/all_flash/sep-firmware.n66m.RELEASE.im4p  
  inflating: Firmware/all_flash/LLB.n66m.RELEASE.im4p.plist  
  inflating: Firmware/all_flash/iBoot.n66.RELEASE.im4p.plist  
  inflating: Firmware/dfu/iBSS.n56.RELEASE.im4p  
  inflating: Firmware/all_flash/DeviceTree.n66map.im4p.plist  
  inflating: Firmware/all_flash/DeviceTree.n56ap.im4p.plist  
  inflating: Firmware/all_flash/LLB.n66.RELEASE.im4p.plist  
   creating: Firmware/AOP/
  inflating: Firmware/AOP/aopfw-s8000aop.im4p  
  inflating: Firmware/dfu/iBEC.n56.RELEASE.im4p  
  inflating: Firmware/all_flash/LLB.n66m.RELEASE.im4p  
  inflating: Firmware/all_flash/iBoot.n66.RELEASE.im4p  
  inflating: Firmware/all_flash/sep-firmware.n66.RELEASE.im4p  
  inflating: Firmware/048-31952-103.dmg.trustcache  
  inflating: Firmware/all_flash/sep-firmware.n66.RELEASE.im4p.plist  
  inflating: Firmware/dfu/iBSS.n66.RELEASE.im4p.plist  
  inflating: Firmware/all_flash/DeviceTree.n66map.im4p  
  inflating: Firmware/dfu/iBSS.n66m.RELEASE.im4p.plist  
  inflating: Firmware/all_flash/[email protected]~iphone.im4p  
  inflating: Firmware/all_flash/iBoot.n66m.RELEASE.im4p.plist  
  inflating: 048-32651-104.dmg       
  inflating: Firmware/all_flash/LLB.n56.RELEASE.im4p.plist  
  inflating: Firmware/dfu/iBEC.n66.RELEASE.im4p  
  inflating: Firmware/dfu/iBEC.n66.RELEASE.im4p.plist  
  inflating: Firmware/dfu/iBEC.n66m.RELEASE.im4p  
  inflating: kernelcache.release.iphone7  
  inflating: Firmware/048-32651-104.dmg.trustcache  
  inflating: Firmware/Mav13-5.21.00.Release.plist  
  inflating: Firmware/all_flash/DeviceTree.n56ap.im4p  
  inflating: Firmware/Mav10-7.21.00.Release.bbfw  
  inflating: 048-32459-105.dmg       
  inflating: kernelcache.release.n66  
 extracting: 048-31952-103.dmg       

Next, we need to clone the supporting scripts repository:

Downloads jonathanafek$ git clone [email protected]:alephsecurity/xnu-qemu-arm64-tools.git
Cloning into 'xnu-qemu-arm64-tools'...
remote: Enumerating objects: 16, done.
remote: Counting objects: 100% (16/16), done.
remote: Compressing objects: 100% (11/11), done.
remote: Total 16 (delta 4), reused 16 (delta 4), pack-reused 0
Receiving objects: 100% (16/16), 5.16 KiB | 5.16 MiB/s, done.
Resolving deltas: 100% (4/4), done.

And extract the ASN1 encoded kernel image:

Downloads jonathanafek$ python3 xnu-qemu-arm64-tools/bootstrap_scripts/asn1kerneldecode.py kernelcache.release.n66 kernelcache.release.n66.asn1decoded

This decoded image now includes the lzss compressed kernel. You can use this code to decompress it or use this translated code with python2.

Downloads jonathanafek$ python2 xnu-qemu-arm64-tools/bootstrap_scripts/decompress_lzss.py kernelcache.release.n66.asn1decoded kernelcache.release.n66.out

Now let’s prepare a device tree which we can boot with (more details about the device tree in the second post). First, extract it from the ASN1 encoded file:

Downloads jonathanafek$ python3 xnu-qemu-arm64-tools/bootstrap_scripts/asn1dtredecode.py Firmware/all_flash/DeviceTree.n66ap.im4p Firmware/all_flash/DeviceTree.n66ap.im4p.out

Now we have to set up the ram disk. First, ASN1 decode it:

Downloads jonathanafek$ python3 xnu-qemu-arm64-tools/bootstrap_scripts/asn1rdskdecode.py ./048-32651-104.dmg ./048-32651-104.dmg.out

Next, resize it so it has room for the dynamic loader cache file (needed by bash and other executables), mount it, and force usage of file permissions on it:

Downloads jonathanafek$ hdiutil resize -size 1.5G -imagekey diskimage-class=CRawDiskImage 048-32651-104.dmg.out
Downloads jonathanafek$ hdiutil attach -imagekey diskimage-class=CRawDiskImage 048-32651-104.dmg.out
Downloads jonathanafek$ sudo diskutil enableownership /Volumes/PeaceB16B92.arm64UpdateRamDisk/

Now let’s mount the regular update disk image 048-31952-103.dmg:

Downloads jonathanafek$ hdiutil attach ./048-31952-103.dmg 

Create a directory for the dynamic loader cache in the ram disk, copy the cache from the update image and chown it to root:

Downloads jonathanafek$ sudo mkdir -p /Volumes/PeaceB16B92.arm64UpdateRamDisk/System/Library/Caches/com.apple.dyld/
Downloads jonathanafek$ sudo cp /Volumes/PeaceB16B92.N56N66OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64 /Volumes/PeaceB16B92.arm64UpdateRamDisk/System/Library/Caches/com.apple.dyld/
Downloads jonathanafek$ sudo chown root /Volumes/PeaceB16B92.arm64UpdateRamDisk/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64

Get precompiled user mode tools for iOS, including bash, from rootlessJB and/or iOSBinaries. Alternatively, compile your own iOS console binaries as described here.

Downloads jonathanafek$ git clone https://github.com/jakeajames/rootlessJB
Cloning into 'rootlessJB'...
remote: Enumerating objects: 6, done.
remote: Counting objects: 100% (6/6), done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 253 (delta 2), reused 0 (delta 0), pack-reused 247
Receiving objects: 100% (253/253), 7.83 MiB | 3.03 MiB/s, done.
Resolving deltas: 100% (73/73), done.
Downloads jonathanafek$ cd rootlessJB/rootlessJB/bootstrap/tars/
tars jonathanafek$ tar xvf iosbinpack.tar
tars jonathanafek$ sudo cp -R iosbinpack64 /Volumes/PeaceB16B92.arm64UpdateRamDisk/
tars jonathanafek$ cd -

Configure launchd to not execute any services:

Downloads jonathanafek$ sudo rm /Volumes/PeaceB16B92.arm64UpdateRamDisk/System/Library/LaunchDaemons/*

And now, configure it to launch the interactive bash shell by creating a new file under /Volumes/PeaceB16B92.arm64UpdateRamDisk/System/Library/LaunchDaemons/bash.plist with the following contents:

<?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">

As a side note, you can always convert the binary plist files that you find natively in iOS images to text xml format and back to binary format with:

Downloads jonathanafek$ plutil -convert xml1 file.plist
Downloads jonathanafek$ vim file.plist
Downloads jonathanafek$ plutil -convert binary1 file.plist

For launch daemon, iOS accepts both xml and binary plist files.

Since the new binaries are signed, but not by Apple, they need to be trusted by the static trust cache that we will create. To do this, we need to get jtool (also available via Homebrew: brew cask install jtool). Once we have the tool, we have to run it on every binary we wish to be trusted, extract the first 40 characters of its CDHash, and put it in a new file named tchashes. A sample execution of jtool looks like this:

Downloads jonathanafek$ jtool --sig --ent /Volumes/PeaceB16B92.arm64UpdateRamDisk/iosbinpack64/bin/bash
Blob at offset: 1308032 (10912 bytes) is an embedded signature
Code Directory (10566 bytes)
                Version:     20001
                Flags:       none
                CodeLimit:   0x13f580
                Identifier:  /Users/jakejames/Desktop/jelbreks/multi_path/multi_path/iosbinpack64/bin/bash (0x58)
                CDHash:      7ad4d4c517938b6fdc0f5241cd300d17fbb52418b1a188e357148f8369bacad1 (computed)
                # of Hashes: 320 code + 5 special
                Hashes @326 size: 32 Type: SHA-256
 Empty requirement set (12 bytes)
Entitlements (279 bytes) :
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">

In the above case, we need to write down 7ad4d4c517938b6fdc0f5241cd300d17fbb52418 in tchashes. For convenience, the following command will extract the correct part of the hash from each of the binaries we put in the image:

Downloads jonathanafek$ touch ./tchashes
Downloads jonathanafek$ for filename in $(find /Volumes/PeaceB16B92.arm64UpdateRamDisk/iosbinpack64 -type f); do jtool --sig --ent $filename 2>/dev/null; done | grep CDHash | cut -d' ' -f6 | cut -c 1-40 >> ./tchashes

Create the static trust cache blob:

Downloads jonathanafek$ python3 xnu-qemu-arm64-tools/bootstrap_scripts/create_trustcache.py tchashes static_tc

Now is a good time to unmount both volumes.

Downloads jonathanafek$ hdiutil detach /Volumes/PeaceB16B92.arm64UpdateRamDisk
Downloads jonathanafek$ hdiutil detach /Volumes/PeaceB16B92.N56N66OS   

We now have all the images and files prepared. Let’s get the modified QEMU code (more detailed info on the work done in QEMU will be in the second post in the series):

Downloads jonathanafek$ git clone [email protected]:alephsecurity/xnu-qemu-arm64.git
Cloning into 'xnu-qemu-arm64'...
remote: Enumerating objects: 377340, done.
remote: Total 377340 (delta 0), reused 0 (delta 0), pack-reused 377340
Receiving objects: 100% (377340/377340), 187.68 MiB | 5.32 MiB/s, done.
Resolving deltas: 100% (304400/304400), done.
Checking out files: 100% (6324/6324), done.

and compile it:

Downloads jonathanafek$ cd xnu-qemu-arm64
xnu-qemu-arm64 jonathanafek$ ./configure --target-list=aarch64-softmmu --disable-capstone --disable-pie --disable-slirp

Install prefix    /usr/local
BIOS directory    /usr/local/share/qemu
firmware path     /usr/local/share/qemu-firmware
binary directory  /usr/local/bin
library directory /usr/local/lib
module directory  /usr/local/lib/qemu
libexec directory /usr/local/libexec
include directory /usr/local/include
config directory  /usr/local/etc
local state directory   /usr/local/var
Manual directory  /usr/local/share/man
ELF interp prefix /usr/gnemul/qemu-%M
Source path       /Users/jonathanafek/Downloads/xnu-qemu-arm64
GIT binary        git
GIT submodules    ui/keycodemapdb tests/fp/berkeley-testfloat-3 tests/fp/berkeley-softfloat-3 dtc
C compiler        cc
Host C compiler   cc
C++ compiler      c++
Objective-C compiler clang
ARFLAGS           rv
CFLAGS            -O2 -g
QEMU_CFLAGS       -I/opt/local/include/pixman-1 -I$(SRC_PATH)/dtc/libfdt -D_REENTRANT -I/opt/local/include/glib-2.0 -I/opt/local/lib/glib-2.0/include -I/opt/local/include -m64 -mcx16 -DOS_OBJECT_USE_OBJC=0 -arch x86_64 -D_GNU_SOURCE -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE -Wstrict-prototypes -Wredundant-decls -Wall -Wundef -Wwrite-strings -Wmissing-prototypes -fno-strict-aliasing -fno-common -fwrapv  -Wno-error=address-of-packed-member -Wno-string-plus-int -Wno-initializer-overrides -Wexpansion-to-defined -Wendif-labels -Wno-shift-negative-value -Wno-missing-include-dirs -Wempty-body -Wnested-externs -Wformat-security -Wformat-y2k -Winit-self -Wignored-qualifiers -Wold-style-definition -Wtype-limits -fstack-protector-strong -I/opt/local/include -I/opt/local/include/p11-kit-1 -I/opt/local/include  -I/opt/local/include/libpng16 -I/opt/local/include
LDFLAGS           -framework Hypervisor -m64 -framework CoreFoundation -framework IOKit -arch x86_64 -g
QEMU_LDFLAGS      -L$(BUILD_DIR)/dtc/libfdt
make              make
install           install
python            python3 -B (3.7.6)
slirp support     no 
module support    no
host CPU          x86_64
host big endian   no
target list       aarch64-softmmu
gprof enabled     no
sparse enabled    no
strip binaries    yes
profiler          no
static build      no
Cocoa support     yes
SDL support       no 
SDL image support no
GTK support       no 
GTK GL support    no
VTE support       no 
TLS priority      NORMAL
GNUTLS support    yes
libgcrypt         no
nettle            yes (3.4.1)
  XTS             no
libtasn1          yes
PAM               yes
iconv support     yes
curses support    no
virgl support     no 
curl support      yes
mingw32 support   no
Audio drivers     coreaudio 
Block whitelist (rw) 
Block whitelist (ro) 
VirtFS support    no
Multipath support no
VNC support       yes
VNC SASL support  yes
VNC JPEG support  yes
VNC PNG support   yes
xen support       no
brlapi support    no
bluez  support    no
Documentation     no
PIE               no
vde support       yes
netmap support    no
Linux AIO support no
ATTR/XATTR support no
Install blobs     yes
KVM support       no
HAX support       yes
HVF support       yes
WHPX support      no
TCG support       yes
TCG debug enabled no
TCG interpreter   no
malloc trim support no
RDMA support      no
PVRDMA support    no
fdt support       git
membarrier        no
preadv support    no
fdatasync         no
madvise           yes
posix_madvise     yes
posix_memalign    yes
libcap-ng support no
vhost-net support yes
vhost-crypto support yes
vhost-scsi support no
vhost-vsock support no
vhost-user support yes
vhost-user-fs support yes
Trace backends    log
spice support     no 
rbd support       no
xfsctl support    no
smartcard support no
libusb            yes
usb net redir     no
OpenGL support    no
OpenGL dmabufs    no
libiscsi support  no
libnfs support    no
build guest agent yes
QGA VSS support   no
QGA w32 disk info no
QGA MSI support   no
seccomp support   no
coroutine backend sigaltstack
coroutine pool    yes
debug stack usage no
mutex debugging   no
crypto afalg      no
GlusterFS support no
gcov              gcov
gcov enabled      no
TPM support       yes
libssh support    no
QOM debugging     yes
Live block migration yes
lzo support       no
snappy support    no
bzip2 support     yes
lzfse support     no
NUMA host support no
libxml2           yes
tcmalloc support  no
jemalloc support  no
avx2 optimization no
replication support yes
VxHS block device no
bochs support     yes
cloop support     yes
dmg support       yes
qcow v1 support   yes
vdi support       yes
vvfat support     yes
qed support       yes
parallels support yes
sheepdog support  yes
capstone          no
libpmem support   no
libudev           no
default devices   yes
plugin support    no
cross containers  no

xnu-qemu-arm64 jonathanafek$ make -j16
xnu-qemu-arm64 jonathanafek$ cd -

And all there’s left to do is execute:

Downloads jonathanafek$ ./xnu-qemu-arm64/aarch64-softmmu/qemu-system-aarch64 -M iPhone6splus-n66-s8000,kernel-filename=kernelcache.release.n66.out,dtb-filename=Firmware/all_flash/DeviceTree.n66ap.im4p.out,ramdisk-filename=048-32651-104.dmg.out,tc-filename=static_tc,kern-cmd-args="debug=0x8 kextlog=0xfff cpus=1 rd=md0 serial=2",xnu-ramfb=off -cpu max -m 6G -serial mon:stdio

iBoot version:
corecrypto_kext_start called
FIPSPOST_KEXT [38130750] fipspost_post:156: PASSED: (6 ms) - fipspost_post_integrity
FIPSPOST_KEXT [38201250] fipspost_post:162: PASSED: (2 ms) - fipspost_post_hmac
FIPSPOST_KEXT [38233562] fipspost_post:163: PASSED: (0 ms) - fipspost_post_aes_ecb
FIPSPOST_KEXT [38275375] fipspost_post:164: PASSED: (1 ms) - fipspost_post_aes_cbc
FIPSPOST_KEXT [41967250] fipspost_post:165: PASSED: (153 ms) - fipspost_post_rsa_sig
FIPSPOST_KEXT [44373250] fipspost_post:166: PASSED: (99 ms) - fipspost_post_ecdsa
FIPSPOST_KEXT [44832437] fipspost_post:167: PASSED: (18 ms) - fipspost_post_ecdh
FIPSPOST_KEXT [44861312] fipspost_post:168: PASSED: (0 ms) - fipspost_post_drbg_ctr
FIPSPOST_KEXT [44922625] fipspost_post:169: PASSED: (2 ms) - fipspost_post_aes_ccm
FIPSPOST_KEXT [44994250] fipspost_post:171: PASSED: (2 ms) - fipspost_post_aes_gcm
FIPSPOST_KEXT [45042125] fipspost_post:172: PASSED: (1 ms) - fipspost_post_aes_xts
FIPSPOST_KEXT [45109687] fipspost_post:173: PASSED: (2 ms) - fipspost_post_tdes_cbc
FIPSPOST_KEXT [45167062] fipspost_post:174: PASSED: (1 ms) - fipspost_post_drbg_hmac
FIPSPOST_KEXT [45178250] fipspost_post:197: all tests PASSED (300 ms)
Darwin Image4 Validation Extension Version 1.0.0: Tue Oct 16 21:46:27 PDT 2018; root:AppleImage4-1.200.18~1853/AppleImage4/RELEASE_ARM64
AppleS8000IO::start: chip-revision: A0
AppleS8000IO::start: this: <ptr>, TCC virt addr: <ptr>, TCC phys addr: 0x202240000
AUC[<ptr>]::probe(<ptr>, <ptr>)
AppleCredentialManager: init: called, instance = <ptr>.
ACMRM: _loadRestrictedModeForceEnable: restricted mode force-enabled = 0 .
ACMRM-A: init: called, .
ACMRM-A: _loadAnalyticsCollectionPeriod: analytics collection period = 86400 .
ACMRM: _loadStandardModeTimeout: standard mode timeout = 259200 .
ACMRM-A: notifyStandardModeTimeoutChanged: called, value = 259200 (modified = YES).
ACMRM: _loadGracePeriodTimeout: device lock timeout = 3600 .
ACMRM-A: notifyGracePeriodTimeoutChanged: called, value = 3600 (modified = YES).
AppleCredentialManager: init: returning, result = true, instance = <ptr>.
virtual bool AppleARMLightEmUp::start(IOService *): starting...
AppleKeyStore starting (BUILT: Oct 17 2018 20:34:07)
AppleSEPKeyStore::start: _sep_enabled = 1
AppleCredentialManager: start: called, instance = <ptr>.
ACMRM: _publishIOResource: AppleUSBRestrictedModeTimeout = 259200.
AppleCredentialManager: start: initializing power management, instance = <ptr>.
AppleCredentialManager: start: started, instance = <ptr>.
AppleCredentialManager: start: returning, result = true, instance = <ptr>.
AppleARMPE::getGMTTimeOfDay can not provide time of day: RTC did not show up
: apfs_module_start:1277: load: com.apple.filesystems.apfs, v748.220.3, 748.220.3, 2018/10/16
com.apple.AppleFSCompressionTypeZlib kmod start
IOSurface disallowing global lookups
apfs_sysctl_register:911: done registering sysctls.
com.apple.AppleFSCompressionTypeZlib load succeeded
L2TP domain init
L2TP domain init complete
PPTP domain init
BSD root: md0, major 2, minor 0
apfs_vfsop_mountroot:1468: apfs: mountroot called!
apfs_vfsop_mount:1231: unable to root from devvp <ptr> (root_device): 2
apfs_vfsop_mountroot:1472: apfs: mountroot failed, error: 2
hfs: mounted PeaceB16B92.arm64UpdateRamDisk on device b(2, 0)
: : Darwin Bootstrapper Version 6.0.0: Tue Oct 16 22:26:06 PDT 2018; root:libxpc_executables-1336.220.5~209/launchd/RELEASE_ARM64
boot-args = debug=0x8 kextlog=0xfff cpus=1 rd=md0 serial=2
Thu Jan  1 00:01:05 1970 localhost com.apple.xpc.launchd[1] <Notice>: Restore environment starting.
Thu Jan  1 00:01:05 1970 localhost com.apple.xpc.launchd[1] <Notice>: Early boot complete. Continuing system boot.
Thu Jan  1 00:01:06 1970 localhost com.apple.xpc.launchd[1] (com.apple.xpc.launchd.domain.system) <Error>: Could not read path: path = /AppleInternal/Library/LaunchDaemons, error = 2: No such file or directory
Thu Jan  1 00:01:06 1970 localhost com.apple.xpc.launchd[1] (com.apple.xpc.launchd.domain.system) <Error>: Could not read path: path = /System/Library/NanoLaunchDaemons, error = 2: No such file or directory
Thu Jan  1 00:01:06 1970 localhost com.apple.xpc.launchd[1] (com.apple.xpc.launchd.domain.system) <Error>: Failed to bootstrap path: path = /System/Library/NanoLaunchDaemons, error = 2: No such file or directory
bash-4.4# export PATH=$PATH:/iosbinpack64/usr/bin:/iosbinpack64/bin:/iosbinpack64/usr/sbin:/iosbinpack64/sbin
bash-4.4# id
uid=0(root) gid=0(wheel) groups=0(wheel),1(daemon),2(kmem),3(sys),4(tty),5(operator),8(procview),9(procmod),20(staff),29(certusers),80(admin)
bash-4.4# pwd
bash-4.4# ls -la
total 18
drwxr-xr-x  17 root    wheel  748 Jun 10  2019 .
drwxr-xr-x  17 root    wheel  748 Jun 10  2019 ..
-rw-r--r--   1 root    wheel    0 Oct 20  2018 .Trashes
drwx------   2 mobile  staff  170 Jun 10  2019 .fseventsd
drwxr-xr-x   4 root    wheel  136 Oct 20  2018 System
drwxr-xr-x   2 root    wheel  272 Oct 20  2018 bin
dr-xr-xr-x   3 root    wheel  660 Jan  1 00:01 dev
lrwxr-xr-x   1 root    wheel   11 Oct 20  2018 etc -> private/etc
drwxr-xr-x   7 root    wheel  374 Jun 10  2019 iosbinpack64
drwxr-xr-x   2 root    wheel   68 Oct 20  2018 mnt1
drwxr-xr-x   2 root    wheel   68 Oct 20  2018 mnt2
drwxr-xr-x   2 root    wheel   68 Oct 20  2018 mnt3
drwxr-xr-x   2 root    wheel   68 Oct 20  2018 mnt4
drwxr-xr-x   2 root    wheel   68 Oct 20  2018 mnt5
drwxr-xr-x   2 root    wheel   68 Oct 20  2018 mnt6
drwxr-xr-x   2 root    wheel   68 Oct 20  2018 mnt7
drwxr-xr-x   4 root    wheel  136 Oct 20  2018 private
drwxr-xr-x   2 root    wheel  510 Oct 20  2018 sbin
drwxr-xr-x   9 root    wheel  306 Oct 20  2018 usr
lrwxr-xr-x   1 root    admin   11 Oct 20  2018 var -> private/var

And we have an interactive bash shell! :)

Note that the last flag (-serial mon:stdio) will forward all shell combinations (such as Ctrl+C) to the shell. To shut down QEMU, close its (empty) window.

To get a kernel debugger, -S -s should be added to the QEMU command line, and then it’s possible to execute target remote :1234 in a gdb console which supports this architecture. More details on how to get this gdb and perform this can be found in here. You can also get the relevant gdb on OSX with mac ports, while adding the multiarch and python27 options to the gdb port.

Future Improvements

  1. Make this boot much faster, without a long suspension before mounting the ram disk.
  2. Add support to emulate the iOS as a USB device and communicate over usbmuxd. This will enable us to connect over SSH and therefore copy files using scp, have a more robust terminal, conduct security research for network protocols, use gdbserver to debug user mode applications and more.
  3. Add support for emulated physical storage to work with a r/w mounted disk, that won’t be a ram disk and will not be limited to 2GB.
  4. Add support for devices such as screen, touch, wifi, BT, etc…
  5. Add support for more iDevices and iOS versions.

Fun Feature

User applications get loaded at different addresses every boot because of ASLR, and can share virtual addresses with one another, so using a regular breakpoint on a static virtual address in gdb can be challenging, when debugging user mode applications. Therefore, I added another fun feature to help debug user mode applications in this kernel debugger. When QEMU encounters the HLT aarch64 instruction, it breaks in gdb just as if it was a gdb breakpoint, so all you have to do to debug user mode applications in the kernel debugger is to patch the application with an HLT instruction, using ghidra, for example: HLT patch in ghidra 1 HLT patch in ghidra 2 HLT patch in ghidra 3

and then sign it using jtool with any required entitlements:

Downloads jonathanafek$ ./jtool/jtool --sign --ent ent.xml --inplace bin

After that, you need to add the new CDHash to the tchashes file, and recreate the static trust cache.

Following that, gdb will break when it encounters the HLT instruction in the user mode application so we can debug the application in the kernel debugger: HLT break in gdb


If you have gotten this far, and are interested in learning about some of the research and some of the implementation details, then carry on and read the second blog post in the series, as well as the code, comments and commit messages. The second blog post in the series will be available soon, stay tuned.

Please let me know if you have any ideas, suggestions, or other comments below.