Aoang's Blog

岁月在电波中流淌,人生在音乐中升华

QQ 安全中心动态密钥

Aoang's Avatar 2020-08-30

不得不说,有现成的技术方案不用,非要跑去魔改一个,这真是国内众多大厂的特征之一,QQ 安全中心的动态密钥就是如此。

现有的 One Time Password 方案大多分为 HOTP 和 OTOP,前者基于事件,后者基于时间。
在两步验证的安全策略中,大多数都是采用基于时间的 OTOP 算法。OTOP 算法一般来说都是通用的,因为都是遵循 RFC6238 来实现的。
而 QQ 安全中心的算法是魔改 RFC6238 实现的,导致除了它自己的客户端,其他的 OTOP 客户端都没办法用。

这里就不得不吐槽了,QQ 安全中心已经被砍成残废了,但是不知道为什么,还是有部分服务是依赖它的。
比如,某龙的微信全面抛弃了这种带有 QQ 字眼的安全中心,但是他的邮箱还严重依赖 QQ 安全中心。

平时基本上不怎么用,万年都不开一次,有时候还会风控你,过于依赖手机,只能想办法迁移到其他平台,比如 Bitwarden。
可惜这种丧心病狂的魔改版,没有什么其他软件会兼容,当然腾讯手机管家这种要吞掉 QQ 安全中心的除外。

不过幸运的是找 Github 上找到了 HyperSineforensic-qqtoken ,里面描述了算法的思路、公式及其解密方法。

能解密就好说了,谁骚操作不是一大堆。

很麻利的从手机把相关文件拖到电脑上了,然后跑 Python 脚本。
嗯?怎么第一步就报错了,啥情况?哦,是 WSL 里的 Python 没 cryptography,给它装上。继续跟着教程傻瓜式的走哇,等会等会,WSL 没装 sqlite3…看着 zsh: command not found: sqlite3 不知道是该感叹自己手太快了还是脑袋太飘了。

提取出密钥,验证一下生成的验证码是不是和 QQ 安全中心一致,好像完事了?

嗯?我是谁?我在哪儿?我不可能每次用都给它跑一遍吧?这在手机上的话,难不成还先 SSH 到服务器上跑个验证码下来再填?

不行不行,这个轮子它又方又…太方了,得想个办法整成六边形的。

要不,咱写个 API 然后起个服务器跑?好像有点浪费哈,先不说这东西平时根本用不上,API 服务做的方便就不安全,做的安全又不方便,很难两全其美,这个不行不行,这最多就是五边形。

或许可以通过 Telegram Bot 来交互?这样似乎可行?但是单独起个服务还是不太爽的样子,这勉勉强强算是六边形了。

不想这种东西占用自己的服务器资源的话,该怎么做呢?陷入沉思…

要不,用 GitHub Action 定时把一段时间的验证码全部跑出来,然后把功能集成到自己的 Bot 里,让它直接去读 GitHub 仓库的内容?怎么好像越来越复杂了…这肯定是个三角形!

Vercel 不是有免费的 Serverless 可以用吗?

利用 Telegram Webhook 的机制,有消息传入的时候 Telegram 调起 Serverless,生成验证码后发送给用户。

这个思路好像不错,要不,实现一个?这应该算是个椭圆形了吧,推它一把它能自己滚一圈儿,符合椭圆形的标准。

出于对 Python 的「喜爱」,我决定使用 Go 来写。
首先,根据 HyperSine 写的生成函数 重写了一个 Go 版本的。

在用非常规标准(用 main 函数跑测试,跑完了就把测试代码删了的人还有没有?)完成了函数的单元测试之后,开始考虑下一步该怎么做。

平时使用 OTOP 的时候,大部分服务是有容差的。

啥是容差?一看就知道你没看上面发的 RFC6238,这里不发了,自己搜去。

OTOP 默认是 30 秒一刷新,很多服务都允许你有时间误差,具体没测试过。
实现机制可能就是简单的计算了一下当前时间的验证码、已经失效的一个验证码,然后拿着你输入的进行对比。

因为不清楚 QQ安全中心是否有容差机制,再加上是使用 Telegram 而不是专业的密码管理器,很大概率是需要手动复制的,所以就多生成几个。
这样可以避免因为网络问题,验证码一再过期这种情况。

那么就根据当前时间计算出当前的验证码,然后再计算出当前验证码的前一个和后一个,在 Telegram 上以这种样式来展示 593111 | 243735 | 166949 刚刚好。
当前验证码和未来的验证码可能是需要复制的,所以使用等宽,这样在 Telegram 上点击即可复制。
已经失效的验证码使用删除线显示,毕竟只是需要看看,没有其他的特殊需求了。

接下来需要测试一下 Webhook,毕竟之前没有做过这种用 Serverless 来实现 Telegram Bot 的骚操作,稳一点比较好。

先使用默认的轮询获取了几次 Telegram 发给自己的数据。然后将代码调整为 Webhook 模式,监听本地 127.0.0.1,用 Postman 给程序发送之前 Telegram 给我发过的消息。

嗯,测试完成。

这里有个题外话,Telegram 设置 Webhook 的 URL 应当是不被暴露的,因为 Telegram 给你发送的消息是没有鉴权机制的。
如果有人知道了这个服务的 Webhook URL,然后拿到了账户的 Telegram ID,就可以通过不断的给 Webhook URL 发送消息完成来骚扰。

这个问题有没有办法解决呢,办法是有的,检查来源 IP 是不是 Telegram 的就好了。

来自 149.154.160.0/2091.108.4.0/22POST 请求就是 Telegram 的服务器,当然还有一些不怎么需要提及的要求,比如必须能处理 TLS1.2+ 的 HTTPS 流量,还需要服务器提供经过验证的非通配符证书,或者是自签名证书。
貌似 Vercel 默认给你提供的就是通配符证书,不过我绑定了域名,不受这个影响。

又想到了一个安全问题,任何账户给这个机器人发消息都能获取到验证码可不行呢,但是我 Telegram 账户有点多,这个问题怎么整?

将自己的 Telegram ID 存为数组,收到消息时,拿到 Telegram ID 后查一遍数组,看看是否存在?然后再发送验证码?

这种办法只能自己一个人用啊,如果服务共享给其他人呢?
map[int64]*Data 来实现,Telegram ID 是 int64 的,将 Secret 等数据丢入 Data 结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Data struct {
Secret string
}

m := make(map[int64]*Data)

m[123456789] = &Data{}

myData := &Data{
Secret: "qwertyuiop",
}

m[10001] = myData
m[10002] = myData
m[10003] = myData

m[40001] = &Data{
Secret: "asdfghjkl",
}

一对一和多对一就完成了,至于多对多那就算了吧。
多对多的实现想要人性化一点,就得折腾 Telegram 那个什么鬼键盘,然后根据键盘里面的回调数据来让程序生成验证码。多对多的那个多有点多的时候,还得考虑键盘的布局翻页。
这么复杂,谁这么玩,打死他!

不过如果有人愿意自己挖坑自己埋,弄出来之后还比较好用的话,请告诉我!


代码整理了一下,放在 GitHub 上了,有需要的可以自己拿去改改,也可以直接弄去部署。
但是因为安全问题,记得认真看说明,不然小心裤子没了。