一次基于ast的python反混淆尝试
0x00 前言
之前看到一个python混淆的网站,感觉很不错,就拿过来研究了一下,当时霸王硬上弓(指扔给idea美化代码之后然后插print)硬是给撅出来了


当时硬print的时候就在想,能否以python解释器的方法来处理分析这些内容,然后就接触到一个东西”AST”。
最近刚好看到他们家换了新的混淆方法,就拿过来好好研究怎么用AST分析了。
0x01 前置内容
Python 源码编译过程
Python 源码到机器码的过程,以 CPython 为例,编译过程如下:
将源代码解析为解析树(Parser Tree)
将解析树转换为抽象语法树(Abstract Syntax Tree)
将抽象语法树转换到控制流图(Control Flow Graph)
根据流图将字节码(bytecode)发送给虚拟机(ceval)
可以使用以下模块进行操作:
ast 模块可以控制抽象语法树的生成和编译
py-compile 模块能够将源码换成字节码(编译),保存在 __pycache__ 文件夹,以 .pyc 结尾(不可读)
dis 模块通过反汇编支持对字节码的分析(可读)
一些简单代码的ast分析
这里先上一些简单的例子,为后面的分析铺垫。
1  | import ast  | 
得到的输出是这样的:
1  | python .\test.py  | 
大致看一看,不难发现,ast处理后的python其实已经和bytecode都差不多了,相对应的位置都一样,只不过可读性比字节码好上了不少,看起来还特别的直观。
Python ast的抽象文法:
这里就摘了一部分,是这次反混淆需要用到的
Abstract Grammar
| mod | Module Interactive Expression FunctionType  | 
| stmt | FunctionDef<brReturn Deleate Assign …  | 
| expr | BllOp NamedExpr Lambda …  | 
| … | … | 
在我们一个python代码里,出现的主要还是 stmt 里的内容,定义函数FunctionDef,赋值Assign,在这些stmt里面是各种expr
结合上面的语句大致对照一下:
1  | Import(names=[alias(name='requests')]), --> import requests  | 
0x02 开淦
这是一个print(1+1)

被加密&混淆后的结果
这里先剧透一下,上面的obfuscate在第一层用不上,这里就先看前面的加密部分。
大致扫了一眼,整个代码里面,全是变量定义外加lambda,最后执行的表达式(expr)也被混在了这一行里面,这里仿照ast.py中注释给出的代码样例,搓出Assign和Expr的抽象文法
1  | class AssignExtractor(ast.NodeVisitor):  | 
开始分析:
先来一个print(1+1)
1  | #pip install pycryptodome , It works only v3.11 Above.  | 
乍一看,用了大量的lambda把代码写成了一行,想提取里面的代码很难,我第一次破的时候是直接扔到pycharm里面格式化到处插print看变量的值,为了对准lambda表达式后面的;挺痛苦的,上AST看一下。
因为里面的代码主要是赋值(Assign)语句,最后执行的代码(Expr)跟在了lambda的分号后面了,先大致拉出来看看:
1  | from extractors import AssignExtractor,ExprExtractor  | 


代码的结构就能很轻松地找出来,之后便可以通过在代码里插print的方法来确定我被混淆的代码最后哪里运行的
1  | ____(''.join((chr(int(OO00O0OO00O0O0OO00 / 2)) for OO00O0OO00O0O0OO00 in [202, 240, 202, 198] if _____ != ______)))(f"""____("".join(chr(i) for i in [101,120,101,99]))({____(base64.b64decode(codecs.decode(zlib.decompress(base64.b64decode(b'eJw9kN1ygjAUhF8JIkzlMo6mEnIcHVIM3AGtoPIT2wSSPH2p7fTu252d2T3n3MkyK896dLvrSMIeaGxEGn0l/rpiLu3hlXm5yxDmO8tQZIDoeUQLr4oWePxk8VZfBpr9af8mXdzLTk8swRbP25bNzPvP8qwWJDRA8RX4vhLkfvuk0QRl3DOUekDC9xHZVnBcyUnXY7mtBrIOBDEKXNRl3KiBBor25l5MN7U5qSA/HsJiVpfsVIQ/Hj4dgoSYOndx+7tZLZ2m3qA4AFpUD6RDsbLXB2m0dPuPZa8GblvoGm/gthdI+8PxyYtnXqRLl9uiJi+xBbqtCmKm8/K3b7hsbmQ=')).decode(), ''.join((chr(int(i / 8)) for i in [912, 888, 928, 392, 408]))).encode()))})""")  | 
把这段代码单独拎出来,看看里面长啥样:
这里用graphviz库来做个可视化  Pythonの抽象構文木をGraphvizで可視化する
pip install graphviz
1  | from graphviz import Digraph  | 

当这张图拉出来了之后,一目了然,把这个搜索改成广搜,来一层一层拨开
1  | import random ,base64,codecs,zlib;pyobfuscate=""  | 

这里想到用eval直接打印出来变量,直接看

我在整理笔记到这里的时候,突发奇想,既然我都能把整个逻辑弄成图,为啥不直接把对应的值也弄上去,到时候分析起来就更方便了,也就不需要到处print看值
1  | def bfs_visit(root):  | 
这不比命令行里一条条对数据方便多了
仔细看看,左边NoneType上方是exec,右边是个JoinedStr,结合这里面一堆call,不难看出最后执行的是exec(JoinedStr)

但是右边又套娃套了好几层

用随机数特定的种子来生成特定的文字,玩的挺脏的,得到反混淆后的代码:
1  | 
  | 
简单一看,又想骂娘https://pyobfuscate.com/public/pyd2/aes.txt
代码还是那一些,同样的思路继续干,这里把最后一个Expr拿出来吧,原代码太长了
1  | _______.___________________(_______.________________(_______.________________________())[:______._ * ______._] + _______.________________(_______._______________())[______.___] + _______.________________(_______._________________([]))[______.___] + _______.________________(_______.________________________())[______._ ** ______._ + ______.__])(____, _______._______________________(), _______.________________(_______.________________________())[______._ ** ______._ + ______.__] + _______.________________(_______.______________())[______._] + _______.________________(_______.________________________())[______._ ** ______._ + ______.__] + _______.________________(_______.________________________())[______.___])  | 
左边exec
右边compile
看到这里,compile上加载了一个前面代码的变量,这里再改进一下代码,同时更换输出格式为svg,避免因为图片过大报错
1  | import ast  | 
这里放张svg图,建议新建窗口打开
找到了,因为变量类型是ast.Moudle,就用ast.unpares还原代码
print(ast.unparse(____))

1  | try:  | 
仔细审审的话,发现上面的try必定出错,不需要管,最后得到解密代码是:
1  | import base64, os, hashlib, random  | 
嗯哼,这么强的混淆只是为了保护一个解密器的代码。
0x03 复盘
其实只是破解这个混淆的话,根本不需要上ast来大炮打蚊子,直接把以下内容送进去混淆然后运行:
1  | import pdb  | 

dis一下

ast分析确实有很多好处的,首先是整个代码的就变得非常直观了,比较适合研究这个混淆的原理,能一眼看出来我这个运行的代码是如何被一步一步拼接出来的,比如这个compile
最后生成分析图像的代码:
1  | from queue import Queue  | 


















