zoukankan      html  css  js  c++  java
  • 通过字节码混淆来保护Python代码

    转载:http://blog.csdn.net/ir0nf1st/article/details/61650984

    <0x00> 前言

    Python开发者常常面临这样一个难题,即如何保护代码中的技术秘密。笔者尝试过的一些Python代码保护工具要么难以有效实现该目标,要么有效但是有着不可忽视的缺点。最近笔者也遇到了这个问题,在难以找到一个有效解决方案的情况下,不得不自行开发了一个字节码混淆器。本文首先对常见的Python代码保护机制以及几个比较容易获得的Python代码保护工具进行了简单的分析,然后展示了通过字节码混淆来保护Python代码的技术原理。

    <0x01> 源码混淆

    笔者尝试过两个源码混淆工具。一个是pyminifier,另一个是提供在线源码混淆服务的http://pyob.oxyry.com/。这两个工具的工作方法类似,他们对类名/函数名/变量名进行重新命名,pyminifier甚至能够对部分Python常量进行扰乱(如True/False/None),然而代码的逻辑与控制流并没有被改变。阅读被混淆过的源码对于读者的眼睛来说是一种摧残,也带来了理解上的困难,但是简单的改名甚至无法对抗基本的文本查找与替换。源码混淆如果只在名字替换上下功夫,要实现代码保护无异于缘木求鱼。笔者认为:源码混淆要实现代码保护,则必须提取目标程序的抽象语法树(Abstract Syntax Tree)并对语法树进行修改,再根据修改后的语法树生成新的源码。然而这么做的工作量不会比实现一个编译器来得更少。这篇英文文章更深入的介绍了基于AST分析的Python源码混淆方法,有兴趣的读者可参考。 
    以下是pyminifier试用结果,读者可以评估一下名字替换是否可以有效保护源码。http://pyob.oryry.com提供的服务要比pyminifier来得更简单,这里就不提供试用效果了。 
    混淆前的样例代码:

    class SampleClass:
        def __init__(self):
            self.data = None
    
        def method1SampleClass(self, arg):
            self.data = arg
    
    def function_with_if(arg):
        if arg == True:
            pass
        else:
            pass
    
    def function_with_if1(arg=True):
        if arg == True:
            print('True')
        else:
            print('False')
    
    def function_with_if2():
        if True:
            print('True')
        else:
            print('False')
    
    def function_with_try_except1():
        try:
            data = 1/0
        except:
            print('Constructed Control Flow')
    
    def function_with_try_except2():
        try:
            pass
            print('Constructed Control Flow')
        except:
            pass
    
    global_var1, global_var2, global_var3
    pass
    
    a = SampleClass()
    a.method1SampleClass()
    function_with_if(False)
    function_with_if1()
    del global_var1, global_var2, global_var3

    混淆后的代码:

    class N:
    y=None
    T=True
    H=False
     def __init__(P):
      P.data=y
     def b(P,R):
      P.data=R
    def x(R):
     if R==T:
      pass
     else:
      pass
    def L(arg=T):
     if arg==T:
      print('True')
     else:
      print('False')
    def I():
     if T:
      print('True')
     else:
      print('False')
    def d():
     try:
      n=1/0
     except:
      print('Constructed Control Flow')
    def E():
     try:
      pass
      print('Constructed Control Flow')
     except:
      pass
    global_var1,global_var2,global_var3
    pass
    a=N()
    a.method1SampleClass()
    x(H)
    L()
    del global_var1,global_var2,global_var3
    # Created by pyminifier (https://github.com/liftoff/pyminifier)

    值得一提的是pyminifier在对None/True/False进行扰乱的时候似乎有一个bug。

    class N:
    y=None
    T=True
    H=False
     def __init__(P):
      P.data=y
     def b(P,R):
      P.data=R
    def x(R):
     if R==T:
      pass
     else:
      pass

    其中变量y/T/H的作用域在class N内部,然而下面的函数并不是class N的方法,函数中对T/H的引用超出了其作用域。

    def L(arg=T):
     if arg==T:
      print('True')
     else:
      print('False')
    def I():
     if T:
      print('True')
     else:
      print('False')

    如果读者使用pyminifier,需要注意这个问题。

    <0x02> 将Python代码打包为可执行文件

    py2exe, PyInstaller将Python代码以及Python运行环境(如Python解释器,应用依赖的标准模块等)打包为可执行文件,这样你的Python代码就可以在一个没有事先安装Python的目标机器上运行。py2exe将Python代码及其依赖文件打包成一个zip包,解压后你会发现所有文件都在那等着被反编译。PyInstaller比py2exe更安全一些,它支持对Python代码进行AES加密,然而明文的AES密钥也被存储在打包文件中。 
    另外一个选择是Cython。这是是一个将Python扩展到C的模块,开发者可以在Python中直接使用类似于C的语法进行开发,或者间接使用C语言进行开发。开发者开发的C模块可以被Python代码调用,同时该C模块在运行环境上是native binary code(x86 Windows平台上就是x86_PE格式,ARM Linux平台则为 arm_elf,arm_eabi或者其他)。一定程度上native binary code对逆向工程者提出了更高的技术要求,增加了逆向工程的难度,从而实现了对开发者代码的保护。但是本质上来说,它保护的不过是开发者的C代码,而不是Python代码。而使用Cython的缺点也是显而易见的,C语言开发难度要显著高于Python,C语言开发的模块也导致整个软件丧失了跨平台的特性。 
    如果你不在意Cython带来的缺点,使用Cython来保护你的C代码不失为一个好的选择。

    <0x03> 使用私有Python Bytecode指令集

    对于同一个版本的Python,Python编译器、解释器、反汇编器以及反编译器都使用同样的Bytecode指令集。不同版本的Python则使用不同的Bytecode指令集,这也是为何Python 2.X编译器产生的pyc文件无法被Python 3.x解释器执行的原因之一。 
    如果使用私有的Bytecode指令集,那么通常的Python反汇编器和反编译器无法工作在由你私有Python编译器产生的pyc文件上,也相当于保护了你的Python代码。这么做的代价是你的Python应用只能在你的私有Python解释器上运行。

    <0x04> 字节码混淆

    字节码混淆可以非常容易的欺骗通常的反汇编器和反编译器,同时不影响代码的正常执行。下面这个例子展示了如何欺骗Uncompyle6反编译器以及dis反汇编器:

    #一个简单的Python应用 sample1.py
    print 'Hello World'

    对其进行编译:

    python -m py_compile sample1.py

    对编译后的sample1.pyc使用Python内置dis模块反汇编:

    >>> import marshal,dis
    >>> fd = open('sample1.pyc', 'rb')
    >>> fd.seek(8)
    >>> sample1_code_obj = marshal.load(fd)
    >>> fd.close()
    >>> dis.dis(sample1_code_obj)
      1           0 LOAD_CONST               0 ('Hello World')
                  3 PRINT_ITEM
                  4 PRINT_NEWLINE
                  5 LOAD_CONST               1 (None)
                  8 RETURN_VALUE
    >>>

    以上的汇编代码笔者肉眼反汇编的结果如下:

    0 LOAD_CONST     0 ('Hello World') #加载co_consts[0]到栈顶,co_consts[0]存储着常量字符串'Hello World'
    3 PRINT_ITEM                       #打印栈顶到sys.stdout,即print 'Hello World'
    4 PRINT_NEWLINE                    #打印新行到sys.stdout,此指令因print语句而由编译器自动生成
    5 LOAD_CONST     1 (None)          #加载co_consts[1]到栈顶,co_consts[1]存储着None
    8 RETURN_VALUE                     #将栈顶返回给调用者,此两条指令为编译器自动生成

    现在我们修改sample1.pyc,在程序入口增加一条绝对跳转指令(可以使用UltraEdit 16进制插入功能修改pyc文件,”JUMP_ABSOLUTE 3”在Python 2.7中对应的字节码为 0x71 0x03 0x00。修改code string内容的同时应修改code string的长度,此处增加了一个3字节指令),使用内置dis模块反汇编的结果如下:

      1           0 JUMP_ABSOLUTE            3                 #自行添加
            >>    3 LOAD_CONST               0 ('Hello World')
                  6 PRINT_ITEM
                  7 PRINT_NEWLINE
                  8 LOAD_CONST               1 (None)
                 11 RETURN_VALUE

    如果读者对汇编代码有一定认识,就会明白此处的绝对跳转对Python虚拟机执行此程序基本没有影响(除了增加一个指令执行周期),然而这个绝对跳转将成功欺骗反编译器。使用Uncompyle6反编译的结果如下:

    <<< Error: Decompiling stopped due to <class 'uncompyle6.semantics.pysource.ParserError'>
    • 1

    如果一个pyc文件无法被反编译,初级的破解者可能就会止步于此了,但对于有经验的工程师来说这还远远不够。同样的,我们还要让通常的反汇编器也无法工作才行。按下面的汇编代码继续加工上面的sample1.pyc。

    |   1           0 JUMP_ABSOLUTE        [71 06 00]     6 
    |               3 LOAD_CONST           [64 FF FF] 65535 (FAKE!)
    |         >>    6 LOAD_CONST           [64 00 00]     0 (Hello World)
    |               9 PRINT_ITEM           [47 -- --]
    |              10 PRINT_NEWLINE        [48 -- --]
    |              11 LOAD_CONST           [64 01 00]     1 (None)
    |              14 RETURN_VALUE         [53 -- --]

    以上第二条指令的意思是加载code object常量表的第65535项到栈顶。在上述sample1.pyc中,常量表的长度为2,下标65535已超出常量表的范围,所以这是条非法指令。但由于第一条绝对跳转的存在,第二条指令永远都不会被执行。通常的反汇编器如dis会尽全力列举有用的信息,但并不能理解实际执行的控制流,当反汇编器尝试反汇编第二条指令时,会试着去读取code object常量表的第65535项并且抛出一个’tuple index out of range’的意外。Python内置dis模块的出错信息如下:

    >>> fd = open('sample1.pyc', 'rb')
    >>> fd.seek(8)
    >>> import marshal,dis
    >>> sample1_code_obj = marshal.load(fd)
    >>> dis.dis(sample1_code_obj)
      1           0 JUMP_ABSOLUTE            6
                  3 LOAD_CONST           65535
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "C:Python27libdis.py", line 43, in dis
        disassemble(x)
      File "C:Python27libdis.py", line 96, in disassemble
        print '(' + repr(co.co_consts[oparg]) + ')',
    IndexError: tuple index out of range
    >>>

    现在Uncompyle6和dis都被欺骗了,代码得到了有效的保护。

    <0x05> 更多的字节码混淆技术

    <0x05 0x01>虚假分支

    开发者可以故意构造复杂的分支结构,然而通过预置条件来实现仅覆盖特定分支,可以有效的浪费手动逆向者的时间与精力,即便逆向者使用控制流分析软件也无济于事。

    #flag可以是一些计算的结果
    #或者是隐藏在某处的预置常量
    #也可以是某次函数调用的返回值
    if flag is condition:
        normal_processing()
    else
        useless_but_complicated_obfuscating_code()
        or_even_invalid_code()
    try:
        some_processing()
        raise_exeception = __import__('module_does_not_exist')
        #上面的调用将抛出一个'ImportError'的意外,控制流将转向except分支
        useless_but_complicated_obfuscating_code()
    except:
        continue_normal_processing()
    try:
        some_processing()
        raise_exeception = __import__('sys').non_exist_function()
        #上面的调用将抛出一个'AttributeError'的意外,控制流将转向except分支
        useless_but_complicated_obfuscating_code()
    except:
        continue_normal_processing()
    try:
        some_processing()
        raise_exeception = 1/0
        #上面的语句将抛出一个'ZeroDivisionError'的意外,控制流将转向except分支
        useless_but_complicated_obfuscating_code()
    except:
        continue_normal_processing()

    <0x05 0x02>重叠指令

    重叠指令(Overlapping Instruction)在有变长指令的CISC机器(如X86)上有广泛应用。以x86汇编举例说明重叠指令:

    #例1单重叠指令
    00: EB 01           jmp  3
    02: 68 c3 90 90 90  push 0x909090c3
    
    #例1实际执行
    00: EB 01           jmp  3
    03: C3              retn
    #例2多重叠指令
    00: EB02                    jmp  4
    02: 69846A40682C104000EB02  imul eax, [edx + ebp*2 + 0102C6840], 0x002EB0040
    
    #例2实际执行
    00: EB02       jmp  4
    04: 6A40       push 040
    06: 682C104000 push 0x40102C
    0B: EB02       jmp  0xF
    #例3跳转至自身
    00: EBFF    jmp 1
    02: C0C300  rol bl, 0
    
    #例3实际执行
    00: EBFF    jmp 1
    01: FFC0    inc eax
    03: C3      retn

    与单一跳转指令相比,重叠指令是在跳转基础上进一步混淆控制流的技术手段,可以有效对抗逆向者。Python字节码类似于RISC指令(如ARM),其指令长度要么是三字节要么是一字节,但任然可以构造重叠指令:

    #例1 Python单重叠指令
     0 JUMP_ABSOLUTE        [71 05 00]     5 
     3 PRINT_ITEM           [47 -- --]
     4 LOAD_CONST           [64 64 01]     356
     7 STOP_CODE            [00 -- --]
    #例1 实际执行
     0 JUMP_ABSOLUTE        [71 05 00]     5 
     5 LOAD_CONST           [64 01 00]     1
    #例2 Python多重叠指令
     0 EXTENDED_ARG         [91 00 64] 
     3 EXTENDED_ARG         [91 00 53]
     6 JUMP_ABSOLUTE        [71 02 00]
    #例2 实际执行
     0 EXTENDED_ARG         [91 00 64] 
     3 EXTENDED_ARG         [91 00 53]
     6 JUMP_ABSOLUTE        [71 02 00]
     2 LOAD_CONST           [64 91 00]
     5 RETURN_VALUE         [53 -- --]

    <0x06>对抗手动逆向工程

    以上展示的是欺骗机器(反编译器和反汇编器)的技术,但是并不存在一种技术可以欺骗人类。对于愿意进行手动逆向的人来说,唯一可行的手段是增加其逆向的难度和时间成本。引入更复杂的控制流可以略微增加逆向的难度,但也不会太多, 有经验的破解者通常会对你的代码使用控制流分析软件。 
    代码扰乱可以在对抗人类的路上走得更远一些。真正的应用代码可以被加密存储在pyc文件的一个或者多个字符串常量中,程序执行时首先有一段解扰代码对加密存储的应用代码进行解扰,然后真正的应用代码被执行。精心设计的扰码算法可以对抗破解者静态分析你的应用代码。下面是一个简单的代码扰乱例子。 
    仍以上面的sample1.pyc为例,对其进行加扰:

    >>> fd = open('sample1.pyc', 'rb')  
    >>> fd.seek(8)  
    >>> import marshal  
    >>> co = marshal.load(fd)  
    >>> fd.close()  
    >>> code_string = marshal.dumps(co)  
    >>> scrambled_code = code_string.encode('zlib').encode('base64')  
    >>> print scrambled_code  
    eJxLZoACRiB2AOJifiBRyMaQ8v9/CgODu0cKI0OwBhNIghtIeKTm5OQrhOcX5aT4aYC0oRHFXCAi  
    MbcgJ9VIr6CyhAPItcnNTynNSbUD2VACUgQAIHcTlg==  
    

    将加扰后的代码串拷贝到下面的descramble.py中

    scrambled_code_string='eJxLZoACRiB2AOJifiBRyMaQ8v9/CgODu0cKI0OwBhNIghtIeKTm5OQrhOcX5aT4aYC0oRHFXCAiMbcgJ9VIr6CyhAPItcnNTynNSbUD2VACUgQAIHcTlg=='  
    exec __import__('marshal').loads(scrambled_code_string.decode('base64').decode('zlib'))  

    执行descrmble.py

    >python descramble.py  
    Hello World  

    不要在意这个简单的加扰算法,本例只是展示加扰的概念。

    <0x07>后记

    字节码混淆(汇编混淆)在x86平台上早已广泛应用,并不是什么新技术,除了应用在Python上,其他使用字节码/汇编代码的编程语言应该都可以采用同样的原理进行代码保护。

  • 相关阅读:
    Mysql登录错误:ERROR 1045 (28000): Plugin caching_sha2_password could not be loaded
    Docker配置LNMP环境
    Docker安装mysqli扩展和gd扩展
    Docker常用命令
    Ubuntu常用命令
    单例模式的优缺点和使用场景
    ABP 多租户数据共享
    ABP Core 后台Angular+Ng-Zorro 图片上传
    ERROR Error: If ngModel is used within a form tag, either the name attribute must be set or the form control must be defined as 'standalone' in ngModelOptions.
    AbpCore 执行迁移文件生成数据库报错 Could not find root folder of the web project!
  • 原文地址:https://www.cnblogs.com/DeeLMind/p/7691019.html
Copyright © 2011-2022 走看看