Skip to content

瑞数4代

更新: 2025/2/24 字数: 0 字 时长: 0 分钟

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

难度:地狱

网站地址:http://www.fangdi.com.cn/new_house/new_house_detail.html

打桩法

**打桩法也叫环境自吐法,常用于控制流平坦化中,所谓的控制流平坦化,就是一行代码能解决的问题,写成很多行代码,提升爬虫工程师来跟踪调试的难度。**例如瑞数里面的大循环嵌套,里面走了很多的流程,其实很多的流程并不重要,但为了不漏掉重要的步骤,还是需要一步步跟踪,使用打桩法可以让我们的跟踪省力很多。

首先,我们进入瑞数有控制流的js脚本的第一行(注意此时不能再继续运行):

20221101153804

接着,找到控制流开始的地方,在下面两行的左侧点击右键,选择 Add logpoint 选项:

20221101154413

里面填入控制流中的两个重要的变量参数:

20221101154824

填入后保存,此时左侧就会出现粉红色的标志:

20221101155139

这时我们再运行js脚本,就可以看到在控制台输出中该控制流走了440步,每步变量的值也都在左侧一一显示,这样就方便我们进行跟踪了。

20221101155442

充分认识

请求流程

前面讲过瑞数有三次请求,首次请求响应码是202(瑞数3、4代)或者412(瑞数4代、5代)并且返回cookie_s,接着请求一个js混淆文件生成cookie_t,携带cookie_t才能去成功请求页面:

20220825170801

第一次请求返回状态码202与cookie_s其为 FSSBBIl1UgzbN7N80S 的值,其中80代表http服务器的端口号,如果值是443,代表https服务器的端口号:

20220826114036

四大部分

因为后续要分析Cookie的生成,所以必须要观察一下这个状态码为202页面的代码,如下图所示:

20863124-5b34855ee3317ebe

第1部分,meta标签有个content内容,每次都是变化的,它会在后面的VM部分中用到;

第2部分,引用了一个名称为 c.FxJzG50F.dfe1675.js JS文件,虽然它的内容是乱码,但它会在第3部分中还原(这个外部JS在不同网站中内容也是有所差别的,但在同一个网站中内容一般是固定的);

第3部分,一个自执行的JS函数,变量名每次都是变化的,但整体逻辑不变的,它的主要做了两件事,一是给全局变量 window.$_ts 赋了很多属性变量,二是将第2部分引用的JS还原并通过 call 函数生成的VM部分的1w+行的正常代码;

20863124-9dfb80b26209d36f

VM部分,主要作用就是生成cookie。

430bdef6ee004971a626a73f245b5f6f

中间人攻击

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

20863124-fec1d1efcd63ecae

?> 提示:由于很多值、变量都是动态变化的,我们在处理新的网站时,全都可以用下面的步骤最左侧的行数进行定位。

逆向流程

这里扣代码步骤太多,不可能每一步都截图写出来,只写一下比较重要的,如有遗漏的地方,也没办法。

简单补环境

首先,我们需要简单的补一下环境:

javascript
// 补环境
var eval_js = ""

window = {
    $_ts: {},
    eval: function (data) {
        eval_js = data
    },
    location: {
        "ancestorOrigins": {},
        "href": "http://www.fangdi.com.cn/new_house/new_house_detail.html",
        "origin": "http://www.fangdi.com.cn",
        "protocol": "http:",
        "host": "www.fangdi.com.cn",
        "hostname": "www.fangdi.com.cn",
        "port": "",
        "pathname": "/new_house/new_house_detail.html",
        "search": "",
        "hash": ""
    },
    document: {
        "scripts": ["script", "script"]
    }
}

定位Cookie

打开Chrome无痕(没有浏览器缓存),监听script断点,然后访问网页,会在图中位置断住:这一步加载了上面第2部分中外链js文件代码,并赋给了 $_ts 对象。

4f9e5b4b10274143ab84a46fe02259b3

注意,如果你直接通过JS外链下载下来用编辑器打开可能会被自动编码,和原始数据有出入,导致运行报错,这里建议直接在浏览器中访问这个文件手动复制过来或在抓包软件里将响应内容复制过来;

20863124-41a0eb76c43b53ed

继续往下调试,跳到如下图:也就是上面的第3部分

91d575de519041c18631e0e13213c0b0

我们也将这部分自执行代码的内容拷贝到工具当中:

20220831150004

在第3部分中搜索 .call 发现只有一个函数,这里就是上面VM部分的入口。此时,_$xD 变量为函数eval,_$qQ 变量为全局变量window,_$2M 变量为还原后的第2部分中外链js文件代码:

20863124-9dfb80b26209d36f

点击右上角的单步调试,跳到如下图:也就是上面的VM部分

430bdef6ee004971a626a73f245b5f6f

进来后就要定位Cookie,可以最左侧的行数进行定位,当然也可以通过Hook来定位,使用Fiddler等抓包工具、油猴脚本、浏览器插件等方式注入以下Hook代码:

javascript
(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 生成的地方,分别往上跟栈,会发现两个 Cookie 分别是经过了两个不同方法得到的。第一次生成的是一个假Cookie,但这个假Cookie后面会参与到真Cookie的生成当中:

20863124-5e9c71d487957b33

20863124-83f550a3d5a337bb

?> 提示:这里还有一个方法可以快速定位,就是搜索 (768, 1)直接定位到第二次Cookie生成的地方,且这个数值是不改变的。

假Cookie逻辑

首先,从假Cookie的生成逻辑开始逆向扣代码,在跟的时候你会走到这个逻辑里面:

20863124-12a02b9de1db635f

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

20863124-171dc1feb5b441db

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

20863124-77a9e47f9b6167fa

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

20863124-51b1cb09911036b6

20863124-6cad4960c8293d8b

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

20863124-b9908172e6eadb4f

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

20863124-a67a63b43be3511a

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

20863124-87c647d00b2ed80b

继续走,会走到以下逻辑中,这里会遇到六个数组,他们都已经有值了:

20863124-d32a70eca112dd2d

任意搜索其中一个数组名称,会找到定义和赋值的地方:

20863124-4cf49267f144b0ad

20863124-ea5bc9f79772a7b2

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

20863124-759580432c2b59f6

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

20863124-5487a2863c0a36b4

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

20863124-ee352072e7ab1548

真Cookie逻辑

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

20863124-59b13206765367ba

走 768 号控制流的方法,继续跟,生成真 Cookie 的方法基本上在 747 号控制流,单步跟 747 号控制流,会有个进入第 709 号控制流的步骤,会取先前生成的假 Cookie,经过一系列操作之后返回一个数组:

20863124-90c0fddbe2f96c93

20863124-ec7117c2d5378779

自动化工具检测

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

20863124-7c167ab51759715f

20863124-fcc1f06ba2eaa75c

20位核心数组

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

20863124-40e22ee107ead7eb

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

20863124-3dfb2cb3698b68fe

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

20863124-1f330f99b629eed7

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

20863124-962c3977dfbedf0a

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

20863124-61e7499c66bbc313

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

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

20863124-b7afdc9ab404c3c8

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

20863124-9ea4bce19c4ba33e

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

20863124-27cb9becdb9825b6

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

20863124-0c6cf0646b4bfcc7

20863124-c9bb29a071c362e0

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

20863124-bac4368743424792

MmEwMD逻辑

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

20863124-4a257a776599c5c5

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

20863124-19c7858e2d769fe1

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

20220831111010

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

20863124-ded308ed7d628759

注意事项

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

20863124-d662564da77351a2

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

20863124-0227c1782689e46b

爬虫代码

首先需要轻度补一下环境,缺啥补啥,大致补一下 window、location、document 就行了,大致的补环境代码如下:

javascript

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

20863124-6df8e21fcbc10108