[JS reverse hundred examples] infinite debugger and data dynamic encryption of an air quality monitoring platform

Time:2022-1-22

Focus on official account dry cargo WeChat public: K brother crawler, keep sharing crawler advance, JS/ Android reverse technology dry goods!

statement

All contents in this article are for learning and communication only. The packet capturing content, sensitive website and data interface have been desensitized. It is strictly prohibited to use them for commercial and illegal purposes, otherwise all the consequences have nothing to do with the author. If there is infringement, please contact me and delete them immediately!

Reverse target

  • Objective: unlimited debugger of an air quality monitoring platform and dynamic encryption and decryption of request data and return data
  • Home page:aHR0cHM6Ly93d3cuYXFpc3R1ZHkuY24v
  • Interface:aHR0cHM6Ly93d3cuYXFpc3R1ZHkuY24vYXBpbmV3L2FxaXN0dWR5YXBpLnBocA==

Write in front

This site is updated frequently. Many bloggers have written analysis articles on the site before brother K. recently, some readers asked for data encryption and decryption of returned data, and found that the encryption and decryption JS has become dynamic. The solutions mentioned in previous articles are not very good, but on the whole, it is not very difficult, but it is a little troublesome to deal with, There are some small details to pay attention to.

It can be seen from the “about system” of the website that this website seems to be maintained by individual developers, which was first established in 2013. In the friendship sponsorship list, it can be seen that most of them are university professionals and researchers related to environment, surveying and mapping and public health. It can be guessed that these data are very helpful for their research, Coupled with frequent anti climbing updates, we can see that the webmaster is suffering from crawlers, and brother K doesn’t want to add a burden to the webmaster. After all, we should support this kind of site and let him maintain it for a long time,Therefore, this K brother only analyzes the logic and a few parts of the code, and then does not put the complete code. If there are professionals who need to grab data to do research, they can contact me in the background of the official account.

Bypass infinite debugger

Right click F12 and you will be prompted that the right button is disabled. It doesn’t matter. Use the shortcut keyCtrl+Shift+iOr in the upper right corner of the browser, more tools and developer tools can still be opened.

01.png

Method 1

After opening the console, you will enter the first infinite debugger, followed by a stack, and you can see a try catch statement. You will find that it will go all the way to catch and callsetTimeout()The method is used to call functions or compute expressions after specified milliseconds. Notice that the debugger is passed to the construction method constructor, so here are two ways to get rid of debugger and Hook to drop constructor or setTimeout.

02.png

//Choose one of the two hooks
//Hook construction method
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)
}

Then we came to the second infinite debugger, which is also similar to the stack. We found a setinterval timer and constructor. Similarly, we can hook up and drop constructor or setinterval. Note: the timer also detects the height and width of the window. Even if you pass the constructor or setinterval, it will not work if you don’t take out the developer tool alone. It will continue to output “illegal debugging detected”.

03.png

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

We have observed that these two infinite debuggers can pass through the hook constructor, so we can directly inject the code of the hook constructor into Fiddler:

04.png

Method 2

When we encounter the second infinite debugger, we can also directly follow the stack to a city_ realtime. There are two Eval statements in the PHP page. When you execute the first Eval statement, you will find that it is the debugger code we saw in the VM virtual machine. Therefore, in theory, you can directly replace this page. If you remove the eval statement, there will be no infinite debugger, but brother K told you first that it can’t work now, Because there is a JS loaded in it, this JS will be used in later encryption and decryption, but this JS is dynamic and will change every 10 minutes. We need to obtain the dynamic JS through this page later, so it cannot be replaced! Here is just a mention of this idea!

05.png

06.png

Method 3

Of course, there is also the simplest method. Right click and select never pause here. You also need to take out the developer tool window separately, otherwise “illegal debugging detected” will be output all the time.

07.png

Packet capture analysis

On the real-time monitoring page, click to query a city, and you can see that the requested form data and the returned data are encrypted, as shown in the figure below:

08.png

Encryption entry

Since it is XHR, we can easily find the encrypted location by directly following the stack:

09.png

10.png

You can see the passed data key value pairs:{hXM8NDFHN: p7crXYR}, the key is written dead in this JS, and the value is through a methodpU14VhqrofroULds()As a result, this method needs to pass two parameters, the first is the constant value GetData, and the second is the city name. Let’s follow up to see what this method is:

11.png

For some appid, timestamp, city and other parameters, MD5 and Base64 operations are performed, and the returned param is the value we want. It doesn’t seem difficult. Let’s find out how the returned encrypted data is decrypted. We notice that the Ajax request has a success keyword. Even if we don’t understand JS logic, we can guess that it should be the processing operation after the request is successful, as shown in the figure below: the incoming dzjmi is the returned encrypted data, which is processeddb0HpCYIy97HkHS7RkhUn()Method, the decryption is successful:

12.png

follow-updb0HpCYIy97HkHS7RkhUn()Method, you can see that AES + des + Base64 decrypts, and the incoming key and offset IV are defined in the header:

13.png

14.png

Dynamic JS

After the above analysis, we have completed the encryption and decryption logic. However, if you debug more, you will find that the JS for encryption and decryption changes dynamically, and the defined key and offset IV will change every other period of time. If you break the point in this code for a long time, and suddenly find that the breakpoint fails and cannot be broken, that is, the JS has changed, The current code has expired.

We randomly collect two different JS (hint: JS will change every 10 minutes, which will be analyzed in detail later). Using pycharm’s file comparison function (select View – compare with in turn), we can summarize the following changes (the change of variable name does not count):

  1. The values of the first 8 parameters: two AES keys and IV, two des keys and IV;

15.png

  1. When generating an encrypted param, the appid changes. The final encryption is divided into AES, DES and no encryption (this is the most easily ignored place. It is not noticed here. The request may prompt that the appid is invalid):

16.png

  1. When the request is finally sent, the data key value pair, in which the key is also changed:

17.png

We have found the changes. How can we get this JS? Because this JS is in the VM, we have to find its source and where it comes from. We can see a special JS, similar to encrypt, when we grab the package_ xxxxxx. JS, it’s not easy to see the name. It returns the code of an eval package:

18.png

We are already familiar with eval. Remove Eval directly and let it execute. You can see that it is the JS we need:

19.png

Here is a small detail. If you use the console, you will find that it has been printing img tags, which affects our input. Here you can directly follow in and temporarily stop it from running at the next breakpoint. There is no need to do other operations to waste time:

20.png

You think it’s almost done here? Wrong, the same encrypt_ xxxxxx. JS also has a mystery:

  1. encrypt_ xxxxxx. The name of JS is dynamic, and the subsequent V value is a second timestamp. It will change every 600 seconds, that is, ten minutes. This JS can be displayed in city_ realtime. PHP page found. Remember we said that bypassing infinite debugger can’t replace this page? We want to get dynamic JS through this page, so it can’t be replaced!

21.png

22.png

  1. encrypt_ xxxxxx. Not all JS returned by JS can get plaintext code by executing Eval once. It is a combination of Eval and Base64. The first time is Eval, but it is uncertain later. It may directly produce results. It may need Base64, it may need Base64 twice, and it may need Eval after Base64 twice. In short, except that the first time is Eval, Whether Base64 and eval are needed later, as well as the number and order of needs, are uncertain! For example:

23.png

24.png

25.png

Someone here may ask, how can you see that it’s Base64? It’s very simple. Enter it directly in the console of the website pagedswejwehxt, click to see this function, which is Base64:

26.png

So for encrypt_ xxxxxx. JS content is uncertain, we can write a method to get encrypt_ xxxxxx. JS, Eval is executed when Eval needs to be executed, and Base64 is executed when Base64 needs to be executed. Until there is no Eval and Base64, strings can be used respectivelyeval(functionanddswejwehxt(To determine whether Eval and Base64 are needed (of course, there are other ways, such as()The sample code is as follows:

def get_decrypted_js(encrypted_js_url):
    """
    :param encrypted_ js_ url: encrypt_ xxxxxx. JS address
    : Return: decrypted JS
    """
    decrypted_js = requests.get(url=encrypted_js_url, headers=headers).text
    flag = True
    while flag:
        if "eval(function" in decrypted_js:
            #Eval is required
            Print ("Eval required!")
            replace_js = decrypted_js.replace("eval(function", "(function")
            decrypted_js = execjs.eval(replace_js)
        elif "dswejwehxt(" in decrypted_js:
            #Base64 decoding required
            base64_num = decrypted_js.count("dswejwehxt(")
            Print ("need% s Base64 decodes!")% 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:
            #Get plaintext
            flag = False
    # print(decrypted_js)
    return decrypted_js

Local overwrite

Through the above functions, we get the dynamic JS. Can we directly execute the JS we get back? Of course not. You can execute it locally and find cryptojs, Base64 and Hex in it_ MD5 needs to be supplemented, so here we have two methods:

  1. After getting the decrypted dynamic JS, the dynamic JS and Base64 and hex written by ourselves_ MD5 and other methods form a new JS code, execute the new JS code and get the parameters. It should also be noted here that because other method names are dynamic, you have to find a way to match the correct method name to call, so this method is still a little troublesome;
  2. We write a JS locally. After we get the decrypted dynamic JS, we match the key, IV, appid, data key name and param whether they need AES or des encryption. Then we send them to the JS we write and call our own methods to get the encryption results.

Although both methods are troublesome, brother K can’t think of a better solution for the time being. Friends with better ideas can leave a message.

Take the second method as an example, our local JS example (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);
}

Python code examples matching various parameters in JS (encryption methods matching 8 keys, IV values, appid and param):

def get_key_iv_appid(decrypted_js):
    """
    :param decrypted_ JS: decrypted encrypt_ xxxxxx. js
    : Return: some parameters required by the request
    """
    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)

    #Judge whether param is encrypted by AES or des or not
    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 = {
        #The positions of key and IV are the same as those in the original 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],
        #The key name of the data to send the request
        "request_data_name": request_data_name[0].strip(),
        #What kind of encryption is required to send the requested data value
        "request_param_encrypt": request_param_encrypt
    }
    # print(key_iv_appid)
    return key_iv_appid

Python code example of sending request and decrypting return value (take Beijing as an example):

def get_data(key_iv_appid):
    """
    :param key_ iv_ appid: get_ key_ iv_ The value returned by the appid () method
    """
    request_method = "GETDATA"
    request_ City = {"city": "Beijing"}
    with open('main.js', 'r', encoding='utf-8') as f:
        execjs_ = execjs.compile(f.read())

    #Call different methods according to different encryption methods to obtain the param parameters of the request for encryption
    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)

    #Decrypt the obtained encrypted data
    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))

Run the result, successfully request and decrypt the return value:

27.png