iOS-Security[12]-防护

安全防护包含检测是否越狱,检测调试器依附,本地数据存储安全,网络通信安全等。

检测越狱

NSFileManager

越狱环境下,一般会安装一些常用的工具,例如 Cydia,MobileSubstrate 等,可以利用 NSFileManager 判断其路径是否存在。但是, NSFileManager 也可以被 hook,所以并不完全有效。

1
2
3
4
5
6
7
8
9
10
11
12
const char* jailBorkenPath[] = {"/Applications/Cydia.app",
"/Library/MobileSubstrate/MobileSubstrate.dylib",
"/bin/bash",
"/usr/sbin/sshd",
"/etc/apt",
"/var/lib/cydia/",
"/var/cache/apt"};

for(uint32_t i = 0; i < sizeof(jailBorkenPath) / sizeof(char*); ++i){
const char *path = jailBorkenPath[i];
NSLog(@"NSFileManager:%s:%d", path, [[NSFileManager defaultManager] fileExistsAtPath:[NSString stringWithUTF8String:path]]);
}

Stat

由于 NSFileManager 是OC语言编写的,可能被 hook,因此,可以用 Stat 替换,Stat 是用 C 语言编写的,相对安全一些,但是也可能被 hook。

1
2
3
4
5
6
7
8
9
10
11
12
const char* jailBorkenPath[] = {"/Applications/Cydia.app",
"/Library/MobileSubstrate/MobileSubstrate.dylib",
"/bin/bash",
"/usr/sbin/sshd",
"/etc/apt",
"/var/lib/cydia/",
"/var/cache/apt"};
struct stat stat_info;
for(uint32_t i = 0; i < sizeof(jailBorkenPath) / sizeof(char*); ++i){
const char *path = jailBorkenPath[i];
NSLog(@"stat:%s:%d", path, 0 == stat(path, &stat_info));
}

dylib_info.dli_fname

由于 Stat 也可能被 hook, 可以判断 Stat 是否出自系统库 libsystem_kernel.dylib:

1
2
3
4
5
6
7
int ret;
Dl_info dylib_info;
int (*func_stat)(const char *,struct stat *) = stat;
const void *func_void = (void*)func_stat;
if ((ret = dladdr(func_void, &dylib_info))) {
NSLog(@"%s:%d", dylib_info.dli_fname, strcmp(dylib_info.dli_fname, "/usr/lib/system/libsystem_kernel.dylib"));
}

如果 Stat 没被 hook,那么结果应该为 0。

_dyld_get_image_name

通过检测链接的动态库的名字,来判断是否有越狱注入的动态库,例如 MobileSubstrate:

1
2
3
4
5
6
7
8
uint32_t count = _dyld_image_count();
for (uint32_t i = 0 ; i < count; ++i) {
NSString *name = [[NSString alloc] initWithUTF8String:_dyld_get_image_name(i)];
if([name containsString:@"MobileSubstrate"]){
NSLog(@"_dyld_get_image_name:%d", 1);
break;
}
}

CanOpenUrl

利用 Cydia 的 URLScheme 进行判断,但是 Cydia 的 URLScheme 也是可以被篡改的,所以也不完全有效。

1
NSLog(@"canOpenURL:%d", [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"cydia://testjailbroken"]]);

Env

通过检测是否有注入动态库来判断是否越狱,如果有注入的动态库,只会返回环境变量,否则返回 NULL。此方法可能有审核风险。

1
2
char *env = getenv("DYLD_INSERT_LIBRARIES");
NSLog(@"DYLD_INSERT_LIBRARIES:%d", env != NULL);

Fork

越狱环境可以突破沙盒环境,因此可以 fork 子进程,否则会返回 -1,依据这个可以进行判断。

1
NSLog(@"Fork:%d", fork());

Summary

下面是在越狱设备上的运行结果:

iOS-Security-Check-ailbroken

在正常设备上的运行结果:

iOS-Security-Check-NotJailbroken

结合在一起:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#import <sys/stat.h>
#import <dlfcn.h>
#import <mach-o/dyld.h>

- (BOOL)isJailBroken{

const char* jailBorkenPath[] = {"/Applications/Cydia.app",
"/Library/MobileSubstrate/MobileSubstrate.dylib",
"/bin/bash",
"/usr/sbin/sshd",
"/etc/apt",
"/var/lib/cydia/",
"/var/cache/apt"};

for(uint32_t i = 0; i < sizeof(jailBorkenPath) / sizeof(char*); ++i){
if([[NSFileManager defaultManager] fileExistsAtPath:[NSString stringWithUTF8String:jailBorkenPath[i]]])
return YES;
}

struct stat stat_info;
for(uint32_t i = 0; i < sizeof(jailBorkenPath) / sizeof(char*); ++i){
if(0 == stat(jailBorkenPath[i], &stat_info))
return YES;
}


if([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"cydia://testjailbroken"]])
return YES;

int ret;
Dl_info dylib_info;
int (*func_stat)(const char *,struct stat *) = stat;
const void *func_void = (void*)func_stat;
if ((ret = dladdr(func_void, &dylib_info))) {
if(0 != strcmp(dylib_info.dli_fname, "/usr/lib/system/libsystem_kernel.dylib"))
return YES;
}


uint32_t count = _dyld_image_count();
for (uint32_t i = 0 ; i < count; ++i) {
NSString *name = [[NSString alloc] initWithUTF8String:_dyld_get_image_name(i)];
if([name containsString:@"MobileSubstrate"]){
return YES;
}
}

if(getenv("DYLD_INSERT_LIBRARIES") != NULL)
return YES;

if(-1 != fork())
return YES;

return NO;
}

防越狱检测

XCon 是一款通过底层 hook 的方式来防越狱检测的插件,不过经过测试,目前已无效。

检测调试器依附

SEC_IS_BEING_DEBUGGED_RETURN_NIL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

#ifndef NSObject_debugcheck_h
#define NSObject_debugcheck_h

#import <Foundation/Foundation.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/sysctl.h>
#include <string.h>

@interface NSObject (debugCheck)

#define SEC_IS_BEING_DEBUGGED_RETURN_VOID() size_t size = sizeof(struct kinfo_proc); \
struct kinfo_proc info; \
int ret, name[4]; \
memset(&info, 0, sizeof(struct kinfo_proc)); \
name[0] = CTL_KERN; \
name[1] = KERN_PROC; \
name[2] = KERN_PROC_PID; \
name[3] = getpid(); \
if ((ret = (sysctl(name, 4, &info, &size, NULL, 0)))) { \
if (ret) return; \
} \
if (info.kp_proc.p_flag & P_TRACED) return

#define SEC_IS_BEING_DEBUGGED_RETURN_NIL() size_t size = sizeof(struct kinfo_proc); \
struct kinfo_proc info; \
int ret, name[4]; \
memset(&info, 0, sizeof(struct kinfo_proc)); \
name[0] = CTL_KERN; \
name[1] = KERN_PROC; \
name[2] = KERN_PROC_PID; \
name[3] = getpid(); \
if ((ret = (sysctl(name, 4, &info, &size, NULL, 0)))) { \
if (ret) return nil; \
} \
if (info.kp_proc.p_flag & P_TRACED) return nil

@end

#endif /* NSObject_debugcheck_h */

SEC_IS_BEING_DEBUGGED_RETURN_VOID 和SEC_IS_BEING_DEBUGGED_RETURN_NIL 两个宏可以用于判断是否有调试器依附(GDB, LLDB),如果有,则直接 return。

使用方式:

1
2
3
4
5
6
7
8
9
10
11
+ (ViewController *)sharedInstance
{
#ifndef DEBUG
SEC_IS_BEING_DEBUGGED_RETURN_NIL();
#endif
static ViewController *instance = nil;
if (!instance) {
instance = [ViewController new];
}
return instance;
}

注意,该宏只能在 Release 环境下使用,所以要加上宏判断。

PT_DENY_ATTACH

PT_DENY_ATTACH 可以阻止调试器依附,参考苹果开发文档:

PT_DENY_ATTACH:This request is the other operation used by the traced process; it allows a process that is not currently being traced to deny future traces by its parent. All other arguments are ignored. If the process is currently being traced, it will exit with the exit status of ENOTSUP; otherwise, it sets a flag that denies future traces. An attempt by the par-ent parent ent to trace a process which has set this flag will result in a segmentation violation in the parent.

使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import <dlfcn.h>
#import <sys/types.h>

typedef int (*ptrace_ptr_t)(int _request, pid_t _pid, caddr_t _addr, int _data);
#if !defined(PT_DENY_ATTACH)
#define PT_DENY_ATTACH 31
#endif

void disable_gdb() {
void* handle = dlopen(0, RTLD_GLOBAL | RTLD_NOW);
ptrace_ptr_t ptrace_ptr = dlsym(handle, "ptrace");
ptrace_ptr(PT_DENY_ATTACH, 0, 0, 0);
dlclose(handle);
}

int main(int argc, char *argv[])
{

#ifndef DEBUG
disable_gdb();
#endif

@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

加上前后对比:

iOS-Security-DB-Forbidden

防护

类和方法名伪装

为了防止类和方法被直接 hook,直接使用一些伪装的类名和方法名,例如不要直勾勾地用 DataUtil,而是用 UIColorAdditions 替换类名,不要直接用 isJailBroken,而是使用类似 isDefaultColor 之类的名称,增加其 hook 的难度。

1
2
DataUtil -> UIColorAdditions
isJailBroken -> isDefaultColor

阻止调试器依附

利用 SEC_IS_BEING_DEBUGGED_RETURN_NIL 和 PT_DENY_ATTACH 等方式阻止 GDB、 LLDB 依附。

直接退出

当检测到越狱环境,或者有调试器尝试依附时,可以选择直接退出应用:

1
exit(-1);

检查动态库注入

利用 dylib_info.dli_fname 以及 _dyld_get_image_name 等方式,来检测是否有动态库注入,有的话,可以选择直接闪退,将设备号上传到服务端,通知其设备不安全等。

URL Schemes校验

接收到 URL Schemes 请求时,需要校验其 sourceApplication 是否在允许操作的白名单中,另外,也可以弹出弹窗询问用户是否确认操作,来规避一些安全攻击。

本地数据存储

本地数据存储需要注意:

  • 不要保存在 plist 文件中;
  • 尽量不要保存在 NSUserDefaults中,NSUserDefaults 即使在非越狱环境下,也可查看,很不安全;
  • 尽可能保存到 KeyChain 中, 但在越狱环境下,KeyChain 也并不安全, 所以可以对数据进行加密后再存储,例如使用SQLCipher;
  • 注意 CoreData 也是保存在 db 文件中,未加密,可以用 Sqlite 软件查看。

网络通信

网络通信需要注意:

  • 不要明文传敏感信息,例如密码;
  • 不要直接用 MD5,一些常用的 MD5 已经被记录,可被逆推;
  • 使用 HTTPS 时,注意身份认证,每次传输时,校验证书,防止中间人劫持;
  • 可以使用自定义协议,一方面减少数据量,另一方面安全性更高。