Web的开放与便捷带来了极高速的发展,但同时也带来了至关多的隐患,特别是针对于核心代码保护上,自做者从事Web前端相关开发的相关工做以来,并未听闻到太多相关于此的方案,『前端代码无秘密』这句话好似一个业界共识通常在前端领域传播。但在平常的开发过程当中,咱们又会涉及以及须要至关强度的前端核心代码的加密,特别是在于与后端的数据通讯上面(包括HTTP、HTTPS请求以及WebSocket的数据交换)。javascript
考虑一个场景,在视频相关的产品中,咱们一般须要增长相关的安全逻辑防止被直接盗流或是盗播。特别是对于直播来讲,咱们的直播视频流文件一般会被划分为分片而后经过协商的算法生成对应的URL参数并逐次请求。分片一般以5至10秒一个间隔,若是将分片URL的获取做为接口彻底放置于后端,那么不只会给后端带来极大的压力外还会带来直播播放请求的延迟,所以咱们一般会将部分实现放置于前端以此来减小后端压力并加强体验。对于iOS或是Android来讲,咱们能够将相关的算法经过C/C++进行编写,而后编译为dylib或是so并进行混淆以此来增长破解的复杂度,可是对于前端来讲,并无相似的技术可使用。固然,自从asm.js及WebAssembly的全面推动后,咱们可使用其进一步加强咱们核心代码的安全性,但因为asm.js以及WebAssembly标准的开放,其安全强度也并不是想象中的那么美好。html
本文首先适当回顾目前流行的前端核心代码保护的相关技术思路及简要的实现,后具体讲述一种更为安全可靠的前端核心代码保护的思路(SecurityWorker)供你们借鉴以及改进。固然,做者并不是专业的前端安全从业者,对部分技术安全性的理解可能稍显片面及不足,欢迎留言一块儿探讨。前端
在咱们的平常开发过程当中,对于JavaScript的混淆器咱们是不陌生的,咱们经常使用其进行代码的压缩以及混淆以此来减小代码体积并增长人为阅读代码的复杂度。常使用的项目包括:java
JavaScript混淆器的原理并不复杂,其核心是对目标代码进行AST Transformation(抽象语法树改写),咱们依靠现有的JavaScript的AST Parser库,能比较容易的实现本身的Javascript混淆器。如下咱们借助 acorn 来实现一个if语句片断的改写。node
假设咱们存在这么一个代码片断:c++
for(var i = 0; i < 100; i++){
if(i % 2 == 0){
console.log("foo");
}else{
console.log("bar");
}
}
复制代码
咱们经过使用UglifyJS进行代码的混淆,咱们可以获得以下的结果:git
for(var i=0;i<100;i++)i%2==0?console.log("foo"):console.log("bar");
复制代码
如今让咱们尝试编写一个本身的混淆器对代码片断进行混淆达到UglifyJS的效果:github
const {Parser} = require("acorn")
const MyUglify = Parser.extend();
const codeStr = ` for(var i = 0; i < 100; i++){ if(i % 2 == 0){ console.log("foo"); }else{ console.log("bar"); } } `;
function transform(node){
const { type } = node;
switch(type){
case 'Program':
case 'BlockStatement':{
const { body } = node;
return body.map(transform).join('');
}
case 'ForStatement':{
const results = ['for', '('];
const { init, test, update, body } = node;
results.push(transform(init), ';');
results.push(transform(test), ';');
results.push(transform(update), ')');
results.push(transform(body));
return results.join('');
}
case 'VariableDeclaration': {
const results = [];
const { kind, declarations } = node;
results.push(kind, ' ', declarations.map(transform));
return results.join('');
}
case 'VariableDeclarator':{
const {id, init} = node;
return id.name + '=' + init.raw;
}
case 'UpdateExpression': {
const {argument, operator} = node;
return argument.name + operator;
}
case 'BinaryExpression': {
const {left, operator, right} = node;
return transform(left) + operator + transform(right);
}
case 'IfStatement': {
const results = [];
const { test, consequent, alternate } = node;
results.push(transform(test), '?');
results.push(transform(consequent), ":");
results.push(transform(alternate));
return results.join('');
}
case 'MemberExpression':{
const {object, property} = node;
return object.name + '.' + property.name;
}
case 'CallExpression': {
const results = [];
const { callee, arguments } = node;
results.push(transform(callee), '(');
results.push(arguments.map(transform).join(','), ')');
return results.join('');
}
case 'ExpressionStatement':{
return transform(node.expression);
}
case 'Literal':
return node.raw;
case 'Identifier':
return node.name;
default:
throw new Error('unimplemented operations');
}
}
const ast = MyUglify.parse(codeStr);
console.log(transform(ast)); // 与UglifyJS输出一致
复制代码
固然,咱们上面的实现只是一个简单的举例,实际上的混淆器实现会比当前的实现复杂得多,须要考虑很是多的语法上的细节,此处仅抛砖引玉供你们参考学习。web
从上面的实现咱们能够看出,JavaScript混淆器只是将JavaScript代码变化为另外一种更不可读的形式,以此来增长人为分析的难度从而达到加强安全的目的。这种方式在好久之前具备很不错的效果,可是随着开发者工具愈来愈强大,实际上经过单步调试能够很容易逆向出原始的Javascript的核心算法。固然,后续也有至关多的库作了较多的改进,JavaScript Obfuscator Tool 是其中的表明项目,其增长了诸如反调试、变量前缀、变量混淆等功能加强安全性。但万变不离其宗,因为混淆后的代码仍然是明文的,若是有足够的耐心并借助开发者工具咱们仍然能够尝试还原,所以安全性仍然大打折扣。算法
在Flash还大行其道的时期,为了更好的方便引擎开发者使用C/C++来提高Flash游戏相关引擎的性能,Adobe开源了 CrossBridge 这个技术。在这种过程当中,原有的C/C++代码通过LLVM IR变为Flash运行时所须要的目标代码,无论是从效率提高上仍是从安全性上都有了很是大的提高。对于目前的开源的反编译器来讲,很难反编译由CorssBridge编译的C/C++代码,而且因为Flash运行时生产环境中禁用调试,所以也很难进行对应的单步调试。
使用Flash的C/C++扩展方式来保护咱们的前端核心代码看起来是比较理想的方法,但Flash的移动端上已经没有任何可被使用的空间,同时Adobe已经宣布2020年再也不对Flash进行维护,所以咱们彻底没有理由再使用这种方法来保护咱们前端的核心代码。
固然,因为Flash目前在PC上仍然有很大的占有率,而且IE10如下的浏览器仍然有很多份额,咱们仍旧能够把此做为一种PC端的兼容方案考虑进来。
为了解决Javascript的性能问题,Mozilla提出了一套新的面相底层的Javascript语法子集 -- asm.js,其从JIT友好的角度出发,使得Javascript的总体运行性能有了很大的提高。后续Mozilla与其余厂商进行相关的标准化,产出了WebAssembly标准。
无论是asm.js或是WebAssembly,咱们均可以将其看做为一个全新的VM,其余语言经过相关的工具链产出此VM可执行的代码。从安全性的角度来讲,相比单纯的Javascript混淆器而言,其强度大大的增长了,而相比于Flash的C/C++扩展方式来讲,其是将来的发展方向,并现已被主流的浏览器实现。
能够编写生成WebAssembly的语言及工具链很是多,咱们使用C/C++及其Emscripten做为示范编写一个简单的签名模块进行体验。
#include <string>
#include <emscripten.h>
#include <emscripten/bind.h>
#include "md5.h"
#define SALTKEY "md5 salt key"
std::string sign(std::string str){
return md5(str + string(SALTKEY));
}
// 此处导出sign方法供Javascript外部环境使用
EMSCRIPTEN_BIND(my_module){
emscripten::function("sign", &sign);
}
复制代码
接着,咱们使用emscripten编译咱们的C++代码,获得对应的生成文件。
em++ -std=c++11 -Oz --bind \
-I ./md5 ./md5/md5.cpp ./sign.cpp \
-o ./sign.js
复制代码
最后,咱们引入生成sign.js文件,而后进行调用。
<body>
<script src="./sign.js"></script>
<script> // output: 0b57e921e8f28593d1c8290abed09ab2 Module.sign("This is a test string"); </script>
</body>
复制代码
目前看起来WebAssembly是目前最理想的前端核心代码保护的方案了,咱们可使用C/C++编写相关的代码,使用Emscripten相关工具链编译为asm.js和wasm,根据不一样的浏览器的支持状况选择使用asm.js仍是wasm。而且对于PC端IE10如下的浏览器,咱们还能够经过CrossBridge复用其C/C++代码,产出对应的Flash目标代码,从而达到很是好的浏览器兼容性。
然而使用asm.js/wasm后对于前端核心代码的保护就能够高枕无忧了么?因为asm.js以及wasm的标准规范都是彻底公开的,所以对于asm.js/wasm标准实现良好反编译器来讲,彻底能够尽量的产出阅读性较强的代码从而分析出其中的核心算法代码。但幸运的是,目前做者还暂时没有找到实现良好的asm.js/wasm反编译器,所以我暂时认为使用此种方法在保护前端核心代码的安全性上已经可堪重用了。
做者在工做当中常常性会编写前端核心相关的代码,而且这些代码大部分与通讯相关,例如AJAX的请求数据的加解密,WebSocket协议数据的加解密等。对于这部分工做,做者一般都会使用上面介绍的asm.js/wasm加CrossBridge技术方案进行解决。这套方案目前看来至关不错,可是仍然存在几个比较大的问题:
所以咱们花费两周时间编写一套基于asm.js/wasm更好的前端核心代码保护方案:SecurityWorker。
SecurityWorker的目标至关简单:可以尽量温馨的编写具备极强安全强度的核心算法模块。其拆分下来实际上须要知足如下8点:
接下来咱们会逐步讲解SecurityWorker如何达成这些目标并详细介绍其原理,供你们参考改进。
如何在WebAssembly基础上提高安全性?回想以前咱们的介绍,WebAssembly在安全性上一个比较脆弱的点在于WebAssembly标准规范的公开,若是咱们在WebAssembly之上再建立一个私有独立的VM是否是能够解决这个问题呢?答案是确定的,所以咱们首要解决的问题是如何在WebAssembly之上创建一个Javascript的独立VM。这对于WebAssembly是垂手可得的,有很是多的项目提供了参考,例如基于SpiderMonkey编译的 js.js 项目。但咱们并无考虑使用SpiderMonkey,由于其产出的wasm代码达到了50M,在Web这样代码体积大小敏感的环境基本不具备实际使用价值。但好在ECMAScirpt相关的嵌入式引擎很是之多:
通过比较选择,咱们选择了duktape做为咱们基础的VM,咱们的执行流程变成了以下图所示:
固然,从图中咱们能够看到整个过程实际上会有一个比较大的风险点,因为咱们的代码是经过字符串加密的方式嵌入到C/C++中进行编译的,所以在执行过程当中,咱们是能在内存的某一个运行时期等待代码解密完成后拿到核心代码的,以下图所示:
如何解决这个问题?咱们的解决思路是将Javascript变成另外一种表现形式,也就是咱们常见的opcode,例如假设咱们有这样的代码:
1 + 2;
复制代码
咱们会将其转变相似汇编指令的形式:
SWVM_PUSH_L 1 # 将1值压入栈中
SWVM_PUSH_L 2 # 将2值压入栈中
SWVM_ADD # 对值进行相加,并将结果压入栈中
复制代码
最后咱们将编译获得的opcode bytes按照uint8数组的方式嵌入到C/C++中,而后进行总体编译,如图所示:
整个过程当中,因为咱们的opcode设计是私有不公开的,而且已经不存在明文的Javascript代码了,所以安全性获得了极大的提高。如此这样咱们解决了目标中的#一、#二、#4。但Javascript已经被从新组织为opcode了,那么如何保证目标中的#8呢?解决方式很简单,咱们在Javascript编译为opcode的关键步骤上附带了相关的信息,使得代码执行出错后,可以根据相关信息进行准确的报错。与此同时,咱们精简了opcode的设计,使得生成的opcode体积小于原有的Javascript代码。
duktape除了语言实现和部分标准库外并不还有一些外围的API,例如AJAX/WebSocket等,考虑到使用的便捷性以及更容易被前端开发者接收并使用,咱们为duktape实现了部分的WebWorker环境的API,包括了Websocket/Console/Ajax等,并与Emscripten提供的Fetch/WebSocket等实现结合获得了SecurityWorker VM。
那么最后的问题是咱们如何减少最终生成的asm.js/wasm代码的体积大小?在不进行任何处理的时候,咱们的生成代码因为包含了duktape以及诸多外围API的实现,即便一个Hello World的代码gzip后也会有340kb左右的大小。为了解决这个问题,咱们编写了SecurityWorker Loader,将生成代码进行处理后与SecurityWorker Loader的实现一块儿编译获得最终的文件。在代码运行时,SecurityWorker Loader会对须要运行的代码进行释放而后再进行动态执行。如此一来,咱们将原有的代码体积从原有gzip也会有340kb左右的大小下降到了180kb左右。
SecurityWorker解决了以前方案的许多问题,但其一样不是最完美的方案,因为咱们在WebAssembly上又建立了一个VM,所以当你的应用对于体积敏感或是要求极高的执行效率时,SecurityWorker就不知足你的要求了。固然SecurityWorker能够应用多种优化手段在当前基础上再大幅度的缩减体积大小以及提升执行效率,但因为其已经达到咱们本身现有的需求和目标,所以目前暂时没有提高的相关计划。
咱们经过回顾目前主流的前端核心保护方案,并详细介绍了基于以前方案作的提高方案SecurityWorker,相信你们对整个前端核心算法保护的技术方案已经有一个比较清晰的认识了。固然,对于安全的追求没有终途,SecurityWorker也不是最终完美的方案,但愿本文的相关介绍能让更多人参与到WebAssembly及前端安全领域中来,让Web变得更好。
方老湿,您学会了么?