iOS应用安全
攻易防难,唯有缜密、多层的防护网络才能可靠的保护我们iOS应用程序的安全。那么,一个完善的iOS应用安全防护框架都要写哪些东西呢? 首先,先梳理一下常见的逆向及攻击工具。
iOS应用逆向常用工具
- Reveal
- Cycript
- Class-dump
- Keychain-Dumper
- gdb
- iNalyzer
- introspy
- Fishhook
- removePIE
- IDA pro or Hopper
- snoop-it
- iDB
- Charles
- SSL Kill Switch
裸奔app的安全隐患
一部越狱的iOS设备,外加上述的逆向工具,给裸奔的iOS应用程序带来哪些威胁呢?
- 任意读写文件系统数据
- HTTP(S)实时被监测
- 重新打包ipa
- 暴露的函数符号
- 未加密的静态字符
- 篡改程序逻辑控制流
- 拦截系统框架API
- 逆向加密逻辑
- 跟踪函数调用过程(objc_msgSend)
- 可见视图的具体实现
- 伪造设备标识
- 可用的URL schemes
- runtime任意方法调用
- ……
iOS应用安全防护开源工具
ios-class-guard 是对抗class-dump的利器,作用是将ObjC类名方法名等重命名为难以理解的字符。
iOS应用安全防护框架概述
针对上述安全隐患,我们的iOS应用安全防护框架需实现的任务大致如下:
-
防护
- ObjC类名方法名等重命名为难以理解的字符
- 加密静态字符串运行时解密
- 混淆代码使其难于反汇编
- 本地存储文件防篡改
-
检测
- 调试状态检测
- 越狱环境检测
- ObjC的Swizzle检测
- 任意函数的hook检测
- 指定区域或数据段的校验和检测
-
自修复
- 自修复被篡改的数据和代码段
此外,还需要多层的防护,通过高层保护低层的方式来保证整个防护机制不失效。
常用的防护手段
一、加密静态字符串运行时解密
一个编译成功的可执行程序,其中已初始化的字符串都是完整可见的。 针对于iOS的Mach-O二进制通常可获得以下几种字符串信息:
- 资源文件名
- 可见的函数符号名
- SQL语句
- format
- 通知名
- 加密算法的key
攻击者如何利用字符串
资源文件名通常用来快速定位逆向分析的入口点。 想要知道判断购买金币成功与否的代码位置?只要确定购买成功时播放的音频文件名字或者背景图名字就可以顺藤摸瓜了。
kLoginSuccessNotification类似这种通知名称格外炸眼,利用Cycript发个此通知试试,也许会有什么意外收获。 拿到对称加密算法的key是件很幸福的事情。
字符串异或加解密
是的,字符串需要加密处理,但只需要对高度敏感字符数据做加密,比如对称加密算法的key。 其他的,需要提高编程安全意识来弥补。
常规办法是通过异或来加解密,来写个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是大多数hackers的首选,阻止GDB依附到应用的常规办法是:
#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真实的运行环境是没有sys/ptrace.h抛出的。虽然 ptrace 方法没有被抛出, 但是不用担心,我们可以通过dlopen拿到它。
dlopen: 当path 参数为0是,他会自动查找 DYLD_LIBRARY_PATH, $DYLD_FALLBACK_LIBRARY_PATH 和 当前工作目录中的动态链接库。
#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]));
}
}
三、自定义安全键盘
大部分中文应用弹出的默认键盘是简体中文输入法键盘,在输入用户名和密码的时候,如果使用简体中文输入法键盘,输入英文字符和数字字符的用户名和密码时,会自动启动系统输入法自动更正提示,然后用户的输入记录会被缓存下来。
系统键盘缓存最方便拿到的就是利用系统输入法自动更正的字符串输入记录。
缓存文件的地址是:/private/var/mobile/Library/Keyboard/dynamic-text.dat
导出该缓存文件,查看内容,欣喜的发现一切输入记录都是明文存储的。因为系统不会把所有的用户输入记录都当作密码等敏感信息来处理。
一般情况下,一个常规iPhone用户的dynamic-text.dat文件,高频率出现的字符串就是用户名和密码。
所以,一般银行客户端app输入密码时都不使用系统键盘,而使用自己定制的键盘,原因主要有2个:
1)避免第三方读取系统键盘缓存
2)防止屏幕录制 (自己定制的键盘按键不加按下效果)
那么,如何实现自定义安全键盘呢?大致思路如下:
1)首先捕获系统键盘的弹出、收回通知
2)创建一个更高级别的window挡住系统键盘
3)需要抛出一个 id<UITextInput>textInput 的弱引用切换焦点。
四、二进制和资源文件自检
我们把自己的程序发布到app store,但是不能保证每一个用户都是从app store下载官方app,也不能保证每一个用户都不越狱。
换句话说,我们无法保证程序运行环境在MAC管控策略下就绝对的安全。
所以,在有些情况下,尤其是和钱有关系的app,我们有必要在和服务器通信时,让服务器知道客户端到底是不是官方正版的app。
何以判断自己是不是正版app呢?hackers们破解你的app,无非就2个地方可以动,1个是二进制,1个是资源文件。
二进制都重新编译过了自然肯定是盗版……
有些低级的hackers喜欢修改人家的资源文件然后贴上自己的广告,或者给用户错误的指引……修改资源文件是不需要重新编译二进制的。
因此,我们有必要在敏感的请求报文中,增加正版应用的二进制和资源文件的标识,让服务器知道,此请求是否来自正版的未经修改的app。
在沙盒中,我们可以读到自己程序的二进制,也可以读到资源文件签名文件,这两个文件都不算大,我们可以对其取md5值然后以某种组合算法得到一个标记字符串,然后发给服务器。
我封装了相关文件的读取地址
@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];
}
五、数据擦除
对于敏感数据,我们不希望长时间放在内存中,而希望使用完后立即就被释放掉。
但是不管是ARC还是MRC,自动释放池也有轮循工作周期,我们都无法控制内存数据被擦除的准确时间,让hackers们有机可乘。
本文介绍一个小技巧——及时数据擦除。
假如一个View Controller A的一个数据被绑在一个property上,
@interface WipingMemoryViewController : UIViewController
@property (nonatomic,copy) NSString *text;
@end
当A push到 另外一个View Controller B时,该数据还是有可能被读到的
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 =
可以看到,我们想要保护的数据,被有效的擦除了。
还有提个醒,如果是这样
_text = @"information";
创建的字符串,是会被分配到data区,而是无法修改的。
如果有兴趣也有闲心,可以试试运行下面的代码,有彩蛋哦:
_text = @"information";
memset((__bridge voidvoid *)(_text), 0, _text.length - 1);
NSString *myString = [[NSString alloc]initWithFormat:@"information"];
NSLog(@"Origal text : %@ \n",myString);
编译器把两个information的省略到一个地址了~
六、数据保护API
1) 文件保护
文件系统中的文件、keychain中的项,都是加密存储的。当用户解锁设备后,系统通过UDID密钥和用户设定的密码生成一个用于解密的密码密钥,存放在内存中,直到设备再次被锁,开发者可以通过Data Protection API 来设定文件系统中的文件、keychain中的项应该何时被解密。
/* 为filePath文件设置保护等级 */
NSDictionary *attributes = [NSDictionary dictionaryWithObject:NSFileProtectionComplete
forKey:NSFileProtectionKey];
[[NSFileManager defaultManager] setAttributes:attributes
ofItemAtPath:filePath
error:nil];
//文件保护等级属性列表
NSFileProtectionNone //文件未受保护,随时可以访问 (Default)
NSFileProtectionComplete //文件受到保护,而且只有在设备未被锁定时才可访问
NSFileProtectionCompleteUntilFirstUserAuthentication //文件收到保护,直到设备启动且用户第一次输入密码
NSFileProtectionCompleteUnlessOpen //文件受到保护,而且只有在设备未被锁定时才可打开,不过即便在设备被锁定时,已经打开的文件还是可以继续使用和写入
2) keychain项保护
/* 设置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项受到保护,只有在设备未被锁定时才可以访问
kSecAttrAccessibleAfterFirstUnlock //keychain项受到保护,直到设备启动并且用户第一次输入密码
kSecAttrAccessibleAlways //keychain未受保护,任何时候都可以访问 (Default)
kSecAttrAccessibleWhenUnlockedThisDeviceOnly //keychain项受到保护,只有在设备未被锁定时才可以访问,而且不可以转移到其他设备
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly //keychain项受到保护,直到设备启动并且用户第一次输入密码,而且不可以转移到其他设备
kSecAttrAccessibleAlwaysThisDeviceOnly //keychain未受保护,任何时候都可以访问,但是不能转移到其他设备
应用实例
把一段信息infoStrng字符串写进文件,然后通过Data Protection API设置保护。
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];
设备锁屏(带密码保护)后,即使是越狱机,在root权限下cat读取那个文件信息也会被拒绝。
七、越狱检测
在应用开发过程中,我们希望知道设备是否越狱,正以什么权限运行程序,好对应采取一些防御和安全提示措施。
iOS7相比之前版本的系统而言,升级了沙盒机制,封锁了几乎全部应用沙盒可以共享数据的入口。即使在越狱情况下,限制也非常多,大大增加了应用层攻击难度。比如,在iOS7之前,我们可以尝试往沙盒外写文件判断是否越狱,但iOS7越狱后也无该权限,还使用老方法检测会导致误判。
那么,到底应该如何检测越狱呢?攻击者又会如果攻破检测呢?本文就着重讨论一下越狱检测的攻与防。
首先,你可以尝试使用NSFileManager判断设备是否安装了如下越狱常用工具:
/Applications/Cydia.app
/Library/MobileSubstrate/MobileSubstrate.dylib
/bin/bash
/usr/sbin/sshd
/etc/apt
但是不要写成BOOL开关方法,给攻击者直接锁定目标hook绕过的机会。
+(BOOL)isJailbroken{
if ([[NSFileManager defaultManager] fileExistsAtPath:@"/Applications/Cydia.app"]){
return YES;
}
// ...
}
攻击者可能会改变这些工具的安装路径,躲过你的判断。
那么,你可以尝试打开cydia应用注册的URL scheme:
if([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"cydia://package/com.example.package"]]){
NSLog(@"Device is jailbroken");
}
但是不是所有的工具都会注册URL scheme,而且攻击者可以修改任何应用的URL scheme。
那么,你可以尝试读取下应用列表,看看有无权限获取:
if ([[NSFileManager defaultManager] fileExistsAtPath:@"/User/Applications/"]){
NSLog(@"Device is jailbroken");
NSArray *applist = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:@"/User/Applications/"
error:nil];
NSLog(@"applist = %@",applist);
}
越了狱的设备是可以获取到的:
攻击者可能会hook NSFileManager 的方法,让你的想法不能如愿。
那么,你可以回避 NSFileManager,使用stat系列函数检测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是不是出自系统库,有没有被攻击者换掉:
#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);
}
}
如果结果不是 /usr/lib/system/libsystem_kernel.dylib 的话,那就100%被攻击了。
如果 libsystem_kernel.dylib 都是被攻击者替换掉的……
那也没什么可防的大哥你随便吧……
那么,你可能会想,我该检索一下自己的应用程序是否被链接了异常动态库。
列出所有已链接的动态库:
#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);
}
}
通常情况下,会包含越狱机的输出结果会包含字符串: Library/MobileSubstrate/MobileSubstrate.dylib 。
攻击者可能会给MobileSubstrate改名,但是原理都是通过DYLD_INSERT_LIBRARIES注入动态库。
那么,你可以通过检测当前程序运行的环境变量:
void printEnv(void)
{
charchar *env = getenv("DYLD_INSERT_LIBRARIES");
NSLog(@"%s", env);
}
未越狱设备返回结果是null,越狱设备就各有各的精彩了,尤其是老一点的iOS版本越狱环境。