首页   注册   登录
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
华为云
V2EX  ›  分享创造

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

  •  1
     
  •   lxy · 89 天前 · 1406 次点击
    这是一个创建于 89 天前的主题,其中的信息可能已经有所发展或是发生改变。

    本文也发布在我的博客

    之前在 V 站上看到一个帖子,内容是楼主想抓取数据的网站做了参数加密,不知如何解密。刚好最近有点空闲时间,可以尝试解密一下网页参数,而且很久没练手了。

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

    1. 一夫当关 - XHR Breakpoints

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

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

    1.png

    请求参数如下:

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

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

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

    image

    image

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

    image

    我们也可以在 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

    image

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

    image

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

    image

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

    image

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

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

    3. 顺藤摸瓜 - Call Stack

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

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

    image

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

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

    4. 偷天换日 - Hijack The Response

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

    image

    可以看到虽然变量 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 插件,用法很简单就不具体介绍了。

    image

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

    image

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

    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 同时也是一个解密函数,异或运算经常被用来加解密。

    image

    5. 东搜西罗 - Search

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

    image

    搜索相关错误信息后得知网页用了 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()
    

    EOF

    20 回复  |  直到 2018-08-07 10:17:49 +08:00
        1
    dobelee   89 天前
    人生在于折腾。
        2
    lxy   89 天前
    悲剧,后面图片顺序乱了……还是到我博客看吧 http://blowingdust.com/
        3
    LeungJZ   89 天前
    赞👍。
        4
    glacer   89 天前
    2 年前破解过过去哪儿网网页的酒店订单接口,深感这才是做爬虫的真正乐趣!
        5
    qqpkat2   89 天前
    自从用了一个 XX 控件就能直接把 js 执行后的 html 代码给返回,就再也懒得去分析 js 加密了
        6
    soulmine   89 天前
    为嘛不直接用 chromeless 呢 有解密混淆的功夫爬虫都写完了 emmm
        7
    qsnow6   89 天前   ♥ 1
    干货!分享一个调试 cookies 的东西,只要监控到 cookies 被改写,就自动跳断点
    https://github.com/paulirish/break-on-access
        8
    yamedie   89 天前 via Android
    @soulmine 是 chrome headless😀
        9
    wdd2007   89 天前
    厉害了。
        10
    lh900519   89 天前
    学习了
        11
    soulmine   89 天前
    @yamedie 不 我说的是 js 包
        12
    codehz   89 天前 via Android
    chrome 有本地 override 功能的,不需要外部辅助
        13
    xuanyuanaosheng   88 天前 via Android
    学习,mark
        14
    frankyxu   87 天前
    666666,学习了,向大佬致敬
        15
    sergiojune   17 天前
    @lxy 您好,请问可以转载到日常学 python 公众号吗?会标明原创地址
        16
    lxy   17 天前
    @sergiojune 可以的
        17
    mrcomer   9 天前
    @qsnow6 您分享的工具里的 snippets 的导入的链接失效了,能讲解一下这个工具如何导入 chrome 开发者工具使用吗,望回复,谢谢!!!
        18
    mrcomer   9 天前
    @qsnow6 专门为您注册了 V2EX 的账号,然后等了一天回来给您发的提问,对 V2EX 提问规则,我昨天差点把电脑给砸了。。。。
        19
    lxy   8 天前   ♥ 1
    @mrcomer 安装扩展后直接添加规则就可以了,映射到本地文件。Chrome 扩展需要勾选允许访问文件网址。

    https://i.loli.net/2018/08/06/5b67b97cc80c8.png
        20
    mrcomer   7 天前
    @lxy 感谢解答!!!
    关于   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   鸣谢   ·   实用小工具   ·   1996 人在线   最高记录 3762   ·  
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.1 · 17ms · UTC 15:16 · PVG 23:16 · LAX 08:16 · JFK 11:16
    ♥ Do have faith in what you're doing.
    沪ICP备16043287号-1