移动端相比服务器,更容易被进行攻击,因此我们有必要了解一些安全防护的知识,从而保障用户信息安全,我们主要从本地文件和数据
、源代码
、网络通讯
三部分进行防护
1、本地文件与数据安全
1.1、程序文件安全
隐患:
- iOS应用大部分逻辑都是在编译后的二进制文件中,但如果由于Hybrid等原因打包了js文件或其他文件,攻击者如果拿到iOS安装文件并解压,就能看到包内容,并根据js文件代码了解调用逻辑,在越狱手机上甚至可以修改js代码,从而进行攻击
安全方案:
- 通过
将js源码进行混淆和加密
,防止Hacker轻易阅读和篡改相关逻辑
1.2、本地数据安全
iOS应用的数据在本地通常保存在本地文件或本地数据库中,如果本地数据未进行加密处理,很可能被Hacker篡改(存储形式:Plist文件、SQLite数据库文件、Keychain文件、缓存文件、日志文件)
因此对于本地重要数据,我们应该加密存储或将其保存到keychain中,以保证其不被篡改
1.2.1、Plist文件
属性列表
(Plist,Property List)是一种结构化的二进制格式文件,包含了内嵌键值对的可执行bundle的基本配置信息
-
Plist文件 主要用于存储App的用户设置及配置信息,例如,游戏类App经常会在Plist文件中存储游戏等级和分数信息
- 比如你做了一款游戏,将剩余命数命名为leaveLifes存在本地plist文件中,那么就有可能被Hacker篡改成无限命
-
一般来说,App会将存储用户数据的Plist文件保存在
[App home目录]/documents/
目录下 -
Plist文件 可以是XML格式 或 二进制格式
隐患:
- Plist文件主要用于存储用户设置及App的配置信息,但App可能使用Plist文件存储明文的用户名、密码或其它一些个人敏感信息,而保存在Plist文件中的二进制格式文件数据则可以使用Plist文件编辑器(如plutil)进行查看或修改,即使在一个没有越狱的设备上,plist文件也可以通过工具iExplorer获取
安全方案:
- 尽量不要在iOS设备的Plist文件中保存敏感信息(如证件号、银行卡号、详细住址及其各对应的编码格式等)
- 对于有些APP功能需求,如果一定需要在iOS设备本地保存敏感信息,则 可采用iOS提供的加密接口(如CommonCrypto)进行安全加密后保存
1.2.2、SQLite存储
SQLite是一种 自包含、可嵌入、0配置 的SQL数据库引擎的跨平台C库文件;它的表、触发器和视图整个数据库都包含在一个硬盘文件中;SQLite数据库常见的后缀一般有.sqlitedb
和.db
,APP一般会将其保存在[App home目录]/documents/
目录下
隐患:
-
信息泄露:iOS自带的SQLite数据库没有内置的加密支持,因此,许多iOS APP会直接以明文格式将许多敏感数据存储在SQLite数据库中,除非APP自身对数据进行加密后再存储
例如银行APP将用户的登录手机号、登录密码、手势密码全部都以明文方式存在了客户端本地的SQLite数据库中,一旦可以物理访问到设备或其备份文件,存储在SQLite中未加密的敏感信息容易被泄露
-
数据恢复:在iOS中,恢复被删除的SQLite数据库记录比恢复被删除的文件更为容易,因为如果删除一条记录,SQLite仅会将该记录标记为已删除,但不会清除它们,只要SQLite数据库文件本身没被删除,数据库中被删除的记录则会一直保留在SQLite文件的未分配空间内,
直到新的记录覆盖它们
攻击者可以使用
strings命令
打印SQLite数据库文件中数据,这其中就包括了被删除的数据
安全方案:
- 最简单的方法就是尽量不在客户端的SQLite数据库中保存敏感信息
- 如果确实需要将某些敏感信息保存在SQLite数据库中时,可以结合使用以下几种方案:
数据加密
:使用如AES256加密算法对数据进行安全加密后再存入SQLite中整库加密
:可使用第三方的SQLite扩展库,对数据库进行整体的加密数据覆盖
:在删除SQLite数据库某条记录之前,可以使用垃圾数据update一下该条目,这样即使有人尝试从SQLite文件中恢复已删除的数据库时,他们也无法获取到实际的数据
1.2.3、键盘缓存
为提供自动填充和纠正的功能,iOS系统的自带键盘会缓存用户的输入信息,其会保存一个接近600个单词的列表,存放在Library/Keyboard/ en_GB-dynamic-text.dat
或/private/var/mobile/Library/Keyboard/dynamic-text.dat
文件中(iOS版本不同,位置及文件名会略有不同)
隐患:
- 这个功能会带来一个安全问题:它会明文存储用户在输入框中输入过的所有信息,如用户名、密码短语、安全问题回答等;要想查看该键盘缓存,可以将上述如 en_GB-dynamic-text.dat 文件复制到电脑上,并使用十六进制编辑器打开
安全方案:
- 对于以下位置或方式的输入,iOS不会对其输入内容进行缓存:
- 在 标记为secure的字段、passwords字段 内输入的内容不会缓存
- 输入 只包括数字的字符串不会被缓存,这也即意味着银行卡号、信用卡号是安全的(iOS 5之前的版本会缓存)
- 非常短的输入,如只有1或者2个字母组成的单词 不会被缓存
禁用了自动纠正功能的文本框
不会缓存输入的内容
-
在不需要缓存的文本框处禁用自动纠正功能,如下代码所示:
UITextField *textField = [[UITextField alloc] initWithFrame: frame]; textField.autocorrectionType = UITextAutocorrectionTypeNo; 复制代码
-
输入框也可以被标记为密码输入类型,使得输入变得更加安全,防止缓存:
textField.secureTextEntry = YES; 复制代码
-
在所有敏感信息输入处均使用自定义键盘,当然自定义键盘也不能缓存用户输入(这可能会影响用户体验)
-
除了文本输入的地方,在iOS系统上,当数据被复制到粘贴板上的时候,也会被进行明文缓存,而且粘贴板内容所有APP均可访问,为禁用文本框的复制/粘贴功能,使得用户无法在某些地方进行复制和粘贴,可在该文本输入的地方添加以下方法:
-(BOOL)canPerformAction:(SEL)action withSender:(id)sender { UIMenuController *menuController = [UIMenuController sharedMenuController]; if (menuController) { menuController.menuVisible = NO; } return NO; } 复制代码
1.2.4、应用快照缓存
当一个应用在后台被挂起时,iOS会生成一个当前屏幕的快照,当应用被重新唤起时,可以快速还原该APP之前的内容,以提高用户的使用体验
隐患:
- 应用快照保存在
/var/mobile/Containers/Data/Application/XXXXXXX-XXXXXXXXX-XXXXXXXXX/Library/Caches/Snapshots/
目录下,恶意APP可通过读取该文件并发送至远程服务端,从而获得其快照内容信息,其中就可能包含敏感信息
安全方案:
-
要防止这种信息泄露途径,屏幕内容就必须在iOS系统进行屏幕快照之前进行隐藏或模糊化处理,而iOS系统也提供了许多回调方法来提示程序将被挂起,例如以下两个方法:
// 应用程序将要入非活动状态执行,在此期间,应用程序不接收消息或事件 -(void)applicationWillResignActive:(UIApplication *)application // 程序将被推送到后台 -(void)applicationDidEnterBackground:(UIApplication *)application 复制代码
-
下图为APP压入后台的过程,右边则是可供APP回调的方法,可利用其实现自己的一些需求:
-
隐藏界面:设置关键窗口的hidden属性为YES,这样当前在屏幕上显示的内容将被隐藏,返回一个空白的快照来替代任何内容
[UIApplication sharedApplication].keyWindow.hidden = YES; 复制代码
注意:如果在当前窗口后面有其他的窗口,当关键窗口被隐藏时,那些窗口将会被显示出来,所以当使用这种方法时要确保也隐藏了其他窗口
- 当应用程序即将进入非活动状态时(如接到一个电话或切换到其他应用程序时),
applicationWillResignActive
方法会被回调,因此可以用以下代码来隐藏窗口-(void)applicationWillResignActive:(UIApplication *)application { [UIApplication sharedApplication].keyWindow.hidden = YES; } 复制代码
- 当APP被压入后台但在屏幕快照被调用前,
aplicationDidEnterBackground
方法中会被调用-(void)applicationDidEnterBackground:(UIApplication *)application { [UIApplication sharedApplication].keyWindow.hidden = YES; } 复制代码
- 当应用程序即将进入非活动状态时(如接到一个电话或切换到其他应用程序时),
-
模糊处理:除隐藏当前界面内容外,还可以对当前界面所展示的内容进行模糊化处理;在上述的回调方法中,通过使用iOS的毛玻璃(blur glass)技术,可以达到程序后台运行界面的模糊化效果
1.2.5、应用日志
基于iOS APP程序开发排错的需要,开发人员一般都会写一些数据到日志中,而这些数据就可能包括证件号、登录用户名和密码、认证token或其它的一些敏感信息
隐患:
- 应用程序的
错误日志是不被应用程序的沙盒隔离保护的
,一个APP产生的错误日志可以被另一个APP读取,因此,如果一个APP使用日志功能输出了某些敏感信息,那么恶意APP就能够读取到这些信息,并可将其发送到一个远程服务器上 - 可以直接从AppStore上下载安装
console
应用,查看iOS系统及APP输出的错误日志信息,可以在/var/log/
目录下找到iOS的日志文件
安全方案:
- 不要在APP的日志中记录或打印敏感信息
在正式发布时关闭日志打印开关
1.2.6、Keychain存储
Keychain是一个拥有有限访问权限的SQLite数据库
(AES256加密),可以为多种应用程序或网络服务存储少量的敏感数据(如用户名、密码、加密密钥等,以提供透明的认证,使得不必每次都提示用户登录);在iPhone上,Keychain存放在/private/var/Keychains/keychain-2.db
的SQLite数据库
-
Keychain数据库包含了一些Keychain条目,每个条目都由加密的数据和一系列未加密的描述属性组成,Keychain条目被分为5种类型:
类型 缩写 普通密码(generic passwords) kSecClassGenericPassword 网络密码(internet passwords) kSecClassInternerPassword 证书
(certificates)kSecClassCertificate 秘钥(keys) kSecClassKey 数字身份(digital identities) = 证书 + 秘钥 kSecClassIdentity -
在iOS的Keychain中,所有的Keychain条目都被存储在Keychain SQLite数据库的4张表中:
genp
、inet
、cert
和keys
- genp数据表 存储了 普通密码 的Keychain条目
- inet数据表 存储了 网络密码 的Keychain条目
- cert数据表 存储了 证书 的Keychain条目
- keys数据表 存储了 密钥 的Keychain条目
-
Keychain的数据库内容使用了
设备唯一的硬件密钥进行加密
,该硬件密钥无法从设备上导出;因此,存储在Keychain中的数据只能在该台设备上读取
,而无法复制到另一台设备上解密后读取 -
iOS APP的
Keychain数据是存储在应用沙箱外面的
,各APP的keychain数据内容为逻辑隔离
,由系统进程securityd实施访问控制- 一个应用默认无法读取到另一个应用在Keychain中存储的数据
- 在iOS系统中,每个APP都附带一个唯一的应用标识符,而Keychain服务则使用这个应用标识符限制其对其它Keychain数据的访问;默认情况下,APP只能访问与他们的应用标识符相关联的数据
- 为了在多个APP间能够共享Keychain信息,Apple引入了 Keychain访问组 概念:
拥有相同Keychain访问组标识符的应用,可以共享Keychain数据
- APP的Keychain访问权限(即标识符)被加密嵌入在了APP的二进制文件中,但可以通过使用
grep
或sed
命令将其从该文件中提取出来
-
随着iOS引入了数据保护机制,存储在Keychain中的数据被另一层,与
用户设备密码
(passcode)相关联的加密机制保护-
数据保护加密密钥(保护类密钥-protection class keys)是由一个设备 硬件密钥 和一个 由用户密码衍生的密钥 共同产生的
-
可以通过向Keychain接口的
SecItemAdd
或SecItemUpdate
方法的kSecAttrAccessible
属性提供一个数据保护可访问常量
来启用Keychain的数据保护
数据保护可访问常量 的值决定一个Keychain条目在何时可被应用访问,同时也决定某个Keychain条目是否允许移动到另一台设备上
常量 Keychain条目访问情况 注意 kSecAttrAccessibleWhenUnlocked 只能在设备被解锁后被访问 此时用于解密Keychain条目的数据保护类密钥只在设备被解锁后才会被加载到内存中,并且当设备锁定后,加密密钥将在10s钟内自动清除 kSecAttrAccessibleAfterFirstUnlock 可以在设备第一次解锁到重启过程中被访问 此时用于解密Keychain条目的数据保护类密钥只有在用户重启并解锁设备后才会被加载到内存中,并且该密钥将一直保留在内存中,直到下一次设备重启 kSecAttrAccessibleAlways 即使在设备锁定时也可被访问 此时用于解密Keychain条目的数据保护类密钥一直被加载在内存中 kSecAttrAccessibleWhenUnlockedThisDeviceOnly 只能在设备被解锁后被访问 无法在设备间移动 kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly 可在设备第一次解锁后访问 无法在设备间移动 kSecAttrAccessibleAlwaysThisDeviceOnly 即使在设备锁定时也可被访问 无法在设备将移动 -
-
Keychain条目的 数据保护可访问常量 被映射到各Keychain表(genp、inet…)中的pdmn列(protection domain),下表显示了keychain数据保护可访问常量和pdmn值之间的映射关系:
隐患:
-
为避免每次提示用户登录,APP很可能会在Keychain中直接存储明文的认证信息(如WiFi、邮箱密码),对于越狱的iOS设备使用Keychain_dumper工具可导出所有Keychain条目,它使用“*”通配符格式的Keychain访问组权限,因而可以访问到设备上所有的Keychain条目
-
APP在添加Keychain条目时可能也会设置 数据保护可访问常量,如前文所述,该可访问常量决定了Keychain条目何时才能被访问,而数据保护机制又是与用户密码相关联的,只有在当用户设置iOS密码时,它才会保护数据
- 当设置了iOS密码且未解锁时,无法访问到Keychain中的数据
- 解锁后 或 未设置iOS密码,即可访问到Keychain中的数据
- 当设置了iOS密码且未解锁时,无法访问到Keychain中的数据
安全方案:
- 一旦攻击者能够物理接触到没有设置密码的iOS设备时,他就可以通过越狱该设备,运行如keychain_dumper这样的工具,读取到设备所有的Keychain条目,获取里面存储的明文信息。而 即使在应用向Keychain中存储数据时使用了数据保护的接口(即上文提到的keychain数据保护可访问常量),未设置用户密码的Keychain数据仍没有被有效地保护着,为保证Keychain中存储的数据的安全,可采用以下建议:
- 尽量不在Keychain中直接存储明文的敏感信息
- 向Keychain中存储数据时,不要使用kSecAttrAccessibleAlways,而是 使用更安全的
kSecAttrAccessibleWhenUnlocked
或kSecAttrAccessibleWhenUnlockedThisDeviceOnly
选项 - 如果必须要存储,则可先
检测用户是否设置了设备密码
,并进行相应的风险提示
2、源码安全
2.1、反汇编二进制文件
隐患:
- 通过
class-dump
、theos、otool、Hopper
、IDA
等反汇编工具,Hacker可以对编译后的二进制文件进行反汇编,得到可读性很高的内容,从而制作注册机、破解网络协议制作机器人账号等
安全方案:
-
使用宏(#define),来混淆类名、替换易读的字符串,但手动写入文件工作量巨大,因此可以借助代码混淆工具;比如 codeobscure(原理是通过运行ruby脚本来遍历工程中的属性、方法、类名进行混淆),可能的坑:
- 三方库、以及其他不需要混淆的文件注意分文件夹管理,以备后续通过配置忽略路径来忽略这些不需要混淆的文件
- 由于
codeobscure 不会混淆静态字符串
,如@"text",因此以下几类不能混淆:- 与网络请求相关的模型类的属性不能混淆,并且您使用了MJExtension等直接将json对象转换为模型的三方库
- 使用了 NSClassFromString(@"classNameA")方法将静态字符串转换为Class的该类的类名不能混淆
被键值观察的属性不能被混淆
, 如以下canExchange这个属性就不能被混淆:[self.viewModel addObserver:self forKeyPath:@"canExchange" options:NSKeyValueObservingOptionNew context:nil]; 复制代码
-
关键逻辑使用纯C实现(微信的通讯底层就是C实现)
2.2、阻止动态调试
隐患:
- Hacker可能会使用调试器 GDB、LLDB来攻击你的App
安全方案:
- 你可以在 main.m 文件中插入以下代码:
#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, char *argv[]) { // Don't interfere with Xcode debugging sessions. #if !(DEBUG) disable_gdb(); #endif @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([MyAppDelegate class])); } } 复制代码
2.3、模拟器检测
- OC提供了宏
TARGET_OS_SIMULATOR
来检查你的App是否运行在模拟器环境。if (TARGET_OS_SIMULATOR) { // 模拟器 } else { // 非模拟器 } 复制代码
2.4、防止二次打包
隐患:
- 有些Hacker可能会通过篡改你的程序包(包括资源文件和二进制代码)加入一些广告或者修改你程序的逻辑,然后重新签名打包
安全方案:
- 由于
Hacker获取不到签名证书的私钥
,因此 会替换掉程序包中签名相关的文件:embedded.mobileprovision
,我们可以直接检查此文件是否被修改,来判断是否被二次打包,如果程序被篡改,则退出程序// 校验值,可通过上一次打包获取 #define PROVISION_HASH @"w2vnN9zRdwo0Z0Q4amDuwM2DKhc=" static NSDictionary * rootDic=nil; void checkSignatureMsg() { NSString *newPath=[[NSBundle mainBundle] resourcePath]; if (!rootDic) { rootDic = [[NSDictionary alloc] initWithContentsOfFile:[newPath stringByAppendingString:@"/_CodeSignature/CodeResources"]]; } NSDictionary*fileDic = [rootDic objectForKey:@"files2"]; NSDictionary *infoDic = [fileDic objectForKey:@"embedded.mobileprovision"]; NSData *tempData = [infoDic objectForKey:@"hash"]; NSString *hashStr = [tempData base64EncodedStringWithOptions:0]; if (![PROVISION_HASH isEqualToString:hashStr]) { abort();//退出应用 } } 复制代码
- 可以在多个重要的地方加入此代码,防止第三方攻击人员发现某一处检测代码后绕过检测
3、网络安全
移动端与服务器通讯的网络传输过程中可能会经过不安全的中间节点,即使是HTTPS的加密通讯,Hacker也可能通过中间人攻击
(Man-in-the-middle attack)来截取通讯内容,所以我们要对数据加密保护
3.1、密码传输安全
3.1.1、URL明文防护
隐患:
-
登录时
不能使用明文密码进行登录
,尤其是不能使用HTTP的GET方式进行登录,因为GET的参数直接拼接到url中,被截获一眼就能知道密码;且GET的url数据一般会保存在服务器的access log中,因此Hacker万一攻破服务器,只要扫描access log就能获得所有用户的密码 -
用自己的电脑在公共场所设置免费Wifi,如果有人连接了你的Wifi同时使用了一款明文传输密码的APP,那么他的密码就会轻易被你获取
3.1.2、HTTPS安全
隐患:
-
在iOS应用程序中,使用
HTTPS
进行通信是一种更为安全的做法,也是官方所推荐的做法,但是即使使用了 HTTPS,也有可能因为没有校验服务器证书
的原因导致 被中间人劫持。如果交互请求数据处理不当,攻击者可以解密得到明文通信数据;甚至进一步伪造App的请求,这是极大的安全隐患 -
这个检测方法也非常简单,就是打开APP登录帐号,使用抓包工具如
Charles
去看是否有请求获取敏感信息,比如获取资源包或者文件脚本
ip、域名、DNS、端口、内网、http协议等介绍
- 2台设备A与B通讯就像2个人写信:
ip地址
:就是家庭详细地址(省市区详细地址门牌号)域名
:就是ip地址方便记忆的别名(比如这个地址门牌是蜜雪冰城西安路店)域名解析
:从域名解析到ip地址的过程- 域名解析是由
DNS服务器
完成的,当我们访问www.baidu.com时,浏览器会向DNS服务器提交域名,DNS服务器返回这个域名的ip地址给浏览器,我们看到的是通过域名访问,实际上所有的通讯都是通过ip进行的 DNS劫持
:Hacker通过 DNS缓存感染或信息劫持 等方式,将错误的ip放回给用户,导致用户被引导到其他不良网站,或者插入广告等不法目的
端口
:就是家中具体收信的人JSON数据
:就是信的内容(因轻便简单替换了XML)内网
(局域网):就是2人的不同小区- 如果收件人和寄件人身处通过小区,那他们的ip地址其实只需要包含楼栋和门牌号即可,如(5#1004),ip地址亦然
- IPv4地址中预留了3个IP地址段,作为私有地址,共家庭、企业、学校等内部组网使用,最常见的是C类地址段
192.168.0.0
-192.168.255.255
,192.168.1.1 是大多数路由器的ip地址,也就是小区大门的ip地址,这样就可以连接192.168.1.1
(不包含)-192.168.1.255
共254台设备,一般家庭路由器使用已经足够了 - 这样就会导致不同的局域网,可能有相同的ip地址,就像不同的小区都有(5#1004)一样,但是信息仍然可以准确送达,正是因为
192.168.x.x
是局域网地址,如果需要跨小区交流还需要公网地址
路由器
(网关):就是小区大门,对进出内容进行管控- 因特网:就是门外世界
- 请求头、响应头、请求体、响应体:请求头用于携带附加的信息,GET请求和POST请求都包含请求头,请求头通常包含:
【Host】
:目标地址的地址+端口【User-Agent】
:浏览器的类型【Content-Length】
:请求消息内容的长度【Content-type】
:请求消息内容的类型,如 application/x-www-form-urlencoded 或application/json
;等
ARP欺骗
ip与MAC地址关系
:当我们的设备联网时会获得一个在这个局域网内唯一的ip地址,如果没有指定ip地址,我们的设备每次联网ip地址都可能发生改变;因此ip地址仅代表这个时候,这台设备在网络中的位置,它只代表一个别名,MAC地址也叫作物理地址,类似设备的身份证号,是设备的唯一标识,不管在何种网络环境下,MAC地址都是不会改变的- ARP协议用于将IP地址转换为MAC地址;当局域网内的两台设备需要通讯,表面上看它们是通过ip知道对方的位置进行通讯,实际上它们需要先根据ip找到对方的MAC地址,网络设备是通过MAC地址进行通讯的,而非ip地址
- 使用ARP欺骗的攻击者通过伪造数据包ARP报文,向局域网内的网络设备广播,将自己的MAC地址伪装成网关的MAC地址,使得局域网内的网络设备的
ARP缓存表中的网关IP对应的MAC地址
变为自己设备的MAC地址,此时其他设备的所有请求都将发往攻击者的设备上,若攻击者对这些请求转发至网关或外网,则请求正常,否则其他设备将全部断网 - 攻击者通过ARP欺骗将自己的设备伪造成网关,使得连接这个路由器的所有网络设备的请求都经过自己的设备,通过这个方法,攻击者无需是路由器的所有者,也无需或者路由器的访问权限即可捕获甚至修改局域网内网络设备的所有请求
加密方式
对称加密
(AES、DES):加密和解密是同一把秘钥非对称加密
(RSA):公私钥一者用于加密则需用另外一者解密Hash算法
(MD5):将任意长度的字符串生成16字节的散列值,不同字符串散列值不同,相同字符串有相同的散列值,并且无法根据散列值倒推回原来的明文
,只能从 MD5字符串 - 密文对应表 中匹配暴力破解(字符串简单在对应表中被找到了你就被破解了)
* HTTPS原理与中间人攻击
-
https是指
http + SSL/TLS协议
,在传输过程中对数据进行加密,开发者无法自己设定加密逻辑 -
移动端与服务器端通过https进行通讯,数据加密 使用到了
非对称加密
(一般是RSA)和对称加密
,大致流程如下:- 移动端向服务端发起https请求
- 服务端 将之前生成的一对RSA密钥中的
公钥返回给客户端
- 移动端
随机生成一个对称加密
(如AES)的key(密钥),并通过服务器提供的RSA公钥,对这个key进行加密
,将加密后的对称加密key发送给服务端 - 服务端 拿到这个对称加密后的key,
通过自己的私钥对这个密文进行解密
,拿到与移动端进行对称加密通讯的明文key - 服务端与移动端通过这个key加密密文进行通信
疑问1:
为什么这个流程这么繁琐?服务端直接把对称加密的密钥给移动端不行吗?答:
服务端把对称加密密钥给移动端的途中,可能被中间人拦截,中间人获得 对称加密的key 之后,加密形同虚设 -
中间人攻击
流程:- 上述流程中有个很大的漏洞!服务端将
RSA非对称加密的公钥发送给移动端
的途中,中间人可以将公钥拦截下来 - 中间人自己也创建一对RSA密钥,
将中间人的RSA公钥发送给移动端
- 移动端 通过这个被篡改的RSA公钥对自己随机生成的对称加密密钥进行加密 后,发送给服务端
- 在发送途中,中间人可以将此密文拦截下来,并
通过中间人的RSA私钥进行解密
(因为客户端实际上是用自己的公钥加密的),此时中间人可以拿到明文的对称加密密钥 - 中间人 通过先前截获的服务端的RSA公钥对此对称密钥进行加密,并发送给服务端
- 服务端通过自己的RSA私钥可以获取到对称加密密钥的明文,此时 中间人已经悄悄获取了二者将来进行的对称加密通讯的密钥,就可以轻松解密和篡改二者的通讯信息了
安全措施:
实际上https通讯过程中还有一个极为重要的角色:CA
(Certificate Authority)数字证书颁发机构,CA实际上是指多个权威的证书颁发机构,CA会生成一对公钥和私钥
,并将公钥存储于操作系统和浏览器中
,通过CA的介入可以完美解决,CA介入以后的加密流程如下:- 移动端向服务端发起https请求
- 服务端
将RSA公钥,通过CA的RSA私钥加密
,返回给移动端 - 移动端拿到加密后的服务器公钥,通过浏览器或系统中预先安装好的
CA公钥进行解密
- 若可以解密,则https请求可以继续进行,移动端顺利拿到服务器公钥的明文,并使用这个公钥对随机生成的对称加密密钥加密,将加密后的对称加密key发送给服务端
- 若无法解密,则代表公钥被篡改,https请求终止,且浏览器会显示warning
- 服务端拿到这个对称加密后的key,
通过服务端私钥对这个密文进行解密
,拿到与移动端进行对称加密通讯的明文key - 服务端与移动端通过这个key加密密文进行通信
- 如果这个加密后的公钥若被中间人拦截
- 因为CA的公钥是公开的,所以中间人可以解密并获取服务器的公钥明文
- 但是中间人无法将服务端公钥替换为自己的公钥,因为
需要CA的私钥来对自己的公钥来进行加密
- 若直接将明文的公钥或是使用他人私钥加密后的公钥提交给客户端,由于客户端中没有可以解密的公钥,https请求将终止
疑问2:
为什么大多数的抓包工具,在我的手机安装一个根证书
并让我信任后,就可以抓到https请求?答:
- CA证书防护的是
中间人向移动端传递篡改的RSA公钥过程
(中间人传递给服务端的过程不管),因为中间人一旦拆开服务端经CA私钥加密的内容,他就装不回去了 - 那么只有一种办法,就是换个包装,然后欺骗移动端说这是新包装也是正品请放心使用~
- 根证书中包含着一对公钥和私钥,安装根证书并信任,实际上就是在
系统和浏览器的CA公钥列表中插入中间人的RSA公钥
,如此就能让移动端解密私钥从而通过CA身份验证
注意:
-
信任根证书意味着信任根证书下的所有证书,此操作会极大影响用户的信息安全!!!
-
避免用户抓取https请求 -->
SSL Pinning
:一些应用将服务端的证书直接打包到App中,可以在https建立连接时对比本地证书与服务器返回的证书信息,若不匹配,则立即终止https连接;但是由于是在移动端判断证书一致性,因此可以 通过hook的方式修改移动端的判断逻辑来绕过SSL pinning -
以上https请求被拦截和篡改的前提是,用户主动在设备中安装并信任了根证书,因此在绝大多数情况下,https可以极大保障用户的信息安全,很多企业和机构开始强制要求开发者使用https进行通讯以保证用户的信息安全
- 上述流程中有个很大的漏洞!服务端将
安全方案
- 使用POST方式传递密码
- 避免使用有漏洞的第三方网络库(如
AFNetworking < 2.5.3
版本) - 关键数据(如登录密码、卡号、交易密码等)单独加密
- App内要
对HTTPS证书做校验
- 使用非对称加密(RSA),事先生成一对用于加密的公私钥
- 移动端登录时,使用公钥将用户密码加密,再将密文传输到服务器
- 服务器使用私钥解密密码,然后
加盐
(Salt:通过在密码任意位置插入特定的字符串,让散列后的结果和使用原始密码的散列结果不相符) - 再
多次进行MD5加密
,最后与服务器中存储的密码匹配返回登录结果
- 注意:
-
服务器只需要匹配加密后密码一致,而不是用户明文密码;这也是现在APP只提供重置密码而非找回密码的原因,因为他们也不知道密码是多少
-
Hacker没有私钥,即使截获密文也无法获知密码;而服务器即使被攻破,也无法从加盐并多次MD5的密码中还原出原始密码
// 这种方式很多时候就够用了 NSString *publicKey = @"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC0OiXHaees9Aj5h31YYGw5nfCUdS6MK0T5UrJAfIdwkUbadDOXclDVK+ftBMe+DVAn7xSORPi1cjiBBjU+lo/hmNGoDWQGgxr/LAkaJz3/A1Sv+S1d3deTc6SFN+toDQbpsx3jYOUrJM1B8olUI1a9f+DgzkF/sIKJ7V4Wh7XtlQIDAQAB"; //私钥 NSString *privateKey = @"MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALQ6Jcdp56z0CPmHfVhgbDmd8JR1LowrRPlSskB8h3CRRtp0M5dyUNUr5+0Ex74NUCfvFI5E+LVyOIEGNT6Wj+GY0agNZAaDGv8sCRonPf8DVK/5LV3d15NzpIU362gNBumzHeNg5SskzUHyiVQjVr1/4ODOQX+wgontXhaHte2VAgMBAAECgYAljox63sXpk70fCq4DMay74P7WYQj/KrEn56S/rXOn8I48TcTGhYr0sT6WdM2O/EU83SSCdTsCzLebo4iK72Mx/VI1alKWSfyncfXi51gZePpgVaudTG9kcI8sszRG+P7zfPptr4HxZ7X2LveJy5myImqQomESEUvDVHdZCtIIgQJBANxT7Bev+LA+jH9gSV4Uc14B5YYabpLso39t/uhTaOdVNxV2eV2UAF1PGg2R3IMwlzHcjRkDDpBiWuXaxbTKCaECQQDRaCUb7J1mPDBrEIi1Aupho16b3Sy82vzZ9WGLgMl+eMKbSy0rAdJA10CtNKL2Gq7EGNoN4CpDPPulJTmm6Cd1AkAajH5BaHHmAtN5McgFbx9rr3zRyPOT/rHA1CdIJWzZmzoU+v6q2P+mPrbb9byFjmBZoMLbxbOGkGN1mQQDweihAkEAkAJ9Mr0AaeSOr7KJMWK16Tu+vpXWRHKdXQ9Ba/y/lThbLQ0AHQl9nJXrprICOBmVgspMeypkJiV0Mdht03joWQJAaF8kDoCNkpp++6aqVbqFBYysiW83AiHgL0JA5dhQ2XzIFYZIpLOsM+Je4yw9ppQ76DqePg6pqRKjR6m9Gatn+A=="; //测试要加密的数据 NSString *sourceStr = @"iOS端RSA加密"; //公钥加密 NSString *encryptStr = [RSA encryptString:sourceStr publicKey:publicKey]; //私钥解密 NSString *decrypeStr = [RSA decryptString:encryptStr privateKey:privateKey]; NSLog(@"公钥加密私钥解密后的数据 %@",decrypeStr); //私钥加密 NSString *encryptStr1 = [RSA encryptString:sourceStr privateKey:privateKey]; //公钥解密 NSString *decrypeStr1 = [RSA decryptString:encryptStr1 publicKey:publicKey]; NSLog(@"私钥加密公钥解密后的数据 %@",decrypeStr1); 复制代码
-
请求验签
:- 假设用户进行登录操作,发送请求 {"account":"123","pwd":"456"},通过将json转化为特定的格式,如 account=123&pwd=456(一般还会
加盐
),并对其进行md5加密 - 将加密后的MD5值放在 sign字段 中,就得到 {"account":"123","pwd":"456","sign":"MD5加密后的值"} 发送给服务器
- 服务器拿到这个json的时候,对sign前内容与移动端进行相同的加密操作,比对加密出来的结果与sign值,若相同,则可以认为account和pwd未被篡改,可信任这个请求,若不同,则代表必然有信息被篡改,则废弃这个请求
- 假设用户进行登录操作,发送请求 {"account":"123","pwd":"456"},通过将json转化为特定的格式,如 account=123&pwd=456(一般还会
3.2、通讯协议防护
隐患:
- 移动端除了明文传输密码问题外,还要面对Hacker破解通讯协议的威胁,如果通讯协议被破解,Hacker可以模拟客户端登录,进而伪造用户行为,制作机器人或发送垃圾广告
安全方案:
- 可以选择
Protobuf
(Google提供的开源数据交换格式,特点是基于二进制
,因此体积比JSON格式要小)之类的二进制通讯协议,或自己实现通讯协议来对传输内容进行加密
3.3、验证应用内支付凭证
隐患:
- 越狱手机不受沙盒保护,且苹果为了保护用户隐私,
支付凭证中不包含任何账号信息
(类似于不记名购物卡),因此对越狱手机Hacker可能截获支付凭证转手倒卖
安全方案:
- 验证支付凭证真伪的同时,也需要告知用户在越狱手机上支付的风险
3.4、防止网络请求被Charles抓包
- 要攻击一个APP ,抓包是必不可少的,那么如何防止被
Charles
之类(中间人攻击类型)的抓包软件抓包呢?主要有以下几个思路:检测是否使用了代理
,检测到使用了代理就关闭网络请求- 使用自签名证书的应用和双向验证的应用
- 通过
HTTP/1.1
及以上版本的CONNECT
请求方式 - 对返回的数据进行加密(
RSA | token | AES128
等,不防抓包,增加解密难度)
3.4.1、绕过代理发送请求
-
对于复杂的APP,有很多接口是不用加密的,这样就出现了
防代理模式
-
代理检测
:当进行网络请求的时候,客户端判断当前是否设置了代理,如果设置了代理,不允许进行访问数据,示例:+ (BOOL)getProxyStatus { NSDictionary *proxySettings = NSMakeCollectable([(NSDictionary *)CFNetworkCopySystemProxySettings() autorelease]); NSArray *proxies = NSMakeCollectable([(NSArray *)CFNetworkCopyProxiesForURL((CFURLRef)[NSURL URLWithString:@"http://www.google.com"], (CFDictionaryRef)proxySettings) autorelease]); NSDictionary *settings = [proxies objectAtIndex:0]; NSLog(@"host=%@", [settings objectForKey:(NSString *)kCFProxyHostNameKey]); NSLog(@"port=%@", [settings objectForKey:(NSString *)kCFProxyPortNumberKey]); NSLog(@"type=%@", [settings objectForKey:(NSString *)kCFProxyTypeKey]); if ([[settings objectForKey:(NSString *)kCFProxyTypeKey] isEqualToString:@"kCFProxyTypeNone"]) { //没有设置代理 return NO; } else { //设置代理了 return YES; } } 复制代码
-
绕过代理请求设置
:如果在初始化NSURLSession
的时候将connectionProxyDictionary
属性设置为空,那么,当手机开启了代理服务(如:Charles
)的时候,用这个 session 发起的网络请求并不会去走这个代理,而且默认的不走代理直接发起网络请求// 对于H5页面的网络请求还是可以抓到的 configuration.connectionProxyDictionary = @{};
#import "NSURLSession+SafeHttpProxy.h" #import "SafeURLProtocol.h" #import <objc/runtime.h> static BOOL isDisableHttpProxy = YES; @implementation NSURLSession (SafeHttpProxy) +(void)load{ [super load]; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [NSURLProtocol registerClass:[SafeURLProtocol class]]; Class class = [NSURLSession class]; [self swizzingMethodWithClass:class orgSel:NSSelectorFromString(@"sessionWithConfiguration:") swiSel:NSSelectorFromString(@"Safe_sessionWithConfiguration:")]; [self swizzingMethodWithClass:class orgSel:NSSelectorFromString(@"sessionWithConfiguration:delegate:delegateQueue:") swiSel:NSSelectorFromString(@"Safe_sessionWithConfiguration:delegate:delegateQueue:")]; }); } +(void)disableHttpProxy{ isDisableHttpProxy = YES; } +(void)enableHttpProxy{ isDisableHttpProxy = NO; } +(NSURLSession *)Safe_sessionWithConfiguration:(NSURLSessionConfiguration *)configuration delegate:(nullable id<NSURLSessionDelegate>)delegate delegateQueue:(nullable NSOperationQueue *)queue{ if (!configuration){ configuration = [[NSURLSessionConfiguration alloc] init]; } if(isDisableHttpProxy){ configuration.connectionProxyDictionary = @{}; } return [self Safe_sessionWithConfiguration:configuration delegate:delegate delegateQueue:queue]; } +(NSURLSession *)Safe_sessionWithConfiguration:(NSURLSessionConfiguration *)configuration{ if (configuration && isDisableHttpProxy){ configuration.connectionProxyDictionary = @{}; } return [self Safe_sessionWithConfiguration:configuration]; } +(void)swizzingMethodWithClass:(Class)cls orgSel:(SEL) orgSel swiSel:(SEL) swiSel{ Method orgMethod = class_getClassMethod(cls, orgSel); Method swiMethod = class_getClassMethod(cls, swiSel); method_exchangeImplementations(orgMethod, swiMethod); } @end 复制代码
-
开VPN时可能会误伤APP导致无法使用
3.4.2、通过 HTTP/1.1 及以上版本的 CONNECT 请求方式
- 什么是
CONNECT
请求?-
平时工作中,
GET
跟POST
是我们用的比较多的请求方式,HTTP/1.0
定义了三种请求方法: GET, POST 和HEAD
方法 -
HTTP/1.1
新增了五种请求方法:OPTIONS、 PUT、DELETE、 TRACE 和 CONNECT
方法 -
CONNECT 主要是把服务器作为跳板,先验证用户名和密码等信息,再让服务器代替用户去访问其它网页,之后把数据返回给用户
-
- 之所以说采用 CONNECT 请求当跳板,可以防止
Charles
抓包,是因为 Charles 抓 CONNECT 的请求,会识别为unknown
,所以就能达到防抓包的目的
3.4.3、SSL Pinning
通过 对服务端生成的.cer证书进行域名校验,服务器通过.crt证书导出.cer放到客户端进行处理
-
NSURLSession处理
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler { //得到远程证书 SecTrustRef serverTrust = challenge.protectionSpace.serverTrust; SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, 0); //设置ssl政策来检测主域名 NSMutableArray *policies = [NSMutableArray array]; [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)challenge.protectionSpace.host)]; //验证服务器证书 SecTrustResultType result; SecTrustEvaluate(serverTrust, &result); BOOL certificateIsValid = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed); //得到本地和远程证书data NSData *remoteCertificateData = CFBridgingRelease(SecCertificateCopyData(certificate)); BOOL allChrls = NO; allChrls = DebugNet; if (allChrls) { NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust]; completionHandler(NSURLSessionAuthChallengeUseCredential,credential); }else{ NSString *pathToCer = [[NSBundle mainBundle] pathForResource:@"xiaoqi" ofType:@"cer"]; NSData *localCertificate = [NSData dataWithContentsOfFile:pathToCer]; //检查 if ([remoteCertificateData isEqualToData:localCertificate] && certificateIsValid) { self.allowTask = YES; NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust]; completionHandler(NSURLSessionAuthChallengeUseCredential,credential); }else { self.allowTask = NO; [_dataTask cancel]; [_downloadTask cancel]; [_uploadTask cancel]; completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge,NULL); } } } 复制代码
-
AF中自定义
// 自定义安全策略 + (AFSecurityPolicy *)customSecurityPolicy { // 获取证书 NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"xiaoqi" ofType:@"cer"]; NSData *certData = [NSData dataWithContentsOfFile:cerPath]; NSSet *pinnedCertificates = [[NSSet alloc] initWithObjects:certData, nil]; /* 安全模式 AFSSLPinningModeNone:完全信任服务器证书; AFSSLPinningModePublicKey:只比对服务器证书和本地证书的Public Key是否一致,如果一致则信任服务器证书; AFSSLPinningModeCertificate:比对服务器证书和本地证书的所有内容,完全一致则信任服务器证书 */ AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey withPinnedCertificates:pinnedCertificates]; // allowInvalidCertificates 是否允许无效证书(也就是自建的证书),默认为NO // 如果是需要验证自建证书,需要设置为YES securityPolicy.allowInvalidCertificates = YES; /* validatesDomainName 是否需要验证域名,默认为YES; 假如证书的域名与你请求的域名不一致,需把该项设置为NO; 如设成NO的话,即服务器使用其他可信任机构颁发的证书,也可以建立连接,这个非常危险,建议打开。 置为NO,主要用于这种情况:客户端请求的是子域名,而证书上的是另外一个域名。 因为SSL证书上的域名是独立的,假如证书上注册的域名是www.google.com,那么mail.google.com是无法验证通过的; 当然,有钱可以注册通配符的域名*.google.com,但这个还是比较贵的。 如置为NO,建议自己添加对应域名的校验逻辑。 */ securityPolicy.validatesDomainName = YES; return securityPolicy; } 复制代码
-
证书会失效
,证书由于是服务端生成的根据域名来的
,所以一般最长的是一年的证书,到期不换可能会对接口请求产生问题
结语:
APP的安全防护是门很深奥的学问,参考市面上的三方防护服务就知道还有很多很多要学习的地方,后续要涉及逆向方面的知识,大家根据需要学习提升吧
参考链接:
www.jianshu.com/p/dc5cef72f…
blog.csdn.net/g270382086/…
cloud.tencent.com/developer/a…