CVE-2024-36120 (published 2024-05-31T17:15Z)

javascript-deobfuscator removes common JavaScript obfuscation techniques. In affected versions crafted payloads targeting expression simplification can lead to code execution. This issue has been patched in version 1.1.0. Users are advised to update. Users unable to upgrade should disable the expression simplification feature.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/ben-sb/javascript-deobfuscator
commit_details: repo_url: https://github.com/ben-sb/javascript-deobfuscator
author: ben-sb
author_date: 2024-05-31 10:53:30+01:00
msg: fix: replaced expression simplification with approach from other deobfuscator
repository: repo_url: https://github.com/ben-sb/javascript-deobfuscator
repo_name: ben-sb/javascript-deobfuscator
description: General purpose JavaScript deobfuscator
repo_language: TypeScript
owner: ben-sb
file_changes: filename: expressionSimplifier.ts
change_type: ModificationType.MODIFY
diff: @@ -5,6 +5,36 @@ import TraversalHelper from '../../helpers/traversalHelper';
 
 export default class ExpressionSimplifier extends Modification {
     private readonly types = new Set(['BinaryExpression', 'UnaryExpression']);
+    private static readonly RESOLVABLE_UNARY_OPERATORS: Set<string> = new Set([
+        '-',
+        '+',
+        '!',
+        '~',
+        'typeof',
+        'void'
+    ]);
+    private static readonly RESOLVABLE_BINARY_OPERATORS: Set<string> = new Set([
+        '==',
+        '!=',
+        '===',
+        '!==',
+        '<',
+        '<=',
+        '>',
+        '>=',
+        '<<',
+        '>>',
+        '>>>',
+        '+',
+        '-',
+        '*',
+        '/',
+        '%',
+        '**',
+        '|',
+        '^',
+        '&'
+    ]);
 
     /**
      * Creates a new modification.
@@ -47,140 +77,252 @@ export default class ExpressionSimplifier extends Modification {
     private simplifyExpression(expression: Shift.Expression): Shift.Expression {
         switch (expression.type) {
             case 'BinaryExpression':
-                return this.simplifyBinaryExpression(expression);
+                return this.simplifyBinaryExpression(expression) || expression;
 
             case 'UnaryExpression':
-                return this.simplifyUnaryExpression(expression);
+                return this.simplifyUnaryExpression(expression) || expression;
 
             default:
                 return expression;
         }
     }
 
+    /**
+     * Attempts to simplify a unary expression node.
+     * @param expression The unary expression node.
+     */
+    private simplifyUnaryExpression(expression: Shift.UnaryExpression): Shift.Expression | undefined {
+        if (!ExpressionSimplifier.RESOLVABLE_UNARY_OPERATORS.has(expression.operator)) {
+            return expression;
+        } else if (expression.operator == '-' && expression.operand.type == 'LiteralNumericExpression') {
+            return expression; // avoid trying to simplify negative numbers
+        }
+
+        const argument = this.simplifyExpression(expression.operand);
+
+        if (this.isResolvableExpression(argument)) {
+            const argumentValue = this.getResolvableExpressionValue(argument);
+            const value = this.applyUnaryOperation(
+                expression.operator as ResolvableUnaryOperator,
+                argumentValue
+            );
+            return this.convertValueToExpression(value);
+        } else {
+            return expression;
+        }
+    }
+
     /**
      * Attempts to simplify a binary expression node.
      * @param expression The binary expression node.
      */
-    private simplifyBinaryExpression(expression: Shift.BinaryExpression): Shift.Expression {
+    private simplifyBinaryExpression(expression: Shift.BinaryExpression): Shift.Expression | undefined {
+        if (
+            !expression.left.type.endsWith('Expression') ||
+            !ExpressionSimplifier.RESOLVABLE_BINARY_OPERATORS.has(expression.operator)
+        ) {
+            return undefined;
+        }
+
         const left = this.simplifyExpression(expression.left);
         const right = this.simplifyExpression(expression.right);
 
-        const leftValue = this.getExpressionValueAsString(left);
-        const rightValue = this.getExpressionValueAsString(right);
-
-        if (leftValue != null && rightValue != null) {
-            const code = `${leftValue} ${expression.operator} ${rightValue}`;
-            const simplified = this.evalCodeToExpression(code);
-            return simplified != null ? simplified : expression;
-        } else {
+        if (this.isResolvableExpression(left) && this.isResolvableExpression(right)) {
+            const leftValue = this.getResolvableExpressionValue(left);
+            const rightValue = this.getResolvableExpressionValue(right);
+            const value = this.applyBinaryOperation(
+                expression.operator as ResolvableBinaryOperator,
+                leftValue,
+                rightValue
+            );
+            return this.convertValueToExpression(value);
+        } else if (expression.operator == '-' && right.type == 'UnaryExpression' && right.operator == '-' && right.operand.type == 'LiteralNumericExpression') {
+            // convert (- -a) to +a (as long as a is a number)
+            expression.right = right.operand;
+            expression.operator = '+';
             return expression;
+        } else {
+            return undefined;
         }
     }
 
     /**
-     * Attempts to simplify a unary expression node.
-     * @param expression The unary expression node.
+     * Applies a unary operation.
+     * @param operator The operator.
+     * @param argument The argument value.
+     * @returns The resultant value.
      */
-    private simplifyUnaryExpression(expression: Shift.UnaryExpression): Shift.Expression {
-        expression.operand = this.simplifyExpression(expression.operand);
-        const code = this.getExpressionValueAsString(expression);
+    private applyUnaryOperation(operator: ResolvableUnaryOperator, argument: any): any {
+        switch (operator) {
+            case '-':
+                return -argument;
+            case '+':
+                return +argument;
+            case '!':
+                return !argument;
+            case '~':
+                return ~argument;
+            case 'typeof':
+                return typeof argument;
+            case 'void':
+                return void argument;
+        }
+    }
 
-        if (code != null) {
-            const simplified = this.evalCodeToExpression(code);
-            return simplified != null ? simplified : expression;
-        } else {
-            return expression;
+    /**
+     * Applies a binary operation.
+     * @param operator The resolvable binary operator.
+     * @param left The value of the left expression.
+     * @param right The value of the right expression.
+     * @returns The resultant value.
+     */
+    private applyBinaryOperation(operator: ResolvableBinaryOperator, left: any, right: any): any {
+        switch (operator) {
+            case '==':
+                return left == right;
+            case '!=':
+                return left != right;
+            case '===':
+                return left === right;
+            case '!==':
+                return left !== right;
+            case '<':
+                return left < right;
+            case '<=':
+                return left <= right;
+            case '>':
+                return left > right;
+            case '>=':
+                return left >= right;
+            case '<<':
+                return left << right;
+            case '>>':
+                return left >> right;
+            case '>>>':
+                return left >>> right;
+            case '+':
+                return left + right;
+            case '-':
+                return left - right;
+            case '*':
+                return left * right;
+            case '/':
+                return left / right;
+            case '%':
+                return left % right;
+            case '**':
+                return left ** right;
+            case '|':
+                return left | right;
+            case '^':
+                return left ^ right;
+            case '&':
+                return left & right;
         }
     }
 
     /**
-     * Returns the value of a node as a string, null if not possible.
-     * @param expression The expression node.
+     * Gets the real value from a resolvable expression.
+     * @param expression The resolvable expression.
+     * @returns The value.
      */
-    private getExpressionValueAsString(expression: Shift.Expression): string | null {
+    private getResolvableExpressionValue(expression: ResolvableExpression): any {
         switch (expression.type) {
-            case 'LiteralStringExpression':
-                const value = expression.value
-                    .replace(/"/g, '\\"')
-                    .replace(/\n/g, '\\n')
-                    .replace(/\r/g, '\\r');
-                return `"${value}"`;
-
             case 'LiteralNumericExpression':
+            case 'LiteralStringExpression':
             case 'LiteralBooleanExpression':
-                return expression.value.toString();
-
-            case 'ArrayExpression':
-                if (expression.elements.length == 0) {
-                    return '[]';
-                } else if (expression.elements.every(e => !e || e.type.startsWith('Literal'))) {
-                    let content = '';
-                    for (let i = 0; i < expression.elements.length; i++) {
-                        if (expression.elements[i]) {
-                            content += `${this.getExpressionValueAsString(
-                                expression.elements[i] as Shift.Expression
-                            )},`;
-                        } else {
-                            content += ',';
-                        }
-                    }
-                    return `[${content.substring(0, content.length - 1)}]`;
-                } else {
-                    return null;
-                }
-
-            case 'ObjectExpression':
-                if (expression.properties.length == 0) {
-                    expression.properties;
-                    return '[]';
-                } else {
-                    return null;
-                }
-
+                return expression.value;
             case 'UnaryExpression':
-                const operand = this.getExpressionValueAsString(expression.operand);
-                return operand != null ? `${expression.operator} ${operand}` : null;
-
-            default:
+                return -this.getResolvableExpressionValue(
+                    expression.operand as Literal
+                );
+            case 'LiteralNullExpression':
                 return null;
+            case 'IdentifierExpression':
+                return undefined;
+            case 'ArrayExpression':
+                return [];
+            case 'ObjectExpression':
+                return {};
         }
     }
 
     /**
-     * Evaluates a given piece of code and converts the result to an
-     * expression node if possible.
-     * @param code The code to be evaluated.
+     * Attempts to convert a value of unknown type to an expression node.
+     * @param value The value.
+     * @returns The expression or undefined.
      */
-    private evalCodeToExpression(code: string): Shift.Expression | null {
-        let value;
-        try {
-            value = eval(code);
-        } catch (err) {
-            return null;
-        }
-
+    private convertValueToExpression(value: any): Shift.Expression | undefined {
         switch (typeof value) {
             case 'string':
-                return new Shift.LiteralStringExpression({
-                    value: value
-                });
-
+                return new Shift.LiteralStringExpression({ value });
             case 'number':
-                return new Shift.LiteralNumericExpression({
-                    value: value
-                });
-
+                return value >= 0
+                    ? new Shift.LiteralNumericExpression({ value })
+                    : new Shift.UnaryExpression({ operator: '-', operand: new Shift.LiteralNumericExpression({ value: Math.abs(value) })});
             case 'boolean':
-                return new Shift.LiteralBooleanExpression({
-                    value: value
-                });
-
+                return new Shift.LiteralBooleanExpression({ value });
+            case 'undefined':
+                return new Shift.IdentifierExpression({ name: 'undefined' });
             default:
-                return null;
+                return undefined;
         }
     }
 
-    private isSimpleArray(array: Shift.ArrayExpression): boolean {
-        return array.elements.every(e => !e || e.type.startsWith('Literal'));
+    /**
+     * Returns whether a node is a resolvable expression that can be
+     * evaluated safely.
+     * @param node The AST node.
+     * @returns Whether.
+     */
+    private isResolvableExpression(node: Shift.Node): node is ResolvableExpression {
+        return (
+            this.isLiteral(node) ||
+            (node.type == 'UnaryExpression' && node.operator == '-' && node.operand.type == 'LiteralNumericExpression') ||
+            (node.type == 'IdentifierExpression' && node.name == 'undefined') ||
+            (node.type == 'ArrayExpression' && node.elements.length == 0) ||
+            (node.type == 'ObjectExpression' && node.properties.length == 0)
+        );
+    }
+
+    /**
+     * Returns whether a node is a literal.
+     * @param node The AST node.
+     * @returns Whether.
+     */
+    private isLiteral(node: Shift.Node): node is Literal {
+        return node.type == 'LiteralNumericExpression' || node.type == 'LiteralStringExpression' || node.type == 'LiteralBooleanExpression' || node.type == 'LiteralNullExpression';
     }
 }
+
+type Literal = Shift.LiteralNumericExpression | Shift.LiteralStringExpression | Shift.LiteralBooleanExpression | Shift.LiteralNullExpression;
+type ResolvableExpression =
+    | Literal
+    | (Shift.UnaryExpression & { operator: '-'; argument: Literal })
+    | (Shift.IdentifierExpression & { name: 'undefined' })
+    | (Shift.ArrayExpression & { elements: [] })
+    | (Shift.ObjectExpression & { properties: [] });
+
+type ResolvableUnaryOperator = '-' | '+' | '!' | '~' | 'typeof' | 'void';
+
+type ResolvableBinaryOperator =
+    | '=='
+    | '!='
+    | '==='
+    | '!=='
+    | '<'
+    | '<='
+    | '>'
+    | '>='
+    | '<<'
+    | '>>'
+    | '>>>'
+    | '+'
+    | '-'
+    | '*'
+    | '/'
+    | '%'
+    | '**'
+    | '|'
+    | '^'
+    | '&';
\ No newline at end of file

programming_language: TypeScript