脚本代码混淆-Python篇-pyminifier(1)
前言
最近研究了一下脚本语言的混淆方法,比如 python,javascript
等。脚本语言属于动态语言,代码大多无法直接编译成二进制机器码,发行脚本基本上相当于暴露源码,这对于一些商业应用是无法接受的。因此对脚本代码进行加固,成为很多应用的首选。代码加固的一项措施是代码混淆,增加逆向人员阅读代码逻辑的难度,拖延被破解的时间。
今天讲解一下Python代码的混淆方法,Python代码一般用作web,提供服务接口,但也有一些桌面的应用,这一部分就需要对代码进行混淆保护。以一个开源项目pyminifier (https://github.com/qiyeboy/pyminifier)来说明混淆的技巧方法,这个项目已经有4年没更新,有一些bug,但是依然值得我们学习和入门。
项目结构
框架详情:
1 2 3 4 5 | 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中代码如下:
1 2 3 4 5 | 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 2 3 4 5 6 7 8 | [[ 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代表的是token的类型
-
def是提取的token字符串
-
(1, 0)代表的是token字符串的起始行与列
-
(1, 3)代表的是token字符串的结束行与列
-
'def listified_tokenizer(source):\n' 代表所在的行
Token还原代码
能从源文件中提取token 列表,如何从token列表还原为源代码呢?其实很简单,因为提取token 列表里面有位置信息和字符串信息,所以进行字符串拼接即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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中使用的是精简方式,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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. 脚本编码
1 2 | #!/usr/bin/env python # -*- coding: utf-8 -*- |
remove_docstrings
去掉doc所指定的内容,example:
1 2 3 4 | __doc__ = """\ Module for minification functions. """ |
fix_empty_methods
修改空函数变成pass
1 2 | def myfunc(): '''This is just a placeholder function.''' |
转化为:
1 | def myfunc(): pass |
join_multiline_pairs
(1) 第一种情况:
1 2 3 | test = ( "This is inside a multi-line pair of parentheses" ) |
转化为:
1 | test = ( "This is inside a multi-line pair of parentheses" ) |
(2)第二种情况:
1 2 3 | test = [ "This is inside a multi-line pair of parentheses" ] |
转化为:
1 | test = [ "This is inside a multi-line pair of parentheses" ] |
(3)第三种情况:
1 2 3 4 5 6 7 | test = { "parentheses" : "This is inside a multi-line pair of parentheses" } |
转化为:
1 | test = { "parentheses" : "This is inside a multi-line pair of parentheses" } |
remove_blank_lines
移除空白行。
1 2 3 4 5 | test = "foo" test2 = "bar" |
转化为:
1 2 | test = "foo" test2 = "bar" |
reduce_operators
移除操作符之间的空格。
1 2 | def foo(foo, bar, blah): test = "This is a %s" % foo |
修改为:
1 2 | def foo(foo,bar,blah): test = "This is a %s" % foo |
dedent
替换代码间的缩进,比如替换成单个空格
1 2 3 | def foo(bar): test = "This is a test" |
修改为:
1 2 3 | def foo(bar): test = "This is a test" |
压缩
在这个项目中的compression.py,提供了4种代码压缩的方法,其中3个原理是一样,只不过使用的压缩算法不一样。
bz2,gz,lzma 压缩执行原理
假如新建一个1.py,并保存如下内容:
1 2 3 | if __name__ = = "__main__" : print (__name__) |
以bz2为例子,首先使用bz2算法压缩代码,然后转化成base64编码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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执行
1 2 | import bz2, base64 exec (bz2.decompress(base64.b64decode( "QlpoOTFBWSZTWdfQmoEAAAHbgEAQUGAAEgAAoyNUACAAIam1NNGgaaFNMjExMQ2Za0TTvJepAjgXb2pDBBGoliFIT04+LuSKcKEhr6E1Ag==" ))) |
这段代码就代表了最原始的代码,而使用gz,lzma压缩方式,将bz2包换成zlib 或者lzma即可。
zip执行原理
可能很多朋友不知道,Python是可以直接运行zip文件的(特别的),主要是为了方便开发者管理和发布项目。Python能直接执行一个包含 __main__.py的目录或者zip文件。
举个例子:
1 2 3 4 5 | |—— ABC / |—— A.py |—— __main__.py |
示例代码:
1 2 3 4 5 6 7 8 9 10 | # A.py def echo(): print ( 'ABC!' ) # __main__.py if __name = = '__main__' : import A A.echo() |
可以直接将多个文件压缩成一个zip文件,直接运行zip文件就可以。目录结构:
1 2 3 4 5 | |—— ABC. zip / |—— A.py |—— __main__.py |
运行情况:
1 2 3 | $ python ABC. zip ABC! |
未完待续。。。
最后
关注公众号:七夜安全博客
- 回复【1】:领取 Python数据分析 教程大礼包
- 回复【2】:领取 Python Flask 全套教程
- 回复【3】:领取 某学院 机器学习 教程
- 回复【4】:领取 爬虫 教程
- 回复【5】:领取 编译原理 教程
- 回复【6】:领取 渗透测试 教程
- 回复【7】:领取 人工智能数学基础 教程
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· 展开说说关于C#中ORM框架的用法!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?