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 (node instanceof UglifyJS.AST_Number) { //查找需要修改的叶子节点

        node.value = '0x' + Number(node.value).toString(16);

        return node; //返回一个新的叶子节点 替换原来的叶子节点

    };

});

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混淆在对抗中必不可少

    · 实现一款自己的混淆器并没有那么难

    · 混淆器对性能的影响是可控的

js 安全的更多相关文章

  1. Vue.js 和 MVVM 小细节

    MVVM 是Model-View-ViewModel 的缩写,它是一种基于前端开发的架构模式,其核心是提供对View 和 ViewModel 的双向数据绑定,这使得ViewModel 的状态改变可以自 ...

  2. js学习笔记:操作iframe

    iframe可以说是比较老得话题了,而且网上也基本上在说少用iframe,其原因大致为:堵塞页面加载.安全问题.兼容性问题.搜索引擎抓取不到等等,不过相对于这些缺点,iframe的优点更牛,跨域请求. ...

  3. js学习笔记:webpack基础入门(一)

    之前听说过webpack,今天想正式的接触一下,先跟着webpack的官方用户指南走: 在这里有: 如何安装webpack 如何使用webpack 如何使用loader 如何使用webpack的开发者 ...

  4. JS调用Android、Ios原生控件

    在上一篇博客中已经和大家聊了,关于JS与Android.Ios原生控件之间相互通信的详细代码实现,今天我们一起聊一下JS调用Android.Ios通信的相同点和不同点,以便帮助我们在进行混合式开发时, ...

  5. jquery和Js的区别和基础操作

    jqery的语法和js的语法一样,算是把js升级了一下,这两种语法可以一起使用,只不过是用jqery更加方便 一个页面想要使用jqery的话,先要引入一下jqery包,jqery包从网上下一个就可以, ...

  6. 利用snowfall.jquery.js实现爱心满屏飞

    小颖在上一篇一步一步教你用CSS画爱心中已经分享一种画爱心的方法,这次再分享一种方法用css画爱心,并利用snowfall.jquery.js实现爱心满屏飞的效果. 第一步: 利用伪元素before和 ...

  7. node.js学习(三)简单的node程序&&模块简单使用&&commonJS规范&&深入理解模块原理

    一.一个简单的node程序 1.新建一个txt文件 2.修改后缀 修改之后会弹出这个,点击"是" 3.运行test.js 源文件 使用node.js运行之后的. 如果该路径下没有该 ...

  8. JS正则表达式常用总结

    正则表达式的创建 JS正则表达式的创建有两种方式: new RegExp() 和 直接字面量. //使用RegExp对象创建 var regObj = new RegExp("(^\\s+) ...

  9. 干货分享:让你分分钟学会 JS 闭包

    闭包,是 Javascript 比较重要的一个概念,对于初学者来讲,闭包是一个特别抽象的概念,特别是ECMA规范给的定义,如果没有实战经验,很难从定义去理解它.因此,本文不会对闭包的概念进行大篇幅描述 ...

  10. JS核心系列:理解 new 的运行机制

    和其他高级语言一样 javascript 中也有 new 运算符,我们知道 new 运算符是用来实例化一个类,从而在内存中分配一个实例对象. 但在 javascript 中,万物皆对象,为什么还要通过 ...

随机推荐

  1. Win8 Metro中文件读写删除与复制操作

    Win8Metro中,我们不能在向以前那样调用WIN32的API函数来进行文件操作,因此,下面就来介绍一下Win8 Metro中文件的读写操作. 1 Windows 8 Metro Style App ...

  2. 记一次MySQL找回用户数据

    事情经过 有天,我们公司外区的一个销售C说他8月3号以前的工作流记录找不到了.问清缘由,原来是更新了微信号(我们公司的工作流是基于企业微信开发的).经过分析,微信号和流程数据并没什么关系,所以初步得出 ...

  3. 关于jquery的on,你怎么绑定就怎么解除

    打开jquery3.0一看,竟然把一大半的时间绑定的东西都废弃了 基本上绑定时间可以统一成了on,不管你是不是动态进来的dom节点 live我们知道1.7的时候就已经废弃了,还在觉得live很好用的同 ...

  4. php求斐波那契数列

    <?php function feibonaqi(){ //参数$num表示为第$num个数之前的所有斐波那契数列 $arr = array(); //定义一个空变量用来存放斐波那契数列的数组 ...

  5. kvm 虚拟化 SMP(对称多处理器)介绍及配置

    一.SMP(对称多处理器)介绍 1)SMP简介 在计算机技术很普及和日益发达的今天,以 Intel.IBM 为代表的一些大公司推动着中央处理器(CPU)技术的飞速发展和更新换代.在如今计算机系统中,多 ...

  6. Drupal启动阶段之二:页面缓存

    页面缓存是什么意思?有些页面浏览量非常大,而且与状态无关,这类页面就可以使用页面缓存技术.在页面第一次请求完毕以后,将响应结果保存起来.下一次再请求同一页面时,就不需要从头到尾再执行一遍,只需要将第一 ...

  7. Layui 弹出层组件——layer的模块化开发实例应用

    Layui 弹出层组件——layer的模块化开发实例应用 1.首先在package.json中引入layer组件依赖 2.在源码中应用这个依赖 3.在源码中编写代码应用此组件 4.效果验证:点击日历上 ...

  8. linux 重启网卡的方法

    http://blog.163.com/drzxqing@126/blog/static/59351445201052392516841/

  9. 企业信息系统集成框架(设计思路)C++模式

    设计要求: 1.企业信息系统框架.第三方产品通过接口层进行分层. 2.企业信息系统框架如何自由的继承第三方产品:通过一个抽象类.(软件设计要求:模块要求松,接口要求紧). 设计步骤: 1.报文的接受与 ...

  10. 爬虫1:get请求的翻页及思考

    刚开始接触爬虫,理解还不透彻,说一些初始阶段的想法{1.因为get请求的方式(请求体无数据,不能通过Request.add_data()函数来添加数据,实现对网址翻页:需要直接对网址进行操作来实现翻页 ...