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 (
    Node,
    Transformer,
    _remove_from_parent,
)
from refinery.lib.scripts.js.deobfuscation.helpers import remove_declarator
from refinery.lib.scripts.js.model import (
    JsBlockStatement,
    JsCallExpression,
    JsExpressionStatement,
    JsIdentifier,
    JsScript,
    JsStringLiteral,
    JsVariableDeclaration,
    JsVariableDeclarator,
)

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


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

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

    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
        if not isinstance(guard_decl.init.callee, JsIdentifier):
            return
        guard_name = guard_decl.id.name
        factory_name = guard_decl.init.callee.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)
        referenced = False
        for n in body_parent.walk():
            if isinstance(n, JsIdentifier) and n.name == factory_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 == factory_name
                    ):
                        remove_declarator(d)
        self.mark_changed()

    def generic_visit(self, node: Node):
        pass

Classes

class JsRemoveReDoS

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

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

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

    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
        if not isinstance(guard_decl.init.callee, JsIdentifier):
            return
        guard_name = guard_decl.id.name
        factory_name = guard_decl.init.callee.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)
        referenced = False
        for n in body_parent.walk():
            if isinstance(n, JsIdentifier) and n.name == factory_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 == factory_name
                    ):
                        remove_declarator(d)
        self.mark_changed()

    def generic_visit(self, node: Node):
        pass

Ancestors

Methods

def visit_JsScript(self, node)
Expand source code Browse git
def visit_JsScript(self, node: JsScript):
    for literal in list(node.walk()):
        if (
            isinstance(literal, JsStringLiteral)
            and _REDOS_SIGNATURE in literal.value
        ):
            self._remove_pattern(literal)
    return None
def generic_visit(self, node)
Expand source code Browse git
def generic_visit(self, node: Node):
    pass