diff --git a/README.md b/README.md index 6f94a23..36ca935 100644 --- a/README.md +++ b/README.md @@ -2,45 +2,72 @@ [![Node.js CI](https://github.com/PerimeterX/obfuscation-detector/actions/workflows/node.js.yml/badge.svg?branch=main)](https://github.com/PerimeterX/obfuscation-detector/actions/workflows/node.js.yml) [![Downloads](https://img.shields.io/npm/dm/obfuscation-detector.svg?maxAge=43200)](https://www.npmjs.com/package/obfuscation-detector) -Detect different types of JS obfuscation by their AST structure. +## Overview +Obfuscation Detector is a tool for identifying different types of JavaScript obfuscation by analyzing the code's Abstract Syntax Tree (AST). It is designed for security researchers, reverse engineers, and developers who need to quickly determine if and how a JavaScript file has been obfuscated. + +**Use Cases:** +- Automated analysis of suspicious or third-party JavaScript +- Security auditing and malware research +- Integration into CI/CD pipelines to flag obfuscated code +- Educational purposes for understanding obfuscation techniques + +## How it Works +Obfuscation Detector parses JavaScript code into an AST using [flAST](https://www.npmjs.com/package/flast) and applies a series of modular detectors. Each detector looks for specific patterns or structures that are characteristic of known obfuscation techniques. The tool can return all matching types or just the most likely (best) match. ## Installation -`npm install obfuscation-detector` +```shell +npm install obfuscation-detector +``` ## Usage -### Module +### As a Module ```javascript import fs from 'node:fs'; import detectObfuscation from 'obfuscation-detector'; const code = fs.readFileSync('obfuscated.js', 'utf-8'); -const most_likely_obfuscation_type = detectObfuscation(code); -// const all_matching_obfuscation_types = detectObfuscation(code, false); -console.log(`Obfuscation type is probably ${most_likely_obfuscation_type}`); +const bestMatch = detectObfuscation(code); // returns [bestMatch] or [] +const allMatches = detectObfuscation(code, false); // returns all matches as an array +console.log(`Obfuscation type(s): ${allMatches.join(', ')}`); ``` ### CLI -```bash -obfuscation-detector /path/to/obfuscated.js [stopAfterFirst] +```shell +obfuscation-detector /path/to/obfuscated.js [--bestMatch|-b] +cat /path/to/obfuscated.js | obfuscation-detector [--bestMatch|-b] +obfuscation-detector --help ``` -Getting all matching obfuscation types for a file: -```bash -$ obfuscation-detector /path/to/obfuscated.js -[+] function_to_array_replacements, augmented_proxied_array_function_replacements -``` +#### CLI Options +- `--bestMatch`, `-b`: Return only the first (most likely) detected obfuscation type. +- `--help`, `-h`: Show usage instructions. +- Unknown flags will result in an error and print the usage. -Getting just the first match: -```bash -$ obfuscation-detector /path/to/obfuscated.js stop -[+] function_to_array_replacements -``` +#### Examples +- **All matches:** + ```shell + $ obfuscation-detector /path/to/obfuscated.js + [+] function_to_array_replacements, augmented_proxied_array_function_replacements + ``` +- **Best match only:** + ```shell + $ obfuscation-detector /path/to/obfuscated.js --bestMatch + [+] function_to_array_replacements + ``` +- **From stdin:** + ```shell + $ cat obfuscated.js | obfuscation-detector -b + [+] function_to_array_replacements + ``` - -The `stopAfterFirst` arguments doesn't have to be any specific string, it just needs not to be empty. +## API Reference +### `detectObfuscation(code: string, stopAfterFirst: boolean = true): string[]` +- **code**: JavaScript source code as a string. +- **stopAfterFirst**: If `true`, returns after the first positive detection (default). If `false`, returns all detected types. +- **Returns**: An array of detected obfuscation type names. Returns an empty array if no known type is detected. ## Supported Obfuscation Types -You can find descriptions of the different types in the code itself, and more info [here](src/detectors/README.md). +Descriptions and technical details for each type are available in [src/detectors/README.md](src/detectors/README.md): - [Array Replacements](src/detectors/arrayReplacements.js) - [Augmented Array Replacements](src/detectors/augmentedArrayReplacements.js) - [Array Function Replacements](src/detectors/arrayFunctionReplacements.js) @@ -49,5 +76,14 @@ You can find descriptions of the different types in the code itself, and more in - [Obfuscator.io](src/detectors/obfuscator-io.js) - [Caesar Plus](src/detectors/caesarp.js) +## Troubleshooting +- **No obfuscation detected:** The code may not be obfuscated, or it uses an unknown technique. Consider contributing a new detector! +- **Error: File not found:** Check the file path and try again. +- **Unknown flag:** Run with only `--help` to see what options are available. +- **Performance issues:** For very large files, detection may take longer. Consider running with only the detectors you need (advanced usage). + ## Contribution -To contribute to this project see our [contribution guide](CONTRIBUTING.md) \ No newline at end of file +To contribute to this project, see our [contribution guide](CONTRIBUTING.md). + +--- +For technical details on each obfuscation type and how to add new detectors, see [src/detectors/README.md](src/detectors/README.md). \ No newline at end of file diff --git a/bin/obfuscation-detector.js b/bin/obfuscation-detector.js index 26f15ba..6a256fc 100755 --- a/bin/obfuscation-detector.js +++ b/bin/obfuscation-detector.js @@ -1,19 +1,55 @@ #!/usr/bin/env node import fs from 'node:fs'; -import {fileURLToPath} from 'node:url'; import {detectObfuscation} from './../src/index.js'; -if (process.argv[1] === fileURLToPath(import.meta.url)) { - try { - const args = process.argv.slice(2); - if (args.length) { - let content = fs.readFileSync(args[0], 'utf-8'); - const stopAfterFirst = !!args[1]; - const obfuscationType = detectObfuscation(content, stopAfterFirst); - if (obfuscationType.length) console.log('[+] ' + obfuscationType.join(', ')); - else console.log('[-] No obfuscation detected / unknown obfuscation'); - } else console.log('Usage: obfuscation-detector /path/to/obfuscated.js [stopAfterFirst]'); - } catch (e) { - console.error(`[X] Critical Error: ${e.message}`); +function printUsage() { + console.log('Usage: obfuscation-detector /path/to/obfuscated.js [--bestMatch|-b]'); + console.log(' obfuscation-detector < file.js [--bestMatch|-b]'); + console.log(' obfuscation-detector --help|-h'); +} + +const args = process.argv.slice(2); +const allowedFlags = new Set(['--help', '-h', '--bestMatch', '-b']); + +// Check for unknown flags +const unknownFlags = args.filter(arg => arg.startsWith('-') && !allowedFlags.has(arg)); +if (unknownFlags.length) { + console.error(`[-] Unknown flag(s): ${unknownFlags.join(', ')}`); + printUsage(); + process.exit(1); +} + +if (args.includes('--help') || args.includes('-h')) { + printUsage(); + process.exit(0); +} + +try { + let content = ''; + // Default: show all matches unless --bestMatch or -b is present + let stopAfterFirst = args.includes('--bestMatch') || args.includes('-b'); + + // Remove flags from file argument + const fileArg = args.find(arg => !arg.startsWith('-')); + + if (fileArg) { + if (!fs.existsSync(fileArg)) { + console.error(`[-] File not found: ${fileArg}`); + printUsage(); + process.exit(1); + } + content = fs.readFileSync(fileArg, 'utf-8'); + } else if (!process.stdin.isTTY) { + content = fs.readFileSync(0, 'utf-8'); + } else { + printUsage(); + process.exit(1); } + + const obfuscationType = detectObfuscation(content, stopAfterFirst); + if (obfuscationType.length) console.log('[+] ' + obfuscationType.join(', ')); + else console.log('[-] No obfuscation detected / unknown obfuscation'); +} catch (e) { + console.error(`[X] Critical Error: ${e.message}`); + process.exit(1); } diff --git a/eslint.config.js b/eslint.config.js index b11875e..ff65ea3 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -38,6 +38,6 @@ export default [{ }], semi: ["error", "always"], - "no-empty": ["off"], + "no-empty": ["error", { "allowEmptyCatch": true }], }, }]; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fd0b241..c570489 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,20 +9,20 @@ "version": "2.0.5", "license": "MIT", "dependencies": { - "flast": "^2.2.1" + "flast": "^2.2.3" }, "bin": { "obfuscation-detector": "bin/obfuscation-detector.js" }, "devDependencies": { - "eslint": "^9.16.0", + "eslint": "^9.26.0", "husky": "^9.1.7" } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, "license": "MIT", "dependencies": { @@ -62,13 +62,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", - "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.5", + "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -76,10 +76,20 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", + "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/core": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz", - "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -90,9 +100,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "license": "MIT", "dependencies": { @@ -114,19 +124,22 @@ } }, "node_modules/@eslint/js": { - "version": "9.16.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz", - "integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==", + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", + "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", - "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -134,12 +147,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz", - "integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", + "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", "dev": true, "license": "Apache-2.0", "dependencies": { + "@eslint/core": "^0.14.0", "levn": "^0.4.1" }, "engines": { @@ -199,9 +213,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -222,9 +236,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", "dev": true, "license": "MIT" }, @@ -236,9 +250,9 @@ "license": "MIT" }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -369,9 +383,9 @@ "license": "MIT" }, "node_modules/cross-spawn": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", - "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -384,9 +398,9 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -490,30 +504,31 @@ } }, "node_modules/eslint": { - "version": "9.16.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.16.0.tgz", - "integrity": "sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==", + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", + "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.9.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.16.0", - "@eslint/plugin-kit": "^0.2.3", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.14.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.27.0", + "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", + "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.5", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", + "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", @@ -550,9 +565,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", @@ -701,13 +716,13 @@ } }, "node_modules/flast": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/flast/-/flast-2.2.1.tgz", - "integrity": "sha512-04Pd2dulGdhZ9GJ1CNzsx9gTAu8oAvlwFm6n0HLDVdHhcaTGOb3xzntZSFeHEFsYf5QkVy7koFFqvZKmKLrZqA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/flast/-/flast-2.2.3.tgz", + "integrity": "sha512-w/K9/qijdnOxfjo7RfVYvXC8Fg1zwxp4+Gve7CvpJpP2jLDOIu0dbiCAr7DQVvzFLj/1ihc/Cyo/dXFyaorjBg==", "license": "MIT", "dependencies": { "escodegen": "npm:@javascript-obfuscator/escodegen", - "eslint-scope": "^8.2.0", + "eslint-scope": "^8.3.0", "espree": "^10.3.0" } }, @@ -726,9 +741,9 @@ } }, "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, "license": "ISC" }, @@ -795,9 +810,9 @@ } }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 858a438..355f7c8 100644 --- a/package.json +++ b/package.json @@ -34,10 +34,10 @@ }, "homepage": "https://github.com/PerimeterX/obfuscation-detector#readme", "devDependencies": { - "eslint": "^9.16.0", + "eslint": "^9.26.0", "husky": "^9.1.7" }, "dependencies": { - "flast": "^2.2.1" + "flast": "^2.2.3" } } diff --git a/src/detectors/README.md b/src/detectors/README.md index 9c728e9..08f73f9 100644 --- a/src/detectors/README.md +++ b/src/detectors/README.md @@ -1,158 +1,79 @@ -# Obfuscation Types - -* [Obfuscator.io](#obfuscatorio) - * [Debug Protection Explained](#debug-protection-explained) - -## Obfuscator.io - -This is [a free online JS obfuscation tool](https://obfuscator.io/) which includes features to hinder reversing and debugging the code. -This tool is still being updated, and not all of its features are currently covered by this deobfuscator. - -One of the features is debug protection which is implemented by two regex checks that verify if trap functions -in the code have been beautified, as one would do when investigating the code. -These traps, when triggered, may result in endless loops, overflowing the process memory until the process hangs/crashes. - -The way to solve the problem was to replace the two trap functions with a static string which satisfies the test condition. - -Once these traps are deactivated, the rest of the script can be deobfuscated via the [augmented function array]() -deobfuscation, and other generic deobfuscation methods. - -More research into this tool is required, to learn more of its obfuscation methods and develop counter measures. - -### Debug Protection Explained -```javascript -var _ya = ['\x59\x32\x68\x68\x61\x57\x34\x3d', '\x5a\x47\x56\x69\x64\x51\x3d\x3d', '\x5a\x56\x42\x51\x62\x6c\x51\x3d', '\x5a\x32\x64\x6c\x63\x67\x3d\x3d', '\x59\x32\x39\x75\x63\x33\x52\x79\x64\x57\x4e\x30\x62\x33\x49\x3d', '\x64\x57\x31\x6f\x54\x56\x49\x3d', '\x59\x57\x4e\x30\x61\x57\x39\x75', '\x64\x32\x68\x70\x62\x47\x55\x67\x4b\x48\x52\x79\x64\x57\x55\x70\x49\x48\x74\x39', '\x54\x6d\x46\x6b\x57\x47\x51\x3d', '\x5a\x32\x56\x30\x55\x58\x56\x6c\x64\x57\x55\x3d', '\x55\x57\x68\x6c\x5a\x57\x38\x3d', '\x59\x32\x46\x77\x64\x47\x4e\x6f\x59\x56\x46\x31\x5a\x58\x56\x6c', '\x62\x48\x42\x6d\x53\x47\x55\x3d', '\x63\x33\x52\x79\x61\x57\x35\x6e', '\x59\x32\x39\x74\x63\x47\x6c\x73\x5a\x51\x3d\x3d', '\x61\x57\x35\x77\x64\x58\x51\x3d', '\x59\x32\x46\x73\x62\x41\x3d\x3d', '\x64\x45\x70\x4d\x51\x33\x67\x3d', '\x5a\x58\x46\x45\x57\x6d\x38\x3d', '\x64\x47\x56\x7a\x64\x41\x3d\x3d', '\x5a\x32\x56\x30\x54\x47\x46\x30\x5a\x58\x4e\x30\x52\x57\x78\x6c\x62\x57\x56\x75\x64\x41\x3d\x3d', '\x59\x30\x78\x4c\x52\x30\x73\x3d', '\x58\x43\x74\x63\x4b\x79\x41\x71\x4b\x44\x38\x36\x57\x32\x45\x74\x65\x6b\x45\x74\x57\x6c\x38\x6b\x58\x56\x73\x77\x4c\x54\x6c\x68\x4c\x58\x70\x42\x4c\x56\x70\x66\x4a\x46\x30\x71\x4b\x51\x3d\x3d', '\x62\x47\x56\x75\x5a\x33\x52\x6f', '\x64\x6b\x39\x58\x5a\x47\x77\x3d', '\x5a\x58\x68\x77\x62\x33\x4a\x30\x63\x77\x3d\x3d', '\x54\x32\x78\x46\x57\x56\x67\x3d', '\x59\x32\x39\x31\x62\x6e\x52\x6c\x63\x67\x3d\x3d', '\x62\x45\x4a\x61\x63\x58\x49\x3d', '\x59\x57\x52\x6b', '\x62\x6c\x46\x54\x5a\x55\x30\x3d', '\x59\x57\x78\x53\x51\x55\x6f\x3d', '\x5a\x57\x52\x61\x53\x45\x34\x3d', '\x5a\x6e\x56\x75\x59\x33\x52\x70\x62\x32\x34\x67\x4b\x6c\x77\x6f\x49\x43\x70\x63\x4b\x51\x3d\x3d', '\x59\x58\x42\x77\x62\x48\x6b\x3d', '\x57\x57\x78\x6f\x64\x6d\x51\x3d', '\x53\x6e\x4a\x57\x63\x46\x41\x3d', '\x51\x58\x70\x45\x54\x55\x63\x3d', '\x63\x33\x52\x68\x64\x47\x56\x50\x59\x6d\x70\x6c\x59\x33\x51\x3d', '\x63\x33\x42\x73\x61\x57\x4e\x6c', '\x52\x48\x52\x6d\x61\x6b\x49\x3d', '\x61\x57\x35\x70\x64\x41\x3d\x3d', '\x58\x69\x68\x62\x58\x69\x42\x64\x4b\x79\x67\x67\x4b\x31\x74\x65\x49\x46\x30\x72\x4b\x53\x73\x70\x4b\x31\x74\x65\x49\x46\x31\x39', '\x53\x6b\x35\x46\x62\x46\x6f\x3d', '\x63\x48\x56\x7a\x61\x41\x3d\x3d', '\x63\x6d\x56\x30\x64\x58\x4a\x75\x49\x43\x38\x69\x49\x43\x73\x67\x64\x47\x68\x70\x63\x79\x41\x72\x49\x43\x49\x76', '\x59\x6d\x5a\x78\x63\x56\x45\x3d', '\x5a\x47\x56\x73\x5a\x58\x52\x6c', '\x61\x47\x46\x7a\x55\x58\x56\x6c\x64\x57\x55\x3d', '\x52\x45\x31\x45\x62\x31\x45\x3d']; - (function (a, seedNumber) { - var shiftArray = function (numberOfShifts) { - while (--numberOfShifts) { - a['push'](a['shift']()); - } - }; - var shiftArrayIfScriptNotBeautified = function () { - var e = { - 'data': { - 'key': 'cookie', - 'value': 'timeout' - }, - 'setCookie': function (stringsArray, valueName, numericValue, obj) { - obj = obj || {}; - var keyValuePair = valueName + '=' + numericValue; - var unusedCounter = 0x0; - for (var loopIndex = 0x0, lengthOfStringArray = stringsArray['length']; loopIndex < lengthOfStringArray; loopIndex++) { - var valueAtIndex = stringsArray[loopIndex]; - if (valueAtIndex) { - keyValuePair += ';\x20' + valueAtIndex; - var falsyValue = stringsArray[valueAtIndex]; - stringsArray['push'](falsyValue); - lengthOfStringArray = stringsArray['length']; - if (falsyValue !== !![]) { - keyValuePair += '=' + falsyValue; - } - } - } - obj['cookie'] = keyValuePair; - }, - 'removeCookie': function () {return 'dev';}, - 'getCookie': function (meaninglessFunctionWrapper, valueName) { - meaninglessFunctionWrapper = meaninglessFunctionWrapper || function (unchangedValue) { - return unchangedValue; - }; - var extractedCookieValue = meaninglessFunctionWrapper(new RegExp('(?:^|;\x20)' + valueName['replace'](/([.$?*|{}()[]\/+^])/g, '$1') + '=([^;]*)')); - var runFuncWithAdvancedCounter = function (funcToRun, counter) { - funcToRun(++counter); - }; - runFuncWithAdvancedCounter(shiftArray, seedNumber); - return extractedCookieValue ? decodeURIComponent(extractedCookieValue[0x1]) : undefined; - } - }; - var isScriptBeautified = function () { - var unbeautifiedFuncRegex = new RegExp("\\w+ *\\(\\) *{\\w+ *['|\"].+['|\"];? *}"); - return unbeautifiedFuncRegex['test'](e['removeCookie']['toString']()); - }; - e['updateCookie'] = isScriptBeautified; - var extractedCookieValue = ''; - var scriptIsNotBeautified = e['updateCookie'](); - if (!scriptIsNotBeautified) { - e['setCookie'](['*'], 'counter', 0x1); - } else if (scriptIsNotBeautified) { - extractedCookieValue = e['getCookie'](null, 'counter'); - } else { // This is never reached - e['removeCookie'](); - } - }; - shiftArrayIfScriptNotBeautified(); - }(_ya, 0x10a)); - - - var _yb = function (numericValue, b) { - numericValue = numericValue - 0x0; - var valueFromArr = _ya[numericValue]; - if (_yb['alwaysTrue'] === undefined) { - (function () { - var getWindowObject = function () { - var h; - try { - h = Function('return\x20(function()\x20' + '{}.constructor(\x22return\x20this\x22)(\x20)' + ');')(); - } catch (i) { - h = window; - } - return h; - }; - var f = getWindowObject(); - var base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; - f['atob'] || (f['atob'] = function (h) { - var i = String(h)['replace'](/=+$/, ''); - var j = ''; - for (var k = 0x0, l, m, n = 0x0; m = i['charAt'](n++); ~m && (l = k % 0x4 ? l * 0x40 + m : m, k++ % 0x4) ? j += String['fromCharCode'](0xff & l >> (-0x2 * k & 0x6)) : 0x0) { - m = base64Chars['indexOf'](m); - } - return j; - }); - }()); - _yb['decodeString'] = function (e) { - var f = atob(e); - var g = []; - for (var h = 0x0, j = f['length']; h < j; h++) { - g += '%' + ('00' + f['charCodeAt'](h)['toString'](0x10))['slice'](-0x2); - } - return decodeURIComponent(g); - }; - _yb['cache'] = {}; - _yb['alwaysTrue'] = !![]; - } - var valueFromCache = _yb['cache'][numericValue]; - if (valueFromCache === undefined) { // If value is not yet cached - var e = function (f) { - this['theOriginalFunction'] = f; - this['countersArray'] = [0x1, 0x0, 0x0]; - this['unbeautifiedFunc'] = function () {return 'newState';}; - this['firstHalfOfRegexp'] = "\\w+ *\\(\\) *{\\w+ *"; - this['secondHalfOfRegexp'] = "['|\"].+['|\"];? *}"; - }; - e['prototype']['testIfScriptWasBeautified'] = function () { - var testRegexp = new RegExp(this['firstHalfOfRegexp'] + this['secondHalfOfRegexp']); - var g = testRegexp['test'](this['unbeautifiedFunc']['toString']()) ? --this['countersArray'][0x1] : --this['countersArray'][0x0]; - return this['expectMinusOne'](g); - }; - e['prototype']['expectMinusOne'] = function (f) { - if (!Boolean(~f)) { - return f; - } - return this['lengthenTheCounterArray'](this['theOriginalFunction']); - }; - e['prototype']['lengthenTheCounterArray'] = function (f) { - for (var g = 0x0, h = this['countersArray']['length']; g < h; g++) { - this['countersArray']['push'](Math['round'](Math['random']())); - h = this['countersArray']['length']; - } - return f(this['countersArray'][0x0]); - }; - new e(_yb)['testIfScriptWasBeautified'](); - valueFromArr = _yb['decodeString'](valueFromArr); - _yb['cache'][numericValue] = valueFromArr; - } else { - valueFromArr = valueFromCache; - } - return valueFromArr; - } -``` \ No newline at end of file +# Obfuscation Detectors + +## Overview + +This directory contains all the detection logic for identifying different types of JavaScript obfuscation. Each **detector** is a self-contained module that analyzes the AST (Abstract Syntax Tree) of JavaScript code for patterns characteristic of a specific obfuscation technique. + +Detectors are modular and easy to extend. + +--- + +## List of Detectors + +| Detector Name | What It Detects | Implementation File | +|-----------------------------------------------|------------------------------------------------------|----------------------------------------------------------| +| **Array Replacements** | Large arrays of strings used as lookup tables | [arrayReplacements.js](arrayReplacements.js) | +| **Augmented Array Replacements** | Array replacements with IIFE wrappers | [augmentedArrayReplacements.js](augmentedArrayReplacements.js) | +| **Array Function Replacements** | Functions that return values from obfuscated arrays | [arrayFunctionReplacements.js](arrayFunctionReplacements.js) | +| **Augmented Array Function Replacements** | Array function replacements with IIFE wrappers | [augmentedArrayFunctionReplacements.js](augmentedArrayFunctionReplacements.js) | +| **Augmented Proxied Array Function Replacements** | Obfuscation using proxies and function arrays | [augmentedProxiedArrayFunctionReplacements.js](augmentedProxiedArrayFunctionReplacements.js) | +| **Function To Array Replacements** | Variables assigned to function calls, used as objects | [functionToArrayReplacements.js](functionToArrayReplacements.js) | +| **Obfuscator.io** | Patterns from the obfuscator.io tool | [obfuscator-io.js](obfuscator-io.js) | +| **Caesar Plus** | Caesar cipher-like obfuscation with 3-letter IDs | [caesarp.js](caesarp.js) | + +--- + +## Detector Details + +### Array Replacements +- **File:** [arrayReplacements.js](arrayReplacements.js) +- **Description:** Detects large arrays of strings used as lookup tables for obfuscated code. + +### Augmented Array Replacements +- **File:** [augmentedArrayReplacements.js](augmentedArrayReplacements.js) +- **Description:** Like array replacements, but the array is passed to an IIFE (Immediately Invoked Function Expression). + +### Array Function Replacements +- **File:** [arrayFunctionReplacements.js](arrayFunctionReplacements.js) +- **Description:** Detects functions that return values from an obfuscated array, often used to hide string literals. + +### Augmented Array Function Replacements +- **File:** [augmentedArrayFunctionReplacements.js](augmentedArrayFunctionReplacements.js) +- **Description:** Like array function replacements, but with IIFE wrappers for added obfuscation. + +### Augmented Proxied Array Function Replacements +- **File:** [augmentedProxiedArrayFunctionReplacements.js](augmentedProxiedArrayFunctionReplacements.js) +- **Description:** Uses proxies and function arrays to further complicate deobfuscation. + +### Function To Array Replacements +- **File:** [functionToArrayReplacements.js](functionToArrayReplacements.js) +- **Description:** Variables assigned to function calls, then used as objects of member expressions. + +### Obfuscator.io +- **File:** [obfuscator-io.js](obfuscator-io.js) +- **Description:** Detects patterns generated by the [obfuscator.io](https://obfuscator.io/) tool, including debug protection and trap functions. +- **Example:** See the detailed explanation and code sample below. + +### Caesar Plus +- **File:** [caesarp.js](caesarp.js) +- **Description:** Detects Caesar cipher-like obfuscation, typically with 3-letter function names and specific identifier usage. + +--- + +## How to Add a New Detector + +1. **Create a new file** in this directory, e.g., `myNewDetector.js`. +2. **Export a function** named `detectMyNewDetector` that takes the AST (flatTree) as input and returns the obfuscation name (or `''` if not detected). +3. **Add your detector** to `index.js` using `export * from './myNewDetector.js';`. +4. **Document your detector** in this README (add a row to the table above and a short description). +5. **Add tests** in the `tests/` directory to ensure your detector works as expected. + +--- + +## References & Further Reading +- [obfuscator.io](https://obfuscator.io/) +- [AST Explorer](https://astexplorer.net/) + +--- + +For questions or contributions, see the main [README](../README.md) and [CONTRIBUTING.md](../CONTRIBUTING.md). \ No newline at end of file diff --git a/src/detectors/arrayFunctionReplacements.js b/src/detectors/arrayFunctionReplacements.js index 5c2efb6..8fd4d67 100644 --- a/src/detectors/arrayFunctionReplacements.js +++ b/src/detectors/arrayFunctionReplacements.js @@ -3,26 +3,26 @@ import {findArrayDeclarationCandidates, functionHasMinimumRequiredReferences} fr const obfuscationName = 'array_function_replacements'; /** - * Array-Function Replacements obfuscation type has the following characteristics: - * - An array (A) is with many strings is defined. + * Detects the Array-Function Replacements obfuscation type. + * + * Characteristics: + * - An array (A) with many strings is defined. * - A function (B) that returns a single value from the array A, based on provided arguments is present. * - There are many call expressions to function B, with only literals as arguments. - * - There are no more than 2 reference to array A. + * - There are no more than 2 references to array A. * - * @param {ASTNode[]} flatTree - * @return {string} The obfuscation name if detected; empty string otherwise. + * @param {ASTNode[]} flatTree - The flattened AST of the code. + * @returns {string} The obfuscation name if detected; otherwise, an empty string. */ function detectArrayFunctionReplacements(flatTree) { const candidates = findArrayDeclarationCandidates(flatTree); - for (const c of candidates) { - // A matching array would not have more than two reference to it - if (c.id.references.length > 2) continue; - for (const ref of c.id.references) { - if (functionHasMinimumRequiredReferences(ref, flatTree)) return obfuscationName; - } - } - return ''; + const isFound = candidates.some(c => { + // A matching array would not have more than two reference to it + if (c.id.references.length > 2) return false; + return c.id.references.some(ref => functionHasMinimumRequiredReferences(ref, flatTree)); + }); + return isFound ? obfuscationName : ''; } -export {detectArrayFunctionReplacements}; +export {detectArrayFunctionReplacements}; \ No newline at end of file diff --git a/src/detectors/arrayReplacements.js b/src/detectors/arrayReplacements.js index 3e67f76..50f5295 100644 --- a/src/detectors/arrayReplacements.js +++ b/src/detectors/arrayReplacements.js @@ -3,21 +3,23 @@ import {arrayHasMinimumRequiredReferences, findArrayDeclarationCandidates} from const obfuscationName = 'array_replacements'; /** - * Array Replacements obfuscation type has the following characteristics: - * - An array (A) is with many strings is defined. + * Detects the Array Replacements obfuscation type. + * + * Characteristics: + * - An array (A) with many strings is defined. * - There are many member expression references where the object is array A. * - * @param {ASTNode[]} flatTree - * @return {string} The obfuscation name if detected; empty string otherwise. + * @param {ASTNode[]} flatTree - The flattened AST of the code. + * @returns {string} The obfuscation name if detected; otherwise, an empty string. */ function detectArrayReplacements(flatTree) { const candidates = findArrayDeclarationCandidates(flatTree); - for (const c of candidates) { + const isFound = candidates.some(c => { const refs = c.id.references.map(n => n.parentNode); - if (arrayHasMinimumRequiredReferences(refs, c.id.name, flatTree)) return obfuscationName; - } - return ''; + return arrayHasMinimumRequiredReferences(refs, c.id.name, flatTree); + }); + return isFound ? obfuscationName : ''; } -export {detectArrayReplacements}; +export {detectArrayReplacements}; \ No newline at end of file diff --git a/src/detectors/augmentedArrayFunctionReplacements.js b/src/detectors/augmentedArrayFunctionReplacements.js index 4cc928d..9a10145 100644 --- a/src/detectors/augmentedArrayFunctionReplacements.js +++ b/src/detectors/augmentedArrayFunctionReplacements.js @@ -3,24 +3,29 @@ import {arrayIsProvidedAsArgumentToIIFE, findArrayDeclarationCandidates, functio const obfuscationName = 'augmented_array_function_replacements'; /** - * Augmented Array-Function Replacements obfuscation type has the following characteristics: + * Detects the Augmented Array-Function Replacements obfuscation type. + * + * Characteristics: * - The same characteristics as an Array-Function Replacements obfuscation type. - * - An IIFE with a reference to Array A as one if its arguments. - * @param {ASTNode[]} flatTree - * @return {string} The obfuscation name if detected; empty string otherwise. + * - An IIFE with a reference to Array A as one of its arguments. + * + * @param {ASTNode[]} flatTree - The flattened AST of the code. + * @returns {string} The obfuscation name if detected; otherwise, an empty string. */ function detectAugmentedArrayFunctionReplacements(flatTree) { const candidates = findArrayDeclarationCandidates(flatTree); - for (const c of candidates) { - if (c.id.references.length > 2) continue; + const isFound = candidates.some(c => { + if (c.id.references.length > 2) return false; const refs = c.id.references.map(n => n.parentNode); - if (!arrayIsProvidedAsArgumentToIIFE(refs, c.id.name)) continue; - for (const ref of c.id.references) { - if (functionHasMinimumRequiredReferences(ref, flatTree)) return obfuscationName; - } - } - return ''; + const iife = arrayIsProvidedAsArgumentToIIFE(refs, c.id.name); + if (!iife) return false; + const relevantFunc = c.id.references.find(ref => ref.parentKey === 'arguments' && + ref.parentNode?.type === 'CallExpression' && + ref.parentNode.callee.type === 'FunctionExpression')?.parentNode; + return functionHasMinimumRequiredReferences(relevantFunc, flatTree); + }); + return isFound ? obfuscationName : ''; } export {detectAugmentedArrayFunctionReplacements}; \ No newline at end of file diff --git a/src/detectors/augmentedArrayReplacements.js b/src/detectors/augmentedArrayReplacements.js index a47c74b..0373fed 100644 --- a/src/detectors/augmentedArrayReplacements.js +++ b/src/detectors/augmentedArrayReplacements.js @@ -3,22 +3,25 @@ import {arrayHasMinimumRequiredReferences, arrayIsProvidedAsArgumentToIIFE, find const obfuscationName = 'augmented_array_replacements'; /** - * Augmented Array Replacements obfuscation type has the following characteristics: + * Detects the Augmented Array Replacements obfuscation type. + * + * Characteristics: * - The same characteristics as an Array Replacements obfuscation type. - * - An IIFE with a reference to Array A as one if its arguments. - * @param {ASTNode[]} flatTree - * @return {string} The obfuscation name if detected; empty string otherwise. + * - An IIFE with a reference to Array A as one of its arguments. + * + * @param {ASTNode[]} flatTree - The flattened AST of the code. + * @returns {string} The obfuscation name if detected; otherwise, an empty string. */ function detectAugmentedArrayReplacements(flatTree) { const candidates = findArrayDeclarationCandidates(flatTree); - for (const c of candidates) { + const isFound = candidates.find(c => { const refs = c.id.references.map(n => n.parentNode); - // Verify the IIFE exists and has the candidate array as one of its arguments. - if (arrayIsProvidedAsArgumentToIIFE(refs, c.id.name) && - arrayHasMinimumRequiredReferences(refs, c.id.name, flatTree)) return obfuscationName; - } - return ''; + // Verify the IIFE exists and has the candidate array as one of its arguments. + return arrayIsProvidedAsArgumentToIIFE(refs, c.id.name) && + arrayHasMinimumRequiredReferences(refs, c.id.name, flatTree); + }); + return isFound ? obfuscationName : ''; } export {detectAugmentedArrayReplacements}; \ No newline at end of file diff --git a/src/detectors/augmentedProxiedArrayFunctionReplacements.js b/src/detectors/augmentedProxiedArrayFunctionReplacements.js index b4a2a3b..ffa5491 100644 --- a/src/detectors/augmentedProxiedArrayFunctionReplacements.js +++ b/src/detectors/augmentedProxiedArrayFunctionReplacements.js @@ -1,21 +1,25 @@ const obfuscationName = 'augmented_proxied_array_function_replacements'; /** - * @param {ASTNode} node - * @param {string} refName - * @return {boolean} + * Checks if a node is a call expression with a named reference argument. + * @param {ASTNode} node - The AST node to check. + * @param {string} refName - The reference name to look for in arguments. + * @returns {boolean} True if the node is a call expression with the named argument. */ function isCallExpressionWithNamedReferenceArgument(node, refName) { return node?.type === 'CallExpression' && (node.arguments|| []).some(a => a?.name === refName); } /** - * Augmented Proxied Array-Function Replacements obfuscation type has the following characteristics: + * Detects the Augmented Proxied Array-Function Replacements obfuscation type. + * + * Characteristics: * - Has at least 3 root nodes - the last one containing the actual obfuscated code and the rest are obfuscation code. * - Has a function that assigns an array full of strings to itself, and then returns itself. * - Has an anonymous IIFE that is called with the array function as one of its arguments. - * @param {ASTNode[]} flatTree - * @return {string} The obfuscation name if detected; empty string otherwise. + * + * @param {ASTNode[]} flatTree - The flattened AST of the code. + * @returns {string} The obfuscation name if detected; otherwise, an empty string. */ function detectAugmentedProxiedArrayFunctionReplacements(flatTree) { const roots = flatTree[0].childNodes; @@ -38,4 +42,4 @@ function detectAugmentedProxiedArrayFunctionReplacements(flatTree) { return ''; } -export {detectAugmentedProxiedArrayFunctionReplacements}; +export {detectAugmentedProxiedArrayFunctionReplacements}; \ No newline at end of file diff --git a/src/detectors/caesarp.js b/src/detectors/caesarp.js index f9f4f18..f122edf 100644 --- a/src/detectors/caesarp.js +++ b/src/detectors/caesarp.js @@ -1,9 +1,10 @@ const obfuscationName = 'caesar_plus'; /** - * @param {ASTNode} targetNode - * @param {ASTNode} targetScopeBlock - * @return {boolean} true if the target node is found in the targetScope; false otherwise. + * Checks if a target AST node is within a given scope block. + * @param {ASTNode} targetNode - The node to check. + * @param {ASTNode} targetScopeBlock - The scope block to check against. + * @returns {boolean} True if the node is in the scope; otherwise, false. */ function isNodeInScope(targetNode, targetScopeBlock) { if (!targetScopeBlock) return true; @@ -16,16 +17,15 @@ function isNodeInScope(targetNode, targetScopeBlock) { } /** - * Caesar Plus obfuscation type has the following characteristics: + * Detects the Caesar Plus obfuscation type. + * + * Characteristics: * - A function expression A with an id of 3 characters exists. - * - Function A is wrapped in a call expressions without arguments. - * - Function A contains the following identifiers: - * - window - * - document - * - String.fromCharCode + * - Function A is wrapped in a call expression without arguments. + * - Function A contains the following identifiers: window, document, String.fromCharCode. * - * @param {ASTNode[]} flatTree - * @return {string} The obfuscation name if detected; empty string otherwise. + * @param {ASTNode[]} flatTree - The flattened AST of the code. + * @returns {string} The obfuscation name if detected; otherwise, an empty string. */ function detectCaesarPlus(flatTree) { // Verify the main function's name is 3 letters long and has maximum 1 reference; @@ -51,4 +51,4 @@ function detectCaesarPlus(flatTree) { return ''; } -export {detectCaesarPlus}; +export {detectCaesarPlus}; \ No newline at end of file diff --git a/src/detectors/functionToArrayReplacements.js b/src/detectors/functionToArrayReplacements.js index 018c781..73149ff 100644 --- a/src/detectors/functionToArrayReplacements.js +++ b/src/detectors/functionToArrayReplacements.js @@ -1,12 +1,14 @@ const obfuscationName = 'function_to_array_replacements'; /** - * Function To Array obfuscation type has the following characteristics: + * Detects the Function To Array Replacements obfuscation type. + * + * Characteristics: * - A variable A assigned to a call expression with a function for a callee. * - All references to variable A are objects of member expressions. * - * @param {ASTNode[]} flatTree - * @return {string} The obfuscation name if detected; empty string otherwise. + * @param {ASTNode[]} flatTree - The flattened AST of the code. + * @returns {string} The obfuscation name if detected; otherwise, an empty string. */ function detectFunctionToArrayReplacements(flatTree) { return (flatTree[0].typeMap.VariableDeclarator || []).some(n => @@ -18,4 +20,4 @@ function detectFunctionToArrayReplacements(flatTree) { r.parentKey === 'object'))) ? obfuscationName : ''; } -export {detectFunctionToArrayReplacements}; +export {detectFunctionToArrayReplacements}; \ No newline at end of file diff --git a/src/detectors/index.js b/src/detectors/index.js index 8d84db3..afc4c7c 100644 --- a/src/detectors/index.js +++ b/src/detectors/index.js @@ -5,4 +5,4 @@ export * from './augmentedArrayReplacements.js'; export * from './augmentedProxiedArrayFunctionReplacements.js'; export * from './caesarp.js'; export * from './functionToArrayReplacements.js'; -export * from './obfuscator-io.js'; +export * from './obfuscator-io.js'; \ No newline at end of file diff --git a/src/detectors/obfuscator-io.js b/src/detectors/obfuscator-io.js index fd5d3c5..1da2396 100644 --- a/src/detectors/obfuscator-io.js +++ b/src/detectors/obfuscator-io.js @@ -1,5 +1,10 @@ const obfuscationName = 'obfuscator.io'; +/** + * Checks if an object expression with a 'setCookie' key and a function containing a for statement exists. + * @param {ASTNode[]} flatTree - The flattened AST of the code. + * @returns {boolean} True if the pattern is found. + */ function setCookieIndicator(flatTree) { const candidate = (flatTree[0].typeMap.ObjectExpression || []).find(n => n.type === 'ObjectExpression' && @@ -18,6 +23,11 @@ function setCookieIndicator(flatTree) { return false; } +/** + * Checks for a specific Boolean tilde pattern in the AST. + * @param {ASTNode[]} flatTree - The flattened AST of the code. + * @returns {boolean} True if the pattern is found. + */ function notBooleanTilde(flatTree) { const candidates = (flatTree[0].typeMap.BlockStatement || []).filter(n => n.type === 'BlockStatement' && @@ -39,18 +49,20 @@ function notBooleanTilde(flatTree) { } /** - * Obfuscator.io obfuscation type has the following characteristics: + * Detects the Obfuscator.io obfuscation type. + * + * Characteristics: * - The same characteristics as an Augmented Array Function Replacements obfuscation type. * - An object expression A with a key of 'setCookie' exists. * - The value of object expression A is a function expression containing a for statement. * - * @param {ASTNode[]} flatTree - * @param {string[]} pdo A list of names of previously detected obfuscations - * @return {string} The obfuscation name if detected; empty string otherwise. + * @param {ASTNode[]} flatTree - The flattened AST of the code. + * @param {string[]} [pdo=[]] - A list of names of previously detected obfuscations. + * @returns {string} The obfuscation name if detected; otherwise, an empty string. */ function detectObfuscatorIo(flatTree, pdo = []) { return (pdo.includes('augmented_array_function_replacements') && setCookieIndicator(flatTree)) || notBooleanTilde(flatTree) ? obfuscationName : ''; } -export {detectObfuscatorIo}; +export {detectObfuscatorIo}; \ No newline at end of file diff --git a/src/detectors/sharedDetectionMethods.js b/src/detectors/sharedDetectionMethods.js index 2d63543..d373e95 100644 --- a/src/detectors/sharedDetectionMethods.js +++ b/src/detectors/sharedDetectionMethods.js @@ -1,65 +1,70 @@ /** - * Shared code used by more than one detector. + * Shared detection methods used by multiple obfuscation detectors. + * @module sharedDetectionMethods */ const minMeaningfulPercentageOfReferences = 2; // 2% -const minMeaningfulArrayContentLegthPercentage = 2; // 2% +const minMeaningfulArrayContentLengthPercentage = 2; // 2% /** - * @param {ASTNode[]} targetArray - * @param {ASTNode[]} flatTree - * @return {boolean} Whether the number of array elements presents a meaningful percentage of all nodes. + * Checks if the number of array elements presents a meaningful percentage of all AST nodes. + * @param {ASTNode[]} targetArray - The array elements to check. + * @param {ASTNode[]} flatTree - The flattened AST. + * @returns {boolean} True if the array is considered meaningful in length. */ function arrayHasMeaningfulContentLength(targetArray, flatTree) { - return targetArray.length / flatTree.length * 100 >= minMeaningfulArrayContentLegthPercentage; + return Math.floor(targetArray.length / (flatTree.length || 1) * 100) >= minMeaningfulArrayContentLengthPercentage; } /** - * @param {ASTNode[]} flatTree - * @return {ASTNode[]} Candidates matching the target profile of an array with more than a few items, all literals. + * Finds variable declarators that are arrays with more than a few literal items. + * @param {ASTNode[]} flatTree - The flattened AST. + * @returns {ASTNode[]} Array declaration candidates. */ function findArrayDeclarationCandidates(flatTree) { return (flatTree[0].typeMap.VariableDeclarator || []).filter(n => - n.type === 'VariableDeclarator' && n?.init?.type === 'ArrayExpression' && arrayHasMeaningfulContentLength(n.init.elements, flatTree) && !n.init.elements.some(el => el.type !== 'Literal')); } /** - * @param {ASTNode[]} references - * @param {string} targetArrayName - * @param {ASTNode[]} flatTree - * @return {boolean} true if the target array has at least the minimum required references; false otherwise. + * Checks if the target array has at least the minimum required references in the AST. + * @param {ASTNode[]} references - References to the array. + * @param {string} targetArrayName - The name of the array variable. + * @param {ASTNode[]} flatTree - The flattened AST. + * @returns {boolean} True if the array has enough references. */ function arrayHasMinimumRequiredReferences(references, targetArrayName, flatTree) { return references.filter(n => n.type === 'MemberExpression' && - n.object.name === targetArrayName).length / flatTree.length * 100 >= minMeaningfulPercentageOfReferences; + n.object.name === targetArrayName).length / (flatTree.length || 1) * 100 >= minMeaningfulPercentageOfReferences; } /** - * @param {ASTNode[]} references - * @param {string} targetArrayName - * @return {boolean} true if an IIFE with the target array as one of its arguments exists; false otherwise. + * Checks if an IIFE exists with the target array as one of its arguments. + * @param {ASTNode[]} references - References to the array. + * @param {string} targetArrayName - The name of the array variable. + * @returns {ASTNode|null} The IIFE node if found, otherwise null. */ function arrayIsProvidedAsArgumentToIIFE(references, targetArrayName) { - return references.some(n => + return references.find(n => n.type === 'CallExpression' && n.callee.type === 'FunctionExpression' && - n.arguments.some(arg => arg.name === targetArrayName)); + n.arguments.some(arg => arg.name === targetArrayName)) || null; } /** - * @param {ASTNode} reference - * @param {ASTNode[]} flatTree - * @return {boolean} true if the minimum required references to the target function were found; false otherwise. + * Checks if the minimum required references to the target function were found. + * @param {ASTNode} reference - A reference node to the function. + * @param {ASTNode[]} flatTree - The flattened AST. + * @returns {boolean} True if the function has enough relevant references. */ function functionHasMinimumRequiredReferences(reference, flatTree) { const funcRef = reference.scope.block; const funcRefs = funcRef?.id?.references || funcRef?.parentNode?.id?.references; if (funcRefs?.length) { - // References can be call expressions or right side of assignement expressions if proxied. + // References can be call expressions or right side of assignment expressions if proxied. let relevantRefs = funcRefs.filter(n => (n.parentNode.type === 'CallExpression' && n.parentNode.arguments.length && @@ -70,8 +75,9 @@ function functionHasMinimumRequiredReferences(reference, flatTree) { if (relevantRefs.length && relevantRefs[0].parentNode.type === 'VariableDeclarator') { relevantRefs = relevantRefs.map(r => r.parentNode.id.references).flat(); } - return relevantRefs.length / flatTree.length * 100 >= minMeaningfulPercentageOfReferences; + return relevantRefs.length / (flatTree.length || 1) * 100 >= minMeaningfulPercentageOfReferences; } + return false; } export { @@ -79,4 +85,4 @@ export { arrayIsProvidedAsArgumentToIIFE, findArrayDeclarationCandidates, functionHasMinimumRequiredReferences, -}; +}; \ No newline at end of file diff --git a/src/index.js b/src/index.js index a166d13..3cc143f 100644 --- a/src/index.js +++ b/src/index.js @@ -1,17 +1,24 @@ +/** + * Main entry point for obfuscation detection. + * Exports the detectObfuscation function. + * @module obfuscation-detector + */ + import {generateFlatAST, logger} from 'flast'; import * as detectors from './detectors/index.js'; /** - * @param {string} code - * @param {boolean} stopAfterFirst If true, return results after the first positive detection. - * @return {string[]} All detected obfuscation types (or just the first one if stopAfterFirst is set to true); - * An empty array if no known obfuscation type matched. + * Detects obfuscation types in JavaScript code by analyzing its AST. + * + * @param {string} code - The JavaScript source code to analyze. + * @param {boolean} [stopAfterFirst=true] - If true, returns after the first positive detection; if false, returns all matches. + * @returns {string[]} An array of detected obfuscation type names. Returns an empty array if no known type is detected. */ function detectObfuscation(code, stopAfterFirst = true) { const detectedObfuscations = []; try { const tree = generateFlatAST(code); - for (const detectorName in detectors) { + for (const detectorName of Object.keys(detectors)) { try { const detectionType = detectors[detectorName](tree, detectedObfuscations); if (detectionType) { @@ -28,4 +35,4 @@ function detectObfuscation(code, stopAfterFirst = true) { return detectedObfuscations; } -export {detectObfuscation}; +export {detectObfuscation}; \ No newline at end of file