iOS應(yīng)用安全
攻易防難,唯有縝密、多層的防護網(wǎng)絡(luò)才能可靠的保護我們iOS應(yīng)用程序的安全。那么,一個完善的iOS應(yīng)用安全防護框架都要寫哪些東西呢? 首先,先梳理一下常見的逆向及攻擊工具。
iOS應(yīng)用逆向常用工具
- Reveal
- Cycript
- Class-dump
- Keychain-Dumper
- gdb
- iNalyzer
- introspy
- Fishhook
- removePIE
- IDA pro or Hopper
- snoop-it
- iDB
- Charles
- SSL Kill Switch
裸奔app的安全隱患
一部越獄的iOS設(shè)備,外加上述的逆向工具,給裸奔的iOS應(yīng)用程序帶來哪些威脅呢?
- 任意讀寫文件系統(tǒng)數(shù)據(jù)
- HTTP(S)實時被監(jiān)測
- 重新打包ipa
- 暴露的函數(shù)符號
- 未加密的靜態(tài)字符
- 篡改程序邏輯控制流
- 攔截系統(tǒng)框架API
- 逆向加密邏輯
- 跟蹤函數(shù)調(diào)用過程(objc_msgSend)
- 可見視圖的具體實現(xiàn)
- 偽造設(shè)備標識
- 可用的URL schemes
- runtime任意方法調(diào)用
- ……
iOS應(yīng)用安全防護開源工具
ios-class-guard 是對抗class-dump的利器,作用是將ObjC類名方法名等重命名為難以理解的字符。
iOS應(yīng)用安全防護框架概述
針對上述安全隱患,我們的iOS應(yīng)用安全防護框架需實現(xiàn)的任務(wù)大致如下:
-
防護
- ObjC類名方法名等重命名為難以理解的字符
- 加密靜態(tài)字符串運行時解密
- 混淆代碼使其難于反匯編
- 本地存儲文件防篡改
-
檢測
- 調(diào)試狀態(tài)檢測
- 越獄環(huán)境檢測
- ObjC的Swizzle檢測
- 任意函數(shù)的hook檢測
- 指定區(qū)域或數(shù)據(jù)段的校驗和檢測
-
自修復
- 自修復被篡改的數(shù)據(jù)和代碼段
此外,還需要多層的防護,通過高層保護低層的方式來保證整個防護機制不失效。
常用的防護手段
一、加密靜態(tài)字符串運行時解密
一個編譯成功的可執(zhí)行程序,其中已初始化的字符串都是完整可見的。 針對于iOS的Mach-O二進制通??色@得以下幾種字符串信息:
- 資源文件名
- 可見的函數(shù)符號名
- SQL語句
- format
- 通知名
- 加密算法的key
攻擊者如何利用字符串
資源文件名通常用來快速定位逆向分析的入口點。 想要知道判斷購買金幣成功與否的代碼位置?只要確定購買成功時播放的音頻文件名字或者背景圖名字就可以順藤摸瓜了。
kLoginSuccessNotification類似這種通知名稱格外炸眼,利用Cycript發(fā)個此通知試試,也許會有什么意外收獲。 拿到對稱加密算法的key是件很幸福的事情。
字符串異或加解密
是的,字符串需要加密處理,但只需要對高度敏感字符數(shù)據(jù)做加密,比如對稱加密算法的key。 其他的,需要提高編程安全意識來彌補。
常規(guī)辦法是通過異或來加解密,來寫個sample code:
#define XOR_KEY 0xBB
void xorString(unsigned char *str, unsigned char key)
{
unsigned char *p = str;
while( ((*p) ^= key) != '\0') p++;
}
- (void)testFunction
{
unsigned char str[] = {(XOR_KEY ^ 'h'),
(XOR_KEY ^ 'e'),
(XOR_KEY ^ 'l'),
(XOR_KEY ^ 'l'),
(XOR_KEY ^ 'o'),
(XOR_KEY ^ '\0')};
xorString(str, XOR_KEY);
static unsigned char result[6];
memcpy(result, str, 6);
NSLog(@"%s",result); //output: hello
}
這樣就無法從二進制中直接分析得到字符串“hello”了。
二、阻止GDB依附
GDB是大多數(shù)hackers的首選,阻止GDB依附到應(yīng)用的常規(guī)辦法是:
#import <sys/ptrace.h>
int main(int argc, charchar *argv[])
{
#ifndef DEBUG
ptrace(PT_DENY_ATTACH,0,0,0);
#endif
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([WQMainPageAppDelegate class]));
}
}
但遺憾的是,iPhone真實的運行環(huán)境是沒有sys/ptrace.h拋出的。雖然 ptrace 方法沒有被拋出, 但是不用擔心,我們可以通過dlopen拿到它。
dlopen: 當path 參數(shù)為0是,他會自動查找 DYLD_LIBRARY_PATH, $DYLD_FALLBACK_LIBRARY_PATH 和 當前工作目錄中的動態(tài)鏈接庫。
#import <dlfcn.h>
#import <sys/types.h>
typedef int (*ptrace_ptr_t)(int _request, pid_t _pid, caddr_t _addr, int _data);
#if !defined(PT_DENY_ATTACH)
#define PT_DENY_ATTACH 31
#endif // !defined(PT_DENY_ATTACH)
void disable_gdb() {
void* handle = dlopen(0, RTLD_GLOBAL | RTLD_NOW);
ptrace_ptr_t ptrace_ptr = dlsym(handle, "ptrace");
ptrace_ptr(PT_DENY_ATTACH, 0, 0, 0);
dlclose(handle);
}
int main(int argc, charchar *argv[])
{
#ifndef DEBUG
disable_gdb();
#endif
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([WQMainPageAppDelegate class]));
}
}
三、自定義安全鍵盤
大部分中文應(yīng)用彈出的默認鍵盤是簡體中文輸入法鍵盤,在輸入用戶名和密碼的時候,如果使用簡體中文輸入法鍵盤,輸入英文字符和數(shù)字字符的用戶名和密碼時,會自動啟動系統(tǒng)輸入法自動更正提示,然后用戶的輸入記錄會被緩存下來。
系統(tǒng)鍵盤緩存最方便拿到的就是利用系統(tǒng)輸入法自動更正的字符串輸入記錄。
緩存文件的地址是:/private/var/mobile/Library/Keyboard/dynamic-text.dat
導出該緩存文件,查看內(nèi)容,欣喜的發(fā)現(xiàn)一切輸入記錄都是明文存儲的。因為系統(tǒng)不會把所有的用戶輸入記錄都當作密碼等敏感信息來處理。
一般情況下,一個常規(guī)iPhone用戶的dynamic-text.dat文件,高頻率出現(xiàn)的字符串就是用戶名和密碼。
所以,一般銀行客戶端app輸入密碼時都不使用系統(tǒng)鍵盤,而使用自己定制的鍵盤,原因主要有2個:
1)避免第三方讀取系統(tǒng)鍵盤緩存
2)防止屏幕錄制 (自己定制的鍵盤按鍵不加按下效果)
那么,如何實現(xiàn)自定義安全鍵盤呢?大致思路如下:
1)首先捕獲系統(tǒng)鍵盤的彈出、收回通知
2)創(chuàng)建一個更高級別的window擋住系統(tǒng)鍵盤
3)需要拋出一個 id<UITextInput>textInput 的弱引用切換焦點。
四、二進制和資源文件自檢
我們把自己的程序發(fā)布到app store,但是不能保證每一個用戶都是從app store下載官方app,也不能保證每一個用戶都不越獄。
換句話說,我們無法保證程序運行環(huán)境在MAC管控策略下就絕對的安全。
所以,在有些情況下,尤其是和錢有關(guān)系的app,我們有必要在和服務(wù)器通信時,讓服務(wù)器知道客戶端到底是不是官方正版的app。
何以判斷自己是不是正版app呢?hackers們破解你的app,無非就2個地方可以動,1個是二進制,1個是資源文件。
二進制都重新編譯過了自然肯定是盜版……
有些低級的hackers喜歡修改人家的資源文件然后貼上自己的廣告,或者給用戶錯誤的指引……修改資源文件是不需要重新編譯二進制的。
因此,我們有必要在敏感的請求報文中,增加正版應(yīng)用的二進制和資源文件的標識,讓服務(wù)器知道,此請求是否來自正版的未經(jīng)修改的app。
在沙盒中,我們可以讀到自己程序的二進制,也可以讀到資源文件簽名文件,這兩個文件都不算大,我們可以對其取md5值然后以某種組合算法得到一個標記字符串,然后發(fā)給服務(wù)器。
我封裝了相關(guān)文件的讀取地址
@implementation WQPathUtilities
+ (NSString *)directory:(NSSearchPathDirectory)dir
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(dir, NSUserDomainMask, YES);
NSString *dirStr = [paths objectAtIndex:0];
return dirStr;
}
+ (NSString *)documentsDirectory
{
return [WQPathUtilities directory:NSDocumentDirectory];
}
+ (NSString *)cachesDirectory
{
return [WQPathUtilities directory:NSCachesDirectory];
}
+ (NSString *)tmpDirectory
{
return NSTemporaryDirectory();
}
+ (NSString *)homeDirectory
{
return NSHomeDirectory();
}
+ (NSString *)codeResourcesPath
{
NSString *excutableName = [[NSBundle mainBundle] infoDictionary][@"CFBundleExecutable"];
NSString *tmpPath = [[WQPathUtilities documentsDirectory] stringByDeletingLastPathComponent];
NSString *appPath = [[tmpPath stringByAppendingPathComponent:excutableName]
stringByAppendingPathExtension:@"app"];
NSString *sigPath = [[appPath stringByAppendingPathComponent:@"_CodeSignature"]
stringByAppendingPathComponent:@"CodeResources"];
return sigPath;
}
+ (NSString *)binaryPath
{
NSString *excutableName = [[NSBundle mainBundle] infoDictionary][@"CFBundleExecutable"];
NSString *tmpPath = [[WQPathUtilities documentsDirectory] stringByDeletingLastPathComponent];
NSString *appPath = [[tmpPath stringByAppendingPathComponent:excutableName]
stringByAppendingPathExtension:@"app"];
NSString *binaryPath = [appPath stringByAppendingPathComponent:excutableName];
return binaryPath;
}
@end
md5方法:
#import "CommonCrypto/CommonDigest.h"
+(NSString *)md5WithString:(NSString *)string
{
const charchar *cStr = [string UTF8String];
unsigned char result[CC_MD5_DIGEST_LENGTH];
CC_MD5(cStr, strlen(cStr), result);
return [[NSString stringWithFormat:@"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
result[0], result[1], result[2], result[3],
result[4], result[5], result[6], result[7],
result[8], result[9], result[10], result[11],
result[12], result[13], result[14], result[15]
] lowercaseString];
}
五、數(shù)據(jù)擦除
對于敏感數(shù)據(jù),我們不希望長時間放在內(nèi)存中,而希望使用完后立即就被釋放掉。
但是不管是ARC還是MRC,自動釋放池也有輪循工作周期,我們都無法控制內(nèi)存數(shù)據(jù)被擦除的準確時間,讓hackers們有機可乘。
本文介紹一個小技巧——及時數(shù)據(jù)擦除。
假如一個View Controller A的一個數(shù)據(jù)被綁在一個property上,
@interface WipingMemoryViewController : UIViewController
@property (nonatomic,copy) NSString *text;
@end
當A push到 另外一個View Controller B時,該數(shù)據(jù)還是有可能被讀到的
WipingMemoryViewController *lastController = (WipingMemoryViewController *)self.navigationController.viewControllers[0];
NSLog(@"text = %@",lastController.text);
于是,“用后即擦”變得十分必要:
_text = [[NSString alloc]initWithFormat:@"information"];
NSLog(@"Origal string = %@",_text);
//do something...
charchar *string = (charchar *)CFStringGetCStringPtr((CFStringRef)_text, CFStringGetSystemEncoding());
memset(string, 0, [_text length]);
NSLog(@"final text = %@",_text);
Log輸出如下:
WipingMemory[2518:70b] Origal string = information
WipingMemory[2518:70b] final text =
可以看到,我們想要保護的數(shù)據(jù),被有效的擦除了。
還有提個醒,如果是這樣
_text = @"information";
創(chuàng)建的字符串,是會被分配到data區(qū),而是無法修改的。
如果有興趣也有閑心,可以試試運行下面的代碼,有彩蛋哦:
_text = @"information";
memset((__bridge voidvoid *)(_text), 0, _text.length - 1);
NSString *myString = [[NSString alloc]initWithFormat:@"information"];
NSLog(@"Origal text : %@ \n",myString);
編譯器把兩個information的省略到一個地址了~
六、數(shù)據(jù)保護API
1) 文件保護
文件系統(tǒng)中的文件、keychain中的項,都是加密存儲的。當用戶解鎖設(shè)備后,系統(tǒng)通過UDID密鑰和用戶設(shè)定的密碼生成一個用于解密的密碼密鑰,存放在內(nèi)存中,直到設(shè)備再次被鎖,開發(fā)者可以通過Data Protection API 來設(shè)定文件系統(tǒng)中的文件、keychain中的項應(yīng)該何時被解密。
/* 為filePath文件設(shè)置保護等級 */
NSDictionary *attributes = [NSDictionary dictionaryWithObject:NSFileProtectionComplete
forKey:NSFileProtectionKey];
[[NSFileManager defaultManager] setAttributes:attributes
ofItemAtPath:filePath
error:nil];
//文件保護等級屬性列表
NSFileProtectionNone //文件未受保護,隨時可以訪問 (Default)
NSFileProtectionComplete //文件受到保護,而且只有在設(shè)備未被鎖定時才可訪問
NSFileProtectionCompleteUntilFirstUserAuthentication //文件收到保護,直到設(shè)備啟動且用戶第一次輸入密碼
NSFileProtectionCompleteUnlessOpen //文件受到保護,而且只有在設(shè)備未被鎖定時才可打開,不過即便在設(shè)備被鎖定時,已經(jīng)打開的文件還是可以繼續(xù)使用和寫入
2) keychain項保護
/* 設(shè)置keychain項保護等級 */
NSDictionary *query = @{(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrGeneric:@"MyItem",
(__bridge id)kSecAttrAccount:@"username",
(__bridge id)kSecValueData:@"password",
(__bridge id)kSecAttrService:[NSBundle mainBundle].bundleIdentifier,
(__bridge id)kSecAttrLabel:@"",
(__bridge id)kSecAttrDescription:@"",
(__bridge id)kSecAttrAccessible:(__bridge id)kSecAttrAccessibleWhenUnlocked};
OSStatus result = SecItemAdd((__bridge CFDictionaryRef)(query), NULL);
//keychain項保護等級列表
kSecAttrAccessibleWhenUnlocked //keychain項受到保護,只有在設(shè)備未被鎖定時才可以訪問
kSecAttrAccessibleAfterFirstUnlock //keychain項受到保護,直到設(shè)備啟動并且用戶第一次輸入密碼
kSecAttrAccessibleAlways //keychain未受保護,任何時候都可以訪問 (Default)
kSecAttrAccessibleWhenUnlockedThisDeviceOnly //keychain項受到保護,只有在設(shè)備未被鎖定時才可以訪問,而且不可以轉(zhuǎn)移到其他設(shè)備
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly //keychain項受到保護,直到設(shè)備啟動并且用戶第一次輸入密碼,而且不可以轉(zhuǎn)移到其他設(shè)備
kSecAttrAccessibleAlwaysThisDeviceOnly //keychain未受保護,任何時候都可以訪問,但是不能轉(zhuǎn)移到其他設(shè)備
應(yīng)用實例
把一段信息infoStrng字符串寫進文件,然后通過Data Protection API設(shè)置保護。
NSString *documentsPath =[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString *filePath = [documentsPath stringByAppendingPathComponent:@"DataProtect"];
[infoString writeToFile:filePath
atomically:YES
encoding:NSUTF8StringEncoding
error:nil];
NSDictionary *attributes = [NSDictionary dictionaryWithObject:NSFileProtectionComplete
forKey:NSFileProtectionKey];
[[NSFileManager defaultManager] setAttributes:attributes
ofItemAtPath:filePath
error:nil];
設(shè)備鎖屏(帶密碼保護)后,即使是越獄機,在root權(quán)限下cat讀取那個文件信息也會被拒絕。
七、越獄檢測
在應(yīng)用開發(fā)過程中,我們希望知道設(shè)備是否越獄,正以什么權(quán)限運行程序,好對應(yīng)采取一些防御和安全提示措施。
iOS7相比之前版本的系統(tǒng)而言,升級了沙盒機制,封鎖了幾乎全部應(yīng)用沙盒可以共享數(shù)據(jù)的入口。即使在越獄情況下,限制也非常多,大大增加了應(yīng)用層攻擊難度。比如,在iOS7之前,我們可以嘗試往沙盒外寫文件判斷是否越獄,但iOS7越獄后也無該權(quán)限,還使用老方法檢測會導致誤判。
那么,到底應(yīng)該如何檢測越獄呢?攻擊者又會如果攻破檢測呢?本文就著重討論一下越獄檢測的攻與防。
首先,你可以嘗試使用NSFileManager判斷設(shè)備是否安裝了如下越獄常用工具:
/Applications/Cydia.app
/Library/MobileSubstrate/MobileSubstrate.dylib
/bin/bash
/usr/sbin/sshd
/etc/apt
但是不要寫成BOOL開關(guān)方法,給攻擊者直接鎖定目標hook繞過的機會。
+(BOOL)isJailbroken{
if ([[NSFileManager defaultManager] fileExistsAtPath:@"/Applications/Cydia.app"]){
return YES;
}
// ...
}
攻擊者可能會改變這些工具的安裝路徑,躲過你的判斷。
那么,你可以嘗試打開cydia應(yīng)用注冊的URL scheme:
if([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"cydia://package/com.example.package"]]){
NSLog(@"Device is jailbroken");
}
但是不是所有的工具都會注冊URL scheme,而且攻擊者可以修改任何應(yīng)用的URL scheme。
那么,你可以嘗試讀取下應(yīng)用列表,看看有無權(quán)限獲取:
if ([[NSFileManager defaultManager] fileExistsAtPath:@"/User/Applications/"]){
NSLog(@"Device is jailbroken");
NSArray *applist = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:@"/User/Applications/"
error:nil];
NSLog(@"applist = %@",applist);
}
越了獄的設(shè)備是可以獲取到的:
攻擊者可能會hook NSFileManager 的方法,讓你的想法不能如愿。
那么,你可以回避 NSFileManager,使用stat系列函數(shù)檢測Cydia等工具:
#import <sys/stat.h>
void checkCydia(void)
{
struct stat stat_info;
if (0 == stat("/Applications/Cydia.app", &stat_info)) {
NSLog(@"Device is jailbroken");
}
}
攻擊者可能會利用 Fishhook原理 hook了stat。
那么,你可以看看stat是不是出自系統(tǒng)庫,有沒有被攻擊者換掉:
#import <dlfcn.h>
void checkInject(void)
{
int ret ;
Dl_info dylib_info;
int (*func_stat)(const charchar *, struct stat *) = stat;
if ((ret = dladdr(func_stat, &dylib_info))) {
NSLog(@"lib :%s", dylib_info.dli_fname);
}
}
如果結(jié)果不是 /usr/lib/system/libsystem_kernel.dylib 的話,那就100%被攻擊了。
如果 libsystem_kernel.dylib 都是被攻擊者替換掉的……
那也沒什么可防的大哥你隨便吧……
那么,你可能會想,我該檢索一下自己的應(yīng)用程序是否被鏈接了異常動態(tài)庫。
列出所有已鏈接的動態(tài)庫:
#import <mach-o/dyld.h>
void checkDylibs(void)
{
uint32_t count = _dyld_image_count();
for (uint32_t i = 0 ; i < count; ++i) {
NSString *name = [[NSString alloc]initWithUTF8String:_dyld_get_image_name(i)];
NSLog(@"--%@", name);
}
}
通常情況下,會包含越獄機的輸出結(jié)果會包含字符串: Library/MobileSubstrate/MobileSubstrate.dylib 。
攻擊者可能會給MobileSubstrate改名,但是原理都是通過DYLD_INSERT_LIBRARIES注入動態(tài)庫。
那么,你可以通過檢測當前程序運行的環(huán)境變量:
void printEnv(void)
{
charchar *env = getenv("DYLD_INSERT_LIBRARIES");
NSLog(@"%s", env);
}
未越獄設(shè)備返回結(jié)果是null,越獄設(shè)備就各有各的精彩了,尤其是老一點的iOS版本越獄環(huán)境。