流量解密-Web&小程序 | 无限debugger bypass、常规js逆向、非常规js逆向

admin 2025年3月25日10:06:08评论13 views字数 10211阅读34分2秒阅读模式

原创投稿,作者:white-beer,无限debugger bypass。

流量解密-Web&小程序 | 无限debugger bypass、常规js逆向、非常规js逆向

1
Bypass 无限 debugger反调试

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

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

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

(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 的触发入口。

流量解密-Web&小程序 | 无限debugger bypass、常规js逆向、非常规js逆向

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

流量解密-Web&小程序 | 无限debugger bypass、常规js逆向、非常规js逆向

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

流量解密-Web&小程序 | 无限debugger bypass、常规js逆向、非常规js逆向

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

流量解密-Web&小程序 | 无限debugger bypass、常规js逆向、非常规js逆向

2
常规JS逆向

定位加密方式

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

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

流量解密-Web&小程序 | 无限debugger bypass、常规js逆向、非常规js逆向

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

流量解密-Web&小程序 | 无限debugger bypass、常规js逆向、非常规js逆向

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

流量解密-Web&小程序 | 无限debugger bypass、常规js逆向、非常规js逆向

反混淆

将其反混淆后能看到源码为:因此确定其为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 的 WordArrayvar 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);

流量解密-Web&小程序 | 无限debugger bypass、常规js逆向、非常规js逆向

解密成功:

流量解密-Web&小程序 | 无限debugger bypass、常规js逆向、非常规js逆向

瑞数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到了函数。一步步,步过。

流量解密-Web&小程序 | 无限debugger bypass、常规js逆向、非常规js逆向

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

流量解密-Web&小程序 | 无限debugger bypass、常规js逆向、非常规js逆向

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

流量解密-Web&小程序 | 无限debugger bypass、常规js逆向、非常规js逆向流量解密-Web&小程序 | 无限debugger bypass、常规js逆向、非常规js逆向将函数提升为全局

流量解密-Web&小程序 | 无限debugger bypass、常规js逆向、非常规js逆向

3
非常规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 .......

流量解密-Web&小程序 | 无限debugger bypass、常规js逆向、非常规js逆向

工具配置

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

window.loginDecrypt=this.encrypt_ecb

流量解密-Web&小程序 | 无限debugger bypass、常规js逆向、非常规js逆向

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

流量解密-Web&小程序 | 无限debugger bypass、常规js逆向、非常规js逆向

启动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);});

流量解密-Web&小程序 | 无限debugger bypass、常规js逆向、非常规js逆向

编写python脚本进行测试

import requestsdata = {        "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)

流量解密-Web&小程序 | 无限debugger bypass、常规js逆向、非常规js逆向

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

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

流量解密-Web&小程序 | 无限debugger bypass、常规js逆向、非常规js逆向

 

原文始发于微信公众号(李白你好):流量解密-Web&小程序 | 无限debugger bypass、常规js逆向、非常规js逆向

 

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年3月25日10:06:08
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   流量解密-Web&小程序 | 无限debugger bypass、常规js逆向、非常规js逆向http://cn-sec.com/archives/3881591.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息