Disclaimer: I do not support piracy. In this post, I’m not gonna share any real encryption keys or even the name of the app. The primary purpose of this project was to improve my reverse engineering skills.

Intro

I’m a huge fan of podcasts and audiobooks. I always listen to something while driving or during workouts. Recently I found an awesome online book store that has tons of new Ukrainian audiobooks. Unfortunately, the only option to listen to them is by using an iOS or Android app. But I really wanted to get them on my Garmin watch so I can listen during my runs 🏃‍♂️. So the only solution was to extract the files directly from the app. Doing so revealed that the books are encrypted. And that led to this post.

The post consists of two parts. The first part is about extracting and decrypting the books - a relatively easy task 😅. The second - bonus part is about figuring out how the actual encryption key is created. Is it unique per user? Is it hardcoded or comes from the backend? etc.

Part I

Extracting the files

All app generated content is located at:

iphone-7p:~ root# ls /var/mobile/Containers/Data/Application/<GUID>/

Where <GUID> is a unique identifier. You can find a correct <GUID> by looking into .com.apple.mobile_container_manager.metadata.plist file at the top of each directory. The file contains an app bundle id under MCMMetadataIdentifier key. Or by using an app like Filza that can extract that info for you and render an app name alongside the <GUID> while browsing the filesystem.

Filza

For this particular app, I found all the books data under:

/var/mobile/Containers/Data/Application/<GUID>/Library/Application\ Support/books_data/

Each audio book is just a bunch of .mp3 files - chapters. Extracting those is as simple as just copying them over ssh:

mbp:~ scp -P 2222 root@localhost:/var/mobile/Containers/Data/Application/<GUID>/Library/Application\ Support/books_data/* .

Playing around with files on my Mac, reveals that they are encrypted. The first and obvious - they can’t be simply played, and the second - binwalk shows homogeneous entropy of 0.997, which means that file content is close to absolute randomness, so it’s either encrypted or compressed.

mbp:~ binwalk -E 1.mp3

DECIMAL       HEXADECIMAL     ENTROPY
--------------------------------------------------------------------------------
0             0x0             Rising entropy edge (0.996870)

Decryption routine

To find a decryption routine, I started by putting breakpoint into a libSystem’s open function, which powers all the higher-level Foundation APIs responsible for working with files.

(lldb) b open
Breakpoint 1: 193 locations.

Now, when we try to play something, a breakpoint is hit:

Process 1051 resuming
Process 1051 stopped
* thread #39, queue = 'load_local_file', stop reason = breakpoint 1.36
    frame #0: 0x00000001b7976774 libsystem_kernel.dylib` open
libsystem_kernel.dylib open:
->  0x1b7976774 <+0>:  sub    sp, sp, #0x20             ; =0x20
    0x1b7976778 <+4>:  stp    x29, x30, [sp, #0x10]
    0x1b797677c <+8>:  add    x29, sp, #0x10            ; =0x10
    0x1b7976780 <+12>: tbnz   w1, #0x9, 0x1b797678c     ; <+24>
    0x1b7976784 <+16>: mov    w8, #0x0
    0x1b7976788 <+20>: b      0x1b7976790               ; <+28>
    0x1b797678c <+24>: ldr    w8, [x29, #0x10]
    0x1b7976790 <+28>: and    w2, w8, #0xffff
Target 0: (TheApp) stopped.
(lldb) x/s $x0
0x16d432766: "/var/mobile/Containers/Data/Application/<GUID>/Library/Application Support/books_data/1.mp3"

Now we can see the backtrace:

(lldb) bt
* thread #39, queue = 'load_local_file', stop reason = breakpoint 1.36
  * frame #0: 0x00000001b7976774 libsystem_kernel.dylib open
    frame #1: 0x000000018db0a054 Foundation` _NSOpenFileDescriptor  + 40
    frame #2: 0x000000018da445b4 Foundation` -[NSConcreteFileHandle initWithURL:flags:createMode:error:]  + 124
    frame #3: 0x000000018da4c588 Foundation` +[NSFileHandle fileHandleForReadingFromURL:error:]  + 52
    frame #4: 0x0000000102d7cdbc TheApp` ___lldb_unnamed_symbol13093$$TheApp  + 1356
    frame #5: 0x0000000102c7687c TheApp` ___lldb_unnamed_symbol6667$$TheApp  + 20
    frame #6: 0x000000018c489298 libdispatch.dylib` _dispatch_call_block_and_release  + 24
    frame #7: 0x000000018c48a280 libdispatch.dylib` _dispatch_client_callout  + 16
    frame #8: 0x000000018c42f390 libdispatch.dylib` _dispatch_continuation_pop$VARIANT$mp  + 412
    frame #9: 0x000000018c42ead4 libdispatch.dylib` _dispatch_async_redirect_invoke  + 596
    frame #10: 0x000000018c43bf3c libdispatch.dylib` _dispatch_root_queue_drain  + 376
    frame #11: 0x000000018c43c704 libdispatch.dylib` _dispatch_worker_thread2  + 124
    frame #12: 0x00000001d2eee568 libsystem_pthread.dylib` _pthread_wqthread  + 212
(lldb) im loo -a 0x0000000102d7cdbc
      Address: TheApp[0x0000000100210dbc] (TheApp.__TEXT.__text + 2149520)
      Summary: TheApp ___lldb_unnamed_symbol13093$$TheApp + 1356

Loading the app into a Hoppper Dissassembler and looking at address 0x100210dbc reveals the origin of this call. But more interestingly it shows a call to CCCryptorGetOutputLength not far away.

Extracting the key

CCCryptor* functions are part of Apple’s CommonCrypto framework, which, as the name suggests, provides some basic crypto routines. We can find all the imported functions in the disassembler or by using otool:

mbp:~ otool -IV TheApp | grep CCCryptor
0x0000000100309cd8   794 _CCCryptorCreateWithMode
0x0000000100309ce4   795 _CCCryptorGetOutputLength
0x0000000100309cf0   796 _CCCryptorRelease
0x0000000100309cfc   797 _CCCryptorUpdate

This way, we can get the list of all the CommonCrypto functions used by the app. The most interesting are CCCryptorCreateWithMode and CCCryptorUpdate. CCCryptorCreateWithMode - actually creates a cryptographic context with all the parameters (twelve in total), including the type of the encryption algorithm and the key! CCCryptorUpdate - performs the encryption/decryption and writes data to provided buffer.

Let’s set the breakpoints on those two calls.

(lldb) b CCCryptorCreateWithMode
Breakpoint 2: where = libcommonCrypto.dylib CCCryptorCreateWithMode, address = 0x00000001d2d7502c
(lldb) b CCCryptorUpdate
Breakpoint 3: where = libcommonCrypto.dylib CCCryptorUpdate, address = 0x00000001d2d75770

After tapping the play button in the app, we got a hit!

Process 1051 stopped
* thread #44, queue = 'loader_queue', stop reason = breakpoint 2.1
    frame #0: 0x00000001d2d7502c libcommonCrypto.dylib CCCryptorCreateWithMode
libcommonCrypto.dylib CCCryptorCreateWithMode:
->  0x1d2d7502c <+0>:  sub    sp, sp, #0x80             ; =0x80
    0x1d2d75030 <+4>:  stp    x28, x27, [sp, #0x20]
    0x1d2d75034 <+8>:  stp    x26, x25, [sp, #0x30]
    0x1d2d75038 <+12>: stp    x24, x23, [sp, #0x40]
    0x1d2d7503c <+16>: stp    x22, x21, [sp, #0x50]
    0x1d2d75040 <+20>: stp    x20, x19, [sp, #0x60]
    0x1d2d75044 <+24>: stp    x29, x30, [sp, #0x70]
    0x1d2d75048 <+28>: add    x29, sp, #0x70            ; =0x70

As we can see first eight params are passed through the registers x0-x7 and the rest - through the stack:

(lldb) reg r x0 x1 x2 x3 x4 x5 x6 x7
      x0 = 0x0000000000000001
      x1 = 0x0000000000000004
      x2 = 0x0000000000000000
      x3 = 0x0000000000000000
      x4 = 0x0000000000000000
      x5 = 0x0000000281c73bd0
      x6 = 0x0000000000000020
      x7 = 0x0000000000000000
(lldb) mem read $sp -c 24
0x16d91e740: 00 00 00 00 00 00 00 00 00 00 00 00 02 00 00 00  ................
0x16d91e750: 58 a7 a8 6d 01 00 00 00                          X..m....

Following the CommonCryptor API we can see what’s going on here:

x0 = 0x0000000000000001 - op - kCCDecrypt
x1 = 0x0000000000000004 - mode - kCCModeCTR
x2 = 0x0000000000000000 - alg - kCCAlgorithmAES128
x3 = 0x0000000000000000 - padding
x4 = 0x0000000000000000 - iv
x5 = 0x0000000281c73bd0 - key
x6 = 0x0000000000000020 - keyLength
x7 = 0x0000000000000000 - tweak
sp = 0x0000000000000000 - tweakLength
sp+0x08 = 0x0000000000000000 - numRounds
sp+0x0c = 0x0000000000000002 - options - kCCModeOptionCTR_BE
sp+0x10 = 0x000000016da8a758 - cryptorRef

Dumping the key:

(lldb) mem read $x5 -c 0x20
0x281c1cff0: b9bf 9db3 5cae e7f7 b884 ad74 f4a9 2a17  ....\......t..*.
0x281c1d000: e0ac d503 717f 1a92 c3a1 9f25 e6ce 4691  ....q......%..F.

Or if we base64 encode it: ub+ds1yu5/e4hK109KkqF+Cs1QNxfxqSw6GfJebORpE=

Now we can continue to CCCryptorUpdate.

Process 1089 stopped
* thread #26, queue = 'load_local_file', stop reason = breakpoint 2.1
    frame #0: 0x00000001d2d75770 libcommonCrypto.dylib CCCryptorUpdate
libcommonCrypto.dylib CCCryptorUpdate:
->  0x1d2d75770 <+0>:  sub    sp, sp, #0x80             ; =0x80
    0x1d2d75774 <+4>:  stp    x28, x27, [sp, #0x20]
    0x1d2d75778 <+8>:  stp    x26, x25, [sp, #0x30]
    0x1d2d7577c <+12>: stp    x24, x23, [sp, #0x40]
    0x1d2d75780 <+16>: stp    x22, x21, [sp, #0x50]
    0x1d2d75784 <+20>: stp    x20, x19, [sp, #0x60]
    0x1d2d75788 <+24>: stp    x29, x30, [sp, #0x70]
    0x1d2d7578c <+28>: add    x29, sp, #0x70            ; =0x70
(lldb) reg r x0 x1 x2 x3 x4 x5
      x0 = 0x000000015110a000
      x1 = 0x0000000152bd4000
      x2 = 0x0000000000f423ff
      x3 = 0x0000000153b18000
      x4 = 0x0000000000f423ff
      x5 = 0x000000016dcbad08
x0 = 0x000000015110a000 - cryptorRef
x1 = 0x0000000152bd4000 - dataIn
x2 = 0x0000000000f423ff - dataInLength
x3 = 0x0000000153b18000 - dataOut
x4 = 0x0000000000f423ff - dataOutAvailable
x5 = 0x000000016dcbad08 - dataOutMoved

Run the decryption routine and dump decrypted data:

(lldb) finish
Process 1089 stopped
* thread #26, queue = 'load_local_file', stop reason = step out
    frame #0: 0x0000000102ae2dbc TheApp ___lldb_unnamed_symbol5582$$TheApp  + 24
TheApp ___lldb_unnamed_symbol5582$$TheApp:
->  0x102ae2dbc <+24>: cbnz   x21, 0x102ae2dc4          ; <+32>
    0x102ae2dc0 <+28>: str    w0, [x19]
    0x102ae2dc4 <+32>: ldp    x29, x30, [sp, #0x10]
    0x102ae2dc8 <+36>: ldp    x20, x19, [sp], #0x20
    0x102ae2dcc <+40>: ret
TheApp ___lldb_unnamed_symbol5583$$TheApp:    0x102ae2dd0 <+0>: mov    x3, x0
    0x102ae2dd4 <+4>:  ldp    x10, x1, [x20, #0x10]
    0x102ae2dd8 <+8>:  ldp    x8, x9, [x20, #0x20]
Target 0: (TheApp) stopped.
(lldb) mem read 0x0000000153b18000
0x153b18000: 49 44 33 03 00 00 00 09 0e 18 54 41 4c 42 00 00  ID3.......TALB..
0x153b18010: 00 1d 00 00 01 ff fe 48 00 6f 00 6c 00 6f 00 64  .......H.o.l.o.d

From this dump, we can clearly see the .mp3 magic - 49 44 33 - ID3

Now we can dump the file to our Mac and play it!

(lldb) mem read --force --binary --outfile /tmp/1.mp3 0x0000000153b18000 0x0000000153b18000+0x0000000000f423ff
15999999 bytes written to '/tmp/1.mp3'
mbp:~ file /tmp/1.mp3
/tmp/1.mp3: Audio file with ID3 version 2.3.0, contains:MPEG ADTS, layer III, v1, 192 kbps, 48 kHz, JntStereo

That’s it! Knowing the crypto configuration and the key, we can re-create the decryption routine with a couple of lines in Python.

from Crypto.Cipher import AES
from Crypto.Util import Counter

ctr = Counter.new(128, initial_value=0)
cipher = AES.new(key, AES.MODE_CTR, counter=ctr)
plaintext = cipher.decrypt(ciphertext)

Part II

Figuring out the key

While on breakpoint at CCCryptorCreateWithMode we can move up the call stack and land on -[_TtC4TheApp22ResourceLoaderDelegate resourceLoader:shouldWaitForLoadingOfRequestedResource:].

This is a callback, delegate method of AVAssetResourceLoader (part of AVFoundation).

Process 1089 stopped
* thread #57, queue = 'loader_queue', stop reason = breakpoint 3.1
    frame #0: 0x0000000102afd99c TheApp ___lldb_unnamed_symbol6264$$TheApp
TheApp ___lldb_unnamed_symbol6264$$TheApp:
->  0x102afd99c <+0>:  stp    x24, x23, [sp, #-0x40]!
    0x102afd9a0 <+4>:  stp    x22, x21, [sp, #0x10]
    0x102afd9a4 <+8>:  stp    x20, x19, [sp, #0x20]
    0x102afd9a8 <+12>: stp    x29, x30, [sp, #0x30]
    0x102afd9ac <+16>: add    x29, sp, #0x30            ; =0x30
    0x102afd9b0 <+20>: mov    x19, x3
    0x102afd9b4 <+24>: mov    x20, x2
    0x102afd9b8 <+28>: mov    x21, x0
(lldb) po $x0
<TheApp.ResourceLoaderDelegate: 0x282e8a710>

(lldb) po (SEL)$x1
"resourceLoader:shouldWaitForLoadingOfRequestedResource:"

(lldb) po $x2
<AVAssetResourceLoader: 0x280fae440>

By closely inspecting the TheApp.ResourceLoaderDelegate object, we can see that it actually holds the key:

Process 1161 stopped
* thread #48, queue = 'loader_queue', stop reason = breakpoint 7.1
    frame #0: 0x000000010461999c TheApp ___lldb_unnamed_symbol6264$$TheApp
TheApp ___lldb_unnamed_symbol6264$$TheApp:
->  0x10461999c <+0>:  stp    x24, x23, [sp, #-0x40]!
    0x1046199a0 <+4>:  stp    x22, x21, [sp, #0x10]
    0x1046199a4 <+8>:  stp    x20, x19, [sp, #0x20]
    0x1046199a8 <+12>: stp    x29, x30, [sp, #0x30]
    0x1046199ac <+16>: add    x29, sp, #0x30            ; =0x30
    0x1046199b0 <+20>: mov    x19, x3
    0x1046199b4 <+24>: mov    x20, x2
    0x1046199b8 <+28>: mov    x21, x0
(lldb) x/4a '*(void **)($x0+0x20)'
0x282efb570: 0x00000001e32f7a48 type metadata for Foundation.__DataStorage
0x282efb578: 0x0000000000000003
0x282efb580: 0x00000002803f0180
0x282efb588: 0x0000000000000020
(lldb) mem read 0x00000002803f0180
0x2803f0180: b9 bf 9d b3 5c ae e7 f7 b8 84 ad 74 f4 a9 2a 17  ....\......t..*.
0x2803f0190: e0 ac d5 03 71 7f 1a 92 c3 a1 9f 25 e6 ce 46 91  ....q......%..F.

According to the documentation:

You do not create resource loader objects yourself. Instead, you retrieve a resource loader from the resourceLoader property of an AVURLAsset object and use it to assign your custom delegate object.

Let’s break on -[AVURLAsset initWithURL:options:] and to see how AVURLAsset object is being crreated.

This is a decompiled code as seen by Ghidra:

uVar5 = __stubs::Foundation.URL._bridgeToObjectiveC();
uVar7 = __stubs::_swift_getInitializedObjCClass(&_OBJC_CLASS_$_AVURLAsset);

// 1
__stubs::_objc_msgSend(uVar7,"assetWithURL:",uVar5);
uVar7 = __stubs::_objc_retainAutoreleasedReturnValue();

__stubs::_objc_release(uVar5);
(*UNRECOVERED_JUMPTABLE_00)(lVar11,lVar4);

// 2
__stubs::_objc_msgSend(uVar7,"resourceLoader");
uVar5 = __stubs::_objc_retainAutoreleasedReturnValue();

// 3
__stubs::_objc_msgSend
        (uVar5,"setDelegate:queue:",*(undefined8 *)(lVar6 + lVar3),
            *(undefined8 *)(unaff_x20 + local_c0));

Here we can see:

  1. AVURLAsset - being initialized with URL from uVar5 and saved into uVar7
  2. resourceLoader - queried and stored in uVar5
  3. -[AVAssetResourceLoader setDelegate:queue:] called, the delegate object is something located at lVar6 + lVar3 and the queue - something being passed through the register + offset 0xc0

By analyzing where the delegate object came from, we can see the following lines:

uVar7 = FUN_1000fdf3c();
__stubs::_objc_release(lVar6);
__stubs::_objc_release(uVar5);
lVar3 = _TtC4TheApp19AudiobookPlayerItem::resourceLoaderDelegate;
uVar5 = *(undefined8 *)(lVar6 + _TtC4TheApp19AudiobookPlayerItem::resourceLoaderDelegate);
*(undefined8 *)(lVar6 + _TtC4TheApp19AudiobookPlayerItem::resourceLoaderDelegate) = uVar7;

Looks like the delegate object is being returned by FUN_1000fdf3c and saved at address lVar6 + offset lVar3.

When we go into function FUN_1000fdf3c we can see some new references to Objective-C methods:

uVar9 = FUN_1000c0d40(DAT_100493600);
uVar4 = __stubs::_objc_allocWithZone(&_OBJC_CLASS_$_NSData);
__stubs::_swift_bridgeObjectRetain(pcVar8);
uVar9 = __stub_helper::thunk_FUN_10030cc0c(uVar9,pcVar8);
pcVar6 = "initWithBase64EncodedString:options:";
lVar5 = __stubs::_objc_msgSend(uVar4,"initWithBase64EncodedString:options:",uVar9,0);

Let’s break on initWithBase64EncodedString:options: and see what data it operates on:

Process 1161 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
    frame #0: 0x000000018da672c4 Foundation -[NSData(NSData) initWithBase64EncodedString:options:]
Foundation -[NSData(NSData) initWithBase64EncodedString:options:]:
->  0x18da672c4 <+0>:  sub    sp, sp, #0x30             ; =0x30
    0x18da672c8 <+4>:  stp    x20, x19, [sp, #0x10]
    0x18da672cc <+8>:  stp    x29, x30, [sp, #0x20]
    0x18da672d0 <+12>: add    x29, sp, #0x20            ; =0x20
    0x18da672d4 <+16>: cbz    x2, 0x18da672f0           ; <+44>
    0x18da672d8 <+20>: adrp   x8, 293093
    0x18da672dc <+24>: add    x1, x8, #0x23b            ; =0x23b
    0x18da672e0 <+28>: ldp    x29, x30, [sp, #0x20]
(lldb) mem read $x2 -c 0x50
0x282ec0f00: 00 6c 28 e3 01 00 00 00 03 00 00 00 04 00 00 00  .l(.............
0x282ec0f10: 30 00 00 00 00 00 00 00 2c 00 00 00 00 00 00 f0  0.......,.......
0x282ec0f20: 75 62 2b 64 73 31 79 75 35 2f 65 34 68 4b 31 30  ub+ds1yu5/e4hK10
0x282ec0f30: 39 4b 6b 71 46 2b 43 73 31 51 4e 78 66 78 71 53  9KkqF+Cs1QNxfxqS
0x282ec0f40: 77 36 47 66 4a 65 62 4f 52 70 45 3d 00 00 00 00  w6GfJebORpE=....

This is clearly our key! (And it’s length - 0x2c at 0x282ec0f18)

We can follow how uVar9 param is created, and we can see that we get it from FUN_1000c0d40. But this function also takes some data located at DAT_100493600as a param. Also above that, there is a code that initialized this data once from the hardcoded value at address DAT_10047bd08:

DAT_100493600 = __stubs::_swift_initStaticObject(uVar1,&DAT_10047bd08);

Looks like data at DAT_10047bd08 is some kind of higher-level Data-type object because it also contains the length (0x2c) and other metadata, so here is only a part of it that is interesting for us:

mbp:~ dd if=TheApp.decrypted skip=0x47bd58 count=0x40 bs=1 | xxd
64+0 records in
64+0 records out
64 bytes transferred in 0.000108 secs (592573 bytes/sec)
00000000: 2c00 0000 0000 0000 5800 0000 0000 0000  ,.......X.......
00000000: 3412 5b20 165d 1c12 545b 0061 1b2e 4371  4.[ .]..T[.a..Cq
00000010: 493b 2f14 2a4e 2412 4534 1b0b 030a 3023  I;/.*N$.E4....0#
00000020: 0772 220a 2f02 033b 3725 3658 0000 0000  .r"./..;7%6X....

Also, if we stop at FUN_1000c0d40, we can see indeed that the same data that is stored at DAT_10047bd08 is being passed to this function:

Process 1161 resuming
Process 1161 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 4.1
    frame #0: 0x00000001045dcd40 TheApp ___lldb_unnamed_symbol4818$$TheApp
TheApp ___lldb_unnamed_symbol4818$$TheApp:
->  0x1045dcd40 <+0>:  stp    x28, x27, [sp, #-0x60]!
    0x1045dcd44 <+4>:  stp    x26, x25, [sp, #0x10]
    0x1045dcd48 <+8>:  stp    x24, x23, [sp, #0x20]
    0x1045dcd4c <+12>: stp    x22, x21, [sp, #0x30]
    0x1045dcd50 <+16>: stp    x20, x19, [sp, #0x40]
    0x1045dcd54 <+20>: stp    x29, x30, [sp, #0x50]
    0x1045dcd58 <+24>: add    x29, sp, #0x50            ; =0x50
    0x1045dcd5c <+28>: sub    sp, sp, #0x20             ; =0x20
Target 0: (TheApp) stopped.
(lldb) mem read $x0 -c 0x50
0x104997d48: b8 a6 83 42 01 00 00 00 ff ff ff ff 04 00 00 80  ...B............
0x104997d58: 2c 00 00 00 00 00 00 00 58 00 00 00 00 00 00 00  ,.......X.......
0x104997d68  34 12 5b 20 16 5d 1c 12 54 5b 00 61 1b 2e 43 71  4.[ .]..T[.a..Cq
0x104997d78  49 3b 2f 14 2a 4e 24 12 45 34 1b 0b 03 0a 30 23  I;/.*N$.E4....0#
0x104997d88  07 72 22 0a 2f 02 03 3b 37 25 36 58 00 00 00 00  .r"./..;7%6X....

And when we exit this function:

(lldb) finish
Process 1161 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step out
    frame #0: 0x000000010461a148 TheApp ___lldb_unnamed_symbol6273$$TheApp  + 524
TheApp ___lldb_unnamed_symbol6273$$TheApp:
->  0x10461a148 <+524>: mov    x22, x0
    0x10461a14c <+528>: mov    x21, x1
    0x10461a150 <+532>: ldr    x0, [x25, #0x168]
    0x10461a154 <+536>: bl     0x1048263d4               ; symbol stub for: objc_allocWithZone
    0x10461a158 <+540>: mov    x23, x0
    0x10461a15c <+544>: mov    x0, x21
    0x10461a160 <+548>: bl     0x104826848               ; symbol stub for: swift_bridgeObjectRetain
    0x10461a164 <+552>: mov    x0, x22
Target 0: (TheApp) stopped.
(lldb) mem read $x1 -c 0x50
0x282ee9d60: 00 6c 28 e3 01 00 00 00 03 00 00 00 00 00 00 00  .l(.............
0x282ee9d70: 30 00 00 00 00 00 00 00 2c 00 00 00 00 00 00 f0  0.......,.......
0x282ee9d80: 75 62 2b 64 73 31 79 75 35 2f 65 34 68 4b 31 30  ub+ds1yu5/e4hK10
0x282ee9d90: 39 4b 6b 71 46 2b 43 73 31 51 4e 78 66 78 71 53  9KkqF+Cs1QNxfxqS
0x282ee9da0: 77 36 47 66 4a 65 62 4f 52 70 45 3d 00 00 00 00  w6GfJebORpE=....

We can see that we get our key as output. So the function FUN_1000c0d40 is what decrypts hardcoded key. Now let’s find out how exactly it works.

FUN_1000c0d40 - is a large function, but at it’s core - it is a simple while loop that XORs input data with the XOR-key one byte at a time:

                     loc_1000c0ebc:
00000001000c0ebc         add        x26, x26, #0x1
00000001000c0ec0         eor        w8, w28, w19
00000001000c0ec4         str        x23, [x22, #0x10]
00000001000c0ec8         add        x9, x22, x25
00000001000c0ecc         strb       w8, [x9, #0x20]
00000001000c0ed0         cmp        x20, x26
00000001000c0ed4         b.ne       loc_1000c0e74
00000001000c0ed8         b          loc_1000c0f04
Process 1161 resuming
Process 1161 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 5.1
    frame #0: 0x00000001045dcec0 TheApp ___lldb_unnamed_symbol4818$$TheApp  + 384
TheApp ___lldb_unnamed_symbol4818$$TheApp:
->  0x1045dcec0 <+384>: eor    w8, w28, w19
    0x1045dcec4 <+388>: str    x23, [x22, #0x10]
    0x1045dcec8 <+392>: add    x9, x22, x25
    0x1045dcecc <+396>: strb   w8, [x9, #0x20]
    0x1045dced0 <+400>: cmp    x20, x26
    0x1045dced4 <+404>: b.ne   0x1045dce74               ; <+308>
    0x1045dced8 <+408>: b      0x1045dcf04               ; <+452>
    0x1045dcedc <+412>: cmp    x8, #0x0                  ; =0x0
Target 0: (TheApp) stopped.
(lldb) reg r w28 w19 x26
     w28 = 0x00000041
     w19 = 0x00000034
     x26 = 0x0000000000000001

We can deduce the XOR-key by running this loop for a couple of iterations. And surprise - the XOR-key is equal to AppDelegateUser. I don’t know why this particular string was chosen. Maybe, so there is no cleartext XOR-key hardcoded in the app, and instead, it’s retrieved dynamically from different objects in memory, or maybe just to confuse researchers 🤔.

But we can easily verify that this XOR-key is correct by manually XORing hardcoded encryption key with this XOR-key using something like CyberChef:

CyberChef

We can clearly see that this is the same key we got by inspecting the call to CCCryptorCreateWithMode in Part I:

CyberChef

That’s basically it!

PS

After extracting end encrypting the files, I’ve used a tool called m4b-tool to convert multiple .mp3 files into a single .m4b audiobook that Garmin (and Apple Books) supports.

  1. CommonCryptor.h
  2. m4b-tool - cmd utility to work with audiobooks