【技术推荐】前端JS攻防对抗
对于攻击者,如何去绕过反爬虫手段,分析前端JS代码就成为了必经之路。那么JS如何不被破解,也成为了反爬虫的关键。
简介
网络爬虫——一种让网站维护人员长期头痛的存在。网站维护人员既要考虑为搜索引擎开方便之门,以便提升网站排名、广告引入等,又要应对恶意爬虫所带来的危害,如数据被非法获取,甚至出售。因此,爬虫和反爬虫一直是场旷日持久的战斗。
爬虫的开发从最初的简单脚本到 PhantomJs、selenium 再进化到 puppeteer、playwright 等,和浏览器结合越来越密切。
反爬虫的手段从 ua、Header 检测到 IP 频率检测再到网站重构、验证码、JS加密等,手段越来越多样。
下表是爬虫攻防手段发展一个简单的对比。
反爬虫的手段到现在已经成体系化了,访问令牌(身份认证)、验证码(滑动、逻辑、三维等)、行为&指纹检测(人机区分)、请求&响应加密等。所有这些功能的实现都是依靠前端JS代码,对于攻击者,如何去绕过反爬虫手段,分析前端JS代码就成为了必经之路。那么JS如何不被破解,也成为了反爬虫的关键。
本文只探讨JS如何防破解,其它反爬虫手段不展开讨论。
JS 防破解
JS 防破解主要客户分为两个部分:代码混淆和反调试 。
1 代码混淆
从代码布局、数据、控制三个方面入手,进行混淆。
(1)布局混淆
常见手段有无效代码删除,常量名、变量名、函数名等标识符混淆等。
- 无效代码删除
注释文本对于理解代码逻辑有很多帮助,生产环境需要删除。
调试信息对于开发者调试Bug有很大的帮助,生产环境需要删除。
无用函数和数据需要删除,避免攻击者能够猜到开发者意图,和垃圾代码添加不同。
缩进、换行符删除,减小代码体积,增加阅读难度。
- 标识符重命名
单字母。还可以是aa、a1等,需要注意避免作用域内标识符冲突
var animal = 'shark' //源代码
十六进制
var animal = 'shark' //源代码
使用十六进制重命名可以衍生到其它方法,但重命名最重要的还要使用简短的字符替换所有的标识符,并且作用域内不碰撞,不同作用域尽量碰撞。
这种重命名方式对于常量同样有效。
var _$Qo = window , _$Q0 = String, _$Q0O = Array, _$QO = document, _$$Q0O = Date
变量名不同作用碰撞。函数名和函数局部变量碰撞,不用函数内局部变量碰撞,全局变量和局部变量碰撞等等。
function _$QQO(){
垃圾代码
在源代码中填写大量的垃圾代码达到混淆视听的效果。
(2)数据混淆
常见数据类型混淆有数字、字符串、数组、布尔等。
数字
数字类型混淆主要是进制转换,还有一些利用数学技巧。
var number = 233 //十进制
字符串
字符串的混淆主要是编码。
还有其它的手法,比如拆分字符串,加密字符串然后解密,这里不展开说明。
1. 十六进制
var user = 'shark' //混淆前
2. Unicode
var user = 'shark' //混淆前
3. 转数组,把字符串转为字节数组
console.log('s'.charCodeAt(0)) //115
4. Base64
var user = 'shark'
还有其它的手法,比如拆分字符串,加密字符串然后解密,这里不展开说明。
- 数组
数组的混淆主要是元素引用和元素顺序。
var arr = ['log','Date','getTime']
在对元素做编码之后,之后进行引用会有一个问题,数组索引和数组元素是一一对应的,这样可以很直观的找出元素。可以进行元素顺序打乱,再通过函数还原。
var arr = ['\u006C\u006F\u0067','\u0044\u0061\u0074\u0065','\u0067\u0065\u0074\u0054\u0069\u006D\u0065']
布尔值
主要是使用一些计算来替代true和false。
undefined //false
(3)控制混淆
通过上面的混淆手段可以把代码混淆的已经很难读了,但是代码的执行流程没有改变,接下来介绍下混淆代码执行流程的方法。
控制流平坦化
代码原始流程是一个线性流程执行,通过平坦化之后会变成一个循环流程进行执行。
图1原流程
图2平坦化流程
function source(){
上面是一个比较简单示例,平坦化一般有几种表示,"while...switch...case"、"while...if....elesif"。
"while...if...eleseif" 的还原难度更高。比如 "if(seq == 1)...elseif..." 可以优化成 "if(seq &0x10==1)...elseif..." 。
逗号表达式
通过逗号把语句连接在一起,还可以结合括号进行变形。
(4)混淆工具
在线混淆
在线obfuscator混淆网站
能够满足基本混淆的力度,但也要自己调整,否则可能会很耗性能。不过ob的混淆现在网上有很多还原的工具。
AST
对Javascript来说,用AST可以按照自己的需求进行混淆,也可以很好的用来解混淆。是一个终极工具。
AST在线转换,利用这个网站进行AST解析后,再本地使用AST库进行语法树转换、生成。
1. AST处理控制流平坦化
var array = '4|3|8|5|4|0|2|3'.split('|'), index = 0;
先把上面的代码放到AST网站进行解析生成语法树。
这里使用babel进行转换。
还原的思路:先获取分发器生成的顺序,随后把分支语句和条件对应生成case对象,再利用分发器顺序从case对象获取case,最后输出即可。
// 转换为 ast 树
上面时AST处理的核心代码,转换后如下
vararray='4|3|8|5|4|0|2|3'.split('|'),
当然其它的控制流平坦化也是可以还原的,有兴趣的可以自己探索,有问题可以探讨交流。
2反调试
代码混淆只能给攻击者增加代码阅读的难度,但是如果进行动态调试分析结合本地静态代码分析还是可以找到代码关键逻辑。那么如何防调试就是很重要的一点。
下面从JS调试的攻防角度做一个统计。
(1)控制****台
- 打开
删除打开控制台的快捷键阻止控制台打开。
绕过:从菜单启动开发者工具。
window.addEventListener('keydown', function(event){
宽度检测判断窗口是否变化。可能会存在误检测的情况,需要注意。
(function () {
- 调试器
开发者工具打开之后,需要选择调试器功能进行调试分析。通过设置debugger来阻止调试器调试。
1.定时debugger
function debug() {
这种debugger会一直停住,对调试影响很大。
2.时间差debugger
addEventListener("load", () => {
由于 debugger 会停止使调试器停止,可以通过计算时间差来判断时否打开调试器。还可以单独时间差进行检测,debugger放在其它地方。
绕过:借助的调试器的"条件断点"、'"Never pause here"'功能。
输出
清空控制台的打印,可以避免攻击者修改代码打印对象等。
function clear() {
绕过:对于直接在控制台打印变量没有影响,并且调试时可以直接查看变量。
断点调试
如何去检测攻击者是否在打断点调试,有两种思路,一种是通过时间来检测,另外一种是依靠 scope 来检测。两种都有各自的问题。
1. 时间检测断点调试
var timeSinceLast;
当页面加载完成时,执行函数,定义一个时间基线,检测代码执行时间差是不是超过时间基线,一旦存在断点,必然会超过时间基线,那么就检出断点调试。但这里有个问题是如果浏览器执行代码的时间差也超过时间基线也会被检出,也就是误检。这种情况出现的机率还挺高,如果业务前端比较复杂(现在一般都是),使用性能不好的浏览器就会出现误检。
2.scope检测
function malicious() {
变量在被定义之后,调试器在断点执行的时候获取其scope,从而触发toString函数。浏览器的兼容性是这个方法的缺陷。
- 事件调用
攻击者经常利用控制台执行事件调用,例如通过获取按钮元素后,点击,提交用户名和密码登录。函数堆栈就可以检测出这种情况。
function test(){
1. Firefox
2. Chrome
从 Firefox 和 Chrome 的结果可以看出来,代码自执行的堆栈和控制台执行的堆栈是不同的。
函数、对象属性修改
攻击者在调试的时,经常会把防护的函数删除,或者把检测数据对象进行篡改。可以检测函数内容,在原型上设置禁止修改。
// eval函数
//设置函数属性之后,无法被修改
**(2)**NodeJS调试
攻击者在本地分析调试时需要把代码进行格式化后才能够分析。
//格式化后
执行格式化后的代码会出现递归爆炸的情况,因为匹配了换行符。
结语
本文只列举了一些常见的前端 JS 防破解手段,还有一些更高级的手段,例如:自定义编译器、WebAssembly、浏览器特性挖掘等。结合自身的业务合理的使用代码混淆和反调试手法,来保证业务不被恶意分析,避免遭到爬虫的危害。