Python 的 .egg 文件和 OpenVPN Access Server 破解补丁小研究

近期由于某地方需要一个能推路由且平台兼容性好一点的 VPN(wg 和 IPSec 之类都因为需要本机配路由或是兼容问题不想在 Windows 平台折腾)于是盯上了 openvpn,然后又由于懒得自己折腾 CA,又稍微看了看 openvpn 的收费版本 openvpn access server(简称 ovpnas 或 oas),在不激活的情况下,oas 支持两个设备同时连接,个人自己用其实是够了的,但是我出于好奇搜了一下有没有相关破解,并发现这个所谓的破解就是个 .egg 文件,听说将其替换就能绕过未激活版本的设备限制,于是又稍微花了点时间研究这个所谓的破解是怎么实现的。

声明:本文基于技术研究角度,笔者不支持对商业软件的破解行为,也不会传播相关破解补丁

小谈 .egg 文件

首先了解一下 egg 文件是什么,egg 文件和 whl 一样,是 python 的一种打包重分发文件格式,有句话叫 “Eggs are to Pythons as Jars are to Java”,进一步说,egg 是 pip 出现之前的事实标准,而 whl 是随着 pip 出现的 egg 的后继者,其对比官方有说明:https://packaging.python.org/discussions/wheel-vs-egg/ ,但是实质上,egg 和 whl 都是 zip,它们的处理上大同小异,欲知更多关于 python 打包的问题可以参见:https://blog.zengrong.net/post/python_packaging/ ,本文不主要讨论 python 包的重分发问题,因此只讨论 egg。一个最最简单的 egg 文件都来自于这样的一个 setup.py,

from setuptools import setup, find_packages
setup(
    name = "demo",
    version = "0.1",
    packages = find_packages(),
)

可以使用python setup.py bdist_egg命令借助 setuptool 创建一个 egg 文件,创建后的目录大致如此:

demo
|-- build
| `-- bdist.linux-x86_64
|-- demo.egg-info
| |-- dependency_links.txt
| |-- PKG-INFO
| |-- SOURCES.txt
| `-- top_level.txt
|-- dist
| `-- demo-0.1-py2.7.egg
`-- setup.py

easy_install /path/to/egg 即可安装一个 egg 包,但正如先前提到的,egg 是一个比较历史久远的打包格式,实践中基本已经被 whl 所替代,所以最新的 setuptool 甚至已经没有了easy_install,可以手动执行 wget https://bootstrap.pypa.io/ez_setup.py -O - | python 进行安装,注意到安装中也有提示告诉你 ez_setup 已经过时:ez_setup.py is deprecated and when using it setuptools will be pinned to 33.1.1 since it’s the last version that supports setuptools self upgrade/installation, check https://github.com/pypa/setuptools/issues/581 for more info; use pip to install setuptools

一般来说 egg 包会被安装到/usr/local/lib/python2.7/dist-packages/(dist-packages 和 site-packages 的主要区别在于前者是使用包管理器时的安装目录,apt 和 pip 都将文件安装在 dist-packages),然后就可以在 python 交互 shell 或是其他 py 文件中直接 import module

OAS 的破解逻辑

说完了 egg 文件是个啥,回来看看我们最初的目的,由于找到的这个补丁是针对 2.5 版本的,也想看看相同的破解套路能不能用于最新的 2.7.5 版本(这个 egg 文件包含了大部分 OAS 的业务逻辑,直接覆盖当然是不行的)。由于 egg 文件的本质是 zip,直接对 egg 文件解压后和原版解压结果进行 diff,根据 diff 结果发现主要改动在 /pyovpn/lic/uprop.pyo,从文件名来看 uprop.py 是 Usage Properties 的意思,但是 pyo 文件是 python 生成的字节码,我们还需要使用工具对字节码进行反编译,这里使用 https://github.com/rocky/python-uncompyle6 ,用法很简单,uncompyle6 /path/to/pyo > /path/to/py 即可,最终我们看见的实质性破解逻辑就只有这么几行:

import uprop2
old_figure = None

def new_figure(self, licdict):
    ret = old_figure(self, licdict)
    ret['concurrent_connections'] = 1024
    return ret


for x in dir(uprop2):
    if x[:2] == '__':
        continue
    if x == 'UsageProperties':
        exec 'old_figure = uprop2.UsageProperties.figure'
        exec 'uprop2.UsageProperties.figure = new_figure'
    exec '%s = uprop2.%s' % (x, x)

发现实际上这个 uprop2 就是原本的 uprop 模块改名,原补丁通过暴力改名的方式将原本去往 uprop 的方法劫持掉,附上原 uprop 模块的部分代码逻辑:

class UsageProperties(object):

    def figure(self, licdict):
        proplist = set(('concurrent_connections', ))
        good = set()
        ret = None
        if licdict:
            for key, props in licdict.items():
                if 'quota_properties' not in props:
                    print 'License Manager: key %s is missing usage properties' % key
                    continue
                proplist.update(props['quota_properties'].split(','))
                good.add(key)

        for prop in proplist:
            ···
            apc = self._apc()
            v_agg += apc
            if ret == None:
                ret = {}
            ret[prop] = max(v_agg + v_nonagg, bool('v_agg') + bool('v_nonagg'))
            ret['apc'] = bool(apc)

        return ret

    def _apc(self):
        try:
            pcs = AWSInfo.get_product_code()
            if pcs:
                return pcs['snoitcennoCtnerrucnoc'[::-1]]
        except:
            if DEBUG:
                print Passthru('UsageProperties._apc')

        return 0

    @staticmethod
    def _expired(today, props):
        if 'expiry_date' in props:
            exp = YYYYMMDD.validate(props['expiry_date'])
            return today > exp
        else:
            return False

class UsagePropertiesValidate(object):
    proplist = ('concurrent_connections', 'client_certificates')

    def validate(self, usage_properties):
        ···
        return lp

可以看到UsageProperties.figure主要是一系列对授权有效期、授权数量等,然而没有对授权的内容(也就是 concurrent_connections)进行整体的 hash+ 签名防止篡改,因此原补丁使用exec(作用是执行一句或多句语句,和 eval 的主要区别是 exec 在 python2 中是一个内置语句,在 python3 中是函数且永远返回 None,exec 修改的变量可以在当前作用域外生效)直接修改 figure 方法的返回值,令 concurrent_connections 始终为 1024。

通过比对发现 OAS 在 2.5-2.7.5 版本之间验证授权的逻辑没有任何变化,将类似修改应用到最新的 egg 文件即可生效,python -O -m compileall /path/to/pyo 可以将 py 文件重新编译成 pyo(理论上 python runtime 不区分 py 和 pyo/pyc,只要文件名前缀相同就能运行,但是可能性能会差一些),直接将修改后的目录 zip 后覆盖回该 egg 的原本路径,重新初始化 OAS 即可生效。

OAS 很有一贯的老外写的企业级商业软件的风格,可以说是只要稍微有意就没有破解难度,虽然另一方面也是 Python 这种语言本身加密困难的原因,但是想要恶心一下我这种三脚猫选手还是没有什么问题,几个版本都没有改过验证授权逻辑且基本没有做校验和暗桩,相比行业毒瘤某杰某克丁而言实属良心,因此我也无意扩散 OAS 的破解,Just hacking for fun。


本文链接:https://www.starduster.me/2019/12/19/talk-about-egg-file-and-hacking-ovpnas/
本站基于 Creactive Commons BY-NC-SA 4.0 License 允许并欢迎您在注明来源和非商业使用前提下自由地对本文进行复制、分享或基于本文进行创作。
请注意:受限于笔者水平,本站内容可能存在主观臆断或事实错误,文中信息也可能因时间推移而不再准确,在此提醒读者结合自身判断谨慎地采纳。

5 条评论




  1. 安装2.7.5后,修改/usr/local/openvpn_as/lib/python2.7/site-packages/pyovpn-2.0-py2.7/pyovpn/lic/uprop.pyo文件后,会报错

    Detected an existing OpenVPN-AS configuration.
    Continuing will delete this configuration and restart from scratch.
    Please enter ‘DELETE’ to delete existing configuration: DELETE

    OpenVPN Access Server

    Initial Configuration Tool

    Error: Could not establish license validation machine lock.
    Access Server will not be able to activate a license key on this host.
    Please see http://www.openvpn.net/access-server/rd/liman-id-failed.html

    回复

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据