如何加密你的 Python 代码( 五 )

  • 通过 python -m <module> 的方式来加载模块时 , 其入口函数是 Py_Main 函数
  • 通过 import <module> 的方式来加载模块时 , 其入口函数是 builtin___import__ 函数
  • 通过 reload(<module>) 的方式来加载模块时 , 其入口函数是 builtin_reload 函数
但不论是哪种方式 , 最终都会调用 find_module 函数 , 我们看看这个函数中是否暗藏乾坤呢?
[Python/import.c]--------------------------------------static struct filedescr *find_module(char *fullname, char *subname, PyObject *path, char *buf,size_t buflen, FILE **p_fp, PyObject **p_loader){...fp = fopen(buf, filemode);...}我们在 find_module 函数中找到了打开文件的逻辑 , 如果直接改成前文实现的 decrypt_open  , 岂不是就能达成加载模块时解密的目的了?
总体思路是这样的 , 但有个细节需要注意 ,  buf 不一定就是 .py 文件 , 也可能是 .pyc 文件 , 我们只对 .py 文件做改动 , 则可以这么写:
[Python/import.c]--------------------------------------static struct filedescr *find_module(char *fullname, char *subname, PyObject *path, char *buf,size_t buflen, FILE **p_fp, PyObject **p_loader){...if (fdp->type == PY_SOURCE) {fp = decrypt_open(buf, filemode);}else {fp = fopen(buf, filemode);}...}经过上述改动 , 就实现了加载模块时解密的目的了 。
支持指定密钥文件前文中还留有一个待解决的问题:我们一开始是假定解释器已获取到了密钥内容并存放在了全局变量 aes_passwd 中 , 那么密钥内容怎么获取呢?
我们需要 Python 解释器能支持一个新的参数选项 , 通过它来指定已加密的密钥文件 , 然后再通过非对称算法进行解密 , 得到 aes_passed。
假定这个参数选项是 -k <filename>  , 则可使用如 python -k enpasswd.txt 的方式来告知解释器加密密钥的文件路径 。其实现如下:
[Modules/main.c]--------------------------------------/* 命令行选项 , 注意k:是新增的内容 */#define BASE_OPTS "3bBc:dEhiJk:m:OQ:RsStuUvVW:xX?".../* Long usage message, split into parts < 512 bytes */static char *usage_1 = "...-k key : decrypt source file by using key filen...";...intPy_Main(int argc, char **argv){...char *keyfilename = NULL;...while ((c = _PyOS_GetOpt(argc, argv, PROGRAM_OPTS)) != EOF) {...case 'k':keyfilename = (char *)malloc(strlen(_PyOS_optarg) + 1);if (keyfilename == NULL)Py_FatalError("not enough memory to copy -k argument");strcpy(keyfilename, _PyOS_optarg);keyfilename[strlen(_PyOS_optarg)] = '';break;...}...if (keyfilename != NULL) {int passwdlen;char *passwd = NULL;passwdlen = rsa_decrypt(keyfilename, &passwd);set_aes_passwd(passwd);if (passwdlen < 0) {fprintf(stderr, "%s: parsing key file '%s' errorn", argv[0], keyfilename);free(keyfilename);return 2;} else {free(keyfilename);}}...}其逻辑如下:
  • k: 中的 k 表示支持 -k 选项; : 表示选项后跟一个参数 , 即这里的已加密密钥文件的路径
  • 解释器在处理到 -k 参数时 , 获取其后所跟的文件路径 , 记录在 keyfilename 中
  • 使用自定义的 rsa_decrypt 函数(限于篇幅 , 不列出如何实现的逻辑)对已加密密钥文件进行非对称解密 , 获得密钥的原始内容
  • 将该密钥内容写入到 aes_passwd 中
由此 , 通过显示地指定已加密密钥文件 , 解释器获得了原始密钥 , 进而通过该密钥解密已加密代码 , 再执行原始代码 。但是 , 这里面还潜藏着一个 风险 :执行代码的过程中会生成 .pyc 文件 , 通过它反编译出的 .py 文件是未加密的 。换句话说 , 恶意用户可以通过这种手段绕过限制 。所以 , 我们需要 禁用字节码
禁用字节码不生成 .pyc 文件首先要做的就是不生成 .pyc 文件 , 这样 , 恶意用户就没法直接根据 .pyc 文件来得到源码 。
我们知道 , 通过 -B 选项可以告知 Python 解释器不生成 .pyc 文件 。既然定制的 Python 解释器就不生成 .pyc 我们干脆禁用这个选项:
[Modules/main.c]--------------------------------------/* 命令行选项 , 注意移除了B */#define BASE_OPTS "3bc:dEhiJm:OQ:RsStuUvVW:xX?".../* Long usage message, split into parts < 512 bytes */static char *usage_1 = "...//-B: don't write .py[co] files on import; also PYTHONDONTWRITEBYTECODE=xn...";...intPy_Main(int argc, char **argv){...// 不生成 py[co]Py_DontWriteBytecodeFlag++;...}


推荐阅读