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/
本站允许也欢迎您:自由地对本文进行复制、分享或基于本文进行创作
但您需同意并遵守:对本文署名并标记来源、使用相同方式共享、不将其用于商业用途
更严谨和完整的声明请参见关于本站内容许可

发表评论

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

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