极客时间已完结课程限时免费阅读

加餐2 | 前端安全:如何打造一个可信的前端环境?

加餐2 | 前端安全:如何打造一个可信的前端环境?-极客时间

加餐2 | 前端安全:如何打造一个可信的前端环境?

讲述:何为舟

时长14:53大小11.92M

你好,我是何为舟,欢迎来到安全专栏的第二次加餐时间。
前端的安全性一直是我们在考虑安全问题时,没有办法绕过的关键问题。今天,我就来和你聊一聊如何保护前端的安全性。
我们先来看一个攻击事件。2017 年,12306 网站被曝出有“买下铺”的功能。我们都有过买票的经历,当我们在 12306 上买卧铺的时候,是没法选择上铺、中铺还是下铺的。但是,有人去分析了 12306 的前端代码,发现里面其实包含了选铺位的功能,只是默认为随机,没有展示出来。所以,有人通过篡改前端代码,就将这个功能开放出来了。
一旦黑客能够完全摸清楚应用的前端代码,就能够任意地篡改前端的逻辑,实现带有想要功能的前端应用了。
如果说 12306 的例子,还不足以让你对前端安全产生警惕的话,你可以想一想,我们在网上看到的各种所谓的“破解版”软件,其实都是人为修改了应用的前端认证功能,从而不需要认证就可以正常使用。
除了篡改前端代码,黑客还可以通过对前后端接口的调用过程进行分析,复刻出一个自己的前端应用。在黑客复刻的前端应用中,所有的接口认证和加密都合法,只是调用的顺序完全由黑客掌控。粉丝圈比较流行的各类明星应援工具,其实都是基于这个原理实现的:黑客通过分析微博客户端的接口,自己打包了一个前端应用,实现了一键关注、点赞等功能。因为这些接口都是合法的,所以后端人员很难分辨出这些请求是来自于正规的应用,还是黑客自己实现的应用。
针对前端的攻击可以说是“防不胜防”,这让后端没有办法信任前端的环境,甚至没有办法信任前端发起的请求和上传的数据,极大地影响了公司和应用的正常发展。那么,我们应该通过什么方法来保障前端的可信呢?

什么是混淆技术?

要解决这个问题,我们可以先想一下黑客攻击前端的过程:黑客通过分析前端代码,来篡改前端逻辑,实现带有想要功能的前端应用。那有没有一种方法,无法让黑客在前端代码中分析出有效信息呢?答案就是混淆
在理想状态下,我们混淆了前端代码之后,不仅能让黑客无法篡改前端代码,还能保证即使黑客成功篡改代码,那么篡改后的前端代码依然不可用。同时,黑客无法获得前端的接口密钥和签名等信息,也就无法伪造正常的前端应用去发起请求了。
我们知道,安全中通常不存在理想状态。我们最需要做的,就是不断地升级对抗,来接近这个理想的目标。
刚才我们说的是混淆技术可以实现的结果,那混淆技术究竟是什么呢?在不同的语言和环境(如:Android 、iOS 和 Web)中,混淆技术都是相对独立的。尽管混淆技术相对独立,但我还是希望,你可以通过理解一门语言的混淆技术和思路,做到“一通百通”。我也希望能够更好地启发你去思考,如何去做好前端安全。接下来,我就以 JavaScript 为例,带你梳理混淆的常见技术和思路。

1. 清晰代码无序化

在实际工作中,开发人员总是会要求自己写出清晰简洁的代码。但是,这也为黑客的代码分析提供了便利。因此,混淆的第一步,一定是想办法让我们的 JavaScript 代码变得“难看”,也就是将整洁的代码变得无序。
有什么办法能让代码变得“难看”呢?我这里通过一个例子来具体解释一下,你就能明白了。
我们先来看一段代码。
function obfuscate() {
console.log("I'm obfuscator!");
}
obfuscate();
我们一眼就能够看出这段代码的逻辑:有一个 obfucate 方法,这个方法会打出一行日志,日志内容为“I’m obfuscator!”。
在 JavaScript 中,空格、换行、缩进这些内容,只是为了让代码看起来更清晰。所以,这些对代码没有影响,只是便于开发人员查看的内容,完全可以去除。这样一来,这段代码我们就可以改成下面这样:
function obfuscate(){console['log']('I'm obfuscator!');}obfuscate();
把代码压缩成一行后,黑客想要阅读就已经比较吃力。在此基础上,我们还可以让它变得更“难看”。实际上,JavaScript 中的方法名和变量名也不影响逻辑执行,只是开发人员用来表示方法和变量的含义,完全可以用没有意义的随机字符替代。随机字符代替后的效果如下:
function _0xc648a(){console['log']('I\x27m\x20obfuscator!');}_0xc648a();

2. 简单逻辑复杂化

对于上面这段无序化后的代码,只要黑客稍微花点心思去阅读,再配合一些 JavaScript 格式化的工具,也能够弄明白它的逻辑。归根结底还是因为这段代码“太简单了”。那么,我们是不是能够让原本简单的代码变得复杂呢?实现方法有很多种,我们先来看最简单的一种:加入无意义的代码。
我们还是以最开始的简单代码为例。为了方便你查看,我把前面那段简单代码重新贴在这里。
function obfuscate() {
console.log("I'm obfuscator!");
}
obfuscate();
在这段代码中,本来输出的日志就是一个固定的字符串“I’m obfuscator!”。但是,我们可以先将这段字符串放在一个字典中,然后再通过字典去获取字符串。修改后的效果如下:
function obfuscate() {
var _0x16df9a = { 'HXGCi': 'I\x27m\x20obfuscator!' };
console['log'](_0x16df9a['HXGCi']);
}
obfuscate();
这就是通过字典等形式,将常量变成变量的混淆方法。在此基础上,我们还可以加入一些无意义的 switch、if 和 while 语句,进一步将代码复杂化。
除了加入一些无意义的代码,我们还可以加入一些不会被执行的代码,让混淆的结果更有威慑力。比如下面这段代码:
(function (_0x2177d9, _0x1442cc) {
var _0xb84613 = function (_0x5a2b5f) {
while (--_0x5a2b5f) {
_0x2177d9['push'](_0x2177d9['shift']());
}
};
_0xb84613(++_0x1442cc);
}(_0x1808, 0x1ea));
function obfuscate() {
console['log']('I\x27m\x20obfuscator!');
}
obfuscate();
在这段代码中,中间的 function (_0x2177d9, _0x1442cc) 就不会被执行,它的目的仅仅是让代码看起来更复杂而已。

3. 固定字符动态化

在我们前面说的这几个混淆代码的例子中,关键字符串“I’m obfuscator!”始终都存在。如果黑客关心的只是这个字符串,那它通过搜索就可以很快定位到。也就是说,通过前面几种方式混淆的前端代码,其中的接口、密钥和签名等信息,黑客还是很容易就可以获取到。
既然关键字符串“存在”于代码中就不安全,那有没有方法可以让关键字符串“消失”呢?我们可以通过加入一些逻辑,让这些关键字符串只有在实际运行的时候,才会被计算出来。
最简单、最直接的思路就是,我们可以将关键字符串改成多个字符串拼接的形式。效果如下:
function obfuscate() {
console['log']('I\x27m\x20o' + 'bfusc' + 'ator!');
}
obfuscate();
通过这样改写的方式,黑客就没有办法通过搜索功能,找到“I’m obfuscator!”的位置了。
但是,这种简单分割字符串的方式很容易被发现。所以,我们可以将这些字符串从它原本的位置拿出来,通过更复杂的方法(如:数组的引用、方法的调用等)来获取。效果如下:
var _0x5e96 = [
'bfusc',
'ator!',
'log',
'I\x27m\x20o'
];
(function (_0x520fe6, _0x366376) {
var _0x38fe5f = function (_0x456d44) {
while (--_0x456d44) {
_0x520fe6['push'](_0x520fe6['shift']());
}
};
_0x38fe5f(++_0x366376);
}(_0x5e96, 0x15e));
var _0x40ca = function (_0x520fe6, _0x366376) {
_0x520fe6 = _0x520fe6 - 0x0;
var _0x38fe5f = _0x5e96[_0x520fe6];
return _0x38fe5f;
};
function obfuscate() {
console[_0x40ca('0x0')](_0x40ca('0x1') + _0x40ca('0x2') + _0x40ca('0x3'));
}
obfuscate();
这样一来,黑客想要快速找到 _0x40ca(‘0x1’) 具体指什么,就需要花上一番功夫了。

4. 反调试

前面 3 种技术都是直接对源码进行混淆。但是,大多数情况下,黑客在分析代码的时候,不是直接阅读源码,而是通过调试的方法在 JavaScript 代码运行过程中,获取实际的代码执行方向以及变量的值。因此,为了保护前端安全,我们要采用反调试技术。在 JavaScript 中,主要有两种方法可以对抗调试:域名锁定和无限断点。下面,我们一一来看。
第一种是域名锁定。
当黑客来想要分析一个网页的时候,通常会将代码下载下来放到本地运行。但是,我们更希望这个分析过程仍然发生在当前的域名下,这样我们就能够通过请求去分析黑客到底干了什么。因此,我们可以在 JavaScript 中加入一段域名判断的逻辑。这样一来,当 JavaScript 运行的环境是 localhost(本地主机)域名,或者其他未知的域名时,JavaScript 就会产生错误,黑客就无法正常运行下载后的 JavaScript 文件了。
我来举个例子。在 JavaScript 中,我们可以通过 window.location.host 获取当前域名,然后判断这个域名是否等于网站的域名,比如 server.com。如果不等于的话, 说明 JavaScript 不是通过正常访问域名的形式执行的。因此,JavaScript 会直接返回,不执行后续的逻辑。代码如下:
function obfuscate() {
if(window.location.host != 'server.com'){
return;
}
console.log("I'm obfuscator!");
}
obfuscate();
第二种是无线断点。
在调式技术中,我们最常用到的功能就是断点。通过设置断点,我们可以让程序停留在某一个代码或者指令上,方便查看停留的这个时刻中各个变量的具体值是什么。
在 JavaScript 中,debugger 指令就是用来添加断点的。所以,在反调试的时候,我们可以在 JavaScript 中开启一个单独的线程,来循环调用 debugger。这样一来,如果黑客进入到调试模式,就会不断地停滞在无意义的断点处,从而无法正常调试。在正常运行 JavaScript 的时候,debugger 不会生效,也就不会影响用户的正常使用。
除此之外,针对提供了额外的 JavaScript 接口的浏览器(比如 Chrome),我们可以通过在 JavaScript 中检测开发者工具是否开启等特征,来实现反调试。开发者工具是开发人员在调试过程中必须使用的工具,一旦开启,基本就代表已经进入调试状态了。因此,我们可以在检测到开发者工具开启的时候,不去执行正常的 JavaScript 逻辑,这样就能够起到反调试的作用了。
好了,说完了这 4 种混淆技术,我要提醒你一点。这些混淆技术不是独立使用的,而应该是组合使用的。完整的混淆流程应该是这样的:首先,我们可以在原有的 JavaScript 代码中加入反调试的逻辑,然后通过简单逻辑复杂化和固定字符动态化的方法,隐藏原有的逻辑和反调试的逻辑。最后,通过清晰代码无序化,将所有的额外信息进行剔除,最终将代码变成了压缩成一行的 JavaScript 文件。

混淆技术有什么负面影响?

尽管混淆技术是保护前端安全的重要技术,但混淆技术改变了前端代码,就不可避免会影响前端的功能。这也是混淆始终达不到理想状态的一个主要原因。对于 JavaScript 的混淆来说,它的负面影响主要包括三个方面:增加体积、影响性能和无法分析报错。
混淆带来的最直接影响就是增加代码体积。在固定字符动态化的例子中,原本简单的 4 行代码经过混淆之后,变成了几十行。如果应用更复杂一些,一个几 KB 的 JavaScript 文件经过混淆之后变成几百 KB,也是很正常的事情。这样一来,用户网络加载一个大型的 JavaScript 文件,所面对的消耗、加载时的延迟以及运行时的内存等都会有明显增长。
除了增加代码体积以外,混淆还会增加额外的执行逻辑,降低代码执行的速度影响性能。比如说,console.log 本来只是一个简单的指令,但是在混淆之后,JavaScript 需要对它进行数据的取值、索引的计算以及字符串的拼接等操作。这样一来,混淆后的代码执行速度必然会下降。
而且这些无用的操作,事实上是可以无限添加的。因此,在混淆的时候,如何把控复杂化的程度,是我们需要谨慎考量和测试的。
还有一点是不可避免的,那就是混淆后的代码,不仅黑客无法阅读,你其实也无法阅读。在混淆之前,如果前端出现错误,我们可以直接通过错误信息定位错误;但是在混淆之后,错误信息会变得“很难看”,而且代码只会剩下一行,我们也就无法定位了。
你还需要注意一点:混淆不可能让代码变得完全不可读。因为你的代码最终需要执行在用户终端,而执行的条件就是终端能够读懂代码。以 JavaScript 为例,黑客完全可以自己定义一个浏览器来执行 JavaScript 代码。这样一来,尽管黑客没办法直接阅读 JavaScript 文件,但仍然可以通过浏览器执行的指令集和内存环境来进行分析。

总结

好了,今天的加餐就到这里。
我们主要以 JavaScript 为例,梳理了混淆的主要技术和思路。虽然通过混淆,我们能大大增加黑客分析前端代码的难度,但是,混淆同样会给我们的正常工作和应用的执行增加难度,带来负面影响。所以,我们在使用混淆技术的时候,必须要经过谨慎的考量和测试。

思考题

最后,还是给你留一道思考题。
我们知道,不同的语言和环境,其混淆的技术和思路都存在各自的特点。你可以试着分析一下,在你熟悉的语言和环境中,有哪些方式可以用来进行代码混淆?
欢迎留言和我分享你的思考和疑惑,也欢迎你把文章分享给你的朋友。我们下一讲再见!
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 2

提建议

上一篇
加餐1 | 数据安全:如何防止内部员工泄露商业机密?
下一篇
加餐3 | 职业发展:应聘安全工程师,我需要注意什么?
unpreview
 写留言

精选留言(10)

  • 岑小白
    2020-01-31
    😂 前端 debug 本来就是史诗级难度,这样混淆之后基本上自绝于 debug 了。保密性要求没那么高的项目一般也就 ugly 一下了。这样看来,安全也是需要极大成本的啊。

    作者回复: 也不完全是,混淆之后只是报错信息不可用了。但是你自己埋入的日志还是可以看到的,所以想要debug的话,只能依靠自己埋入足够充分的错误判断和日志了。

    2
  • leslie
    2020-02-07
    个人觉得有一个还不错的方式不过仅仅用在最关键的地方或场合:写2套代码或本地测试环境留注释完整的源代码,线上环境把相关注释全部祛除。线上出现问题时,问题放到测试环境调试,毕竟线上代码的修改必须经过测试环境测试才能提交生产环境;测试系统有完整注释的源代码便于测试和解决问题就能解决问题。 这是个人学完今天课程的一点扩展的想法:唯一的代价就是稍微多花点时间;不过能真正解决问题不再乎那几分钟的删除注释的时间。谢谢老师的分享。
    展开

    作者回复: 测试流程通常要求的是测试环境和线上环境代码一致,不然容易出现因混淆或其他因素的代码改动,导致测试环境正常,线上环境却出BUG的情况。当然,如果线上BUG能够稳定复现的花,其实还是可以通过在本地原始代码调试,去进行DEBUG的。不过我也经常遇到,线上的偶发BUG,不知道崩溃的原因,导致无法排查的情况。

    2
  • longsang
    2020-07-06
    看着老师说的无限debug,试了一下,可以通过如下的代码实现: <script> function tryDebugger() { try { if (('' + 0 / 0)) { debugger } } catch (err) {} } setInterval(tryDebugger, 4000) </script> 嗯,如果一打开控制台,就跳到debugger界面,想想还是很恶心的 但实际上,破解方法也非常简单: CTRL + F ’debugger' , 替换为 ‘’ 2333333
    展开

    作者回复: 加固做得好的js,都不支持下载运行,需要线上环境调试,改代码没那么简单~

    1
  • darktemple9
    2020-02-03
    webpack打包可以用uglify插件,问题是混淆强度怎么确定了,特别是业务看重体积和性能的情况下,这些策略怎么上呢?

    作者回复: 一般是先会定义一个标准,即应用的体积和性能最差能到什么程度,然后按照这个标准,去调整混淆的强度。大部分的混淆插件都能够去配置混淆的强度。

    1
  • Geek_145846
    2022-05-13
    混淆代码真的是杀敌100自损90的方法,有没有更好的方法呢
  • Bob
    2021-11-25
    代码自带混淆效果就不用这么麻烦了:P
  • 亚林
    2021-07-16
    iOS和Android混淆太难了
  • 皮特尔
    2020-05-24
    总结一下: 1. 清晰代码无序化; 2. 简单逻辑复杂化; 3. 固定字符动态化; 4. 反调试。 这些方法主要用于前端Javascript代码,其他平台,比如 iOS、Android,都有各自的反调试方法。
    展开
  • iLeGeND
    2020-01-31
    混淆具体是怎么操作的呢 不会是手动吧

    作者回复: 网上有很多公开的平台和工具可以帮你作混淆,搜索一下,直接使用就好了。

  • LEON
    2020-01-31
    您好,如果浏览器前端不支持JS脚本怎么办?只有JS脚本可以进行前端混淆吗?还有什么技术可以进行前端混淆?谢谢。

    作者回复: 1、浏览器不支持JS脚本,那就只能是静态HTML页面了。这种页面里面没有逻辑,把注释和隐藏信息去掉就行了。 2、每个语言都有各自的混淆技术,所以这里没提。比如iOS自带反编译工具,安卓有各类加固平台可以使用。

    共 2 条评论