代码保护——可信前端之路 _前端开发者

前端开发者web前端开发
https://www.rokub.com
可信前端之路 – 代码保护0x00 前言在信息安全领域, 可信系统( Trusted system) 是一个让人心动的目标, 它指的是一个通过实施特定的安全策略而达到一定可信程度的系统。
在计算机中, 可信平台模块( Trusted Platform Module, TPM) 已经投入使用, 它符合可信赖计算组织( Trusted Computing Group, TCG) 制定的TPM规范, 是为了实现可信系统目标的而打造的一款安全芯片。
作为可信系统的信任根, TPM是可信计算的核心模块, 为计算机安全提供了强有力的保障。
而在我们的web系统中, 想打造一个可信系统似乎是个伪命题,” 永远不要相信客户端的输入” 是基本的安全准则。
实际上, 在可信系统中的可信也并不是说真的是绝对安全, 维基上对其的解释为:“ 可信的”( Trusted) 未必意味着对用户而言是“ 值得信赖的”( Trustworthy)。
确切而言, 它意味着可以充分相信其行为会更全面地遵循设计, 而执行设计者和软件编写者所禁止的行为的概率很低。
从这个角度讲, 我们把其当做一个美好的愿景, 我们希望能够构造一个web系统中的TPM, 可以把恶意行为控制在一定的概率之内, 从而实现一个相对可信的web系统。
0x01 可信前端在可信系统中, TPM的一个重要作用就是鉴别消息来源的真实性, 保障终端的可信。
web系统中, 我们的消息来源就是用户。
随着撞库、 恶意注册、 薅羊毛等产业的蓬勃发展, 在越来越多的场景我们需要鉴别请求数据是否来自真实的用户, 保护真实用户的数据安全。
所以想要构造一个web系统中的TPM, 首要问题就是需要保证输入数据安全, 打造一个相对可信的前端环境。
但是由于web的开放特性, 前端作为数据采集的最前线, js代码始终暴露在外, 在这种情况下, 防止恶意伪造请求变得非常困难, 可信前端也就成了无稽之谈。
在反复对抗中, 代码保护也就是通常意义上的js代码混淆的重要性逐渐彰显出来。
今天我就想和大家聊一聊js混淆的问题。
1、 为什么需要js混淆显而易见, 是为了保护我们的前端代码逻辑。
在web系统发展早期, js在web系统中承担的职责并不多, 只是简单的提交表单, js文件非常简单, 也不需要任何的保护。
随着js文件体积的增大, 为了缩小js体积, 加快http传输速度, 开始出现了很多对js的压缩工具, 比如 uglify、 compressor、 clouser。。。
它们的工作主要是· 合并多个js文件· 去除js代码里面的空格和换行· 压缩js里面的变量名· 剔除掉注释压缩后的代码虽然压缩工具出发点都是为了减少js文件的体积, 但是人们发现压缩替换后的代码已经比源代码可读性差了很多, 间接起到了代码保护的作用, 于是压缩js文件成为了前端发布的标配之一。
但是后来市面上主流浏览器chrome、 Firefox等都提供了js格式化的功能, 能够很快的把压缩后的js美化, 再加上现代浏览器强大的debug功能, 单纯压缩过的js代码对于真正怀有恶意的人, 已经不能起到很好的防御工作, 出现了” 防君子不防小人” 的尴尬局面。
chrome开发者工具格式化之后的代码而在web应用越来越丰富的今天, 伴随着浏览器性能和网速的提高, js承载了更多的工作, 不少后端逻辑都在向前端转移, 与此同时也让更多的不法分子有机可乘。
在web模型中, js往往是不法分子的第一个突破口。
知晓了前端逻辑, 不法分子可以模拟成一个正常的用户来实施自己的恶意行为。
所以, 在很多登录、 注册、 支付、 交易等等页面中, 关键业务和风控系统依赖的js都不希望被人轻易的破解, js混淆应运而生。
2、 js混淆是不是纸老虎这是一个老生常谈的问题。
实际上, 代码混淆早就不是一个新鲜的名词, 在桌面软件时代, 大多数的软件都会进行代码混淆、 加壳等手段来保护自己的代码。
Java和.NET都有对应的混淆器。
黑客们对这个当然也不陌生, 许多病毒程序为了反查杀, 也会进行高度的混淆。
只不过由于js是动态脚本语言, 在http中传输的就是源代码, 逆向起来要比打包编译后的软件简单很多, 很多人因此觉得混淆是多此一举。
.NET混淆器dotFuscator其实正是因为js传输的就是源代码, 我们才需要进行混淆, 暴露在外的代码没有绝对的安全, 但是在对抗中, 精心设计的混淆代码能够给破坏者带来不小的麻烦, 也能够为防守者争取更多的时间, 相对于破解来说, 混淆器规则的更替成本要小得多, 在高强度的攻防中, 可以大大增加破解者的工作量, 起到防御作用。
从这个角度来讲, 关键代码进行混淆是必不可少的步骤。
3、 如何进行js混淆js混淆器大致有两种: ·通过正则替换实现的混淆器· 通过语法树替换实现的混淆器第一种实现成本低, 但是效果也一般, 适合对混淆要求不高的场景。
第二种实现成本较高, 但是更灵活, 而且更安全, 更适合对抗场景, 我这里主要讲一下第二种。
基于语法层面的混淆器其实类似于编译器, 基本原理和编译器类似, 我们先对编译器做一些基本的介绍。
名词解释token: 词法单元, 也有叫词法记号的, 词法分析器的产物, 文本流被分割后的最小单位。
AST: 抽象语法树, 语法分析器的产物, 是源代码的抽象语法结构的树状表现形式。
编译器VS混淆器编译器工作流程简单的说, 当我们读入一段字符串文本( source code), 词法分析器会把它拆成一个一个小的单位( token), 比如数字1 是一个token, 字符串’ abc’ 是一个token等等。
接下来语法分析器会把这些单位组成一颗树状结构( AST), 这个树状结构就代表了token们的组成关系。
比如 1 + 2 就会展示成一棵加法树, 左右子节点分别是token– 1 和token– 2, 中间token表示加法。
编译器根据生成的AST转换到中间代码, 最终转换成机器代码。
对编译器更多细节感兴趣的同学可以移步龙书: 编译原理混淆器工作流程编译器需要把源代码编译成中间代码或者机器码, 而我们的混淆器输出其实还是js。
所以我们从语法分析之后往下的步骤并不需要。
想想我们的目标是什么, 是修改原有的js代码结构, 在这里面这个结构对应的是什么呢? 就是AST。
任何一段正确的js代码一定可以组成一颗AST, 同样, 因为AST表示了各个token的逻辑关系, 我们也可以通过AST反过来生成一段js代码。
所以, 你只需要构造出一颗AST, 就能生成任何js代码! 混淆过程如上右图所示通过修改AST生成一个新的AST, 新的AST就可以对应新的JavaScript代码。
规则设计知道了大致的混淆流程, 最重要的环节就是设计规则。
我们上面说了, 我们需要生成新的AST结构意味着会生成和源代码不一样的js代码, 但是我们的混淆是不能破坏原有代码的执行结果的, 所以混淆规则必须保证是在不破坏代码执行结果的情况下, 让代码变得更难以阅读。
具体的混淆规则各位可以自行根据需求设计, 比如拆分字符串、 拆分数组, 增加废代码等等。
参考: 提供商业混淆服务的jscramble的混淆规则实现很多人看到这里就望而却步, 因为词法分析和文法分析对编译原理要求较高。
其实这些现在都有工具可以帮助搞定了, 借助工具, 我们可以直接进行最后一步, 对AST的修改。
市面上JavaScript词法和文法分析器有很多, 比如其实v8就是一个, 还有mozilla的SpiderMonkey, 知名的esprima等等, 我这里要推荐的是uglify, 一个基于nodejs的解析器。
它具有以下功能:· parser, 把 JavaScript 代码解析成抽象语法树· code generator, 通过抽象语法树生成代码· scope analyzer, 分析变量定义的工具· tree walker, 遍历树节点· tree transformer, 改变树节点对比下我上面给出的混淆器设计的图, 发现其实只需要修改语法树 这一步自己完成。
实例说了这么多, 可能很多人还是一头雾水, 为了帮助各位理解, 我准备了一个简单的例子, 假设我们的混淆规则是想把
var a = 1;
中的数字1换成16进制, 我们该如何设计混淆器呢。
首先对源代码做词法分析和语法分析, uglify一个方法就搞定了, 生成一颗语法树, 我们需要做的就是找到语法树中的数字然后修改成16进制的结果, 如下图所示: 实例代码: var UglifyJS = require(“uglify-js”);
var code = “var a = 1;”;
var toplevel = UglifyJS.parse(code);
toplevel就是语法树
var transformer = new UglifyJS.TreeTransformer(function (node) {
if (nodeinstanceofUglifyJS.AST_Number) {
查找需要修改的叶子节点node.value =’0x’+Number(node.value).toString(16);
returnnode;
返回一个新的叶子节点替换原来的叶子节点
};
});
toplevel.transform(transformer);
遍历AST树
var ncode = toplevel.print_to_string();
从AST还原成字符串 console.log(ncode);
var a = 0x1;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
var UglifyJS = require(“uglify-js”);
var code = “var a = 1;”;
var toplevel = UglifyJS.parse(code);
toplevel就是语法树
var transformer = new UglifyJS.TreeTransformer(function (node) {
if (nodeinstanceofUglifyJS.AST_Number) {
查找需要修改的叶子节点node.value =’0x’+Number(node.value).toString(16);
returnnode;
返回一个新的叶子节点替换原来的叶子节点
};
});
toplevel.transform(transformer);
遍历AST树
var ncode = toplevel.print_to_string();
从AST还原成字符串 console.log(ncode);
var a = 0x1;
上面的代码很简单, 首先通过parse方法构建语法树, 然后通过TreeTransformer遍历语法树, 当遇到节点属于UglifyJS.AST_Number类型( 所有的AST类型见ast), 这个token具有一个属性 value 保存着数字类型的具体值, 我们将其改成16进制表示, 然后
return node 就会用新的节点代替原来的节点。
效果展示贴一个我自己设计的混淆器混淆前后代码: 4、 混淆对性能的影响由于增加了废代码, 改变了原有的AST, 混淆对性能肯定会造成一定的影响, 但是我们可以通过规则来控制影响的大小。· 减少循环混淆, 循环太多会直接影响代码执行效率· 避免过多的字符串拼接, 因为字符串拼接在低版本IE下面会有性能问题· 控制代码体积, 在插入废代码时应该控制插入比例, 文件过大会给网络请求和代码执行都带来压力我们通过一定的规则完全可以把性能影响控制在一个合理的范围内, 实际上, 有一些混淆规则反而会加快代码的执行, 比如变量名和属性名的压缩混淆, 会减小文件体积, 比如对全局变量的复制, 会减少作用域的查找等等。
在现代浏览器中, 混淆对代码的影响越来越小, 我们只需要注意合理的混淆规则, 完全可以放心的使用混淆。
5、 混淆的安全性混淆的目的是保护代码, 但是如果因为混淆影响了正常功能就舍本逐末了。
由于混淆后的AST已经和原AST完全不同了, 但是混淆后文件的和原文件执行结果必须一样, 如何保证既兼顾了混淆强度, 又不破坏代码执行呢? 高覆盖的测试必不可少:· 对自己的混淆器写详尽的单元测试· 对混淆的目标代码做高覆盖的功能测试, 保证混淆前后代码执行结果完全一样· 多样本测试, 可以混淆单元测试已经完备了的类库, 比如混淆 jquery、 AngularJS 等, 然后拿混淆后的代码去跑它们的单元测试, 保证和混淆前执行结果完全一样0x02 总结· 可信web系统是我们的愿景· 可信web系统离不开可信的前端环境· js混淆在对抗中必不可少· 实现一款自己的混淆器并没有那么难· 混淆器对性能的影响是可控的0x03
参考https:// en.wikipedia.orgwikiTrusted_Platform_Module
https:// en.wikipedia.orgwikiTrusted_system
http:// lisperator.netuglifyjs
http://esprima.org
前端开发者丨WEB前端开发
赞(2)
前端开发者 » 代码保护——可信前端之路 _前端开发者
64K

评论 抢沙发

评论前必须登录!