Files
Python-Obfuscation/transformers/class_mapper.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

357 lines
14 KiB
Python

"""
@file transformers/class_mapper.py
@brief Class, method, and attribute mapping analyzer and transformer.
@details Builds consistent rename mappings across classes, resolves inheritance,
and applies the mappings back to the AST for coherent obfuscation.
"""
import ast
from typing import Dict, Set, List, Tuple, Optional
import logging
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("ClassMapper")
class ClassMapping:
"""
@brief Stores all mappings related to classes in a centralized way.
@details Tracks class renames, method and attribute mappings, inheritance structure,
and seen method calls to ensure complete coverage.
"""
def __init__(self):
# Original class name -> obfuscated class name
self.class_names: Dict[str, str] = {}
# Original class name -> {original method name -> obfuscated method name}
self.class_methods: Dict[str, Dict[str, str]] = {}
# Original class name -> {original attr name -> obfuscated attr name}
self.class_attributes: Dict[str, Dict[str, str]] = {}
# Child class -> list of parent classes (original names)
self.inheritance: Dict[str, List[str]] = {}
# Track all seen method calls to ensure complete coverage
self.seen_method_calls: Dict[str, Set[str]] = {}
def debug_info(self) -> str:
"""Return debug information about mappings."""
info = []
info.append(f"Class mappings: {len(self.class_names)} classes")
for cls_name, obf_name in self.class_names.items():
info.append(f" {cls_name} -> {obf_name}")
if cls_name in self.class_methods:
methods = self.class_methods[cls_name]
info.append(f" Methods: {len(methods)}")
for method, obf_method in methods.items():
info.append(f" {method} -> {obf_method}")
if cls_name in self.class_attributes:
attrs = self.class_attributes[cls_name]
info.append(f" Attributes: {len(attrs)}")
for attr, obf_attr in attrs.items():
info.append(f" {attr} -> {obf_attr}")
return "\n".join(info)
class ClassMapAnalyzer(ast.NodeVisitor):
"""
@brief Analyze the AST to create a complete class mapping.
@details Performs multi-pass analysis to collect classes, methods, attributes,
inheritance, and method-call references and builds consistent mappings.
"""
def __init__(self, name_generator):
self.name_generator = name_generator
self.mapping = ClassMapping()
self.current_class: Optional[str] = None
self.current_method: Optional[str] = None
self.processed_classes: Set[str] = set()
def analyze(self, tree: ast.AST) -> ClassMapping:
"""
@brief Perform a complete analysis of the AST.
@param tree Parsed AST to analyze.
@return ClassMapping Aggregated mappings for class renaming and members.
"""
# First pass: collect all class definitions, methods, and inheritance
self.visit(tree)
# Second pass: resolve inheritance and method mappings
self._resolve_inheritance()
self._ensure_complete_method_mapping()
logger.info(f"Class analysis complete: {len(self.mapping.class_names)} classes processed")
logger.debug(self.mapping.debug_info())
return self.mapping
def visit_ClassDef(self, node: ast.ClassDef):
"""
@brief Process a class definition.
@param node ast.ClassDef node.
"""
prev_class = self.current_class
self.current_class = node.name
# Skip if already processed
if node.name in self.processed_classes:
self.current_class = prev_class
return
# Add class name mapping
if node.name not in self.mapping.class_names:
self.mapping.class_names[node.name] = self.name_generator.generate_name()
# Initialize dictionaries
if node.name not in self.mapping.class_methods:
self.mapping.class_methods[node.name] = {}
if node.name not in self.mapping.class_attributes:
self.mapping.class_attributes[node.name] = {}
if node.name not in self.mapping.seen_method_calls:
self.mapping.seen_method_calls[node.name] = set()
# Record inheritance
parent_classes = []
for base in node.bases:
if isinstance(base, ast.Name):
parent_classes.append(base.id)
if parent_classes:
self.mapping.inheritance[node.name] = parent_classes
# Process class body
for item in node.body:
if isinstance(item, ast.FunctionDef):
self.visit_method_def(item)
elif isinstance(item, ast.Assign):
self.visit_assign_in_class(item)
elif isinstance(item, ast.Expr):
# Could contain calls to self.methods
self.visit(item)
elif isinstance(item, ast.ClassDef):
# Nested class
self.visit(item)
else:
# Other nodes that might contain self.method calls
self.visit(item)
self.processed_classes.add(node.name)
self.current_class = prev_class
def visit_method_def(self, node: ast.FunctionDef):
"""
@brief Process a method definition in a class.
@param node ast.FunctionDef node.
"""
if not self.current_class:
return
prev_method = self.current_method
self.current_method = node.name
# Skip dunder methods from obfuscation
if not (node.name.startswith('__') and node.name.endswith('__')):
# Map method name if not already mapped
if node.name not in self.mapping.class_methods[self.current_class]:
obf_name = self.name_generator.generate_name()
self.mapping.class_methods[self.current_class][node.name] = obf_name
logger.debug(f"Mapped method {self.current_class}.{node.name} to {obf_name}")
# Visit method body to find self.method calls and self.attr assignments
for item in node.body:
self.visit(item)
self.current_method = prev_method
def visit_assign_in_class(self, node: ast.Assign):
"""
@brief Process assignments in class body or methods.
@param node ast.Assign possibly containing self.attr writes.
"""
if not self.current_class:
return
# Check for self.attribute assignments
for target in node.targets:
if isinstance(target, ast.Attribute) and isinstance(target.value, ast.Name) and target.value.id == 'self':
attr_name = target.attr
# Map attribute name if not already mapped
if attr_name not in self.mapping.class_attributes[self.current_class]:
obf_name = self.name_generator.generate_name()
self.mapping.class_attributes[self.current_class][attr_name] = obf_name
logger.debug(f"Mapped attribute {self.current_class}.{attr_name} to {obf_name}")
# Visit the value to find nested self.method calls
self.visit(node.value)
def visit_Attribute(self, node: ast.Attribute):
"""
@brief Process attribute access like self.method or self.attr.
@param node ast.Attribute node.
"""
if self.current_class and isinstance(node.value, ast.Name) and node.value.id == 'self':
# Record this access for later processing
method_name = node.attr
self.mapping.seen_method_calls[self.current_class].add(method_name)
logger.debug(f"Recorded method call: {self.current_class}.{method_name}")
# Continue traversal
self.generic_visit(node)
def visit_Assign(self, node: ast.Assign):
"""
@brief Process assignments that might contain self.attr references.
@param node ast.Assign node.
"""
# Visit both sides of the assignment
for target in node.targets:
self.visit(target)
self.visit(node.value)
def _resolve_inheritance(self):
"""
@brief Ensure child classes inherit method mappings from parent classes.
@details Copies parent method mappings into children when not overridden.
"""
def process_inheritance(class_name):
if class_name not in self.mapping.inheritance:
return
for parent in self.mapping.inheritance[class_name]:
# Process parent's inheritance first
process_inheritance(parent)
# Skip if parent isn't in our mappings
if parent not in self.mapping.class_methods:
continue
# Copy parent's method mappings to child if not overridden
for method_name, obf_name in self.mapping.class_methods[parent].items():
if method_name not in self.mapping.class_methods[class_name]:
self.mapping.class_methods[class_name][method_name] = obf_name
logger.debug(f"Inherited method {class_name}.{method_name} from {parent}")
# Process all classes
for class_name in list(self.mapping.class_methods.keys()):
process_inheritance(class_name)
def _ensure_complete_method_mapping(self):
"""
@brief Ensure all method calls have corresponding mappings.
@details Handles methods called but not defined in the class.
"""
for class_name, method_calls in self.mapping.seen_method_calls.items():
if class_name not in self.mapping.class_methods:
continue
for method_name in method_calls:
# Skip dunder methods
if method_name.startswith('__') and method_name.endswith('__'):
continue
# Add mapping if method was called but not defined
if method_name not in self.mapping.class_methods[class_name]:
obf_name = self.name_generator.generate_name()
self.mapping.class_methods[class_name][method_name] = obf_name
logger.debug(f"Added mapping for called method {class_name}.{method_name} -> {obf_name}")
class ClassTransformer(ast.NodeTransformer):
"""
@brief Transform class-related nodes using the mapping.
@details Renames class names, methods, and self.attr/self.method references
according to the analyzed mappings.
"""
def __init__(self, mapping: ClassMapping):
self.mapping = mapping
self.current_class: Optional[str] = None
def visit_ClassDef(self, node: ast.ClassDef):
"""
@brief Transform class name and process its body.
@param node ast.ClassDef node.
@return ast.ClassDef Transformed class node.
"""
prev_class = self.current_class
orig_name = node.name
self.current_class = orig_name
# Rename class if it's in our mapping
if node.name in self.mapping.class_names:
node.name = self.mapping.class_names[node.name]
logger.debug(f"Transformed class {orig_name} -> {node.name}")
# Process class body
node.body = [self.visit(item) for item in node.body]
self.current_class = prev_class
return node
def visit_FunctionDef(self, node: ast.FunctionDef):
"""
@brief Transform method name.
@param node ast.FunctionDef node.
@return ast.FunctionDef Transformed method node.
"""
if self.current_class and node.name in self.mapping.class_methods.get(self.current_class, {}):
orig_name = node.name
node.name = self.mapping.class_methods[self.current_class][node.name]
logger.debug(f"Transformed method {self.current_class}.{orig_name} -> {node.name}")
# Visit the method body
node.body = [self.visit(item) for item in node.body]
return node
def visit_Attribute(self, node: ast.Attribute):
"""
@brief Transform self.method and self.attr references.
@param node ast.Attribute node.
@return ast.Attribute Transformed attribute node.
"""
# Process any child nodes first (for nested attributes)
node.value = self.visit(node.value)
# Check if this is a self.attr or self.method reference
if self.current_class and isinstance(node.value, ast.Name) and node.value.id == 'self':
orig_name = node.attr
# Check in method mappings first
if self.current_class in self.mapping.class_methods and node.attr in self.mapping.class_methods[self.current_class]:
node.attr = self.mapping.class_methods[self.current_class][node.attr]
logger.debug(f"Transformed self.method {self.current_class}.{orig_name} -> {node.attr}")
# Then check attribute mappings
elif self.current_class in self.mapping.class_attributes and node.attr in self.mapping.class_attributes[self.current_class]:
node.attr = self.mapping.class_attributes[self.current_class][node.attr]
logger.debug(f"Transformed self.attr {self.current_class}.{orig_name} -> {node.attr}")
return node
# Helper function to apply the class mapping transformation
def apply_class_mapping(tree: ast.AST, name_generator) -> ast.AST:
"""
@brief Analyze and transform classes consistently.
@param tree Input AST.
@param name_generator Name generator used to create obfuscated identifiers.
@return Tuple[ast.AST, ClassMapping] Transformed AST and the mapping produced.
"""
# First pass: analyze all classes
analyzer = ClassMapAnalyzer(name_generator)
mapping = analyzer.analyze(tree)
# Second pass: transform using the mapping
transformer = ClassTransformer(mapping)
transformed = transformer.visit(tree)
return transformed, mapping