166 lines
5.0 KiB
Python
Executable File
166 lines
5.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Environment patcher for running obfuscated JS in Node.js VM sandbox.
|
|
|
|
Diagnoses missing browser APIs and generates patches to make the JS runnable.
|
|
|
|
Usage:
|
|
python3 scripts/env_patcher.py --input bundle.js --output artifacts/env_report.json
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
from common import dump_json, load_json
|
|
|
|
|
|
DIAGNOSE_SCRIPT = r'''
|
|
const vm = require('vm');
|
|
const fs = require('fs');
|
|
|
|
const input = process.argv[2];
|
|
const code = fs.readFileSync(input, 'utf8');
|
|
|
|
// Create sandbox with basic browser APIs
|
|
const sandbox = {
|
|
window: {}, self: {}, global: {}, globalThis: {},
|
|
console: { log: () => {}, warn: () => {}, error: () => {} },
|
|
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
setInterval: (fn, ms) => setInterval(fn, ms),
|
|
clearTimeout: (id) => clearTimeout(id),
|
|
clearInterval: (id) => clearInterval(id),
|
|
atob: (s) => Buffer.from(s, 'base64').toString('binary'),
|
|
btoa: (s) => Buffer.from(s, 'binary').toString('base64'),
|
|
TextEncoder: TextEncoder,
|
|
TextDecoder: TextDecoder,
|
|
URL: URL,
|
|
URLSearchParams: URLSearchParams,
|
|
};
|
|
|
|
// Make window/self/global/globalThis point to sandbox
|
|
sandbox.window = sandbox;
|
|
sandbox.self = sandbox;
|
|
sandbox.global = sandbox;
|
|
sandbox.globalThis = sandbox;
|
|
|
|
// Proxy to track undefined access
|
|
const undefinedPaths = new Set();
|
|
const proxy = new Proxy(sandbox, {
|
|
get(target, prop) {
|
|
if (prop === Symbol.unscopables) return undefined;
|
|
if (prop in target) return target[prop];
|
|
if (typeof prop === 'string' && !prop.startsWith('_')) {
|
|
undefinedPaths.add(prop);
|
|
}
|
|
return undefined;
|
|
},
|
|
set(target, prop, value) {
|
|
target[prop] = value;
|
|
return true;
|
|
}
|
|
});
|
|
|
|
// Run in VM
|
|
const context = vm.createContext(proxy);
|
|
try {
|
|
vm.runInContext(code, context, { timeout: 5000 });
|
|
console.log(JSON.stringify({
|
|
success: true,
|
|
error: null,
|
|
undefinedPaths: Array.from(undefinedPaths).sort(),
|
|
stats: { events: undefinedPaths.size }
|
|
}));
|
|
} catch (error) {
|
|
console.log(JSON.stringify({
|
|
success: false,
|
|
error: error.message,
|
|
undefinedPaths: Array.from(undefinedPaths).sort(),
|
|
stats: { events: undefinedPaths.size }
|
|
}));
|
|
}
|
|
'''
|
|
|
|
|
|
def diagnose(js_path: str) -> dict:
|
|
"""Run diagnosis on a JS file to find missing browser APIs."""
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False) as f:
|
|
f.write(DIAGNOSE_SCRIPT)
|
|
diag_script = f.name
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
['node', diag_script, js_path],
|
|
capture_output=True, text=True, timeout=10
|
|
)
|
|
if result.returncode == 0:
|
|
return json.loads(result.stdout)
|
|
return {"success": False, "error": result.stderr, "undefinedPaths": []}
|
|
except Exception as e:
|
|
return {"success": False, "error": str(e), "undefinedPaths": []}
|
|
finally:
|
|
Path(diag_script).unlink(missing_ok=True)
|
|
|
|
|
|
# Mapping from undefined paths to required modules
|
|
MODULE_MAP = {
|
|
'navigator': 'bom/navigator',
|
|
'location': 'bom/location',
|
|
'screen': 'bom/screen',
|
|
'localStorage': 'bom/storage',
|
|
'sessionStorage': 'bom/storage',
|
|
'crypto': 'bom/crypto',
|
|
'performance': 'bom/performance',
|
|
'history': 'bom/history',
|
|
'document': 'dom/document',
|
|
'Element': 'dom/elements',
|
|
'HTMLElement': 'dom/elements',
|
|
'Event': 'dom/event',
|
|
'fetch': 'webapi/fetch',
|
|
'XMLHttpRequest': 'webapi/xhr',
|
|
'Blob': 'webapi/blob',
|
|
'FormData': 'webapi/blob',
|
|
'File': 'webapi/blob',
|
|
'WebSocket': 'webapi/network',
|
|
'TextEncoder': 'encoding/textencoder',
|
|
'TextDecoder': 'encoding/textencoder',
|
|
'atob': 'encoding/atob',
|
|
'btoa': 'encoding/atob',
|
|
}
|
|
|
|
|
|
def select_modules(undefined_paths: list[str]) -> list[str]:
|
|
"""Select required modules based on undefined paths."""
|
|
modules = set()
|
|
for path in undefined_paths:
|
|
for prefix, module in MODULE_MAP.items():
|
|
if path.startswith(prefix) or path == prefix:
|
|
modules.add(module)
|
|
return sorted(modules)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Environment patcher for JS sandbox.")
|
|
parser.add_argument("--input", required=True, help="Path to JS file.")
|
|
parser.add_argument("--output", required=True, help="Path to output report.")
|
|
args = parser.parse_args()
|
|
|
|
report = diagnose(args.input)
|
|
if report.get("undefinedPaths"):
|
|
report["selectedModules"] = select_modules(report["undefinedPaths"])
|
|
|
|
dump_json(args.output, report)
|
|
print(json.dumps({
|
|
"success": report.get("success", False),
|
|
"undefinedPaths": len(report.get("undefinedPaths", [])),
|
|
"selectedModules": len(report.get("selectedModules", [])),
|
|
"output": args.output
|
|
}, ensure_ascii=False))
|
|
return 0 if report.get("success") else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|