[转载] JetBrains License Server 原理及 40 行 Python 代码的实现

 文章转自:https://www.bennythink.com/jbls.html 作者: Benny 小土豆

 

 

 

JetBrains License Server 原理及 40 行 Python 代码的实现

不懂编程 Benny 小土豆 2 年前 (2018-02-06 10:53) 11810 次浏览 5707 字 52 个评论
文章目录 [显示]
这篇文章在 2018 年 08 月 31 日 11:08:55 更新了哦~

Repository:
开源地址

警告:
本文仅供学习参考,请勿用于其他用途。如果你喜欢 JetBrains 的产品,请购买正版授权。

 

近期 license server 升级,可以阅读官方的这篇文章,在 2018.2.1(2018 年第三季度左右发布)之后旧版服务器将无法使用,所以先别升级到 2018.2.1,留在 2018.2 吧。关于新的激活服务器,似乎有点难度,欢迎各位提供任何可能会有帮助的资料和工具(包括但不限于本地激活器,license server,以及相关介绍文章和抓包记录等),更多详情请参考文末记录

估计是个程序员都应该使用过 JetBrains 的 IDE(尤其是 Python 程序员吧)。JetBrains 的产品线非常丰富,PHP 的 PHPStorm,前端的 Webstorm,Python 的 Pycharm,Java 的 IntelliJ,Ruby 的 RubyMine,Go 的 Goland,数据库的 DataGrip……

几个月之前,我曾经尝试着把 Java 版本的 License Server 移植到 Python,但是失败了。但是今天我就成功了…… 好了不废话了,咱来一步一步的学习下怎么用 40 行代码写一个 JetBrains License Server

JetBrains License Server 激活流程

打开 IDE,依次点击 Help-Register,输入服务器地址,点击 Activate

JetBrains License Server原理及40行Python代码的实现

这时就会从 License Server 取得一个 Ticket 了。

友情提示:

如果你有 edu 邮箱的话,那快去申请免费正版授权吧,哪怕你没有 edu 邮箱,也可以试试什么注册 edu 邮箱的,比如说这个

抓包与分析

网上已经有人写好了几个版本的授权服务器,我参考了 Java 版的、golang 版的还有 JavaScript 版的。

于是我找了那个能用的 Java 版本的,自己搭建,然后尝试激活、抓包并过滤 http,进行分析。

通过追踪 HTTP 流我们能发现,IDE 向指定服务器发起了一个 GET 请求,请求中包含很多参数,之后服务器返回了一个像是散列值,还有一小串 XML

JetBrains License Server原理及40行Python代码的实现

IDE 请求:

  1. GET /rpc/obtainTicket.action?buildDate=20170719&buildNumber=2017.2.2+Build+CL-172.3968.17&clientVersion=4&hostName=xxxxxxm&machineId=51xxxxx20a-14d259f1fc94&productCode=cfc7xxxx978-a2a2-46fxxxx405&productFamilyId=cfxxxx-ae43-4978-a2a2-46feb1679405&salt=1506491409302&secure=false&userName=xxxx&version=2017200&versionNumber=2017200

服务器返回:

  1. <!-- 61b3a2f53ffxxxxxxxxx6b132e0270275d12434f217ba3231b5 -->
  2.  
  3. <ObtainTicketResponse><message></message><prolongationPeriod>607875500</prolongationPeriod><responseCode>OK</responseCode><salt>1506491409302</salt><ticketId>1</ticketId><ticketProperties>licensee=xxxxx licenseType=0 </ticketProperties></ObtainTicketResponse>

由此我们可以很清楚的了解到激活的步骤:请求 + 相应 + 验证,那么大体猜测一下应该是服务端持有一个私钥,对请求字符串进行加密,然后返回,客户端使用公钥验证。

验证猜想

为了验证我们的猜想,咱其实是要去读其他版本的源代码的…… 鉴于这个流程挺复杂,要求你会多门语言,所以咱就不演示了…… 总之,经过我的研读,我发现的细节是这样的:

服务端构造如下的字符串:

  1. <ObtainTicketResponse><message></message><prolongationPeriod>607875500</prolongationPeriod><responseCode>OK</responseCode><salt>1506491409302</salt><ticketId>1</ticketId><ticketProperties>licensee=xxxxx licenseType=0 </ticketProperties></ObtainTicketResponse>

其中 salt 是发起请求时的时间戳(单位毫秒),licensee 是发起请求的用户名(其实这个用户名是随便的,在代码里写死也是可以的)。

然后服务器使用 RSA with MD5 and PKCS1v15 进行加密,把加密后的内容转换为十六进制,加上 HTML 注释 <!-- hex -->(注意前后的两个空格),把十六进制 HTML 注释和构造的字符串一起返回,客户端使用 hex 和返回的字符串(以及公钥)进行验证。

其实验证猜想的这一步才是最复杂的,需要参考很多源代码做出适当的实验。

既然猜想已经得到了证实,那么剩下来就开始写了。基础的结构很简单,用 flask 搭建一个简易的服务器,用 cryptography 完成密码学相关的操作。

cryptography 完成 RSA 签名与十六进制转换

Python 中比较安全好用的密码学库是 cryptography(唉 好吧)、pycrypto(这货很久不更新了,不敢用),顺便说一句,进行安全随机盐散列函数的有 passlib

首先需要引入对应的库

  1. from cryptography.hazmat.backends import default_backend
  2. from cryptography.hazmat.primitives import hashes
  3. from cryptography.hazmat.primitives import serialization
  4. from cryptography.hazmat.primitives.asymmetric import padding

然后载入 key(文件形式)

  1. with open('jbls_private_key.pem') as f:
  2. private_key = serialization.load_pem_private_key(f.read(), password=None, backend=default_backend())

执行加密

  1. i = bytes(content)
  2. signature = private_key.sign(i, padding.PKCS1v15(), hashes.MD5())

转换二进制为十六进制表达

  1. binascii.hexlify(signature)

大概这么五行,我们就完成了加密与转换操作,剩下我们只要在 Flask 端做一下逻辑处理就好了。

Flask 进行逻辑处理

这一步也很简单,用@app.route()装饰器确定路由,构造字符串与返回。真的是很简单,大概就是如下这么十几行:

  1. @app.route("/rpc/obtainTicket.action")
  2. def obtain():
  3. salt = request.args.get('salt')
  4. user_name = request.args.get('userName')
  5. content = "<ObtainTicketResponse><message></message>" + \
  6. "<prolongationPeriod>607875500</prolongationPeriod><responseCode>OK</responseCode>" + \
  7. "<salt>" + salt + "</salt><ticketId>1</ticketId>" \
  8. "<ticketProperties>licensee=" + user_name + "\tlicenseType=0\t</ticketProperties>" \
  9. "</ObtainTicketResponse>"
  10. verification_hex = hex_signature(content)
  11. return "<!-- " + verification_hex + " -->\n" + content

通过 Flask 的 requests 拿到 salt 和 username,构造 XML,调用函数,返回 hex 以及 XML。

所以加起来,整体代码真的只有不到 40 行。就算把 KEY 嵌入到源代码中,也只是 43 行而已……

最终结果

服务端的请求日志

JetBrains License Server原理及40行Python代码的实现

客户端显示激活成功

JetBrains License Server原理及40行Python代码的实现

附录与 FAQ

监听 0.0.0.0

如果想要把这个服务部署在服务器上,那么最好就监听 0.0.0.0,只需要把 app.run() 写成 app.run(host='0.0.0.0') 即可。

持久运行

当然我们希望这个 Flask 程序能够长期运行而不退出,此时我们最好使用 supervisor 或者 systemd 啦,详细参考此篇《Linux 怎么让程序持续运行:简单说说几种好玩的办法》

示例源代码

本示例源代码可以到下面的地址查看:
开源地址

为啥不放到 GitHub 呢?被 DMCA Takedown 啦ε=ε=ε=┏(゜ロ゜;)┛

需要注意的是,需要使用 pip 安装 cryptography、flask,并且 jbls.py 同时支持 Python 2 和 Python 3.

可否提供一个有效的私钥

不好意思,不提供…… 至于我这个测试的私钥是在哪找的…… 你们猜呀。

如何生成 exe 文件

使用 pyinstaller 就可以了。

  1. pyinstaller -F demo.py

travis-ci

我们用 travis-ci 做持续集成测试,但是 travis-ci 并不是为了运行程序而准备的。但是我们这个 Flask 应用还必须得运行才能测试。那咋办呢?subprocess.Popen()?反正各种奇技淫巧都试过了,不好用的。劝大家还是试试app.test_client().get(url).data吧,这样就能获取到响应然后 assert 了。

 

新版 JetBrains License Server 备注

以下内容全文引用自 lanyus 评论

贵站居然没有过滤 html 的特殊字符?重新发一下吧,以下是原文:

Jetbrains 家产品线激活服务器认证从 2018.2 开始使用两个 handler 来处理,第一个 handler 会对老的签名方式进行认证(<!-- abcdef1234567890 --><ObtainTicketResponse></ObtainTicketResponse > 类样式),该 handler 会判断签名是否包含 hex 之外的字符。如果没有,按老版本方式来验证签名,如果存在 hex 之外字符则抛异常由自己接住后进入第二个 handler 来验证。
从版本 2018.2.1 开始便去掉了第一个 handler,改为只由第二个 handler 来验证,由此老的验证服务器全面失效。
来说说第二个 handler 的验证逻辑吧:
新的报文返回还是形同:<!-- sign_body --><xxxResponse></xxxResponse>,其中 < xxxResponse></xxxResponse > 这样的响应正文中相比较老版本添加 < serveruid>xyz</serveruid > 配置,这个 xyz 后文中有用到,不可缺少。
sign_body 格式较老版本的 hex 字符串变动很大,其示例格式如下:SHA1withRSA-xxxxxxxxxxxxxx-yyyyyyyyyyyyyyy,以 - 符号分隔,SHA1withRSA 对应 java 中签名所使用的算法(可换为 MD5withRSA 等),xxxxxxxxxxxxxx 部分是使用该算法以私钥签名的 bin -> base64 字符串,yyyyyyyyyyyyyyy 部分则是 pem 格式的证书字符串(可直接放证书正文,不换行)。
其中 yyyyyyyyyyyyyyy 证书必须由 Jetbrains 产品内置的一个 CN 为 License Server CA 的 CA 证书签发才有效,而且 yyyyyyyyyyyyyyy 证书中必须包含 CN=xyz.lsrv.jetbrains.com(其中 xyz 对应前文 < serveruid>xyz</serveruid > 部分所填)。
证书验证通过后才会取出证书中所包含的公钥来验证签名 <xxxResponse></xxxResponse > 响应正文(和 base64Decode(xxxxxxxxxxxxxx) 比对),验证通过后继续完成服务器认证(解析正文内容,逻辑同旧版)。
啰啰嗦嗦这么多,总结一下:如果我们想要自己造验证服务器,则需要拿到一张 License Server CA 签发的证书(包括私钥),而且这个证书被 Jetbrains 发现可能会被吊销!所以这个恐怕有些痴人说梦了。
对了 License Server CA 证书的内容由另一张 Jetprofile 签发的叫 prod3y 的证书公钥定时校验(数据格式同上文所述,不过正文部分是 CA 证书字符串),应该是防止被替换吧。
还有类似 idea.lanyus.com 这样的认证服务器域名黑名单也由这张 prod3y 证书公钥定时校验,也是怕被篡改吧。如果要替换 CA 要从替换 prod3y 证书开始,但那是破解的内容了,不如注册服务器优雅。
就这么多。

 

所以估计 License Server 这条路可能走不通了,只能通过 javaagent 这种方法修改客户端(IDE)代码然后达到目的了。


文章版权归原作者所有丨本站默认采用 CC-BY-NC-SA 4.0 协议进行授权 |
转载必须包含本声明,并以超链接形式注明原作者和本文原始地址:
https://www.bennythink.com/jbls.html
posted @ 2020-03-07 19:16  蓝天上的云℡  阅读(2644)  评论(0编辑  收藏  举报