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/
  • 相关阅读:
    CentOS 6.9/7通过yum安装指定版本的MySQL
    CentOS的el5, el6, el7代表什么
    MySQL的mysql.sock文件作用(转)
    MySQL常用命令
    Linux下以特定用户运行命令
    简述TCP的三次握手过程
    Tomcat-connector的微调(1): acceptCount参数
    tomcat修改jsessionid在cookie中的名称
    使用@Async异步注解导致该Bean在循环依赖时启动报BeanCurrentlyInCreationException异常的根本原因分析,以及提供解决方案【享学Spring】
    [LeetCode] Construct the Rectangle 构建矩形
  • 原文地址:https://www.cnblogs.com/qiyeboy/p/11524806.html
Copyright © 2011-2022 走看看