iOS安全檢測(cè):是否越獄、動(dòng)態(tài)庫(kù)注入、重簽名

前沿

公司項(xiàng)目,最新的一個(gè)版本,bugly崩潰率突然升高,而且發(fā)生在一個(gè)未知的動(dòng)態(tài)庫(kù)。觀察了幾天發(fā)現(xiàn):每天發(fā)生有固定的時(shí)間斷,手機(jī)設(shè)備也很類(lèi)似,所以就懷疑是被攻擊了,然后又分析埋點(diǎn)統(tǒng)計(jì)數(shù)據(jù),奈何統(tǒng)計(jì)數(shù)據(jù)沒(méi)有上報(bào)關(guān)鍵的信息,還是無(wú)法拿出確鑿證據(jù)。于是就網(wǎng)上找了些方法,添加一些日志到bugly和埋點(diǎn)統(tǒng)計(jì)里面,再繼續(xù)跟進(jìn)一下。本文主要參考鏈接:https://github.com/SmileZXLee/ZXHookDetection

Demo下載:https://github.com/ZhangJingHao/ZJHSafeCheckDemo.git

一、是否越獄

1、使用NSFileManager檢測(cè)關(guān)鍵文件

使用NSFileManager通過(guò)檢測(cè)一些越獄后的關(guān)鍵文件/路徑是否可以訪問(wèn)來(lái)判斷是否越獄 常見(jiàn)的文件/路徑有

static char *JailbrokenPathArr[] = {"/Applications/Cydia.app","/usr/sbin/sshd","/bin/bash","/etc/apt","/Library/MobileSubstrate","/User/Applications/"};

判斷是否越獄(使用NSFileManager)

/// 使用NSFileManager通過(guò)檢測(cè)一些越獄后的關(guān)鍵文件是否可以訪問(wèn)來(lái)判斷是否越獄
+ (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;
}

但是攻擊者可以通過(guò)hook NSFileManager的fileExistsAtPath方法來(lái)繞過(guò)檢測(cè)

//繞過(guò)使用NSFileManager判斷特定文件是否存在的越獄檢測(cè),此時(shí)直接返回NO勢(shì)必會(huì)影響程序中對(duì)這個(gè)方法的正常使用,因此可以先打印一下path,然后判斷如果path是用來(lái)判斷是否越獄則返回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語(yǔ)言函數(shù)stat判斷文件是否存在

使用C語(yǔ)言函數(shù)stat判斷文件是否存在(注:stat函數(shù)用于獲取對(duì)應(yīng)文件信息,返回0則為獲取成功,-1為獲取失敗)

/// 使用stat通過(guò)檢測(cè)一些越獄后的關(guān)鍵文件是否可以訪問(wèn)來(lái)判斷是否越獄
+ (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函數(shù),fishhook通過(guò)在mac-o文件中查找并替換函數(shù)地址達(dá)到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);
}

在動(dòng)態(tài)庫(kù)加載的時(shí)候,調(diào)用statHook

%ctor{
    [StatHook statHook];
}

判斷stat的來(lái)源是否來(lái)自于系統(tǒng)庫(kù),因?yàn)閒ishhook通過(guò)交換函數(shù)地址來(lái)實(shí)現(xiàn)hook,若hook了stat,則stat來(lái)源將指向攻擊者注入的動(dòng)態(tài)庫(kù)中 因此我們可以完善上方的isJailbroken2判斷規(guī)則,若stat來(lái)源非系統(tǒng)庫(kù),則直接返回已越獄

+ (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、通過(guò)環(huán)境變量DYLD_INSERT_LIBRARIES判斷

通過(guò)環(huán)境變量DYLD_INSERT_LIBRARIES判斷是否越獄,若獲取到的為NULL,則未越獄

+ (BOOL)isJailbroken3{
    if(TARGET_IPHONE_SIMULATOR)return NO;
    return !(NULL == getenv("DYLD_INSERT_LIBRARIES"));
}

此時(shí)依然可以使用fishhook hook函數(shù)getenv,攻防方法同上,此處不再贅述。

二、是否動(dòng)態(tài)庫(kù)注入

注入檢測(cè)可以判斷加載模塊中有沒(méi)有一些不在正常加載列表中的模塊,使用 _dyld_get_image_name 獲取模塊名,然后進(jìn)行對(duì)比,具體如下

/// 是否注入動(dòng)態(tài)庫(kù):返回nil則未注入,有值表示已注入
/// @return 有值時(shí),會(huì)返回首次獲取的動(dòng)態(tài)庫(kù)名,方便bugly查看日志
+ (NSString *)isInjectDylib {
    if(TARGET_IPHONE_SIMULATOR) return nil;
    
    // 通過(guò)遍歷dyld_image檢測(cè)非法注入的動(dòng)態(tài)庫(kù)
    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];
                
        // 過(guò)濾非dylib后綴的路徑
        if(![res hasSuffix:@".dylib"]){
            continue;
        }
        
        // 越獄設(shè)備動(dòng)態(tài)庫(kù)
        if ([res containsString:@"/Library/MobileSubstrate/DynamicLibraries"]) {
            return [res lastPathComponent];
        }
        // 非越獄設(shè)備動(dòng)態(tài)庫(kù)
        else if([res containsString:@"/var/containers/Bundle/Application"])  {
            // 這邊還需要過(guò)濾掉自己項(xiàng)目中本身有的動(dòng)態(tài)庫(kù)
            return [res lastPathComponent];
        }
    }
    
    return nil;
}

三、是否重簽名

通過(guò)檢測(cè)ipa中的embedded.mobileprovision中的我們打包Mac的公鑰來(lái)確定是否簽名被修改,但是需要注意的是此方法只適用于Ad Hoc或企業(yè)證書(shū)打包的情況,App Store上應(yīng)用由蘋(píng)果私鑰統(tǒng)一打包,不存在embedded.mobileprovision文件

/// 是否重簽名:返回nil表示未重簽,有值表示已重簽名
/// @param publicKey 打包時(shí)的公鑰
/// @return 有值時(shí),會(huì)返回對(duì)檢測(cè)出來(lái)的公鑰值
+ (NSString *)isResignWithPublicKey:(NSString *)publicKey {
    if(TARGET_IPHONE_SIMULATOR) return nil;

    /* 通過(guò)檢測(cè)ipa中的embedded.mobileprovision中的我們打包Mac的公鑰來(lái)確定是否簽名被修改,
       但是需要注意的是此方法只適用于Ad Hoc或企業(yè)證書(shū)打包的情況,
       App Store上應(yīng)用由蘋(píng)果私鑰統(tǒng)一打包,不存在embedded.mobileprovision文件
       來(lái)源于http://www.itdecent.cn/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:@"<string>"].location+8;
            
            NSInteger toPosition = [embeddedProvisioningLines[i+1] rangeOfString:@"</string>"].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;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容