王小强,顾乃杰
(1.中国科学技术大学 a.计算机科学与技术学院; b.先进技术研究院,合肥 230027;2.安徽省计算与通信软件重点实验室,合肥 230027)
Python是一种面向对象的解释型计算机程序设计语言,由于语法简洁清晰、简单易学、免费、开源、扩展性强等优点,自从20世纪90年代诞生以来,就广受欢迎和使用,如网络文件同步工具Dropbox、社交新闻站点Reddit、网络问答社区知乎,都是由Python编程语言所开发。Python字节码文件(.pyc)由Python编程语言编写的程序编译而成,与传统的针对特定处理器和操作系统的二进制文件相比,Python字节码文件保留了Python源码文件中的全部信息,是针对Python虚拟机的具有特定结构和特征的文件,具有可跨平台使用的特性。正是因为字节码文件的这种结构和特征,导致其极易被攻击者反编译出其中的源码,如使用uncompyle2[1]、Decompyle++[2]、Easy Python Decompiler[3]等工具就可以将Python字节码文件反编译为源码文件,这不仅会造成重要数据结构、算法、业务逻辑的暴露,而且会给开发者带来巨大的经济损失,有时甚至具有严重的安全隐患。
针对上述问题,本文提出一种基于操作码合并的Python程序防逆转算法。在不影响Python程序正确执行的前提下,引入新操作码对原Python操作码集进行扩充,并在编译生成字节码文件时使用新操作码来代替操作码序列中的2个或多个操作码,从而缩短Python字节码文件中操作码序列的长度,改变操作码序列的结构和语义。
代码混淆旨在通过布局混淆、数据混淆、控制流混淆[4-5]等措施,将一个程序转换为能够妨碍攻击者理解其中算法和数据结构或能够阻止攻击者从程序文本中提取有价值信息的另一种形式[6-7]。
虽然Java、Python等脚本语言经代码混淆后编译生成的字节码文件可以跨平台使用,但是其仍然遵守原来的文件格式和指令集,因此,代码混淆技术对Python字节码文件的保护能力不足。此外,代码混淆在处理多文件项目中的导入模块和对象名称时有局限性,且该方法会给代码执行效率带来一定的影响。
本地编译是一种将Python脚本程序和解释器一起编译为平台相关程序的技术,其工作步骤为[8]:首先编写Python源码程序,然后借助特定的工具(如py2exe[9])将目标脚本程序进行转换并且将Python解释器编译为平台相关的动态链接库,最后运用一个额外的可执行程序来运行该动态链接程序和转换后的文件。
虽然本地编译技术对Python程序起到了一定的保护作用,但是采用这种方式,Python程序在发布时需要同时发布解释器对应的动态链接程序和脚本文件所依赖的所有库文件,导致目标程序所占用的空间大大增加。
数字水印通常是永久镶嵌在宿主数据中的具有可鉴别性的数字信号或模式,其可以用来标识作者、所有者、发行者、使用者等信息[10-11],且不影响宿主数据的可用性。使用数字水印技术并不能阻止字节码文件被反编译[12],但是能够阻止数字产品被偷窃,或者当偷窃发生时为使用者提供数字产品的拥有权证明[13]。
虽然理论上水印能够作为软件所有权的有力凭证,但实际中无论是静态水印还是动态水印,都很容易通过混淆变换和优化等措施从软件中被移除[14-15],因此,水印技术并不能为Java、Python等字节码文件提供有效保护。
除上述防逆转方法外,研究人员在操作码替换领域也进行了研究。操作码替换技术是将字节码文件中的每个操作码替换为其他操作码,使得编译生成的字节码文件对于标准的解释器而言包含非法的操作码序列,以此达到防逆转的目的。
操作码替换技术已分别被基于虚拟机Dropbox[16]和基于PC的应用,在操作码随机化的安全[17]中实现,且网络上已有工具python-obfuscation[18]对Python操作码进行随机替换。但是,该技术的实质是利用密码学中的单表代替密码[19],如果攻击者了解采取的保护方式,就可以利用操作码的一些统计学规律进行分析,如采用频率分析进行攻击,因此,该技术的安全性不足。
Python字节码文件易被反编译的主要原因是字节码文件保留了Python源码文件的所有信息,且字节码文件中操作码序列的结构和每个操作码的含义极易被攻击者分析和理解。因此,改变操作码序列的结构或隐藏操作码的含义,是保护Python字节码文件的重要方法,但是,这类方法又会对虚拟机执行字节码文件的结果造成影响。鉴于此,本文将探索如何利用窥孔优化策略,在不影响字节码文件正确执行的前提下,通过引入新操作码来对Python操作码集进行扩充,进而改变Python字节码文件中操作码序列的结构,最终达到防逆转的目的。
定义1操作码
操作码即Python虚拟机中的指令码,占1 Byte的长度,其规定了Python虚拟机需要执行哪一条指令。目前,Python 2.7.9版本的虚拟机规范中定义了110个操作码,其中不带参数的操作码共61个,带一个参数的操作码共49个,每个参数占2 Byte的长度。
定义2操作码序列
一个字节码文件的操作码序列S由操作码和其所带的参数构成,操作码序列结构如图1所示,其中,OPi为操作码,ARGij为OPi的参数。OPi、ARGij均为正整数,1≤i≤n,1≤j≤2,n为一个字节码文件所包含的操作码总数。
图1操作码序列示意图
Python虚拟机执行字节码文件的本质是利用循环依序读取操作码序列中的一个操作码OPi和2个字节参数ARGi1与ARGi2,然后查找并执行OPi对应的解释程序,修改虚拟机内部众多的状态值,接着再读取下一个操作码OP(i+1)和字节参数ARG(i+1)1与ARG(i+1)2,重复上述步骤直至操作码序列读取结束。
定义3基本块
基本块是由S中顺序执行的若干个操作码构成的序列。通常情况下,若S中没有出现条件跳转、绝对跳转操作,则这些操作码处于同一个基本块中。
定义4基本块信息
字节码文件的基本块信息B是一个由正整数构成的单调不减序列,其长度与操作码序列S相同,且其中的每个元素与S中的每个操作码一一对应,元素值为对应的操作码在S中所在的基本块号。图2所示为一个基本块信息,从图中可以看出,S中第1个操作码处于第1个基本块,第2个~第5个操作码处于第2个基本块,依此类推。
图2基本块信息示意图
本文提出的操作码合并算法流程如图3所示。其中,操作码序列提取与频率统计的目的是寻找待合并的操作码序列;基本块信息提取是为了从大量的待合并操作码序列中选出可以合并的序列,减少对操作码集扩充的干扰;操作码集扩充是添加新操作码的定义与解释执行。
图3操作码合并算法流程
2.2.1 操作码序列提取
本文提取Python/Lib下的1 950个库文件源程序对应的1 950个操作码序列。这些程序通常提供接口给其他模块进行导入,其功能多样且不会随意发生改变,选它们进行操作码序列提取较为合理。1 950个操作码序列中,最长的操作码序列包含10 000多个操作码,最短的也包含60个操作码,共有操作码240 263个。
2.2.2 频率统计
频率统计的目的是获取候选合并对象。在2.2.1节获取多个操作码序列Sx的基础上,统计出Sx中所有长度为len的操作码子序列出现的次数,并按出现次数从高到底进行排序,其中,2≤len,1≤x≤k。
2.2.3 基本块信息提取
基本块信息的提取是在众多的候选子序列中筛选出可以合并的子序列。在2.2.2节的排序结果中,靠前的对象并不一定都是可以进行合并的操作码子序列,原因是有些子序列中的操作码处于不同的基本块中,如果将该类操作码进行合并,将会破坏程序的执行逻辑。因此,需要进一步根据程序执行逻辑来提取上述操作码序列Sx的基本块信息Bx,计算表达式如下:
其中,n为Sx中操作码的个数,1≤i 表1 部分长度为2 Byte的操作码子序列以及其出现次数 2.2.4 操作码集扩充 操作码集扩充是利用窥孔优化策略将出现频率较高且处于同一个基本块中的操作码子序列(OP1,OP2,…,OPlen)合并为一个新的操作码OPnew,并在Python虚拟机中添加对OPnew的解释执行过程,使得对OPnew的解释执行效果与依次执行(OP1,OP2,…,OPlen)的效果等价。 图4 操作码合并示意图 因为在Python字节码序列中任意一个操作码仅占1 Byte的大小,所以最多可以定义28个操作码。而在Python 2.7.9版本的虚拟机规范中仅使用了其中的110个操作码,因此,可以利用剩余的146个操作码来定义新操作码,从而在利用操作码合并缩短序列的同时隐藏操作码序列的含义。本文对表1中的5个子序列进行合并,并在Python 2.7.9中利用剩余的146个操作码中的5个来表示合并后的OPnew。 Python 3在Python 2的基础上对某些语法和模块进行调整和改动,因为同样遵循操作码仅占1 Byte长度的特性和操作码解释执行的机制,所以操作码合并算法可以在Python 3上实现,如Python 3.6.1使用了256个操作码中的119个,只能利用剩余的137个操作码来定义新操作码。 在操作码仅占1 Byte长度的基础上进行操作码合并,会大大限制操作码合并的扩展空间。若要生成和使用更多的操作码,则必须增加操作码所占存储空间的大小(如将操作码大小全部设为2 Byte或将部分操作码设为2 Byte),但该操作会因多次读取操作码而导致程序执行效率下降。 2.3.1 安全性 Ox=min(256nx+2mx,f) (1) 2.3.2 执行效率 当Python虚拟机执行字节码文件时,每次只能读取一个操作码和一个参数,并调用相应的解释执行过程。操作码合并前后Python虚拟机执行字节码文件运行过程如图5所示。 图5 操作码合并前后执行过程对比 如图5(a)所示,由于操作码序列中包含ADD、SUB列操作,因此需要读取2次操作码和参数以对栈顶的2个元素a和b出栈、求和,将结果r1压栈,然后对栈顶元素r1和c出栈、求差,将结果r再压栈。式(2)为操作码合并前Python虚拟机执行源码文件filex.py的时间。 (2) 如图5(b)所示,操作码合并后用新操作码ADD_SUB代替原先的2个操作码,用新操作码一次完成栈顶3个元素a、b、c出栈,并将a+b-c的结果r运算完之后再压栈。该操作虽然增加了编译源码文件所需的时间,但减少了冗余的读取操作及修改Python虚拟机内部状态的次数,且编译后的字节码文件在以后的运行过程中无需再次编译,从而提升了字节码文件的执行效率。 式(3)为操作码合并后Python虚拟机执行源码文件filex.py的时间。 (3) 操作码合并算法的整体性能与合并前后的字节码文件执行时间有关。式(4)为操作码合并前后字节码文件执行时间的变化率。 (4) 2.3.3 存储空间 操作码合并将操作码序列Sx中连续出现的多个操作码合并为一个新操作码,在不减少Sx中参数个数的同时减少了操作码的个数,从而缩短了操作码序列的长度,如原本需占用(nx+2mx) Byte的操作码序列,现只需占用(nx+2mx-gx(num)) Byte即可,字节码文件的大小缩小了(gx(num)) Byte。在需要存储和导入大量模块时,该方法有利于节约计算机磁盘和内存。同时,基于操作码合并生成的字节码文件需要新的解释器来解释执行,这就需要客户安装指定的Python解释器。虽然在首次使用时会给客户带来一些不便(以后无需再次安装),但该过程也是为了确保程序的安全性。 本文基于Python 2.7.9版本,实验测试平台为Intel Xeon E5-2690,主频2.6 GHz,252 GB内存,操作系统为CentOS 6.7 64位,实验对象为表1中的5对操作码。采用microbenchmark[20]来对新生成运行环境的防逆转效果、执行效率、文件大小进行检验。microbenchmark包含数字运算、字符处理等多个方面的Python源码文件,是一个较好的测试集。 3.2.1 安全性 本文通过操作码合并改变了原操作码序列的结构,对原操作序列信息进行了较好的隐藏。传统的反编译工具uncompile2、Decompyle++等无法识别合并后的操作码序列结构,且无法理解新操作码所包含的语义信息,因此,无法对新的字节码文件进行反编译。 3.2.2 执行效率 表2 操作码合并前后文件执行时间比较 从表2中的实验数据可以看出,使用操作码合并算法在参数个数不变的前提下,减少了字节码文件中操作码序列的操作码个数,即减少了CPU读取操作码的次数和多个操作码执行之间的跳转次数,从而缩短了程序的执行时间。 从表2实验数据中还可以看出,操作码合并后test.pyc和pydigits.pyc文件执行时间较之前有明显的减少,通过分析发现其原因为,test.pyc操作码序列中包含的操作码个数较少,只需要合并若干个操作码就可以使其个数有明显的减少;合并前的pydigits.pyc操作码包含在循环中,因为循环中的操作码被多次执行,每次执行的时间较合并前都有所减少,所以使得整个操作码序列的执行时间有明显减少。 3.2.3 存储空间 表3 操作码合并前后文件大小比较 从表3可以看出,操作码合并后所有字节码文件的大小都有所减小。 本文提出基于操作码合并的Python字节码文件防逆转算法,与文献[21]基于隐藏参数的方法相同,都是通过扩充操作码集来改变操作码序列的内容和结构,最终达到防逆转的目的。虽然目前两者都受限于操作码仅占1 Byte长度的特性,存在扩展空间不足的问题,但本文算法建立在对操作码序列特征统计分析的基础上,其不会因程序运行时参数的变化而产生影响,通用性较强。实验结果表明,在字节码文件执行效率和文件大小两方面,本文算法在操作码合并前后都有明显提升。为进一步提升字节码文件的安全性和算法的适用性,下一步将考虑通过抽象语法树的变换和优化,对多基本块之间的操作码进行合并操作。 [1] Mysterie.A Python 2.7 byte-code decompiler[EB/OL].[2017-03-10].https://github.com/wibiti/uncompyle2. [2] Niwit.Decompyle-Python decompiler[EB/OL].[2017-03-10].https://sourceforge.net/projects/decompyle/?source=typ_redirect. [3] Extremecoders.Python 1.0-3.4 bytecode decompiler[EB/OL].[2017-03-10].https://sourceforge.net/projects/easypytho ndecompiler/. [4] 蒋 华,刘 勇,王 鑫.基于控制流的代码混淆技术研究[J].计算机应用研究,2013,30(3):897-899. [5] 杨 乐,周强强,薛锦云.基于垃圾代码的控制流混淆算法[J].计算机工程,2011,37(12):23-25. [6] KUZURIN N,SHOKUROV A,VARNOVSKY N,et al.On the concept of software obfuscation in computer security[C]//Proceedings of International Conference on Information Security.Washington D.C.,USA:IEEE Press,2007:281-298. [7] 徐海银,雷植洲,李 丹.代码混淆技术研究[J].计算机与数字工程,2007,35(10):4-7. [8] 鲍福良,彭俊艳,方志刚.Java类文件保护方法综述[J].计算机系统应用,2007,16(6):124-126. [9] Py2exe:A Python distutils extension which converts Python scripts into executable windows programs[EB/OL].[2017-03-10].http://www.py2exe.org/. [10] 陈明奇,钮心忻.数字水印的研究进展和应用[J].通信学报,2001,22(5):71-79. [11] 孙圣和,陆哲明.数字水印处理技术[J].电子学报,2000,28(8):85-90. [12] 陈 晗,赵轶群,缪亚波.Java字节码的水印嵌入[J].计算机应用,2003,23(9):96-98. [13] COLLBERG C S,THOMBORSON C.Watermarking tamper-proofing and obfuscation[J].IEEE Transactions on Software Engineering,2000,28(8):735-746. [14] HAMILTON J,DANICIC S.An evaluation of static Java bytecode watermarking[EB/OL].[2017-02-25].https://jameshamilton.eu/sites/default/files/JavaBytecodeWatermar kingSurvey.pdf. [15] KUMAR K,KEHAR V,KAUR P.An evaluation of dynamic Java bytecode software watermarking algori-thms[J].International Journal of Security and Its Applications,2016,10(7):147-156. [16] KHOLIA D,WEGRZYN P.Looking inside the (Drop) box[EB/OL].[2017-03-01].https://www.usenix.org/system/files/conference/woot13/woot13-kholia.pdf. [17] J.C.斯普拉德林.通过操作码随机化的安全:CN 102592082 A[P].2012-07-18. [18] Omab.Python-obfuscation[EB/OL].[2017-03-10].https://github.com/citrusbyte/python-obfuscation. [19] STALLINGS M.Cryptography and network security[M].王章宜,杨 敏,杜瑞颖,等,译.北京:电子工业出版社,2012. [20] MODZELEWSKI K.Microbenchmarks[EB/OL].[2017-03-10].https://github.com/dropbox/pyston/tree/master/micro benchmarks. [21] GUELTON S.Building an obfuscated Python interpreter:we need more opcodes[EB/OL].[2017-03-10].https://blog.quarkslab.com/building-an-obfuscated-python-interpreter-we-need-more-opcodes.html.2.3 算法性能分析
3 实验结果与分析
3.1 实验环境
3.2 结果分析
4 结束语