流量解密-Web

[TOC]

Web

bypass 无限 debugger 反调试

参考文章:https://mp.weixin.qq.com/s/hpJ3qSaYI4yFJU0LWsm23A

无限debugger bypass已经是老生常谈的问题了,原理其实就是hook setInterval&eval&setTimeout&constructor 去解决常见无限debugger的几种形式(setInterval、setTimeout、原型链)。这里就不再赘述了

个人推荐上文中大佬的万能脚本。

(function() {  // 立即执行函数,创建独立作用域
    'use strict';  // 启用严格模式,防止一些不规范的代码写法

    // 调试配置对象
    //按0开启日志,默认关闭
    const DEBUG = {
        enable: 1,    // 控制是否输出日志信息的开关
        deb: 1       // 控制是否在关键位置设置断点的开关
    };

//作者:Dexter
//公众号:我不是蜘蛛

    // 定义日志输出函数,根据 DEBUG.enable 决定是否输出日志
    const log = function(...args) {
        if (DEBUG.enable==0) {
            console.log(...args);
        }
    };

        // 保存原始的 setInterval 函数
        var originalSetInterval = window.setInterval;

        // 用新的函数替换原始的 setInterval
        window.setInterval = function(callback, delay) {
            // 获取除了 callback 和 delay 的其他额外参数
            var args = Array.prototype.slice.call(arguments, 2);

            // 如果 callback 是字符串,则删除其中的 debugger 语句
            if (typeof callback === 'string') {
                callback = callback.replace(/debugger;/g, '');
            } else if (typeof callback === 'function') {
                // 如果 callback 是函数,替换包含 debugger 的回调
                var originalCallback = callback;
                callback = function() {
                    // 获取原始回调函数的源码
                    var callbackSource = originalCallback.toString();

                    // 如果源码包含 debugger,将其删除并创建新的函数
                    if (callbackSource.indexOf('debugger') !== -1) {
                        callbackSource = callbackSource.replace(/debugger;/g, '');
                        originalCallback = new Function('return ' + callbackSource)();
                    }

                    // 调用修改后的回调函数,并传入参数
                    originalCallback.apply(this, arguments);
                };
            }

            // 调用原始的 setInterval 函数,并传入修改后的 callback、delay 和其他参数
            return originalSetInterval.apply(window, [callback, delay].concat(args));
        }
    //============ toString 相关防护 ============
    var temp_eval = eval;                      // 保存原始的 eval 函数
    var temp_toString = Function.prototype.toString;  // 保存原始的 toString 方法



    // 改进toString方法的处理
    Function.prototype.toString = function () {
        if (this === eval) {
            return 'function eval() { [native code] }';
        } else if (this === Function) {
            return 'function Function() { [native code] }';
        } else if (this === Function.prototype.toString) {
            return 'function toString() { [native code] }';
        } else if(this===window.setInterval){
            return 'function setInterval() { [native code] }';
        }
        return temp_toString.apply(this, arguments);
    }

    //============ eval 相关hook ============
    window.eval = function () {  // 重写全局 eval 函数
        const stackTrace = new Error().stack;  // 获取调用栈
        const callLocation = stackTrace;       // 保存调用位置
        log(callLocation);                     // 输出调用信息
        if (DEBUG.deb==0) {                       // 根据配置决定是否断点
            debugger;
        }
        log("=============== eval end ===============");

        // 处理传入的字符串参数,移除 debugger 语句
        if (typeof arguments[0] == "string") {
            var temp_length = arguments[0].match(/debugger/g);
            if (temp_length != null) {
                temp_length = temp_length.length;
                var reg = /debugger/;
                while (temp_length) {
                    arguments[0] = arguments[0].replace(reg, "");
                    temp_length--;
                }
            }
        }
        return temp_eval(...arguments);  // 使用原始 eval 执行处理后的代码
    }

    //============ Function 相关hook ============
    var _debugger = Function;  // 保存原始 Function 构造函数

    Function = function () {  // 重写 Function 构造函数
        const stackTrace = new Error().stack;  // 获取调用栈
        const callLocation = stackTrace;       // 保存调用位置
        log(callLocation);                     // 输出调用信息
        if (DEBUG.deb==0) {                       // 根据配置决定是否断点
            debugger;
        }
        log("=============== Function end ===============");

        // 处理所有参数中的 debugger 语句
        var reg = /debugger/;
        for (var i = 0; i < arguments.length; i++) {
            if (typeof arguments[i] == "string") {
                var temp_length = arguments[i].match(/debugger/g);
                if (temp_length != null) {
                    temp_length = temp_length.length;
                    while (temp_length) {
                        arguments[i] = arguments[i].replace(reg, "");
                        temp_length--;
                    }
                }
            }
        }
        return _debugger(...arguments);  // 使用原始 Function 构造函数创建函数
    }

    // 保持原型链的完整性
    Function.prototype = _debugger.prototype;

    // 重写 Function 构造函数的 constructor
    Function.prototype.constructor = function () {
        const stackTrace = new Error().stack;  // 获取调用栈
        const callLocation = stackTrace;       // 保存调用位置
        log(callLocation);                     // 输出调用信息
        if (DEBUG.deb==0) {                       // 根据配置决定是否断点
            debugger;
        }
        log("=============== Function constructor end ===============");

        // 处理所有参数中的 debugger 语句
        var reg = /debugger/;
        for (var i = 0; i < arguments.length; i++) {
            if (typeof arguments[i] == "string") {
                var temp_length = arguments[i].match(/debugger/g);
                if (temp_length != null) {
                    temp_length = temp_length.length;
                    while (temp_length) {
                        arguments[i] = arguments[i].replace(reg, "");
                        temp_length--;
                    }
                }
            }
        }
        return _debugger(...arguments);  // 使用原始 Function 构造函数
    }

    // 确保构造函数的原型链正确
    Function.prototype.constructor.prototype = Function.prototype;
})();

首先需要找到无限debugger 触发的入口,以瑞数waf举例(其他的没啥意思)

一般来说会有个这样的文件夹,这个文件就是无限debugger 的触发入口。

或者也可以通过在堆栈中定位到入口位置。

找到位置后在其开头打上断点,刷新界面,此时就会在无限debugger 逻辑进入内存之前断住了。紧接着在控制台输入上面的万能js脚本

然后放开断点,此时无限debugger就已成功绕过。

常规js逆向

定位加密方式

首先想到的就是全局搜encrypt/encode,但是这个站存在js混淆因此难以直接定位到,此时可以通过一些特定参数进行定位。一些混淆的不狠的,用火狐浏览器能返混淆出来一部分。

通过抓包定位到加密内容放在cipherH5参数中。

因为一般混淆不会混淆参数,只会混淆函数名称之类的。全局搜这个参数,此时就能定位到加密函数了。

DEBUG跟入即可看到key和iv值(CryptoJS的WordArray格式)。

反混淆

将其反混淆后能看到源码为:因此确定其为aes-cbc-PKCS7加密方式。

this['Encrypt'] = function(input) {
    var helper = _0x251c08; // 辅助函数
    var parsedInput = CryptoJS['enc']['Utf8']['parse'](input); // 将输入字符串解析为 CryptoJS 的 WordArray
    var encrypted = CryptoJS['AES']['encrypt'](parsedInput, this['key'], { // 使用 AES 加密
        'iv': this['aesIv'], // 初始化向量
        'mode': CryptoJS['mode']['CBC'], // 加密模式为 CBC
        'padding': CryptoJS['pad']['Pkcs7'] // 填充模式为 PKCS7
    });
    return encrypted['toString'](); // 返回加密后的字符串
};

接下来将CryptoJS的WordArray格式的key和iv还原即可,iv为例。

// 假设 aesIv 是从控制台获取的数组
var aesIv = [
    943208504,
    926365495,
    909522486,
    892679220
];

// 将数组转换为 CryptoJS 的 WordArray
var ivWordArray = CryptoJS.lib.WordArray.create(aesIv);

// 将 WordArray 转换为 16 字节的十六进制字符串
var ivHex = ivWordArray.toString(CryptoJS.enc.Hex);
console.log('IV (Hex):', ivHex);

// 将 WordArray 转换为 Base64 字符串
var ivBase64 = ivWordArray.toString(CryptoJS.enc.Base64);
console.log('IV (Base64):', ivBase64);

解密成功:

瑞数waf全混淆定位加解密函数

hook Json Parse & hook Json stringify

其实无论是什么样的js,在其发送加密数据之前肯定是需要进行json序列化的。生命周期:

明文数据 -> json.stringify 序列化 -> 调用加密函数

同理在其解密数据之后肯定需要json反序列化

调用解密函数 -> json.Parse 反序列化 -> 明文数据

那么其实就意味着我们可以通过hook stringify&Parse 这两个函数,在其前后堆栈中找到加解密函数。这样无论他是混淆还是隐式调用都能找到加密函数了。这里以某金融网站为例:

无限debugger的过程就不赘述了上文中已经提及。

以加密为例,首先在控制台输入hook JSON.stringify的

let _JSONStringify = JSON.stringify;
JSON.stringify = function (value, replacer, space) {
    console.log('JSON.stringify 被调用了!');
    console.log('待序列化内容:', value);
    debugger;
    // 调用原始方法执行序列化
    const result = _JSONStringify.call(this, value, replacer, space);

    console.log('序列化结果:', result);

    // 如果需要,修改最终结果(这里可以自定义逻辑)
    // return result + '\n/* 自定义注释 */';

    // 返回序列化之后的结果
    return result;
};

然后点击功能点,此时hook到了函数。一步步,步过。

此时即可看到动态调用的加密函数

在该处打上断点,步入后将其提升为全局函数即可。(可能需要步入很多层)

将函数提升为全局

非常规js逆向

前言:由于金融、证券等客户的Web站大概率存在全流量加密并且加密方式不是简单的AES-CBC/ECB,大多数情况都是国密SM4或自定义的加密算法。因此下文用来解决该问题。

工具安装&运行(docker)

docker pull registry.cn-beijing.aliyuncs.com/iinti/common:sekiro-all-in-one-latest;

docker run -d -p 5612:5612 -v ~/sekiro-mysql-data:/var/lib/mysql --name sekiro-all-in-one registry.cn-beijing.aliyuncs.com/iinti/common:sekiro-all-in-one-latest

定位加密方法

首先先定位目标站前端界面中的加密方法,这个方法有很多:抓个包定位包中的特殊参数、全局搜encrypt/encode .......

工具配置

在debug到对应加密函数的情况下,将该函数扩展到全局范围。在控制台输入:

window.loginDecrypt=this.encrypt_ecb

并进行验证,如图即使成功。

启动docker工具,并在控制台输入:其中wss协议对应https,ws对应http(根据站是啥协议定),group可以是任意的,registerAction注册的action名称也是任意的,后续的根据具体加密逻辑实现。

请注意,如果目标网站是https,且demo无法正确连接,请下载证书并安装到你的系统中

点击这里 pem格式 或者 crt格式下载证书,并跟随系统引导安装根证书

function SekiroClient(e){if(this.wsURL=e,this.handlers={},this.socket={},!e)throw new Error("wsURL can not be empty!!");this.webSocketFactory=this.resolveWebSocketFactory(),this.connect()}SekiroClient.prototype.resolveWebSocketFactory=function(){if("object"==typeof window){var e=window.WebSocket?window.WebSocket:window.MozWebSocket;return function(o){function t(o){this.mSocket=new e(o)}return t.prototype.close=function(){this.mSocket.close()},t.prototype.onmessage=function(e){this.mSocket.onmessage=e},t.prototype.onopen=function(e){this.mSocket.onopen=e},t.prototype.onclose=function(e){this.mSocket.onclose=e},t.prototype.send=function(e){this.mSocket.send(e)},new t(o)}}if("object"==typeof weex)try{console.log("test webSocket for weex");var o=weex.requireModule("webSocket");return console.log("find webSocket for weex:"+o),function(e){try{o.close()}catch(e){}return o.WebSocket(e,""),o}}catch(e){console.log(e)}if("object"==typeof WebSocket)return function(o){return new e(o)};throw new Error("the js environment do not support websocket")},SekiroClient.prototype.connect=function(){console.log("sekiro: begin of connect to wsURL: "+this.wsURL);var e=this;try{this.socket=this.webSocketFactory(this.wsURL)}catch(o){return console.log("sekiro: create connection failed,reconnect after 2s:"+o),void setTimeout(function(){e.connect()},2e3)}this.socket.onmessage(function(o){e.handleSekiroRequest(o.data)}),this.socket.onopen(function(e){console.log("sekiro: open a sekiro client connection")}),this.socket.onclose(function(o){console.log("sekiro: disconnected ,reconnection after 2s"),setTimeout(function(){e.connect()},2e3)})},SekiroClient.prototype.handleSekiroRequest=function(e){console.log("receive sekiro request: "+e);var o=JSON.parse(e),t=o.sekiro_seq;if(o.action){var n=o.action;if(this.handlers[n]){var s=this.handlers[n],i=this;try{s(o,function(e){try{i.sendSuccess(t,e)}catch(e){i.sendFailed(t,"e:"+e)}},function(e){i.sendFailed(t,e)})}catch(e){console.log("error: "+e),i.sendFailed(t,":"+e)}}else this.sendFailed(t,"no action handler: "+n+" defined")}else this.sendFailed(t,"need request param {action}")},SekiroClient.prototype.sendSuccess=function(e,o){var t;if("string"==typeof o)try{t=JSON.parse(o)}catch(e){(t={}).data=o}else"object"==typeof o?t=o:(t={}).data=o;(Array.isArray(t)||"string"==typeof t)&&(t={data:t,code:0}),t.code?t.code=0:(t.status,t.status=0),t.sekiro_seq=e;var n=JSON.stringify(t);console.log("response :"+n),this.socket.send(n)},SekiroClient.prototype.sendFailed=function(e,o){"string"!=typeof o&&(o=JSON.stringify(o));var t={};t.message=o,t.status=-1,t.sekiro_seq=e;var n=JSON.stringify(t);console.log("sekiro: response :"+n),this.socket.send(n)},SekiroClient.prototype.registerAction=function(e,o){if("string"!=typeof e)throw new Error("an action must be string");if("function"!=typeof o)throw new Error("a handler must be function");return console.log("sekiro: register action: "+e),this.handlers[e]=o,this};
var client = new SekiroClient("
wss://127.0.0.1:5612/business/register?group=test_web&clientId=" + Math.random());
client.registerAction("
River6Enc", function (request, resolve, reject) {
var data1 = request['data1'];
var data2 = request['data2'];
var res = window.River6Enc(data1,data2);
resolve(res);
});

编写python脚本进行测试

import requests
data = {
        "group": "test_web",#之前设置的group
        "action": "loginDecrypt",#之前注册的action
        "data1":"ff98a4726147460dadmin",#加密函数参数
        "data2":"ZWQxNGIyZmQxNGZh"#加密函数参数
}
res = requests.get("http://127.0.0.1:5612/business/invoke",params=data )
print(res.text)

Yakit热加载

根据

编写热加载代码即可:

loginDecryot = func(lfz) {
  data1='ff98a4726147460d'+lfz
  data2='ZWQxNGIyZmQxNGZh'
  rsp = http.Get(f"http://127.0.0.1:5612/business/invoke?group=test_web&action=loginDecrypt&data1=${data1}&data2=${data2}")~
  printf("data: ")
  return json.Find(rsp.Data(), "$.data")
}

此时用户名就实现了自动加密。