前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >脚本代码混淆-Python篇-pyminifier(1)

脚本代码混淆-Python篇-pyminifier(1)

作者头像
七夜安全博客
发布2019-11-06 14:23:28
1.9K0
发布2019-11-06 14:23:28
举报
文章被收录于专栏:七夜安全博客

微信公众号:七夜安全博客 关注信息安全技术、关注 系统底层原理。问题或建议,请公众号留言。

前言

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

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

项目结构

框架详情:

代码语言:javascript
复制
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中代码如下:

代码语言:javascript
复制
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列表的结构。

代码语言:javascript
复制
[[1, 'def', (1, 0), (1, 3), 'def listified_tokenizer(source):\n'],
[1, 'listified_tokenizer', (1, 4), (1, 23), 'def listified_tokenizer(source):\n'],
[53, '(', (1, 23), (1, 24), 'def listified_tokenizer(source):\n'],
[1, 'source', (1, 24), (1, 30), 'def listified_tokenizer(source):\n'],
[53, ')', (1, 30), (1, 31), 'def listified_tokenizer(source):\n'],
[53, ':', (1, 31), (1, 32), 'def listified_tokenizer(source):\n'],
[4, '\n', (1, 32), (1, 33), 'def listified_tokenizer(source):\n'],
 ......

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

  1. 1代表的是token的类型
  2. def是提取的token字符串
  3. (1, 0)代表的是token字符串的起始行与列
  4. (1, 3)代表的是token字符串的结束行与列
  5. 'def listified_tokenizer(source):\n' 代表所在的行

Token还原代码

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

代码语言:javascript
复制
def untokenize(tokens):
    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 != '\n':
            out += (" " * (start_col - last_col))
        out += token_string
        last_col = end_col
        last_lineno = end_line
    return out

精简与压缩代码

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

精简

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

代码语言:javascript
复制
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. 脚本编码

代码语言:javascript
复制
#!/usr/bin/env python

# -*- coding: utf-8 -*-
remove_docstrings

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

代码语言:javascript
复制
__doc__ = """\
Module for minification functions.
"""
fix_empty_methods

修改空函数变成pass

代码语言:javascript
复制
def myfunc():
    '''This is just a placeholder function.'''

转化为:

代码语言:javascript
复制
def myfunc():pass
join_multiline_pairs

(1) 第一种情况:

代码语言:javascript
复制
test = (
            "This is inside a multi-line pair of parentheses"
        )

转化为:

代码语言:javascript
复制
test = (   "This is inside a multi-line pair of parentheses")

(2)第二种情况:

代码语言:javascript
复制
test = [
            "This is inside a multi-line pair of parentheses"
        ]

转化为:

代码语言:javascript
复制
test = [   "This is inside a multi-line pair of parentheses"]

(3)第三种情况:

代码语言:javascript
复制
test = {
            "parentheses":"This is inside a multi-line pair of parentheses"
        }

转化为:

代码语言:javascript
复制
test = {   "parentheses":"This is inside a multi-line pair of parentheses"}
remove_blank_lines

移除空白行。

代码语言:javascript
复制
test = "foo"

  test2 = "bar"

转化为:

代码语言:javascript
复制
test = "foo"
  test2 = "bar"
reduce_operators

移除操作符之间的空格。

代码语言:javascript
复制
def foo(foo, bar, blah):
        test = "This is a %s" % foo

修改为:

代码语言:javascript
复制
def foo(foo,bar,blah):
        test="This is a %s"%foo
dedent

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

代码语言:javascript
复制
def foo(bar):
    test = "This is a test"

修改为:

代码语言:javascript
复制
def foo(bar):
 test = "This is a test"

压缩

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

bz2,gz,lzma 压缩执行原理

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

代码语言:javascript
复制
if __name__=="__main__":
    print(__name__)

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

代码语言:javascript
复制
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'))

输出:

代码语言:javascript
复制
QlpoOTFBWSZTWdfQmoEAAAHbgEAQUGAAEgAAoyNUACAAIam1NNGgaaFNMjExMQ2Za0TTvJepAjgXb2pDBBGoliFIT04+LuSKcKEhr6E1Ag==

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

代码语言:javascript
复制
import bz2, base64
exec(bz2.decompress(base64.b64decode("QlpoOTFBWSZTWdfQmoEAAAHbgEAQUGAAEgAAoyNUACAAIam1NNGgaaFNMjExMQ2Za0TTvJepAjgXb2pDBBGoliFIT04+LuSKcKEhr6E1Ag==")))

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

zip执行原理

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

举个例子:

代码语言:javascript
复制
|—— ABC/
    |—— A.py
    |—— __main__.py

示例代码:

代码语言:javascript
复制
# A.py
def echo():
    print('ABC!')

# __main__.py
if __name == '__main__':
    import A
    A.echo()

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

代码语言:javascript
复制
|—— ABC.zip/
    |—— A.py
    |—— __main__.py

运行情况:

代码语言:javascript
复制
$ python ABC.zip
ABC!
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-10-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 七夜安全博客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 项目结构
    • 提取Token
      • Token还原代码
        • 精简与压缩代码
          • 精简
            • remove_comments
            • remove_docstrings
            • fix_empty_methods
            • join_multiline_pairs
            • remove_blank_lines
            • reduce_operators
            • dedent
          • 压缩
            • bz2,gz,lzma 压缩执行原理
            • zip执行原理
        相关产品与服务
        文件存储
        文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档