REstringer

repository·main·Indexed 20 days ago

https://github.com/humansecurity/restringer

A JavaScript deobfuscation tool focused on reconstructing strings and simplifying complex logic. It features a modular architecture with safe syntax transformations and unsafe dynamic analysis using isolated-vm. REstringer provides a CLI, a module API for integration, and specialized processors to handle patterns from tools like javascript-obfuscator, augmented arrays, and Caesar ciphers.

Tokens
19.9K
Snippets
60
Records
72
Agent score
68%

What's inside restringer

  1. Implement a module using the Match/Transform pattern

    main

    All modules in REstringer must follow the match/transform pattern to separate concerns and ensure predictable orchestration. This pattern consists of three parts:

    1. Match function: moduleNameMatch(arb, candidateFilter = () => true) identifies target nodes within the Abstract Representation (arb).
    2. Transform function: moduleNameTransform(arb, node) modifies the matched nodes. Crucially, this function must explicitly return the arb object.
    3. Main function: Orchestrates the process by calling the match function, iterating through matches, and capturing the returned arb from each transformation.

    Key Requirement: The main function must use arb = moduleNameTransform(arb, matches[i]) to capture the returned state.

    // Match function - identifies target nodes
    export function moduleNameMatch(arb, candidateFilter = () => true) {
      const matches = [];
      const candidates = arb.ast[0].typeMap.TargetNodeType
                            .concat(arb.ast[0].typeMap.AnotherTargetNodeType);
      
      for (let i = 0; i < candidates.length; i++) {
        const node = candidates[i];
        if (matchesCriteria(node) && candidateFilter(node)) {
          matches.push(node);
        }
      }
      return matches;
    }
    
    // Transform function - modifies matched nodes
    export function moduleNameTransform(arb, node) {
      // Apply transformations
      performTransformation(node);
      return arb; // Must explicitly return arb
    }
    
    // Main function - orchestrates match and transform
    export default function moduleName(arb, candidateFilter = () => true) {
      let currentArb = moduleNameMatch(arb, candidateFilter);
      
      for (let i = 0; i < currentArb.length; i++) {
        arb = moduleNameTransform(arb, currentArb[i]); // Capture returned arb
      }
      return arb;
    }
  2. How REstringer modules and processing works

    main

    REstringer uses a modular architecture to deobfuscate JavaScript. Modules are split into two categories based on their safety profile:

    • Safe Modules: Perform syntax transformations (like string normalization or dead code removal) without executing any code. They pose no risk of running malicious logic.
    • Unsafe Modules: Perform dynamic analysis by evaluating code. These use eval() within a secure sandbox provided by isolated-vm to resolve complex expressions and function calls safely.

    The Processing Pipeline:

    1. Detection: Identifies the obfuscation type using pattern recognition.
    2. Preprocessing: Applies specific preparations based on the detected obfuscator.
    3. Core Deobfuscation: Runs safe and unsafe modules iteratively.
    4. Postprocessing: Cleans up and optimizes the resulting code.
    5. Validation: Ensures the output is correct.
  3. Processor Architecture and Export Requirements

    main

    Processors do not use a default export function. Instead, they must export preprocessors and postprocessors arrays containing transformation functions. This allows the engine to run multiple stages of preparation or cleanup.

    While modules may internally separate logic into match and transform functions for organization, the processor itself can be a single function or a complex structure. The core requirement is the exported arrays.

    // Main processor logic
    function myProcessorLogic(arb, candidateFilter = () => true) {
      const candidates = arb.ast[0].typeMap.TargetNodeType
                            .concat(arb.ast[0].typeMap.AnotherTargetNodeType);
      
      for (let i = 0; i < candidates.length; i++) {
        const node = candidates[i];
        if (matchesCriteria(node) && candidateFilter(node)) {
          // Apply transformation directly
          performTransformation(node);
        }
      }
      return arb;
    }
    
    // Processors MUST export arrays of functions
    export const preprocessors = [myProcessorLogic];
    export const postprocessors = [];
  4. What are REstringer Processors and how do they work?

    main

    Processors are specialized modules designed to handle obfuscation-specific patterns and anti-debugging mechanisms. They function as specialized handlers that:

    • Remove anti-debugging traps that prevent deobfuscation.
    • Prepare scripts for the main deobfuscation pipeline.
    • Apply targeted transformations for specific obfuscation tools.
    • Clean up results after core deobfuscation is complete.

    Processors are lazily loaded only when the Obfuscation Detector identifies a specific type, when manually selected, or when part of a custom pipeline. They are categorized into preprocessors (run before main deobfuscation) and postprocessors (run after).

  5. Code standards for REstringer modules

    main

    When implementing or refactoring modules, adhere to these structural and performance patterns:

    Structural Patterns:

    • Separation of concerns: Split logic into match and transform functions.
    • Static extraction: Extract static arrays and sets outside of functions to improve performance.
    • Looping: Use traditional for loops with an i variable.
    • AST Transformation Pattern: Use the arb = transform(arb, node) pattern and ensure explicit arb returns.

    Documentation & Style:

    • Use specific types in JSDoc.
    • Add non-trivial inline comments only (avoid obvious comments).
    • Ensure commit messages are concise and focus on the changes made.
  6. Testing requirements for REstringer modules

    main

    When writing tests for REstringer, ensure you cover the following categories to maintain high reliability:

    • TP (True Positive): Cases where the transformation is expected to occur.
    • TN (True Negative): Cases where the transformation should NOT occur.
    • Edge Cases: Boundary conditions and unusual inputs.
    • Different operand types: Test all relevant AST node types as operands.

    Best Practices:

    • Use descriptive test names that explain the intent.
    • Ensure proper assertions where expected results match actual behavior.
    • Always run the full test suite (npm test) before submitting, as changes in one module can affect others.
  7. Install REstringer

    main

    REstringer requires Node.js v20+ (v22+ is recommended). You can install it globally as a CLI tool, locally as a module, or via git for development.

    Global Installation (CLI)

    Use this to run the restringer command from your terminal.

    npm install -g restringer

    Local Installation (Module)

    Use this to import REstringer into your JavaScript projects.

    npm install restringer

    Development Installation

    git clone https://github.com/HumanSecurity/restringer.git
    cd restringer
    npm install
    npm install -g restringer
  8. Submit a Pull Request to REstringer

    main

    To contribute to the REstringer repository, follow this workflow:

    1. Fork the repository.
    2. Create a feature branch: git checkout -b feature-name.
    3. Implement changes following the project's coding standards.
    4. Add comprehensive tests for any new functionality.
    5. Update documentation (READMEs and JSDoc) as needed.
    6. Run the full test suite: npm test.
    7. Submit a pull request with a clear, descriptive summary of your changes.
    git checkout -b feature-name
    # ... make changes ...
    npm test
  9. Customize or replace built-in deobfuscation methods

    main

    You can intercept and replace built-in methods by accessing the unsafeMethods array on the REstringer instance. This is useful for adding limits (like execution counts) or custom logic to existing deobfuscators.

    import fs from 'node:fs';
    import {REstringer} from 'restringer';
    
    const code = fs.readFileSync('obfuscated.js', 'utf-8');
    const restringer = new REstringer(code);
    
    // Find and replace a specific method
    const targetMethod = restringer.unsafeMethods.find(m => 
      m.name === 'resolveLocalCalls'
    );
    
    if (targetMethod) {
      let processedCount = 0;
      const maxProcessing = 5;
      
      // Custom implementation with limits
      const customMethod = function limitedResolveLocalCalls(arb) {
        return targetMethod(arb, () => processedCount++ < maxProcessing);
      };
      
      // Replace the method in the array
      const index = restringer.unsafeMethods.indexOf(targetMethod);
      restringer.unsafeMethods[index] = customMethod;
    }
    
    restringer.deobfuscate();
    const targetMethod = restringer.unsafeMethods.find(m => m.name === 'resolveLocalCalls');
    if (targetMethod) {
      const index = restringer.unsafeMethods.indexOf(targetMethod);
      restringer.unsafeMethods[index] = customMethod;
    }
    restringer.deobfuscate();
  10. Optimize module performance

    main

    REstringer requires high-performance code. Follow these optimization guidelines:

    Loop Optimization

    • Prefer traditional for (let i = 0; i < length; i++) loops over for..of or for..in.
    • Use i as the iteration variable.

    Memory & Allocation

    • Static Extraction: Move static arrays or sets outside of functions to avoid re-allocation on every call.
    • Array Operations: Use .concat() for concatenation and .slice() for copying.
    • Object Cloning: Use the spread operator { ...obj } for AST node cloning.
    • Data Structures:
      • Use Arrays for small collections (≤10 elements).
      • Use Sets for large collections to achieve O(1) lookup performance.

    Performance Anti-pattern Example

    // ❌ Bad - recreated every call
    function someFunction() {
        const types = ['Type1', 'Type2'];
        const relevantNodes = [...(arb.ast[0].typeMap.NodeType || [])];
        // ...
    }
    
    // ✅ Good - static extraction and direct access
    const ALLOWED_TYPES = ['Type1', 'Type2'];
    function someFunction() {
        const relevantNodes = arb.ast[0].typeMap.NodeType;
        // ...
    }
  11. Run REstringer tests

    main

    Use the following npm commands to run different test suites:

    • Full test suite: Runs all tests with sample files. Use this for final validation. npm test
    • Quick test suite: Recommended for active development to save time. npm run test:quick
    • Watch mode: Runs the quick test suite in watch mode for continuous feedback. npm run test:quick:watch
    npm test
    npm run test:quick
    npm run test:quick:watch