JS逆向: 解决调试chrome extension时, 遇到的无限递归debugger反调试

反调试是一种重要的软件保护技术,特别是在各种游戏保护中被尤其重视。 另外,恶意代码往往也会利用反调试来对抗安全分析。 当程序意识到自己可能处于调试中的时候,可能会改变正常的执行路径或者修改自身程序让自己崩溃,从而增加调试时间和复杂度。

在Windows和Android平台下, 反调试随处可见, 它的存在有效的增加了软件逆向的成本, 遇到反调试, 大部分情况下需要见招拆招, 下面将介绍笔者遇到的第一个js反调试手段, 为了能理解其原理和灵活的拆招方式, 笔者将举两个demo案例来进行说明, 并在结尾附上实战案例.

案例1

新建一个html文件, 写入如下代码

<html>
<head>
    <script>
        setInterval(function () {
            debugger;
        }, 500);
    </script>
</head>
<body></body>
</html>

用浏览器打开这个文件后, 可以正常运行, 但按下F12(打开开发者工具)后,程序就会断到debugger这行代码, 直接影响到我们进行调试分析.

你有张良计, 我有过墙梯, 遇到这种情况, 有如下几种方案:

  1. 点击源代码->文件系统->向工作区添加文件夹, 随便选择一个文件夹, 用于存储当前网页资源的副本, 之后浏览器会弹出一个选项, 点击允许.

    image

    然后我们切换到源代码->替换, 勾选启用本地替换, 找到网页资源的副本, 这时网站代码便可随意修改, 下图中我将debugger指令去掉了, 然后F5刷新, 更改便生效了

image

  1. 由于该案例中代码比较简单, 实际上有另一种更加简单方式来绕过这个反调试, 如图所示, 直接忽略掉这个debugger指令 , 就能绕过该反调试了

案例2
<html>
    <head>
        <script>
            var count = 0;
            function checkDebugger() {  
                if(count>=20)
                {
                    count=0;
                    return;
                }
                count++;
                debugger;
                checkDebugger();
            }      
            setInterval(function(){
                checkDebugger();
            },5000);          
           
        </script>
    </head>
    <body></body>
</html>

这个案例会递归调用, 使用案例1中的方法2就不起作用了, 甚至有的程序会检测代码完整性, 使得案例1中的方法1也失效(点名chrome extension Line), 下面将介绍另一种办法来pass这个反调试.

观察代码, 不难发现, 我们只需要把这里的定时器给去掉, 就可以达成目的. 在控制台输入以下代码

// 先保存原来的setInterval函数指针
x = setInterval
// 将新函数的地址赋值给旧的setInterval指针
// 新函数里判断setInterval的第二个参数:间隔时间, 如果是5000,则啥也不干, 否则放行
setInterval = function(a,b){if(b!=5000){x();}}

这个方法可以使上例中的定时器失效, 且不影响其他定时器的逻辑, 不过仍然有些瑕疵, 更好的办法是,使checkDebugger失效, 为此, 我们可以在控制台输入以下代码

checkDebugger=function(){}
实战

如下是chrome extension store的某个扩展程序, 打开开发者面板后, 立马断到了debuuger处, 无论如何单步, 执行, 永远出不去, 程序一直呈卡死状态. 且使用案例1的方案1来修改代码, 会导致程序直接崩溃, 目前尚未找到检测代码完整性的地方( 这里就不得不吐槽chrome dev tools了, 如果是其他调试器, 只需对代码下内存断点, 即可找到扫描检测代码完整性的地方)

image

这段程序大概长这样

...............
function b(a) {
    var c = {
        'DZPan': function(e, f) {
            return e === f;
        },
        'HPymq': 'strin' + 'g',
        'keOAk': function(e) {
            return e();
        },
        'gOBNe': function(e, f) {
            return e !== f;
        },
        'ohRmf': function(e, f) {
            return e + f;
        },
        'hgrur': function(e, f) {
            return e / f;
        },
        'Cmstj': 'lengt' + 'h',
        'qZWsJ': function(e, f) {
            return e % f;
        },
        'bNwsW': function(e, f) {
            return e(f);
        },
        'JUpev': function(e, f) {
            return e(f);
        }
    };
    function d(e) {
        if (c['DZPan'](typeof e, c['HPymq'])) {
            var f = function() {
                while (!![]) {}
            };
            return c['keOAk'](f);
        } else {
            if (c['gOBNe'](c['ohRmf']('', c['hgrur'](e, e))[c['Cmstj']], -0xa89 + -0x1a0d * -0x1 + -0xb * 0x169) || c['DZPan'](c['qZWsJ'](e, 0x11 * -0x134 + 0x1 * -0x1db3 + 0x323b), -0x17f8 + 0xabd + 0x469 * 0x3))
                debugger ;
            else
                debugger ;
        }
        c['bNwsW'](d, ++e);
    }
    try {
        if (a)
            return d;
        else
            c['JUpev'](d, -0x4fa * -0x1 + 0x1f * -0xe2 + 0x1664);
    } catch (e) {}
}

其中最外层的b()函数是一个全局范围的函数, c是它的局部变量, 反调试代码主要是在d()函数里, c['bNwsW']传递进去的第一个参数就是d本身, 这样一来, 就实现了无限递归debugger, 直到stack overflow.

对于此, 我们有多种思路,如将c['bNwsW']置程空函数, 阻断递归调用. 不过需要注意的是, c是b的局部变量, 想要更改c, 则当前栈必须在b函数的范围内.

c['bNwsW'] = function(){}

改完之后, 再跑一次, 发现c['bNwsW']又被改回来了, 原来每次进入b函数, 都会给c重新赋值. 既然这样, 那我们就把'bNwsW': function(e, f) 'JUpev': function(e, f)两个函数给删掉吧, 复制原本的b函数, 在控制台输入以下代码:

b= function(a) {
    var c = {
        'DZPan': function(e, f) {
            return e === f;
        },
        'HPymq': 'strin' + 'g',
        'keOAk': function(e) {
            return e();
        },
        'gOBNe': function(e, f) {
            return e !== f;
        },
        'ohRmf': function(e, f) {
            return e + f;
        },
        'hgrur': function(e, f) {
            return e / f;
        },
        'Cmstj': 'lengt' + 'h',
        'qZWsJ': function(e, f) {
            return e % f;
        },
        'bNwsW': function(e, f) {
            /********************/
            /* 这里删掉原本的实现 */
            /********************/
        },
        'JUpev': function(e, f) {
            /********************/
            /* 这里删掉原本的实现 */
            /********************/
        }
    };
    function d(e) {
        if (c['DZPan'](typeof e, c['HPymq'])) {
            var f = function() {
                while (!![]) {}
            };
            return c['keOAk'](f);
        } else {
            if (c['gOBNe'](c['ohRmf']('', c['hgrur'](e, e))[c['Cmstj']], -0xa89 + -0x1a0d * -0x1 + -0xb * 0x169) || c['DZPan'](c['qZWsJ'](e, 0x11 * -0x134 + 0x1 * -0x1db3 + 0x323b), -0x17f8 + 0xabd + 0x469 * 0x3))
                debugger ;
            else
                debugger ;
        }
        c['bNwsW'](d, ++e);
    }
    try {
        if (a)
            return d;
        else
            c['JUpev'](d, -0x4fa * -0x1 + 0x1f * -0xe2 + 0x1664);
    } catch (e) {}
}

大功告成!

image

经过更深入的调试, 发现整个b函数就没啥实际作用, 完全是用来反调试的, 这样我们就可以直接将b函数置空, 而不用担心会影响到正常的功能.

b=function(){}
c['JUpev']=function(){}
posted @   FeJQ  阅读(1238)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示