数据抓取实践:对加密参数及压缩混淆 JS 的逆向分析

转载自 https://www.v2ex.com/t/455492#reply15 如需要转载请联系作者本人

本文会介绍几种分析技巧,需要一点前端知识(总感觉在前端做防爬没什么意义,因为源码都是公开的)。文末附上爬虫 Demo 验证,虽然对于这个案例来说使用 Selenium 可能才是合适的解决方法,但暴力破解才是男人的浪漫!...嗯本文的重点只是在于分析解密的过程。有些图片因代码过长未包含在内,意会即可。

1. 一夫当关 - XHR Breakpoints

网站是七麦数据。我们要抓取的内容是页面上的 App Store 排行榜数据。

通过分析网络请求我们可以发现,榜单数据是通过 Ajax 请求来获取的。返回的数据格式是明文 Json。

请求参数如下:

analysis: dDB4Fi8wUEF...(太长,略)
brand: all
country: cn
device: iphone
genre: 36
date: 2018-05-14
page: 1

各个参数所代表的含义都较为易懂,除了 analysis。猜测是一个经过 Base64 编码后的加密参数,事实上的确如此,隔一段时间再利用相同的 analysis 提交请求时会被拒绝。

要解密参数,只能去看 JS 的加密代码。我们需要查看是哪部分的 JS 代码发起了请求,一般的方法是点击请求列表的 Initiator 跳转到代码部分。

不过我更推荐另一种方法:添加 XHR Breakpoints 拦截请求。这样有两个好处,一是可以直接观察代码上下文的变量在发出请求前的数值,二是方便直接调试。此处填入 URL 包含的关键词 indexPlus

我们也可以在 Watch 处添加变量 h 进行观察,得知 h 是一个 XHR 对象。

然后再来看代码……写得乱七八糟的是什么鬼!

2. 穿针引线 - Module Require

为了应对 Web 应用越来越复杂的趋势,前端趋向模块化开发,各种自动化构建工具成为必不可少的开发利器。跳蚤大神说得好呀,若想真正掌握爬虫技术,就要了解一个网站是怎样构建起来的。在此意义上,学习爬虫所需的前置知识还挺多的。

来观察这堆 JS 中的一段代码。

"7O1s": function(t, e, n) {
    var r = n("DIVP")
        , i = n("XSOZ")
        , o = n("kkCw")("species");
    t.exports = function(t, e) {
        var n, a = r(t).constructor;
        return void 0 === a || void 0 == (n = r(a)[o]) ? e : i(n)
    }
},
"7UMu": function(t, e, n) {
    var r = n("R9M2");
    t.exports = Array.isArray || function(t) {
        return "Array" == r(t)
    }
},
"7gX0": function(t, e) {
    var n = t.exports = {
        version: "2.5.5"
    };
    "number" == typeof __e && (__e = n)
},

虽然代码经过了混淆,但是依然可以看出类似 7O1s 之类的随机字符串是模块名称,而 var r = n("DIVP") 即引入模块,正常的写法可能是 import a from 'b' 或者 const a = require('b')

这里发起 Ajax 请求的函数很可能只是一个被封装了的模块供整个项目调用,粗略看一下函数代码也没有发现计算加密的部分。

针对这种模块化开发,一个逆向的思路是,只要查看该模块被引用的情况,不断向上追溯,总能找到最初发起请求和加密的函数。

将网站所有 JS 文件拷贝到本地,检索断点所在的模块名 7GwW

得知其由模块 KCLY 引入,接着检索 KCLY

得知其有三个模块引入,不怕,我们先选第一个XmWM继续检索,如果没找到再回来

得知其由模块 tIFN 引入,继续检索,由mtWM引入,继续检索

得出最终结果,是由模块 gXmS 组装的请求参数。

只要找到组装请求的代码,分析过程就算完成了一半。

3. 顺藤摸瓜 - Call Stack

可能有人要抱怨了,感觉这样查找好麻烦呀,有没有更简便的方法?

当然有。在理解了第二点分析的模块化组织代码的原理后,我们可以使用更简便的方法——Call Stack。

如图,通过从上至下依次查看调用栈上的代码,检查一下其所在的模块是否是要查找的目标。

get 方法即是模块 gXmS 中的函数。需要注意的是,此方法在模块的末尾,如果不细心可能会错过。

4. 偷天换日 - Hijack The Response

先在目标模块 gXmS 下个断点吧

可以看到虽然变量 f 被很机智地用 Base64 重新编码了(不完全是,还有个解密函数,注意这里的 p.g 和 p.a),但是在调试器下其解码值 analysis 很容易暴露。

不过重点还是在 l.a.interceptors.request.use 的那行,里面是完整的参数组装过程,附这一段代码:

l.a.interceptors.request.use(function(a) {
    try {
        var n = +new Date - (h.difftime ? h.difftime : 0) - 1515125653845
            , e = ""
            , t = [];
        return void 0 === a.params && (a.params = {}),
        m()(a.params).forEach(function(n) {
            if (n == f)
                return !1;
            a.params.hasOwnProperty(n) && t.push(a.params[n])
        }),
        t = t.sort().join(""),
        t = Object(p.d)(t),
        t += "@#" + a.url.replace(a.baseURL, ""),
        t += "@#" + n,
        t += "@#1",
        e = Object(p.d)(Object(p.g)(t)),
        -1 == a.url.indexOf(f) && (a.url += "?" + f + "=" + encodeURIComponent(e)),
        a
    } catch (a) {}
}, function(a) {
    return i.a.reject(a)
}),

代码被用逗号运算符精简了。由于 Chrome 调试器的单步执行是以表达式为单位,因此这里我们无法对重要的变量 t 的每一步转换进行观察调试。

我们需要对代码进行修改,然后让浏览器运行修改后的代码。方法是劫持 JS 文件。具体手段有很多种,比如 Nginx 反代,Fiddler 等。在此我选择 ReRes 插件,用法很简单就不具体介绍了。

劫持后,我们可以随意更改代码以方便输出获取我们想要的信息,如下图我加了一个 v1 中间变量。

通过单步调试后,得出组装的过程,大致步骤如下:

  1. 设置一个时间差变量
  2. 提取查询参数值(除了 analysis)
  3. 排序拼接参数值字符串并 Base64 编码
  4. 拼接自定义字符串
  5. 自定义加密后再 Base64 编码
  6. 拼接 URL

那么如何得知自定义加密函数和 Base64 编码函数?

通过这一行 var v1 = Object(p.d)(t); 我们可得知 p.d 是 Base64 编码函数,因为单步调试时可观察到变量 t 经过这行代码后被编码。

然后看这一行 e = Object(p.d)(Object(p.g)(t));,我们已知 p.d 是 Base64 编码函数,那么 p.g 就是一个未知函数。需要找到 p.g 的函数本体。

在 Watch 一栏添加 p.g,点击 FunctionLocation 的值,即可跳转到该函数。其实 p.g 同时也是一个解密函数,异或运算经常被用来加解密。

5. 东搜西罗 - Searching

这里再多说一点技巧。由于模块化开发,可能会引用到第三方库的模块,因此我们可以利用模块中的一些附带字符串在网上搜索也许会得到额外的信息。

搜索相关错误信息后得知网页用了 Node.js 的 Buffer 模块。

进一步分析还发现引入 Buffer 模块的目的之一就是为了方便 Base64 编码。

6. 一锤定音 - Crawler

最后写一个50行的简单爬虫来验证分析,抓取 iPhone 免费榜单。

#!/usr/bin/env python3

import time
import json
import base64
import requests
from urllib.parse import urlencode


headers = {
    "Accept": "application/json, text/plain, */*",
    "Referer": "https://www.qimai.cn/rank",
    "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:57.0) Gecko/20100101 Firefox/59.0"
}

params = {
    "brand": "all",
    "country": "cn",
    "date": "2018-05-14",
    "device": "iphone",
    "genre": "36",
    "page": 1
}


# 自定义加密函数
def encrypt(a, n="9d1abd758c043319aee5ee1c0e3f26c6"):
    s = list(a)
    n = list(n)
    sl = len(s)
    nl = len(n)
    for i in range(0, sl):
        s[i] = chr(ord(s[i]) ^ ord(n[i % nl]))
    return "".join(s)


def main():
    # iPhone 免费榜单
    # 提取查询参数值并排序
    s = "".join(sorted([str(v) for v in params.values()]))
    # Base64 Encode
    s = base64.b64encode(bytes(s, encoding="ascii"))
    # 时间差
    t = str(int((time.time() * 1000 - 1515125653845)))
    # 拼接自定义字符串
    s = "@#".join([s.decode(), "/rank/indexPlus/brand_id/1", t, "1"])
    # 自定义加密 & Base64 Encode
    s = base64.b64encode(bytes(encrypt(s), encoding="ascii"))
    # 拼接 URL
    params["analysis"] = s.decode()
    url = "https://api.qimai.cn/rank/indexPlus/brand_id/1?{}".format(urlencode(params))
    # 发起请求
    res = requests.get(url, headers=headers)
    j = json.loads(res.text)
    print(j)


if __name__ == '__main__':
    main()

ps:如果觉得文章不错的话,欢迎随手点赞转发支持

原文发布于微信公众号 - 日常学python(daily_learn)

原文发表时间:2018-07-29

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏从零开始学自动化测试

Selenium2+python自动化44-元素定位参数化(find_element)

前言 元素定位常用的有八种方法,这个能看到这一篇的小伙伴都知道了,那么有没有一种方法,可以把常用的八种定位合为一种呢?也就是把定位的方式参数化,如id,name...

3475
来自专栏Python小屋

BeautifulSoup解析库select方法实例——获取企业信息

本文内容由浙江浦江中学方春林老师提供。 Requests 是用Python语言编写,基于urllib,采用Apache2 Licensed 开源协议的 HTTP...

3345
来自专栏Java帮帮-微信公众号-技术文章全总结

JavaWeb08-XML,tomcat,HTTP轻松入门

XML&tomcat&HTTP 一.XML基础知识 1. xml介绍 XML 指可扩展标记语言(EXtensible Markup Language),也是一种...

4079
来自专栏从零开始学自动化测试

Selenium2+python自动化39-关于面试的题

前言 最近看到群里有小伙伴贴出一组面试题,最近又是跳槽黄金季节,小编忍不住抽出一点时间总结了下, 回答不妥的地方欢迎各位高手拍砖指点。 一、selenium中如...

3366
来自专栏java一日一条

JAVA:自定义套件...

各位亲爱的小伙伴们大家好,最近很多伙伴都问我该怎么做自定义套件封装,我在这里做了一个教程分享给大家。

802
来自专栏Google Dart

Flutter Widget框架之旅 顶

Flutter小部件采用现代反应式框架构建,从React中获得灵感。 中心思想是你从小部件中构建你的UI。 小组件描述了他们的视图在给定其当前配置和状态时应该看...

6142
来自专栏安恒网络空间安全讲武堂

Web for Pentester 实验合集

0x00 introduction Pentester Lab 是渗透测试学习实战平台,在里面提供各种漏洞实验的虚拟机镜像文件,让网络安全爱好者和黑阔进行实战式...

2595
来自专栏为数不多的Android技巧

如何安全地打印日志

如何打印日志?这不是很简单,直接使用android.util.Log这个类不就行了?然而,日志属于非常敏感的信息;逆向工程师在逆向你的程序的时候,本来需要捕捉你...

2003
来自专栏向治洪

Google V8引擎

V8的前世今生 V8是JavaScript渲染引擎,第一个版本随着Chrome的发布而发布(具体时间为2008年9月2日)。在运行JavaScript之前,相比...

4475
来自专栏Golang语言社区

说说JSON和JSONP,也许你会豁然开朗-转

今天在写底层通信框架的时候,遇到了跨域的问题;随便给不知道的童鞋们分享下基础知识。 前言   由于Sencha Touch 2这种开发模式的特性,基本...

4086

扫码关注云+社区

领取腾讯云代金券