岭猿 发表于 2025-10-21 22:10:02

做了一个概率小游戏,没想到服务器被打爆被攻击了!原因竟然是他?真没想到...

1. 前言

事情是这样的,上个月在刷知乎的过程中,发现了以下几个有趣的问题。

[*]《每毫秒给你1个亿,代价是你每秒被动触发一次1亿分之一的死亡率,你愿意吗?》
[*]《“100%概率获得200万”和“99%概率获得2个亿”,你选哪个?》
作为程序员,看着这种概率与决策,感觉非常有趣,有时候常在想,能不能用程序模拟一下,选择哪个选择,我最终取胜的概率最大呢?
于是就有了我的服务器被打爆攻击的事情了,欲哭无泪。让我给大家讲讲我怎么和攻击者在线上斗智斗勇的。
先给大家简单看看这个游戏的效果。

[*]体验地址
[*]github源码地址



2. 事件过程

2.1 事情起因

这个小游戏是非常简单的,完全看个人运气,有些人可能运气就能抽中比较长的时间,有些人可能运气非常差抽中时间很短。因此在10.6号左右为了增加游戏的趣味性,我就上线了排行榜机制!!!
万万没想到,大家的‘斗志’实在太高了,有些人通过爬取我的后端接口,给自己一个非常夸张的数据,让自己排第一名,也就是他也是一名程序员,然后通过绕过前端的手段,直接给我后端放进夸张的数据。当时用户名满天飞,什么‘xxx一日游’, ‘我是第一名’,‘比不过我吧’等等名称满天飞。
作为资深程序员,我能忍?平时的八股文派上用场了。
2.2 第一回合 - 防重放


[*]先做一些简单的数据校验,比如用户名的长度,数据的范围等等,非常的基础
[*]对前端的UA,REFER等做一些基础的校验
[*]加一个token校验,也就是前端要通过某些规则生成一个token传给后端,后端在根据这个规则来校验这个token是否合法,如果不合法,则直接拒绝,说明用户是非法请求,代码如下
public static boolean extractSecret(StringRedisTemplate redisService, String timestamp, String token, TreeMap<String, String> map) {
        if (StringUtils.isEmpty(timestamp) || StringUtils.isEmpty(token)) {
                return false;
        }
        long ts = NumberUtils.toLong(timestamp, 0);
        long now = System.currentTimeMillis();
        if ((now - ts) > SecretUtils.NONCE_DURATION) {
                return false;
        }

        StringBuilder sb = new StringBuilder();
        map.put("salt", SALT);
        for (Map.Entry<String, String> entry : map.entrySet()) {
                String key = entry.getKey();
                String value = entry.getValue();
                if (sb.length() > 0) {
                        sb.append("&");
                }
                sb.append(key).append("=").append(value);
        }

        String targetToken = DigestUtils.md5DigestAsHex(sb.toString().getBytes());
        if (!token.equals(targetToken)) {
                return false;
        }

        String s = redisService.opsForValue().get(timestamp);
        if (StringUtils.isNotEmpty(s)) {
                return false;
        } else {
                redisService.opsForValue().set(timestamp, timestamp, NONCE_DURATION, TimeUnit.MILLISECONDS);
        }

        return true;
}       

[*]首先前端会生成一个时间戳,然后将时间戳+所有请求的参数,通过字典序进行排序
[*]排序之后,生成一个类似于 a=1&b=2&c=3 的字符串,然后使用md5生成一个token传给后端
[*]后端接收以后,首先判断时间戳是否过了很久,保证不是手动生成的
[*]其他根据规则自己也生成一个token,比较两个token是否相同,为了保险起见,一般双方会约定一个salt盐这个一个参数,起到混淆视听的作用
[*]最后后端会把这个时间戳、token放进redis,保证一个token只能使用一次,使用了之后就不能再次使用了
通过这个防重放的防御,大部分水货程序员就会拦截在了门外
2.3 第二回合 - 前端js加盐混淆视听

过了一段时间,攻击者竟然破解了我的加密手段,针对token的生成规则,有些大佬可以通过f12非常方便的看到前端的代码,然后获取到规则,然后利用代码进行攻击,于是乎

[*]针对前端盐,我进行了混淆视听,举一个例子
[*]比如我的salt=abc 现在我换成如下代码,你还能看得懂么?
// 签名生成核心
const _0xsig = {
// 混淆配置矩阵(多层编码)
_0xa1: ,
_0xa2: ,
_0xa3: ,
_0xb1: ,
_0xb2: ,
_0xb3: ,
_0xc1: 0x1a2b,
_0xc2: 0x3c4d,
_0xc3: 0x5e6f,

// 生成签名
_0xgen(_0xdata) {
    const _0xt = Date.now()();
    const _0xp = { ..._0xdata, timestamp: _0xt };
   
    // 提取密钥和值
    const _0xkeys = this._0xextK();
    const _0xvals = this._0xextV();
   
    // 构建参数对象
    const _0xall = { ..._0xp };
    for (let _0xi = 0; _0xi < _0xkeys.length; _0xi++) {
      _0xall] = _0xvals;
    }
   
    // 排序并拼接
    const _0xks = Object.keys(_0xall).sort();
    const _0xstr = _0xks.map(_0xk => `${_0xk}=${_0xall}`).join('&');
    const _0xtk = _0xmd5(_0xstr);
    return { ..._0xp, token: _0xtk };
},

// 创建签名
create(_0xparams) {
    return this._0xgen(_0xparams);
}
};大概率你看的很懵逼,这种方式一般人几乎破解不了,除非通过AI进行分析

[*]我通过对字符a等进行16进制,然后通过增加多盐的方式,增加攻击者的攻击成本
[*]像网易云、知乎等都是采用这种方法
2.4 第三回合 - IP限流

过了一段时间,攻击者又又又破解了,并且好像非常生气,开始恶意请求我的接口了,通过脚本一直刷我的接口,让我的服务器直接挂掉,当时我的服务器承受不住这么高的流量,就直接重启了,重启后,又被打爆,我当时真的特么无语了,对这种人。
而且我的服务器的流量一直被刷,都快刷欠费了,真的不能忍,我都想直接把应用给下线了。当然作为资深程序员怎么能忍受

[*]针对高频IP地址进行限流,比如1s内请求10s,10s内请求100次的ip,肯定不是一个正常用户,是一个非法用户,直接封禁
[*]加密代码不再开源(之前一直开源,感觉攻击者偷偷看我的commit,我在明他在暗,怎么玩),直接修改salt参数,并且启用多重盐,让你怎么破解,具体限流代码如下
private boolean checkRateLimit(String ip, String uri, HttpServletResponse response) throws IOException {
    // 1. 检查是否在黑名单中
    String blacklistKey = BLACKLIST_KEY_PREFIX + ip;
    String blacklistValue = stringRedisTemplate.opsForValue().get(blacklistKey);
    if (blacklistValue != null) {
      Long ttl = stringRedisTemplate.getExpire(blacklistKey, TimeUnit.SECONDS);
      log.error("IP黑名单拦截 - IP={}, URI={}, 剩余时长={}秒", ip, uri, ttl);
      writeErrorResponse(response, "签名验证失败");
      return false;
    }

    // 2. 检查访问频率
    String rateLimitKey = RATE_LIMIT_KEY_PREFIX + ip;
    String countStr = stringRedisTemplate.opsForValue().get(rateLimitKey);
   
    long count = 0;
    if (countStr != null) {
      count = Long.parseLong(countStr);
    }

    // 3. 递增计数
    Long newCount = stringRedisTemplate.opsForValue().increment(rateLimitKey, 1);
   
    // 4. 如果是第一次访问,设置过期时间
    if (count == 0) {
      stringRedisTemplate.expire(rateLimitKey, RATE_LIMIT_WINDOW, TimeUnit.SECONDS);
      log.info("IP限流 - IP={}, {}秒内第1次请求{}", ip, RATE_LIMIT_WINDOW, uri);
      return true;
    }

    // 5. 检查是否超过限制
    if (newCount > RATE_LIMIT_MAX_COUNT) {
      // 超过限制,加入黑名单
      stringRedisTemplate.opsForValue().set(
                blacklistKey,
                String.valueOf(newCount),
                BLACKLIST_DURATION,
                TimeUnit.SECONDS
      );
      
      log.error("IP限流触发 - IP={}, {}秒内请求{}次(限制{}次),已拉黑{}秒, URI={}",
                ip, RATE_LIMIT_WINDOW, newCount, RATE_LIMIT_MAX_COUNT, BLACKLIST_DURATION, uri);
      
      writeErrorResponse(response, "签名验证失败");
      return false;
    }

    // 6. 正常通过,记录日志
    Long ttl = stringRedisTemplate.getExpire(rateLimitKey, TimeUnit.SECONDS);
    log.info("IP限流 - IP={}, {}秒内第{}次请求{}(限制{}次),剩余{}秒",
            ip, RATE_LIMIT_WINDOW, newCount, uri, RATE_LIMIT_MAX_COUNT, ttl);
   
    return true;
}大概的意思就是请求多少秒内请求超过多少次,我就认为你不是一个正常用户,直接封禁即可。
2.5 第四回合

别看了,木有了,又又又又被破解了,我实在没招了,看看评论区的大佬们有没有什么好的办法支支招
3. 最后

通过这个例子,我们发现攻击者与我们一来一回,真所谓是道高一丈,魔高一丈。攻击者力量比较大,毕竟人多。
我们简单总结一下,我们大概有以下技术手段可以防止攻击者的攻击

[*]后端的一些基础数据校验,比如针对用户名,存活时间,浏览器UA等等
[*]防重放token校验,通过和前端约定一些规则,通过规则来生成token,防止恶意请求
[*]在token校验的基础上,我们使用了salt盐,并且对盐的生成进行了混淆,导致攻击者的攻击成本非常的高
[*]针对大量脚本刷接口的行为,我们利用redis进行了ip限流,如果在某个时间内请求超过了某个次数,直接禁止请求
基本上通过以上技术手段,我们可以拦截99%的恶意请求了,你还有更好的防攻击手段么,欢迎评论区留言讨论。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

零幸 发表于 2025-10-27 00:46:50

谢谢楼主提供!

吉芷雁 发表于 2025-11-11 09:51:09

鼓励转贴优秀软件安全工具和文档!

旁拮猾 发表于 2025-12-5 21:15:30

收藏一下   不知道什么时候能用到

挚魉 发表于 3 天前

谢谢分享,辛苦了
页: [1]
查看完整版本: 做了一个概率小游戏,没想到服务器被打爆被攻击了!原因竟然是他?真没想到...