脚本代码混淆-Python篇-pyminifier(1)
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
脚本代码混淆-Python篇-pyminifier(1)
前⾔
最近研究了⼀下脚本语⾔的混淆⽅法,⽐如python,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中代码如下:
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):\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 列表⾥⾯有位置信息和字符串信息,所以进⾏字符串拼接即可。
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 != '\n':
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 = press(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】:领取⼈⼯智能数学基础教程。