A stack based virtual machine with encryption for protecting JavaScript code on the web.
Note: This is my first (big) C++ project, so the code and techniques are probably not optimal. Still, I thought it would be interesting to give it a try and see how far I could get.
- Custom bytecode compiler for JavaScript subset
- Session-based opcode randomization
- Bytecode encryption
- WebAssembly VM for browser execution
- Native compiler tool (with JS AST)
Currently the compiler is very basic and does not support strings or any complex javascript. Also interacting with the DOM or external libraries is not possible.
For Native Compiler:
- CMake 3.10+
- C++17 compatible compiler (GCC 7+, Clang 5+, MSVC 2017+)
- Nodejs with
erpismamodule
For WASM Module:
- Emscripten SDK
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.shnpm install esprimachmod +x build.sh
./build.shThis creates build/native/pvmc - the compiler executable.
chmod +x build_wasm.sh
./build_wasm.shThis creates build/wasm/vm.js and build/wasm/vm.wasm
./build_compiler.sh && ./build_wasm.sh examples/fib.js ./web --debug
./build_compiler.sh && ./build_wasm.sh examples/fib.js ./web --releaseThe release tag will disable all debug output to devtools and will compile more optimized.
PRINT(1)Will print a variable or input to the devtools console.
EXIT(1)Will load a variable or output onto the stack and run the HALT opcode which returns the element on top of the stack out of the VM.
# Create a JS file
echo "let x = 10; let y = 5; let result = x + y;" > example.js
# Compile it
./build_compiler.sh && ./build_wasm.sh example.js ./web --debug
npm run serve
# This generates:
# - output.bc (encrypted bytecode)
# - output.bc.session (session keys for WASM)
# - Serves a test page where the code can be exucted (DevTools to see debug messages)<!DOCTYPE html>
<html>
<head>
<script src="vm.js"></script>
</head>
<body>
<script>
// Initialize WASM
WasmVM().then(module => {
module.then(module => {
// Load your bytecode
fetch('output.bc')
.then(r => r.arrayBuffer())
.then(data => {
const result = module.executeCode(new Uint8Array(data));
console.log('Result:', result);
});
});
}).catch(err => {
console.log('Failed to load WASM');
});
</script>
</body>
</html>Developer Machine: User's Browser:
┌─────────────┐ ┌─────────────┐
│ source.js │ │encrypted.bc │ ← Public
└──────┬──────┘ └──────┬──────┘
│ │
▼ │
┌─────────────┐ │
│ Compiler │ │
└──────┬──────┘ │
│ │
├──→ encrypted.bc │
└──→ encrypted.bc.session │
│ │
│ │
┌──────▼────────┐ │
│ EMBED SESSION │ │
│ IN vm.wasm │ │
└──────┬────────┘ │
│ │
▼ │
┌─────────────┐ │
│ vm.wasm │ ←─────────────────┤
│ ┌────────┐ │ │
│ │KEYS 🔑 │ │ ▼
│ └────────┘ │ ┌─────────────┐
│ decrypt │ │ EXECUTE │
│ execute │ │ result: 42 │
└─────────────┘ └─────────────┘
- Bytecode Obfuscation: Source code converted to binary bytecode
- Encryption: XOR encryption of bytecode (Should be changed in real scenarios)
- Opcode Randomization: Different opcode mappings per session
- WASM Protection: VM logic compiled to WebAssembly
- Add string support
- Add access to DOM (and external libraries)
- Better encryption
- Look further in obfuscation and protecting the WASM
This project is currently very basic and will barely work for any legit usecase.
Educational use only.