国内 Web 防护天花板,瑞数 4 代 JS 逆向分析


javascript_reverse

声明

本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!

本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请通过邮件 admin@itbob.cn 联系我立即删除!

前言

01

瑞数动态安全 Botgate(机器人防火墙)以“动态安全”技术为核心,通过动态封装、动态验证、动态混淆、动态令牌等技术对服务器网页底层代码持续动态变换,增加服务器行为的“不可预测性”,实现了从用户端到服务器端的全方位“主动防护”,为各类 Web、HTML5 提供强大的安全保护。

瑞数 Botgate 多用于政企、金融、运营商行业,曾一度被视为反爬天花板,随着近年来逆向大佬越来越多,相关的逆向文章也层出不穷,真正到了人均瑞数的时代了,这里也感谢诸如 Nanda、懒神等逆向大佬,揭开了瑞数神秘的面纱,总结的经验让后来人少走了不少弯路。

过瑞数的方法基本上有以下几种:自动化工具(要隐藏特征值)、RPC 远程调用、JS 逆向(硬扣代码和补环境),本文介绍的是 JS 逆向硬扣代码,尽可能多的介绍各种细节。

瑞数特征以及不同版本的区别

对于绝大多数使用了瑞数的网站来说,有以下几点特征(可能有特殊版本不一样,先仅看主流的):

1、打开开发者工具(F12)会依次出现两个典型的无限 debugger:

02

03

2、瑞数的 JS 混淆代码中,变量、方法名大多类似于 _$xx,有众多的 if-else 控制流,新版瑞数还可能会有 jsvmp 以及众多三目表达式的情况:

04

3、看请求,会有典型的三次请求,首次请求响应码是 202(瑞数3、4代)或者 412(瑞数5代),接着单独请求一个 JS 文件,然后再重新请求页面,后续的其他 XHR 请求中,都带有一个后缀,这个后缀的值是由 JS 生成的,每次都会变化,后缀的值第一个数字为瑞数的版本,比如 MmEwMD=4xxxxx 就是4代瑞数,bX3Xf9nD=5xxxxx 就是5代瑞数:

05

06

07

08

4、看 Cookie,瑞数 3、4 代有以 T 和 S 结尾的两个 Cookie,其中以 S 开头的 Cookie 是第一次的 201 那个请求返回的,以 T 开头的 Cookie 是由 JS 生成的,动态变化的,T 和 S 前面一般会跟 80 或 443 的数字,Cookie 值第一个数字为瑞数的版本(为什么可以通过第一个数字来判断版本?难道相同版本第一个数字不会变吗?这些问题我们在分析 JS 的时候可以找到答案),比如:

  • FSSBBIl1UgzbN7N80T=37Na97B.nWX3....:数字 80 是 http 协议的默认端口号,对应 http 请求,其值第一位为 3,表示 3 代瑞数;
  • FSSBBIl1UgzbN7N443T=4a.tr1kEXk.....:数字 443 是 https 协议的默认端口号,对应 https 请求,其值第一位为 4,表示 4 代瑞数。

09

瑞数 5 代也有以 T 和 S 结尾的两个 Cookie,但有些特殊的 5 代瑞数也有以 O 和 P 结尾的,同样的,以 O 开头的是第一次的 412 那个请求返回的,以 P 开头的是由 JS 生成的,Cookie 值第一个数字同样为瑞数的版本,和 3、4 代不同的是,5 代没有加端口号了,比如:

  • vsKWUwn3HsfIO=57C6DwDUXS.....:以 O 结尾,其值第一位为 5,表示 5 代瑞数;
  • WvY7XhIMu0fGT=53.9fybty......:以 T 结尾,其值第一位为 5,表示 5 代瑞数。

10

11

5、看入口,瑞数有个流程是在虚拟机 VM 中加载 1w+ 行的代码,加载此代码的入口,不同版本也不一样(这个入口具体在哪里?怎么定位?在后续逆向分析中再详细介绍),示例如下:

  • 3 代:_$aW = _$c6[_$l6()](_$wc, _$mo);_$c6 实际上是 eval_$l6() 实际上是 call

12

  • 4 代:ret = _$DG.call(_$6a, _$YK);_$DG 实际上是 eval,有关键字 retcall 是明文;

13

  • 5 代:5 代种类比较多了,最初和 4 代的类似,比如 ret = _$Yg.call(_$kc, _$mH);,有关键字 ret,call 是明文,也有没有 ret 关键字的版本,比如 _$ap = _$j5.call(_$_T, _$gp);,也有像 3 代那样全部混淆了的,比如:_$x8 = _$mP[_$nU[15]](_$z3, _$Ec);_$mP 实际上是 eval_$nU[15] 实际上是 call,混淆的 call 与 3 代的区别就是 5 代是在一个数组里取值得到的;

14

15

16

当然要想精准区分不同版本,得各个条件结合起来看,最主要的还是得看看内部的实现逻辑,以及页面的代码结构,比如 4 代有一个生成假 Cookie 的步骤,而 5 代没有,有的特殊版本虽然看起来是 5 代,但是加了 jsvmp 和三目表达式,和传统的 5 代又有区别,偶尔愚人节啥的突然来个新版本,也会不一样,各版本在分析一遍之后,就很容易区分了。

本文案例中瑞数 4 代网站为:aHR0cDovL3d3dy5mYW5nZGkuY29tLmNuL25ld19ob3VzZS9uZXdfaG91c2VfZGV0YWlsLmh0bWw=

首先过掉无限 debugger(过不过其实无所谓,后面的分析其实这个基本上没影响),直接右键 Never pause here 永不在此处断下即可:

17

定位 Cookie,首选 Hook 来的最快,通过 Fiddler 等抓包工具、油猴脚本、浏览器插件等方式注入以下 Hook 代码:

(function() {
    // 严谨模式 检查所有错误
    'use strict';
    // document 为要hook的对象 这里是hook的cookie
    var cookieTemp = "";
    Object.defineProperty(document, 'cookie', {
        // hook set方法也就是赋值的方法 
        set: function(val) {
                // 这样就可以快速给下面这个代码行下断点
                // 从而快速定位设置cookie的代码
                console.log('Hook捕获到cookie设置->', val);
                debugger;
                cookieTemp = val;
                return val;
        },
        // hook get 方法也就是取值的方法 
        get: function()
		{
            return cookieTemp;
        }
    });
})();

Hook 发现会有生成两次 Cookie 的情况,断下之后往上跟栈,可以看到组装 Cookie 的代码,类似如下结构:

18

仔细观察这两次 Cookie 生成的地方,分别往上跟栈,你就会发现两个 Cookie 分别是经过了两个不同方法得到的,如下图所示:

19

20

这里的代码存在于 VM 虚拟机中,且是 IIFE 自执行代码,我们还得往前跟栈看看这些 VM 代码是从哪里加载出来的,跟栈来到首页(202页面)带有 call 的位置:

21

我们在文章开头介绍的这个位置就是这么分析得来的,这个位置通常在分析瑞数的时候作为入口,图中 _$te 实际上是 eval 方法,传入的第一个参数 _$fY 是 Window 对象,第二个对象 _$F8 是我们前面看到的 VM 虚拟机中的 IIFE 自执行代码。

在知道了瑞数大致的入口之后,我们也可以使用事件监听中的 Script 断点,一直下一个断点(F8)就可以走到 202 页面,然后搜索 call 关键字就能快速定位到入口,Script 断点中的两个选项,第一个表示运行 JS 脚本的第一条语句时断下,第二个表示 JS 因为内容安全政策而被屏蔽时断下,一般选择第一个就可以了,如下图所示:

22

文件结构与逻辑

想要后续分析 Cookie 的生成,我们不得不要观察一下 202 页面的代码,meta 标签有个 content 内容,引用了一个类似于 c.FxJzG50F.dfe1675.js 的 JS 文件,接着跟一个自执行的 JS,如下图所示:

23

第1部分 meta 标签的 content 内容,每次都是变化的,第2部分引用的这个外部 JS 在不同页面也有所差别,但是同一个网站同一个页面 JS 里的内容一般是固定不会变的,第3部分自执行代码每次变化的只是变量名,整体逻辑不变,后续我们在扣代码的时候,也会用到这里的部分方法。自执行代码里同样也是有很多 if-else 控制流,开头的那个数组,比如上图中的 _$Dk 就是用来控制后续的控制流的。

引用的 c.FxJzG50F.dfe1675.js 直接打开看是乱码的,而自执行 JS 的主要作用是将这 JS 乱码还原成 VM 里的 1w+ 行的正常代码,并且定义了一个全局变量 window.$_ts 并赋了许多值,这个变量在后续 VM 中作用非常大,meta 标签的 content 内容同样也会在 VM 里用到。

由于很多值、变量都是动态变化的,肯定不利于我们的分析,所以我们需要固定一套代码到本地,打断点、跟栈都会更加方便,随便保存一份 202 页面的代码,以及该页面对应的外链 JS 文件,如 c.FxJzG50F.dfe1675.js 到本地,使用浏览器自带的 overrides 重写功能、或者浏览器插件 ReRes、或者抓包工具的响应替换功能(如 Fiddler 的 AutoResponder)进行替换。

24

VM 里面的代码是生成 Cookie 的主要代码,包含众多的 if-else 控制流,无疑增加了我们分析代码的成本,这里就可以使用 AST 技术做一下反混淆,比如 Nanda 就将 if-else 控制流转换成了 switch-case 的,同一个控制流下的代码放在了同一个 case 下,然后在 call 入口那个地方,将 VM 代码做一下本地替换,具体可以参考 Nanda 的文章:《某数4代逻辑分析》,感兴趣的可以试试,不了解 AST 的可以看看以前的文章《逆向进阶,利用 AST 技术还原 JavaScript 混淆代码》,后续有时间再写写 AST 还原瑞数代码的实战,本文咱们选择硬刚!

25

VM 代码以及 $_ts 变量获取

前面我们了解了 VM 代码和 $_ts 的重要性,所以我们第一步是要想办法拿到他们,至于在什么时候有用到,文章后续再说,复制外链 JS,即 c.FxJzG50F.dfe1675.js 的代码和 202 页面的自执行代码到文件,本地直接运行即可,需要轻度补一下环境,缺啥补啥,大致补一下 window、location、document 就行了,补的具体内容可以直接在浏览器控制台使用 copy() 命令复制过来,然后 VM 代码我们就可以直接 Hook eval 的方式得到,大致的补环境代码如下:

var eval_js = ""

window = {
    $_ts:{},
    eval:function (data) {
        eval_js = data
    }
}

location = {
    "ancestorOrigins": {},
    "href": "http://www.脱敏处理.com.cn/new_house/new_house_detail.html",
    "origin": "http://www.脱敏处理.com.cn",
    "protocol": "http:",
    "host": "www.脱敏处理.com.cn",
    "hostname": "www.脱敏处理.com.cn",
    "port": "",
    "pathname": "/new_house/new_house_detail.html",
    "search": "",
    "hash": ""
}

document = {
    "scripts": ["script", "script"]
}

25

观察 $_ts 的 key 和 value,和浏览器中得到的是一样的:

26

注意事项:c.FxJzG50F.dfe1675.js 外链 JS 如果你直接下载下来用编辑器打开可能会被自动编码,和原始数据有出入,导致运行报错,这里建议直接在浏览器在线访问这个文件,手动复制过来,或者在抓包软件里将响应内容复制过来,观察以下两种情况,第一种情况就可能会导致运行出错,第二种是正常的:

27

扣代码

前面说了这么多,现在终于可以进入主题了,那就是扣代码,找个好椅子,准备把屁股坐穿,此时你的键盘只有 F11 有用,不断单步调试,只需要亿点点细节,就完事儿了!

扣代码步骤太多,不可能每一步都截图写出来,只写一下比较重要的,如有遗漏的地方,那也没办法,首先先在我们替换的 202 页面里,自执行代码开始的地方手动加个 debugger,一进入页面就断下,方便后续的分析:

28

通过前面我们的分析,已经知道了入口在 call 的地方,快速搜索并下断点:

29

通过前面我们的分析,我们也知道了有两次生成 Cookie 的地方,快速搜索 (5),搜索结果第二个即为入口:

30

首先单步跟假 Cookie,虽然是假的,但是后续生成真 Cookie 中会用到,在跟的时候你会走到这个逻辑里面:

31

有一步会调用 _$8e() 方法,而 _$8e = _$Q9_$Q9 又嵌套在 _$d0 里的,搜索一下哪里调用了 _$d0,发现是代码开头:

32

那么传入的参数 _$Wn 是啥呢?单步跟入,是一个方法,作用就是取 202 页面的 content 内容,那么我们在本地就直接删掉这个 _$Wn 方法,直接传入 content 的值即可,如下图所示:

33

另外,我们发现,代码有非常多的在数组里面按索引取值的情况,比如上图中的 _$PV[68] 的值,实际上就是字符串 content,很显然我们要把这个数组的来源找到,直接搜索 _$PV = ,可以找到疑似定义和赋值的地方:

34

35

所以我们得看看这个 _$iL 方法,传入了一个非常长的字符串,打断点进去看看,果然生成了 _$PV,是一个 725 位的数组:

36

接下来在扣代码的过程中,你会经常遇到一个变量,在本文中是 _$sX

37

有没有很熟悉?这个值就是我们前面拿到的 $_ts 变量,在开头就可以看到是将 window.$_ts 赋值给了 _$sX

38

继续走,会走到以下逻辑中:

39

这里会遇到六个数组,他们都已经有值了,所以我们得找到他们是咋来的,任意搜索其中一个数组名称,会找到定义和赋值的地方:

40

41

赋值明显是调用了 _$rv 方法,再搜 _$rv 方法,发现是开头就调用了:

42

后续没有什么特别的,一直单步,最后有个 join('') 操作,就生成了假 Cookie:

43

接下来是生成 Cookie 的名字 FSSBBIl1UgzbN7N80T,然后将 Cookie 赋值给 document.cookie,然后又向 localStorage 里面的 $_ck 赋了个值,localStorage 的内容可以直接复制下来,没有太大影响。

44

单步跟真 Cookie,在本文中也就是 _$ZN(768, 1);,可以看到开始进入了无穷无尽的 if-else 控制流:

45

这里本地应该怎样处理呢?我的做法是以 _$Hn 和其值命名函数,function _$Hn768(){} 就表示所有走 768 号控制流的方法,继续跟,生成真 Cookie 的方法基本上在 747 号控制流,后续我们主要以 747 号控制流的各个步骤来看,747 号控制流扣出来的代码大致如下:

46

单步跟 747 号控制流,会有个进入第 709 号控制流的步骤,会取先前生成的假 Cookie,经过一系列操作之后返回一个数组:

47

48

至此我们在本地同步扣的代码,如果正常的话,返回的数组也应该是一样的(后续的数据就不一样了,有一些时间戳之类的参数参与运算):

49

自动化工具检测

继续跟 747 号控制流,会进入 268 号控制流,接着进入 154 号控制流,这里面会针对自动化工具做一些检测,如下图所示:

50

51

这里定义了一个变量 _$iL,检测不通过就是1,后续又把这个变量赋值给了 _$aW,所以我们本地保持一致,也为 false 即可(其实我们不用自动化工具的话,这一段检测就不用管直接返回 false 就行):

52

20 位核心数组

继续跟 268 号控制流,会进入 668 号控制流,668 号控制流就两个操作,一是生成一个 16 位数组,二是取 $_ts 里面的 4 个变量,加到前面的 16 位后面,组成一个 20 位数组,这 20 位数组的最后 4 位是瑞数核心,其中的映射关系搞错了请求是通不过的,在五代中这部分的处理逻辑会更加复杂。

53

54

这里不是单纯的取 $_ts 里的键值对,你在扣代码的时候,你也许会发现怎么本地到这里取值的时候,取出来的不是数字,而是字符串呢?就像下面这种情况:

55

实际上我们最开始得到的 $_ts 值,是经过了二次处理的,我们以第一个 _$sX._$Xb 为例,直接搜索 _$sX._$Xb,可以发现这么一个地方:

56

很明显这里给 _$sX._$Xb 重新赋值了一遍,我们可以看到等号右边,先取了一次 _$sX._$Xb,其值为 _$Rm,这和我们初始 $_ts 里面对应的值是一样的,然后我们就得再看看 _$sX["_$Rm"] 又是何方神圣,直接搜索发现是开头赋值了一个方法,通过调用这个方法来生成新的值:

57

另外其他三个值也是同样的套路,赋值的代码分别为:

_$sX._$Xb = _$sX[_$sX._$Xb](_$BH, _$DP);
_$sX._$oI = _$sX[_$sX._$oI](_$ZJ, _$DS)
_$sX._$EN = _$sX[_$sX._$EN]();
_$sX._$D9 = _$sX[_$sX._$D9](_$iL);

实际上应该是:

_$sX._$Xb = _$sX["_$Rm"](_$BH, _$DP);
_$sX._$oI = _$sX["_$Nw"](_$ZJ, _$DS)
_$sX._$EN = _$sX["_$Uh"]();
_$sX._$D9 = _$sX["_$ci"](_$iL);

进一步来说,实际上是:

_$sX._$Xb = _$1k(_$BH, _$DP);
_$sX._$oI = _$jH(_$ZJ, _$DS)
_$sX._$EN = _$9M();
_$sX._$D9 = _$oL(_$iL);

静态分析没问题,我们可以先固定下来,但是实际应用当中这些值都是动态的,那我们应该怎么处理呢?先来多看几个对比一下找找规律:

58

59

可以发现每次对应的位次都不一样,但是实际上相同位置的方法点进去都是一样的,也就是说,变的只有方法名和变量名,实现的逻辑是不变的,所以我们只要知道了这四个值分别对应的位置,就能够拿到正确的值,在本地,我们就可以这样做:

1、先利用正则匹配出这四个值,如:[_$sX._$Xb, _$sX._$oI, _$sX._$EN, _$sX._$D9]

60

2、再匹配出 VM 代码开头的 20 个赋值的语句,如:_$sX._$RH = _$wI; _$sX._$i5 = _$n5; 等;

61

3、然后通过 $_ts 取这四个值对应的值,相当于:_$sX._$Xb = _$ts._$Xb = _$Rm;然后再找这四个值所定义的方法在 20 个赋值语句中的位置,相当于:查找 _$sX._$Rm = _$1k; 在 20 个赋值语句中的位置为 7(索引从 0 开始)

62

4、我们知道了这四个方法在 20 个赋值语句中的位置,那么我们直接匹配本地对应位置的名称,进行动态替换即可,当然前提是咱们本地已经扣了一套代码出来了:

63

64

经过这样处理后,就能够保证这四个值的准确性了。

其他用到 $_ts 值的地方

除了上面说的 20 位数组里用到了 4 个 $_ts 的值以外,还有其他地方有 7 个值也用到了,直接搜索就能定位,这 7 个值相对较简单,每次都是固定取 $_ts 里面的第 2、3、4、15、16、17、19 位的值,同样的,找到对应位置,进行动态替换即可:

65

注意事项

特别注意 VM 代码开头,会直接调用执行一些方法,某些变量的值就是通过这些方法生成的,当你一步一步跟的时候发现某些参数不对,或者没有,那么就得注意开头这些方法了,可能一开始就已经生成了。

66

后缀 MmEwMD 生成逻辑

后续的其他 XHR 请求中,都带有一个后缀,这个后缀的值同样是由 JS 生成的,每次都会变化,当然不同网站,后缀名不一定都是一样的,本例中是 MmEwMD,先下一个 XHR 断点,当 XHR 请求中包含了 MmEwMD= 时就断下,然后刷新网页:

67

可以看到后传入 l.open() 的 URL 还是正常的,断下后到 l.send() 就带有后缀了,再看 l.open() 其实就是 xhr.open(),明显和正常的有区别,同样这个方法也在 VM 代码里,应该是重写了方法,可以和正常的做对比:

68

跟到 VM 代码里去看看,经过了 _$sd(arguments[1]) 方法就变成了带有后缀的完整链接了:

69

跟进 _$sd 方法,前面都是对 url 做一些处理,后面有个进入第 779 号控制流的流程,实际上就是原来我们生成 Cookie 的步骤,跟一下就行了。

70

善用 Watch 跟踪功能

71

开发者工具的 Watch 功能能够持续跟踪某个变量的值,对于这种控制流很多的情况,设置相应的变量跟踪,能够让你知道你现在处于哪个控制流中,以及生成的数组的变化,不至于跟着跟着不知道到哪一步了。

结果验证

如果整个流程没问题,代码也扣得正确,携带正确的 Cookie 和正确的后缀,就能成功访问:

72

73