type
status
date
slug
summary
tags
category
icon
password
前言:
刚开始接触js逆向的时候感觉这玩意儿挺简单的,想着至少不会像之前分析程序那样没有源代码,只能是汇编之后看伪代码。不过对这玩意儿有了一定了解后发现是我太天真了,一些网站的js代码要恶心起来,确实是让人头皮发麻。这篇博客我将以我半吊子的js逆向能力对js逆向做一个知识点的梳理和实战经验的分享。
首先先说一下我的学习路径,第一个是看B站的志远大佬的入门视频,这里大概补全了一些JavaScript的编程知识、浏览器的开发者工具的使用,以及一些js逆向的相关知识点。之后是学习悦来客栈的老板的一些AST反混淆知识点。目前阶段就是关注一些大佬的博客或微信公众号,比如王平大佬的猿人学,十一姐的逆向OneByOne,爬虫术与道等。这些大概就是我的一些学习历程。下面正式进入js逆向相关介绍。
tips:这篇文章更多的是我对自己在js逆向过程中学到东西的一个复盘,如果要系统学习的话还是去B站看看视频好一点。
JS逆向·理论篇:
在介绍理论知识之前我们先假定一个目标:分析一个网站的加密字段。
有了目标之后我们一步步按照其中涉及到的知识点来进行介绍,在分析加密字段之前我们首先可以做一些前期的信息收集。比如看看关键js文件的混淆特征(这个在之后的各厂商混淆分析再介绍)、涉及这个字段的js文件有哪些,哪些字段是由服务器发送过来的等。
信息收集阶段:
前期的信息主要可以从两个方面获得,一个是开发者工具的网络选项卡,一个是用fiddler抓包。
开发者工具的网络选项卡:
开发者工具的网络选项卡可以用来观察cookies设计到的js文件有哪些
For Example:


这里提一下启动器模块,它可以用来定位对应调用js的堆栈情况。
fiddler工具:
fiddler工具可以方便我们观察浏览器对目标服务器发送请求的流程。
For Example:

这里我抓了访问马蜂窝网站的请求过程,前面两次的相应码为512,可以观察一下第一个包服务器返回的数据:

document.cookie等于后面一段编码,这段编码解码后的内容就是设置的cookies值。将这段代码直接放到控制台运行,就可以看到具体的值。

关于fiddler工具的一些补充:
- 刚安装的fiddler是无法直接抓到https数据包的,需要设置,具体设置可以看这个链接:
- 编程猫开发了一个fiddler的插件,具体有以下几个功能,还挺方便的,插件可以在k哥爬虫里获取到

JS逆向过程:
大致对目标网站有了一定的了解后,就可以开始着手逆向了。首先我们了解一下一个网站代码的运行流程:①
加载html 加载js
→② 运行js初始化
→③ 用户触发了某个事件
→ ④调用了某段js
→⑤ 加密函数
→ ⑥给服务器发信息(XHR-send)
→ ⑦接收到服务器数据
→⑧ 解密函数
→ ⑨刷新网页进行渲染
。而cookies的生成基本也是走这么一个流程,先根据一些明文参数拼凑出这个cookie的明文数据,通过加密函数对这些明文数据进行加密,最后使用document.cookie命令将加密后的数据设置为这个cookie的值。之后发送给目标服务器的包就会带上这个cookie字段。
For Example:
假设有一个cookie字段的名称为uname,uname的值一开始是放在了tmp变量中。

了解了字段的生成过程,我们整理一下分析cookie字段的思路:找到设置cookie的语句,根据堆栈向上回溯到加密函数,再向上回溯分析字段在明文时的内容由哪些变量构成。
定位目标字段:
全局搜索:
确定好分析思路后第一步就是定位设置cookie的语句,如何定位呢,最基础的定位方法就是使用开发者工具的全局搜索,Ctrl+shift+F可以调出开发者工具,它的搜索范围是网站的所有文件。
For Example:

上图是示例网站的cookie字段,假设要找的是“x-s3-sid”字段。可以直接搜索字段名称x-s3-sid

HOOK:
全局搜索这个办法并不一定能成功,因为许多网站对其代码中的字符串都是做了混淆的大部分时候我们是无法直接搜索到目标字段,还是以上面的示例网站举例,其中的’x-s3-s4e’字段就无法通过全局搜索直接定位到。

- hook介绍:
这个时候可以考虑使用hook来进行定位,首先介绍一下hook:
hook就是把一个函数进行重写,在重写时在函数中添加自己的代码如debugger、console.log等,这样后面的js代码调用重写后函数时就会执行我们想要其执行的代码。比如可以重写document.cookie方法,在其中加上debugger语句,当程序调用document.cookie时程序就会自动断下来,我们再根据堆栈情况就可以快速定位目标字段了。
- hook代码的编写:
hook代码的编写思路主要是两个步骤:先对原函数进行保存→重写原函数。如下面的代码hook了document.cookie的set方法,也就是赋值方法。在运行完这段代码之后再调用这个方法就会被断下来。之后就可以通过观察堆栈来定位到对应cookie设置代码了。
Object.defineProperty(obj, prop, descriptor)
,它的作用就是直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,接收的三个参数含义如下:obj
:需要定义属性的当前对象;prop
:当前需要定义的属性名;descriptor
:属性描述符,可以取以下值:属性名 | 默认值 | 含义 |
get | undefined | 存取描述符,目标属性获取值的方法 |
set | undefined | 存取描述符,目标属性设置值的方法 |
value | undefined | 数据描述符,设置属性的值 |
writable | false | 数据描述符,目标属性的值是否可以被重写 |
enumerable | false | 目标属性是否可以被枚举 |
configurable | false | 目标属性是否可以被删除或是否可以再次修改特性 |
- 设置hook的方法有三种:
- 开发者工具:
- TamperMonkey:
- fiddler注入:


可以在开发者工具中新建一个代码端然后右击运行。
使用油猴也可以实现hook:

选择添加新脚本:

hook代码的编写在下面这个箭头处,上面的箭头是该脚本的一些配置,相关配置如下:
属性名 | 作用 |
@name | 油猴脚本的名字 |
@namespace | 命名空间,用来区分相同名称的脚本,一般写成作者名字或者网址就可以了 |
@version | 脚本版本,油猴脚本的更新会读取这个版本号 |
@description | 描述,用来告诉用户这个脚本是干什么用的 |
@author | 作者名字 |
@match | 只有匹配的网址才会执行对应的脚本,例如 *、http://*、http://www.baidu.com/* 等 |
@grant | 指定脚本运行所需权限,如果脚本拥有相应的权限,就可以调用油猴扩展提供的API与浏览器进行交互。如果设置为none的话,则不使用沙箱环境,脚本会直接运行在网页的环境中,这时候无法使用大部分油猴扩展的API。如果不指定的话,油猴会默认添加几个最常用的API |
@require | 如果脚本依赖其他js库的话,可以使用require指令,在运行脚本之前先加载其他库,常见用法是加载jquery,导库,和node差不多,相当于导入外部的脚本 |
@run-at | 脚本注入时机,这个比较重要,有时候是能不能hook到的关键, document-start :网页开始时;document-body :body出现时;document-end :载入时或者之后执行;document-idle :载入完成后执行,默认选项 |
@connect | 当用户使用GM_xmlhttpRequest请求远程数据的时候,需要使用connect指定允许访问的域名,支持域名、子域名、IP地址以及*通配符 |
@updateURL | 脚本更新网址,当油猴扩展检查更新的时候,会尝试从这个网址下载脚本,然后比对版本号确认是否更新 |
着重介绍一下
@match
和@run-at
,是用来指定hook的目标网址,是用来确定脚本的注入时机。补充
油猴官方文档:
关于使用油猴来实现hook的具体流程可以看这篇文章:
没用过可以看这篇文章:
方法介绍完了,最后实操一下尝试找到前面提到的全局搜索搜不到的“x-s3-s4e”
这里使用的办法是使用油猴进行hook:添加一个新脚本,将上面的hook的代码复制进去,修改上面的配置代码为:(hook的注入时机为文档开始时)

保存后启动该脚本,清除当前的cookies然后刷新页面。

可以看到如下图代码断下来了然后观察调用堆栈前往上层代码。

在上层代码中通过控制台,观察输出就可以看到加密过的原字符串了。

hook是一种可以快速定位参数的办法。在我们找到了参数的位置之后,会发现一个问题,这些代码不太好读
JS混淆与解混淆办法:
由于js代码是公开展现在用户面前的,为了保护自己的代码不被利用,一些开发者会选择使用JS混淆器来混淆和压缩代码,使其难以理解和修改。
对于js代码的混淆最基本的办法是将JavaScript代码的变量名、函数名、常量名等进行修改或替换,以使其难以被识别。除此之外还有eval加密、代码压缩、控制流平坦化、僵尸代码植入等,这里将使用右图的示例代码对常见的混淆方式进行逐个分析对比。
tips:一般js混淆是多种混淆方式一起使用,这里为了方便分析,每次混淆尽量只使用了一种技术。
变量、函数名混淆:
一般对于变量名或函数名的混淆,可以使用base64编码或RC4算法对其进行编码替换,不过现在大部分的混淆工具会直接替换成与被替换变量名没啥关系的下划线加十六进制数的组合,这使得及时反混淆后变量名也无法还原。
tips:这里顺便介绍一些小的混淆手段。
- 对于数值常量的混淆可以将其转换成的等价的数学表达式。
- 字符串数组反转:对于一些字符串的赋值,会利用reverse函数进行反转。
使用在线加密网站obfuscator的免费加密功能将上面的示例代码进行加密得出以下结果:
可以看到变量的名称已经更换成下划线和十六进制数的组合(变量名称的混淆往往是不可逆转的),str变量赋值的字符串先是倒序显示,然后在赋值时通过reverse函数有倒转回来。count变量的100转换成了
575490 ^ 575590
。

上面这些是最基本的混淆手段,一般是配合其他高级混淆手段一起使用。
tips:还有一些通过编码的方式对js代码进行混淆,具体使用编码为aaencode,jsfuck和jjencode这些特征较为明显如:aaencode的颜文字 这些编码放到在线解码的网站就可以瞬间还原代码,所以这里不做过多介绍。
插入僵尸代码:
僵尸代码指的就是在程序中存在,但实际上从不执行或使用的代码。在JavaScript中插入僵尸代码的方法有很多,以下是一些常见的例子:
- 添加无关的变量和函数:
在代码中添加不相关的变量和函数,这些变量和函数从未被调用或使用。
- 使用死代码(Dead code):
死代码是指由于条件永远不满足而无法执行的代码。
- 添加无意义的操作:
在代码中插入无意义的操作,如在计算过程中添加无关的变量赋值,或者在逻辑判断中添加多余的条件。
- 插入隐蔽的恶意代码:
恶意代码可以被插入到僵尸代码中,以便在不引起注意的情况下执行。
这些就是一些插入僵尸代码的思路,还是之前的示例代码,对它进行僵尸代码的插入的情况如下:
去除僵尸代码的办法也比较简单,可以利用一些代码压缩工具,对一些不会被执行的代码进行一个优化,类似的工具有UglifyJS
tips:js代码结构的优化实现的原理是利用AST。
控制流平坦化:
控制流平坦化在程序逆向中已经是一个老生常谈的话题了,关于控制流平坦化这个概念大致可以用一张图来解释:

控制流平坦化大致的意思就是将程序按照基本块作为分割,然后将其控制结构转换为一系列的状态跳转。关于控制流平坦化的原理可以阅读这篇文章:https://bbs.kanxue.com/thread-266082.htm。
下面将展示经过控制流平坦化后js代码的一个效果。tips:为了突出控制流平坦化的混淆效果在示例代码中添加了一个for循环,和一个if判断。右边为修改后的示例代码,在
function b()
中添加了一个循环和判断。下面的代码则是经过控制流平坦化混淆后的代码,原本的控制流全部换成了While或者Switch…case…的形式。 经过控制流平坦化混淆后的代码特征最明显的就是控制流全部是While和Switch…case…。下面大致分析一下js的控制流平坦化的实现。大致的思路是使用
@babel/parser
来解析源代码,然后使用@babel/traverse
来遍历抽象语法树(AST),@babel/generator
来生成新的代码,以及用@babel/types
来操作AST节点。具体实现代码:
首先解析示例代码,然后使用
@babel/traverse
遍历AST,遍历AST时,脚本查找FunctionDeclaration
节点(即函数声明),并对其进行操作。在这个例子中,我们将原始的if-else
结构替换为控制流平坦化的while
循环和switch-case
结构。对于每个找到的
FunctionDeclaration
节点,进行以下步骤操作:- 获取函数体,以便稍后进行替换。
- 创建状态变量(
state
)和其初始值。
- 创建结果变量(
result
)。
- 创建一个新的函数体,其中包含控制流平坦化的
while
循环和switch-case
结构。这部分代码与先前示例中展示的平坦化代码类似。
- 使用新创建的函数体替换原始函数体。
在处理完AST后,我们使用
@babel/generator
将修改后的AST生成为新的JavaScript代码。然后,将新生成的控制流平坦化代码输出到控制台,并将其写入名为output.js
的文件。补充(evalencode)
EvalEncode:
EvalEncode是利用eval函数特性的一种代码混淆方式,据说是很早之前就有的混淆办法,不过EvalEncode虽然看着很复杂不过其特征过于明显,解密也十分简单,所以安全系数并不高。先看一下示例代码经过EvalEncode加密后的js代码特征:
EvalEncode加密后js代码以eval()开头,然后后面function的格式也是基本固定,下面分析一下该方法的加密代码:
encode函数负责加密,在
encode
函数内部,首先移除代码中的换行符和转义单引号。使用正则表达式 \b(\w+)\b
,从代码中提取所有的单词(变量、函数名等),并将它们存入 tmp
数组。然后对这个数组进行排序。然后遍历 tmp
数组,将不重复的单词添加到 dict
数组中。之后调用 num
函数将 dict
数组中的每个单词替换为对应的字符(加密过程),并用新的字符替换原始代码中的相应单词。最后返回一个字符串,该字符串包含一个自解密的代码段,其中 eval
函数用于执行解密后的原始代码。对于evalencode的加密方式的解密方法可以直接找在线网站解密。
在了解了基础的混淆手段之后,这里贴一下AST相关知识点的笔记:
关于AST的知识可以关注悦来客栈的老板。
js混淆相关的知识到这差不多就介绍完了,内容偏理论,实际逆向遇到的混淆会要难一些。本来按照顺序,下一步就要介绍参数相关的加密算法的,但是关于加密算法相关的知识我在后面有做一个系统的归纳,需要了解这部分知识的直接去看那篇文章中的现代加密算法就成,古典密码就没必要看了,那是为了CTF整理的实用性不大。
按照现在这个逻辑从前期信息收集到目标字段定位,再到js反混淆,最后到加密算法分析。这整个过程的基础知识就介绍得差不多了。但还有一个东西贯穿了整个的分析过程就是调试技术,这个东西不太好介绍,主要是从实操中积累,下面主要罗列一些js调试的技巧。
最后编辑时间8月1日,未完待续。
- 作者:Reveone
- 链接:http://www.reveone.cn/article/99827d5d-4d4f-4a52-ae43-92e543ce46b3
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。