网络接口数据安全的【攻与防大全】

[toc]

一、对数据加密的概述

1、安全的数据传输是什么样的

数据传输:都是使用密文(客户端提供加密的参数,服务端提供加密的结果)。

【客户端提供加密的参数】:需要在客户端加密后,上传到服务端,并在服务端对其进行解密,以确认传过来的参数。

【服务端提供加密的结果】:需要在服务端加密后,返回给客户端,并在客户端对其进行解密,以确认传过来的结果。

数据安全传输(AES)

若要了解加密方式可查看下文【附1:加密的方式有哪些

2、一端的AESKey怎么【安全的】传给另一端 / 为什么要对AES的密钥进行RSA公钥加密后传输??

答:为了安全,一端的AESKey必须先加密,然后才能传给另一端,这样才能防止用户从请求中抓取到AESKey。同时另一端需要对传过来的数据进行解密得到AESKey原值,后续两端才能都通过AESkey原值进行两边数据的对称加解密。即数据加密采用AES,而把AES的加密秘钥用RSA加密。(附:为什么AESKey不是使用md5方式加密?答:因为另一端要解密来使用原值。)。

问题:为什么要对AES的密钥进行RSA公钥加密后传输?

AES是对称加密算法,加密和解密的密钥都是同一个,为了防止被别人恶意获取到该密钥,然后对我们的业务请求参数进行解密,我们需要将AES密钥进行非对称加密后再进行传输。

2.1、密钥来源于客户端(类似Https)

数据安全传输(AES+RSA)

上图和Https原理有点相像。

2.1.1、客户端密钥保存的安全问题

如果密钥是来源于客户端,那如何在客户端安全的保存密钥?请查看下文中的【附2:如何在客户端安全的保存密钥AESKey

2.2、密钥来源于服务端(为了密钥能够像版本那样进行控制)

实际过程中,可能你的AESKey为了能够像版本那样进行控制,可能你的AESKey是由服务端分发的,则其过程可能如下:

数据安全传输(AES+RSA)

空 ==> getServicePublicKey ==> servicePublicKey

clientPublicKey(使用ServicePublicKey加密) ==> sendClientPublicKey ==> 使用servicePrivateKey解密得到clientPublicKey

空 ==> getServiceAESKey ==> aesKey(使用clientPublicKey加密)

参考文章:

最终网络数据传输,可能就只剩下类似下面一样,只有一个参数和

1
2
3
4
5
只剩一个参数:
encryptedParametersString: xxxx

只剩一个返回:
encryptedResultString: yyyy

二、被破解后,篡改参数值,进行请求攻击,怎么办?(二次加密:签名)

如果上面的加密过程被破解了,用户就能自己还原原始参数和原始返回值了。这时候如果他篡改参数值进行请求攻击怎么办?

1、请求参数值被篡改的场景

购物车多商品结算时候,本需共100美元的商品。在生成订单的时候,携带了所要支付的各个商品的id,以及总费用。结果总费用被篡改成以0.01美元的价格生成了订单Id。而后服务端却没有校验这些商品是不是0.01美元,从而导致平台损失。

以购物车结算为例,传统的传给服务器的参数一般是如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
goods: [
{
id: xxx
title:
price:
},
{
id: yyy
title:
price:
}
],
totoalPrice: 100

当用户破解了我们之前的加密算法后,其就能还原原始参数结果,并篡改参数值,然后再利用破解所得的加密算法得到一个新的加密参数如下:

1
2
3
4
5
篡改前所传参数:
encryptedParametersString: xxxx1

篡改后所传参数:
encryptedParametersString: xxxx2

之后,如果服务器接收和处理的就是被修改过的值了。

那这种情况下怎么办法呢?答:再加一层加密算法,即签名。

2、请求参数值被篡改的解决/防篡改方案

为了防篡改,通常有以下三个步骤:

1、我们通过上述原始参数计算得出一个签名值。然后将签名值添加一起传给服务端。

1
2
3
4
5
6
7
8
9
篡改前所传参数:
encryptedParametersString: xxxx1
signatureValue: xxxx1的签名值
signatureMethod: xxx1的签名方式(如果客户端有md5、sha256等多种加密方式)

篡改后所传参数:
encryptedParametersString: xxxx2
signatureValue: xxxx1的签名值(用户不知道此签名值的计算方法)
signatureMethod: xxx1的签名方式(如果客户端有md5、sha256等多种加密方式)

由于用户不知道签名值的计算方法,所以即使其篡改了原原始参数中的部分参数值并生成加密参数,其也仍需要再破解出这个签名值的计算方法,如果其不修改签名值,而是把签名值直接上传到服务端,则服务端会进行以下操作。

2、然后服务端接收到所传的参数(含签名值)后,也按照和客户端一样的方式对除签名值外的其他参数再计算一遍签名值。

3、如果所得的签名值和客户端传过来的签名值不一样,则是参数被篡改了,服务端放弃该请求。(注:为了保证正常情况下计算出的客户端和服务端的签名一样,我们会对请求参数进行字典排序后再签名。

所以,防篡改的解决办法是客户端加签,服务端验签。

2.1、加签的方法

对上述的 encryptedParametersString: xxxx1 进行md5、sha256或其他加密算法。

2.1.1、签名算法的防猜测

如果签名的加密算法被猜到是用md5进行的签名。则用户可以也跟着对encryptedParametersString: xxxx1 进行md5加密即可得到签名会被服务端认证通过的请求了。那是不是md5不能用呢?如果用怎么用?答:md5加盐,进一步增加破解的难度。

为什么加盐?
避免用户直接使用参数值进行md5加密,发现签名值就是由参数值md5加密而来。所以为了让MD5码更加安全 ,我们现在都采用加盐,盐要越长越乱。

1
2
3
4
5
加盐前:
digest = hash(input)

加盐后:用户拿参数值直接md5发现和签名值匹配不上,则一般就不会发现是用md5加密了。
digest = hash(salt + input)

这样加盐后,当用户再拿参数值直接进行md5后,发现和签名值匹配不上,则一般就不会想到算法是用md5加密了。

这个salt可以看作是一个额外的“认证码”,可以是固定的值,也可以是动态的值,如每个用户自己的userid。

更多MD5及其加盐可查看下文中的【附3:签名 –> md5(加盐))】

2.1.2、签名算法关注的内容

除了请求参数外,签名算法还关注请求的接口和方法,即其一般是对含参数在内、外加其他值(如URL、Method)一起进行签名。

参考文章:

三、不破解,但进行二次请求/API重放攻击(Replay Attacks)怎么办?

加密后,破解难度增加了,但是如果用户不破解,而是通过API重放攻击你的服务器呢,怎么办?

1、重放攻击(Replay Attacks)概述及危害介绍

API重放攻击(Replay Attacks)又称为重播攻击、回放攻击。它的原理就是把之前窃听到的数据原封不动的重新发送给接收方。

1.1、即使有幂等性情况下,重放请求的危害

追问:服务端不是应该有做幂等性了吗,为什么还需要处理防重放问题?

答:虽然如果一个系统操作是幂等的,即使攻击者重放了一个请求,系统也不会因为重复执行而产生不同的结果或状态。在一定程度上可以减少重放攻击的潜在损害(例如,一个幂等的HTTP GET请求,无论请求多少次,服务器都会返回相同的结果,而不会对服务器的状态产生影响)。但是即使请求本身是幂等的,也需要防止恶意用户发送大量重放请求来耗尽系统资源,从而实现拒绝服务攻击(DoS)。

若要了解幂等性知识,可查看

1.2、没有幂等性情况下,重放请求的危害

如果服务端没有幂等性,则假设有一个在线投票系统,用于选举或表决,每个用户只能投票一次。那么攻击者可能会尝试重放投票请求,进行重复投票,以增加他们支持的候选人或选项的票数。

2、重放攻击(Replay Attacks)的解决方案

防止重放攻击必须要保证请求仅一次有效。需要通过在请求体中携带当前请求的唯一标识,并且进行签名防止被篡改。所以,防止重放攻击需要建立在添加签名且防止签名被篡改的基础之上。

2.1、措施一:添加请求的时效性参数 Timestamp

即某个请求,其请求时间戳Timestamp,和服务端的当前时间在规定时间内(如1分钟内)则为合法请求,反之,则视为无效请求。

服务端时间戳的获取时机和服务端的RSA公钥一致,你可理解为网络库的初始化阶段(虽然这是真正初始化之后的请求的”二次初始化”)。

数据安全传输_防重放_1加timestamp

客户端与服务端的时间戳差值 diff = 服务器返回的时间戳 - 客户端请求结束的时间戳

校准服务端后的客户端的时间戳 = 当前客户端时间戳 + 客户端与服务端的时间戳差值 diff

2.2、措施二:添加一个唯一的随机数Nonce

2.2.1、Nonce的核心思路:

调用者每次调用时:

​ A:调用者生成并带上一个随机数Nonce(可以考虑用用户id+当前时间戳+接口url)

​ B:服务端该随机数是否已出现,有则拒绝,无则存储该随机数并放过请求。

则请求参数变为如下:

1
2
3
4
5
6
7
8
9
10
11
未加 timestamp 和 nonce 前:
encryptedParametersString: xxxx1
signatureValue: xxxx1的签名值
signatureMethod: xxx1的签名方式(如果客户端有md5、sha256等多种加密方式)

加了 timestamp 和 nonce 后:
encryptedParametersString: xxxx1
signatureValue: xxxx1的签名值,📢:要把timestamp和nonce也包括进去,避免它们被篡改
signatureMethod: xxx1的签名方式(如果客户端有md5、sha256等多种加密方式)
nonce: xxx1的nonce
timestamp: xxx1的timestamp

这里注意:计算xxxx1的签名值要把timestamp和nonce也包括进去,避免它们被篡改。

2.2、Nonce的优化思路:

这里服务端要保证Nonce唯一,就得存储已经用过的Nonce,但长期保持会带来两个问题

​ (1)存储成本增加,日积月累,这里要存储的Nonce会越来越多,需要的存储空间就越大

​ (2)碰撞概率增加,正常服务被拒绝概率增大;这里随着生成Nonce值越来越多,碰撞的概率一定越来越大,若通过增加Nonce值的长度,有增加存储成本。

​ 那么,另一个可行的办法,就是调用者每次请求时带上当前请求时间点Timestamp,然后由服务端限制请求的时效性。

​ 如此,上面提到的Nonce值存储成本可能比较大的问题,在结合Timestamp后,可大大降低存储成本,如Timestamp=1min,则仅需存储1min内的请求Nonce值即可,大大减少存储的量级。

至此,客户端发送的参数为:

1
2
3
4
5
encryptedParametersString: xxxx1
signatureValue: xxxx1的签名值,📢:要把timestamp和nonce也包括进去,避免它们被篡改
signatureMethod: xxx1的签名方式(如果客户端有md5、sha256等多种加密方式)
nonce: xxx1的nonce
timestamp: xxx1的timestamp

3、扩展:客户端自己如何防止接口重复请求?

1、界面添加 loading

2、进行节流(最后执行)和防抖(立即执行)中的防抖。

3、请求队列:类似SDWebImage加载相同地址的图片。

如果重复请求的话,客户端的优化:

1、缓存

其他可参考:功能问题:如何防止接口重复请求?


##

附1:加密的方式有哪些

对称加密 非对称加密
概念 加密密钥和解密密钥相同 加密密钥和解密密钥不相同
算法 DES、AES、RC2、RC4、RC5 等 RSA、ECC 等
缺点 速度快,安全性低。使用同一个密钥,如果一方密钥泄露,数据就不安全了 非对称加密的安全性更高,但加密速度较慢。

注意:非对称加密的安全性更高,但加密速度较慢。一般我们在项目中采用的是 ”结合对称和非对称加密,数据加密采用AES,而把AES的加密秘钥用RSA加密,这样兼顾速度及安全性“https://juejin.cn/post/7002058547395559438

1.1、对称加密

加密方式_对称

1、李雷想要给韩梅梅发送消息,他们约定使用对称加密的方式把消息进行加密

2、李雷用密钥把消息加密然后发送给韩梅梅

3、韩梅梅用同一个密钥解密,然后看到李雷发送给自己的消息

1.2、非对称加密

加密方式_非对称

1、李雷想要给韩梅梅发送消息,他们约定使用非对称加密的方式把消息进行加密

2、李雷首先要得到韩梅梅的公钥

2、李雷用韩梅梅的公钥把消息加密然后发送给韩梅梅

3、韩梅梅用自己的私钥解密,然后看到李雷发送给自己的消息

韩梅梅给李雷发送消息同理。

附2:如何在客户端安全的保存密钥AESKey

如果密钥是来源于客户端,那如何在客户端安全的保存密钥?

1、密钥在代码上(内部)

1.1、存放密码的常见的代码

1
2
3
4
#define kSecret "abcd1234"

// 或者
const char* kSecret = "abcd1234";

这是非常危险的,因为常量会被直接编译到可执行文件的data段,只要对生成的可执行文件使用stringsotool等命令就可以dump出原始字符串。

1.2、对密码加密

仍然存

1、类的代码层面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 方法1:密码在可执行文件的data段,且是明文
#define kSecret "abcd1234"
void getSecret {
return kSecret
}

// 方法2:密码在可执行文件的data段,虽是明文,但取到的值要再经过一层你自己知道的方法解密
#define kSecret "\x7e\x77\x64\x3c\xa7\xd4\x6d\x46\x29\x8b\xe3\x23\x9f\x1a\x5c\xdb"; // 使用aes加密后的
#define kAESKey "abcdefgh12345678";
char* getSecret() {
return decryptSecretByAESKey(kSecret, kAESKey);
}
// 方法2的优化:内部代码使用额外包在宏(kAppSecret)上,由于宏没有明确的入口,使得静态分析相比直接调用函数形式的getSecret更加不容易被破解。
#define kAppSecret \
({ \
char* buf = decryptSecretByAESKey(kSecret, kAESKey); \
[NSString stringWithUTF8String:buf]; \
})

附:带返回值的宏的写法 见宏定义的两个技巧:

1
2
3
4
#define SOME_MACRO \ 
({ \
expression; \
})

最后一个表达式的值就是宏的返回值,使用时更像函数的返回值。

2、类本身(可包含类中的函数名)

因为objc代码的动态性,编译器会在binary中留下类名、函数名等信息,这些信息是可以被class-dump-z等工具提取的,友好的命名让程序猿更方便,但同时也方便了破解者。对安全相关的重要模块类,可以故意混淆类名,让人不容易轻易联想到该的真实目的。

PS、更多混淆知识可看我写的另一篇文章:iOS APP安全加固方案(一、代码混淆CodeObfuscation)

2、密钥保存在外部

2.1、密钥保存在Keychain

密钥保存在Keychain并不安全,iOS越狱后可以导出Keychain的内容。应该尽量避免存放重要信息(如:token、用户名、密码等)在Keychain中,即使要存放,也一定要加密后存放。参考:iOS安全攻防(九):使用Keychain-Dumper导出keychain数据

2.2、密钥保存在文件

保存在app bundle、plist等配置文件更不安全,但可以使用隐写术等方式迷惑hackers。

2.2.1、隐写术

隐写术的载体:相比视频、音乐等文件,通常选择使用图片这种占用空间小的文件,但因为Xcode打包时会对png图片做特殊处理,如果将密码携带在png中,可能会在使用的时候无法复原。所以若非确定的隐写方法,则建议用bmp图片。

本节点【如何防止客户端被破解/密钥的保护】部分内容摘自/参考:如何防止客户端被破解

LSB算法:LSB全称为 Least Significant Bit(最低有效位),是一种常被用做图片隐写的算法。为了避免我们对图片进行的处理可能会在压缩的过程中被破坏,建议在无压缩的bmp图片上实现。(png图片是无损压缩,jpg图片是有损压缩。)

steganography_lbs图片来源于:《网络加密.graffle》中的【隐写术】

bmp的隐写,相比lsb隐写,bmp简单的多,并且达到了无损隐写。

bmp了解:BMP位图隐写

ctf杂项中图片隐写拿到一张图片常见的解题方法

附3:签名 –> md5(加盐)

md5是哈希函数。SHA-256或SHA-3也是哈希函数,相比md5更安全。

Mac:在终端上实现将abcd1234进行MD5(Message Digest algorithm 5 ,信息摘要算法)加密。

1
2
3
echo -n 'abcd1234' | md5

得到结果:e19d5cd5af0378da05f63f891c7467af

使用MD5就是将一串字符串通过某特定的算法来将其变成另一种形式,这样子就在外观上起到了加密的效果,但是由于背后的算法(md5是一种常见的hash算法)是固定的,所以每一个字符串都有固定的MD5格式。(反过来,一个hash值是否只能是一个值才可能?答:不是)

因为每一个字符串就只有一种特定的MD5格式,所以md5的破解(从c=hash(m)逆向得到原始明文m)容易想到的方式有:

方法 优缺点 推荐程度
暴力破解法 时间成本太高 ⭐️
字典法 提前构建一个“明文->密文”对应关系的一个大型数据库,破解时通过密文直接反查明文。但存储一个这样的数据库,空间成本是惊人的 ⭐️⭐️⭐️
构建彩虹表 在字典法的基础上改进,以时间换空间。是现在破解哈希常用的办法。
彩虹表:彩虹表记录了几乎所有字符串的MD5对照表。
⭐️⭐️⭐️⭐️⭐️

md5在线解密网站

md5在线解密

因为已经有了类似彩虹表的破解方法,所以如果不‘加盐’,则当用户知道是 e19d5cd5af0378da05f63f891c7467af 时候,就很容易知道原值是 ‘abcd1234’了。

所以Md5加密方式的进化之路小结:

密码存储的种方式
1 直接存储密码明文m 明文存储,无安全性可言。
2 存储密码明文的哈希值hash(m) 虽然是入侵者得到的是hash值,但由于彩虹表的存在,也很容易批量还原出密码明文来。
3 存储密码明文的加盐哈希 hash(m+salt)。
这里的salt可以是用户名,手机号等,但必须保证每个用户的salt都不一样才是安全的。
相对前两种安全

网路本身的安全

HTTPS是应用层的安全协议。TCP是传输层的协议,但是它不安全,因为它是明文传输的,所以SSL的诞生就是给TCP加了一层保险,使HTTPS和TCP之间使用加密传输。而TLS只是SSL的升级版,他们的作用是一样的。

以下内容摘自HTTPS、SSL/TLS、TCP之间错综复杂的情感纠葛

SSL(Secure Sockets Layer) 安全套接层:为网络通信提供安全及数据完整性的一种安全协议。

TLS(Transport Layer Security)安全传输层协议:用于在两个通信应用程序之间提供保密性和数据完整性。该协议由两层组成: TLS 记录协议(TLS Record)和 TLS 握手协议(TLS Handshake),是更新、更安全的SSL版本。

SSL/TLS协议的基本思路是采用公钥加密法,也就是说,客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。==> 类似 RSA

但是,这里有两个问题。

(1)如何保证公钥不被篡改?

解决方法:将公钥放在数字证书中。只要证书是可信的,公钥就是可信的。

(2)公钥加密计算量太大,如何减少耗用的时间?

解决方法:每一次对话(session),客户端和服务器端都生成一个”对话密钥”(session key),用它来加密信息。由于”对话密钥”是对称加密,所以运算速度非常快,而服务器公钥只用于加密”对话密钥”本身,这样就减少了加密运算的消耗时间。 ==> 类似 AES

HTTPS(超文本传输安全协议)是建立在HTTP(超文本传输协议)之上,通过SSL/TLS(安全套接字层/传输层安全协议)进行加密的安全协议。在HTTPS的握手过程中,SSL/TLS握手会先于HTTP握手发生。以下是HTTPS连接建立的基本步骤:

  1. TCP握手:客户端首先通过TCP(传输控制协议)与服务器建立一个普通的连接。这个过程包括三次握手,确保双方都准备好进行数据传输。
  2. SSL/TLS握手:一旦TCP连接建立,SSL/TLS握手就开始了。这个过程包括:
  3. 应用层协议:一旦SSL/TLS握手完成,客户端和服务器就可以使用这个安全的连接来传输加密的数据。这时,HTTP请求和响应就会在SSL/TLS层之上进行传输。

因此,HTTPS的连接建立过程中,是先进行TCP握手,然后进行SSL/TLS握手,最后才开始HTTP通信。

其他参考文章

End