zoukankan      html  css  js  c++  java
  • 脚本代码混淆-Python篇-pyminifier(1)

    前言

    最近研究了一下脚本语言的混淆方法,比如 python,javascript等。脚本语言属于动态语言,代码大多无法直接编译成二进制机器码,发行脚本基本上相当于暴露源码,这对于一些商业应用是无法接受的。因此对脚本代码进行加固,成为很多应用的首选。代码加固的一项措施是代码混淆,增加逆向人员阅读代码逻辑的难度,拖延被破解的时间。

    今天讲解一下Python代码的混淆方法,Python代码一般用作web,提供服务接口,但也有一些桌面的应用,这一部分就需要对代码进行混淆保护。以一个开源项目pyminifier (https://github.com/qiyeboy/pyminifier)来说明混淆的技巧方法,这个项目已经有4年没更新,有一些bug,但是依然值得我们学习和入门。

    项目结构

    框架详情:

    analyze.py - 用于分析Python代码
    compression.py - 使用压缩算法压缩代码
    minification.py - 用于简化Python代码
    obfuscate.py - 用于混淆Python 代码
    token_utils.py - 用于收集Python Token
    

      

    从项目代码中,可以看到pyminifier的混淆方法是基于Token的,即基于词法分析,假如大家之前做过混淆的话,这应该属于混淆的初级方案,因为这样的混淆并不会修改代码原有的逻辑结构。

    提取Token

    如何提取Python语言的Token呢?Python中提供了专门的包进行词法分析: tokenize。使用起来很简单,在token_utils.py中代码如下:

    def listified_tokenizer(source):
    
    """Tokenizes *source* and returns the tokens as a list of lists."""
         io_obj = io.StringIO(source)
         return [list(a) for a in tokenize.generate_tokens(io_obj.readline)]

    首先读取源文件,然后通过tokenize.generate_tokens生成token列表。咱们就将这个提取token的函数保存起来,然后让他自己提取自己,看一下token列表的结构。

    [[1, 'def', (1, 0), (1, 3), 'def listified_tokenizer(source):
    '],
    [1, 'listified_tokenizer', (1, 4), (1, 23), 'def listified_tokenizer(source):
    '],
    [53, '(', (1, 23), (1, 24), 'def listified_tokenizer(source):
    '],
    [1, 'source', (1, 24), (1, 30), 'def listified_tokenizer(source):
    '],
    [53, ')', (1, 30), (1, 31), 'def listified_tokenizer(source):
    '],
    [53, ':', (1, 31), (1, 32), 'def listified_tokenizer(source):
    '],
    [4, '
    ', (1, 32), (1, 33), 'def listified_tokenizer(source):
    '],
    ......

    每一个Token对应一个list,以第一行 [1,'def',(1,0),(1,3),'def listified_tokenizer(source): ']为例子进行解释:

    1. 1代表的是token的类型

    2. def是提取的token字符串

    3. (1, 0)代表的是token字符串的起始行与列

    4. (1, 3)代表的是token字符串的结束行与列

    5. 'def listified_tokenizer(source): ' 代表所在的行

    Token还原代码

    能从源文件中提取token 列表,如何从token列表还原为源代码呢?其实很简单,因为提取token 列表里面有位置信息和字符串信息,所以进行字符串拼接即可。

    def untokenize(tokens):
        """
        Converts the output of tokenize.generate_tokens back into a human-readable
        string (that doesn't contain oddly-placed whitespace everywhere).
        .. note::
    
            Unlike :meth:`tokenize.untokenize`, this function requires the 3rd and
            4th items in each token tuple (though we can use lists *or* tuples).
        """
        out = ""
        last_lineno = -1
        last_col = 0
        for tok in tokens:
            token_string = tok[1]
            start_line, start_col = tok[2]
            end_line, end_col = tok[3]
            # The following two conditionals preserve indentation:
            if start_line > last_lineno:
                last_col = 0
            if start_col > last_col and token_string != '
    ':
                out += (" " * (start_col - last_col))
            out += token_string
            last_col = end_col
            last_lineno = end_line
        return out

    精简与压缩代码

    在pyminifier中,有两个缩小Python代码的方法:一个是精简方式,另一个是使用压缩算法的方式。

    精简

    在minification.py中使用的是精简方式,具体代码如下:

    def minify(tokens, options):
        """
        Performs minification on *tokens* according to the values in *options*
        """
        # Remove comments
        remove_comments(tokens)
        # Remove docstrings
        remove_docstrings(tokens)
        result = token_utils.untokenize(tokens)
        # Minify our input script
        result = multiline_indicator.sub('', result)
        result = fix_empty_methods(result)
        result = join_multiline_pairs(result)
        result = join_multiline_pairs(result, '[]')
        result = join_multiline_pairs(result, '{}')
        result = remove_blank_lines(result)
        result = reduce_operators(result)
        result = dedent(result, use_tabs=options.tabs)
        return result 

    上面的代码总共使用了9种方法来缩小脚本的体积:

    remove_comments

    去掉代码中的注释,但是有两类要保留:1.脚本解释器路径 2. 脚本编码

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-

     

    remove_docstrings

    去掉doc所指定的内容,example:

    __doc__ = """
    Module for minification functions.
    
    """

     

    fix_empty_methods

    修改空函数变成pass

    def myfunc():
    '''This is just a placeholder function.'''

    转化为:

    def myfunc():pass

     

    join_multiline_pairs

    (1) 第一种情况:

    test = (
    "This is inside a multi-line pair of parentheses"
    )

    转化为:

    test = ( "This is inside a multi-line pair of parentheses")

     

    (2)第二种情况:

    test = [
    "This is inside a multi-line pair of parentheses"
    ]

    转化为:

    test = [ "This is inside a multi-line pair of parentheses"]
    

      

    (3)第三种情况:

    test = {
    
    
    "parentheses":"This is inside a multi-line pair of parentheses"
    
    
    }

    转化为:

    test = { "parentheses":"This is inside a multi-line pair of parentheses"}

     

    remove_blank_lines

    移除空白行。

    test = "foo"
    
     
    
    test2 = "bar"

    转化为:

    test = "foo"
    test2 = "bar"

     

    reduce_operators

    移除操作符之间的空格。

    def foo(foo, bar, blah):
        test = "This is a %s" % foo

    修改为:

    def foo(foo,bar,blah):
        test="This is a %s"%foo
    

      

    dedent

    替换代码间的缩进,比如替换成单个空格

    def foo(bar):
    
        test = "This is a test"

    修改为:

    def foo(bar):
    
     test = "This is a test"
    

     

    压缩

    在这个项目中的compression.py,提供了4种代码压缩的方法,其中3个原理是一样,只不过使用的压缩算法不一样。

    bz2,gz,lzma 压缩执行原理

    假如新建一个1.py,并保存如下内容:

    if __name__=="__main__":
    
        print(__name__)

    以bz2为例子,首先使用bz2算法压缩代码,然后转化成base64编码。

    code='''
    
    
    if __name__=="__main__":
    
    
        print(__name__)
    
    
    '''
    
    import bz2,base64
    compressed_source = bz2.compress(code.encode("utf-8"))
    print(base64.b64encode(compressed_source).decode('utf-8'))

    输出:

    QlpoOTFBWSZTWdfQmoEAAAHbgEAQUGAAEgAAoyNUACAAIam1NNGgaaFNMjExMQ2Za0TTvJepAjgXb2pDBBGoliFIT04+LuSKcKEhr6E1Ag==

    代码压缩完成后,如何执行呢?其实就用到了exec这个函数/关键字。将编码好的内容,先base64解码,再使用bz2算法解压缩,最后获得真实的代码,并使用exec执行

    import bz2, base64
    exec(bz2.decompress(base64.b64decode("QlpoOTFBWSZTWdfQmoEAAAHbgEAQUGAAEgAAoyNUACAAIam1NNGgaaFNMjExMQ2Za0TTvJepAjgXb2pDBBGoliFIT04+LuSKcKEhr6E1Ag==")))
    

     

    这段代码就代表了最原始的代码,而使用gz,lzma压缩方式,将bz2包换成zlib 或者lzma即可。

    zip执行原理

    可能很多朋友不知道,Python是可以直接运行zip文件的(特别的),主要是为了方便开发者管理和发布项目。Python能直接执行一个包含 __main__.py的目录或者zip文件。

    举个例子:

    |—— ABC/
    
    |—— A.py
    
    |—— __main__.py

    示例代码:

    # A.py
    def echo():
    
        print('ABC!')
    
    # __main__.py
    if __name == '__main__':
    
        import A
        A.echo()
    

      

    可以直接将多个文件压缩成一个zip文件,直接运行zip文件就可以。目录结构:

    |—— ABC.zip/
    
    |—— A.py
    
    |—— __main__.py
    

      

    运行情况:

    $ python ABC.zip
    
    ABC!

    未完待续。。。

    最后

    关注公众号:七夜安全博客

    • 回复【1】:领取 Python数据分析 教程大礼包
    • 回复【2】:领取 Python Flask 全套教程
    • 回复【3】:领取 某学院 机器学习 教程
    • 回复【4】:领取 爬虫 教程
    • 回复【5】:领取 编译原理 教程 
    • 回复【6】:领取 渗透测试 教程 
    • 回复【7】:领取 人工智能数学基础 教程
    本文章属于原创作品,欢迎大家转载分享,禁止修改文章的内容。尊重原创,转载请注明来自:七夜的故事 http://www.cnblogs.com/qiyeboy/
  • 相关阅读:
    中国石油昆仑加油卡
    157 01 Android 零基础入门 03 Java常用工具类01 Java异常 01 异常介绍 02 异常内容简介
    156 01 Android 零基础入门 03 Java常用工具类01 Java异常 01 异常介绍 01 Java常用工具类简介
    155 01 Android 零基础入门 02 Java面向对象 07 Java多态 07 多态知识总结 01 多态总结
    154 01 Android 零基础入门 02 Java面向对象 07 Java多态 06 内部类 05 匿名内部类
    153 01 Android 零基础入门 02 Java面向对象 07 Java多态 06 内部类 04 方法内部类
    152 01 Android 零基础入门 02 Java面向对象 07 Java多态 06 内部类 03 静态内部类
    151 01 Android 零基础入门 02 Java面向对象 07 Java多态 06 内部类 02 成员内部类
    150 01 Android 零基础入门 02 Java面向对象 07 Java多态 06 内部类概述 01 内部类概述
    149 01 Android 零基础入门 02 Java面向对象 07 Java多态 05 接口(重点)07 接口的继承
  • 原文地址:https://www.cnblogs.com/qiyeboy/p/11524806.html
Copyright © 2011-2022 走看看