[Cientos de casos JS Reverse] Depurador ilimitado y cifrado dinámico de datos de una plataforma de monitoreo de la calidad del aire

Preste atención a la cuenta pública de WeChat: rastreador Brother K, continúe compartiendo productos técnicos secos como rastreador avanzado, JS / Android inverso.

declaración

Todo el contenido de este artículo es solo para el aprendizaje y la comunicación. El contenido capturado, las URL confidenciales y las interfaces de datos se han insensibilizado. Los usos comerciales e ilegales están estrictamente prohibidos. De lo contrario, todas las consecuencias que surjan no tienen nada que ver con el autor. Si hay alguna infracción, por favor póngase en contacto conmigo para eliminar inmediatamente!

objetivo inverso

  • Objetivo: depurador ilimitado para una plataforma de monitoreo de la calidad del aire y cifrado y descifrado dinámico de datos de solicitud y datos de retorno
  • Página principal:aHR0cHM6Ly93d3cuYXFpc3R1ZHkuY24v
  • interfaz:aHR0cHM6Ly93d3cuYXFpc3R1ZHkuY24vYXBpbmV3L2FxaXN0dWR5YXBpLnBocA==

escribir delante

Este sitio se actualiza con frecuencia. Antes de Brother K, muchos bloggers escribieron artículos de análisis en el sitio. Recientemente, un lector preguntó sobre el cifrado de los datos solicitados y el descifrado de los datos devueltos, y descubrió que el cifrado y descifrado JS se ha vuelto dinámico. Las soluciones mencionadas en los artículos anteriores no son muy buenas, pero en general no es difícil, pero es un poco más problemático de manejar, y hay algunos pequeños detalles a los que se debe prestar atención.

En "Acerca del sistema" del sitio web, puede ver que este sitio parece ser mantenido por desarrolladores individuales. Ha existido desde 2013. En la lista de patrocinios de amistad, puede ver que la mayoría de ellos son ambientales, topografía y mapeo, salud pública Los estudiantes universitarios relevantes y el personal del instituto de investigación pueden adivinar que estos datos son muy útiles para su investigación. Junto con las frecuentes actualizaciones anti-rastreo, se puede ver que el webmaster sufre de reptiles, y el hermano K sí. no quiero dárselo al webmaster. Añade una carga. Después de todo, debemos apoyar este tipo de sitio y dejar que lo mantenga durante mucho tiempo. Por lo tanto, en este número, el hermano K solo analiza la lógica y una pequeña parte de el código, y no pone el código completo.Si hay profesionales relevantes que realmente necesitan capturar datos para la investigación, pueden contactarme en el fondo de la cuenta oficial.

Omitir el depurador infinito

Haga clic derecho en F12, le indicará que el clic derecho está deshabilitado, no importa, use la tecla de acceso directo Ctrl+Shift+io la esquina superior derecha del navegador, aún se pueden abrir más herramientas, herramientas de desarrollador.

01.png

método uno

Después de abrir la consola, ingresará al primer depurador infinito, seguirá una pila hacia arriba, podrá ver una instrucción try-catch, encontrará que seguirá capturando y llamando al setTimeout()método . utilizado en los milisegundos especificados Llame a la función o calcule la expresión después de contar. Tenga en cuenta que el depurador se pasa al constructor, por lo que aquí tenemos dos formas de deshacerse del depurador. Enganche el constructor o establezca el tiempo de espera.

02.png

// 两种 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)
}

Luego llegué al segundo depurador infinito, que también es el mismo que la pila, y descubrí que hay un temporizador setInterval y un constructor de método de construcción. De manera similar, podemos desconectar el constructor o setInterval. Nota: El temporizador también detecta la altura y el ancho de la ventana. Incluso si ha pasado el constructor o setInterval, si no saca las herramientas de desarrollo por separado, no funcionará y seguirá mostrando "depuración ilegal detectada". .

03.png

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

Hemos observado que, de hecho, estos dos depuradores infinitos pueden ser superados por el constructor Hook, por lo que Fiddler puede inyectar directamente el código del constructor Hook:

04.png

Método dos

Cuando nos encontramos con el segundo depurador infinito, también podemos seguir directamente la pila a una página city_realtime.php, hay dos declaraciones de evaluación en ella, ejecute la declaración en la primera evaluación y encontrará que estamos en la VM El código del depurador visto en la máquina virtual, por lo que, en teoría, esta página se puede reemplazar directamente aquí, elimine la declaración eval, no habrá un depurador infinito, pero el hermano K le dirá primero, no puede funcionar ahora, porque hay un cierto cargar en él. JS, este JS se usará en el cifrado y descifrado posterior, pero este JS es dinámico y cambiará cada 10 minutos. Usaremos esta página para obtener JS dinámico más adelante, ¡así que no se puede reemplazar! ¡Solo para mencionar esta idea aquí!

05.png

06.png

Método tres

Por supuesto, también existe el método más simple. Haga clic con el botón derecho para seleccionar Nunca pausar aquí y nunca pausar aquí. También debe sacar la ventana de la herramienta de desarrollador por separado, de lo contrario, siempre mostrará "Depuración ilegal detectada".

07.png

Análisis de captura de paquetes

En la página de monitoreo en tiempo real, hacemos clic para consultar una ciudad por cierto, y podemos ver que los Datos del formulario solicitados y los datos devueltos están encriptados, como se muestra en la siguiente figura:

08.png

Entrada encriptada

Dado que es XHR, seguimos la pila directamente y es fácil encontrar la ubicación cifrada:

09.png

10.png

Puede ver el par de clave-valor de datos pasado: {hXM8NDFHN: p7crXYR}la clave está codificada en este JS, el valor se pU14VhqrofroULds()obtiene , este método necesita pasar dos parámetros, el primero es el valor fijo GETDATA, el segundo es el nombre de la ciudad, hagamos un seguimiento para ver cuál es este método:

11.png

Algunos parámetros como appId, marca de tiempo, ciudad, etc. han realizado algunas operaciones MD5 y base64, y el parámetro devuelto es el valor que queremos. No parece difícil. Averigüemos cómo descifrar los datos cifrados devueltos. Notamos que la solicitud ajax tiene una palabra clave exitosa. Incluso si no entendemos la lógica JS, podemos adivinar que debería ser la operación de procesamiento después la solicitud es exitosa, como se muestra en la siguiente figura: el dzJMI entrante son los datos cifrados devueltos, después de pasar por el db0HpCYIy97HkHS7RkhUn()método , el descifrado es exitoso:

12.png

Siga el db0HpCYIy97HkHS7RkhUn()método , puede ver que es un descifrado AES+DES+BASE64, y la clave de entrada y el desplazamiento iv se definen en el encabezado:

13.png

14.png

JS dinámico

Después del análisis anterior, nuestra lógica de cifrado y descifrado se ha completado, pero si depura más, encontrará que este JS de cifrado y descifrado cambia dinámicamente, y la clave definida y el desplazamiento iv se cambian cada vez. Si establece un punto de interrupción en este código, permanece demasiado tiempo y de repente descubre que el punto de interrupción falla y no se puede romper, significa que JS ha cambiado y el código actual ha fallado.

Seleccionemos dos JS diferentes (pista: JS cambiará cada 10 minutos, y se dará un análisis detallado más adelante), y usando la función de comparación de archivos de PyCharm (seleccione Ver - Comparar con a su vez), podemos resumir los siguientes cambios. (los cambios en los nombres de las variables no cuentan):

  1. Los valores de los primeros 8 parámetros: dos aes key e iv, dos des key e iv;

15.png

  1. Cuando se genera el parámetro cifrado, se cambia el ID de la aplicación. El cifrado final se divide en AES, DES y sin cifrado. Hay tres casos (este es el lugar más fácil de ignorar, no me di cuenta aquí, la solicitud puede provocar el ID de aplicación no válido):

16.png

  1. Cuando finalmente se envía la solicitud, el par clave-valor de datos, cuyas claves también se modifican:

17.png

Encontramos el cambio, entonces, ¿cómo obtenemos este JS? Debido a que este JS está en la máquina virtual VM, tenemos que encontrar su origen y de dónde proviene. Podemos ver un JS especial cuando capturamos el paquete, similar a encrypt_xxxxxx.js. Simple, lo que se devuelve es una pieza de evaluación -código envuelto:

18.png

Ya estamos familiarizados con eval, simplemente elimine eval y deje que lo ejecute, y podemos ver que es el JS que necesitamos:

19.png

Aquí hay un pequeño detalle. Si usa la consola, encontrará que ha estado imprimiendo etiquetas img, lo que afecta nuestra entrada. Aquí puede hacer un seguimiento directo e ir al punto de interrupción para detener temporalmente su ejecución. No necesita hacer otras operaciones para perder el tiempo:

20.png

¿Crees que ya casi has terminado aquí? Incorrecto, el mismo encrypt_xxxxxx.js también tiene un misterio:

  1. El nombre de encrypt_xxxxxx.js es dinámico, y el siguiente valor v es una marca de tiempo de segundo nivel, que cambiará cada 600 segundos, es decir, diez minutos. Este JS se puede encontrar en la página city_realtime.php. Recuerde el bypass que mencionado anteriormente ¿No puede el depurador infinito reemplazar esta página? ¡Queremos obtener JS dinámico a través de esta página, por lo que no se puede reemplazar!

21.png

22.png

  1. El JS devuelto por encrypt_xxxxxx.js, no todo el código de texto sin formato se puede obtener ejecutando eval una vez. Es una combinación de eval y base64. La primera vez es eval, pero puede ser posterior. Es posible que necesite base64, es posible base64 dos veces, y es posible que se requiera eval después de dos veces de base 64. En resumen, además de la primera vez de eval, si se requieren base64 y eval más tarde, y la cantidad de veces y el orden en que se requeridas, son todas irrelevantes. Para dar algunos ejemplos:

23.png

24.png

25.png

Algunas personas pueden preguntar aquí, ¿cómo puedes saber que es base64? Muy sencillo, entra directamente en la consola de la página del sitio web dswejwehxt, haz clic para ver esta función, que es base64:

26.png

Luego, en el caso de que el contenido de encrypt_xxxxxx.js sea incierto, podemos escribir un método.Después de obtener encrypt_xxxxxx.js, ejecute eval si necesita ejecutar eval, ejecute base64 si necesita ejecutar base64, hasta que no haya eval y base64, puede usar caracteres respectivamente String eval(functiony dswejwehxt(para determinar si se necesitan eval y base64 (por supuesto, hay otras formas, como ()el número de, etc.), el código de muestra es el siguiente:

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

reescritura local

A través de las funciones anteriores, obtenemos el JS dinámico, entonces, ¿podemos ejecutar directamente el JS que obtuvimos? Por supuesto que no, puede ejecutarlo localmente, puede encontrar que CryptoJS, Base64, hex_md5 deben completarse, así que aquí tenemos dos formas:

  1. Después de obtener el JS dinámico descifrado, el JS dinámico y nuestros propios métodos, como Base64 y hex_md5, forman un nuevo código JS y ejecutan el nuevo código JS para obtener los parámetros. También debemos prestar atención aquí porque los otros nombres de métodos son dinámicos. . , por lo que debe encontrar una manera de hacer coincidir el nombre del método correcto para llamar, por lo que este método personalmente se siente un poco problemático;
  2. Escribimos un JS localmente. Después de obtener el JS dinámico descifrado, necesitamos cifrar la clave, iv, appId, el nombre de la clave de datos y el parámetro si se requiere AES o DES. Esta información se compara y luego se pasa a nuestra propia escritura. JS, llame a nuestro propio método para obtener el resultado cifrado.

Aunque ambos métodos son muy problemáticos, el hermano K no puede pensar en una mejor solución por el momento. Los amigos que tienen mejores ideas pueden dejar un mensaje y hablar sobre ello.

Tome el segundo método como ejemplo, nuestro ejemplo local de 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);
}

Hacemos coincidir el ejemplo de código Python de varios parámetros en JS (haciendo coincidir el método de cifrado de 8 claves, valores iv, appId y 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

Un ejemplo de nuestro código Python para enviar una solicitud y descifrar el valor de retorno (tomando Beijing como ejemplo):

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))

Resultado en ejecución, solicitud exitosa y valor de retorno de descifrado:

27.png

{{o.nombre}}
{{m.nombre}}

Supongo que te gusta

Origin my.oschina.net/u/4585873/blog/5395812
Recomendado
Clasificación