前沿
公司項(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;
}