关于python:JS-逆向百例某空气质量监测平台无限-debugger-以及数据动态加密

32次阅读

共计 9642 个字符,预计需要花费 25 分钟才能阅读完成。

关注微信公众号:K 哥爬虫,继续分享爬虫进阶、JS/ 安卓逆向等技术干货!

申明

本文章中所有内容仅供学习交换,抓包内容、敏感网址、数据接口均已做脱敏解决,严禁用于商业用途和非法用处,否则由此产生的所有结果均与作者无关,若有侵权,请分割我立刻删除!

逆向指标

  • 指标:某空气质量监测平台有限 debugger 以及申请数据、返回数据动静加密、解密
  • 主页:aHR0cHM6Ly93d3cuYXFpc3R1ZHkuY24v
  • 接口:aHR0cHM6Ly93d3cuYXFpc3R1ZHkuY24vYXBpbmV3L2FxaXN0dWR5YXBpLnBocA==

写在后面

这个站点更新频率很高,在 K 哥之前也曾经有很多博主写了该站点的剖析文章,近期有读者问申请数据的加密和返回数据的解密,发现其加解密 JS 变成了动静的,以前的那些文章提到的解决思路不太行了,但整体上来说也不是很难,只不过解决起来比拟麻烦一点,还有一些小细节须要留神。

在网站的“对于零碎”里能够看到,这个站貌似是集体开发者在保护,最早在 2013 年就有了,在情谊资助列表里,能够看到大多数都是一些环境、测绘、公共卫生相干的大学业余、研究院人员,能够猜测到这些数据对于他们的钻研是十分有帮忙的,再加上反爬更新频繁,能够看出站长饱受爬虫之苦,K 哥也不想给站长增加累赘,毕竟这种站点咱们应该反对,让他短暂保护上来, 所以本期 K 哥只剖析逻辑和少部分代码,就不放残缺代码了,如果有相干专业人士的确须要抓取数据做钻研的,能够在公众号后盾分割我。

绕过有限 debugger

右键 F12,会提醒右键被禁用,不要紧,应用快捷键 Ctrl+Shift+i 或者浏览器右上角,更多工具,开发者工具,照样能关上。

办法一

关上控制台后会进入第一个有限 debugger,往上跟一个栈,能够看到一个 try-catch 语句,你下断点会发现他会始终走 catch,调用 setTimeout() 办法,该办法用于在指定的毫秒数后调用函数或计算表达式,留神下面,是将 debugger 传递给了构造方法 constructor,所以这里咱们有两种办法过掉 debugger,Hook 掉 constructor 或 setTimeout 都能够。

// 两种 Hook 任选一中
// Hook 构造方法
Function.prototype.constructor_ = Function.prototype.constructor;
Function.prototype.constructor = function (a) {if(a == "debugger") {return function (){};}
    return Function.prototype.constructor_(a);
};

// Hook setTimeout
var setTimeout_ = setTimeout
var setTimeout = function (func, time){if (func == txsdefwsw){return function () {};}
    return setTimeout_(func, time)
}

而后就来到了第二个有限 debugger,同样跟栈,发现有个 setInterval 定时器和构造方法 constructor,相似的,咱们 Hook 掉 constructor 或 setInterval 都能够。留神:定时器这里还检测了窗口高宽,即使是你过了 constructor 或 setInterval,如果不把开发者工具独自拿进去也是不行的,会一直输入“检测到非法调试”。

// Hook setInterval
var setInterval_ = setInterval
setInterval = function (func, time){if (time == 2000) {return function () {};}
    return setInterval_(func, time)
}

咱们察看到,其实这两个有限 debugger 都能够 Hook 构造方法来过掉,所以间接 Fiddler 注入该 Hook 构造方法的代码即可:

办法二

在咱们遇到第二个有限 debugger 的时候,还能够间接跟栈到一个 city_realtime.php 的页面,外面有两个 eval 语句,执行第一个 eval 外面的语句你就会发现正是后面咱们在 VM 虚拟机外面看到的 debugger 代码,所以这里实践上能够间接替换掉这个页面,去掉 eval 语句,就不会有有限 debugger 了,然而 K 哥先通知你,当初不行了,因为外面有加载了某个 JS,这个 JS 在前面加密解密中会用到,然而这个 JS 是动静的,每 10 分钟就会扭转,咱们前面还要通过此页面来获取动静的 JS,所以是不能替换的!这里只是提一下这个思路!

办法三

当然,这里还有一种最简略的办法,间接右键抉择 Never pause here,永不在此处断下即可,同样还须要把开发者工具窗口独自拿进去,不然会始终输入“检测到非法调试”。

抓包剖析

咱们在实时监控页面,顺便点击查问一个城市,能够看到申请的 Form Data 和返回的数据都是加密的,如下图所示:

加密入口

因为是 XHR,所以咱们间接跟栈,很容易找到加密的地位:

能够看到传递的 data 键值对:{hXM8NDFHN: p7crXYR},键在这个 JS 里是写死的,值是通过一个办法 pU14VhqrofroULds() 失去的,这个办法须要传递两个参数,第一个是定值 GETDATA,第二个就是城市名称,咱们再跟进看看这个办法是啥:

一些 appId、工夫戳、城市等参数,做了一些 MD5、base64 的操作,返回的 param 就是咱们要的值了。看起来不难,咱们再找找返回的加密数据是如何解密的,咱们留神到 ajax 申请有个 success 关键字,咱们即使是不懂 JS 逻辑,也能够猜到应该是申请胜利后的解决操作吧,如下图所示:传进来的 dzJMI 就是返回的加密的数据,通过 db0HpCYIy97HkHS7RkhUn() 办法后,就解密胜利了:

跟进 db0HpCYIy97HkHS7RkhUn() 办法,能够看到是 AES+DES+BASE64 解密,传入的密钥 key 和偏移量 iv 都在头部有定义:

动静 JS

通过以上剖析后,咱们加密解密的逻辑都搞定了,然而你多调试一下就会发现,这一个加密解密的 JS 是动态变化的,定义的密钥 key 和偏移量 iv 都是隔段时间就会扭转的,如果你在这段代码里下断点,停留时间过长,忽然发现断点生效无奈断下了,那就是 JS 变了,以后代码曾经生效了。

咱们轻易薅两个不同的 JS 下来(提醒:JS 每隔 10 分钟会变动,后文有详细分析),利用 PyCharm 的文件比照性能(顺次抉择 View – Compare With)能够总结出以下几个变动的中央(变量名的变动不算):

  1. 结尾的 8 个参数的值:两个 aes key 和 iv,两个 des key 和 iv;

  1. 生成加密的 param 时,appId 是变动的,最初的加密分为 AES、DES 和没有加密,三种状况(这里是最容易疏忽的中央,这里没有留神到,申请可能会提醒 appId 有效的状况):

  1. 最初发送申请时,data 键值对,其中的键也是变动的:

变动的中央咱们找到了,那咱们怎么获取这个 JS 呢?因为这个 JS 的在 VM 虚拟机里,所以咱们还要找到它的源头,是从哪里来的,咱们抓包能够看到一个比拟非凡的 JS,相似于 encrypt_xxxxxx.js,看这取名就晓得不简略,返回的是一段 eval 包裹的代码:

对于 eval 咱们曾经很相熟了,间接去掉 eval,让他执行一下,就能够看到正是咱们须要的那段 JS:

这里有个小细节,如果你应用控制台,会发现它始终在打印 img 标签,影响咱们的输出,这里能够间接跟进去下断点临时阻止他运行就行了,不须要做其余操作浪费时间:

你认为到这里就差不多搞定了?错了,同样的这个 encrypt_xxxxxx.js 也藏有玄机:

  1. encrypt_xxxxxx.js 的名称是动静的,前面的 v 值是秒级工夫戳,隔 600 秒,也就是十分钟就会扭转,这个 JS 能够在 city_realtime.php 页面找到,还记得咱们后面说过的绕过有限 debugger 不能替换此页面吗?咱们要通过此页面来获取动静的 JS,所以是不能替换的!

  1. encrypt_xxxxxx.js 返回的 JS,并不是所有的执行一遍 eval 就能失去明文代码了,它是 eval 和 base64 相结合的,第一遍都是 eval,然而前面就说不定了,有可能间接出后果,有可能须要 base64,有可能 base64 两遍,有可能两遍 base64 之后还要再 eval,总之,除了第一遍是 eval 以外,前面是否须要 base64 和 eval,以及须要的次数和先后顺序,都是不确定的!举几个例子:

这里可能有人会问,你怎么看进去那是 base64 呢?很简略,间接在网站页面的控制台里输出 dswejwehxt,点击去看这个函数,就是 base64:

那么针对 encrypt_xxxxxx.js 内容不确定的状况,咱们能够写一个办法,获取到 encrypt_xxxxxx.js 后,须要执行 eval 就执行 eval,须要执行 base64 就执行 base64,直到没有 eval 和 base64 即可,能够别离用字符串 eval(functiondswejwehxt( 来判断是否须要 eval 和 base64(当然也有其余形式,比方 () 的个数等),示例代码如下所示:

def get_decrypted_js(encrypted_js_url):
    """
    :param encrypted_js_url: encrypt_xxxxxx.js 的地址
    :return: 解密后的 JS
    """
    decrypted_js = requests.get(url=encrypted_js_url, headers=headers).text
    flag = True
    while flag:
        if "eval(function" in decrypted_js:
            # 须要执行 eval
            print("须要执行 eval!")
            replace_js = decrypted_js.replace("eval(function", "(function")
            decrypted_js = execjs.eval(replace_js)
        elif "dswejwehxt(" in decrypted_js:
            # 须要 base64 解码
            base64_num = decrypted_js.count("dswejwehxt(")
            print("须要 %s 次 base64 解码!" % base64_num)
            decrypted_js = re.findall(r"\('(.*?)'\)", decrypted_js)[0]
            num = 0
            while base64_num > num:
                decrypted_js = base64.b64decode(decrypted_js).decode()
                num += 1
        else:
            # 失去明文
            flag = False
    # print(decrypted_js)
    return decrypted_js

本地改写

通过以上函数咱们就拿到了动静的 JS 了,那么咱们能够间接执行拿回来的 JS 吗?当然是不能够的,你能够本人本地执行一下,能够发现外面的 CryptoJS、Base64、hex_md5 都须要补齐才行,所以到这里咱们就有两种做法:

  1. 拿到解密后的动静 JS 后,动静 JS 和咱们本人写的 Base64、hex_md5 等办法组成新的 JS 代码,执行新的 JS 代码拿到参数,这里还须要留神因为外面的其余办法名都是动静的,所以你还得想方法匹配到正确的办法名来调用才行,所以这种办法个人感觉还是略微有点儿麻烦的;
  2. 咱们本地本人写一个 JS,拿到解密后的动静 JS 后,把外面的 key、iv、appId、data 键名、param 是否须要 AES 或 DES 加密,这些信息都匹配进去,而后传给咱们本人写的 JS,调用咱们本人的办法拿到加密后果。

尽管两种办法都很麻烦,但 K 哥临时也想不到更好的解决办法了,有比拟好的想法的敌人能够留言说一说。

以第二种办法为例,咱们本地的 JS 示例(main.js):

var CryptoJS = require("crypto-js");

var BASE64 = {encrypt: function (text) {return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(text))
    },
    decrypt: function (text) {return CryptoJS.enc.Base64.parse(text).toString(CryptoJS.enc.Utf8)
    }
};

var DES = {encrypt: function (text, key, iv) {var secretkey = (CryptoJS.MD5(key).toString()).substr(0, 16);
        var secretiv = (CryptoJS.MD5(iv).toString()).substr(24, 8);
        secretkey = CryptoJS.enc.Utf8.parse(secretkey);
        secretiv = CryptoJS.enc.Utf8.parse(secretiv);
        var result = CryptoJS.DES.encrypt(text, secretkey, {
            iv: secretiv,
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
        });
        return result.toString();},
    decrypt: function (text, key, iv) {var secretkey = (CryptoJS.MD5(key).toString()).substr(0, 16);
        var secretiv = (CryptoJS.MD5(iv).toString()).substr(24, 8);
        secretkey = CryptoJS.enc.Utf8.parse(secretkey);
        secretiv = CryptoJS.enc.Utf8.parse(secretiv);
        var result = CryptoJS.DES.decrypt(text, secretkey, {
            iv: secretiv,
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
        });
        return result.toString(CryptoJS.enc.Utf8);
    }
};

var AES = {encrypt: function (text, key, iv) {var secretkey = (CryptoJS.MD5(key).toString()).substr(16, 16);
        var secretiv = (CryptoJS.MD5(iv).toString()).substr(0, 16);
        secretkey = CryptoJS.enc.Utf8.parse(secretkey);
        secretiv = CryptoJS.enc.Utf8.parse(secretiv);
        var result = CryptoJS.AES.encrypt(text, secretkey, {
            iv: secretiv,
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
        });
        return result.toString();},
    decrypt: function (text, key, iv) {var secretkey = (CryptoJS.MD5(key).toString()).substr(16, 16);
        var secretiv = (CryptoJS.MD5(iv).toString()).substr(0, 16);
        secretkey = CryptoJS.enc.Utf8.parse(secretkey);
        secretiv = CryptoJS.enc.Utf8.parse(secretiv);
        var result = CryptoJS.AES.decrypt(text, secretkey, {
            iv: secretiv,
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
        });
        return result.toString(CryptoJS.enc.Utf8);
    }
};

function getDecryptedData(data, AES_KEY_1, AES_IV_1, DES_KEY_1, DES_IV_1) {data = AES.decrypt(data, AES_KEY_1, AES_IV_1);
    data = DES.decrypt(data, DES_KEY_1, DES_IV_1);
    data = BASE64.decrypt(data);
    return data;
}

function ObjectSort(obj) {var newObject = {};
    Object.keys(obj).sort().map(function (key) {newObject[key] = obj[key];
    });
    return newObject;
}

function getRequestParam(method, obj, appId) {
    var clienttype = 'WEB';
    var timestamp = new Date().getTime()
    var param = {
        appId: appId,
        method: method,
        timestamp: timestamp,
        clienttype: clienttype,
        object: obj,
        secret: CryptoJS.MD5(appId + method + timestamp + clienttype + JSON.stringify(ObjectSort(obj))).toString()};
    param = BASE64.encrypt(JSON.stringify(param));
    return param;
}

function getRequestAESParam(requestMethod, requestCity, appId, AES_KEY_2, AES_IV_2){var param = getRequestParam(requestMethod, requestCity, appId);
    return AES.encrypt(param, AES_KEY_2, AES_IV_2);
}

function getRequestDESParam(requestMethod, requestCity, appId, DES_KEY_2, DES_IV_2){var param = getRequestParam(requestMethod, requestCity, appId);
    return DES.encrypt(param, DES_KEY_2, DES_IV_2);
}

咱们匹配 JS 外面的各项参数的 Python 代码示例(匹配 8 个 key、iv 值、appId 和 param 的加密形式):

def get_key_iv_appid(decrypted_js):
    """
    :param decrypted_js: 解密后的 encrypt_xxxxxx.js
    :return: 申请必须的一些参数
    """key_iv = re.findall(r'const.*?"(.*?)";', decrypted_js)
    app_id = re.findall(r"var appId.*?'(.*?)';", decrypted_js)
    request_data_name = re.findall(r"aqistudyapi.php.*?data.*?{(.*?):", decrypted_js, re.DOTALL)

    # 判断 param 是 AES 加密还是 DES 加密还是没有加密
    if "AES.encrypt(param" in decrypted_js:
        request_param_encrypt = "AES"
    elif "DES.encrypt(param" in decrypted_js:
        request_param_encrypt = "DES"
    else:
        request_param_encrypt = "NO"

    key_iv_appid = {
        # key 和 iv 的地位和原来 js 里的是一样的
        "aes_key_1": key_iv[0],
        "aes_iv_1": key_iv[1],
        "aes_key_2": key_iv[2],
        "aes_iv_2": key_iv[3],
        "des_key_1": key_iv[4],
        "des_iv_1": key_iv[5],
        "des_key_2": key_iv[6],
        "des_iv_2": key_iv[7],
        "app_id": app_id[0],
        # 发送申请的 data 的键名
        "request_data_name": request_data_name[0].strip(),
        # 发送申请的 data 值须要哪种加密
        "request_param_encrypt": request_param_encrypt
    }
    # print(key_iv_appid)
    return key_iv_appid

咱们发送申请以及解密返回值的 Python 代码示例(以北京为例):

def get_data(key_iv_appid):
    """:param key_iv_appid: get_key_iv_appid() 办法返回的值"""
    request_method = "GETDATA"
    request_city = {"city": "北京"}
    with open('main.js', 'r', encoding='utf-8') as f:
        execjs_ = execjs.compile(f.read())

    # 依据不同加密形式调用不同办法获取申请加密的 param 参数
    request_param_encrypt = key_iv_appid["request_param_encrypt"]
    if request_param_encrypt == "AES":
        param = execjs_.call(
            'getRequestAESParam', request_method, request_city,
            key_iv_appid["app_id"], key_iv_appid["aes_key_2"], key_iv_appid["aes_iv_2"]
        )
    elif request_param_encrypt == "DES":
        param = execjs_.call(
            'getRequestDESParam', request_method, request_city,
            key_iv_appid["app_id"], key_iv_appid["des_key_2"], key_iv_appid["des_iv_2"]
        )
    else:
        param = execjs_.call('getRequestParam', request_method, request_city, key_iv_appid["app_id"])
    data = {key_iv_appid["request_data_name"]: param
    }
    response = requests.post(url=aqistudy_api, headers=headers, data=data).text
    # print(response)

    # 对获取的加密数据解密
    decrypted_data = execjs_.call(
        'getDecryptedData', response,
        key_iv_appid["aes_key_1"], key_iv_appid["aes_iv_1"],
        key_iv_appid["des_key_1"], key_iv_appid["des_iv_1"]
    )
    print(json.loads(decrypted_data))

运行后果,胜利申请并解密返回值:


正文完
 0