Skip to content
This repository has been archived by the owner on Aug 5, 2024. It is now read-only.

Commit

Permalink
add support for obfuscator
Browse files Browse the repository at this point in the history
  • Loading branch information
Fadi002 committed Nov 15, 2023
1 parent 3240b02 commit 33e6a11
Show file tree
Hide file tree
Showing 12 changed files with 464 additions and 7 deletions.
8 changes: 7 additions & 1 deletion INFO/changelog.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@
{
"version": "1.0.1",
"changes": [
"bug fixed for BlankOBF deobfuscator"
"Bug fixed for BlankOBF deobfuscator"
]
},
{
"version": "1.0.2",
"changes": [
"Add support for Hyperion obfuscator"
]
}
]
2 changes: 1 addition & 1 deletion INFO/version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
V1.0.1
V1.0.2
2 changes: 1 addition & 1 deletion config/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__VERSION__ = 'V1.0'
__VERSION__ = 'V1.0.2'
__CHANGELOG_URL__ = 'https://raw.githubusercontent.com/Fadi002/de4py/main/INFO/changelog.json'
__VERSION_URL__ = 'https://raw.githubusercontent.com/Fadi002/de4py/main/INFO/version'
6 changes: 6 additions & 0 deletions deobfuscators/Hyperion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from .erebus.deobfuscator.deobfuscator import Deobfuscator, Result
from .erebus.deobfuscator.unwrapper import unwrap

def Hyperion(file_path: str) -> str:
code = open(file_path, 'r',encoding='utf-8').read()
return Deobfuscator(unwrap(code)).deobfuscate().code
2 changes: 1 addition & 1 deletion deobfuscators/PlusOBF.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ def PlusOBF(file_path):
file.write("# Cleaned with de4py | https://github.com/Fadi002/de4py\n"+cleaned)
return "Saved as "+filename+'-cleaned.py'
except Exception as e:
return 'Detected PlusOBF but Failed to deobfuscate\n'+e
return 'Detected PlusOBF but Failed to deobfuscate\n'+e
6 changes: 4 additions & 2 deletions deobfuscators/detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
from .devtool import devtool
from .blankOBF import BlankOBF
from .PlusOBF import PlusOBF
from .Hyperion import Hyperion
obfuscators = [
("PlusOBF",r"exec\(\"\"\.join\(\[chr\(len\(i\)\) for i in d\]\)\)",PlusOBF),
('jawbreaker', r'([a-zA-Z_]\w{3})\s*=\s*([^;]+);', jawbreaker),
("wodx", r'(?:__NO_NO){23}', wodx),
("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),
("Hyperion", r'__obfuscator__\s*=\s*[\'\"]\s*Hyperion\s*[\'\"]', Hyperion),
('jawbreaker', r'([a-zA-Z_]\w{3})\s*=\s*([^;]+);', jawbreaker)
]
def detect_obfuscator(file_path):
def detect_obfuscator(file_path) -> str:
file_data = open(file_path,'r',encoding='utf8').read()
for obfuscator in obfuscators:
if re.search(obfuscator[1],file_data):
Expand Down
71 changes: 71 additions & 0 deletions deobfuscators/erebus/deobfuscator/deobfuscator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import logging
from ast import NodeTransformer, parse, unparse
from typing import Any, Dict, Tuple, Type

from .transformers import *
from .transformers import constants


class Result:
def __init__(self, code: str, passes: int, variables: Dict[str, Any]) -> None:
self.code = code
self.passes = passes
self.variables = variables

def add_variables(self) -> None:
code = "\n".join(
[f"{name} = {unparse(value)}" for name, value in self.variables.items()]
)
self.code = f"{code}\n{self.code}"


class Deobfuscator:
TRANSFORMERS: Tuple[Type[NodeTransformer], ...] = (
StringSubscriptSimple,
GlobalsToVarAccess,
InlineConstants,
DunderImportRemover,
GetattrConstructRemover,
BuiltinsAccessRemover,
Dehexlify,
UselessCompile,
UselessEval,
ExecTransformer,
UselessLambda,
RemoveFromBuiltins,
)

AFTER_TRANSFORMERS: Tuple[Type[NodeTransformer], ...] = (
LambdaCalls,
EmptyIf,
)

def __init__(self, code: str) -> None:
self.code = code
self.tree = parse(code)

def deobfuscate(self) -> Result:
passes = 0
code = self.code
while True:
for transformer in self.TRANSFORMERS:
try:
self.tree = transformer().visit(self.tree)
except Exception as e:
transformer_name = transformer.__name__
logging.warning(f"Transformer {transformer_name} failed with {e}")
# If nothing changed after a full pass, we're done
if (result := unparse(self.tree)) == code:
for transformer in self.AFTER_TRANSFORMERS:
try:
self.tree = transformer().visit(self.tree)
except Exception as e:
transformer_name = transformer.__name__
logging.warning(
f"Transformer {transformer_name} failed with {e}"
)
code = unparse(self.tree)
break
code = result
passes += 1
return Result(code, passes, constants)
251 changes: 251 additions & 0 deletions deobfuscators/erebus/deobfuscator/transformers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
# type: ignore
import ast
import string
from ast import unparse
from typing import Any, Dict, List

CONSTANTS = (
ast.Name,
ast.Constant,
ast.Str,
ast.Num,
ast.Bytes,
ast.Ellipsis,
ast.NameConstant,
ast.Attribute,
ast.Subscript,
ast.Tuple,
)

__all__ = (
"StringSubscriptSimple",
"GlobalsToVarAccess",
"InlineConstants",
"DunderImportRemover",
"GetattrConstructRemover",
"BuiltinsAccessRemover",
"Dehexlify",
"UselessEval",
"UselessCompile",
"ExecTransformer",
"UselessLambda",
"LambdaCalls",
"EmptyIf",
"RemoveFromBuiltins",
)

constants: Dict[str, Any] = {}
lambdas: List[str] = []


class StringSubscriptSimple(ast.NodeTransformer):
"""Transforms Hyperion specific string slicing into a string literal"""

def visit_Subscript(self, node: ast.Subscript):
if isinstance(node.value, ast.Str) and isinstance(node.slice, ast.Slice):
code = unparse(node.slice.step)
if all(s not in code for s in string.ascii_letters):

s = node.value.s[:: eval(unparse(node.slice.step))]
return ast.Str(s=s)
return super().generic_visit(node)


class GlobalsToVarAccess(ast.NodeTransformer):
def visit_Subscript(self, node: ast.Subscript) -> Any:
if (
(
isinstance(node.value, ast.Call)
and isinstance(node.value.func, ast.Name)
and node.value.func.id in ("globals", "locals", "vars")
)
and isinstance(node.slice, ast.Constant)
and isinstance(node.slice.value, str)
):
return ast.Name(id=node.slice.value, ctx=ast.Load())
return super().generic_visit(node)


class InlineConstants(ast.NodeTransformer):
class FindConstants(ast.NodeTransformer):
def visit_Assign(self, node: ast.Assign) -> Any:
if isinstance(node.value, CONSTANTS) and isinstance(
node.targets[0], ast.Name
):
constants[node.targets[0].id] = node.value
# delete the assignment
return ast.Module(body=[], type_ignores=[])
return super().generic_visit(node)

def visit(self, node: Any) -> Any:
self.FindConstants().visit(node)
return super().visit(node)

def visit_Name(self, node: ast.Name) -> Any:
"""Replace the name with the constant if it's in the constants dict"""
if node.id in constants:
return constants[node.id]
return super().generic_visit(node)


class DunderImportRemover(ast.NodeTransformer):
"""Just transform all __import__ calls to the name of the module being imported"""

def visit_Call(self, node: ast.Call) -> Any:
if isinstance(node.func, ast.Name) and node.func.id == "__import__":
return ast.Name(id=node.args[0].s, ctx=ast.Load())
return super().generic_visit(node)


class GetattrConstructRemover(ast.NodeTransformer):
"""Hyperion has an interesting way of accessing module attributes."""

def visit_Call(self, node: ast.Call) -> Any:

if isinstance(node.func, ast.Name) and node.func.id == "getattr":
return ast.Attribute(value=node.args[0], attr=node.args[1].slice.args[0].s)

return super().generic_visit(node)


class BuiltinsAccessRemover(ast.NodeTransformer):
"""Instead of accessing builtins, just use the name directly"""

def visit_Attribute(self, node: ast.Attribute) -> Any:
if isinstance(node.value, ast.Name) and node.value.id == "builtins":
return ast.Name(id=node.attr, ctx=ast.Load())
return super().generic_visit(node)


class Dehexlify(ast.NodeTransformer):
"""Transforms a binascii.unhexlify(b'').decode('utf8') into a string"""

def visit_Call(self, node: ast.Call) -> Any:
if (
isinstance(node.func, ast.Attribute)
and node.func.attr == "decode"
and (
isinstance(node.func.value, ast.Call)
and isinstance(node.func.value.func, ast.Attribute)
and node.func.value.func.attr == "unhexlify"
)
):
return ast.Str(
s=bytes.fromhex(node.func.value.args[0].s.decode()).decode("utf8")
)
return super().generic_visit(node)


class UselessEval(ast.NodeTransformer):
"""Eval can just be replaced with the string"""

def visit_Call(self, node: ast.Call) -> Any:
if (
isinstance(node.func, ast.Name)
and node.func.id == "eval"
and isinstance(node.args[0], ast.Str)
):
return ast.parse(node.args[0].s).body[0].value
return super().generic_visit(node)


class UselessCompile(ast.NodeTransformer):
"""An call to compile() in Hyperion is usually useless"""

def visit_Call(self, node: ast.Call) -> Any:
if (
isinstance(node.func, ast.Name)
and node.func.id == "compile"
and isinstance(node.args[0], ast.Str)
):
return node.args[0]
return super().generic_visit(node)


class ExecTransformer(ast.NodeTransformer):
"""Exec can be just transformed into bare code"""

def visit_Call(self, node: ast.Call) -> Any:
if (
isinstance(node.func, ast.Name)
and node.func.id == "exec"
and isinstance(node.args[0], ast.Str)
):
try:
if result := ast.parse(node.args[0].s).body:
return result[0]
except SyntaxError:
pass
return super().generic_visit(node)


class UselessLambda(ast.NodeTransformer):
# x = lambda: y() -> x = y
def visit_Assign(self, node: ast.Assign) -> Any:
if (
isinstance(node.value, ast.Lambda)
and isinstance(node.value.body, ast.Call)
and not node.value.body.args # make sure both call and lambda have no argument
and not node.value.args
):

return ast.Assign(
targets=node.targets,
value=node.value.body.func,
lineno=node.lineno,
col_offset=node.col_offset,
)

return super().generic_visit(node)


class LambdaSingleArgs(ast.NodeTransformer):
"""Find all lambdas that lambda a: b()"""

def visit_Assign(self, node: ast.Assign) -> Any:
if (
isinstance(node.value, ast.Lambda)
and isinstance(node.value.body, ast.Call)
and not node.value.body.args
and len(node.value.args.args) == 1
):
lambdas.append(node.targets[0].id)
constants[node.targets[0].id] = node.value.body.func
return ast.Module(body=[], type_ignores=[])


class LambdaCalls(ast.NodeTransformer):
"""Transforms calls to an assigned lambda into the lambda itself"""

def visit(self, node: Any) -> Any:
LambdaSingleArgs().visit(node)
return super().visit(node)

def visit_Call(self, node: ast.Call) -> Any:
if isinstance(node.func, ast.Name) and node.func.id in lambdas:

return ast.Call(
func=constants[node.func.id],
args=[],
keywords=[],
)

return super().generic_visit(node)


class EmptyIf(ast.NodeTransformer):
"""Remove useless if statements"""

def visit_If(self, node: ast.If) -> Any:
if type(node.test) is ast.Constant and not bool(node.test.value):
return ast.Module(body=[], type_ignores=[])
return super().generic_visit(node)


class RemoveFromBuiltins(ast.NodeTransformer):
"""Remove all from builtins import *"""

def visit_ImportFrom(self, node: ast.ImportFrom) -> Any:
if node.module == "builtins":
return ast.Module(body=[], type_ignores=[])
return super().generic_visit(node)
Loading

0 comments on commit 33e6a11

Please sign in to comment.