我在《Python3 反爬虫原理与绕过实战》一书中给出了“爬虫与反爬虫都是综合技术的应用”、“技术在对抗中进步”这样的观点。随着时间的推移、技术的普及和进步,Web 应用方给爬虫增长了愈来愈多的限制,其中效果最显著的就是代码混淆。html
单纯的加密算法或者自定义的字符处理函数已经没法知足防护需求了, Web 应用方将目光转移到了代码混淆技术。代码混淆有几个优势:前端
加密算法和字符串处理函数配合代码混淆,防护力直线上升。举个简单例子,一个简单的字符处理函数以下:git
这里有三个函数,stringArray 返回一个包含字符的数组对象、mergeArray 将数组对象里的元素拼接成为一个字符串并返回、main 调用 stringArray 函数和 mergeArray 函数并打印获得的字符串,下方的 output 注释即运行结果。github
这么清晰明了的函数调用,爬虫工程师能看不懂吗?算法
咱们看看上面三个函数混淆后的样子:编程
同样的功能、同样的输出,可是代码却彻底不同了,变得不可读。若是把下面的注释去掉,那你根本就不知道发生了什么,也不知道会输出什么,这就是代码混淆给 Web 应用方带来的防护力。数组
做为一名爬虫工程师,你如今有两个选择:浏览器
第一种方法,就是平时爬虫工程师说的“硬扣”,若是有跨文件的函数调用和冗长复杂的调用链,那“硬扣”真的是会掉头发的。babel
第二种方法的技术门槛稍微高一些,须要爬虫工程师懂得 AST 理论,并学会编写还原代码,将杂乱无章的代码阅读难度下降,从而下降本身阅读代码逻辑或者整理调用链的难度与成本。编程语言
这里引用百度百科对 AST 的解释:
在计算机科学中, 抽象语法树( Abstract Syntax Tree,AST),或简称 语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每一个节点都表示源代码中的一种结构。之因此说语法是“抽象”的,是由于这里的语法并不会表示出真实语法中出现的每一个细节。好比,嵌套括号被隐含在树的结构中,并无以节点的形式呈现;而相似于 if-condition-then 这样的条件跳转语句,可使用带有两个分支的节点来表示。
嗯,这看起来有点绕,我打算用一个例子来表述。JavaScript 变量声明和赋值的代码示例以下:
var nick = "vansenb";
这一行代码会被解析成很长的语法树,具体解析可经过 AST Explorer 查看。如下是 JavaScript 语句和语法树的对应关系:
图有点模糊,想看清晰结构的请移步 AST Explorer。
上图的语法树中代表了程序主体、声明类型、标识符、字面量等信息,由此咱们能够得出:
从人类阅读的角度来看,这行代码:声明了一个名为 nick 、值为 vansenb 的变量。
若是你想改变这行代码,将它变成:
var nick = "James";
只须要改变语法树中 type 为 Literal 下的 value 属性对应的值便可,那么代码的语义就变成了:声明了一个名为 nick 、值为 James 的变量。了解到这一点以后,咱们就能够思路放在代码的混淆和还原上面了。
你想一想,当你使用那些一键混淆/还原工具的时候,是否是只须要将代码粘贴到输入框并点击“混淆”按钮便可获得混淆后的代码?并且相同结构的代码混淆后的结构也是相同的?
这说明一键混淆/还原工具经过改变原代码的抽象语法树实现混淆/还原的效果,例如在树的某个节点先后增长或删除节点,亦或在混淆时将本来直接能够输出结果的单个函数转换为相互调用的多个函数。
语法树并非 JavaScript 独有的,几乎全部编程语言都有语法树,例如 Golang、Python 和 Java。JavaScript 的语法树出现频次较高,这是由于 JavaScript 隔代语法的差别和不得不考虑的兼容性形成的,ES5 和 ES6 语法隔代,在实际应用中会须要进行语法的转换,这就使得语法树可以在实际场景中发挥做用。
语法树的做用就像是一个转接头,把代码的表现形式 A 转换为表现形式 B
JavaScript 领域经常使用的 AST 解析库有 babel、esprima、espree 和 acorn 等,各位工程师可根据本身的喜爱和风格选择趁手的库。
这些库经常被前端开发工程师用来编写代码转换的工具或者代码混淆工具,甚至是将 React 和 Vue 的工程代码编译为浏览器能运行的 JavaScript 代码,而在爬虫工程师这里,大几率会用来辅助本身逆向 JavaScript 代码。
语法树相关的知识和技巧须要必定的时间学习(大概一两个月),对此感兴趣的你能够经过如下几篇实战型文章了解它的具体应用:
AST实战:全自动解密经obfuscator混淆的加密字符串
操做AST还原混淆代码课程九:还原简单的CallExpression 类型
上面列举了经常使用的几个 AST 解析库,虽然各个库解析同一份代码获得的结构不彻底一致,但用于表示节点类型的名词几乎都是一致的,例如 VariableDeclaration 表明这是变量声明语句、CallExpression 表明这是调用表达式。
掌握节点类型的名词,有助于咱们在阅读语法树结构时更清晰地了解节点的做用和意图,也能够说节点名词是咱们成为代码混淆大师或代码逆向大师的必经之路,很是重要!
咱们如下图的代码为例,看看 AST 中经常使用的节点类型名词有哪些。
上图代码包含了 JavaScript 语法中经常使用的语句,例如变量声明、函数声明、三元表达式、if 控制流语句、switch 控制流、函数调用、赋值语句、数组声明、for 循环等。
将上面的代码复制到 AST Explorer 即可以获得语法树,根据左侧的代码和右侧的语法树,咱们能够统计语法树节点名词和具体描述,以下表:
序号 | 类型原名称 | 中文名称 | 描述 |
---|---|---|---|
1 | Program | 程序主体 | 整段代码的主体 |
2 | VariableDeclaration | 变量声明 | 声明一个变量,例如 var let const |
3 | FunctionDeclaration | 函数声明 | 声明一个函数,例如 function |
4 | ExpressionStatement | 表达式语句 | 一般是调用一个函数,例如 console.log() |
5 | BlockStatement | 块语句 | 包裹在 {} 块内的代码,例如 if (condition){var a = 1;} |
6 | BreakStatement | 中断语句 | 一般指 break |
7 | ContinueStatement | 持续语句 | 一般指 continue |
8 | ReturnStatement | 返回语句 | 一般指 return |
9 | SwitchStatement | Switch 语句 | 一般指 Switch Case 语句中的 Switch |
10 | IfStatement | If 控制流语句 | 控制流语句,一般指 if(condition){}else{} |
11 | Identifier | 标识符 | 标识,例如声明变量时 var identi = 5 中的 identi |
12 | CallExpression | 调用表达式 | 一般指调用一个函数,例如 console.log() |
13 | BinaryExpression | 二进制表达式 | 一般指运算,例如 1+2 |
14 | MemberExpression | 成员表达式 | 一般指调用对象的成员,例如 console 对象的 log 成员 |
15 | ArrayExpression | 数组表达式 | 一般指一个数组,例如 [1, 3, 5] |
16 | NewExpression | New 表达式 | 一般指使用 New 关键词 |
17 | AssignmentExpression | 赋值表达式 | 一般指将函数的返回值赋值给变量 |
18 | UpdateExpression | 更新表达式 | 一般指更新成员值,例如 i++ |
19 | Literal | 字面量 | 字面量 |
20 | BooleanLiteral | 布尔型字面量 | 布尔值,例如 true false |
21 | NumericLiteral | 数字型字面量 | 数字,例如 100 |
22 | StringLiteral | 字符型字面量 | 字符串,例如 vansenb |
23 | SwitchCase | Case 语句 | 一般指 Switch 语句中的 Case |
这只是经常使用的那部分,更多节点类型名词在你须要用到时再补充便可。我会持续更新相关资料,感兴趣的朋友能够到夜幕团队的 GitHub 仓库 https://github.com/NightTeam/... 查看 。
有了这些名词对照关系以后,咱们阅读语法树结构就变得简单了。当你看到节点 tpye 为 IfStatement 的时候,你知道后面一定会有至少一个 BlockStatement,即 if (condition){}。
更多关于 AST 理论和实战的内容请关注夜幕团队公众号 NightTeam 或团队仓库 https://github.com/NightTeam。