Skip to content
This repository was archived by the owner on Feb 10, 2025. It is now read-only.

Commit 33e6a11

Browse files
committed
add support for obfuscator
1 parent 3240b02 commit 33e6a11

File tree

12 files changed

+464
-7
lines changed

12 files changed

+464
-7
lines changed

INFO/changelog.json

+7-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@
88
{
99
"version": "1.0.1",
1010
"changes": [
11-
"bug fixed for BlankOBF deobfuscator"
11+
"Bug fixed for BlankOBF deobfuscator"
12+
]
13+
},
14+
{
15+
"version": "1.0.2",
16+
"changes": [
17+
"Add support for Hyperion obfuscator"
1218
]
1319
}
1420
]

INFO/version

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
V1.0.1
1+
V1.0.2

config/config.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
__VERSION__ = 'V1.0'
1+
__VERSION__ = 'V1.0.2'
22
__CHANGELOG_URL__ = 'https://raw.githubusercontent.com/Fadi002/de4py/main/INFO/changelog.json'
33
__VERSION_URL__ = 'https://raw.githubusercontent.com/Fadi002/de4py/main/INFO/version'

deobfuscators/Hyperion.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from .erebus.deobfuscator.deobfuscator import Deobfuscator, Result
2+
from .erebus.deobfuscator.unwrapper import unwrap
3+
4+
def Hyperion(file_path: str) -> str:
5+
code = open(file_path, 'r',encoding='utf-8').read()
6+
return Deobfuscator(unwrap(code)).deobfuscate().code

deobfuscators/PlusOBF.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ def PlusOBF(file_path):
99
file.write("# Cleaned with de4py | https://github.com/Fadi002/de4py\n"+cleaned)
1010
return "Saved as "+filename+'-cleaned.py'
1111
except Exception as e:
12-
return 'Detected PlusOBF but Failed to deobfuscate\n'+e
12+
return 'Detected PlusOBF but Failed to deobfuscate\n'+e

deobfuscators/detector.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
from .devtool import devtool
55
from .blankOBF import BlankOBF
66
from .PlusOBF import PlusOBF
7+
from .Hyperion import Hyperion
78
obfuscators = [
89
("PlusOBF",r"exec\(\"\"\.join\(\[chr\(len\(i\)\) for i in d\]\)\)",PlusOBF),
9-
('jawbreaker', r'([a-zA-Z_]\w{3})\s*=\s*([^;]+);', jawbreaker),
1010
("wodx", r'(?:__NO_NO){23}', wodx),
1111
("BlankOBF", r"import\s*base64,\s*lzma;\s*exec\(compile\(lzma\.decompress\(base64\.b64decode\(b'([A-Za-z0-9+/=]+)'\)\)\s*,\s*\"<string>\"\s*,\s*\"exec\"\)\)", BlankOBF),
12+
("Hyperion", r'__obfuscator__\s*=\s*[\'\"]\s*Hyperion\s*[\'\"]', Hyperion),
13+
('jawbreaker', r'([a-zA-Z_]\w{3})\s*=\s*([^;]+);', jawbreaker)
1214
]
13-
def detect_obfuscator(file_path):
15+
def detect_obfuscator(file_path) -> str:
1416
file_data = open(file_path,'r',encoding='utf8').read()
1517
for obfuscator in obfuscators:
1618
if re.search(obfuscator[1],file_data):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import logging
2+
from ast import NodeTransformer, parse, unparse
3+
from typing import Any, Dict, Tuple, Type
4+
5+
from .transformers import *
6+
from .transformers import constants
7+
8+
9+
class Result:
10+
def __init__(self, code: str, passes: int, variables: Dict[str, Any]) -> None:
11+
self.code = code
12+
self.passes = passes
13+
self.variables = variables
14+
15+
def add_variables(self) -> None:
16+
code = "\n".join(
17+
[f"{name} = {unparse(value)}" for name, value in self.variables.items()]
18+
)
19+
self.code = f"{code}\n{self.code}"
20+
21+
22+
class Deobfuscator:
23+
TRANSFORMERS: Tuple[Type[NodeTransformer], ...] = (
24+
StringSubscriptSimple,
25+
GlobalsToVarAccess,
26+
InlineConstants,
27+
DunderImportRemover,
28+
GetattrConstructRemover,
29+
BuiltinsAccessRemover,
30+
Dehexlify,
31+
UselessCompile,
32+
UselessEval,
33+
ExecTransformer,
34+
UselessLambda,
35+
RemoveFromBuiltins,
36+
)
37+
38+
AFTER_TRANSFORMERS: Tuple[Type[NodeTransformer], ...] = (
39+
LambdaCalls,
40+
EmptyIf,
41+
)
42+
43+
def __init__(self, code: str) -> None:
44+
self.code = code
45+
self.tree = parse(code)
46+
47+
def deobfuscate(self) -> Result:
48+
passes = 0
49+
code = self.code
50+
while True:
51+
for transformer in self.TRANSFORMERS:
52+
try:
53+
self.tree = transformer().visit(self.tree)
54+
except Exception as e:
55+
transformer_name = transformer.__name__
56+
logging.warning(f"Transformer {transformer_name} failed with {e}")
57+
# If nothing changed after a full pass, we're done
58+
if (result := unparse(self.tree)) == code:
59+
for transformer in self.AFTER_TRANSFORMERS:
60+
try:
61+
self.tree = transformer().visit(self.tree)
62+
except Exception as e:
63+
transformer_name = transformer.__name__
64+
logging.warning(
65+
f"Transformer {transformer_name} failed with {e}"
66+
)
67+
code = unparse(self.tree)
68+
break
69+
code = result
70+
passes += 1
71+
return Result(code, passes, constants)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
# type: ignore
2+
import ast
3+
import string
4+
from ast import unparse
5+
from typing import Any, Dict, List
6+
7+
CONSTANTS = (
8+
ast.Name,
9+
ast.Constant,
10+
ast.Str,
11+
ast.Num,
12+
ast.Bytes,
13+
ast.Ellipsis,
14+
ast.NameConstant,
15+
ast.Attribute,
16+
ast.Subscript,
17+
ast.Tuple,
18+
)
19+
20+
__all__ = (
21+
"StringSubscriptSimple",
22+
"GlobalsToVarAccess",
23+
"InlineConstants",
24+
"DunderImportRemover",
25+
"GetattrConstructRemover",
26+
"BuiltinsAccessRemover",
27+
"Dehexlify",
28+
"UselessEval",
29+
"UselessCompile",
30+
"ExecTransformer",
31+
"UselessLambda",
32+
"LambdaCalls",
33+
"EmptyIf",
34+
"RemoveFromBuiltins",
35+
)
36+
37+
constants: Dict[str, Any] = {}
38+
lambdas: List[str] = []
39+
40+
41+
class StringSubscriptSimple(ast.NodeTransformer):
42+
"""Transforms Hyperion specific string slicing into a string literal"""
43+
44+
def visit_Subscript(self, node: ast.Subscript):
45+
if isinstance(node.value, ast.Str) and isinstance(node.slice, ast.Slice):
46+
code = unparse(node.slice.step)
47+
if all(s not in code for s in string.ascii_letters):
48+
49+
s = node.value.s[:: eval(unparse(node.slice.step))]
50+
return ast.Str(s=s)
51+
return super().generic_visit(node)
52+
53+
54+
class GlobalsToVarAccess(ast.NodeTransformer):
55+
def visit_Subscript(self, node: ast.Subscript) -> Any:
56+
if (
57+
(
58+
isinstance(node.value, ast.Call)
59+
and isinstance(node.value.func, ast.Name)
60+
and node.value.func.id in ("globals", "locals", "vars")
61+
)
62+
and isinstance(node.slice, ast.Constant)
63+
and isinstance(node.slice.value, str)
64+
):
65+
return ast.Name(id=node.slice.value, ctx=ast.Load())
66+
return super().generic_visit(node)
67+
68+
69+
class InlineConstants(ast.NodeTransformer):
70+
class FindConstants(ast.NodeTransformer):
71+
def visit_Assign(self, node: ast.Assign) -> Any:
72+
if isinstance(node.value, CONSTANTS) and isinstance(
73+
node.targets[0], ast.Name
74+
):
75+
constants[node.targets[0].id] = node.value
76+
# delete the assignment
77+
return ast.Module(body=[], type_ignores=[])
78+
return super().generic_visit(node)
79+
80+
def visit(self, node: Any) -> Any:
81+
self.FindConstants().visit(node)
82+
return super().visit(node)
83+
84+
def visit_Name(self, node: ast.Name) -> Any:
85+
"""Replace the name with the constant if it's in the constants dict"""
86+
if node.id in constants:
87+
return constants[node.id]
88+
return super().generic_visit(node)
89+
90+
91+
class DunderImportRemover(ast.NodeTransformer):
92+
"""Just transform all __import__ calls to the name of the module being imported"""
93+
94+
def visit_Call(self, node: ast.Call) -> Any:
95+
if isinstance(node.func, ast.Name) and node.func.id == "__import__":
96+
return ast.Name(id=node.args[0].s, ctx=ast.Load())
97+
return super().generic_visit(node)
98+
99+
100+
class GetattrConstructRemover(ast.NodeTransformer):
101+
"""Hyperion has an interesting way of accessing module attributes."""
102+
103+
def visit_Call(self, node: ast.Call) -> Any:
104+
105+
if isinstance(node.func, ast.Name) and node.func.id == "getattr":
106+
return ast.Attribute(value=node.args[0], attr=node.args[1].slice.args[0].s)
107+
108+
return super().generic_visit(node)
109+
110+
111+
class BuiltinsAccessRemover(ast.NodeTransformer):
112+
"""Instead of accessing builtins, just use the name directly"""
113+
114+
def visit_Attribute(self, node: ast.Attribute) -> Any:
115+
if isinstance(node.value, ast.Name) and node.value.id == "builtins":
116+
return ast.Name(id=node.attr, ctx=ast.Load())
117+
return super().generic_visit(node)
118+
119+
120+
class Dehexlify(ast.NodeTransformer):
121+
"""Transforms a binascii.unhexlify(b'').decode('utf8') into a string"""
122+
123+
def visit_Call(self, node: ast.Call) -> Any:
124+
if (
125+
isinstance(node.func, ast.Attribute)
126+
and node.func.attr == "decode"
127+
and (
128+
isinstance(node.func.value, ast.Call)
129+
and isinstance(node.func.value.func, ast.Attribute)
130+
and node.func.value.func.attr == "unhexlify"
131+
)
132+
):
133+
return ast.Str(
134+
s=bytes.fromhex(node.func.value.args[0].s.decode()).decode("utf8")
135+
)
136+
return super().generic_visit(node)
137+
138+
139+
class UselessEval(ast.NodeTransformer):
140+
"""Eval can just be replaced with the string"""
141+
142+
def visit_Call(self, node: ast.Call) -> Any:
143+
if (
144+
isinstance(node.func, ast.Name)
145+
and node.func.id == "eval"
146+
and isinstance(node.args[0], ast.Str)
147+
):
148+
return ast.parse(node.args[0].s).body[0].value
149+
return super().generic_visit(node)
150+
151+
152+
class UselessCompile(ast.NodeTransformer):
153+
"""An call to compile() in Hyperion is usually useless"""
154+
155+
def visit_Call(self, node: ast.Call) -> Any:
156+
if (
157+
isinstance(node.func, ast.Name)
158+
and node.func.id == "compile"
159+
and isinstance(node.args[0], ast.Str)
160+
):
161+
return node.args[0]
162+
return super().generic_visit(node)
163+
164+
165+
class ExecTransformer(ast.NodeTransformer):
166+
"""Exec can be just transformed into bare code"""
167+
168+
def visit_Call(self, node: ast.Call) -> Any:
169+
if (
170+
isinstance(node.func, ast.Name)
171+
and node.func.id == "exec"
172+
and isinstance(node.args[0], ast.Str)
173+
):
174+
try:
175+
if result := ast.parse(node.args[0].s).body:
176+
return result[0]
177+
except SyntaxError:
178+
pass
179+
return super().generic_visit(node)
180+
181+
182+
class UselessLambda(ast.NodeTransformer):
183+
# x = lambda: y() -> x = y
184+
def visit_Assign(self, node: ast.Assign) -> Any:
185+
if (
186+
isinstance(node.value, ast.Lambda)
187+
and isinstance(node.value.body, ast.Call)
188+
and not node.value.body.args # make sure both call and lambda have no argument
189+
and not node.value.args
190+
):
191+
192+
return ast.Assign(
193+
targets=node.targets,
194+
value=node.value.body.func,
195+
lineno=node.lineno,
196+
col_offset=node.col_offset,
197+
)
198+
199+
return super().generic_visit(node)
200+
201+
202+
class LambdaSingleArgs(ast.NodeTransformer):
203+
"""Find all lambdas that lambda a: b()"""
204+
205+
def visit_Assign(self, node: ast.Assign) -> Any:
206+
if (
207+
isinstance(node.value, ast.Lambda)
208+
and isinstance(node.value.body, ast.Call)
209+
and not node.value.body.args
210+
and len(node.value.args.args) == 1
211+
):
212+
lambdas.append(node.targets[0].id)
213+
constants[node.targets[0].id] = node.value.body.func
214+
return ast.Module(body=[], type_ignores=[])
215+
216+
217+
class LambdaCalls(ast.NodeTransformer):
218+
"""Transforms calls to an assigned lambda into the lambda itself"""
219+
220+
def visit(self, node: Any) -> Any:
221+
LambdaSingleArgs().visit(node)
222+
return super().visit(node)
223+
224+
def visit_Call(self, node: ast.Call) -> Any:
225+
if isinstance(node.func, ast.Name) and node.func.id in lambdas:
226+
227+
return ast.Call(
228+
func=constants[node.func.id],
229+
args=[],
230+
keywords=[],
231+
)
232+
233+
return super().generic_visit(node)
234+
235+
236+
class EmptyIf(ast.NodeTransformer):
237+
"""Remove useless if statements"""
238+
239+
def visit_If(self, node: ast.If) -> Any:
240+
if type(node.test) is ast.Constant and not bool(node.test.value):
241+
return ast.Module(body=[], type_ignores=[])
242+
return super().generic_visit(node)
243+
244+
245+
class RemoveFromBuiltins(ast.NodeTransformer):
246+
"""Remove all from builtins import *"""
247+
248+
def visit_ImportFrom(self, node: ast.ImportFrom) -> Any:
249+
if node.module == "builtins":
250+
return ast.Module(body=[], type_ignores=[])
251+
return super().generic_visit(node)

0 commit comments

Comments
 (0)