Python的License授權機制

注:轉載 https://www.csdn.net/tags/MtjaEgxsMjE3NDktYmxvZwO0O0OO0O0O.html

* python腳本很難應用於收費工具領域。
那麼如果我想對一個Python工具的使用加以限制,有沒有什麼方法可以實現呢?本文提供一種簡易的License授權機制實現思路,它主要包括如下幾部分內容:
  -如何生成一個加密的License文件。
  -如何做License內容覈驗。
  -如何加密Python代碼。


其主要流程邏輯如下:

 

 

一、引題

       我寫了一個python腳本eda.py,內容如下。

#!/usr/bin/env python3


def function():

    print('I am an EDA tool!')

function()

 

       我在Linux操作系統中執行了這個程序,成功了,輸出如下。

 $ ./eda.py

  I am an EDA tool!

       我決定把這個“牛逼”的腳本分享給別人使用。但是主要的障礙在於,我不希望別人隨便傳播這個腳本,不希望別人無限期使用這個腳本,不希望別人學會這幾行代碼的寫法,於是我決定對它加上License限制。

 

二、生成License文件

       工具License常在如下方面對工具的使用加以限制:

  1. 使用的MAC地址。(防止工具被隨意拷貝使用)
  2. 使用期限。(過期失效)

       更加複雜的應用場景,還可以對License使用數量、用戶列表、工具feature等因素加以限制。

       按照這個需求,我們要設計一個簡化的License文件,它需要包含以下內容:

  1. MAC :允許工具啓動的機器的MAC地址。
  2. Date :工具有效期。
  3. Sign :簽名,對以上明文內容的加密,用作內容覈驗。

       首先,我們可以用下面的Linux指令獲取機器的MAC地址。

 $ ifconfig -a

  ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500

        inet 192.168.246.128  netmask 255.255.255.0  broadcast 192.168.246.255

        inet6 fe80::7e02:97:820d:1bd3  prefixlen 64  scopeid 0x20<link>

        ether 00:0c:29:4a:d4:6c  txqueuelen 1000  (Ethernet)

        RX packets 1491722  bytes 1542769755 (1.4 GiB)

        RX errors 0  dropped 0  overruns 0  frame 0

        TX packets 533500  bytes 32951033 (31.4 MiB)

        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

 

       在centos7系統下,可以從ether行抓取機器的MAC地址。其他的操作系統略有不同。

       我們限制工具的使用期限,比如2021.05.20。

       最後我們將如上信息加密。

       常見的加密算法有AES(對稱加密)和RSA(非對稱加密),其中AES因執行速度快和硬件支持(部分Intel的處理器支持ASE指令加速)常用作批量內容的數據加密,RSA則由於其公鑰-私鑰機制常用於祕鑰傳輸。此處我們選擇AES加密方式來加密MAC /Date信息。

       下面直接給出生成License文件的腳本gen_license_file.py。

#!/usr/bin/env python3

from Crypto.Cipher import AES
from binascii import b2a_hex

def encrypt(content):
    # content length must be a multiple of 16.
    while len(content) % 16:
        content += ' '

    content = content.encode('utf-8')

    # Encrypt content.
    aes = AES.new(b'2021052020210520', AES.MODE_CBC, b'2021052020210520')
    encrypted_content = aes.encrypt(content)
    return(b2a_hex(encrypted_content))

def gen_license_file():
    license_file = './License.dat'
    with open(license_file, 'w') as LF:
        LF.write('MAC : 00:0c:29:4a:d4:6c\n')
        LF.write('Date : 20210520\n')
        sign = encrypt('00:0c:29:4a:d4:6c#20210520')
        LF.write('Sign : ' + str(sign.decode('utf-8')) + '\n')

if __name__ == '__main__':
    gen_license_file()

       我們利用這個腳本可以生成一個添加了限定條件的License文件。

  $ ./gen_license_file.py

  $ cat License.dat

  MAC : 00:0c:29:4a:d4:6c

  Date : 20210520

  Sign : 6ccda9297714c2b0c9877625ad6d38aacae4d6d97a0652c926c11ff42fc30d1c

       其中Sign部分是以上明文信息的加密,用作校驗。

 

三、覈驗license文件

       在原始的eda.py腳本中,我們需要載入這個License文件,並做如下幾件事情:

  1. 解析License文件,獲取MAC/Date/Sign信息。
  2. 解密Sign信息。
  3. 對比解密的Sign信息和MAC/Date信息,看License文件是否被篡改。
  4. 獲取當前機器MAC信息和當前Date信息。
  5. 將當前機器MAC信息和當前Date信息同Sign的解密信息覈驗,看是否超限。

 

       那麼我們把如上的License覈驗步驟加入到eda.py中,得到了新的代碼如下。

#!/usr/bin/env python3
import os
import re
import sys
import datetime
import subprocess

from Crypto.Cipher import AES
from binascii import a2b_hex
## License check def license_check(): license_dic = parse_license_file() sign = decrypt(license_dic['Sign']) sign_list = sign.split('#') mac = sign_list[0].strip() date = sign_list[1].strip() # Check license file is modified or not. if (mac != license_dic['MAC']) or (date != license_dic['Date']): print('*Error*: License file is modified!') sys.exit(1) # Check MAC and effective date invalid or not. if len(sign_list) == 2: mac = get_mac() current_date = datetime.datetime.now().strftime('%Y%m%d') # Must run this script under specified MAC. if sign_list[0] != mac: print('*Error*: Invalid host!') sys.exit(1) # Current time must be before effective date. if sign_list[1] > current_date: print('*Error*: License is expired!') sys.exit(1) else: print('*Error*: Wrong Sign setting on license file.') sys.exit(1) def parse_license_file(): license_dic = {} license_file = './License.dat' with open(license_file, 'r') as LF: for line in LF.readlines(): if re.match('^\s*(\S+)\s*:\s*(\S+)\s*$', line): my_match = re.match('^\s*(\S+)\s*:\s*(\S+)\s*$', line) license_dic[my_match.group(1)] = my_match.group(2) return(license_dic) def decrypt(content): aes = AES.new(b'2021052020210520', AES.MODE_CBC, b'2021052020210520') decrypted_content = aes.decrypt(a2b_hex(content.encode('utf-8'))) return(decrypted_content.decode('utf-8')) def get_mac(): mac = '' SP = subprocess.Popen('/sbin/ifconfig', shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (stdout, stderr) = SP.communicate() for line in str(stdout, 'utf-8').split('\n'): if re.match('^\s*ether\s+(\S+)\s+.*$', line): my_match = re.match('^\s*ether\s+(\S+)\s+.*$', line) mac = my_match.group(1) break return(mac) # Main function. def function(): print('I am an EDA tool!') license_check() function()

       執行一下,效果如下。

  $ ./eda.py
  *Error*: License is expired!

       License過期了,工具被禁止啓動,限制成功!

 

四、Python加密

       上述的License限制機制生效了,但是Python工具代碼是明文的,所有的限制都可以通過篡改工具代碼而繞過去,頭疼。

       爲此我們需要將Python工具代碼加密,才能保證License的實現機制不被隨便篡改。Python代碼常見的5中加密機制如下:

  1. 將.py文件轉爲.pyc文件,.pyc文件是二進制文件,不具備可讀性,從而實現了代碼隱藏。

      問題:.pyc文件很容易被反編譯,python有一個庫compileall就可以輕鬆實現。

  2. 代碼混淆。

      問題:只是降低了代碼可讀性,並不能完全隱藏代碼邏輯和核心內容的明文信息。

  3. 通過py2exe將python代碼打包成二進制可執行文件。

      問題:只能在windows平臺上使用。

  4. 使用Cython將.py文件編譯.so文件,內容就被加密了且較難破解。

      問題:部分代碼可能存在不兼容的問題。

  5. 修改Python解釋器。

      問題:難度太高!

 

       考慮到加密效果,綜合考量實現難度,我們選擇#4基於Cython的加密方案。

       基於Cython的加密方案需要注意兩點:

  1. .so文件可以被Python文件import,但是不可以直接運行。
  2. 執行Cython加密腳本的Python版本需要同執行Python工具的Python版本保持一致,能夠顯著減少代碼兼容性問題。

 

       第一步,我們需要改造eda.py,將核心代碼移到新文件(比如top.py),頂層腳本eda.py只保留一個空殼,同時把核心功能通過import的方式引入。

       改造後,核心腳本top.py的內容如下。同原先的eda.py相比,執行函數license_check()和function()被移除了,其它一致。

       而改造後的eda.py則成了一個空殼子,如下。

#!/usr/bin/env python3
import top

top.license_check()
top.function()

       然後我們嘗試通過Cython將top.py的內容加密。這個過程主要是藉助Cython和disutils兩個pyton庫來實現。

       首先,我們寫一個setup.py文件。

import os
from distutils.core import setup from Cython.Build import cythonize py_files = ['top.py',] setup(ext_modules = cythonize(py_files),)

       然後用指定Python腳本來執行setup.py。

  $ python3 setup.py build_ext --inplace

  Compiling top.py because it changed.

  [1/1] Cythonizing top.py

  running build_ext

  building 'top' extension

  creating build

  creating build/temp.linux-x86_64-3.6

  gcc -pthread -B /ic/tools/anaconda3/anaconda3.5.2/compiler_compat -Wl,--sysroot=/ -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -I/ic/tools/anaconda3/anaconda3.5.2/include/python3.6m -c top.c -o build/temp.linux-x86_64-3.6/top.o

  gcc -pthread -shared -B /ic/tools/anaconda3/anaconda3.5.2/compiler_compat -L/ic/tools/anaconda3/anaconda3.5.2/lib -Wl,-rpath=/ic/tools/anaconda3/anaconda3.5.2/lib -Wl,--no-as-needed -Wl,--sysroot=/ build/temp.linux-x86_64-3.6/top.o -o /home/liyanqing/test/license/top.cpython-36m-x86_64-linux-gnu.so

       我們看到新生成了.c .so文件和build目錄,其中只有.so文件對我們是有用的。

  $ ls
  build  eda.py  gen_license_file.py  License.dat  setup.py  top.c  top.cpython-36m-x86_64-linux-gnu.so  top.py

       我們清理無用文件(包括top.py),然後將.so文件更名爲top.so(去掉cpython***那一堆字符串)

  $ ls

  backup  eda.py  License.dat  top.so

       這下清爽多了。

       然後重新執行eda.py。

  $ ./eda.py
  *Error*: License is expired!

       Good!還能正常執行。

       而此時我們看一下.so文件中的核心代碼。

  ^?ELF^B^A^A^@^@^@^@^@^@^@^@^@^C^@>^@^A^@^@^@ð6^@^@^@^@^@^@@^@^@^@^@^@^@^@è^T^F^@^@^@^@^@^@^@^@^@@^@8^@^G^@@^@%^@$^@^A^@^@^@^E^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@l^S^... ...

       既然你們都看不懂這些亂碼,我就放心了,哈哈。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章