前沿
公司项目,最新的一个版本,bugly崩溃率突然升高,而且发生在一个未知的动态库。观察了几天发现:每天发生有固定的时间断,手机设备也很类似,所以就怀疑是被攻击了,然后又分析埋点统计数据,奈何统计数据没有上报关键的信息,还是无法拿出确凿证据。于是就网上找了些方法,添加一些日志到bugly和埋点统计里面,再继续跟进一下。本文主要参考链接:https://github.com/SmileZXLee/ZXHookDetection
Demo下载:https://github.com/ZhangJingHao/ZJHSafeCheckDemo.git
一、是否越狱
1、使用NSFileManager检测关键文件
使用NSFileManager通过检测一些越狱后的关键文件/路径是否可以访问来判断是否越狱 常见的文件/路径有
static char *JailbrokenPathArr[] = {"/Applications/Cydia.app","/usr/sbin/sshd","/bin/bash","/etc/apt","/Library/MobileSubstrate","/User/Applications/"};
判断是否越狱(使用NSFileManager)
/// 使用NSFileManager通过检测一些越狱后的关键文件是否可以访问来判断是否越狱
+ (BOOL)isJailbroken1 {
if(TARGET_IPHONE_SIMULATOR) return NO;
for (int i = 0;i < sizeof(JailbrokenPathArr) / sizeof(char *);i++) {
if([[NSFileManager defaultManager] fileExistsAtPath:[NSString stringWithUTF8String:JailbrokenPathArr[i]]]){
return YES;
}
}
return NO;
}
但是攻击者可以通过hook NSFileManager的fileExistsAtPath方法来绕过检测
//绕过使用NSFileManager判断特定文件是否存在的越狱检测,此时直接返回NO势必会影响程序中对这个方法的正常使用,因此可以先打印一下path,然后判断如果path是用来判断是否越狱则返回NO,否则按照正常逻辑返回
%hook NSFileManager
- (BOOL)fileExistsAtPath:(NSString *)path{
if(TARGET_IPHONE_SIMULATOR)return NO;
for (int i = 0;i < sizeof(JailbrokenPathArr) / sizeof(char *);i++) {
NSString *jPath = [NSString stringWithUTF8String:JailbrokenPathArr[i]];
if([path isEqualToString:jPath]){
return NO;
}
}
return %orig;
}
%end
2、使用C语言函数stat判断文件是否存在
使用C语言函数stat判断文件是否存在(注:stat函数用于获取对应文件信息,返回0则为获取成功,-1为获取失败)
/// 使用stat通过检测一些越狱后的关键文件是否可以访问来判断是否越狱
+ (BOOL)isJailbroken2{
if(TARGET_IPHONE_SIMULATOR)return NO;
for (int i = 0;i < sizeof(JailbrokenPathArr) / sizeof(char *);i++) {
struct stat stat_info;
if (0 == stat(JailbrokenPathArr[i], &stat_info)) {
return YES;
}
}
return NO;
}
但是使用fishhook可hook C函数,fishhook通过在mac-o文件中查找并替换函数地址达到hook的目的
static int (*orig_stat)(char *c, struct stat *s);
int hook_stat(char *c, struct stat *s){
for (int i = 0;i < sizeof(JailbrokenPathArr) / sizeof(char *);i++) {
if(0 == strcmp(c, JailbrokenPathArr[i])){
return 0;
}
}
return orig_stat(c,s);
}
+(void)statHook{
struct rebinding stat_rebinding = {"stat", hook_stat, (void *)&orig_stat};
rebind_symbols((struct rebinding[1]){stat_rebinding}, 1);
}
在动态库加载的时候,调用statHook
%ctor{
[StatHook statHook];
}
判断stat的来源是否来自于系统库,因为fishhook通过交换函数地址来实现hook,若hook了stat,则stat来源将指向攻击者注入的动态库中 因此我们可以完善上方的isJailbroken2判断规则,若stat来源非系统库,则直接返回已越狱
+ (BOOL)isJailbroken2{
if(TARGET_IPHONE_SIMULATOR)return NO;
int ret ;
Dl_info dylib_info;
int (*func_stat)(const char *, struct stat *) = stat;
if ((ret = dladdr(func_stat, &dylib_info))) {
NSString *fName = [NSString stringWithUTF8String:dylib_info.dli_fname];
NSLog(@"fname--%@",fName);
if(![fName isEqualToString:@"/usr/lib/system/libsystem_kernel.dylib"]){
return YES;
}
}
for (int i = 0;i < sizeof(JailbrokenPathArr) / sizeof(char *);i++) {
struct stat stat_info;
if (0 == stat(JailbrokenPathArr[i], &stat_info)) {
return YES;
}
}
return NO;
}
3、通过环境变量DYLD_INSERT_LIBRARIES判断
通过环境变量DYLD_INSERT_LIBRARIES判断是否越狱,若获取到的为NULL,则未越狱
+ (BOOL)isJailbroken3{
if(TARGET_IPHONE_SIMULATOR)return NO;
return !(NULL == getenv("DYLD_INSERT_LIBRARIES"));
}
此时依然可以使用fishhook hook函数getenv,攻防方法同上,此处不再赘述。
二、是否动态库注入
注入检测可以判断加载模块中有没有一些不在正常加载列表中的模块,使用 _dyld_get_image_name 获取模块名,然后进行对比,具体如下
/// 是否注入动态库:返回nil则未注入,有值表示已注入
/// @return 有值时,会返回首次获取的动态库名,方便bugly查看日志
+ (NSString *)isInjectDylib {
if(TARGET_IPHONE_SIMULATOR) return nil;
// 通过遍历dyld_image检测非法注入的动态库
int dyld_count = _dyld_image_count();
for (int i = 0; i < dyld_count; i++) {
const char * imageName = _dyld_get_image_name(i);
NSString *res = [NSString stringWithUTF8String:imageName];
// 过滤非dylib后缀的路径
if(![res hasSuffix:@".dylib"]){
continue;
}
// 越狱设备动态库
if ([res containsString:@"/Library/MobileSubstrate/DynamicLibraries"]) {
return [res lastPathComponent];
}
// 非越狱设备动态库
else if([res containsString:@"/var/containers/Bundle/Application"]) {
// 这边还需要过滤掉自己项目中本身有的动态库
return [res lastPathComponent];
}
}
return nil;
}
三、是否重签名
通过检测ipa中的embedded.mobileprovision中的我们打包Mac的公钥来确定是否签名被修改,但是需要注意的是此方法只适用于Ad Hoc或企业证书打包的情况,App Store上应用由苹果私钥统一打包,不存在embedded.mobileprovision文件
/// 是否重签名:返回nil表示未重签,有值表示已重签名
/// @param publicKey 打包时的公钥
/// @return 有值时,会返回对检测出来的公钥值
+ (NSString *)isResignWithPublicKey:(NSString *)publicKey {
if(TARGET_IPHONE_SIMULATOR) return nil;
/* 通过检测ipa中的embedded.mobileprovision中的我们打包Mac的公钥来确定是否签名被修改,
但是需要注意的是此方法只适用于Ad Hoc或企业证书打包的情况,
App Store上应用由苹果私钥统一打包,不存在embedded.mobileprovision文件
来源于https://www.jianshu.com/p/a3fc10c70a29
*/
NSString *embeddedPath = [[NSBundle mainBundle] pathForResource:@"embedded"
ofType:@"mobileprovision"];
if (!embeddedPath) {
return nil;
}
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) {
NSInteger fromPosition =
[embeddedProvisioningLines[i+1] rangeOfString:@""].location+8;
NSInteger toPosition = [embeddedProvisioningLines[i+1] rangeOfString:@" "].location;
NSRange range;
range.location = fromPosition;
range.length = toPosition - fromPosition;
NSString *fullIdentifier = [embeddedProvisioningLines[i+1] substringWithRange:range];
NSArray *identifierComponents = [fullIdentifier componentsSeparatedByString:@"."];
NSString *appIdentifier = [identifierComponents firstObject];
if (![appIdentifier isEqualToString:publicKey]) {
return appIdentifier;
} else {
return nil;
}
}
}
return nil;
}