重放攻击

背景

今天想从【今日热榜】这个 App 中获取榜单的 Get 请求,从而可以“借”它们的数据做个榜单类的小程序。用 Charles 抓手机的包之后,发现可以抓取到一个 Get 请求,回包的内容也正确被抓取了,可是当我复制这段 Get 请求的 url 到浏览器的时候,发现没有对应的回包了

奇怪,为什么会这样呢,决定开始研究下

分析

url 是类似以下结构

1
http://api.tophub.today/nodes/128?nonce=xxx&sign=yyy&timestamp=123

主要有 3 个字段:noncesigntimestamp

基于这几个关键词去搜索,发现原来我这种行为属于【重放攻击】

什么是重放攻击

假如我们要通过 Get 请求传递用户的用户名和密码,明文传输的例子如下

1
http://api.tophub.today/nodes/128?id=hello&password=1234

这是裸奔,我们没这么蠢,于是对 password 进行 MD5 加密之后再传输,如下

1
http://api.tophub.today/nodes/128?id=hello&sign=md5(password)

其中 sign 的常用加密算法为 MD5,MD5 算法是一种不可逆算法,也就是说你加密之后就不能解密了

服务器的验证方法就是根据 id 从数据库查出用户的密码,再对其进行 MD5 加密,得到的结果与 sign 的值进行对比,如果相同则请求合法,反之则 sign 被篡改过

黑客通过抓包获取到该链接,他无法破解加密信息得到用户密码;但是他只要模拟此次的正常请求,就可以伪装成客户端同服务器通信。这样至少可以做两件事,第一件是跟我一样,可以随时随地模拟客户端取服务器的数据;第二件事是可以疯狂发送该请求致使服务器繁忙

如何防止重放攻击

时间戳

首先我们假设一次 HTTP 请求从发出到到达服务器的时间是不会超过 60s 的,而黑客获取链接到篡改链接再发送到服务的时间会超过 60s(不要问为什么是 60s,继续往下看)

当你发送一个请求时必须携带一个当前的时间戳 timestamp。假设值为 10

为了防止黑客修改时间戳,需要将 timestamp 也进行 MD5,放到 sign 中,如下

1
?id=hello&sign=md5(password+timestamp)&timestamp=10

当请求到达服务器之后,服务器会获取当前时间,假设为 t2 = 80,很明显 t2 - timestamp > 60s,那么服务器就认为请求不合法。因为这个请求从客户端到服务器的时间竟然超过 60s。如果在 60s 内,再进行一次 md5 校验,检查 timestamp 有没有被篡改(一旦 timestamp 被篡改过,算出来的 md5 值一定与 sign 不同)

问题来了,如果黑客在 60s 内发起请求,这种方法就失效了

随机数

我们加入一个随机数 nonce,每次成功请求,服务器会保存当前成功请求的随机数 nonce 到缓存或数据库中,当请求再次进到服务器,判断携带的随机数 nonce 是否在缓存或者数据库中已经存在,如果存在,则认为请求非法

url 如下

1
?id=hello&sign=md5(password+nonce)&nonce=31415

同理,为了防止 nonce 被篡改,需要将 nonce 进行 MD5 加密到 sign

随机数的出现保证了请求的唯一性,但是存储 nonce 的集合会越来越大,为了防止 nonce 集合无限大,需要定期清理该集合,但是一旦该集合被清理,我们就无法验证被清理了的 nonce 参数了。
比如,假设该集合平均1天清理一次的话,我们抓取到的该 url,虽然当时无法进行重放攻击,但是我们还是可以每隔一天进行一次重放攻击的。而且存储24小时内,所有请求的 nonce 参数,也是一笔不小的开销

当然随机数还得确保唯一性

时间戳+随机数

时间戳屏蔽了 60s 后的请求,而 60s 内重复的请求可以用随机数来过滤

url 如下

1
?id=hello&sign=md5(password+timestamp+nonce)&timestamp=10&nonce=31415

配合时间戳,服务器只要每次有新的请求进来的时候,确保此次请求没有被篡改的前提下,如果新请求带来的 timestamp 比最后一次更新 nonce 集合的时间晚 60秒,那么就可以清除 nonce 缓存了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 判断 time 参数是否有效
if ($curTime - $time > 60)
{
die("请求超时");
}

// 判断 nonce 参数是否在集合已存在
if (in_array($nonce, $nonceArray))
{
die("请求仅一次有效");
}

// 验证数字签名
if ($sign != md5($password.$time.$nonce))
{
die("数字签名验证失败");
}

// 判断是否需要清理nonce集合
if ($curTime - $nonceArray->lastModifyTime > 60)
{
$nonceArray = null;
}

// 记录本次请求的 nonce 参数
$nonceArray.push($nonce);

//开始处理合法的请求

参考

  1. 背景
  2. 分析
  3. 什么是重放攻击
  4. 如何防止重放攻击
    1. 时间戳
    2. 随机数
    3. 时间戳+随机数
  5. 参考