JS逆向
网站加密和混淆技术
我们在爬取网站的时候,会遇一些需要分析接口或 URL 信息的情况,这时会有各种各样类似加密的情形。
-
某个网站的 URL 带有些看不太懂的长串加密参数,要抓取就必须懂得这些参数是怎么构造的,否则我们连完整的 URL都构造不出来,更不用说爬取了。
-
在分析某个网站的 Ajax 接口时,可以看到接口的那些参数也是加密的, Request Headers 里面也可能带有一些加密参数,如果不知道这些参数的具体构造逻辑,就没法直接用程序来模拟这 Ajax 请求。
-
翻看网站的 JavaScript 源代码.可以发现很多压缩了或者看不太懂的字符,比如 JavaScript 件名被编码、文件的内容被压缩成几行,变量被修改成单个字符或者一些十六进制的字符…... 这些导致我们无法轻易根据 JavaScript 源代码找出某些接口的加密逻辑。
以上情况基本上是网站为了保护其数据而采取的一些措施。我们可以把它归类为两大类 :
-
URL/API参数加密
网站运营者首先想到的防护措施可能是对某些数据接口的参数进行加密,比如说给某些URL的参数加上校验码,给一些ID信息编码,给某些API请求加上token、sign 等签名,这样这些请求发送到服务器时,服务器会通过客户端发来的一些请求信息以及双方约定好的密钥等来对当前的请求进行校验,只有校验通过,才返回对应数据结果。
比如说客户端和服务端约定一种接口校验逻辑,客户端在每次请求服务端接口的时候都会附带一个sign参数,这个sign参数可能是由当前时间信息、请求的URL、请求的数据、设备的ID、双方约定好的密钥经过一些加密算法构造而成的,客户端会实现这个加密算法来构造sign,然后每次请求服务器的时候附带上这个参数。服务端会根据约定好的算法和请求的数据对sign进行校验、只有校验通过、才返回对应的数据,否则拒绝响应。
当然,登录状态的校验也可以看作此类方案、比如一个API的调用必须传一个token,这个token必须在用户登录之后才能获取,如果请求的时候不带该token,API就不会返回任何数据。
倘若没有这种措施,那么URL 或者API接口基本上是完全可以公开访问的、这意味着任何人都可以直接调用来获取数据,几乎是零防护的状态,这样是非常危险的,而且数据也可以被轻易地被爬虫爬取。因此,对 URL/API参数进行加密和校验是非常有必要的。
-
JavaScript压缩、混淆和加密
接口加密技术看起来的确是一个不错的解决方案,但单纯依靠它并不能很好地解决问题。为什么呢?
对于网页来说,其逻辑是依赖于JavaScript来实现的。JavaScript有如下特点。
- JavaScript 代码运行于客户端,也就是它必须在用户浏览器端加载并运行。
- JavaScript代码是公开透明的,也就是说浏览器可以直接获取到正在运行的JavaScript的源码。
基于这两个原因,JavaScript 代码是不安全的,任何人都可以读、分析、复制、盗用甚至篡改代码。
所以说,对于上述情形,客户端JavaScript对于某些加密的实现是很容易被找到或模拟的,了解了加密逻辑后,模拟参数的构造和请求也就轻而易举了,所以如果 JavaScript没有做任何层面的保护的话,接口加密技术基本上对数据起不到什么防护作用。
如果你不想让自己的数据被轻易获取,不想他人了解 JavaScript逻辑的实现、或者想降低被不怀好意的人甚至是黑客攻击的风险,那么就需要用到JavaScript压缩、混淆和加密技术了。
这里压缩、混绢和加密技术简述如下。
-
代码压缩:去除JavaScript代码中不必要的空格、换行等内容,使源码都压缩为几行内容,降低代码的可读性,当然同时也能提高网站的加载速度。
-
代码混淆:使用变量替换、字符串阵列化、控制流平坦化、多态变异、僵尸函数、调试保护等手段,使代码变得难以阅读和分析,达到最终保护的目的。但这不影响代码的原有功能,是理想、实用的JavaScript保护方案。
- 变量名混淆:将带有含义的变量名、方法名、常量名随机变为无意义的类乱码字符串。降低代码的可读性,如转成单个字符或十六进制字符串。
- 字符串混淆:将字符串阵列化集中放置并可进行 MD5或Base64加密存储,使代码中不出现明文字符串,这样可以避免使用全局搜索字符串的方式定位到入口。
- 对象键名替换:针对JavaScript对象的属性进行加密转化,隐藏代码之间的调用关系。
- 控制流平坦化:打乱函数原有代码的执行流程及函数调用关系,使代码逻辑变得混乱无序。
- 无用代码注入:随机在代码中插入不会被执行到的无用代码,进一步使代码看起来更加混乱。
- 调试保护:基于调试器特性,对当前运行环境进行检验,加入一些debugger语句,使其在调试模式下难以顺利执行JavaScript 代码。
- 多态变异:使JavaScript代码每次被调用时,将代码自身立刻自动发生变异,变为与之前完全不同的代码,即功能完全不变,只是代码形式变异,以此杜绝代码被动态分析和调试。
- 域名锁定:使JavaScript 代码只能在指定域名下执行。
- 代码自我保护:如果对JavaScript代码进行格式化,则无法执行,导致浏览器假死。
- 特殊编码:将JavaScript完全编码为人不可读的代码,如表情符号、特殊表示内容等等。
在前端开发中,现在JavaScript混淆的主流实现是javascript-obfuscator和 terser 这两个库。它们都能提供一些代码混淆功能,也都有对应的webpack 和Rollup打包工具的插件。利用它们,我们可以非常方便地实现页面的混淆,最终输出压缩和混淆后的JavaScript代码,使得JavaScript代码的可读性大大降低。
-
代码加密:可以通过某种手段将JavaScript代码进行加密、转成人无法阅读或者解析的代码,如借用WebAssembly技术,可以直接将JavaScript代码用C/C++实现,JavaScript调用其编译后形成的文件来执行相应的功能。
JavaScript Hook的使用
Hook 技术又叫钩子技术,指在程序运行的过程中,对其中的某个方法进行重写,在原先的方法前后加入我们自定义的代码。相当于在系统没有调用该函数之前,钩子程序就先捕获该消息,得到控制权,这时钩子函数既可以加工处理(改变)该函数的执行行为,也可以强制结束消息的传递。
要对JavaScript代码进行Hook操作,就需要额外在页面中执行一些有关Hook逻辑的自定义代码。那么问题来了?怎样才能在浏览器中方便地执行我们所期望执行的JavaScript代码呢?这里推荐一个插件,叫作Tampermonkey。这个插件的功能非常强大,利用它我们几乎可以在网页中执行任何JavaScript代码,实现我们想要的功能。
无限debugger的原理与绕过
原理:在无限for循环、无限 while循环、无限递归调用等中执行debugger语句。
绕过方式:
- 因为debugger其实就是对应的一个断点,它相当于用代码显式地声明了一个断点,要解除它,我们只需要禁用这个断点就好了。
- 禁用所有断点。全局禁用开关位于Sources面板的右上角,叫作 Deactivate breakpoints。
- 在debugger语句所在的行的行号上单击鼠标右键,此时会出现一个快捷菜单,点击Never pause here选项,意思是从不在此处暂停。
- 将远程的 JavaScript 文件替换成本地的JavaScript文件
AST技术
AST的全称叫作Abstract Syntax Tree,中文翻译叫作抽象语法树。如果你对编译原理有所了解的话,一段代码在执行之前,通常要经历这么三个步骤。
- 词法分析:一段代码首先会被分解成一段段有意义的词法单元,比如说const name = 'Germey'这段代码,它就可以被拆解成四部分:const、name 、=、'Cermey',每一个部分都具备一定的含义。
- 语法分析:接着编译器会尝试对一个个词法单元进行语法分析,将其转换为能代表程序语法结构的数据结构。比如,const就被分析为VariableDeclaration类型,代表变量声明的具体定义;name就被分析为Identifier类型,代表一个标识符。代码内容多了,这一个个词法就会有依赖、嵌套等关系,因此表示语法结构的数据结构就构成了一个树状的结构,也就成了语法树,即 AST。
- 指令生成:最后将AST转换为实际真正可执行的指令并执行即可。
AST是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,这种数据结构其实可以类别成一个大的JSON对象。前面我们也介绍过JSON对象,它可以包含列表、字典并且层层嵌套,因此它看起来就像一棵树、有树根、树干、树枝和树叶,无论多大,都是一棵完整的树。
在前端开发中,AST技术应用非常广泛,比如 webpack 打包工具的很多压缩和优化插件,Babel插件、Vue和React的脚手架工具的底层等都运用了AST技术。有了AST,我们可以方便地对JavaScript代码进行转换和改写,因此还原混绢后的JavaScript 代码也就不在话下了。
JavaScript混淆方式多种多样,如表达式还原、字符串还原、无用代码剔除、反控制流平坦化等。
其他混淆方式
除了基于javascript-obfuscator的混淆,还有其他混淆方式,这里介绍几种有代表性的混淆方案(比如AAEncode、JJEncode 、JSFuck)的还原方法。
AAEncode的还原:AAEncode是一种JavaScript代码混淆算法,利用它,我们可以将JavaScript代码转换成颜文字表示的JavaScript 代码。
JJEncode的还原:JJEncode也是一种JavaScript代码混淆算法,其原理和AAEncode大同小异,利用它,我们可以将JavaScript代码转换成颜文字表示的 JavaScript 代码。
JSFuck 的还原:JSFuck也是一种特殊的混淆方案,是基于开源的JSFuck库来实现的。
逆向步骤
JavaScript逆向可以分为三大部分:寻找人口、调试分析和模拟执行。下面我们来分别介绍。
-
寻找入口:这是非常关键的一步,逆向在大部分情况下就是找一些加密参数到底是怎么来的,比如一个请求中token .sign等参数到底是在哪里构造的,这个关键逻辑可能写在某个关键的方法里面或者隐藏在某个关键变量里面。一个网站加载了很多JavaScript文件、那么怎么从这么多JavaScript代码里面找到关键的位置,那就是一个关键问题。这就是寻找入口。
常见的分析入口的方法包括查看请求、搜索参数、分析发起调用、断点、Hook等。还有很多其他方法,比如使用Pyppeteer、PlayWright里面内置的API实现一些数据拦截和过滤功能,也可以使用一些抓包软件对一些请求进行拦截和分析,还可以使用一些第三方工具或浏览器插件来辅助分析。
-
调试分析:找到入口之后,比如说我们可以定位到某个参数可能是在某个方法里面执行的了,那么里面的逻辑究竞是怎样的,里面调用了多少加密算法,经过了多少变量赋值和转换等,这些我们需要先把整体思路搞清楚,以便于我们后面进行模拟调用或者逻辑改写。在这个过程中,我们主要借助于浏览器的调试工具进行断点调试分析,或者借助于一些反混淆工具进行代码的反混淆等。
常见的方法包括格式化、断点调试、反混淆等操作。
-
模拟执行:经过调试分析之后,我们差不多已经搞清楚整个逻辑了,但我们的最终目的还是写爬虫,怎么爬到数据才是根本,因此这里就需要对整个加密过程进行逻辑复写或者模拟执行,以把整个加密流程模拟出来,比如输入是一些已知变量,调用之后我们就可以拿到一些token内容,再用这个token来进行数据爬取即可。
常见的方法包括Python改写或模拟执行、JavaScript模拟执行+API、浏览器模拟执行等。