Module refinery.lib.scripts.js.deobfuscation.antidbg

Remove the self-defending ReDoS anti-tamper pattern injected by javascript-obfuscator.

The obfuscator inserts a guard function that calls

toString().search('(((.+)+)+)+$')

on itself. The catastrophic-backtracking regex causes the JS engine to hang when the code has been reformatted (e.g. by a pretty-printer), acting as an anti-tamper check. This transformer detects the signature regex string after all other deobfuscation has completed and surgically removes the guard call, its variable declarator, and the associated factory function.

Expand source code Browse git
"""
Remove the self-defending ReDoS anti-tamper pattern injected by javascript-obfuscator.

The obfuscator inserts a guard function that calls

    toString().search('(((.+)+)+)+$')

on itself. The catastrophic-backtracking regex causes the JS engine to hang when the code has been
reformatted (e.g. by a pretty-printer), acting as an anti-tamper check. This transformer detects
the signature regex string after all other deobfuscation has completed and surgically removes the
guard call, its variable declarator, and the associated factory function.
"""
from __future__ import annotations

from refinery.lib.scripts import _remove_from_parent
from refinery.lib.scripts.js.deobfuscation.helpers import ScriptLevelTransformer, remove_declarator
from refinery.lib.scripts.js.model import (
    JsBlockStatement,
    JsCallExpression,
    JsExpressionStatement,
    JsFunctionExpression,
    JsIdentifier,
    JsScript,
    JsStringLiteral,
    JsVariableDeclaration,
    JsVariableDeclarator,
)

_REDOS_SIGNATURE = '(((.+)+)+)+$'


class JsRemoveReDoS(ScriptLevelTransformer):
    """
    Detect and remove the self-defending ReDoS pattern by its signature regex string.
    """

    def _process_script(self, node: JsScript):
        for literal in list(node.walk()):
            if (
                isinstance(literal, JsStringLiteral)
                and _REDOS_SIGNATURE in literal.value
            ):
                self._remove_pattern(literal)

    def _remove_pattern(self, redos_literal: JsStringLiteral) -> None:
        guard_decl = redos_literal.parent
        while guard_decl is not None and not isinstance(guard_decl, JsVariableDeclarator):
            guard_decl = guard_decl.parent
        if guard_decl is None or not isinstance(guard_decl.id, JsIdentifier):
            return
        if not isinstance(guard_decl.init, JsCallExpression):
            return
        callee = guard_decl.init.callee
        if isinstance(callee, JsIdentifier):
            factory_name = callee.name
        elif isinstance(callee, JsFunctionExpression):
            factory_name = None
        else:
            return
        guard_name = guard_decl.id.name
        co_names: set[str] = set()
        if factory_name is None:
            for arg in guard_decl.init.arguments:
                if isinstance(arg, JsIdentifier):
                    co_names.add(arg.name)
        var_decl = guard_decl.parent
        if not isinstance(var_decl, JsVariableDeclaration):
            return
        body_parent = var_decl.parent
        if isinstance(body_parent, JsScript):
            body = body_parent.body
        elif isinstance(body_parent, JsBlockStatement):
            body = body_parent.body
        else:
            return
        for stmt in list(body):
            if (
                isinstance(stmt, JsExpressionStatement)
                and isinstance(stmt.expression, JsCallExpression)
                and isinstance(stmt.expression.callee, JsIdentifier)
                and stmt.expression.callee.name == guard_name
                and not stmt.expression.arguments
            ):
                _remove_from_parent(stmt)
        remove_declarator(guard_decl)
        cleanup_names = {factory_name} if factory_name is not None else co_names
        for name in cleanup_names:
            referenced = False
            for n in body_parent.walk():
                if isinstance(n, JsIdentifier) and n.name == name:
                    if isinstance(n.parent, JsVariableDeclarator) and n.parent.id is n:
                        continue
                    referenced = True
                    break
            if not referenced:
                for stmt in list(body):
                    if not isinstance(stmt, JsVariableDeclaration):
                        continue
                    for d in list(stmt.declarations):
                        if (
                            isinstance(d, JsVariableDeclarator)
                            and isinstance(d.id, JsIdentifier)
                            and d.id.name == name
                        ):
                            remove_declarator(d)
        self.mark_changed()

Classes

class JsRemoveReDoS

Detect and remove the self-defending ReDoS pattern by its signature regex string.

Expand source code Browse git
class JsRemoveReDoS(ScriptLevelTransformer):
    """
    Detect and remove the self-defending ReDoS pattern by its signature regex string.
    """

    def _process_script(self, node: JsScript):
        for literal in list(node.walk()):
            if (
                isinstance(literal, JsStringLiteral)
                and _REDOS_SIGNATURE in literal.value
            ):
                self._remove_pattern(literal)

    def _remove_pattern(self, redos_literal: JsStringLiteral) -> None:
        guard_decl = redos_literal.parent
        while guard_decl is not None and not isinstance(guard_decl, JsVariableDeclarator):
            guard_decl = guard_decl.parent
        if guard_decl is None or not isinstance(guard_decl.id, JsIdentifier):
            return
        if not isinstance(guard_decl.init, JsCallExpression):
            return
        callee = guard_decl.init.callee
        if isinstance(callee, JsIdentifier):
            factory_name = callee.name
        elif isinstance(callee, JsFunctionExpression):
            factory_name = None
        else:
            return
        guard_name = guard_decl.id.name
        co_names: set[str] = set()
        if factory_name is None:
            for arg in guard_decl.init.arguments:
                if isinstance(arg, JsIdentifier):
                    co_names.add(arg.name)
        var_decl = guard_decl.parent
        if not isinstance(var_decl, JsVariableDeclaration):
            return
        body_parent = var_decl.parent
        if isinstance(body_parent, JsScript):
            body = body_parent.body
        elif isinstance(body_parent, JsBlockStatement):
            body = body_parent.body
        else:
            return
        for stmt in list(body):
            if (
                isinstance(stmt, JsExpressionStatement)
                and isinstance(stmt.expression, JsCallExpression)
                and isinstance(stmt.expression.callee, JsIdentifier)
                and stmt.expression.callee.name == guard_name
                and not stmt.expression.arguments
            ):
                _remove_from_parent(stmt)
        remove_declarator(guard_decl)
        cleanup_names = {factory_name} if factory_name is not None else co_names
        for name in cleanup_names:
            referenced = False
            for n in body_parent.walk():
                if isinstance(n, JsIdentifier) and n.name == name:
                    if isinstance(n.parent, JsVariableDeclarator) and n.parent.id is n:
                        continue
                    referenced = True
                    break
            if not referenced:
                for stmt in list(body):
                    if not isinstance(stmt, JsVariableDeclaration):
                        continue
                    for d in list(stmt.declarations):
                        if (
                            isinstance(d, JsVariableDeclarator)
                            and isinstance(d.id, JsIdentifier)
                            and d.id.name == name
                        ):
                            remove_declarator(d)
        self.mark_changed()

Ancestors