前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【瑞数】维普期刊JS逆向详细流程

【瑞数】维普期刊JS逆向详细流程

作者头像
andrew_a
发布2021-06-25 20:50:12
2.7K0
发布2021-06-25 20:50:12
举报
代码语言:javascript
复制
文章来源:https://blog.csdn.net/qq_35491275/article/details/117307069
作者:mkdir700
本篇文章为作者授权转载

前言

我所用的方法基于浏览器环境的,非硬解(头秃ing?),文章较长,建议收藏。

这是我第一次接触瑞数加密,比较难,不过学到的东西也是挺多的,也是因为我第一次解瑞数,所以文章写得比较详细甚至是啰嗦,这篇文章大致是以我逆向的思路去写的,应该适合像我这样从未接触过瑞数的朋友。

这次逆向总结,估计会写 3 到 4 篇文章。

  1. 接口签名的生成与获取
  2. cookie 的生成与获取
  3. 基于浏览器环境的爬虫如何部署?
  4. 关于本次瑞数解密的总结

本文中也会有一些调试技巧夹在其中,如有问题或更好的建议欢迎提出!

本文以维普期刊的高级检索接口作为示例,地址:http://qikan.cqvip.com/Qikan/Search/Advance?from=index


正文开始。


过 debugger

定时器 debugger

拿到网站后,一如既往的直接打开审查工具,在这一步直接被 debugger 卡住了。

这是一个定时器无限 debugger,如果两次时间差大于 100,那么就会一直让我们处于 debugger 状态。

当遇到这种反调试的手法时,可在进入 debugger 状态后,在 console 中输入以下代码,以此跳过。

代码语言:javascript
复制
for (var i = 1; i < 99999; i++)window.clearInterval(i);

参考:爬虫漫游指南:瑞数的反调试陷阱

死循环 debugger

过了这一步后,当我回到网页,又会直接进入 debugger 状态。这个 debugger 是在一个判断下,这个比较简单我们直接右键选着**“永不在此处暂停”**。

**注意:**以上步骤都是在谷歌浏览器中调试的,使用火狐情况会不一致,建议使用谷歌浏览器。

这些 if 判断都是在一个 while(1) 循环内,使用火狐会一直在循环内,谷歌只要设置了不在此处暂停就没事,具体为啥不知道。

分析搜索接口

输入关键词,点击检索。很容易找到请求路径为 SearchList?... 的链接就是数据接口。

查看这个请求,需要搜索参数 searchParamModel 和签名 G5tA5iQ4

查看这个请求的调用栈

这里可以先去看看 window.advSearch 这个方法。这里的 urldata 组合后,并不存在这个字段 G5tA5iQ4

所以这个字段的值不是在这里构建,这个字段对应的值就是签名,也是我们必须要解决的。

先这里提前解释下,为什么这里明明没有设置 G5tA5iQ4 的值,却在请求发送时,含有这个签名。

原因很简单,XMLHttpRequestsend 方法被修改了。下图是两者的对比。

签名在何处生成?

进入第一个函数

在这里打上断点,再去点击检索按钮,进入调试。

注意看这个 arguments,这是请求的参数。

由于点击一次检索按钮会有好几个请求,所以这里会有多个请求经过这里。

我们需要调试的是 SearchList 这个接口,所以只要不是 SearchList 的接口参数,直接跳过即可。

结合上文的接口分析,searchParamsModel 就是搜索参数。

不过,不要忘了。我们的目的是找签名在何处生成。

**小提示:**上文说过 XMLHttpRequestsend 方法被修改过,实际上,上图的 _$hp 就是所谓的 send 方法。

此时查看 this

继续往下找,查看 _$a5,(变量名每次请求都不同,知道是它就行),展开并查看它的第一个作用域。

可以看到,在此处的 _$q3 的值就是一个带有签名的链接。

_q3 属于 _M3,那么就可以按 Ctrl+F 搜索关键字 _

注意:_$M3 是一个函数,所以你应该找到是类似下面这样的函数

我们得知 _q3 作为参数传进了 _M3,所以在这个函数内打上断点,重新调试

当我们进来的时候,查看 _q3,发现 _q3 只是请求链接,所以由此可知,签名就是在 _M3 中生成并拼接到 _q3 上的。

为了方便调试,我们可以将这里的断点换成条件断点,即只调试 _$q3==="/Search/SearchList" 这种情况。

将内部函数折叠,可以看到 _M3 内部只是调用了一下 _Vq 函数。

展开 _Vq 函数,分析代码。在这个 _Vq 函数里面,多折腾几下总能找到签名是何处生成的。

当然也可以用点小技巧,我们知道签名最终会拼接到 _$q3 上,所以必然存在类似这样的代码:

代码语言:javascript
复制
_$q3 += 
或者
_$q3 +

断点调试到此处,可知签名是调用 _5Q(_hp, _M_, _No) 得来的。

查看这个函数的三个参数,可知它们分别是 0大小为 16 的整数数组以及一个 undefined

在这里,_M_ 是整数数组,我们可以向上寻找,查看 _M_ 是如何生成的。

可以找出整数数组是将 /Search/SearchList 作为参数传入某个函数而生成的。

中场休息

分析到这里,我们知道了签名生成的流程如下:

  1. 当用户点击搜索按钮,触发点击事件;
  2. 构建请求对象(请求对象的参数没有签名关键字);
  3. 由于 send 方法被修改,所以调用 send 方法时,签名就在这个过程中被生成;

签名代码来源分析

其实你应该发现了,分析了这么久的 JS 代码,却不知道这大段 JS 存放在哪里?

像下图这样,JS 来源显示为 VM+数字 的形式,这就说明这些 JS 代码是后来加载进引擎的。

换句话说就是,这些 JS 代码并不是存在一个 JS 文件里的,实际上是通过 eval 函数将一大堆字符串加载进了内存。

此时就需要寻找以上 JS 代码是如何加载进内存。

这个也是瑞数加密的一大特色,这些加载 JS 代码的代码本身就是被混淆的,并且存在于 Html 页面中。

查看搜索页面源码:view-source:http://qikan.cqvip.com/Qikan/Search/Advance?from=index

**Tips:**如果是加载是空白,那么你需要先正常访问一次搜索页面再查看源代码。这个是 Cookie 的原因,具体的生成机制及解决办法会在下一章讲解到。

这一行代码的后面,就是一堆 JS 代码,我们可以将整个网页代码拷贝至本地编辑器。

将 html 代码格式化后查看。

一开始就加载了一个 JS 文件,为了调试方便,可以将这个 JS 文件下载到本地

http://qikan.cqvip.com/NJDrTcXo8msX/leE4DkIasHMb.f22c526.js

查看 leE4DkIasHMb.f22c526.js,一堆杂乱的字符,其主要的作用就是为 window.$_ts 赋值。

html 代码引入 leE4DkIasHMb.f22c526.js 后,紧接着就是一个自调用的被混淆的 JS 代码。

这段代码的核心作用就是将 leE4DkIasHMb.f22c526.js 中的杂乱字符串通过特定方式还原为代码并加载进内存。

此时的主要工作就是找到,杂乱字符串变成规则字符串代码的位置。

为什么要这么做,在这里举个简单的例子。


举例时间到!

现在这样一串字符串, Y29uc29sZS5sb2coJ2hpaGloaWhpLi4uJyk=

这很常见,这是通过 base64 编码后得到的字符串,那么我们可以通过 base64 解码得到本来的字符串,然后使用 eval 函数执行即可。

以上是原本的功能,打印输出了 hihihihi...

现在我们需要修改这个代码输出的内容,而这可以通过字符串替换方式实现,就像这样。

那么下面的流程图,我想应该可以理解了。

上方的例子是因为我们知道编码方式是 base64,所以可以轻松的将密文转为明文。

对于不常见的加密方式,我们就只有去调试找出明文生成的位置,再加上 JS 代码本事就是混淆的,所以难度就有所提升。

举例结束。


签名代码在何处加载到内存?

仔细想一想,一段字符串想以 js 代码的形式加载进内存,必定会使用 eval 方法。

所以,我们只需要找到哪里使用了调用 eval 即可。

搜索关键词 eval,运气很好可以直接搜索到,下面赋值的操作执行即,_$vo 就是 eval

此时搜索关键词 _$vo,发现有 93 个匹配项

_vo 作为函数,如果被调用,那么调用的写法可能是这样 _vo()

则搜索关键词 _$vo(,匹配项为 0 个。

函数的调用还有一种 函数名.call 的方式,所以不妨搜索 _$vo.call 试试看。

找到一个匹配结果,在这里打上断点,调试过来看看传入的参数是什么。

可见,_$kw 的值就是代码字符串了。

我们在 eval 执行前注入自己的代码即可达成目的。

注入代码

为了大家方便阅读,我将上一步得到的明文代码字符串称为签名代码

虽然这代码还有其他的功能,但对我们来说,只想通过这段代码获取签名,仅此而已。

注入代码是为了让我们可以更方便的获取到签名,最简单的办法就是将签名设置为一个全局变量。

除此之外,为了方便后续调试还可以剔除其中烦人的 debugger

设置签名为全局变量

如果有些忘记了签名是如何生成的,可以先翻到第三节回忆回忆。

由于代码的变量是变化的,所以我们不能直接使用 replace,而是应该用正则匹配的方式去替换或插入代码。

以上是两次请求签名生成的代码行。

在变化中找不变,_f_[5] 和 _A4[5] 它们都是取索引值 5

所以写出正则如下:

代码语言:javascript
复制
(_\$[\w\d_$]{2}) \+= _\$[\w\d_$]{2}\[5\] \+ (_\$[\w\d_$]{2}\([^)]+\);)

Tips:这是基于格式化的 js 代码写的正则,实际的签名代码是被压缩的,所以应该把多余的空格删除。

代码语言:javascript
复制
(_\$[\w\d_$]{2})\+=_\$[\w\d_$]{2}\[5\]\+(_\$[\w\d_$]{2}\([^)]+\);)

匹配出这一段后,我们可以在原本的代码后面,再加上一句全局变量赋值。

那么可以写出 JS 代码

代码语言:javascript
复制
签名代码.replace(/(_\$[\w\d\$]+)\+=_\$[\w\d\$]+\[5\]\+(_\$[\w\d\$]+\([^)]+\);)/gm, `$1="?"+$2window.genUrl=$1;`);

剔除 debugger

在最初分析搜索接口时,就遇见了两个 debugger

一个是明文显示的,这个比较简单,使用 签名代码.replace('debugger', '') 剔除即可。

另一个定时器 debugger 则稍稍有点麻烦。

经过多次调式,可以发现整个代码也是在一个 while 循环中跑,这是瑞数的一大特色。

且 if 语句比较的值是没有变化的,都是变量小于 256,这为我们注入代码提供了方便。

我们以注入的方式,while 内的第一个 if 语句的上方插入以下代码:

代码语言:javascript
复制
console.log(_$Mx)

这个 _$Mx 是一直做比较的,通过层层的 if else,最终执行某段代码。

当进入调试工具后,只要进入了此循环,就会打印 _$Mx

而当进入了定时器 debugger,此时循环停止,通过最后输出的数字,就可以找到进入定时器 debugger 的入口

**Tips:**这一步可能会卡着,稍微等等。

注入后的代码

进入定时器 debugger 后

最后的数字是 388,记着这个 388,一路跟着 if 走就可以看到类似如下代码:

这个代码就是进入定时器 debugger 的入口,那么我们只需要将这行代码注释或者删除即可

代码语言:javascript
复制
签名代码.replace(/(<389\){)[^}]+/gm, `$1`);

至此所有的 debugger 都已去除。

小结

上方的所有代码注入都是在 html源码 上进行的。

这里先理一理我们的流程:

  1. 请求搜索页面,获得页面 html 源码
  2. python 对 html 源码进行修改
  3. 将 html 放入浏览器运行
  4. 调用签名方法获取签名

上方的注入是在 html 源码中进行的。实际情况是使用 python 来完成代码注入。

画个图来说明下,即使用 Python 修改 html 源码,使得 html 中的 js 代码能过将目标代码注入到签名代码中

代码注入示例:

代码语言:javascript
复制
# -*- coding: utf-8 -*-
"""
Created on 2021/5/24 17:15
---------
@summary: 注入代码
1. 去除定时无限debugger
2. 去除死循环debugger
3. 插入searchList方法,用于生成签名
4. 将得到的签名提升至全局变量,可通过 `genUrl` 访问
---------
@author: mkdir700
@email:  mkdir700@gmail.com
"""
import re


def purify_html():
    with open("raw.html", encoding="utf-8") as f:
        text = f.read()
    
    r = re.findall("_\$[\w\d\$]{2}\(79,_\$[\w\d\$]+\);", text)[0]
    var_name = re.findall("_\$[\w\d\$]{2}\(79,(_\$[\w\d\$]+)\);", r)[0]
    
    pp = """%(var_name)s = %(var_name)s.replace(/(_\$[\w\d\$]+)\+=_\$[\w\d\$]+\[5\]\+(_\$[\w\d\$]+\([^)]+\);)/gm, `$1="?"+$2window.genUrl=$1;`);%(var_name)s = %(var_name)s.replace(/(<389\){)[^}]+/gm, `$1`);%(var_name)s = %(var_name)s.replace("debugger", "");""" % {
        'var_name': var_name}
    
    result = text.replace(r, pp + r)
    result = result.replace("/NJDrTcXo8msX/leE4DkIasHMb.f22c526.js",
                            "http://qikan.cqvip.com/NJDrTcXo8msX/leE4DkIasHMb.f22c526.js")
    result = re.sub(r"(/dist)", "http://qikan.cqvip.com/dist", result)
    result = result.replace("</body>",
                            '<script>searchList=function(a){$.ajax({url:"/Search/SearchList",type:"post",dataType:"html",data:{searchParamModel:a},beforeSend:function(){loadding()},complete:function(){loaddingClose()},success:function(){console.log("请求成功")},error:function(){loaddingClose()}})};</script></body>')
    # print(result)
    
    with open('pure.html', "w", encoding="utf-8") as f:
        f.write(result)
    
    print("HTML页面代码注入完成")

**Tips:**签名的触发机制是发送请求,所以注入了一个 ajax 请求,可供我们手动调用。

签名测试

  1. 打开搜索页面
  2. 右键查看搜索页面源码
  3. 使用 python 脚本注入代码,生成新的 html 文件
  4. 在新的 html 文件同目录下,启动简单的 web 服务
代码语言:javascript
复制
python -m http.server 9000
  1. 访问 http://localhost:9000/pure.html
  2. 打开审查工具,调用 searchList 方法,接着访问 genUrl 变量
  1. 在浏览器中获取 cookie
  2. 将 cookie 和签名带入,测试请求是否成功,发送请求的脚本如下:
代码语言:javascript
复制
# -*- coding: utf-8 -*-
"""
Created on 2021/5/23 16:38
---------
@summary: 
---------
@author: mkdir700
@email:  mkdir700@gmail.com
"""
import requests
payload = "searchParamModel=%7B%22ObjectType%22%3A1%2C%22SearchKeyList%22%3A%5B%7B%22FieldIdentifier%22%3A%22M%22%2C%22SearchKey%22%3A%22%E5%8C%97%E5%A4%A7%22%2C%22PreLogicalOperator%22%3A%22%22%2C%22IsExact%22%3A%220%22%7D%5D%2C%22SearchExpression%22%3A%22%22%2C%22BeginYear%22%3A%22%22%2C%22EndYear%22%3A%22%22%2C%22JournalRange%22%3A%22%22%2C%22DomainRange%22%3A%22%22%2C%22PageSize%22%3A%220%22%2C%22PageNum%22%3A%221%22%2C%22Sort%22%3A%220%22%2C%22ClusterFilter%22%3A%22%22%2C%22SType%22%3A%22%22%2C%22StrIds%22%3A%22%22%2C%22UpdateTimeType%22%3A%22%22%2C%22ClusterUseType%22%3A%22Article%22%2C%22IsNoteHistory%22%3A1%2C%22AdvShowTitle%22%3A%22%E9%A2%98%E5%90%8D%E6%88%96%E5%85%B3%E9%94%AE%E8%AF%8D%3D%E5%8C%97%E5%A4%A7%22%2C%22ObjectId%22%3A%22%22%2C%22ObjectSearchType%22%3A%220%22%2C%22ChineseEnglishExtend%22%3A%220%22%2C%22SynonymExtend%22%3A%220%22%2C%22ShowTotalCount%22%3A%220%22%2C%22AdvTabGuid%22%3A%224361c899-1a1a-2b2b-eefe-cf94a80612f7%22%7D"

cookies = input("请键入cookies:\r\n")
genUrl = input("请键入genUrl:\r\n")

session = requests.Session()
session.headers = {
    "Accept": "text/html, */*; q=0.01",
    "Accept-Encoding": "gzip, deflate, br",
    "Accept-Language": "zh-CN,zh;q=0.9",
    "Cache-Control": "no-cache",
    "Connection": "keep-alive",
    "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
    "Cookie": cookies,
    "Host": "qikan.cqvip.com",
    "Origin": "http://qikan.cqvip.com",
    "Pragma": "no-cache",
    "Referer": "http://qikan.cqvip.com/Qikan/Search/Advance?from=index",
    "sec-ch-ua": '" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"',
    "sec-ch-ua-mobile": "?0",
    "Sec-Fetch-Dest": "empty",
    "Sec-Fetch-Mode": "cors",
    "Sec-Fetch-Site": "same-origin",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36",
    "X-Requested-With": "XMLHttpRequest",
}

resp = session.request(
    "POST",
    "http://qikan.cqvip.com/Search/SearchList"+genUrl,
    data=payload
)
print(resp.url)
print(resp.status_code)

效果展示

状态码:成功-200,异常-400

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-05-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Python爬虫scrapy 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 过 debugger
    • 定时器 debugger
      • 死循环 debugger
      • 分析搜索接口
      • 签名在何处生成?
      • 中场休息
      • 签名代码来源分析
      • 签名代码在何处加载到内存?
      • 注入代码
        • 设置签名为全局变量
          • 剔除 debugger
            • 小结
            • 签名测试
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档