ios ZipperDown 漏洞
看了网易的新闻,得知很多app有ZipperDown安全隐患,故了解了一下。仅供参考,望指正。
漏洞的攻击原理。
- 漏洞与zip文件有关,ios没有提供官方的unzip API函数,基本上现有的ios App都是使用的SSZipArchive或ziparchive这两个第三方库来实现解压的功能。使用第三方zip库在解压zip文件过程中没有考虑文件名中带有”../../”这样的情况,从而产生了目录穿越漏洞。因此,如果一个iOS 应用下载了恶意的zip文件,并且使用ziparchive库解压,利用漏洞可以做到app container目录下的任意文件覆盖,如果覆盖了应用重要的文件会造成应用崩溃(DOS),如果覆盖了app的hotpatch文件则会造成代码执行。
- 攻击条件:
1.APP用了ZipArchive
2.原APP下发的某个zip包传输过程没加密,zip包也没加密
3.原APP使用了JSPatch或其他执行引擎,且本地脚本没有加密,只要把脚本放指定目录即可执行
4.用户连上第三方wifi遭受攻击。
漏洞原理
ZipperDown漏洞并非iOS平台自身问题,而是与Zip文件解压有关。iOS平台没有提供官方的unzipAPI函数,而是引用了第三方库来实现解压功能,由于现有的iOS App基本上采用SSZipArchive或Ziparchive来实现解压,因此漏洞是来自使用第三方Zip库解压Zip文件的过程中没有对Zip内文件名做校验导致的。如果文件名中含有“../”则可以实现目录的上一级跳转,从而实现应用内任意目录的跳转,进一步可以实现文件覆盖,如果把App的hotpatch文件覆盖替换了,可以达到执行黑客指定指令,从而按照黑客的意图实现任意应用内攻击。
这个漏洞不禁让易盾联想到不久前Android平台上的unZip解压文件漏洞,和这个漏洞几乎是完全一样,只是平台和第三方解压库不同而已。Android平台上的被称为unZip解压文件漏洞,网易云易盾安全检测平台已经可以实现扫描检测。
- ------- SSZipArchive ---------------
压缩文件是允许路径指向类似../A/../B这种格式的, UNIX下../代表这个文件夹的上一层。比如说/A/B/../C实际上指的是/A/C
有问题的解压库没有对这种../做过滤,也就是说可以往解压路径外的地方解压文件。这不是一个系统级的沙盒逃逸漏洞
很多App会把所谓的热更新补丁放在沙盒内的某个路径下,比如说我们叫Documents/A.js吧,如果热更新的传输过程有中间人攻击的问题或者app被通过某种方式打开恶意的压缩包, 攻击者就可以覆盖A.js的内容,这样下次app启动加载热更新补丁A.js时就会执行恶意代码。
SSZipArchive
我们在开发app的时候,有时会需要对文件进行压缩和解压的操作,比如百度网盘,这个时候我们就必须要用到一个第三方的开源库,SSZipArchive ,来对目标文件进行压缩和解压的操作。
// Unzip 解压
/**
* @param path 源文件
* @param destination 目的文件
* @param uniqueId 标记,用于区别多个解压操作
*
* @return 返回 YES 表示成功,返回 NO 表示解压失败。
*/
+ (BOOL)unzipFileAtPath:(NSString *)path toDestination:(NSString *)destination uniqueId:(NSString *)uniqueId;
/**
* @param path 源文件
* @param destination 目的文件
* @param overwrite YES 会覆盖 destination 路径下的同名文件,NO 则不会。
* @param password 需要输入密码的才能解压的压缩包
* @param error 返回解压时遇到的错误信息
* @param uniqueId 标记,用于区别多个解压操作
*
* @return 返回 YES 表示成功,返回 NO 表示解压失败。
*/
+ (BOOL)unzipFileAtPath:(NSString *)path toDestination:(NSString *)destination overwrite:(BOOL)overwrite password:(NSString *)password error:(NSError **)error uniqueId:(NSString *)uniqueId;
/**
* @param path 源文件
* @param destination 目的文件
* @param delegate 设置代理
* @param uniqueId 标记,用于区别多个解压操作
*
* @return 返回 YES 表示成功,返回 NO 表示解压失败。
*/
+ (BOOL)unzipFileAtPath:(NSString *)path toDestination:(NSString *)destination delegate:(id<SSZipArchiveDelegate>)delegate uniqueId:(NSString *)uniqueId;
/**
* @param path 源文件
* @param destination 目的文件
* @param overwrite YES 会覆盖 destination 路径下的同名文件,NO 则不会。
* @param password 需要输入密码的才能解压的压缩包
* @param error 返回解压时遇到的错误信息
* @param delegate 设置代理
* @param uniqueId 标记,用于区别多个解压操作
*
* @return 返回 YES 表示成功,返回 NO 表示解压失败。
*/
+ (BOOL)unzipFileAtPath:(NSString *)path toDestination:(NSString *)destination overwrite:(BOOL)overwrite password:(NSString *)password error:(NSError **)error delegate:(id<SSZipArchiveDelegate>)delegate uniqueId:(NSString *)uniqueId;
/*
* 解压
*/
+ (BOOL)unzipFileAtPath:(NSString *)path
toDestination:(NSString *)destination
preserveAttributes:(BOOL)preserveAttributes
overwrite:(BOOL)overwrite
nestedZipLevel:(NSInteger)nestedZipLevel
password:(nullable NSString *)password
error:(NSError **)error
delegate:(nullable id<SSZipArchiveDelegate>)delegate
progressHandler:(void (^_Nullable)(NSString *entry, unz_file_info zipInfo, long entryNumber, long total))progressHandler
completionHandler:(void (^_Nullable)(NSString *path, BOOL succeeded, NSError * _Nullable error))completionHandler;
// Zip 压缩
/**
* @param path 目的路径(格式:~/xxx.zip 结尾的路径)
* @param filenames 要压缩的文件路径
*
* @return 返回 YES 表示成功,返回 NO 表示压缩失败。
*/
+ (BOOL)createZipFileAtPath:(NSString *)path withFilesAtPaths:(NSArray *)filenames;
/**
* @param path 目的路径(格式:~/xxx.zip 结尾的路径)
* @param filenames 要压缩的文件目录路径
*
* @return 返回 YES 表示成功,返回 NO 表示压缩失败。
*/
+ (BOOL)createZipFileAtPath:(NSString *)path withContentsOfDirectory:(NSString *)directoryPath;
/**
* 初始化压缩对象
*
* @param path 目的路径(格式:~/xxx.zip 结尾的路径)
*
* @return 初始化后的对像
*/
- (id)initWithPath:(NSString *)path;
/**
* 打开压缩对象
* @return 返回 YES 表示成功,返回 NO 表示失败。
*/
- (BOOL)open;
/**
* 添加要压缩的文件的路径
*
* @param path 文件路径
*
* @return 返回 YES 表示成功,返回 NO 表示失败。
*/
- (BOOL)writeFile:(NSString *)path;
/**
* 向此路径的文件里写入数据
*
* @param data 要写入的数据
* @param filename 文件路径
*
* @return 返回 YES 表示成功,返回 NO 表示失败。
*/
- (BOOL)writeData:(NSData *)data filename:(NSString *)filename;
/**
* 关闭压缩对象
* @return 返回 YES 表示成功,返回 NO 表示失败。
*/
- (BOOL)close;
@optional
//将要解压
- (void)zipArchiveWillUnzipArchiveAtPath:(NSString *)path zipInfo:(unz_global_info)zipInfo;
//解压完成
- (void)zipArchiveDidUnzipArchiveAtPath:(NSString *)path zipInfo:(unz_global_info)zipInfo unzippedPath:(NSString *)unzippedPat uniqueId:(NSString *)uniqueId;
//将要解压
- (void)zipArchiveWillUnzipFileAtIndex:(NSInteger)fileIndex totalFiles:(NSInteger)totalFiles archivePath:(NSString *)archivePath fileInfo:(unz_file_info)fileInfo;
//解压完成
- (void)zipArchiveDidUnzipFileAtIndex:(NSInteger)fileIndex totalFiles:(NSInteger)totalFiles archivePath:(NSString *)archivePath fileInfo:(unz_file_info)fileInfo;
@end
当对文件进行解压的时候,如果文件名包含了../,则可以实现目录上一级跳转,从而实现应用内任意目录的跳转,进一步可以实现文件覆盖。
例如: 有一个文件名为wzg/../wwww.zip,在对该文件下载之后进行解压,如果正常情况下,为下图的第一种情况,解压的test.txt文件会在C文件位置解压出来,如果出现不正确的路径,如下图的第二种情况,解压的test.txt文件会在C的上面一级文件夹路径A中解压出来。
而在三方SSZipArchive中,底层实现如下:
#pragma mark - ================解压===unzipFileAtPath=============================
+ (BOOL)unzipFileAtPath:(NSString *)path
toDestination:(NSString *)destination
preserveAttributes:(BOOL)preserveAttributes
overwrite:(BOOL)overwrite
nestedZipLevel:(NSInteger)nestedZipLevel
password:(nullable NSString *)password
error:(NSError **)error
delegate:(nullable id<SSZipArchiveDelegate>)delegate
progressHandler:(void (^_Nullable)(NSString *entry, unz_file_info zipInfo, long entryNumber, long total))progressHandler
completionHandler:(void (^_Nullable)(NSString *path, BOOL succeeded, NSError * _Nullable error))completionHandler
{
//==============核心代码=================
unzGetCurrentFileInfo(zip, &fileInfo, filename, fileInfo.size_filename + 1, NULL, 0, NULL, 0);
filename[fileInfo.size_filename] = '\0';
BOOL fileIsSymbolicLink = _fileIsSymbolicLink(&fileInfo);
#pragma mark ------------------核查文件的路径是否包含不规范字符---------------------------------
NSString * strPath = [SSZipArchive _filenameStringWithCString:filename size:fileInfo.size_filename];
if ([strPath hasPrefix:@"__MACOSX/"]) {
// ignoring resource forks: https://superuser.com/questions/104500/what-is-macosx-folder
unzCloseCurrentFile(zip);
ret = unzGoToNextFile(zip);
continue;
}
if (!strPath.length) {
// if filename data is unsalvageable, we default to currentFileNumber
strPath = @(currentFileNumber).stringValue;
}
// Check if it contains directory
BOOL isDirectory = NO;
if (filename[fileInfo.size_filename-1] == '/' || filename[fileInfo.size_filename-1] == '\\') {
isDirectory = YES;
}
free(filename);
// Contains a path
if ([strPath rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@"/\\"]].location != NSNotFound) {
strPath = [strPath stringByReplacingOccurrencesOfString:@"\\" withString:@"/"];
}
NSString *fullPath = [destination stringByAppendingPathComponent:strPath];
NSError *err = nil;
NSDictionary *directoryAttr;
if (preserveAttributes) {
NSDate *modDate = [[self class] _dateWithMSDOSFormat:(UInt32)fileInfo.dos_date];
directoryAttr = @{NSFileCreationDate: modDate, NSFileModificationDate: modDate};
[directoriesModificationDates addObject: @{@"path": fullPath, @"modDate": modDate}];
}
#pragma mark ----------------解压路径---------------------------------
//如果是一个沙盒路径, 就创建这个路径,将解压的 东西放到这个路径下
//如果不是则创建一个路径,
/*
stringByDeletingLastPathComponent一个新的字符串由来自接收者的组件删除最后一个路径,以及最终的路径分隔符。
Receiver’s String Value Resulting String
“/tmp/scratch.tiff” “/tmp”
“/tmp/lock/” “/tmp”
“/tmp/” “/”
“/tmp” “/”
“/” “/”
“scratch.tiff” “” (an empty string)
*/
NSLog(@"-------dir-------%@", fullPath);
if (isDirectory) {
[fileManager createDirectoryAtPath:fullPath withIntermediateDirectories:YES attributes:directoryAttr error:&err];
} else {
[fileManager createDirectoryAtPath:fullPath.stringByDeletingLastPathComponent withIntermediateDirectories:YES attributes:directoryAttr error:&err];
}
NSLog(@"-------dir--2222-----%@", fullPath.stringByDeletingLastPathComponent);
if (nil != err) {
if ([err.domain isEqualToString:NSCocoaErrorDomain] &&
err.code == 640) {
unzippingError = err;
unzCloseCurrentFile(zip);
success = NO;
break;
}
NSLog(@"[SSZipArchive] Error: %@", err.localizedDescription);
}
if ([fileManager fileExistsAtPath:fullPath] && !isDirectory && !overwrite) {
//FIXME: couldBe CRC Check?
unzCloseCurrentFile(zip);
ret = unzGoToNextFile(zip);
continue;
}
if (!fileIsSymbolicLink) {
// ensure we are not creating stale file entries
//确保我们没有创建的文件条目
int readBytes = unzReadCurrentFile(zip, buffer, 4096);
if (readBytes >= 0) {
FILE *fp = fopen(fullPath.fileSystemRepresentation, "wb");
while (fp) {
if (readBytes > 0) {
if (0 == fwrite(buffer, readBytes, 1, fp)) {
if (ferror(fp)) {
NSString *message = [NSString stringWithFormat:@"Failed to write file (check your free space)"];
NSLog(@"[SSZipArchive] %@", message);
success = NO;
unzippingError = [NSError errorWithDomain:@"SSZipArchiveErrorDomain" code:SSZipArchiveErrorCodeFailedToWriteFile userInfo:@{NSLocalizedDescriptionKey: message}];
break;
}
}
} else {
break;
}
readBytes = unzReadCurrentFile(zip, buffer, 4096);
if (readBytes < 0) {
// Let's assume error Z_DATA_ERROR is caused by an invalid password
// Let's assume other errors are caused by Content Not Readable
success = NO;
}
}
if (fp) {
//关闭文件
fclose(fp);
if (nestedZipLevel
&& [fullPath.pathExtension.lowercaseString isEqualToString:@"zip"]
&& [self unzipFileAtPath:fullPath
toDestination:fullPath.stringByDeletingLastPathComponent
preserveAttributes:preserveAttributes
overwrite:overwrite
nestedZipLevel:nestedZipLevel - 1
password:password
error:nil
delegate:nil
progressHandler:nil
completionHandler:nil]) {
[directoriesModificationDates removeLastObject];
[[NSFileManager defaultManager] removeItemAtPath:fullPath error:nil];
} else if (preserveAttributes) {
// Set the original datetime property
if (fileInfo.dos_date != 0) {
NSDate *orgDate = [[self class] _dateWithMSDOSFormat:(UInt32)fileInfo.dos_date];
NSDictionary *attr = @{NSFileModificationDate: orgDate};
if (attr) {
if (![fileManager setAttributes:attr ofItemAtPath:fullPath error:nil]) {
// Can't set attributes
NSLog(@"[SSZipArchive] Failed to set attributes - whilst setting modification date");
}
}
}
// Set the original permissions on the file (+read/write to solve #293)
uLong permissions = fileInfo.external_fa >> 16 | 0b110000000;
if (permissions != 0) {
// Store it into a NSNumber
NSNumber *permissionsValue = @(permissions);
// Retrieve any existing attributes
NSMutableDictionary *attrs = [[NSMutableDictionary alloc] initWithDictionary:[fileManager attributesOfItemAtPath:fullPath error:nil]];
// Set the value in the attributes dict
attrs[NSFilePosixPermissions] = permissionsValue;
// Update attributes
if (![fileManager setAttributes:attrs ofItemAtPath:fullPath error:nil]) {
// Unable to set the permissions attribute
NSLog(@"[SSZipArchive] Failed to set attributes - whilst setting permissions");
}
}
}
}
else
{
// if we couldn't open file descriptor we can validate global errno to see the reason
if (errno == ENOSPC) {
NSError *enospcError = [NSError errorWithDomain:NSPOSIXErrorDomain
code:ENOSPC
userInfo:nil];
unzippingError = enospcError;
unzCloseCurrentFile(zip);
success = NO;
break;
}
}
} else {
// Let's assume error Z_DATA_ERROR is caused by an invalid password
// Let's assume other errors are caused by Content Not Readable
success = NO;
break;
}
}
else
{
// Assemble the path for the symbolic link
NSMutableString *destinationPath = [NSMutableString string];
int bytesRead = 0;
while ((bytesRead = unzReadCurrentFile(zip, buffer, 4096)) > 0)
{
buffer[bytesRead] = 0;
[destinationPath appendString:@((const char *)buffer)];
}
if (bytesRead < 0) {
// Let's assume error Z_DATA_ERROR is caused by an invalid password
// Let's assume other errors are caused by Content Not Readable
success = NO;
break;
}
分析解压代码,可以知道,在进行解压的时候,三方库是有对解压的文件夹名字进行做路径判断和处理,
- 过滤了包含__MACOSX/,如果有路径有以__MACOSX/开头,则直接关闭当前文件路径,
- 如果出现\则将\替换为/,
- 如果路径不是一个正确的沙盒路径,则使用fullPath.stringByDeletingLastPathComponent进行处理,参考官方文档如下:
fullPath 处理后的fullPath
“/tmp/scratch.tiff” “/tmp”
“/tmp/lock/” “/tmp”
“/tmp/” “/”
“/tmp” “/”
“/” “/”
“scratch.tiff” “” (an empty string)
但是三方库中并没有在解压的方法中处理出现../这种情况,
最完整的解决方案是对SSZipArchive库进行修补,在解压函数:
- (BOOL)unzipFileAtPath:(NSString *)path toDestination:(NSString *)destination preserveAttributes:(BOOL)preserveAttributes overwrite:(BOOL)overwrite nestedZipLevel:(NSInteger)nestedZipLevel password:(nullable NSString *)password error:(NSError **)error delegate:(nullable id<SSZipArchiveDelegate>)delegate progressHandler:(void (^_Nullable)(NSString *entry, unz_file_info zipInfo, long entryNumber, long total))progressHandler completionHandler:(void (^_Nullable)(NSString *path, BOOL succeeded, NSError * _Nullable error))completionHandler
中对最终解压的strPath进行检测,如果出现可能造成目录穿越的”../”字符串时进行拦截。
-
-------- JSPath 热更新 ---------------
JSPath 是一个ios动态更新框架,它可以让JS调用或者替换任意OC的方法,让ios App具备热更新的能力。因为OC是动态语言,所以OC上所有方法的调用或者类的生成都是通过OC的Runtime 在运行时进行的,JS传递字符串给OC,OC通过Runtime接口调用和替换OC方法。
JSPath 方法调用是借助JavaScriptCore将类名字符串和方法名字符串传递给OC,由OC借助Runtime来反射出类和方法来调用,例如创建一个UIView实例
js端 UIView.alloc() ---------> OC端 [UIView alloc]
image.png
js的方法调用规则是必须对已经存在的对象调用已经存在的方法,构建对象,就是在调用方法前使用require函数为每一个类在js中构建同名全局对象,源码如下:
var _require = function(clsName) {
if (!global[clsName]) {
global[clsName] = {
__clsName: clsName
}
}
return global[clsName]
}
global.require = function(clsNames) {
var lastRequire
clsNames.split(',').forEach(function(clsName) {
lastRequire = _require(clsName.trim())
})
return lastRequire
}
js构建函数,为了避免内存消耗,只定义了一个元函数,在元函数中将类名、方法名以及参数传递给OC,js脚本代码在交由JavaScriptCore执行前,是先经过转换的,所有的方法调用都被转换成了调用__c函数,js源码的这样转换是通过正则匹配替换的,核心代码如下:
NSString *formatedScript = [NSString stringWithFormat:@";(function(){try{\n%@\n}catch(e){_OC_catch(e.message, e.stack)}})();",[_regex stringByReplacingMatchesInString:script options:0 range:NSMakeRange(0, script.length) withTemplate:_replaceStr]];
替换后的JS代码 ps:所有的方法调用都被替换成了__c函数调用并将方法名作为参数传入:
;(function(){try{
require('UIColor,UIImage');
defineClass('CustomCell', {
configWithModel: function(model) {
self.__c("headView")().__c("layer")().__c("setCornerRadius")(5.0);
self.__c("headView")().__c("layer")().__c("setBorderColor")(UIColor.__c("darkGrayColor")().__c("CGColor")());
self.__c("headView")().__c("layer")().__c("setBorderWidth")(1.0);
self.__c("headView")().__c("layer")().__c("setMasksToBounds")(YES);
self.__c("headView")().__c("setImage")(UIImage.__c("imageNamed")(model.__c("imgPath")()));
self.__c("contentLabel")().__c("setText")(model.__c("content")());
self.__c("contentLabel")().__c("setNumberOfLines")(0);
},
});
JS将消息传递给OC,内部实现是根据实例方法或者类方法调用了_OC_callI和_OC_callC中的其中一个,而这两个函数在初始化JPEnige的时候就已经注册到JS上下文了,这是JavaScriptCore的接口,在JS上下文中创建JS函数。当函数被调用,会将消息传递给OC端,同时将参数传递给OC,OC执行相应的block,最后将返回值回传JS,其实js传递消息给oc,是借助于JavaScriptCore。OC从JS端接收了消息,需要调用指定方法。JSPatch在处理的时候是通过NSInvocation来调用的,这是因为:JS传过来的参数类型需要转换成OC相应的类型,而NSInvocation很方便从方法签名中获取方法参数类型。同时,也能根据返回值类型取出返回值。
ps:iOS中可以直接调用 某个对象的消息 方式有2种,一种是performSelector:withObject:另一种是NSInvocation,NSInvocation也是一种消息调用的方法,并且它的参数没有限制,可以处理参数、返回值等相对复杂的操作。
JSPatch通过下发JS脚本文件对app进行修复或更新,JS脚本的权限是很大的,如果在下发传输过程中文件被第三方截获,可以修改了脚本内容,故在使用时需对脚本文件进行加密处理。对脚本文件加密主要有以下方案:
a、可以使用对称加密,服务器端和客户端保存一把相同的私钥,下发脚本文件前先对文件进行加密,客户端拿到脚本文件后用相同的私钥解密。这种方案弊端很明显,密钥保存在客户端,一旦客户端被破解,密钥就泄露了。
b、https传输。不过需要购买证书,部署服务器,这种方案也比较安全可靠。
c、RSA签名验证。通过RSA非对称加密,此时需要服务器端,对要下发的脚本文件计算MD5值,用服务器私钥对MD5只进行加密,将脚本文件和加密后的MD5值下发给客户端,而客户端,需要用服务器端公钥解密加密过的MD5值,对接受的脚本文件计算MD5值,将解密出来的MD5与新计算出来的MD5进行比对校验,如果校验通过,则表明脚本在传输过程中没有被篡改。
-
-------- 梳理整个漏洞攻击的流程如下 ---------------
在启动app的时候,有时会执行js的脚本,即加载自己目录下的/Library/Caches/A/B/patch.js并执行,当我们在使用app时,可能会有通过http下载一个zip包到本地,并使用SSZipArchive库进行解压,如果下载的zip文件含有../则可以实现应用内目录上的跳转,如果将应用内其他文件替换掉,再次运行时,会造成程序奔溃,如果刚好把js文件替换掉,则再次运行app时,会执行替换的js文件,执行下载的js内容。
大致流程如下
image.png
如何来检测ZipperDown漏洞?
通过指纹匹配可以获取疑似受影响的应用列表。但该漏洞形态灵活、变种类型多样,指纹匹配的漏报率很高。所以我们建议通过人工分析的方式确认漏洞是否存在。
ZipperDown漏洞如何触发?
ZipperDown漏洞攻击场景与受影响应用业务场景相关。常见攻击场景包括:在不安全网络环境下使用受影响应用、在攻击者诱导下使用某些应用功能等。
对漏洞进行安全防范
针对 iOS 应用的 ZipperDown 漏洞,对IOS 应用可以进行一下的几点防守策略。
-
数据库文件安全
开发中会使用SQLite数据库来存储应用数据,而数据库本身一般存储在沙盒文件中,如果数据库里面存储的数据没有进行复杂的加密处理,会是应用程序有敏感信息泄漏的风险,同时也有助于攻击者进行逆向分析。
安全实施方案: 使用较复杂的加密加盐算法对敏感数据加密后存储。 -
NSUserDefaults 安全
保存在 NSUserDefaults 中的信息在应用关闭后再次打开依然存在。且保存到 NSUserDefautls 中的数据是没有加密的,可以很轻易地从沙盒中找到。NSUserDefautls 被存储在一个以应用 Bundle ID 为名称的 Plist 文件中。
安全实施方案:重要的敏感数据存储在 Keychain 中。 -
Keychain 安全
Keychain是一个安全的存储容器,它是一个SQlite数据库,位于 /private/var/Keychains/keychain-2.db,其保存的所有数据都是加密过的。Keychain 在沙盒之外 App 会将部分重要数据存放在 Keychain 中使用进行读取,但如果写入后未清除就卸载App,则可能会导致下次安全的时候直接从KeyChain中读取密码登录或手势密码无法解除等问题。
安全实施方案:首次安装应用程序启动后,进行删除keychain数据操作。 -
HTTPS 安全
在使用HTTPS时, 也有可能因为没有校验服务器证书的原因导致被劫持,如果交互请求数据处理不当,攻击者可以解密得到明文通信数据;甚至进一步伪造 App 的请求,这是极大的安全隐患。
安全措施:
a. App内对HTTPS 进行证书做校验
b.避免使用有漏洞的三方库
c. 重要的数据,单独加密 -
WebView安全
WebView 跨平台、动态等特性被广泛使用,同时在ios终端上,WebView可以注册一些敏感信息数据,比如发短信,付款,定位信息等,也会有安全风险。
安全实施方法
a.对输入进行过滤和编码
b.服务端对App发送的数据进行过滤和编码
c. 尽量减少敏感接口的注册,尽量不要加载第三方内容,如果需要加载,则必须在WebView 的Delegate方法中,通过白名单机制实现对调用者的检验 -
加密算法
1.对称加密算法要使用AES,DES 或3DES,避免使用RC4 等目前可能被爆破的算法
2.对于数据安全性要求较高的地方,使用非对称加密算法如RSA等
3.对称加密算法的KEY和IV,应避免硬编码,如果加密数据仅是本地存储,可基于设备相关的信息来生成KEY 和 IV