社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

Python代码保护技术及其破解

山石网科安全技术研究院 • 7 月前 • 363 次点击  

众所周知,Python是一种开源的解释型语言,这使其难以保护源码,但由于恶意软件编写、商业软件发布、关键数据保护等不同的需求,逐渐发展出了各种各样的保护Python代码的方法。本文将从源码、字节码及可执行文件三个层面来大致介绍这些保护方法及其对应的破解,这几个层面在实现和破解上的难度是逐步递增的。


源码层面

01

Oxyry Python Obfuscator

Oxyry Python Obfuscator [1] 是一个针对源码的在线混淆器,支持Python 3.3-3.7,它在处理原代码时会将符号名称(包括变量、函数、类、参数、类私有方法)进行重命名,同时避免了明文名称到混淆名称的1:1映射,相同的名称可能会在不同的范围内转换为多个不同的名称。

可以看出混淆之后的代码里变量名是几乎不可区分的,确实能起到阻止变量跟踪的效果,但不多。在这种混淆下,可以使用一些具有相同文本跟踪功能的编辑器(如sublime)进行变量的跟踪或者在同一个函数下的更名来达到反混淆的效果。

02

pyminifier

Pyminifier [2] 是一个Python代码的压缩器和混淆器,在这里只介绍它的混淆功能。

可以看到混淆前后函数结构没有太大的变化,只是将函数名和变量名进行了替换,对应的替换直接写在了除导入语句以外的最前面。对于此类混淆,方法还是跟前者一样,对代码文本进行全局替换,并将前面的语句注释掉即可。

03

Python Source Obfuscation using ASTs

这是Jurriaan Bremer在2013年的HITB CTF上为了出题而创造的一种混淆方法 [3] ,这种方法的核心思想在于将Python代码解析为抽象语法树(AST),并将其重写,最后通过重写的AST编写出新的Python源代码。

混淆效果看起来还算不错,在对部分变量名更名的同时,将常数拆成了简单算式、字符串拆成了片段或加以反转、将导入模块动态载入。不过这些混淆都可以通过Python解释器重新运行来进行简化,比如:

可能是因为工具比较古早的问题,在具体运行上有一些数据不够准确,但其思想仍然对后续研究有一定的启发作用。


字节码层面

01

字节码文件(.pyc)

当我们运行Python文件程序的时候,Python将源码转换为字节码(byte code),然后再由解释器来执行这些字节码。在这个过程中这些字节码都是在内存中的,众所周知Python的运行性能不如编译型语言,所以Python在某些程序执行结束后会把字节码写入到硬盘中,保存为.pyc文件,目的是下一次再执行python test.py程序时,Python会先在目录下找test.pyc文件来执行,因为.pyc文件里保存的是字节码,所以就节省了Python解析器把test.py翻译成字节码的时间,进而提高了性能。
这个pyc文件实际上是二进制文件,不同版本在实现上大同小异,接下来简单介绍下结构,方便进一步理解后续对pyc的隐写和混淆方法。

pyc文件头

文件头目前来说一共有三个不同的格式。

在Python 2里,文件头长度为8字节,其中有4字节的magic int,用以标识版本信息,通常为2字节版本magic word+b’\x0d\x0a’;还有4字节的timestamp,即Unix Timestamp,从UTC1970年1月1日0时0分0秒起至现在的总秒数,不考虑闰秒。

在Python 3.0-3.6中,文件头长度为12字节,其中除了上文中的4字节magic int及4字节timestamp以外,还追加了4字节source size,用小端序标识源码文件大小。

在Python 3.7以后,文件头长度为16字节,在4字节magic int后面增加了4字节的hash checked flag,用小端序记录,标识pyc文件是否启用hash校验,为0则不启用,为1(UNCHECKED_HASH,默认最新不检查)或3(CHECKED_HASH)则启用,启用以后原来的4字节timestamp+4字节source size会被8字节的siphash13哈希值覆盖。

以上4字节的magic word可以在cpython库中的/Lib/importlib/_bootstrap_external.py路径下找到各版本对应的16bits整数。

pyc文件格式

pyc文件由文件头+序列化的PyCodeObject对象组成,在Python/marshal.c中可以追溯到其序列方式。

首先所有的PyObject都会用一个1字节存储TYPE,PyCodeObject使用TYPE_CODE,其余TYPE如下图所示。

其中比较特殊的是FLAG_REF标识,如果该对象是第一次定义则会使用TYPE | FLAG_REF,然后将其索引存入索引表中,在下一次使用该对象时不必重复定义,直接去调用索引表中对应的索引即可。这种引用的判断节省了整数、元组、字符串等类型对象的空间重复使用。
PyCodeObject的其余参数内容如下:
  • co_argcount:需要的位置参数个数,不包括变长参数(*args 和 **kwargs)

  • co_kwonlyargcount:不定参数后定义的位置参数个数

  • co_nlocals:所有的局部变量的个数,包括所有参数

  • co_stacksize:运行时所需要的最大栈深度

  • co_flags:在 code.h 里定义

  • co_code:PyStringObject,代码对应的字节码

  • co_consts:PyTupleObject,所有常量

  • co_names:PyTupleObject,所用的到符号

  • co_varnames:PyTupleObject,所用到的局部变量名

  • co_freevars:PyTupleObject,所用到的freevar变量名

  • co_cellvars:PyTupleObject,所用到的cellvar变量名

  • co_filename:PyStringObject,对应的python文件

  • co_name:该PyCodeObject的名称

  • co_firstlineno:该PyCodeObject对应的源文件首行行号

  • co_lnotab:PyStringObject,字节码偏移量与源码行号的对应关系

其中存储代码内容的为co_code,顺序存储了代码中的字节码指令。在Python3.6之前使用变长指令(1字节或3字节),第1字节是操作码opcode,如果opcode有参数,则会多加2字节参数(小端序);而在Python3.6及之后使用定长指令(2字节),第1字节同样是opcode,第2字节是参数,如无参数则默认为b’\x00’。不同版本的opcode不同,详细见各版本源码的Include/opcode.h,也可以用以下脚本打印当前版本的opcode。

import opcode
for i, j in enumerate(opcode.opname):
    print("0x%02x(%03d): %s" % (i, i, j))

02

pyc隐写

想要在pyc中隐藏信息,可以利用Python 3.6及以上的版本中字节码恒为2字节、无参opcode默认b’\x00’的特性,在这些冗余的b’\x00’字节上逐个乱序嵌入payload的各个字节,现有工具Stegosaurus [4] 完成了这一实现。

该工具同时内置了隐写和提取接口,当遇到用Stegosaurus隐写的pyc时,可以用其提取出隐写信息。

原版工具仅支持3.6,笔者做了一个对3.7+版本的支持,开源在:https://github.com/c10udlnk/stegosaurus。

03

pyc反编译

对于单纯用Python内置的Py_compile模块编译出来的pyc,反编译技术已经非常成熟了,这里有两个较为常用的pyc反编译工具:uncompyle6和pycdc。
uncompyle6 [5] 是一个Python的跨版本反编译器,针对不同版本的pyc都能很好地实现反编译效果,基本能实现源码的完美还原。

pycdc [6] 是一个集成了Python反编译器及反汇编器的工具,其反编译效果同样出色,笔者个人感觉包容性比uncompyle6稍微优秀一点。

04

pyc反反编译

因为pyc反编译技术已然成熟,出现了针对这些反编译的进一步保护技术,目前常见的有两种混淆方式,从原理上说与二进制文件的反编译抵抗技术有些类似。

在遇到不能反编译的二进制文件时,我们会通过查看其汇编代码来分析其阻碍反编译的原因(比如一些花指令)。同样的,在pyc文件不能被反编译时,我们也可以通过pycdc的分支pycdas对pyc文件进行“反汇编”来协助我们进行分析。后文在破解反反编译技术时,正是依赖于这样类似于汇编的Python字节码。

在跳转间隙填充无意义指令

这种方法的核心原理是通过JUMP_ABSOLUTE跳过无意义字节,但无意义字节仍会被反汇编器处理。这样会在反编译器对pyc进行反汇编时解析不到无意义字节的opcode或者参数(如超出了LOAD_CONST的索引)而导致报错。已有工具实现这样的混淆:pyc_obscure [7] 

在实际实验下来发现,pyc_obscure工具在混淆时没有修改原代码中跳转指令的偏移,导致混淆以后不能正常运行(报错segmentation fault)。

为了使实验继续进行,笔者将该工具进行了修复,在阻止反编译的同时正常运行pyc,开源在https://github.com/c10udlnk/pyc_obscure。

针对这种混淆,破解方法是将这些语句转化为b’\x09\x00\x09\x00’(即两个nop语句),这样修复能保证代码的长度不变,且不会影响原代码中跳转指令的偏移。

修改以后即可使用pycdc正常反编译。

重叠式花指令

在二进制文件的花指令中,有一种很常用的Overlapping Instruction。在使用变长指令的指令系统中,相同的字节序列可能会被处理器解释为完全不同的指令,具体取决于执行开始的确切字节。

Python字节码中同样存在这样的花指令(在Python 3.6以前使用变长指令),目前来说还没有实现这种效果的工具,这里笔者写了一个小脚本加以说明。

可以看到这样就能实现跟在二进制中同样的干扰功能,并且程序能正常运行。那么破解方法其实就也是跟二进制一样,将多余字节(图中标红的部分)全部用b’x09’(NOP)覆盖,这样就能避免干扰,达到正常反编译的目的。


可执行文件层面 

01

Pyinstaller打包

PyInstaller是用来打包python应用程序的一个工具,打包后的程序可以在没有安装Python解释器的机器上运行,直接逆向该程序难度非常大。
在看到此类文件时,通常是使用pyinstxtractor [8] 对可执行文件进行解包,再对解包后的pyc文件使用反编译工具进行反编译得到源码。

02

定制Python解释器

在PyCon China 2018杭州站中有一篇《Python源码加密》的分享 [9] ,里面介绍的方法是通过定制Python解释器来达到保护源码、正常运行的目的。
需要运行该程序的用户会拿到定制的Python解释器、加密密钥和加密代码,在运行程序的过程中,定制的Python解释器使用内置私钥对加密密钥进行解密,再使用解密后的密钥解密加密代码,最后运行解密后的代码。其中对密钥的加解密使用非对称密码算法RSA,对代码的加解密使用对称密码算法AES-128-cbc。这种保护方法依赖于各密码算法的安全性以及私钥能否被从定制的Python解释器中提取出来。

笔者按照其流程改造了一个这样的Python解释器。可能是由于该分享比较古早,在文章中有提到“由于 Python 解释器本身是二进制文件,所以不需要担心内置的私钥会被看到”,且实现上也没有对私钥做特别的保护,在实际破解中可以发现以当前的逆向分析技术可以很轻松地获取到私钥,利用私钥对加密密钥进行解密,进而解密源码。

03

Pyarmor

Pyarmor [10] 是一个加密Python代码并控制代码可运行期限的商业工具,使用Python的C拓展库将自己的加解密函数加入到内置函数库中,兼容Python各版本运行。其加密后的文件结构如下:

其中__init__.py和_pytransform动态库为辅助运行库,foo.py为加密后的代码。

可以看到确实很好地隐藏了源码。该工具有两层加密,一是在最开始运行时对整个二进制串进行解密加载成代码对象并执行,二是在函数运行前会进行第二次解密,在函数运行结束后加密,保证攻击者不能从内存中dump出解密后的函数。

因许可限制,这里不对其二进制辅助库作详细分析,对加密脚本的详解可以参考官方文档:https://pyarmor.readthedocs.io/zh/latest/topic/obfuscated-script.html


[1] https://pyob.oxyry.com

[2] https://github.com/liftoff/pyminifier

[3] http://jbremer.org/python-source-obfuscation-using-asts

[4] https://github.com/AngelKitty/stegosaurus

[5] https://github.com/rocky/python-uncompyle6

[6] https://github.com/zrax/pycdc

[7] https://github.com/marryjianjian/pyc_obscure

[8] https://github.com/extremecoders-re/pyinstxtractor

[9] https://segmentfault.com/a/1190000021660914

[10] https://github.com/dashingsoft/pyarmor


Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/165417
 
363 次点击