一次基于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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import ast
import dis

code = '''
import requests

def req_baidu():
r = requests.get('https://www.baidu.com/')
print(r.text)

req_baidu()
'''


code_ast = ast.parse(code)
code_compile = compile(code_ast, '<string>', 'exec')

print(ast.dump(code_ast,indent=4))
dis.dis(code_compile)

得到的输出是这样的:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
python .\test.py
Module(
body=[
Import(
names=[
alias(name='requests')]),
FunctionDef(
name='req_baidu',
args=arguments(
posonlyargs=[],
args=[],
kwonlyargs=[],
kw_defaults=[],
defaults=[]),
body=[
Assign(
targets=[
Name(id='r', ctx=Store())],
value=Call(
func=Attribute(
value=Name(id='requests', ctx=Load()),
attr='get',
ctx=Load()),
args=[
Constant(value='https://www.baidu.com/')],
keywords=[])),
Expr(
value=Call(
func=Name(id='print', ctx=Load()),
args=[
Attribute(
value=Name(id='r', ctx=Load()),
attr='text',
ctx=Load())],
keywords=[]))],
decorator_list=[]),
Expr(
value=Call(
func=Name(id='req_baidu', ctx=Load()),
args=[],
keywords=[]))],
type_ignores=[])

2 0 LOAD_CONST 0 (0)
2 LOAD_CONST 1 (None)
4 IMPORT_NAME 0 (requests)
6 STORE_NAME 0 (requests)

4 8 LOAD_CONST 2 (<code object req_baidu at 0x000002245F825D10, file "<string>", line 4>)
10 LOAD_CONST 3 ('req_baidu')
12 MAKE_FUNCTION 0
14 STORE_NAME 1 (req_baidu)

8 16 LOAD_NAME 1 (req_baidu)
18 CALL_FUNCTION 0
20 POP_TOP
22 LOAD_CONST 1 (None)
24 RETURN_VALUE

Disassembly of <code object req_baidu at 0x000002245F825D10, file "<string>", line 4>:
5 0 LOAD_GLOBAL 0 (requests)
2 LOAD_METHOD 1 (get)
4 LOAD_CONST 1 ('https://www.baidu.com/')
6 CALL_METHOD 1
8 STORE_FAST 0 (r)

6 10 LOAD_GLOBAL 2 (print)
12 LOAD_FAST 0 (r)
14 LOAD_ATTR 3 (text)
16 CALL_FUNCTION 1
18 POP_TOP
20 LOAD_CONST 0 (None)
22 RETURN_VALUE

大致看一看,不难发现,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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Import(names=[alias(name='requests')]),                             -->    import requests
FunctionDef(
name='req_baidu', --> def req_baidu()
args=arguments(
posonlyargs=[],
args=[],
kwonlyargs=[],
kw_defaults=[],
defaults=[]),
body=[ -->
Assign(
targets=[ -->r = requests.get('https://www.baidu.com/')
Name(id='r', ctx=Store())],
value=Call(
func=Attribute(
value=Name(id='requests', ctx=Load()),
attr='get',
ctx=Load()),
args=[
Constant(value='https://www.baidu.com/')],
keywords=[])),
Expr( --> print(r.text)
value=Call(
func=Name(id='print', ctx=Load()),
args=[
Attribute(
value=Name(id='r', ctx=Load()),
attr='text',
ctx=Load())],
keywords=[]))],
decorator_list=[]),

0x02 开淦

这是一个print(1+1)


被加密&混淆后的结果

这里先剧透一下,上面的obfuscate在第一层用不上,这里就先看前面的加密部分。

大致扫了一眼,整个代码里面,全是变量定义外加lambda,最后执行的表达式(expr)也被混在了这一行里面,这里仿照ast.py中注释给出的代码样例,搓出AssignExpr的抽象文法

1
2
3
4
5
6
7
8
9
10
11
12
13
class AssignExtractor(ast.NodeVisitor):
def __init__(self):
self.assign_nodes = []

def visit_Assign(self, node):
self.assign_nodes.append(node)

class ExprExtractor(ast.NodeVisitor):
def __init__(self):
self.expr_nodes = []

def visit_Expr(self, node):
self.expr_nodes.append(node)

开始分析:

先来一个print(1+1)

1
2
3
4
5
6
#pip install pycryptodome  , It works only v3.11 Above.
import random ,base64,codecs,zlib;pyobfuscate=""

obfuscate = dict(map(lambda map,dict:(map,dict),['(https://pyobfuscate.com)*(decrypt)'],['''Mg9q#UDn(oW~!Zy`F!j-yD|'''.replace('\n','')]))

_=lambda OO00000OOO0000OOO,c_int=100000:(_OOOO00OO0O00O00OO:=''.join(chr(int(int(OO00000OOO0000OOO.split()[OO00O0OO00O0O0OO0])/random.randint(1,c_int)))for OO00O0OO00O0O0OO0 in range(len(OO00000OOO0000OOO.split()))));eval("".join(chr(i) for i in [101,120,101,99]))("\x73\x65\x74\x61\x74\x74\x72\x28\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f\x2c\x22\x5f\x5f\x5f\x5f\x5f\x5f\x22\x2c\x70\x72\x69\x6e\x74\x29\x3b\x73\x65\x74\x61\x74\x74\x72\x28\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f\x2c\x22\x5f\x5f\x5f\x5f\x5f\x22\x2c\x65\x78\x65\x63\x29\x3b\x73\x65\x74\x61\x74\x74\x72\x28\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f\x2c\x22\x5f\x5f\x5f\x5f\x22\x2c\x65\x76\x61\x6c\x29");__='600840 10052792 2475510 107811 3460338 725070 743968 2892000 2595808 1123520 4498098 4658724 9505818 3510345 255392 146490 5557929 9774387 9643374 676195 8169140 8968656 7951905 2729216 6994785 2809039 2272480 238206 8998248 10083880 1132512 1887269 9978295 4040976 199290 720029 6381240 390456 4855272 5536608 8270336 5334956 137240 1950112 813888 1000864 14176 4719645 7434130 4414928 6253299 9947928 1058600 1230358 2126544 2411955 8232000 3136064 3545955 10065990 11478610 1845676 5793228 1659528 8606412 2662784 9252354 3826789 8515228 10136529 9876386 4503170 4636636 3050030 2304864 8648920 3476588 1063810 6624464 4304298 1150491 8042410 11245620 2352544 7278969 5070780 3834960 143016 6244008 3168128 11537244 1865133 1213344 1977057 519120 3126900 1538392 2683994 3910416 125890 1943840 169376 2568608 2306112 1493210 846355 4957785 3989836 8217104 10113987 6212658 6166328 5037850 7088140 89080 2665299 9719915 11920920 8955970 163995 576706 283176 3952332 6138720 8659980 10319940 3459800 1280676 161860 51870 2435250 6931656 3196522 1527030 341905 7265895 9809455 5280688 6588183 1684008 10751112 3620735 3711935 2101440 809948 7445910 7656305 6875824 7874685 7469960 4394725 5493528 3843530 1205130 2690707 1967374 2228611 1179175 1150372 171600 701454 4804904 669900 5363840 4755408 11124985 3124634 2961893 2837437 10306240 6771644 3092793 3541328 182988 7504380 2047000 2964060 3378704 8487488 7190998 3697158 1008513 9005208 7376139 3927743 9552368 2742597 5133926 6206652 2311680 3009798 833028 10506608 3530296 4332300 1356850 2624527 2751793 2669733 2394070 3060196 9653172 845520 3047668 1129650 1732414 1747310 6141852 3553786 8646840 10742180 287180 1469024 8047488 11999933 3563346 859220 420224 1719072 288032 236160 8018628 6755070 3157506 9098557 82624 8832714 3347765 2617768 861504 1658215 5273592 2594072 661024 902160 6018871 5059712 9333546 5543478 10761204 2640896 8903453 1575480 7633185 2561625 10578968 1218540 2351744 2321307 6116045 1633408 7015763 5559960 703580 194336 3119584 275968 733760 8284032 10978086 2905647 3348153 823648 7268835 6811105 2865536 6322155 8007685 196784 7085907 1614012 2185672 1955680 2770597 3622466 1278320 2700033 3743630 6963888 713088 5437432 1507305 2370048 8338983 4488036 4277988 9789636 9784072 5294239 4570980 2052020 2932737 873420 692064 2712832 1440256 493184 2269836 5935947 2087019 3347070 9042473 2466925 1163640 715299 5119400 61600 6803360 3070472 3586505 7106652 2033070 3448770 1332254 3203700 10746064 3431176 5216964 6666840 4895988 1158993 1447466 1891930 7078112 6234472 5222771 3231394 5588080 4378418 11000396 10886880 8793728 1153926 5624706 10051328 4147000 877546 3422952 2137083 9117108 160089 559164 5589552 1199496 4719258 5596015 6874390 2490348 1775612 1560720 4793584 715768 4420870 1858864 1768731 6089081 782892 9675759 443322 3954581 1434120 5588080 7513732 9453620 9258872 2909040 2799450 94254 10129700 9949920 11461032 497182 218660 779670 2491648 2679584 494368 352064 4780650 2815914 294496 7500159 7957680 3969000 180320 2806720 695360 4723901 2923730 6454392 9958698 3237507 9151509 4419136 548540 636352 2456512 1158016 760864 1530048 1579104 2585568 430784 2442792 6334013 8462433 5897208 1869828 4518740 3117160 5861968 1116906 2769468 816450 2827072 1415232 1191040 2284736 8500463 5873256 4862550 8653986 474048 4160392 11480880 2319080 5977776 4726700 1302857 2626355 2011353 6087816 4281612 7839 8072324 1344846 941040 376416 1535392 25216 1638144 940672 908128 1618464 2692032 10648056 9403706 9440490 4338990 8526326 10022230 3095680 5052656 1556850 3580776 899200 322624 1953120 70272 295072 4593225 1466046 1091200 6202410 2524200 3669480 7108528 2021742 3980813 775188 2749880 879060 7325537 2466936 3110290 5079795 2893968 18560 2327936 929024 2551104 2492384 250208 2255232 2757472 1236384 1442994 8935815 6523840 4058288 758816 5608275 159264 4936678 7766440 635360 3872280 3241388 98154 46120 2160368 1370625 2638555 1671604 1677458 10174381 1842902 2885703 1477056 2982847 11056675 3048096 4126658 5386576 8473294 255852 9015797 5719266 523215 5380544 7602876 3131200 3952665 5033820 6584982 3005160 3080910 7898256 1513884 2341428 858130 2530240 1594784 2112896 2613536 9160801 10402320 9666407 2264229 3761800 3583302 3224816 6873656 7062880 2358440 1934464 2074850 443128 2641596 11325900 7407946 5716016 5132800 3202520 2705549 2412399 473240 41376 1962080 2383136 2582624 116230 8708018 5645880 6635178 8949913 7043904 9106580 3237618 801350 193792 558464 1907744 2121536 7285534 6910080 4454403 7914654 3865800 9856668 3906900 1701828 590760 464890';why,are,you,reading,this,thing,huh="\x5f\x5f\x5f\x5f","\x69\x6e\x28\x63\x68\x72\x28\x69\x29\x20\x66\x6f","\x28\x22\x22\x2e\x6a\x6f","\x72\x20\x69\x20\x69\x6e\x20\x5b\x31\x30\x31\x2c\x31\x32\x30\x2c","\x31\x30\x31\x2c\x39\x39","\x5f\x5f\x29\x29","\x5d\x29\x29\x28\x5f\x28";b='eJxzdK8wccz1A+IwYyBt6OheketYHmYKAFuyB3k=';____("".join (chr (int (OO00O0OO00O0O0OO00 /2 ))for OO00O0OO00O0O0OO00 in [202 ,240 ,202 ,198 ] if _____!=______))(f'\x5f\x5f\x5f\x5f\x28\x22\x22\x2e\x6a\x6f\x69\x6e\x28\x63\x68\x72\x28\x69\x29\x20\x66\x6f\x72\x20\x69\x20\x69\x6e\x20\x5b\x31\x30\x31\x2c\x31\x32\x30\x2c\x31\x30\x31\x2c\x39\x39\x5d\x29\x29({____(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()))})')

乍一看,用了大量的lambda把代码写成了一行,想提取里面的代码很难,我第一次破的时候是直接扔到pycharm里面格式化到处插print看变量的值,为了对准lambda表达式后面的;挺痛苦的,上AST看一下。

因为里面的代码主要是赋值(Assign)语句,最后执行的代码(Expr)跟在了lambda的分号后面了,先大致拉出来看看:

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
26
27
from extractors import AssignExtractor,ExprExtractor

import ast
import dis

with open('./obfuscated_code.py', 'r') as f:
code = f.read()

tree = ast.parse(code)
assignExtractor = AssignExtractor()
assignExtractor.visit(tree)
ass_nodes = assignExtractor.assign_nodes

exprExtractor = ExprExtractor()
exprExtractor.visit(tree)
expr_nodes = exprExtractor.expr_nodes

with open('./assin_output.txt','w') as f1:
for i in ass_nodes:
f1.write(ast.dump(i)+'\n')
f1.write(ast.unparse(i)+'\n\n')

with open('./expr_output.txt','w') as f2:
for i in expr_nodes:
f2.write(ast.dump(i)+'\n')
f2.write(ast.unparse(i)+'\n\n')


代码的结构就能很轻松地找出来,之后便可以通过在代码里插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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from graphviz import Digraph

import ast
import dis

def visit(node, nodes, pindex, g):
name = str(type(node).__name__)
index = len(nodes)
nodes.append(index)
g.node(str(index), name)
if index != pindex:
g.edge(str(index), str(pindex))
for n in ast.iter_child_nodes(node):
visit(n, nodes, index, g)

with open('./obfuscated2_code.py', 'r') as f:
code = f.read()

tree = ast.parse(code)

graph = Digraph(format="png")
visit(tree, [], 0, graph)
graph.render("obfuscated2")

当这张图拉出来了之后,一目了然,把这个搜索改成广搜,来一层一层拨开

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import random ,base64,codecs,zlib;pyobfuscate=""
from collections import deque

import ast

obfuscate = dict(map(lambda map,dict:(map,dict),['(https://pyobfuscate.com)*(decrypt)'],['''Mg9q#UDn(oW~!Zy`F!j-yD|'''.replace('\n','')]))

_=lambda OO00000OOO000
....
....
....
yB3k='

# ↓↓↓↓ 下面的这些代码是前面分析出来得到的最后执行的表达式
code = """
_____("".join (chr (int (OO00O0OO00O0O0OO00 /2 ))for OO00O0OO00O0O0OO00 in [202 ,240 ,202 ,198 ] if _____!=______))(f'\x5f\x5f\x5f\x5f\x28\x22\x22\x2e\x6a\x6f\x69\x6e\x28\x63\x68\x72\x28\x69\x29\x20\x66\x6f\x72\x20\x69\x20\x69\x6e\x20\x5b\x31\x30\x31\x2c\x31\x32\x30\x2c\x31\x30\x31\x2c\x39\x39\x5d\x29\x29({____(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()))})')

"""

# ↓↓↓↓ 使用广搜,一层一层分析

def bfs_visit(root):
nodes = []
queue = deque([(root, None)])
while queue:
node, parent_index = queue.popleft()
index = len(nodes)
nodes.append(index)
if parent_index is not None:
nodes.append(index)
for child in ast.iter_child_nodes(node):
try :
print("[+]",eval(ast.unparse(child))) # <--- 这里获取值

except:
print("[-]",ast.unparse(child))
pass
queue.append((child, index))
print("---------------------------------分割线------------------------------")
return nodes

tree = ast.parse(code)
bfs_visit(tree)

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

我在整理笔记到这里的时候,突发奇想,既然我都能把整个逻辑弄成图,为啥不直接把对应的值也弄上去,到时候分析起来就更方便了,也就不需要到处print看值

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
26
27
28
29
30
31
def bfs_visit(root):
queue = Queue()
queue.put((root, 0))
nodes = []
g = Digraph(format='png')
while not queue.empty():
node, pindex = queue.get()
name = str(type(node).__name__)
index = len(nodes)
nodes.append(index)
try:
value = eval(ast.unparse(node))
except:
value = ast.unparse(node)
pass
try:
g.node(str(index), name+' '+str(type(value)) + ' '+str(value))
except:
g.node(str(index), name+' '+str(type(value)))
if index != pindex:
g.edge(str(index), str(pindex))
for n in ast.iter_child_nodes(node):
queue.put((n, index))
return g



tree = ast.parse(code)
tree = ast.parse(code)
graph = bfs_visit(tree)
graph.render("obfuscated2_plus")

这不比命令行里一条条对数据方便多了

仔细看看,左边NoneType上方是exec,右边是个JoinedStr,结合这里面一堆call,不难看出最后执行的是exec(JoinedStr)

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

用随机数特定的种子来生成特定的文字,玩的挺脏的,得到反混淆后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

try:
from requests import get
except:
input("pip install requests \\npress enter to Exit")
__import__('sys').exit()

fl=__import__("requests").get("https://pyobfuscate.com/public/pyd2/aes.txt").text
try:
from os.path import exists as ex
from os import getenv as ge,remove
loc=ge('APPDATA');p1=loc+"\\aes.txt";p2=loc+"\\rsa.txt";p3=loc+"\\aes2.txt"

if ex(p1)==True:
remove(p1)
elif ex(p2)==True:
remove(p2)
if ex(p3)==False:
with open(str(p3),"w") as e:e.write(str(fl))
exec(open(p2).read())

except:
exec(fl)

简单一看,又想骂娘
https://pyobfuscate.com/public/pyd2/aes.txt

代码还是那一些,同样的思路继续干,这里把最后一个Expr拿出来吧,原代码太长了

1
_______.___________________(_______.________________(_______.________________________())[:______._ * ______._] + _______.________________(_______._______________())[______.___] + _______.________________(_______._________________([]))[______.___] + _______.________________(_______.________________________())[______._ ** ______._ + ______.__])(____, _______._______________________(), _______.________________(_______.________________________())[______._ ** ______._ + ______.__] + _______.________________(_______.______________())[______._] + _______.________________(_______.________________________())[______._ ** ______._ + ______.__] + _______.________________(_______.________________________())[______.___])

左边exec

右边compile

看到这里,compile上加载了一个前面代码的变量,这里再改进一下代码,同时更换输出格式为svg,避免因为图片过大报错

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import ast
from graphviz import Digraph
from queue import Queue
from graphviz import Digraph


def bfs_visit(root):
queue = Queue()
queue.put((root, 0))
nodes = []
g = Digraph(format='svg')
while not queue.empty():
node, pindex = queue.get()
name = str(type(node).__name__)
index = len(nodes)
nodes.append(index)
try:
value = eval(ast.unparse(node))
except:
value = ast.unparse(node)
pass
try:
if name == 'Name':
g.node(str(index), name+' '+str(type(value)) + ' id:'+str(node.id)) # 添加加载变量的id
else:
g.node(str(index), name+' '+str(type(value)) + ' '+str(value))
except Exception as e:
print(e)
g.node(str(index), name+' '+str(type(value)))
if index != pindex:
g.edge(str(index), str(pindex))
for n in ast.iter_child_nodes(node):
queue.put((n, index))
return g



tree = ast.parse(code)
tree = ast.parse(code)
graph = bfs_visit(tree)
graph.render("final_express")

这里放张svg图,建议新建窗口打开

找到了,因为变量类型是ast.Moudle,就用ast.unpares还原代码

print(ast.unparse(____))

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
try:
import random, base64, zlib, sys, string
from Crypto.Cipher import AES, PKCS1_OAEP
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.Random import get_random_bytes
from Crypto.Signature import pkcs1_15
from Crypto.Util.Padding import pad, unpad
from Crypto.Cipher import AES
import binascii

class AESEncryption:

def __init__(self, key: bytes) -> None:
self.key = key

@classmethod
def from_nbits(cls, nbits: int=256):
cls.iv = iv
cls.key = keys
cls.mode = mode
return cls(keys)

def encrypt(self, message: bytes) -> bytes:
cipher = AES.new(self.key, self.mode, self.iv)
ciphered_data = cipher.encrypt(pad(message, AES.block_size))
return ciphered_data

def decrypt(self, message: bytes) -> bytes:
cipher = AES.new(self.key, self.mode, self.iv)
decrypted_data = unpad(cipher.decrypt(message), AES.block_size)
return decrypted_data
MESSAGE_LONG = get_random_bytes(100100)
res = ''.join(random.choices(string.ascii_uppercase + string.digits, k=16))
bb = _encrypt[125:]
keys = base64.b85decode(bb)
iv = _pubkey[100:]
mode = AES.MODE_CBC
aes = AESEncryption.from_nbits(256)
encrypted_msg = aes.encrypt(_lambda)
passkey2 = 'Obfuscated by https://pyobfuscate.com'
if not _key == passkey2:
print('Decryption Key Do not Match or Missing AES Salt 256')
sys.exit()
exec(zlib.decompress(aes.decrypt(_lambda)).decode())
except:
import base64, os, hashlib, random
from Crypto.Cipher import AES

def aes_decrypt(encrypted_data, key):
encrypted_data = base64.b85decode(encrypted_data)
salt = encrypted_data[:8]
(key, iv) = derive_key_and_iv(key, salt)
cipher = AES.new(key, AES.MODE_CFB, iv)
data = cipher.decrypt(encrypted_data[8:])
return data.decode()

def derive_key_and_iv(password, salt):
dk = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000)
key = dk[:16]
iv = dk[16:]
return (key, iv)
exec(aes_decrypt(list(obfuscate.values())[0], list(obfuscate.keys())[0][1:-1]))

仔细审审的话,发现上面的try必定出错,不需要管,最后得到解密代码是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import base64, os, hashlib, random
from Crypto.Cipher import AES

def aes_decrypt(encrypted_data, key):
encrypted_data = base64.b85decode(encrypted_data)
salt = encrypted_data[:8]
(key, iv) = derive_key_and_iv(key, salt)
cipher = AES.new(key, AES.MODE_CFB, iv)
data = cipher.decrypt(encrypted_data[8:])
return data.decode()

def derive_key_and_iv(password, salt):
dk = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000)
key = dk[:16]
iv = dk[16:]
return (key, iv)
print(aes_decrypt(list(obfuscate.values())[0], list(obfuscate.keys())[0][1:-1]))

嗯哼,这么强的混淆只是为了保护一个解密器的代码。

0x03 复盘

其实只是破解这个混淆的话,根本不需要上ast来大炮打蚊子,直接把以下内容送进去混淆然后运行:

1
2
import pdb
pdb.set_trace()

dis一下

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

最后生成分析图像的代码:

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
26
27
28
29
30
31
32
33
from queue import Queue
from graphviz import Digraph
import ast

def bfs_visit(root):
queue = Queue()
queue.put((root, 0))
nodes = []
g = Digraph(format='png')
while not queue.empty():
node, pindex = queue.get()
name = str(type(node).__name__)
index = len(nodes)
nodes.append(index)
try:
value = eval(ast.unparse(node))
except:
value = ast.unparse(node)
pass
try:
g.node(str(index), name+' '+str(type(value)) + ' '+str(value))
except:
g.node(str(index), name+' '+str(type(value)))
if index != pindex:
g.edge(str(index), str(pindex))
for n in ast.iter_child_nodes(node):
queue.put((n, index))
return g

tree = ast.parse(code)
tree = ast.parse(code)
graph = bfs_visit(tree)
graph.render("obfuscated2_plus")

Refences

dis — Python 字节码反汇编器
ast — 抽象语法树
Pythonの抽象構文木をGraphvizで可視化する

SSTI学习

首先是这广为人知的图片,就放在这里了,嗯,肥肠不错

0x01 从盘古开天辟地讲起…

Python 一切皆对象

首先,我们来梳理一下python里面错综复杂的关系
python object基类_Python 一切皆对象

object是最顶层基类,object是type的实例,而type又继承object,type是自身的实例。

type也是自身的实例化,object创造type,type创造一切和他自己,有点像道生一,一生三,三生万物…..type连自己都不放过,把自己都变成了自己的对象。type自己实例化自己。

object是父子关系的顶端,所有的数据类型的父类都是它;type是类型实例关系的顶端,所有对象都是它的实例的。

0x02 SSTI 原理

这是四大天王

1
2
3
4
5
6
7
{% ... %} for Statements

{{ ... }} for Expressions to print to the template output

{# ... #} for Comments not included in the template output

# ... ## for Line Statements
{%%}

主要用来声明变量,也可以用于条件语句和循环语句。

    {% set c= 'kawhi' %}
{% if 81==9*9 %}kawhi{% endif %}
{% for i in ['1','2','3'] %}kawhi{%endfor%}

{{}}

用于将表达式打印到模板输出,比如我们一般在里面输入2-1,2*2,或者是字符串,调用对象的方法,都会渲染出结果

    {{2-1}} #输出1
{{2*2}} #输出4

我们通常会用4简单测试页面是否存在SSTI

{##}

表示未包含在模板输出中的注释

    ##

有和{%%}相同的效果

这里的模板注入主要用到的是{{}}和{%%}

类的知识总结

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
26
27
28
__class__            类的一个内置属性,表示实例对象的类。
__base__ 类型对象的直接基类
__bases__ 类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__
__mro__ 此属性是由类组成的元组,在方法解析期间会基于它来查找基类。
__subclasses__() 返回这个类的子类集合,Each class keeps a list of weak references to its immediate subclasses. This method returns a list of all those references still alive. The list is in definition order.
__init__ 初始化类,返回的类型是function
__globals__ 使用方式是 函数名.__globals__获取function所处空间下可使用的module、方法以及所有变量。
__dic__ 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
__getattribute__() 实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
__getitem__() 调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__builtins__ 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。__builtins__与__builtin__的区别就不放了,百度都有。
__import__ 动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
__str__() 返回描写这个对象的字符串,可以理解成就是打印出来。
url_for flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
current_app 应用上下文,一个全局变量。

request 可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
request.args.x1 get传参
request.values.x1 所有参数
request.cookies cookies参数
request.headers 请求头参数
request.form.x1 post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data post传参 (Content-Type:a/b)
request.json post传json (Content-Type: application/json)
config 当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
g {{g}}得到<flask.g of 'flask_ssti'>

构造链思路:

找到父类<type ‘object’> —> 寻找子类 —> 找关于命令执行或者文件操作的模块。

  • 首先,需要一个类

  • 然后,需要获得object类,因为所有的类都是继承自它,可以用__base__,__mro__[-1]拿到

    ‘’.class.bases[0]
    <class ‘object’>

    ‘’.class.base
    <class ‘object’>

    ‘’.class.mro[1]
    <class ‘object’>
    ‘’.class.mro[-1]
    <class ‘object’>

  • 然后,需要用__subclasses__()拿到子类列表

  • 最后,在子类列表里面找到getshell的类

0x03 Bypass

过滤这部分,已经有文章整理的非常好了,俺就懒得搬运了,挂个链接

Flask-jinja2 SSTI 一般利用姿势

Refence

细说Jinja2之SSTI&bypass
Python魔法方法指南 - PyZh
Python:实例讲解Python中的魔法函数(高级语法)
Python type()函数:动态创建类
Python的MRO
SSTI模板注入总结

  

:D 一言句子获取中...