前沿
公司項目衰齐,最新的一個版本,bugly崩潰率突然升高继阻,而且發(fā)生在一個未知的動態(tài)庫耻涛。觀察了幾天發(fā)現(xiàn):每天發(fā)生有固定的時間斷,手機設備也很類似瘟檩,所以就懷疑是被攻擊了抹缕,然后又分析埋點統(tǒng)計數(shù)據(jù),奈何統(tǒng)計數(shù)據(jù)沒有上報關鍵的信息墨辛,還是無法拿出確鑿證據(jù)卓研。于是就網(wǎng)上找了些方法,添加一些日志到bugly和埋點統(tǒng)計里面睹簇,再繼續(xù)跟進一下奏赘。本文主要參考鏈接: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語言函數(shù)stat判斷文件是否存在
使用C語言函數(shù)stat判斷文件是否存在(注:stat函數(shù)用于獲取對應文件信息,返回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函數(shù)喂击,fishhook通過在mac-o文件中查找并替換函數(shù)地址達到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);
}
在動態(tài)庫加載的時候,調(diào)用statHook
%ctor{
[StatHook statHook];
}
判斷stat的來源是否來自于系統(tǒng)庫淤翔,因為fishhook通過交換函數(shù)地址來實現(xiàn)hook翰绊,若hook了stat,則stat來源將指向攻擊者注入的動態(tài)庫中 因此我們可以完善上方的isJailbroken2判斷規(guī)則旁壮,若stat來源非系統(tǒng)庫监嗜,則直接返回已越獄
+ (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、通過環(huán)境變量DYLD_INSERT_LIBRARIES判斷
通過環(huán)境變量DYLD_INSERT_LIBRARIES判斷是否越獄抡谐,若獲取到的為NULL裁奇,則未越獄
+ (BOOL)isJailbroken3{
if(TARGET_IPHONE_SIMULATOR)return NO;
return !(NULL == getenv("DYLD_INSERT_LIBRARIES"));
}
此時依然可以使用fishhook hook函數(shù)getenv,攻防方法同上麦撵,此處不再贅述刽肠。
二、是否動態(tài)庫注入
注入檢測可以判斷加載模塊中有沒有一些不在正常加載列表中的模塊免胃,使用 _dyld_get_image_name 獲取模塊名音五,然后進行對比,具體如下
/// 是否注入動態(tài)庫:返回nil則未注入羔沙,有值表示已注入
/// @return 有值時躺涝,會返回首次獲取的動態(tài)庫名,方便bugly查看日志
+ (NSString *)isInjectDylib {
if(TARGET_IPHONE_SIMULATOR) return nil;
// 通過遍歷dyld_image檢測非法注入的動態(tài)庫
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;
}
// 越獄設備動態(tài)庫
if ([res containsString:@"/Library/MobileSubstrate/DynamicLibraries"]) {
return [res lastPathComponent];
}
// 非越獄設備動態(tài)庫
else if([res containsString:@"/var/containers/Bundle/Application"]) {
// 這邊還需要過濾掉自己項目中本身有的動態(tài)庫
return [res lastPathComponent];
}
}
return nil;
}
三扼雏、是否重簽名
通過檢測ipa中的embedded.mobileprovision中的我們打包Mac的公鑰來確定是否簽名被修改坚嗜,但是需要注意的是此方法只適用于Ad Hoc或企業(yè)證書打包的情況,App Store上應用由蘋果私鑰統(tǒng)一打包诗充,不存在embedded.mobileprovision文件
/// 是否重簽名:返回nil表示未重簽苍蔬,有值表示已重簽名
/// @param publicKey 打包時的公鑰
/// @return 有值時,會返回對檢測出來的公鑰值
+ (NSString *)isResignWithPublicKey:(NSString *)publicKey {
if(TARGET_IPHONE_SIMULATOR) return nil;
/* 通過檢測ipa中的embedded.mobileprovision中的我們打包Mac的公鑰來確定是否簽名被修改蝴蜓,
但是需要注意的是此方法只適用于Ad Hoc或企業(yè)證書打包的情況银室,
App Store上應用由蘋果私鑰統(tǒng)一打包,不存在embedded.mobileprovision文件
來源于http://www.reibang.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:@"<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;
}