237 lines
9.5 KiB
Python
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
|