对于安全性要求高的App,需要添加逆向成本,较少被破解和攻击的风险,防护的方式主要有越狱检测
, 抓包检测
, 防反编译
, 防重签名
, 防hook
, 防动态调试
越狱检测 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 + (BOOL )isJailbroken { NSArray *jailFilePaths = @[@"/Applications/Cydia.app" , @"/Library/MobileSubstrate/MobileSubstrate.dylib" , @"/bin/bash" , @"/usr/sbin/sshd" , @"/etc/apt" ]; for (NSString *filePath in jailFilePaths) { if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { return YES ; } } if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"cydia://package/com.example.package" ]]){ return YES ; } if ([[NSFileManager defaultManager] fileExistsAtPath:@"/User/Applications/" ]){ NSArray *applist = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:@"/User/Applications/" error:nil ]; NSLog (@"applist = %@" ,applist); return YES ; } char *env = getenv("DYLD_INSERT_LIBRARIES" ); if (env != NULL ) { return YES ; } return NO ; }
当然,上面方法很容易被hook,我们可以把它拆成多个方法,并且字符串加密处理,放到C方法中,增加hook的成本
防抓包
防反编译 这里主要是代码混淆,不做展开
类名方法名混淆,OC的话可以用宏占位和宏替换来做
llvm编译器混淆
反注入(已失效) dyld在加载MachO的时候,会判断segments中判断是否有restrict这个段,如果有的话,那么就不会加载DYLD_INSERT_LIBRARIES
环境变量的动态库,所以可以在程序中加入restrict这个字段
在Xcode
-> Build Settings
-> Other Link Flags
添加-Wl,-sectcreate,__RESTRICT,__restrict,/dev/null
标记即可,使用MachOView即可看到load commands中多了__RESTRICT
段
iOS10以后,苹果在dyld中就不在检测__restrict字段,已经失效了
防重签名 我们都知道,App在打包签名后在app里面会带上embedded.mobileprovision
,系统会通过该文件校验应用是否合法,这个文件就是我们打包用的文件,我们我们可以在代码中校验该文件是不是我们自己的,如果不是,则退出程序(AppStore下载的包没有embedded.mobileprovision
)
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 void checkCodesign(NSString *identifier){ NSString *embeddedPath = [[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision" ]; if ([NSFileManager .defaultManager fileExistsAtPath:embeddedPath]) { NSString *embeddedProvisioning = [NSString stringWithContentsOfFile:embeddedPath encoding:NSASCIIStringEncoding error:nil ]; NSArray *embeddedProvisioningLines = [embeddedProvisioning componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]; for (int i = 0 ; i < embeddedProvisioningLines.count; i++) { if ([embeddedProvisioningLines[i] rangeOfString:@"application-identifier" ].location != NSNotFound ) { NSString *value = embeddedProvisioningLines[i + 1 ]; NSInteger start = [value rangeOfString:@"<string>" ].location; NSInteger end = [value rangeOfString:@"</string>" ].location; if (start != NSNotFound && end != NSNotFound ) { NSString *applicationIdentifier = [value substringWithRange:NSMakeRange (start + 8 , end - start - 8 )]; if (![applicationIdentifier isEqual:identifier]) { asm ( "mov X0,#0\n" "mov w16,#1\n" "svc #0x80" ); } } } } } else { } }
防动态调试 使用ptrace
函数反调试 debugserver
之所以可以调试APP, 是依赖一个系统函数ptrace
(process trace 进程跟踪). 此函数提供了一个进程监听控制另外一个进程, 并且可以检查被控制进程的内容和寄存器里面的数据. 可以用来实现断电调试和系统调用跟踪. iOS中没有提供此函数的头文件, 但不是私有API.
ptrace
函数在iOS项目中不能找到,在MacOS工程可以引用到,我们把需要用到的函数声明搬过来
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 int ptrace (int _request, pid_t _pid, caddr_t _addr, int _data) ;#define PT_TRACE_ME 0 #define PT_READ_I 1 #define PT_READ_D 2 #define PT_READ_U 3 #define PT_WRITE_I 4 #define PT_WRITE_D 5 #define PT_WRITE_U 6 #define PT_CONTINUE 7 #define PT_KILL 8 #define PT_STEP 9 #define PT_ATTACH ePtAttachDeprecated #define PT_DETACH 11 #define PT_SIGEXC 12 #define PT_THUPDATE 13 #define PT_ATTACHEXC 14 #define PT_FORCEQUOTA 30 #define PT_DENY_ATTACH 31 #define PT_FIRSTMACH 32
找地方执行,可以在load方法
1 2 3 4 5 + (void )load { ptrace(PT_DENY_ATTACH, getpid(), 0 , 0 ); }
当打开debugserver
的时候会失败(Segmentation fault: 11
)
1 2 3 4 5 6 root debugserver-@( for arm64. Attaching to process Test... Segmentation fault: 11 laboshi:~ root
直接使用ptrace
方法的时候,编译完成后符号表会出现ptrace
符号,提审可能会被拒,这个就看审核员的心情了,我们可以通过dlopen
动态加载系统的动态库和方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <dlfcn.h> int (*ptrace_p)(int _request, pid_t _pid, caddr_t _addr, int _data);void *handler = dlopen("/usr/lib/system/libsystem_kernel.dylib" , RTLD_LAZY);if (handler) { ptrace_p = dlsym(handler, "ptrace" ); if (ptrace_p) { ptrace_p(PT_DENY_ATTACH, 0 , 0 , 0 ); } }
上面的字符串可以做一定的加密处理,减少特征
使用sysctl
函数反调试 使用sysctl
函数可以判断当前程序是否正在被调试,可以隔一段时间检测一下
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 #import <sys/sysctl.h> bool isDebuging () { int name[4 ]; name[0 ] = CTL_KERN; name[1 ] = KERN_PROC; name[2 ] = KERN_PROC_PID; name[3 ] = getpid(); struct kinfo_proc info ; size_t info_size = sizeof (info); int error = sysctl(name, sizeof (name)/sizeof (*name), &info, &info_size, 0 , 0 ); if (!error) { if (info.kp_proc.p_flag & P_TRACED) { return true ; } else { return false ; } } return false ; }
反反调试 上面反调试方法都是C语言的方法,而我们知道fishhook
可以 hook (系统的)C方法,所以上面两个方法可以被fishhook替换掉
这时候我们就需要保护系统的C方法不被hook
,我们可以在别人hook之前换成我们自己的实现,然后别人再hook的时候就只是hook我们替换过的实现了
如何确保我们的hook在别人之前调用
呢?
我们知道,dyld加载App的时候,动态库是先加载的,而动态库的加载顺序是根据MachO文件描述的顺序(也就是Xcode
-> Frameworks,Libraries,and Embedded Content
配置的顺序),我们可以用一个防护的动态库
让我们的动态库先执行
当然如果MachO文件的动态链接库的顺序被改变了,还是会被别人先hook,这个成本就比较高了
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 #import "fishhook.h" #define PT_DENY_ATTACH 31 static int (*ptrace_p)(int _request, pid_t _pid, caddr_t _addr, int _data);int my_ptrace(int _request, pid_t _pid, caddr_t _addr, int _data) { if (_request != PT_DENY_ATTACH) { return ptrace_p(_request, _pid, _addr, _data); } else { return 0 ; } } + (void )load { struct rebinding ptraceBd; ptraceBd.name = "ptrace" ; ptraceBd.replaced = (void *)&ptrace_p; ptraceBd.replacement = my_ptrace; struct rebinding bds[] = {ptraceBd}; rebind_symbols(bds, 1 ); }
防hook 对于OC
的方法的hook通常是使用runtime的方法交换来实现method_exchangeImplementations
,所以我们确保这个方法是安全的,就能很大程度上降低OC方法被hook
由于dyld加载程序时候,对于外部符号(例如系统函数)是lazybind
加载的,编译的时候并不是绑定真实的地址,而是在运行时动态绑定的,所以fishhook
可以hook系统方法
我们可以先把method_exchangeImplementations
先换成我们的函数,然后别人在交换该方法的时候,就无法拿到原本的实现了
如何让我们的hook先调用呢
dyld在加载程序的时候,会先加载动态库,并且是按照MachO文件存储的顺序加载(也就是Xcode链接库的顺序),所以我们可以把我们的hook代码放到动态库放到最前面,就可以然后在load方法交换方法
当然,如果MachO文件的动态库链接顺序也被修改了,那么就没办法了,这时候可以通过一些逻辑判断来增加hook难度,例如如果调用次数多了,就退出程序exit(0)
上面只做了method_exchangeImplementations
方法的防护,还有其他一些潜在的危险方法也需要做防护,例如:method_setImplementation
和method_getImplementation
,通常我们没有用到这两个方法,如果没有用到,就直接替换掉
另外由于程序库内部的C方法
比较难被hook,对于一些敏感的方法可以放到C方法中(在命名也做一些混淆处理)
防fishhook 我们知道系统库的方法可以被fishhook替换掉,如何防fishhook呢
dlopen+dlsym 采用dlopen+dlsym
调用系统方法可以防fishhook,如上面调用ptrace
的第二种方式
syscall 使用系统函数syscall
调用ptrace
通过<sys/syscall.h>
头文件找到对应的ptrace
函数编号为26
1 2 3 4 5 6 7 8 ... #define SYS_setuid 23 #define SYS_getuid 24 #define SYS_geteuid 25 #define SYS_ptrace 26 #define SYS_recvmsg 27 #define SYS_sendmsg 28 ...
调用
1 syscall(26 , PT_DENY_ATTACH, 0 , 0 );
汇编调用 双面两种方式都是基于符号调用函数,这里有个缺点是可以被符号断点
短住,这样攻击者,可以先断住符号断点,然后跳过该符号函数从而让我们的代码失效,如果我们写的是汇编代码,则不会被符号断点跟踪到,下面用汇编执行ptrace
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 asm volatile ( "mov x0,#31\n" "mov x1,#0\n" "mov x2,#0\n" "mov x3,#0\n" "mov x16,#26\n" "svc #0x80\n" ); #ifdef __arm64__ asm ( "mov x0,#0\n" "mov w16,#1\n" "svc #0x80\n" ); #endif #ifdef __arm__ asm ( "mov r0,#0\n" "mov r12,#1\n" "svc #80\n" ); #endif
小结 安全防护之后更好,没有最好,我们只能增加攻击者的成本,增加逆向难度