iOS: Bypassing Jailbreak Detection
In this blog series, we'll delve into how iOS applications detect the presence of a jailbroken device, and explore various methods to bypass these detection mechanisms using both static and dynamic analysis. In this initial post, we'll start with the fundamental concepts and techniques needed to bypass these detections.
Tools
https://github.com/prateek147/DVIA-v2
https://github.com/AloneMonkey/frida-ios-dump
https://frida.re/
https://hex-rays.com/ida-pro/
First Steps
Before we jump into bypassing the jailbreak detection, there are a few preliminary steps that need to be taken. The first is to utilize
frida-ps
to confirm that the application we want to analyze is currently running on the device. It's important to keep in mind that in order to use any Frida tool, the mobile device must either be connected via USB or have OpenSSH enabled, so that we can communicate via SSH. Additionally, it's necessary to ensure that the
frida-server
is running for proper functionality.
We can use the following command
frida-ps -U
to get a list of running processes on the device and their associated PID's. In the output, we can see that DVIA-v2 is indeed running on the device with the PID 3293.
650 CommCenterMobileHelper
746 ContainerMetadataExtractor
753 ContextService
1720 CoreSpotlightService
2784 Cydia
1701 DPSubmissionService
3293 DVIA-v2
1597 EscrowSecurityAlert
1441 GeneralMapsWidget
1719 Health
In general, Mach-O binaries that run on iOS devices can be quite large and contain a multitude of functions, data, strings, and more. As a result, it can be challenging to analyze them through static analysis alone. That's why starting with dynamic analysis, such as function tracing, can often be a more efficient approach. By using function tracing, we can more easily identify and track the various functions and operations within the binary as they're executed.
To conduct function tracing on the DVIA-v2 application and track any changes made within the app on the device, we can utilize
frida-trace
. This dynamic analysis tool allows us to trace various functions and operations as they occur in real time. To begin function tracing, we can run the following command:
frida-trace -U -i "jailbreak" 3293
.
Instrumenting...
_T07DVIA_v232JailbreakDetectionViewControllerC20jailbreakTest2TappedyypF: Auto-generated handler at "/home/redacted/__handlers__/DVIA_v2/_T07DVIA_v232JailbreakDetectionV_6aa20fff.js"
_T07DVIA_v232JailbreakDetectionViewControllerC20jailbreakTest4TappedyypF: Auto-generated handler at "/home/redacted/__handlers__/DVIA_v2/_T07DVIA_v232JailbreakDetectionV_8217c5bc.js"
_T07DVIA_v232JailbreakDetectionViewControllerC20jailbreakTest1TappedyypF: Auto-generated handler at "/home/redacted/__handlers__/DVIA_v2/_T07DVIA_v232JailbreakDetectionV_f34069fe.js"
_T07DVIA_v232JailbreakDetectionViewControllerC20jailbreakTest3TappedyypF: Auto-generated handler at "/home/redacted/__handlers__/DVIA_v2/_T07DVIA_v232JailbreakDetectionV_ab2cd03f.js"
_T07DVIA_v232JailbreakDetectionViewControllerC20jailbreakTest5TappedyypF: Auto-generated handler at "/home/redacted/__handlers__/DVIA_v2/_T07DVIA_v232JailbreakDetectionV_43991a7c.js"
_T07DVIA_v240ApplicationPatchingDetailsViewControllerC19jailbreakTestTappedyypF: Auto-generated handler at "/home/redacted/__handlers__/DVIA_v2/_T07DVIA_v240ApplicationPatching_bdca7b85.js"
_T07DVIA_v232JailbreakDetectionViewControllerC14jailbreakTest3yyF: Auto-generated handler at "/home/redacted/__handlers__/DVIA_v2/_T07DVIA_v232JailbreakDetectionV_f03b6d58.js"
Started tracing 7 functions. Press Ctrl+C to stop.
/* TID 0x303 */
10819 ms _T07DVIA_v232JailbreakDetectionViewControllerC20jailbreakTest1TappedyypF()
Clicking on the first jailbreak detection option in the DVIA-v2 application triggers the function
_T07DVIA_v232JailbreakDetectionViewControllerC20jailbreakTest1TappedyypF()
.
Dumping IPAs (iOS Applications)
Apple uses a Digital Rights Management (DRM) system called FairPlay to encrypt IPA (iOS application) files. FairPlay is designed to prevent unauthorized access and distribution of iOS apps by encrypting the binary executable, as well as any other supporting files or resources that make up the app package.
However, in some cases, researchers may not have direct access to the application files and need to dump them. As IPA files are encrypted, the application files must also be dynamically dumped from memory to be accessed for static analysis.
One way to dynamically dump the application from memory is to use Frida iOS Dump. By running the command
python3 dump.py DVIA-v2
, we can dump the DVIA-v2 application from memory.
libswiftObjectiveC.dylib.fid: 100%|██████████████████████████████████████████████████████████████████████████| 76.9k/76.9k [00:00<00:00, 470kB/s]
start dump /Applications/DVIA-v2.app/Frameworks/libswiftQuartzCore.dylib
libswiftQuartzCore.dylib.fid: 100%|██████████████████████████████████████████████████████████████████████████| 63.3k/63.3k [00:00<00:00, 378kB/s]
start dump /Applications/DVIA-v2.app/Frameworks/libswiftSwiftOnoneSupport.dylib
libswiftSwiftOnoneSupport.dylib.fid: 100%|████████████████████████████████████████████████████████████████████| 489k/489k [00:00<00:00, 2.80MB/s]
start dump /Applications/DVIA-v2.app/Frameworks/libswiftUIKit.dylib
libswiftUIKit.dylib.fid: 100%|█████████████████████████████████████████████████████████████████████████████████| 113k/113k [00:00<00:00, 674kB/s]
start dump /Applications/DVIA-v2.app/Frameworks/libswiftos.dylib
libswiftos.dylib.fid: 100%|██████████████████████████████████████████████████████████████████████████████████| 67.1k/67.1k [00:00<00:00, 410kB/s]
start dump /Applications/DVIA-v2.app/Frameworks/libswiftsimd.dylib
libswiftsimd.dylib.fid: 100%|█████████████████████████████████████████████████████████████████████████████████| 417k/417k [00:00<00:00, 1.90MB/s]
start dump /Applications/DVIA-v2.app/libswiftRemoteMirror.dylib
libswiftRemoteMirror.dylib.fid: 100%|█████████████████████████████████████████████████████████████████████████| 383k/383k [00:00<00:00, 2.75MB/s]
[email protected]: 63.6MB [00:05, 13.0MB/s]
0.00B [00:00, ?B/s]
Generating "DVIA-v2.ipa"
We now have access to extract and analyze the IPA for DVIA-v2.
Static Analysis
With a tool like IDA Pro, we can perform static analysis on the Mach-O binary that contains the jailbreak detection code. By having contextual information, such as which function to examine, we can avoid the tedious task of manually searching through the binary for the relevant code.
Assembly for:
_T07DVIA_v232JailbreakDetectionViewControllerC20jailbreakTest1TappedyypF()
SUB SP, SP, #0x60
STP X20, X19, [SP,#0x50+var_10]
STP X29, X30, [SP,#0x50+var_s0]
ADD X29, SP, #0x50
STUR X0, [X29,#var_18]
STUR X20, [X29,#var_20]
STR X20, [SP,#0x50+var_28]
STR X0, [SP,#0x50+var_30]
BL __T07DVIA_v213DVIAUtilitiesCMa
ADRP X20, #_swift_isaMask_ptr@PAGE
LDR X20, [X20,#_swift_isaMask_ptr@PAGEOFF]
LDR X30, [X0,#0x58]
LDR X8, [SP,#0x50+var_28]
LDR X9, [X8]
LDR X20, [X20]
AND X9, X9, X20
LDR X9, [X9,#0x80]
MOV X20, X8
STR X0, [SP,#0x50+var_38]
STR X30, [SP,#0x50+var_40]
BLR X9
LDR X8, [SP,#0x50+var_28]
STR W0, [SP,#0x50+var_44]
MOV X0, X8 ; id
BL _objc_retain
LDR X9, [SP,#0x50+var_40]
LDR W10, [SP,#0x50+var_44]
AND W11, W10, #1
STR X0, [SP,#0x50+var_50]
MOV X0, X11
MOV X1, X8
LDR X20, [SP,#0x50+var_38]
BLR X9 LDR X0, [SP,#0x50+var_30]
BL ___swift_destroy_boxed_opaque_existential_0
LDP X29, X30, [SP,#0x50+var_s0]
LDP X20, X19, [SP,#0x50+var_10]
ADD SP, SP, #0x60
RET
One important aspect to consider is the invocation of
BL __T07DVIA_v213DVIAUtilitiesCMa
. This call corresponds to the metadata accessor for the
DVIAUtilities
class, which is stored in a lazy cache and holds data about the device, such as whether it is jailbroken or not.
When calling
__T07DVIA_v213DVIAUtilitiesCMa
, the specific metadata is stored on the stack and later loaded into the x8 register using
LDR X8, [SP,#0x50+var_28]
. Then, the x0 register is written to be cleared, and the contents of x8 are loaded into x0.
LDR X8, [SP,#0x50+var_28]
STR W0, [SP,#0x50+var_44]
MOV X0, X8 ; id
Dynamic Analysis
To gain more information and context for our static analysis, we can use Frida to hook into the process and inject our own JavaScript code. By doing so, we can read specific register values and extract more information to inform our analysis.
Since I am not totally proficient in JavaScript, I would like to express my gratitude to
Phil Keeble for sharing his code samples in his blog post on the same topic.
We can use the following JavaScript code to view the data in the x0 register at the time of execution to see what kind of check we are dealing with. Not only will this help us with making further dynamic bypasses, but we can also use this for writing a static bypass.
By utilizing the following JavaScript code, we can view the data in the x0 register during execution to gain insights into the type of jailbreak detection being employed. This information can be useful for both further dynamic bypasses and for crafting static bypass solutions.
var targetModule = 'DVIA-v2';
var addr = ptr(0x192c64);
var moduleBase = Module.getBaseAddress(targetModule);
var targetAddress = moduleBase.add(addr);
Interceptor.attach(targetAddress, {
onEnter: function(args) {
console.log('At the address ' + addr + ' the value is currently ' + this.context.x0);
},
});
We can inject the following Frida script into the DVIA-v2 app by running the command
frida -U -l x0retval.js DVIA-v2
. Once executed, we can trigger the jailbreak detection test and observe the value in the x0 register, which is revealed to be 0x1. This indicates that a simple binary bit check is being performed, with valid jailbreak detection status represented by 0x1.
redacted@redacted:~$ frida -U -l x0retval.js DVIA-v2
____
/ _ | Frida 16.0.19 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to iOS Device
[iOS Device::DVIA-v2 ]-> At the address 0x192c64 the value is currently 0x1
With this additional information, we can now write a simple static patch for the binary to always bypass the jailbreak detection. Here is an example:
0000000100192C64 LDR X8, [SP,#0x50+var_28]
0000000100192C68 STR W0, [SP,#0x50+var_44]
0000000100192C6C MOV W0, #0
__
However,
Phil Keeble's provided code allows us to conduct a dynamic bypass using the following JavaScript code with frida:
var targetModule = 'DVIA-v2';
var addr = ptr(0x192c64);
var moduleBase = Module.getBaseAddress(targetModule);
var targetAddress = moduleBase.add(addr);
Interceptor.attach(targetAddress, {
onEnter: function(args) {
if(this.context.x0 == 0x01){
this.context.x0=0x00
console.log("Bypass Test1");
}
},
});
Jailbreak Detection / Bypass Continued
In this section, we will delve into another aspect of the DVIA-v2 jailbreak detection/bypass challenge. We'll kick things off with another frida trace to determine the function that is invoked when we execute the test within the DVIA-v2 app.
Instrumenting...
_T07DVIA_v232JailbreakDetectionViewControllerC20jailbreakTest2TappedyypF: Loaded handler at "/home/redacted/__handlers__/DVIA_v2/_T07DVIA_v232JailbreakDetectionV_6aa20fff.js"
_T07DVIA_v232JailbreakDetectionViewControllerC20jailbreakTest4TappedyypF: Loaded handler at "/home/redacted/__handlers__/DVIA_v2/_T07DVIA_v232JailbreakDetectionV_8217c5bc.js"
_T07DVIA_v232JailbreakDetectionViewControllerC20jailbreakTest1TappedyypF: Loaded handler at "/home/redacted/__handlers__/DVIA_v2/_T07DVIA_v232JailbreakDetectionV_f34069fe.js"
_T07DVIA_v232JailbreakDetectionViewControllerC20jailbreakTest3TappedyypF: Loaded handler at "/home/redacted/__handlers__/DVIA_v2/_T07DVIA_v232JailbreakDetectionV_ab2cd03f.js"
_T07DVIA_v232JailbreakDetectionViewControllerC20jailbreakTest5TappedyypF: Loaded handler at "/home/redacted/__handlers__/DVIA_v2/_T07DVIA_v232JailbreakDetectionV_43991a7c.js"
_T07DVIA_v240ApplicationPatchingDetailsViewControllerC19jailbreakTestTappedyypF: Loaded handler at "/home/redacted/__handlers__/DVIA_v2/_T07DVIA_v240ApplicationPatching_bdca7b85.js"
_T07DVIA_v232JailbreakDetectionViewControllerC14jailbreakTest3yyF: Loaded handler at "/home/redacted/__handlers__/DVIA_v2/_T07DVIA_v232JailbreakDetectionV_f03b6d58.js"
Started tracing 7 functions. Press Ctrl+C to stop.
/* TID 0x303 */
6457 ms _T07DVIA_v232JailbreakDetectionViewControllerC20jailbreakTest2TappedyypF()
We can see in this case, a similar function is called when we start the test.
_T07DVIA_v232JailbreakDetectionViewControllerC20jailbreakTest2TappedyypF()
Static Analysis
The function we are analyzing this time is much more extensive, as it implements multiple checks to detect jailbroken devices. The method used is to search for on-disk artifacts that would exist on a jailbroken device but not on a normal device.
As an example in the function, we can see mentions of
/Applications/Cydia.app
.
MOV X29, X29
BL _objc_retainAutoreleasedReturnValue
ADRL X8, aApplicationsCy ; "/Applications/Cydia.app"
MOV W9, #0x17
MOV X1, X9
MOV W2, #1
STR X0, [SP,#0x620+var_178]
MOV X0, X8 BL __T0S2SBp21_builtinStringLiteral_Bw17utf8CodeUnitCountBi1_7isASCIItcfC
It will then go on to check whether or not the application present on the device or not by using the Objective-C method
fileExistsAtPath:
.
ADRL X8, selRef_fileExistsAtPath_
LDR X1, [X8] ; "fileExistsAtPath:"
LDR X0, [SP,#0x620+var_188]
LDR X2, [SP,#0x620+var_178]
STR X0, [SP,#0x620+var_190]
MOV X0, X2 ; id
LDR X2, [SP,#0x620+var_190]
BL _objc_msgSend
LDR X1, [SP,#0x620+var_188]
STR W0, [SP,#0x620+var_194]
First, the selector
fileExistsAtPath:
is loaded into register
X1
using the
selRef_fileExistsAtPath_
label. Then, the arguments for the method are loaded into registers
X0
and
X2
from the stack, and the first argument is stored into a temporary stack variable at offset
var_190
.
Next, the
id
object (which represents the receiver of the method, i.e., the object on which the method is called) is moved into register
X0
. The second argument is then loaded into register
X2
from the temporary stack variable at offset
var_190
.
The Objective-C runtime function
_objc_msgSend
is then called with these three registers as arguments, which will invoke the
fileExistsAtPath:
method on the object in
X0
. The return value of the method (which will be a boolean indicating whether the file exists) is stored into a temporary stack variable at offset
CydiaExistCheck
.
The function will also check for
/Library/MobileSubstrate/MobileSubstrate.dylib
alongside multiple other artifacts. Essentially all you need to know if that for each check that occurs in the function, if the artifact if found then it will set a global variable to equal 0x1.
There are two methods to dynamically bypass this jailbreak detection. The first approach involves injecting code into the process and altering a register value that is utilized in the final check.
LDURB W8, [X29,#var_30]
AND W8, W8, #1
TBZ W8, #0, loc_100195DA8
The above assembly code is part of the final jailbreak detection check. To bypass it dynamically, we can use the same JavaScript snippet as before, but modify the address & register context to change the value of the W8 register to 0x0 during execution.
var targetModule = 'DVIA-v2';
var addr = ptr(0x1959d8);
var moduleBase = Module.getBaseAddress(targetModule);
var targetAddress = moduleBase.add(addr);
Interceptor.attach(targetAddress, {
onEnter: function(args) {
if(this.context.x8 == 0x01){
this.context.x8=0x00
console.log("[X] Wrote 0x0 to register x8. Detection Bypassed.");
}
},
});
The jailbreak detection was bypassed.
____
/ _ | Frida 16.0.19 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to iOS Device
[iOS Device::DVIA-v2 ]-> [X] Wrote 0x0 to register x8. Detection Bypassed.
The second method involves dynamically changing the register value for each artifact check that occurs. While not needed in this situation, it can be a useful technique to practice Javascript development and demonstrate the effectiveness of the idea.
Here is the address list that we need:
0x195330
0x1953f8
0x1954c0
0x195588
0x195650
0x1957bC
0x1959d0
Javascript code:
var targetModule = 'DVIA-v2';
var addresses = [
0x195330,
0x1953f8,
0x1954c0,
0x195588,
0x195650,
0x1957bc,
0x1959d0
];
addresses.forEach(function(addr) {
var moduleBase = Module.getBaseAddress(targetModule);
var targetAddress = moduleBase.add(ptr(addr));
Interceptor.attach(targetAddress, {
onEnter: function(args) {
if (this.context.x8 == 0x01) {
this.context.x8 = 0x00
console.log("[X] Wrote 0x0 to register x8. Detection Bypassed.");
}
},
});
});