Extracting and decrypting audiobooks 📚 from an iOS app
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.
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:
AVURLAsset
- being initialized with URL fromuVar5
and saved intouVar7
resourceLoader
- queried and stored inuVar5
-[AVAssetResourceLoader setDelegate:queue:]
called, the delegate object is something located atlVar6 + lVar3
and the queue - something being passed through the register + offset0xc0
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_100493600
as 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:
We can clearly see that this is the same key we got by inspecting the call to CCCryptorCreateWithMode
in Part I:
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.