Files
Python-Obfuscation/transformers/class_analyzer.py
T
zack3d 69184c7cb8
Doxygen to Wiki / Build Doxygen and publish to Wiki (push) Failing after 1m0s
da
2025-08-15 21:06:31 -07:00

237 lines
9.5 KiB
Python

"""
@file transformers/class_analyzer.py
@brief Class analysis utilities for consistent renaming.
@details Pre-analyzes classes, methods, attributes, and inheritance to build
consistent mappings used by obfuscation transformers.
"""
import ast
from typing import Dict, Set, Tuple, List
class ClassMethodMap:
"""
@brief Stores method and attribute mappings for all classes.
"""
def __init__(self):
# Maps: original_class_name -> {original_method_name -> obfuscated_method_name}
self.class_methods: Dict[str, Dict[str, str]] = {}
# Maps: original_class_name -> {original_attr_name -> obfuscated_attr_name}
self.class_attributes: Dict[str, Dict[str, str]] = {}
# Maps: original_class_name -> obfuscated_class_name
self.class_renames: Dict[str, str] = {}
# Track inheritance relationships: child_class -> [parent_classes]
self.inheritance: Dict[str, List[str]] = {}
class ClassAnalyzer(ast.NodeVisitor):
"""
@brief Analyze classes for consistent method/attribute renaming.
@details Ensures self.method() calls match def method() definitions and records inheritance.
"""
def __init__(self, name_generator):
self.name_generator = name_generator
self.method_map = ClassMethodMap()
self.current_class = None
# To avoid duplicate scanning
self.scanned_classes: Set[str] = set()
# Track method calls within each class
self.method_calls: Dict[str, Set[str]] = {}
def analyze(self, tree: ast.AST) -> ClassMethodMap:
"""
@brief Analyze the entire AST and produce mappings.
@param tree Parsed AST of the input module.
@return ClassMethodMap Populated mappings.
"""
self.visit(tree)
self._resolve_inheritance()
self._ensure_consistent_method_mapping()
return self.method_map
def visit_ClassDef(self, node: ast.ClassDef):
"""
@brief Process a class definition and map methods/attrs.
@param node ast.ClassDef node being visited.
"""
prev_class = self.current_class
self.current_class = node.name
# Skip if already processed this class
if node.name in self.scanned_classes:
self.current_class = prev_class
return
# Initialize method calls tracking for this class
self.method_calls[node.name] = set()
# Record class inheritance
parent_classes = []
for base in node.bases:
if isinstance(base, ast.Name):
parent_classes.append(base.id)
if parent_classes:
self.method_map.inheritance[node.name] = parent_classes
# Initialize mappings for this class
if node.name not in self.method_map.class_methods:
self.method_map.class_methods[node.name] = {}
if node.name not in self.method_map.class_attributes:
self.method_map.class_attributes[node.name] = {}
# Create a consistent obfuscated name for this class
if node.name not in self.method_map.class_renames:
new_name = self.name_generator.generate_name()
self.method_map.class_renames[node.name] = new_name
# Process all method definitions in the class
for item in node.body:
# Methods
if isinstance(item, ast.FunctionDef):
# Skip dunder methods
if not (item.name.startswith('__') and item.name.endswith('__')):
# Generate a consistent obfuscated name for this method
new_name = self.name_generator.generate_name()
self.method_map.class_methods[node.name][item.name] = new_name
# Visit the method body to find self.method() calls
self.visit(item)
# Attributes in assignments
elif isinstance(item, ast.Assign):
self.visit_attribute_assign(item)
else:
# Visit other nodes (like if statements that might contain self.method calls)
self.visit(item)
self.scanned_classes.add(node.name)
# Visit any nested classes
for item in node.body:
if isinstance(item, ast.ClassDef):
self.visit(item)
self.current_class = prev_class
def visit_attribute_assign(self, node):
"""
@brief Process self.attr assignments within a class.
@param node ast.Assign node of the assignment.
"""
if not self.current_class:
return
for target in node.targets:
if isinstance(target, ast.Attribute) and isinstance(target.value, ast.Name):
if target.value.id == 'self':
# This is a self.attribute assignment
attr_name = target.attr
if attr_name not in self.method_map.class_attributes[self.current_class]:
new_name = self.name_generator.generate_name()
self.method_map.class_attributes[self.current_class][attr_name] = new_name
# Visit the value part of the assignment for nested self.method() calls
self.visit(node.value)
def visit_Attribute(self, node):
"""
@brief Track self.method references for consistency checks.
@param node ast.Attribute being visited.
"""
if self.current_class:
is_self_method, method_name = get_method_name(node)
if is_self_method:
# Record this method call for consistency checks later
self.method_calls[self.current_class].add(method_name)
# Continue traversing
self.generic_visit(node)
def _ensure_consistent_method_mapping(self):
"""
@brief Ensure all self.method() calls have mappings, even if undefined in class.
"""
for class_name, method_calls in self.method_calls.items():
if class_name not in self.method_map.class_methods:
continue
class_methods = self.method_map.class_methods[class_name]
for method_name in method_calls:
if method_name not in class_methods:
# Skip dunder methods
if method_name.startswith('__') and method_name.endswith('__'):
continue
# Existing check: mapping is generated only once.
new_name = self.name_generator.generate_name()
class_methods[method_name] = new_name
def _resolve_inheritance(self):
"""
@brief Propagate method mappings along inheritance hierarchies.
@details Ensures overridden methods reuse the same obfuscated name.
"""
# Process inheritance depth-first to handle multi-level inheritance
def process_inheritance(class_name):
if class_name not in self.method_map.inheritance:
return
for parent in self.method_map.inheritance[class_name]:
# Process parent's inheritance first
process_inheritance(parent)
# Skip if parent isn't in our mappings (external class)
if parent not in self.method_map.class_methods:
continue
# Inherit parent's methods if not overridden
for method_name, obf_name in self.method_map.class_methods[parent].items():
if method_name not in self.method_map.class_methods[class_name]:
self.method_map.class_methods[class_name][method_name] = obf_name
# Process inheritance for each class
for class_name in list(self.method_map.class_methods.keys()):
process_inheritance(class_name)
def get_method_name(node: ast.Attribute) -> Tuple[bool, str]:
"""
@brief Determine if an attribute is a self.method() call.
@param node Attribute node to check.
@return Tuple[bool, str] (is_self_method, method_name)
"""
if isinstance(node.value, ast.Name) and node.value.id == 'self':
return True, node.attr
return False, ""
def update_obfuscator_with_class_mappings(obfuscator, class_map: ClassMethodMap):
"""
@brief Update obfuscator with class/method/attribute mappings.
@param obfuscator AdvancedObfuscator instance to update.
@param class_map ClassMethodMap mappings produced by analysis.
"""
# Update class name mappings in global_var_renames
for orig_name, obf_name in class_map.class_renames.items():
obfuscator.global_var_renames[orig_name] = obf_name
# Update class attr mapping with our analyzed data
for class_name, class_obf_name in class_map.class_renames.items():
# Initialize if needed
if class_obf_name not in obfuscator.class_attr_mapping:
obfuscator.class_attr_mapping[class_obf_name] = {}
# Copy method mappings
if class_name in class_map.class_methods:
for method, obf_method in class_map.class_methods[class_name].items():
obfuscator.class_attr_mapping[class_obf_name][method] = obf_method
# Copy attribute mappings
if class_name in class_map.class_attributes:
for attr, obf_attr in class_map.class_attributes[class_name].items():
obfuscator.class_attr_mapping[class_obf_name][attr] = obf_attr