Python定制化虚拟机保护对抗
Python定制化虚拟机保护原理
Python虚拟机机制
Python虚拟机是Python编程语言的核心组件,他负责解释和执行Python字节码,即虚拟机逐条执行字节码指令,操作栈帧中的数据。
Python编译之后的字节码储存在pyc文件中,pyc文件实际上就是PyCodeObject对象的序列化文本。
其结构体定义如下:
/* Bytecode object */
typedef struct {
PyObject_HEAD
int co_argcount; /* Code Block的位置参数个数,比如说一个函数的位置参数个数*/
int co_nlocals; /* Code Block中局部变量的个数,包括其中位置参数的个数 */
int co_stacksize; /* 执行该段Code Block需要的栈空间 */
int co_flags; /* CO_..., see below */
PyObject *co_code; /* Code Block编译所得的字节码指令序列。以PyStingObjet的形式存在 */
PyObject *co_consts; /* PyTupleObject对象,保存CodeBlock中的所常量 */
PyObject *co_names; /* PyTupleObject对象,保存CodeBlock中的所有符号 */
PyObject *co_varnames; /* Code Block中的局部变量名集合 */
PyObject *co_freevars; /* Python实现闭包需要用的东西 */
PyObject *co_cellvars; /* Code Block中内部嵌套函数所引用的局部变量名集合 */
/* The rest doesn't count for hash/cmp */
PyObject *co_filename; /* Code Block所对应的.py文件的完整路径 */
PyObject *co_name; /* Code Block的名字,通常是函数名或类名 */
int co_firstlineno; /* Code Block在对应的.py文件中起始行 */
PyObject *co_lnotab; /* 字节码指令与.py文件中source code行号的对应关系,以PyStringObject的形式存在 */
void *co_zombieframe; /* for optimization only (see frameobject.c) */
} PyCodeObject;
其中co_code字段则为Python编译所得的字节码指令序列,反编译工作主要也是针对这个序列识别后续根据AST进行代码还原。
保护实现
既然要保护co_code,观察Python源码可以发现源码中的opcode.py文件(Python-3.8.5\Lib\opcode.py)

Python-3.8.5\Include\opcode.h 则由其生成,显然我们只需要修改其中字节码的映射关系即可起到对抗常规反编译器的作用,这里探讨对抗定制化虚拟机保护机制,如何修改编译则不细讲。
Python定制化虚拟机对抗过程
定制化虚拟机部署代码场景
既然对代码使用定制化的虚拟机编译成pyc文件那么该pyc文件显然无法通过正常的python虚拟机所解释执行,因此通常需要将定制化的虚拟机与产品的pyc代码一起打包发版,这意味着用户可以使用我们的Python虚拟机执行任何py代码。
定制化虚拟机字节码自吐
上文说到既然定制化的虚拟机也需要一起交付,那么用户便可以拿定制化虚拟机执行一些“恶意”的代码。
以编译好的python_360为例,首先我们写一个简单的python代码:
s = '360'
i = 10
def func():
print ('hello 360')
ss = 'halo 360'
return ss
s2 = func()
print (s2)
使用编译好的python3.6运行:

再使用py_compile进行编译
./python3.6 -m py_compile test.py
我们就可以获取编译好的pyc了

显然该pyc是无法使用常规的反编译工具反编译的,例如uncompyle6:

无法解析opcode问题。
但前文提到过我们可以利用该python虚拟机执行自己编写的python代码既然如此这样就出现了一个很爆炸的模块:dis模块,它可以将 Python 源代码编译后的字节码指令以可读的形式展示,帮助开发者深入理解代码的底层执行逻辑,从而优化性能或调试问题。
那我们尝试使用dis模块解析编译好的这个pyc文件。
import marshal
import dis
with open('__pycache__/test.cpython-36.pyc', 'rb') as f:
f.read(12) #跳过文件信息
code = marshal.load(f)
dis.dis(code)
我们得到如下输出:

反序列化解析成功了,此时其实已经可以通过AI工具还原源码了




经过对比与源码一致。
那么既然可以运行dis.dis来解析这个代码,有没有更优雅的方法呢?
我们可以通过dis中opmap字段来打印整个映射关系
import dis as dis
print(dis.opmap)

不难发现所有的字节码映射关系都直接被输出了。
定制化反编译工具
本步骤采用开源工具PYCDC zrax/pycdc: C++ python bytecode disassembler and decompiler (github.com)

我们只需要根据前面获取的映射修改此处的映射即可。
接下来做一个格式化输出字节码的脚本:
import sys
try:
import opcode
except ImportError:
try:
import dis as opcode
except ImportError:
sys.stderr.write("Error: 无法导入 opcode 或 dis 模块,请用其它方式提取字节码映射。\n")
sys.exit(1)
print("BEGIN_MAP(3, 6)")
seen = set()
for op_val, opname in enumerate(opcode.opname):
if not opname:
continue
if opname.startswith("<") and opname.endswith(">"):
continue
# 避免重复输出
if op_val in seen:
continue
# 输出一行
if op_val >= 90:
print(f" MAP_OP({op_val}, {opname}_A)")
else:
print(f" MAP_OP({op_val}, {opname})")
seen.add(op_val)
# 第二轮:遍历 opcode.opmap(name→value),把上面没输出到的也补上
for name, op_val in opcode.opmap.items():
if op_val in seen:
continue
# 同样跳过占位符/伪指令
if name.startswith("<") and name.endswith(">"):
continue
if op_val >= 90:
print(f" MAP_OP({op_val}, {opname}_A)")
else:
print(f" MAP_OP({op_val}, {opname})")
seen.add(op_val)
print("END_MAP()")

修改python_3_6.cpp

然后编译,即可获取pycdc.exe

即可完美反编译。
宝宝好棒~
宝宝好棒
你上一篇文章怎么不写|´・ω・)ノ
因为不想写了哈哈
宝宝好棒~
哥哥好强
学到了,博主更多点