整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

js、css实现混淆压缩

. 准备工作

1.1安装JDK

下载地址为:http://www.oracle.com/technetwork/java/javase/downloads/index.html

1.2下载YUICompressor

下载地址为:https://github.com/yui/yuicompressor/releases

2. 如何压缩

2.1压缩命令

注:--type可以不写,可以自动识别

2.2批量压缩

可以配合maven、ant或批处理命令来实现:

3. 效果

3.1css

压缩之前的版本:

压缩之后的版本:

们已经学到很多反爬机制以及相应的反反爬策略。使用那些手段,其实已经完全可以完成绝大多数的爬虫任务。但是,还是有极个别的情况下,会出现诸如 JS 加密和 JS 混淆之类的高深反爬机制。

如果不幸遇到这种反爬机制,一个明智之举是给站长点个赞,然后恭恭敬敬选择放弃,去别的地方找数据。

当然,还是那句话,我们可以选择不爬,但是对付 JS 加密和 JS 混淆的方法却不可以不会。

这里就以中国空气质量在线检测平台为例,介绍 JS 加密和 JS 混淆的实现和破解方法。

要爬取的网站:https://www.aqistudy.cn/html/city_detail.html

这个网站正在升级,所以页面无法正常显示。这也意味着这个网站本身的 JS 解密是有问题的(如果没问题就能显示了),所以最后我们并不能完全解析出数据来。虽然如此,这个网站仍然是学习 JS 加密和 JS 混淆的相当不错的平台。

闲话少说,开始干活!

首先浏览器打开网页,并打开调试台的抓包工具。修改查询条件(城市的名称 + 时间范围),然后点击查询按钮,捕获点击按钮后发起请求对应的数据包。点击查询按钮后,并没有刷新页面,显然发起的是 ajax 请求。该请求就会将指定查询条件对应的数据加载到当前页面中(我们要爬取的数据就是该 ajax 请求请求到的数据)。

分析捕获到的数据包

  • 提取出请求的 url:https://www.aqistudy.cn/apinew/aqistudyapi.php
  • 请求方式:post
  • 请求参数:d: 动态变化一组数据(且加密)
  • 响应数据:是加密的密文数据

该数据包请求到的是密文数据,为何在前台页面显示的却是原文数据呢?

原来,在请求请求到密文数据后,前台接受到密文数据后使用指定的解密操作(JS 函数)对密文数据进行了解密操作,然后将原文数据显示在了前台页面。

接下来的工作流程:

首先先处理动态变化的请求参数,动态获取该参数的话,就可以携带该参数进行请求发送,将请求到的密文数据捕获到。

  • 将捕获到的密文数据找到对应的解密函数对其进行解密即可。
  • 【重点】需要找到点击查询按钮后对应的 ajax 请求代码,从这组代码中就可以破解动态变化的请求参数和加密的响应数据对应的相关操作。
  • 找 ajax 请求对应的代码,分析代码获取参数 d 的生成方式和加密的响应数据的解密操作。直接在页面中,并没有办法直接找到发送 ajax 请求的函数的,因为它以及被封装到别的文件中了。我们可以基于火狐浏览器定位查询按钮绑定的点击事件。
  • 抽丝剥茧,首先从 getData 函数实现中找寻 ajax 请求对应的代码。在该函数的实现中没有找到 ajax 代码,但是发现了另外两个函数的调用,getAQIData()getWeatherData()。ajax 代码一定是存在于这两个函数实现内部。

    另外,这里记住一个参数,type == ’HOUR‘,它的含义是查询时间是以小时为单位。这个参数我们后来会用到。

    接下来我们就去分析 getAQIData()getWeatherData(),争取能够找到 ajax 代码。

    我们找到这两个函数的定义位置,还是没有找到 ajax 请求代码。不过我们却发现它们同时调用了另外一个函数,getServerData(method,param,func,0.5)。它的参数的值可以为:

    • method 可以是 ‘GETCITYWEATHER’ 或者 ‘GETDETAIL’
    • params 的值是 {city, type, startTime, endTime},也就是查询条件
    • func 是一个匿名函数,看样子是在处理数据。

    下一步当然就要找 getServerData 函数了,看看那个函数里面有没有我们一致想要的发送 ajax 请求的代码。

    我们尝试着在页面中搜索,却找不到这个函数。很显然,它是被封装到其他 js 文件中了。这时,我们可以基于抓包工具做全局搜索。

    好消息是,我们顺利找到了 getServerData 函数!坏消息是,这货长得一点也不像是函数。

    这是因为,这段 JS 函数代码被加密的。这种加密的方式,我们称为 JS 混淆。

    JS 混淆,也就是对核心的 JS 代码进行加密。

    JS 反混淆,则是对 JS 加密代码进行解密。

    接下来我们要做的,就是 JS 反混淆,让这段我们看不懂的东西,显现出庐山真面目。

    我们用的方法十分简单粗暴,也就是暴力破解。使用这个网站就可以实现对 JS 混淆的暴力破解:https://www.bm8.com.cn/jsConfusion/

    将 getServerData 函数所在的那一整行代码都复制过来,粘贴到这个网址的文本输入框中,然后点击 开始格式化 即可:

    终于,我们看到了 getServerData 的代码,并且在其中发现了发送 ajax 的请求:

    function getServerData(method, object, callback, period) {
        const key = hex_md5(method + JSON.stringify(object));
        const data = getDataFromLocalStorage(key, period);
        if (!data) {
            var param = getParam(method, object);
            $.ajax({
                url: '../apinew/aqistudyapi.php',
                data: {
                    d: param
                },
                type: "post",
                success: function (data) {
                    data = decodeData(data);
                    obj = JSON.parse(data);
                    if (obj.success) {
                        if (period > 0) {
                            obj.result.time = new Date().getTime();
                            localStorageUtil.save(key, obj.result)
                        }
                        callback(obj.result)
                    } else {
                        console.log(obj.errcode, obj.errmsg)
                    }
                }
            })
        } else {
            callback(data)
        }
    }

    从这段代码中,我们不难得出下面这几个信息:

    • ajax 请求成功后获得到的 data 是加密的响应数据(就是我们最开始通过抓包工具看到的那一串神秘的相应字符串),通过 decodeData(data) 函数,可以将加密的数据解密成我们需要的明文数据。
    • 发送请求时携带的参数,也就是 d 对应的值 param 是通过 getParam(method, object) 函数返回动的态变化的请求参数。这两个参数我们前面也分析过:参数 method 可以是 ‘GETCITYWEATHER’ 或者 ‘GETDETAIL’参数 object 则为 {city, type, startTime, endTime},是我们的查询条件我们当然还可以继续最终下去,刨根问题找到它们究竟是通过什么方式进行加密和解密的。然后,使用 Python 代码,重复这个加密和解密的过程,完成请求数据的生成和响应数据的解析过程。

    但是我们并不打算这么做。因为再继续深挖下去,难度将会陡然增加。此时我们已经很疲惫了,如果继续下去恐怕要疯掉。而且,JavaScript 和 Python 毕竟是两种语言,它们之间的方法和各种包都不相同。JavaScript 能实现的,Python 未必能够轻松完成。所以重新写一个加密和解密的脚本,并不是明智之举。

    更好的解决方案是,我们提供请求的明文数据,通过网站自己的 JS 代码进行加密,得到加密的请求参数。使用这个参数,我们发送请求给服务端。拿到加密的响应数据后,再通过网站的 JS 代码进行解密。

    也就是说,我们接下来需要做的就是要调用两个 JS 函数 decodeData 和 getParam,并拿到返回结果即可。

    现在的问题是,在 Python 程序中如何调用 JS 函数呢?

    这就涉及到一个新的概念:JS 逆向。JS 逆向,也就是在 Python 中调用 JS 函数代码。

    能够实现 JS 逆向的方式有两种:

    1. 手动将 JS 函数改写称为 Python 函数并执行。
    2. 这种方法我刚刚谈过了,并不现实。因为 JS 能实现的,Python 未必能够轻易实现。而且毕竟还要重写函数,比较麻烦。
    3. 使用固定模块,实现自动逆向(推荐)。
    4. 一个很好用的实现 JS 逆向的 Python 库 是 PyExecJS。
    5. PyExecJS 库用来实现模拟 JavaScript 代码执行获取动态加密的请求参数,然后再将加密的响应数据带入 decodeData 进行解密即可。
    6. PyExecJS 需要在本机安装好 nodejs 的环境。
    7. PyExecJS 的安装:
    pip install PyExecJS

    接下来,我们就可以生成加密的请求数据了。

    首先,把我们解析出来的那串代码保存到本地,比如名为 code.js 的文件中。在里面我们补充一个函数,比如名字叫 getPostParamCode,用来发起我们的数据请求。之所以这样做是因为使用 PyExecJS 调用 JS 函数时,传入的参数只能是字符串。而 getParam 方法的参数需要用到 JS 的自定义对象。

    我们只需在 code.js 中加上下面的代码即可:

    function getPostParamCode(method, type, city, start_time, end_time) {
        var param = {};
        param.type = type;
        param.city = city;
        param.start_time = start_time;
        param.end_time = end_time;
        return getParam(method, param)
    }

    然后,使用 PyExecJS 调用里面的 getParam 方法,将我们的请求数据加密:

    # 模拟执行decodeData的js函数对加密响应数据进行解密
    import execjs
    import requests
    
    node = execjs.get()
    
    # 请求参数
    method = 'GETCITYWEATHER'
    type = 'HOUR'
    city = '北京'
    start_time = '2020-03-20 00:00:00'
    end_time = '2020-03-25 00:00:00'
    
    # 编译js代码
    file = 'code.js'    # js代码的路径
    ctx = node.compile(open(file, encoding='utf-8').read())
    
    # 将请求数据加密
    encode_js = f'getPostParamCode("{method}", "{type}", "{city}", "{start_time}", "{end_time}")'
    params = ctx.eval(encode_js)
    
    # 使用加密的参数,发起post请求
    url = 'https://www.aqistudy.cn/apinew/aqistudyapi.php'
    response_text = requests.post(url, data={'d': params}).text
    
    # 将响应数据解密
    decode_js = f'decodeData("{response_text}")'
    decrypted_data = ctx.eval(decode_js)    # 如果顺利,返回的将是解密后的原文数据
    print(decrypted_data)    # 执行会报错:目前页面中没有数据。解密函数只是针对页面中原始的数据进行解密。

    自此,我们完成了 JS 加密和 JS 混淆的处理。这里我们总结一下这几个概念:

    • JS 加密,也就是通过 JS 代码,将数据进行加密处理,将明文数据变成密文数据。如果不能将其解密,密文数据将毫无用处。
    • JS 解密:通过 JS 代码,将加密的数据解密,也就是将密文数据解析成明文数据。JS 解密是 JS 加密的逆过程。
    • JS 混淆:将 JS 代码(比如 JS 函数)本身进行加密。
    • JS 反混淆:将加密了的 JS 代码解密成常规的 JS 代码。通常直接使用暴力破解即可。
    • JS 逆向(重要):通过 Python 代码调用 JS 的函数。

    附,ajax 请求的各个数据的含义:

    最近的一个漏洞赏金项目中,白帽小哥发现该网站在给每一个请求时(包括GET参数值)签名,从而阻止URL修改,他希望找出他们是如何实现这一点,并尝试找到绕过的方法。

    白帽小哥在修改URL和GET参数值时,收到了一些常见的错误消息,随后他意识到,只有在修改GET参数并非POST参数时,才会出现这些错误。服务器会发送两个头到服务器并验证它们是否匹配。

    • Time: 1703010077113
    • Sign: 16428:088d7f8c3eaa175c94d1ab016be9a0c1132e329f:7a5:6581a7f6

    在不更新这些头部的情况下,试图修改URL会导致以下错误:

    {"error":{"code":401,"message":"Please refresh the page"}}

    从请求中,虽然看不到服务器发送的这些头部值,但是可以知道客户端必须生成它们,因此它们可能存在于JavaScript中,我们首先要做的是打开浏览器开发者工具并搜索这些头部值。

    在Firefox下,使用Ctrl+Shift+F进行搜索,可以搜索在加载DOM中的每个JavaScript资源,Sign和Time这些词相当通用,所以结果很多,但是不幸的是,经过所有的结果搜索,并没能找到它,说明这些值可能是被混淆了。

    在查看了所有的JavaScript库后,白帽小哥终于发现了一个混淆程度很高的文件:

    https://[cdn]/[path]/33415.js?rev=5d210e7-2023-11-29

    网上有很多JavaScript反混淆工具和库,每个工具都有自己的特点,并且根据代码的混淆方式有不同的结果。

    • https://deobfuscate.io/
    • https://deobfuscate.relative.im/
    • http://jsnice.org/

    但是即使通过反混淆工具运行代码,最后仍然被高度混淆,也许有一种特定的工具可以得到更清晰的输出,于是白帽小哥决定尝试自己解决。如果你陷入工具无法提供帮助的情况,那么学习如何做到这一点就非常重要了!

    当尝试理解混淆代码时,白帽小哥发现一种最好的方法是首先尽可能理解伪代码,并开始设置断点:

    1. 代码中没有核心 JavaScript 函数字符串,因此它们混淆了所有字符串值,找到它们在混淆代码中的存储位置以及如何调用它们将是弄清楚代码中发生了什么的第一步
    2. 我们知道字符串值 Sign 和 Time 也被混淆了,因此可能位于同一位置
    3. 它需要请求中的信息才能对其进行签名,我们知道它也应该在代码中的某个位置使用 URL 字符串

    那么如何在浏览器中设置一个断点,YouTube上有一些详细解释这些的优质视频,但简单来说就是:

    1. 按下F12打开浏览器的开发者工具
    2. 在Firefox中,跳转到“调试器”。在Chrome中,是“Sources”选项卡
    3. 虽然浏览器的不同,但它们的操作方式基本相同
    4. 转到“Sources”选项卡,选择一份JavaScript资源文件
    5. 如果源代码被压缩,点击”{}”按钮进行美化
    6. 悬停在每行代码左侧的数字上,会看到可以点击它们
    7. 点击那些数字之一将设置一个断点
    8. 当浏览器执行这段代码时,它会暂停所有执行

    对于工程师来说,这有助于帮助他们看到代码实时发生时的问题,但对于黑客来说,这有助于进行逆向工程以更好地理解它的工作方式。

    在美化了混淆JavaScript代码后并放置一些断点,就可以触发请求了,最终发现下面这些代码变量与请求的签名有关:

    当断点在代码执行处触发时,开发者工具将显示在断点处的DOM中存储的变量值,所以现在就可以通过断点找出这部分代码的运行机制:

            t = n[o( - 570, 'nY58')](u(), W, n[o( - 555, 'U[zo')], '');
            function o(W, n) {
              return d(W - - 774, n)
            }
            const c = n[o( - 467, 'lMAW')](u(), window, n[o( - 557, 'EJC^')], null),
            i = {};
            i[o( - 444, 'BF4)')] = + new Date;
            const f = n[o( - 493, 'jUU[')](u(), e.default, n[o( - 565, '2tt4')], null),
            k = n[o( - 579, 'FRHE')](
              r(),
              [
                n[o( - 501, 'We4x')],
                i[o( - 444, 'BF4)')],
                t,
                f ||
                0
              ][o( - 519, 'r83A')]('\n')
            );

    结合以上代码,我们可以在第一行(变量k)处设置一个断点,当浏览器在该行暂停时,我们可以复制值并将它们发送到控制台:

    可以看到 w 变量是一个包含请求信息的对象,然后使用它将当前 URL 路径分配给 const t

    接下来,我们可以看到const c正在存储我们的请求的User-Agent:

    可以看到变量 i 是一个存储“time”的对象,这是一个 Unix 时间戳,可能用于请求中的时间头。

    我们可以看到变量 f 存储的是值 379578839

    变量k是一个哈希值,但我们不知道它是如何生成的,生成哈希值的代码:

       k = n[o( - 579, 'FRHE')](
              r(),
              [
                n[o( - 501, 'We4x')],
                i[o( - 444, 'BF4)')],
                t,
                f ||
                0
              ][o( - 519, 'r83A')]('\n')
            );

    在 k 上设置断点,然后我们可以使用“Step In”(Firefox 中的 F11),这将引导我们一步一步地执行代码,这有助于我们理解混淆代码在做什么,但最终我们会看到它们在哈希什么,单步执行大约 25 次后,我们最终在下图中看到它正在调用一个名为 createOutputMethod 的函数,其中包含一些我们怀疑的字符串。

    n 的值是:

    "NQ4UQIjeSeFbaORiNgZEt0AVXvwYYGQP\n1703012009162\n/api2/v2/users/notifications/count\n379578839"

    变量 W 是另一个库中名为“createOutputMethod”的函数:
    https://[cdn]/[路径]/chunk-vendors-b49fab05.js
    通过该 JavaScript 文件,我们可以看到该函数是名为 js-sha1 外部库的一部分:

     /*
     * [js-sha1]{@link https://github.com/emn178/js-sha1}
     *
     * @version 0.6.0
     * @author Chen, Yi-Cyuan [emn178@gmail.com]
     * @copyright Chen, Yi-Cyuan 2014-2017
     * @license MIT
     */

    现在我们知道哈希值如下:

    我们可以根据请求来检查这些值,以便更好地了解它们可能是什么:

    我们可以看到哈希末尾的数字(379578839)是请求的User_Id。根据现在掌握的信息,我们可以将混淆的代码重写为更容易理解的代码:

    const c = W["url"];
    
    	// const d = window.navigator.userAgent;
    	const d = userAgent;
    
      	  f["time"] = +new Date;
    	
    	const i = W["headers"]["user-id"];
    
    	const k = sha1(
    		[
    			n["frWIg"], // pE5CRmAhC8fvaWy6u58tKDTEKCZyTKLA
    			f["time"], // time
    			c, // url
    			i || // user-id
    			0
    		]["join"]('\n')
    	);

    现在我们对代码的工作原理有了一些了解,但是 Sign 标头中仍然有我们尚未确定的其它值,在类的末尾,有一个带有嵌套函数调用的巨大返回,为了简单起见,白帽小哥删除了嵌套函数。

     return i[o( - 442, 'WQdV')] = [
              o( - 560, 'r83A'),
              k,
              function (W) {
                function t(W, n) {
                  return o(W - 583, n)
                }
                return Math[t(89, 'BF4)')](
    …
    }(k),
              n[o( - 483, 'Trv&')]
            ][o( - 458, '$LL1')](':'),
            i
          }
        }
      }

    我们可以在其中一个函数中看到它传入“:”,假设 Sign 标头的值由 : 分隔,可以假设这是连接值,我们可以使用断点和控制台技巧来检查它:

    检查加入的值:

    请记住,Sign 标头值如下所示:

    大量的函数调用很可能是数学运算,操纵哈希值得出最终的数字(例如 770)。

    此时我们有几个问题需要考虑:

    1. 我们是否需要彻底完成了逆向分析?如果我们想将其转换为另一种语言,就不得不这么做
    2. 我们是否已经充分了解代码的工作原理以便操纵我们想要的值?
    3. 我们不想手动运行代码来签署请求,这会减慢我们的测试速度,要怎样才能让这项工作自动进行呢?

    我们的一种选择是使用浏览器扩展,例如资源覆盖(Firefox、Chrome)或浏览器内置脚本覆盖,可以通过右键单击Debugger中的Sources来访问它们。

    但这并不高效,如果想在 Burp Suite 中操作请求,那么我们需要重写 Python 或 Java 代码,继续逆向混淆代码并用另一种语言重写它需要花费更多的精力,更快的选择是复制代码,进行我们想要的修改,然后将其设置为 NodeJS 服务器,并在 Burp 中作为插件来请求该服务。

    以下是概念图:

    • https://gist.github.com/ziot/3bf579aa1d27b5cf07de4e7a4a859c45

    既然已经验证了可以操作 URL 并生成正确的哈希值,那么就需要找到一种方法自动将此数据传递给 BurpSuite,如果你之前从未写过Burp插件,并且对插件的API也不熟悉,没关系,因为现在我们有了 ChatGPT 来轻松实现。

    可以看到ChatGPT生成了相当准确的代码,大约有 60% 的功能,当然由于对 Burp插件进行的 API 更改,我们还需要进行一些小的调整。

    最终的插件代码可以查看:https://gist.github.com/ziot/3d5002bcb239591290f22003c6c029de

    要使用该插件,必须确保安装了 Jython.jar 和用于安装的 Python 模块的模块文件夹:

    成功加载扩展插件后,就可以开始在 Burp Suite 中操作请求了:

    现在就可以修改 GET请求中的“limit”参数值,并且不会再收到 401 错误响应了。你学会了么?