Skip to content

Commit fcfb0ae

Browse files
committed
Add improved jailbreak detection logic
1 parent 7d9e784 commit fcfb0ae

File tree

3 files changed

+240
-15
lines changed

3 files changed

+240
-15
lines changed

Sources/KSCrashCore/include/KSSystemCapabilities.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@
126126
#define KSCRASH_HAS_MACH 0
127127
#endif
128128

129+
#if KSCRASH_HOST_IOS || KSCRASH_HOST_TV
130+
#define KSCRASH_HAS_SYSCALL 1
131+
#else
132+
#define KSCRASH_HAS_SYSCALL 0
133+
#endif
134+
129135
// WatchOS signal is broken as of 3.1
130136
#if KSCRASH_HOST_ANDROID || KSCRASH_HOST_IOS || KSCRASH_HOST_MAC || KSCRASH_HOST_TV || KSCRASH_HOST_VISION
131137
#define KSCRASH_HAS_SIGNAL 1

Sources/KSCrashRecording/Monitors/KSCrashMonitor_System.m

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
#import "KSDynamicLinker.h"
3535
#import "KSSysCtl.h"
3636
#import "KSSystemCapabilities.h"
37+
#import "KSJailbreak.h"
3738

3839
// #define KSLogger_LocalLevel TRACE
3940
#import "KSLogger.h"
@@ -289,21 +290,15 @@ static uint64_t usableMemory(void)
289290
*
290291
* @return YES if the device is jailbroken.
291292
*/
292-
static bool isJailbroken(void)
293-
{
294-
static bool sJailbroken;
295-
static dispatch_once_t onceToken;
296-
dispatch_once(&onceToken, ^{
297-
const char *path = "/private/kscrash_jailbreak_test";
298-
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
299-
if (fd < 0) {
300-
sJailbroken = false;
301-
} else {
302-
sJailbroken = true;
303-
unlink(path);
304-
}
305-
});
306-
return sJailbroken;
293+
static inline bool isJailbroken(void) {
294+
static bool initialized_jb;
295+
static bool is_jb;
296+
if(!initialized_jb) {
297+
get_jailbreak_status(&is_jb);
298+
initialized_jb = true;
299+
}
300+
301+
return is_jb;
307302
}
308303

309304
/** Check if the current build is a debug build.
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
//
2+
// KSJailbreak.h
3+
//
4+
// Created by Karl Stenerud on 10.02.21.
5+
// Copyright (c) 2012 Karl Stenerud. All rights reserved.
6+
//
7+
//
8+
// Robust Enough Jailbreak Detection
9+
// ---------------------------------
10+
//
11+
// Perfect jailbreak detection, like perfect copy protection, is a fool's errand.
12+
// But perfection isn't necessary for our purposes. We just need to make it tricky enough
13+
// that only a complicated per-app targeted tweak would work. Once an app gets popular
14+
// enough to warrant the time and effort of a targeted tweak, they'll need custom
15+
// jailbreak detection code anyway for the inevitable cat-and-mouse game.
16+
//
17+
// This code operates on the following anti-anti-jailbreak-detection principles:
18+
//
19+
// - Functions can be patched by a general tweak, but syscalls cannot.
20+
// - "environ" is a global variable, which cannot easily be pre-manipulated without
21+
// potential breakage elsewhere.
22+
//
23+
// We check the following things:
24+
//
25+
// - Can we create a file in /tmp? (/tmp has perms 777, but sandboxed apps can't see it)
26+
// - Does Cydia's MobileSubstrate library exist? (used for tweaks and cracks)
27+
// - Does /etc/apt exist? (Debian's apt is used for non-app-store app distribution)
28+
// - Does the ENV contain an "insert libraries" directive? (used to override functions)
29+
//
30+
// To guard against function overrides, we use syscalls for some of the checks, with a
31+
// graceful fallback to libc functions if we're on an unknown architecture. We also
32+
// stick to very basic and old syscalls that have remained stable for decades.
33+
//
34+
35+
#ifndef HDR_KSJailbreak_h
36+
#define HDR_KSJailbreak_h
37+
38+
#include <dirent.h>
39+
#include <fcntl.h>
40+
#include <stdbool.h>
41+
#include <stdio.h>
42+
#include <sys/types.h>
43+
#include <unistd.h>
44+
#include <TargetConditionals.h>
45+
46+
// The global environ variable must be imported this way.
47+
// See: https://opensource.apple.com/source/Libc/Libc-1439.40.11/man/FreeBSD/environ.7
48+
extern char **environ;
49+
50+
static inline bool ksj_local_is_insert_libraries_env_var(const char* str) {
51+
if (str == NULL) {
52+
return false;
53+
}
54+
55+
// DYLD_INSERT_LIBRARIES lets you override functions by loading other libraries first.
56+
// This is a common technique used for defeating detection.
57+
// See: https://opensource.apple.com/source/dyld/dyld-832.7.3/doc/man/man1/dyld.1
58+
const char insert[] = "DYLD_INSERT_LIBRARIES";
59+
if (strlen(str) < sizeof(insert)) {
60+
return false;
61+
}
62+
return __builtin_memcmp(str, insert, sizeof(insert)) == 0;
63+
}
64+
65+
// Note: The following are written as macros to force them always inline so that there
66+
// are no function calls (which could be patched out). There's enough repetition
67+
// going on that the compiler might refuse to inline if these were functions.
68+
69+
// Reminder: Always follow proper macro hygene to prevent unwanted side effects:
70+
// - Surround macros in a do { ... } while(0) construct to prevent leakage.
71+
// - Surround parameters in parentheses when accessing them.
72+
// - Do not access a parameter more than once.
73+
// - Make local copies of input parameters to help enforce their types.
74+
// - Use pointers for output parameters, with labels that identify them as such.
75+
// - Beware of global consts or defines bleeding through.
76+
77+
#if TARGET_CPU_ARM64 && !TARGET_OS_OSX
78+
#define KSCRASH_HAS_CUSTOM_SYSCALL 1
79+
80+
// ARM64 3-parameter syscall
81+
// Writes -1 to *(pResult) on failure, or the actual result on success.
82+
//
83+
// Implementation Details:
84+
// - Syscall# is in x16, params in x0, x1, x2, and return in x0.
85+
// - Carry bit is cleared on success, set on failure (we copy the carry bit to x3).
86+
// - We must also inform the compiler that memory and condition codes may get clobbered.
87+
#define ksj_syscall3(call_num, param0, param1, param2, pResult) do { \
88+
register uintptr_t call asm("x16") = (uintptr_t)(call_num); \
89+
register uintptr_t p0 asm("x0") = (uintptr_t)(param0); \
90+
register uintptr_t p1 asm("x1") = (uintptr_t)(param1); \
91+
register uintptr_t p2 asm("x2") = (uintptr_t)(param2); \
92+
register uintptr_t carry_bit asm("x3") = 0; \
93+
asm volatile("svc #0x80\n" \
94+
"mov x3, #0\n" \
95+
"adc x3, x3, x3\n" \
96+
: "=r"(carry_bit), "=r"(p0),"=r"(p1) \
97+
: "r"(p0), "r"(p1), "r"(p2), "r"(call), "r"(carry_bit) \
98+
: "memory", "cc"); \
99+
if(carry_bit == 1) { \
100+
*(pResult) = -1; \
101+
} else {\
102+
*(pResult) = (int)p0; \
103+
} \
104+
} while(0)
105+
106+
#elif TARGET_CPU_X86_64 && defined(__GCC_ASM_FLAG_OUTPUTS__) && !TARGET_OS_OSX
107+
#define KSCRASH_HAS_CUSTOM_SYSCALL 1
108+
109+
// X86_64 3-parameter syscall
110+
// Writes -1 to *(pResult) on failure, or the actual result on success.
111+
//
112+
// Implementation Details:
113+
// - Syscall# is in RAX, params in RDI, RSI, RDX, and return in RAX.
114+
// - Carry bit is cleared on success, set on failure.
115+
// - We must also inform the compiler that memory, rcx, r11 may get clobbered.
116+
// The "=@ccc" constraint requires __GCC_ASM_FLAG_OUTPUTS__, not available in Xcode 10
117+
// https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#index-asm-flag-output-operands
118+
#define ksj_syscall3(call_num, param0, param1, param2, pResult) do { \
119+
register uintptr_t rax = (uintptr_t)(call_num) | (2<<24); \
120+
register uintptr_t p0 = (uintptr_t)(param0); \
121+
register uintptr_t p1 = (uintptr_t)(param1); \
122+
register uintptr_t p2 = (uintptr_t)(param2); \
123+
register uintptr_t carry_bit = 0; \
124+
asm volatile( \
125+
"syscall" \
126+
: "=@ccc"(carry_bit), "+a"(rax) \
127+
: "D" (p0), "S" (p1), "d" (p2) \
128+
: "memory", "rcx", "r11"); \
129+
if(carry_bit == 1) { \
130+
*(pResult) = -1; \
131+
} else { \
132+
*(pResult) = (int)rax; \
133+
} \
134+
} while(0)
135+
136+
#else
137+
#define KSCRASH_HAS_CUSTOM_SYSCALL 0
138+
139+
// Unhandled architecture.
140+
// We fall back to the libc functions in this case, mimicing the syscall-like API.
141+
// See definitions below.
142+
143+
#endif /* TARGET_CPU_XYZ */
144+
145+
146+
#if KSCRASH_HAS_CUSTOM_SYSCALL
147+
148+
// See: https://opensource.apple.com/source/xnu/xnu-7195.81.3/bsd/kern/syscalls.master
149+
#define KSCRASH_SYSCALL_OPEN 5
150+
#define ksj_syscall_open(path, flags, mode, pResult) ksj_syscall3(KSCRASH_SYSCALL_OPEN, (uintptr_t)path, flags, mode, pResult)
151+
152+
#else
153+
154+
#define ksj_syscall_open(path, flags, mode, pResult) do {*(pResult) = open(path, flags, mode);} while(0)
155+
156+
#endif /* KSCRASH_HAS_CUSTOM_SYSCALL */
157+
158+
159+
/**
160+
* Get this device's jailbreak status.
161+
* Stores nonzero in *(pIsJailbroken) if the device is jailbroken, 0 otherwise.
162+
* Note: Implemented as a macro to force it inline always.
163+
*/
164+
#if !TARGET_OS_SIMULATOR && !TARGET_OS_OSX && KSCRASH_HAS_SYSCALL
165+
#define get_jailbreak_status(pIsJailbroken) do { \
166+
int fd = 0; \
167+
\
168+
bool tmp_file_is_accessible = false; \
169+
bool mobile_substrate_exists = false; \
170+
bool etc_apt_exists = false; \
171+
bool has_insert_libraries = false; \
172+
\
173+
const char* test_write_file = "/tmp/bugsnag-check.txt"; \
174+
remove(test_write_file); \
175+
ksj_syscall_open(test_write_file, O_CREAT, 0644, &fd); \
176+
if(fd > 0) { \
177+
close(fd); \
178+
tmp_file_is_accessible = true; \
179+
} else { \
180+
ksj_syscall_open(test_write_file, O_RDONLY, 0, &fd); \
181+
if(fd > 0) { \
182+
close(fd); \
183+
tmp_file_is_accessible = true; \
184+
} \
185+
} \
186+
remove(test_write_file); \
187+
\
188+
const char* mobile_substrate_path = "/Library/MobileSubstrate/MobileSubstrate.dylib"; \
189+
ksj_syscall_open(mobile_substrate_path, O_RDONLY, 0, &fd); \
190+
if(fd > 0) { \
191+
close(fd); \
192+
mobile_substrate_exists = true; \
193+
} \
194+
\
195+
const char* etc_apt_path = "/etc/apt"; \
196+
DIR *dirp = opendir(etc_apt_path); \
197+
if(dirp) { \
198+
etc_apt_exists = true; \
199+
closedir(dirp); \
200+
} \
201+
\
202+
for(int i = 0; environ[i] != NULL; i++) { \
203+
if(ksj_local_is_insert_libraries_env_var(environ[i])) { \
204+
has_insert_libraries = true; \
205+
break; \
206+
} \
207+
} \
208+
\
209+
*(pIsJailbroken) = tmp_file_is_accessible || \
210+
mobile_substrate_exists || \
211+
etc_apt_exists || \
212+
has_insert_libraries; \
213+
} while(0)
214+
215+
#else
216+
217+
// "/tmp" is accessible on the simulator, which makes the JB test come back positive, so
218+
// report false on the simulator.
219+
#define get_jailbreak_status(pIsJailbroken) do { *(pIsJailbroken) = 0; } while(0)
220+
221+
#endif /* !TARGET_OS_SIMULATOR */
222+
223+
224+
#endif /* HDR_KSJailbreak_h */

0 commit comments

Comments
 (0)