[Python] 置换CPython 2.7.13的opcode

之前在一个问题下打过一下酱油,

国内有CPython解释器定制相关的工作么? - RednaxelaFX 的回答 - 知乎

,里面提到一种混淆CPython解释器的方式是置换其虚拟机的opcode的值,这样经过修改的CPython把Python源码编译成含有CPython字节码的 *.pyc / *.pyo 文件,就不能直接用未经修改的CPython解释器运行,也不能直接用现成的用来反汇编或反编译CPython字节码的工具来处理。这样就略微提高了Python程序的保密性,假如说要发布一个用Python实现的程序而不想别人读到源码的话,这样就算防不住小人至少也能防住君子吧。

最近有人来问说他自己试着修改了CPython 2.7.13的opcode,但make的时候却出错,想问是怎么回事。这里就来演示一下最简单的opcode置换应该怎么做。

其实超级简单,只要对应修改2个文件即可。下面演示一下把 BINARY_ADD 与 BINARY_SUBTRACT 的opcode编码调换的例子:

(我这里是在CPython 2.7分支的master版代码上做的diff)

diff --git a/Include/opcode.h b/Include/opcode.h
index 9ed5487..8f2e9fa 100644
--- a/Include/opcode.h
+++ b/Include/opcode.h
@@ -27,8 +27,8 @@ extern "C" {
 #define BINARY_MULTIPLY	20
 #define BINARY_DIVIDE	21
 #define BINARY_MODULO	22
-#define BINARY_ADD	23
-#define BINARY_SUBTRACT	24
+#define BINARY_ADD	24
+#define BINARY_SUBTRACT	23
 #define BINARY_SUBSCR	25
 #define BINARY_FLOOR_DIVIDE 26
 #define BINARY_TRUE_DIVIDE 27
diff --git a/Lib/opcode.py b/Lib/opcode.py
index e403365..49c48fa 100644
--- a/Lib/opcode.py
+++ b/Lib/opcode.py
@@ -62,8 +62,8 @@ def_op('BINARY_POWER', 19)
 def_op('BINARY_MULTIPLY', 20)
 def_op('BINARY_DIVIDE', 21)
 def_op('BINARY_MODULO', 22)
-def_op('BINARY_ADD', 23)
-def_op('BINARY_SUBTRACT', 24)
+def_op('BINARY_ADD', 24)
+def_op('BINARY_SUBTRACT', 23)
 def_op('BINARY_SUBSCR', 25)
 def_op('BINARY_FLOOR_DIVIDE', 26)
 def_op('BINARY_TRUE_DIVIDE', 27)

注意一定要把这两个文件都对应修改才可以。如果只修改 Include/opcode.h 的话就会在make的时候遇到这样的错误:

$ make
gcc -c -fno-strict-aliasing -g -O2 -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes  -I. -IInclude -I./Include   -DPy_BUILD_CORE -o Modules/python.o ./Modules/python.c
...
gcc  -u _PyMac_Error -o python.exe \
			Modules/python.o \
			libpython2.7.a -ldl  -framework CoreFoundation     
./python.exe -E -S -m sysconfig --generate-posix-vars ;\
	if test $? -ne 0 ; then \
		echo "generate-posix-vars failed" ; \
		rm -f ./pybuilddir.txt ; \
		exit 1 ; \
	fi
Traceback (most recent call last):
  File "/private/tmp/python2713/Python-2.7.13/Lib/encodings/__init__.py", line 99, in search_function
    mod = __import__('encodings.' + modname, fromlist=_import_tail,
TypeError: unsupported operand type(s) for -: 'str' and 'str'
generate-posix-vars failed
make: *** [pybuilddir.txt] Error 1

这是因为漏做的修改导致一个 str + str 的操作被当作 str - str 了(哈哈

就这么简单。

话说如果真就这样简单置换一下 BINARY_ADD 与 BINARY_SUBTRACT 的opcode的话,“防护”效果肯定能有一点但是应该会很有限。如果不添加或删减任何字节码指令,而纯粹置换 opcode encoding 的话,我会建议参考以下的一些思路:

  • 带参数的 opcode 跟不带参数的 opcode 置换
  • 有控制流语义(跳转、调用、返回)的 opcode 跟没有控制流语义的 opcode 置换
  • 不同功能组之间的 opcode 置换
  • 每次发布重新随机生成一次置换

而如果有条件的话,最好还是增加 / 修改一下 opcode 的指令集。CPython的字节码用的是1-byte opcode,编码空间有256个位置,而现在的CPython才用了一半多一点,还有很多空间可用。例如说

  • 把一些常见的指令序列给它合并起来弄成一个新的 opcode 啦(这种思路叫做 superinstruction);
  • 或者把原本分开的两个功能的字节码指令给它糅合起来变成两个部分功能重叠的字节码指令,一定要以某种顺序执行才等价于原本的其中一条字节码、另一种顺序执行才等价于另一条字节码;
  • 等等。

外加 *.pyc / *.pyo 的格式也可以顺带修改一下,破坏现成工具对这种格式的文件的读取。

这样的话在CPython的源码->字节码编译器里也要做些相应修改才能对应。功夫是要多花一点,但“防护”效果会比简单置换 opcode encoding 要好很多。顺带还可以做点性能优化——更优化的 opcode 指令集可以带来稍微高一点的性能。

就先写这么多。

发布于 2017-03-18 16:54