Introduction to reverse engineering of AppStore apps

Kamil Sławiński

November 14, 2022


In this article, I would like to give a glimpse of how the majority of AppStore apps can be analyzed. To proceed we do assume having access to a physical iPhone that we can jailbreak. At the time of writing, this would exclude iOS 16 and beyond. As a rule of thumb the more modern minimum iOS you are supporting the harder it will be to have the right toolset readily available. The best source of current jailbreak status is The iPhone wiki. Any app supporting up to iOS 15.x can be our target. Unfortunately, there is no jailbreak for tvOS.

Executables in iOS

Let’s get the basics out of the way: what are the major limitations of non-jailbroken devices to accomplish this?

On iOS there we can identify 3 kinds of executables running:

  1. apps
  2. standalone executables - e.g. debugserver - the receiving end of lldb running on the device which Xcode uses
  3. daemons - permanently running core operating system services for stuff like Bluetooth, CoreTelephony etc

The apps that come from AppStore are signed by Apple, the one created by XCode are signed by our Development/Adhoc/Enterprise signing certificate. When the app launches:

  1. its executable binary is checked if it is a valid Mach-o file
  2. the signature is verified by kernel
  3. the executable part is decrypted if it is an AppStore app

The last step implies that ipas (i.e. the apps themselves in distributable form) downloaded from AppStore are useless on their own for analysis.

Our plan for reverse engineering

We want to accomplish the following:

  1. dynamic analysis i.e. debugging the app while it is running, we can use lldb for that (no source code though)
  2. static analysis of the decrypted ipa
  3. inspection of network traffic using tools like Charles Proxy or Proxyman for http(s), Wireshark for low-level TCP/UDP communication

Notice that even though our own apps installed through XCode can be debugged, we cannot attach to apps coming from AppStore.

That is where the jailbreak comes into play. It patches the kernel to bypass the signature check (or technically make the check always succeed). Prior to iOS 15 that was also accompanied by granting write permissions to the iOS root filesystem. In modern iOS the patched kernel state will not survive a reboot, still, the legit unpatched kernel will boot normally. The jailbreak must be reapplied after every reboot. Once jailbroken the device is by definition insecure, I recommend using it only for research on a WIFI network over which have full control.

With the ability to run arbitrary not sandboxed software and non-AppStore way of installing we are ready to start preparing the toolset.

The jailbreak environment setup

In my setup, I did use checkra1n on iPhone 7 iOS 12 4.1 and a mac. checkra1n does require putting the device into DFU mode (steps differ depending on the particular iPhone model). Using checkra1n GUI interface was a pain to get the timing right, but it is possible to run it in the command line when the device has already been put into DFU.

In /Applications/checkra1n.app/Contents/MacOS/

checkra1n -c 

The command above will work after the device has been into DFU prior to running it, which I believe is the most reliable way.

After jailbreaking finishes we can run Cydia - the jailbreak “appstore” and install the following tools:

  1. OpenSSH - daemon for remote ssh shell access through WIFI
  2. frida - reverse engineering toolset, we will only use its decrypted ipa dumping feature
  3. lldb - the very same debugger we use with XCode, but in our case it will run on the device itself

Once you install OpenSSH, from your Mac (or any ssh capable terminal for that matter) you should be able to:

ssh root@yourIphone.IP.AdressOnThe.WIFI

the default password is always “alpine”. Fun fact - it has been the default root password since iOS 1.0, so pretty unlikely to change but who knows :)

If you succeed you should see something like this:

Kamils-iPhone7:~ root#

If you have remote ssh access to the device you can proceed setting up frida. On your mac:

brew install iproxy
git clone https://github.com/AloneMonkey/frida-ios-dump.git  
cd frida-ios-dump
sudo python3 -m pip install -r requirements.txt --upgrade 
python3 -m pip install frida-tools
python3 -m pip install paramiko
python3 -m pip install scp
python3 -m pip install tqdm

On your device: install frida through Cydia

Now every time you want to dump an app you would:

On device: Run the app by pressing its icon

On mac: 1st terminal session:

iproxy 2222 22

2nd terminal session:

cd ~/frida-ios-dump/
python3 dump.py -l 
python3 dump.py bundle.id

If all goes well you should have your unencrypted ipa. You can now plug in the main executable to Hopper the disassembler. It is commercial software, still, the demo is functional for 30 minutes which is pretty gracious. The license costs about $100. If you are intimidated by assembly it offers a pseudo-C view of code too which may also come in handy.

We are left with installing lldb which will be our main inspection tool. Let’s do that through Cydia. If all goes well you should be able to run lldb through ssh on the device.

Debugging the AppStore app

Run an app you would like to analyze, when you are ready use:

ps -A

to lookup the executable exact name (it may differ from the icon app name)

Now you can attach to it:

lldb -n AppExecutableName

If you would like to inspect the launch kill the app and prepare upfront for its launch do this instead:

lldb -n AppExecutableName --wait-for

The exact execution state of the app that we stop on will differ, because lldb has a bit of delay.

Process 536 stopped
* thread #1, stop reason = signal SIGSTOP
    frame #0: 0x0000000104506e60 dyld`mremap_encrypted + 8
dyld`mremap_encrypted:
->  0x104506e60 <+8>:  b.lo   0x104506e78               ; <+32>
    0x104506e64 <+12>: stp    x29, x30, [sp, #-0x10]!
    0x104506e68 <+16>: mov    x29, sp
    0x104506e6c <+20>: bl     0x104505850               ; cerror_nocancel

dyld`mremap_encrypted is a hint we are still in the loading phase during decryption. The app startup times may improve on subsequent launches which may actually hinder our analysis. We may already be past jailbreak detection when lldb attaches too late possibly making us stuck on some “Sorry. You are jailbroken” screen. One way to improve that is to remove the app and reinstall it from AppStore. The first launch after install will always take longer allowing lldb to attach earlier in the app life cycle.
Here are some basic commands to explore what is actually happening:

  1. inspecting the stack trace
    bt
    * thread #1, stop reason = signal SIGSTOP
      * frame #0: 0x0000000104506e60 dyld`mremap_encrypted + 8
     frame #1: 0x00000001044deb48 dyld`ImageLoaderMachOCompressed::registerEncryption(encryption_info_command const*, ImageLoader::LinkContext const&) + 192
     frame #2: 0x00000001044de8dc dyld`ImageLoaderMachOCompressed::instantiateFromFile(char const*, int, unsigned char const*, unsigned long, unsigned long long, unsigned long long, stat const&, unsigned int, unsigned int, linkedit_data_command const*, encryption_info_command const*, ImageLoader::LinkContext const&) + 296
     frame #3: 0x00000001044da2e8 dyld`ImageLoaderMachO::instantiateFromFile(char const*, int, unsigned char const*, unsigned long, unsigned long long, unsigned long long, stat const&, ImageLoader::LinkContext const&) + 164
     frame #4: 0x00000001044c7ca0 dyld`dyld::loadPhase6(int, stat const&, char const*, dyld::LoadContext const&) + 508
     frame #5: 0x00000001044cde4c dyld`dyld::loadPhase5(char const*, char const*, dyld::LoadContext const&, unsigned int&, std::__1::vector<char const*, std::__1::allocator<char const*> >*) + 1080
     frame #6: 0x00000001044cd990 dyld`dyld::loadPhase4(char const*, char const*, dyld::LoadContext const&, unsigned int&, std::__1::vector<char const*, std::__1::allocator<char const*> >*) + 224
     frame #7: 0x00000001044cd2ac dyld`dyld::loadPhase3(char const*, char const*, dyld::LoadContext const&, unsigned int&, std::__1::vector<char const*, std::__1::allocator<char const*> >*) + 340
     frame #8: 0x00000001044c7988 dyld`dyld::loadPhase0(char const*, char const*, dyld::LoadContext const&, unsigned int&, std::__1::vector<char const*, std::__1::allocator<char const*> >*) + 176
     frame #9: 0x00000001044c76a4 dyld`dyld::load(char const*, dyld::LoadContext const&, unsigned int&) + 192
     frame #10: 0x00000001044ce398 dyld`dyld::libraryLocator(char const*, bool, char const*, ImageLoader::RPathChain const*, bool, unsigned int&) + 56
     frame #11: 0x00000001044d78b0 dyld`ImageLoader::recursiveLoadLibraries(ImageLoader::LinkContext const&, bool, ImageLoader::RPathChain const&, char const*) + 588
     frame #12: 0x00000001044d6aa4 dyld`ImageLoader::link(ImageLoader::LinkContext const&, bool, bool, bool, ImageLoader::RPathChain const&, char const*) + 124
     frame #13: 0x00000001044c9a84 dyld`dyld::link(ImageLoader*, bool, bool, ImageLoader::RPathChain const&, unsigned int) + 228
     frame #14: 0x00000001044cafa0 dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 3536
     frame #15: 0x00000001044c5044 dyld`_dyld_start + 68
    
  2. Examining the ARM registers
    re r
    General Purpose Registers:
         x0 = 0x0000000000000000
         x1 = 0x0000000000000000
         x2 = 0x0000000000000001
         x3 = 0x000000000100000c
         x4 = 0x0000000000000000
         x5 = 0x000000000000c000
         x6 = 0x000000010451f5e0  dyld::gLinkContext
         x7 = 0x0000000000000000
         x8 = 0x0000000000000000
         x9 = 0x0000000105b10000
        x10 = 0x000000016f00e540
        x11 = 0x0000000000000010
        x12 = 0x000000000000c8dc
        x13 = 0x0000000000000005
        x14 = 0x000000016f00e070
        x15 = 0x000000016f00e058
        x16 = 0x00000000000001e9
        x17 = 0x0000000000000010
        x18 = 0x0000000000000000
        x19 = 0x0000000104524558  dyld`initialPoolContent + 17176
        x20 = 0x000000010451f5e0  dyld::gLinkContext
        x21 = 0x0000000000000001
        x22 = 0x0000000000004000
        x23 = 0x0000000105b14000
        x24 = 0x000000000100000c
        x25 = 0x0000000000000000
        x26 = 0x0000000000000000
        x27 = 0x000000016f00d8f8
        x28 = 0x000000000000000e
         fp = 0x000000016f00d300
         lr = 0x00000001044deb48  dyld`ImageLoaderMachOCompressed::registerEncryption(encryption_info_command const*, ImageLoader::LinkContext const&) + 192
         sp = 0x000000016f00d2a0
         pc = 0x0000000104506e60  dyld`mremap_encrypted + 8
       cpsr = 0x40000000
    

    If we are lucky lldb will throw in some hints about what they actually mean.

This may sound exciting, however, the dyld startup phase, for the most part, will not be that useful as we want to focus on the code of the app itself. It would be great to find the actual entry point.

Let’s use Hopper to analyze the main executable from ipa we dumped using frida. Looking up plain text search for EntryPoint yields:

                     EntryPoint:
0000000102371f34         stp        x22, x21, [sp, #-0x30]!
0000000102371f38         stp        x20, x19, [sp, #0x10]
0000000102371f3c         stp        fp, lr, [sp, #0x20]
0000000102371f40         add        fp, sp, #0x20
0000000102371f44         mov        x19, x1
0000000102371f48         mov        x20, x0
0000000102371f4c         bl         imp___stubs__objc_autoreleasePoolPush       ; objc_autoreleasePoolPush
0000000102371f50         mov        x21, x0
0000000102371f54         bl         imp___stubs__CFAbsoluteTimeGetCurrent       ; CFAbsoluteTimeGetCurrent
0000000102371f58         adrp       x8, #0x1030fc000
0000000102371f5c         nop
0000000102371f60         str        d0, [x8, #0x900]                            ; double_1030fc900
0000000102371f64         adrp       x8, #0x102e73000
0000000102371f68         ldr        x0, [x8, #0xbf8]                            ; argument "instance" for method imp___stubs__objc_msgSend, objc_cls_ref_AppExecutableNameAppDelegate,__objc_class_SomeAppDelegate_class
0000000102371f6c         adrp       x8, #0x102e2b000
0000000102371f70         ldr        x1, [x8, #0x340]                            ; argument "selector" for method imp___stubs__objc_msgSend, "class",@selector(class)
0000000102371f74         bl         imp___stubs__objc_msgSend                   ; objc_msgSend
0000000102371f78         bl         imp___stubs__NSStringFromClass              ; NSStringFromClass
0000000102371f7c         mov        fp, fp
0000000102371f80         bl         imp___stubs__objc_retainAutoreleasedReturnValue ; objc_retainAutoreleasedReturnValue
0000000102371f84         mov        x22, x0
0000000102371f88         mov        x0, x20
0000000102371f8c         mov        x1, x19
0000000102371f90         mov        x2, #0x0
0000000102371f94         mov        x3, x22
0000000102371f98         bl         imp___stubs__UIApplicationMain              ; UIApplicationMain
0000000102371f9c         mov        x19, x0
0000000102371fa0         mov        x0, x22                                     ; argument "instance" for method imp___stubs__objc_release
0000000102371fa4         bl         imp___stubs__objc_release                   ; objc_release
0000000102371fa8         mov        x0, x21                                     ; argument "pool" for method imp___stubs__objc_autoreleasePoolPop
0000000102371fac         bl         imp___stubs__objc_autoreleasePoolPop        ; objc_autoreleasePoolPop
0000000102371fb0         mov        x0, x19
0000000102371fb4         ldp        fp, lr, [sp, #0x20]
0000000102371fb8         ldp        x20, x19, [sp, #0x10]
0000000102371fbc         ldp        x22, x21, [sp], #0x30
0000000102371fc0         ret

Notice the use of UIApplicationMain. That indeed looks like the main that we expect to be called (in modern Swift it is usually autogenerated by @UIApplicationMain). There is one more challenge we need to cope with, despite Hopper showing the start of our base address of the executable at 0x100000000 we also need account for ASLR (Address space layout randomization). Everytime our executable is loaded an additional random shift is added to the base address. Thankfully lldb itself comes to the rescue.

b -a 0000000102371f34 -s AppExecutableName
Breakpoint 1: where = AppExecutableName`___lldb_unnamed_symbol139640$$AppExecutableName, address = 0x0000000102635f34

Let’s continue in lldb with con and immediately hit a bp.

    frame #0: 0x0000000102635f34 AppExecutableName`___lldb_unnamed_symbol139640$$AppExecutableName
AppExecutableName`___lldb_unnamed_symbol139640$$AppExecutableName:
->  0x102635f34 <+0>:  stp    x22, x21, [sp, #-0x30]!
    0x102635f38 <+4>:  stp    x20, x19, [sp, #0x10]
    0x102635f3c <+8>:  stp    x29, x30, [sp, #0x20]
    0x102635f40 <+12>: add    x29, sp, #0x20            ; =0x20 

If you compare the assembly it is exactly the same code Hopper was showing. To be sure we can show the whole function disassembly in lldb with di. Just for fun, we can also calculate the ASLR shift:

p/x 0x0000000102635f34-0x000000102371f34
0x00000000002c4000

lldb is kind enough to provide us a symbol-like alias ___lldb_unnamed_symbol139640$$AppExecutableName. We can set a bp on it like this:

b ___lldb_unnamed_symbol139640$$AppExecutableName

to revisit some function (presumably on subsequent launch). This might come in handy, especially when making notes about what some particular function/method actually does.

Those lldb generated “symbols” will stay consistent as long as the app is not updated by AppStore. Disabling apps’ auto-update on your jailbroken device would be a sensible precaution.

Let’s inspect initial setup of UIViewControllers. b pushViewController:animated: Notice I am using obj-c selector names as in Apple doc

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.2
    frame #0: 0x00000001d9671e18 UIKitCore`-[UINavigationController pushViewController:animated:]
UIKitCore`-[UINavigationController pushViewController:animated:]:
->  0x1d9671e18 <+0>:  sub    sp, sp, #0x140            ; =0x140 
    0x1d9671e1c <+4>:  stp    d9, d8, [sp, #0xe0]
    0x1d9671e20 <+8>:  stp    x26, x25, [sp, #0xf0]
    0x1d9671e24 <+12>: stp    x24, x23, [sp, #0x100]
 bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.2
  * frame #0: 0x00000001d9671e18 UIKitCore`-[UINavigationController pushViewController:animated:]
    frame #1: 0x000000010044dee4 AppExecutableName`___lldb_unnamed_symbol7153$$AppExecutableName + 732
    frame #2: 0x00000001d9654ee4 UIKitCore`-[UINavigationController initWithRootViewController:] + 140
    frame #3: 0x0000000101bd87a4 AppExecutableName`___lldb_unnamed_symbol99773$$AppExecutableName + 56
    frame #4: 0x0000000101aa2cd8 AppExecutableName`___lldb_unnamed_symbol94245$$AppExecutableName + 1992
    frame #5: 0x0000000101aa142c AppExecutableName`___lldb_unnamed_symbol94228$$AppExecutableName + 344
    frame #6: 0x0000000101aa09d4 AppExecutableName`___lldb_unnamed_symbol94219$$AppExecutableName + 84
    frame #7: 0x00000001d9638780 UIKitCore`-[UITabBarController initWithNibName:bundle:] + 252
    frame #8: 0x0000000102629af8 AppExecutableName`___lldb_unnamed_symbol139507$$AppExecutableName + 2636
    frame #9: 0x00000001d9c9f0f0 UIKitCore`-[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:] + 412
    frame #10: 0x00000001d9ca0854 UIKitCore`-[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] + 3352
    frame #11: 0x00000001d9ca5fe0 UIKitCore`-[UIApplication _runWithMainScene:transitionContext:completion:] + 1540
    frame #12: 0x00000001d95692a4 UIKitCore`__111-[__UICanvasLifecycleMonitor_Compatability _scheduleFirstCommitForScene:transition:firstActivation:completion:]_block_invoke + 776
    frame #13: 0x00000001d957183c UIKitCore`+[_UICanvas _enqueuePostSettingUpdateTransactionBlock:] + 160
    frame #14: 0x00000001d9568f28 UIKitCore`-[__UICanvasLifecycleMonitor_Compatability _scheduleFirstCommitForScene:transition:firstActivation:completion:] + 236
    frame #15: 0x00000001d9569818 UIKitCore`-[__UICanvasLifecycleMonitor_Compatability activateEventsOnly:withContext:completion:] + 1064
    frame #16: 0x00000001d9567b64 UIKitCore`__82-[_UIApplicationCanvas _transitionLifecycleStateWithTransitionContext:completion:]_block_invoke + 744
    frame #17: 0x00000001d956782c UIKitCore`-[_UIApplicationCanvas _transitionLifecycleStateWithTransitionContext:completion:] + 428
    frame #18: 0x00000001d956c36c UIKitCore`__125-[_UICanvasLifecycleSettingsDiffAction performActionsForCanvas:withUpdatedScene:settingsDiff:fromSettings:transitionContext:]_block_invoke + 220
    frame #19: 0x00000001d956d150 UIKitCore`_performActionsWithDelayForTransitionContext + 112
    frame #20: 0x00000001d956c224 UIKitCore`-[_UICanvasLifecycleSettingsDiffAction performActionsForCanvas:withUpdatedScene:settingsDiff:fromSettings:transitionContext:] + 244
    frame #21: 0x00000001d9570f24 UIKitCore`-[_UICanvas scene:didUpdateWithDiff:transitionContext:completion:] + 360
    frame #22: 0x00000001d9ca45e8 UIKitCore`-[UIApplication workspace:didCreateScene:withTransitionContext:completion:] + 540
    frame #23: 0x00000001d98a0e04 UIKitCore`-[UIApplicationSceneClientAgent scene:didInitializeWithEvent:completion:] + 360
    frame #24: 0x00000001afbc69fc FrontBoardServices`-[FBSSceneImpl _didCreateWithTransitionContext:completion:] + 440
    frame #25: 0x00000001afbd040c FrontBoardServices`__56-[FBSWorkspace client:handleCreateScene:withCompletion:]_block_invoke_2 + 256
    frame #26: 0x00000001afbcfc14 FrontBoardServices`__40-[FBSWorkspace _performDelegateCallOut:]_block_invoke + 64
    frame #27: 0x00000001acc897d4 libdispatch.dylib`_dispatch_client_callout + 16
    frame #28: 0x00000001acc2e5dc libdispatch.dylib`_dispatch_block_invoke_direct$VARIANT$mp + 224
    frame #29: 0x00000001afc01040 FrontBoardServices`__FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 40
    frame #30: 0x00000001afc00cdc FrontBoardServices`-[FBSSerialQueue _performNext] + 408
    frame #31: 0x00000001afc01294 FrontBoardServices`-[FBSSerialQueue _performNextFromRunLoopSource] + 52
    frame #32: 0x00000001ad1dc728 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
    frame #33: 0x00000001ad1dc6a8 CoreFoundation`__CFRunLoopDoSource0 + 88
    frame #34: 0x00000001ad1dbf90 CoreFoundation`__CFRunLoopDoSources0 + 176
    frame #35: 0x00000001ad1d6ecc CoreFoundation`__CFRunLoopRun + 1004
    frame #36: 0x00000001ad1d67c0 CoreFoundation`CFRunLoopRunSpecific + 436
    frame #37: 0x00000001af3d779c GraphicsServices`GSEventRunModal + 104
    frame #38: 0x00000001d9ca7c38 UIKitCore`UIApplicationMain + 212
    frame #39: 0x0000000102635f9c AppExecutableName`___lldb_unnamed_symbol139640$$AppExecutableName + 104
    frame #40: 0x00000001acc9a8e0 libdyld.dylib`start + 4

If you look at the very bottom frame #39: 0x0000000102635f9c AppExecutableName`___lldb_unnamed_symbol139640$$AppExecutableName happens to be the EntryPoint we found in the previous step. We are also seeing a mix of Apple API (those are symbolicated) and invocations of methods and functions of the app we are analyzing:

  * frame #0: 0x00000001d9671e18 UIKitCore`-[UINavigationController pushViewController:animated:]
    frame #1: 0x000000010044dee4 AppExecutableName`___lldb_unnamed_symbol7153$$AppExecutableName + 732
    frame #2: 0x00000001d9654ee4 UIKitCore`-[UINavigationController initWithRootViewController:] + 140
    frame #3: 0x0000000101bd87a4 AppExecutableName`___lldb_unnamed_symbol99773$$AppExecutableName + 56
    frame #4: 0x0000000101aa2cd8 AppExecutableName`___lldb_unnamed_symbol94245$$AppExecutableName + 1992
    frame #5: 0x0000000101aa142c AppExecutableName`___lldb_unnamed_symbol94228$$AppExecutableName + 344
    frame #6: 0x0000000101aa09d4 AppExecutableName`___lldb_unnamed_symbol94219$$AppExecutableName + 84
    frame #7: 0x00000001d9638780 UIKitCore`-[UITabBarController initWithNibName:bundle:] + 252
    ...

Now we would like to know what UIViewController is being pushed. Every Objective-C method on C level contains two hidden arguments to conform to objc_msgSend(id self, SEL op, ...). lldb provides very handy aliases for arguments $arg1, $arg2, $arg3, $arg4 etc. What those really are are synonyms for CPU registers used for passing arguments on a given CPU architecture. On ARM64 those would be x0,x1,x2… respectively. Let’s double check that:

re r
General Purpose Registers:
        x0 = 0x0000000106050600
        x1 = 0x000000010267bc0a  "fd_pushViewController:animated:"
        x2 = 0x000000010591ca90
        x3 = 0x0000000000000000
        x4 = 0x000000016fb39280
        ...

Remark: $arg1,$arg2,... apply only to Integer/Pointer arguments but not to floats, the nuances of ABI are way beyond this blog post. Let’s inspect them:

po $arg1
<YSNavigationViewController: 0x106050600>

po (SEL)$arg2
"fd_pushViewController:animated:"

po $arg3
<HCMainViewController: 0x10591ca90>

p $arg4
(unsigned long) $4 = 0

There is a lot going on here so I owe you a thorough explanation. Clearly, the UINavigationController is subclassed with YSNavigationViewController implementation. The 2nd argument is interesting where we see a mismatch between the selector in stacktrace and the name of the selector being sent. My educated guess being that is swizzling accomplished by method_exchangeImplementations. Let’s focus on $arg3 and $arg4: <HCMainViewController: 0x10591ca90> is our pushed view controller and $arg4 being false indicates that happened without an animation.

Wrapping it up

This article has to come up to a stop at some point. I would like to encourage you to explore even without a jailbroken device. You can use techniques described here to investigate how Apple private APIs work and any library for that matter if you do not have its source code. As a side effect, your security wariness may only improve and that cannot be a bad thing.