simple-javascript-obf 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/node.js.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/THIRD_PARTY_NOTICES.md +82 -0
- package/bin/js-obf +228 -0
- package/bin/obf.sh +257 -0
- package/package.json +26 -0
- package/src/index.js +106 -0
- package/src/options.js +128 -0
- package/src/pipeline.js +56 -0
- package/src/plugins/antiHook.js +123 -0
- package/src/plugins/controlFlowFlatten.js +203 -0
- package/src/plugins/deadCode.js +82 -0
- package/src/plugins/encodeMembers.js +44 -0
- package/src/plugins/entry.js +31 -0
- package/src/plugins/rename.js +100 -0
- package/src/plugins/stringEncode.js +494 -0
- package/src/plugins/vm/ast-utils.js +58 -0
- package/src/plugins/vm/compiler.js +113 -0
- package/src/plugins/vm/constants.js +72 -0
- package/src/plugins/vm/emit.js +916 -0
- package/src/plugins/vm/encoding.js +252 -0
- package/src/plugins/vm/index.js +366 -0
- package/src/plugins/vm/mapping.js +24 -0
- package/src/plugins/vm/normalize.js +692 -0
- package/src/plugins/vm/runtime.js +1145 -0
- package/src/plugins/vm.js +1 -0
- package/src/utils/names.js +55 -0
- package/src/utils/reserved.js +57 -0
- package/src/utils/rng.js +55 -0
- package/src/utils/stream.js +97 -0
- package/src/utils/string.js +13 -0
- package/test/bench-runner.js +78 -0
- package/test/benchmark-source.js +35 -0
- package/test/benchmark-vm.js +160 -0
- package/test/dist/bench.obf.js +1 -0
- package/test/dist/bench.original.js +35 -0
- package/test/dist/bench.vm.js +1 -0
- package/test/dist/sample-input.obf.js +1 -0
- package/test/dist/sample-input.vm.js +1 -0
- package/test/generate-obf.js +38 -0
- package/test/obf-smoke.js +129 -0
- package/test/sample-input.js +23 -0
package/bin/obf.sh
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
prompt() {
|
|
5
|
+
local text="$1"
|
|
6
|
+
local value=""
|
|
7
|
+
read -r -p "$text" value
|
|
8
|
+
printf "%s" "$value"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
prompt_yes_no() {
|
|
12
|
+
local text="$1"
|
|
13
|
+
local value=""
|
|
14
|
+
read -r -p "$text" value
|
|
15
|
+
case "${value}" in
|
|
16
|
+
y|Y|yes|YES) return 0 ;;
|
|
17
|
+
*) return 1 ;;
|
|
18
|
+
esac
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
now_ms() {
|
|
22
|
+
date +%s%3N
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
format_duration() {
|
|
26
|
+
local ms="$1"
|
|
27
|
+
awk -v ms="${ms}" 'BEGIN { printf "%.3f", ms / 1000 }'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
root_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
31
|
+
|
|
32
|
+
input_path=$(prompt "Enter a JS file or directory path: ")
|
|
33
|
+
if [[ -z "${input_path}" ]]; then
|
|
34
|
+
echo "No input path provided." >&2
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
input_kind=""
|
|
39
|
+
if [[ -f "${input_path}" ]]; then
|
|
40
|
+
input_kind="file"
|
|
41
|
+
elif [[ -d "${input_path}" ]]; then
|
|
42
|
+
input_kind="dir"
|
|
43
|
+
else
|
|
44
|
+
echo "Input path not found: ${input_path}" >&2
|
|
45
|
+
exit 1
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
abs_input=$(readlink -f "${input_path}")
|
|
49
|
+
if [[ "${input_kind}" == "dir" ]]; then
|
|
50
|
+
input_dir="${abs_input}"
|
|
51
|
+
else
|
|
52
|
+
input_dir=$(dirname "${abs_input}")
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
echo "Input directory: ${input_dir}"
|
|
56
|
+
if [[ "${input_kind}" == "file" ]]; then
|
|
57
|
+
output_dir=$(prompt "Output directory (default: ${input_dir}, use . for current dir): ")
|
|
58
|
+
if [[ -z "${output_dir}" ]]; then
|
|
59
|
+
output_dir="${input_dir}"
|
|
60
|
+
fi
|
|
61
|
+
output_dir=$(readlink -f "${output_dir}")
|
|
62
|
+
mkdir -p "${output_dir}"
|
|
63
|
+
else
|
|
64
|
+
output_dir="${input_dir}"
|
|
65
|
+
if ! prompt_yes_no "This will overwrite all JS files in the directory. Continue? [y/N]: "; then
|
|
66
|
+
echo "Cancelled." >&2
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
fi
|
|
70
|
+
echo "Output directory: ${output_dir}"
|
|
71
|
+
|
|
72
|
+
max_jobs=1
|
|
73
|
+
if [[ "${input_kind}" == "dir" ]]; then
|
|
74
|
+
max_jobs=$(prompt "Concurrency (default 1): ")
|
|
75
|
+
if [[ -z "${max_jobs}" ]]; then
|
|
76
|
+
max_jobs=1
|
|
77
|
+
fi
|
|
78
|
+
if ! [[ "${max_jobs}" =~ ^[0-9]+$ ]] || [[ "${max_jobs}" -lt 1 ]]; then
|
|
79
|
+
echo "Concurrency must be a positive integer." >&2
|
|
80
|
+
exit 1
|
|
81
|
+
fi
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
preset=$(prompt "Preset strength (high/balanced/low, default high): ")
|
|
85
|
+
if [[ -z "${preset}" ]]; then
|
|
86
|
+
preset="high"
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
enable_rename=true
|
|
90
|
+
enable_strings=true
|
|
91
|
+
enable_cff=true
|
|
92
|
+
enable_dead=true
|
|
93
|
+
enable_vm=false
|
|
94
|
+
enable_anti_hook=false
|
|
95
|
+
enable_anti_hook_lock=false
|
|
96
|
+
|
|
97
|
+
if ! prompt_yes_no "Enable variable renaming? [y/N]: "; then
|
|
98
|
+
enable_rename=false
|
|
99
|
+
fi
|
|
100
|
+
if ! prompt_yes_no "Enable string encryption? [y/N]: "; then
|
|
101
|
+
enable_strings=false
|
|
102
|
+
fi
|
|
103
|
+
if ! prompt_yes_no "Enable control-flow flattening? [y/N]: "; then
|
|
104
|
+
enable_cff=false
|
|
105
|
+
fi
|
|
106
|
+
if ! prompt_yes_no "Enable dead-code injection? [y/N]: "; then
|
|
107
|
+
enable_dead=false
|
|
108
|
+
fi
|
|
109
|
+
if prompt_yes_no "Enable VM virtualization? [y/N]: "; then
|
|
110
|
+
enable_vm=true
|
|
111
|
+
fi
|
|
112
|
+
if prompt_yes_no "Enable anti-hook runtime guard? [y/N]: "; then
|
|
113
|
+
enable_anti_hook=true
|
|
114
|
+
if prompt_yes_no "Freeze built-in prototype chains (anti-hook-lock)? [y/N]: "; then
|
|
115
|
+
enable_anti_hook_lock=true
|
|
116
|
+
fi
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
vm_include=""
|
|
120
|
+
if ${enable_vm}; then
|
|
121
|
+
vm_include=$(prompt "Only virtualize function names (comma-separated, optional): ")
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
seed=$(prompt "PRNG seed (optional): ")
|
|
125
|
+
enable_sourcemap=false
|
|
126
|
+
enable_compact=false
|
|
127
|
+
if prompt_yes_no "Generate source map? [y/N]: "; then
|
|
128
|
+
enable_sourcemap=true
|
|
129
|
+
fi
|
|
130
|
+
if prompt_yes_no "Use compact output? [y/N]: "; then
|
|
131
|
+
enable_compact=true
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
common_args=(--preset "${preset}")
|
|
135
|
+
|
|
136
|
+
if ! ${enable_rename}; then
|
|
137
|
+
common_args+=(--no-rename)
|
|
138
|
+
fi
|
|
139
|
+
if ! ${enable_strings}; then
|
|
140
|
+
common_args+=(--no-strings)
|
|
141
|
+
fi
|
|
142
|
+
if ! ${enable_cff}; then
|
|
143
|
+
common_args+=(--no-cff)
|
|
144
|
+
fi
|
|
145
|
+
if ! ${enable_dead}; then
|
|
146
|
+
common_args+=(--no-dead)
|
|
147
|
+
fi
|
|
148
|
+
if ${enable_vm}; then
|
|
149
|
+
common_args+=(--vm)
|
|
150
|
+
if [[ -n "${vm_include}" ]]; then
|
|
151
|
+
common_args+=(--vm-include "${vm_include}")
|
|
152
|
+
fi
|
|
153
|
+
fi
|
|
154
|
+
if ${enable_anti_hook_lock}; then
|
|
155
|
+
common_args+=(--anti-hook-lock)
|
|
156
|
+
elif ${enable_anti_hook}; then
|
|
157
|
+
common_args+=(--anti-hook)
|
|
158
|
+
fi
|
|
159
|
+
if [[ -n "${seed}" ]]; then
|
|
160
|
+
common_args+=(--seed "${seed}")
|
|
161
|
+
fi
|
|
162
|
+
if ${enable_sourcemap}; then
|
|
163
|
+
common_args+=(--sourcemap)
|
|
164
|
+
fi
|
|
165
|
+
if ${enable_compact}; then
|
|
166
|
+
common_args+=(--compact)
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
if [[ "${input_kind}" == "file" ]]; then
|
|
170
|
+
base_name=$(basename "${abs_input}")
|
|
171
|
+
base_no_ext="${base_name%.*}"
|
|
172
|
+
output_path="${output_dir}/${base_no_ext}.obf.js"
|
|
173
|
+
cmd=(node "${root_dir}/bin/js-obf" "${abs_input}" -o "${output_path}" "${common_args[@]}")
|
|
174
|
+
total_start_ms=$(now_ms)
|
|
175
|
+
start_ms=$(now_ms)
|
|
176
|
+
echo "Run: ${cmd[*]}"
|
|
177
|
+
"${cmd[@]}"
|
|
178
|
+
end_ms=$(now_ms)
|
|
179
|
+
duration_ms=$((end_ms - start_ms))
|
|
180
|
+
duration_sec=$(format_duration "${duration_ms}")
|
|
181
|
+
echo "Done: ${output_path} (${duration_sec}s)"
|
|
182
|
+
total_end_ms=$(now_ms)
|
|
183
|
+
total_ms=$((total_end_ms - total_start_ms))
|
|
184
|
+
total_sec=$(format_duration "${total_ms}")
|
|
185
|
+
echo "Total time: ${total_sec}s"
|
|
186
|
+
else
|
|
187
|
+
prune_paths=(
|
|
188
|
+
-type d -name node_modules
|
|
189
|
+
-o -type d -name node_packge
|
|
190
|
+
-o -type d -name node_package
|
|
191
|
+
)
|
|
192
|
+
if [[ "${output_dir}" != "${input_dir}" && "${output_dir}" == "${input_dir}"/* ]]; then
|
|
193
|
+
prune_paths+=(-o -path "${output_dir}")
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
files=()
|
|
197
|
+
while IFS= read -r -d '' file; do
|
|
198
|
+
files+=("${file}")
|
|
199
|
+
done < <(
|
|
200
|
+
find "${input_dir}" \( "${prune_paths[@]}" \) -prune -o \
|
|
201
|
+
-type f -name "*.js" ! -name "*.obf.js" -print0
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if [[ ${#files[@]} -eq 0 ]]; then
|
|
205
|
+
echo "No JS files found to obfuscate." >&2
|
|
206
|
+
exit 1
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
total_start_ms=$(now_ms)
|
|
210
|
+
echo "Found ${#files[@]} JS files. Starting obfuscation..."
|
|
211
|
+
failed=false
|
|
212
|
+
run_file() {
|
|
213
|
+
local file="$1"
|
|
214
|
+
local rel_path="${file#${input_dir}/}"
|
|
215
|
+
local output_path="${output_dir}/${rel_path}"
|
|
216
|
+
mkdir -p "$(dirname "${output_path}")"
|
|
217
|
+
local cmd=(node "${root_dir}/bin/js-obf" "${file}" -o "${output_path}" "${common_args[@]}")
|
|
218
|
+
local start_ms
|
|
219
|
+
local end_ms
|
|
220
|
+
start_ms=$(now_ms)
|
|
221
|
+
echo "Run: ${cmd[*]}"
|
|
222
|
+
"${cmd[@]}"
|
|
223
|
+
end_ms=$(now_ms)
|
|
224
|
+
local duration_ms=$((end_ms - start_ms))
|
|
225
|
+
local duration_sec
|
|
226
|
+
duration_sec=$(format_duration "${duration_ms}")
|
|
227
|
+
echo "Done: ${output_path} (${duration_sec}s)"
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if [[ "${max_jobs}" -le 1 ]]; then
|
|
231
|
+
for file in "${files[@]}"; do
|
|
232
|
+
run_file "${file}"
|
|
233
|
+
done
|
|
234
|
+
else
|
|
235
|
+
for file in "${files[@]}"; do
|
|
236
|
+
run_file "${file}" &
|
|
237
|
+
while (( $(jobs -pr | wc -l) >= max_jobs )); do
|
|
238
|
+
if ! wait -n; then
|
|
239
|
+
failed=true
|
|
240
|
+
fi
|
|
241
|
+
done
|
|
242
|
+
done
|
|
243
|
+
while (( $(jobs -pr | wc -l) > 0 )); do
|
|
244
|
+
if ! wait -n; then
|
|
245
|
+
failed=true
|
|
246
|
+
fi
|
|
247
|
+
done
|
|
248
|
+
if ${failed}; then
|
|
249
|
+
echo "Some files failed to obfuscate. Check the logs." >&2
|
|
250
|
+
exit 1
|
|
251
|
+
fi
|
|
252
|
+
fi
|
|
253
|
+
total_end_ms=$(now_ms)
|
|
254
|
+
total_ms=$((total_end_ms - total_start_ms))
|
|
255
|
+
total_sec=$(format_duration "${total_ms}")
|
|
256
|
+
echo "All done: ${#files[@]} files in ${total_sec}s"
|
|
257
|
+
fi
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "simple-javascript-obf",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Modular JavaScript obfuscator with optional VM and CFF",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"js-obf": "bin/js-obf"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"obf": "node bin/js-obf"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"javascript",
|
|
14
|
+
"obfuscator",
|
|
15
|
+
"vm",
|
|
16
|
+
"control-flow-flattening"
|
|
17
|
+
],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@babel/generator": "^7.26.0",
|
|
21
|
+
"@babel/parser": "^7.26.0",
|
|
22
|
+
"@babel/traverse": "^7.26.0",
|
|
23
|
+
"@babel/types": "^7.26.0",
|
|
24
|
+
"terser": "^5.44.1"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const parser = require("@babel/parser");
|
|
2
|
+
const generate = require("@babel/generator").default;
|
|
3
|
+
const traverse = require("@babel/traverse").default;
|
|
4
|
+
const t = require("@babel/types");
|
|
5
|
+
const { minify } = require("terser");
|
|
6
|
+
const { buildPipeline } = require("./pipeline");
|
|
7
|
+
const { normalizeOptions } = require("./options");
|
|
8
|
+
|
|
9
|
+
function parseSource(code, filename) {
|
|
10
|
+
return parser.parse(code, {
|
|
11
|
+
sourceType: "unambiguous",
|
|
12
|
+
sourceFilename: filename,
|
|
13
|
+
allowReturnOutsideFunction: true,
|
|
14
|
+
plugins: [
|
|
15
|
+
"jsx",
|
|
16
|
+
"typescript",
|
|
17
|
+
"classProperties",
|
|
18
|
+
"classPrivateProperties",
|
|
19
|
+
"classPrivateMethods",
|
|
20
|
+
"decorators-legacy",
|
|
21
|
+
"dynamicImport",
|
|
22
|
+
"objectRestSpread",
|
|
23
|
+
"optionalChaining",
|
|
24
|
+
"nullishCoalescingOperator",
|
|
25
|
+
"numericSeparator",
|
|
26
|
+
"logicalAssignment",
|
|
27
|
+
"topLevelAwait",
|
|
28
|
+
"exportDefaultFrom",
|
|
29
|
+
"exportNamespaceFrom",
|
|
30
|
+
"asyncGenerators",
|
|
31
|
+
"bigInt",
|
|
32
|
+
"optionalCatchBinding",
|
|
33
|
+
"privateIn",
|
|
34
|
+
"importAssertions"
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function obfuscate(source, userOptions = {}) {
|
|
40
|
+
const options = normalizeOptions(userOptions);
|
|
41
|
+
const ast = parseSource(source, options.filename);
|
|
42
|
+
|
|
43
|
+
const pipeline = buildPipeline({
|
|
44
|
+
t,
|
|
45
|
+
traverse,
|
|
46
|
+
options,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
for (const plugin of pipeline) {
|
|
50
|
+
plugin(ast);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const output = generate(
|
|
54
|
+
ast,
|
|
55
|
+
{
|
|
56
|
+
compact: options.compact,
|
|
57
|
+
sourceMaps: options.sourceMap,
|
|
58
|
+
sourceFileName: options.filename,
|
|
59
|
+
retainLines: false,
|
|
60
|
+
comments: false,
|
|
61
|
+
},
|
|
62
|
+
source
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const ecma = Number.isFinite(options.ecma) ? options.ecma : 2015;
|
|
66
|
+
const minifyOptions = {
|
|
67
|
+
ecma,
|
|
68
|
+
compress: {
|
|
69
|
+
defaults: true,
|
|
70
|
+
ecma,
|
|
71
|
+
},
|
|
72
|
+
mangle: true,
|
|
73
|
+
format: { comments: false, ecma },
|
|
74
|
+
};
|
|
75
|
+
if (options.sourceMap) {
|
|
76
|
+
minifyOptions.sourceMap = { content: output.map, asObject: true };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const minified = await minify(output.code, minifyOptions);
|
|
80
|
+
if (minified && minified.error) {
|
|
81
|
+
throw minified.error;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let map = null;
|
|
85
|
+
if (options.sourceMap) {
|
|
86
|
+
if (typeof minified.map === "string") {
|
|
87
|
+
try {
|
|
88
|
+
map = JSON.parse(minified.map);
|
|
89
|
+
} catch {
|
|
90
|
+
map = null;
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
map = minified.map || null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
code: minified.code || "",
|
|
99
|
+
map,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
obfuscate,
|
|
105
|
+
parseSource,
|
|
106
|
+
};
|
package/src/options.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const { DEFAULT_RESERVED } = require("./utils/reserved");
|
|
2
|
+
|
|
3
|
+
const DEFAULT_ECMA = 2015;
|
|
4
|
+
|
|
5
|
+
function normalizeEcma(value) {
|
|
6
|
+
if (value === undefined || value === null || value === "") {
|
|
7
|
+
return DEFAULT_ECMA;
|
|
8
|
+
}
|
|
9
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
const text = String(value).trim().toLowerCase();
|
|
13
|
+
if (!text) {
|
|
14
|
+
return DEFAULT_ECMA;
|
|
15
|
+
}
|
|
16
|
+
const normalized = text.startsWith("es") ? text.slice(2) : text;
|
|
17
|
+
const parsed = Number(normalized);
|
|
18
|
+
if (Number.isFinite(parsed)) {
|
|
19
|
+
return parsed;
|
|
20
|
+
}
|
|
21
|
+
return DEFAULT_ECMA;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const PRESETS = {
|
|
25
|
+
high: {
|
|
26
|
+
rename: true,
|
|
27
|
+
strings: true,
|
|
28
|
+
cff: true,
|
|
29
|
+
dead: true,
|
|
30
|
+
vm: false,
|
|
31
|
+
},
|
|
32
|
+
balanced: {
|
|
33
|
+
rename: true,
|
|
34
|
+
strings: true,
|
|
35
|
+
cff: true,
|
|
36
|
+
dead: false,
|
|
37
|
+
vm: false,
|
|
38
|
+
},
|
|
39
|
+
low: {
|
|
40
|
+
rename: true,
|
|
41
|
+
strings: false,
|
|
42
|
+
cff: false,
|
|
43
|
+
dead: false,
|
|
44
|
+
vm: false,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function normalizeOptions(userOptions = {}) {
|
|
49
|
+
const presetName = userOptions.preset || "high";
|
|
50
|
+
const preset = PRESETS[presetName] || PRESETS.high;
|
|
51
|
+
const stringsUserOptions = userOptions.stringsOptions || {};
|
|
52
|
+
const antiHookUserOptions = userOptions.antiHook;
|
|
53
|
+
const ecma = normalizeEcma(userOptions.ecma);
|
|
54
|
+
|
|
55
|
+
const options = {
|
|
56
|
+
preset: presetName,
|
|
57
|
+
rename: userOptions.rename ?? preset.rename,
|
|
58
|
+
strings: userOptions.strings ?? preset.strings,
|
|
59
|
+
cff: userOptions.cff ?? preset.cff,
|
|
60
|
+
dead: userOptions.dead ?? preset.dead,
|
|
61
|
+
vm: userOptions.vm || { enabled: preset.vm },
|
|
62
|
+
seed: userOptions.seed || "js-obf",
|
|
63
|
+
filename: userOptions.filename || "input.js",
|
|
64
|
+
sourceMap: Boolean(userOptions.sourceMap),
|
|
65
|
+
compact: Boolean(userOptions.compact),
|
|
66
|
+
ecma,
|
|
67
|
+
stringsOptions: {
|
|
68
|
+
minLength: stringsUserOptions.minLength ?? 3,
|
|
69
|
+
maxCount: stringsUserOptions.maxCount ?? 5000,
|
|
70
|
+
encodeConsole: stringsUserOptions.encodeConsole !== false,
|
|
71
|
+
},
|
|
72
|
+
renameOptions: {
|
|
73
|
+
reserved: userOptions.reserved || DEFAULT_RESERVED,
|
|
74
|
+
renameGlobals: userOptions.renameGlobals ?? false,
|
|
75
|
+
},
|
|
76
|
+
deadCodeOptions: {
|
|
77
|
+
probability: 0.15,
|
|
78
|
+
},
|
|
79
|
+
cffOptions: {
|
|
80
|
+
minStatements: 3,
|
|
81
|
+
},
|
|
82
|
+
antiHook: {
|
|
83
|
+
enabled: false,
|
|
84
|
+
lock: false,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (typeof antiHookUserOptions === "boolean") {
|
|
89
|
+
options.antiHook.enabled = antiHookUserOptions;
|
|
90
|
+
} else if (antiHookUserOptions && typeof antiHookUserOptions === "object") {
|
|
91
|
+
options.antiHook.enabled = antiHookUserOptions.enabled !== false;
|
|
92
|
+
options.antiHook.lock = Boolean(antiHookUserOptions.lock);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (typeof options.vm === "boolean") {
|
|
96
|
+
options.vm = { enabled: options.vm };
|
|
97
|
+
}
|
|
98
|
+
const vmOptions = options.vm || {};
|
|
99
|
+
let fakeOpcodes = vmOptions.fakeOpcodes;
|
|
100
|
+
if (fakeOpcodes === undefined || fakeOpcodes === true) {
|
|
101
|
+
fakeOpcodes = 0.15;
|
|
102
|
+
} else if (fakeOpcodes === false) {
|
|
103
|
+
fakeOpcodes = 0;
|
|
104
|
+
} else {
|
|
105
|
+
fakeOpcodes = Number(fakeOpcodes);
|
|
106
|
+
if (Number.isNaN(fakeOpcodes)) {
|
|
107
|
+
fakeOpcodes = 0.15;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
fakeOpcodes = Math.max(0, Math.min(1, fakeOpcodes));
|
|
111
|
+
options.vm = {
|
|
112
|
+
enabled: Boolean(vmOptions.enabled),
|
|
113
|
+
include: vmOptions.include || [],
|
|
114
|
+
all: Boolean(vmOptions.all),
|
|
115
|
+
opcodeShuffle: vmOptions.opcodeShuffle !== false,
|
|
116
|
+
fakeOpcodes,
|
|
117
|
+
bytecodeEncrypt: vmOptions.bytecodeEncrypt !== false,
|
|
118
|
+
constsEncrypt: vmOptions.constsEncrypt !== false,
|
|
119
|
+
downlevel: Boolean(vmOptions.downlevel),
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return options;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = {
|
|
126
|
+
normalizeOptions,
|
|
127
|
+
PRESETS,
|
|
128
|
+
};
|
package/src/pipeline.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const { RNG } = require("./utils/rng");
|
|
2
|
+
const { NameGenerator } = require("./utils/names");
|
|
3
|
+
const entryPlugin = require("./plugins/entry");
|
|
4
|
+
const renamePlugin = require("./plugins/rename");
|
|
5
|
+
const stringPlugin = require("./plugins/stringEncode");
|
|
6
|
+
const memberEncodePlugin = require("./plugins/encodeMembers");
|
|
7
|
+
const cffPlugin = require("./plugins/controlFlowFlatten");
|
|
8
|
+
const deadPlugin = require("./plugins/deadCode");
|
|
9
|
+
const antiHookPlugin = require("./plugins/antiHook");
|
|
10
|
+
const vmPlugin = require("./plugins/vm");
|
|
11
|
+
|
|
12
|
+
function buildPipeline({ t, traverse, options }) {
|
|
13
|
+
const rng = new RNG(options.seed);
|
|
14
|
+
const nameGen = new NameGenerator({
|
|
15
|
+
reserved: options.renameOptions.reserved,
|
|
16
|
+
rng,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const ctx = {
|
|
20
|
+
t,
|
|
21
|
+
traverse,
|
|
22
|
+
options,
|
|
23
|
+
rng,
|
|
24
|
+
nameGen,
|
|
25
|
+
state: {},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const plugins = [];
|
|
29
|
+
|
|
30
|
+
plugins.push((ast) => entryPlugin(ast, ctx));
|
|
31
|
+
if (options.vm.enabled) {
|
|
32
|
+
plugins.push((ast) => vmPlugin(ast, ctx));
|
|
33
|
+
}
|
|
34
|
+
if (options.cff) {
|
|
35
|
+
plugins.push((ast) => cffPlugin(ast, ctx));
|
|
36
|
+
}
|
|
37
|
+
if (options.strings) {
|
|
38
|
+
plugins.push((ast) => memberEncodePlugin(ast, ctx));
|
|
39
|
+
plugins.push((ast) => stringPlugin(ast, ctx));
|
|
40
|
+
}
|
|
41
|
+
if (options.dead) {
|
|
42
|
+
plugins.push((ast) => deadPlugin(ast, ctx));
|
|
43
|
+
}
|
|
44
|
+
if (options.antiHook && options.antiHook.enabled) {
|
|
45
|
+
plugins.push((ast) => antiHookPlugin(ast, ctx));
|
|
46
|
+
}
|
|
47
|
+
if (options.rename) {
|
|
48
|
+
plugins.push((ast) => renamePlugin(ast, ctx));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return plugins;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
buildPipeline,
|
|
56
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const parser = require("@babel/parser");
|
|
2
|
+
|
|
3
|
+
function insertAtTop(programPath, nodes) {
|
|
4
|
+
const body = programPath.node.body;
|
|
5
|
+
let index = 0;
|
|
6
|
+
while (index < body.length) {
|
|
7
|
+
const stmt = body[index];
|
|
8
|
+
if (stmt.type === "ExpressionStatement" && stmt.directive) {
|
|
9
|
+
index += 1;
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
body.splice(index, 0, ...nodes);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function buildRuntime({ lock }) {
|
|
18
|
+
const code = `
|
|
19
|
+
(function () {
|
|
20
|
+
const g = typeof globalThis !== "undefined"
|
|
21
|
+
? globalThis
|
|
22
|
+
: typeof window !== "undefined"
|
|
23
|
+
? window
|
|
24
|
+
: typeof global !== "undefined"
|
|
25
|
+
? global
|
|
26
|
+
: this;
|
|
27
|
+
const nativeMark = "[native code]";
|
|
28
|
+
const fnToString =
|
|
29
|
+
g.Function && g.Function.prototype && g.Function.prototype.toString;
|
|
30
|
+
const isNative = (fn) => {
|
|
31
|
+
if (typeof fn !== "function" || !fnToString) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
return fnToString.call(fn).indexOf(nativeMark) !== -1;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const checkAll = () => {
|
|
41
|
+
const required = [
|
|
42
|
+
g.Function,
|
|
43
|
+
g.eval,
|
|
44
|
+
g.Object && g.Object.defineProperty,
|
|
45
|
+
g.Object && g.Object.getOwnPropertyDescriptor,
|
|
46
|
+
g.Function && g.Function.prototype && g.Function.prototype.toString,
|
|
47
|
+
];
|
|
48
|
+
for (let i = 0; i < required.length; i += 1) {
|
|
49
|
+
const fn = required[i];
|
|
50
|
+
if (!isNative(fn)) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const optional = [
|
|
55
|
+
g.Object && g.Object.getPrototypeOf,
|
|
56
|
+
g.Object && g.Object.keys,
|
|
57
|
+
g.Object && g.Object.freeze,
|
|
58
|
+
g.JSON && g.JSON.parse,
|
|
59
|
+
g.JSON && g.JSON.stringify,
|
|
60
|
+
g.Math && g.Math.random,
|
|
61
|
+
g.Date,
|
|
62
|
+
g.Date && g.Date.now,
|
|
63
|
+
g.Reflect && g.Reflect.apply,
|
|
64
|
+
g.Reflect && g.Reflect.construct,
|
|
65
|
+
];
|
|
66
|
+
for (let i = 0; i < optional.length; i += 1) {
|
|
67
|
+
const fn = optional[i];
|
|
68
|
+
if (typeof fn !== "function") {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (!isNative(fn)) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
};
|
|
77
|
+
if (!checkAll()) {
|
|
78
|
+
throw new Error("Hook detected");
|
|
79
|
+
}
|
|
80
|
+
if (${lock ? "true" : "false"}) {
|
|
81
|
+
const freeze = g.Object && g.Object.freeze;
|
|
82
|
+
if (typeof freeze === "function") {
|
|
83
|
+
const lockProto = (obj) => {
|
|
84
|
+
try {
|
|
85
|
+
if (obj) {
|
|
86
|
+
freeze(obj);
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {}
|
|
89
|
+
};
|
|
90
|
+
lockProto(g.Function && g.Function.prototype);
|
|
91
|
+
lockProto(g.Object && g.Object.prototype);
|
|
92
|
+
lockProto(g.Array && g.Array.prototype);
|
|
93
|
+
lockProto(g.String && g.String.prototype);
|
|
94
|
+
lockProto(g.Number && g.Number.prototype);
|
|
95
|
+
lockProto(g.Boolean && g.Boolean.prototype);
|
|
96
|
+
lockProto(g.Date && g.Date.prototype);
|
|
97
|
+
lockProto(g.RegExp && g.RegExp.prototype);
|
|
98
|
+
lockProto(g.Error && g.Error.prototype);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
})();
|
|
102
|
+
`;
|
|
103
|
+
return parser.parse(code, { sourceType: "script" }).program.body;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function antiHook(ast, ctx) {
|
|
107
|
+
if (!ctx.options.antiHook || !ctx.options.antiHook.enabled) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
let programPathRef = null;
|
|
111
|
+
ctx.traverse(ast, {
|
|
112
|
+
Program(path) {
|
|
113
|
+
programPathRef = path;
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
if (!programPathRef) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const runtime = buildRuntime({ lock: ctx.options.antiHook.lock });
|
|
120
|
+
insertAtTop(programPathRef, runtime);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = antiHook;
|