一、背景
这几天准备做个工具,涉及到一些公开数据收集工作,没想到过程曲折超过了想象。其中一个必须解决的问题是无限debugger绕过。本来的原则有困难就躲开,绕过算了,不过这里太有用了,所以对这块进行了一点点研究,试试掏它老窝。
二、无限debugger它到底长啥样
1、直接加到代码里
            
            
              javascript
              
              
            
          
          debugger
eval("debugger")
        这种太简单了,估计没多少网站在用了。啥?还有人用,这人好善良。
2、无限循环
- while 循环
 - for 循环
 - 计时器
 
for和while循环就是方法体里加debugger。JS中的定时器是setInterval ,参数:第一个参数是要定时执行的代码,第二个参数是时间。下面的代码就是利用定时器来实现debugger操作:
            
            
              javascript
              
              
            
          
          setInterval(function () {
         debugger;
    }, 1000
)
        这种稍微有点难度,不过还好。
3、下面上难度:混淆
比较隐蔽的方法时混淆调用debugger的代码,如下面的几种方式
            
            
              javascript
              
              
            
          
          Function("debugger;").call()
Function("debugger;").apply()
或者
variable = Function("debugger;")
variable();
xxx.constructor("debugger").call("action")
Fuction.constructor("debugger").call("action")
Fuction.constructor("debugger").apply("action")
  
(function(){return !![];}["constructor"]("debugger")["call"]("action"))
['constructor']('debugger')['call']('action') : function() {return ![];}
        混淆过后代码可能长这样,文件名可能一样,或者每次都不一样,代码也可能每次请求都不一样。我遇到的是文件名和代码都动态,zhenima!!
            
            
              less
              
              
            
          
            if (('' + _0x6dfb46 / _0x6
                 dfb46)[_0xaed838(-0x340, -0x201, -0x2ba, -0x1b8, -0x84)] !== 0x1 || _0x6dfb46 % 0x14 === 0x0) {
                (function() {
                    return !![];
                }
                )['constructor'](_0x43a3d0(0x4ae, 0x34e, 0x44e, 0x433, "\u0059\u0045\u0073\u002a") + _0xaed838(-0x1f8, -0x31f, -0x24a, -0x413, -0x2b2))["\u0063\u0061\u006c\u006c"](_0x2826e3(-0xc5, -0x1b, -0x4b, -0x96, 0x5));
            } else {
                (function() {
                    return ![];
                }
                )["\u0063\u006f\u006e\u0073\u0074\u0072\u0075\u0063\u0074\u006f\u0072"](_0x6582e1['LeKcb'] + _0x43a3d0(0x436, 0x3c3, 0x470, 0x56c, "\u006b\u005a\u0047\u0046"))['apply'](_0x51784e(0x47d, 0x45b, 0x35c, "\u0075\u0043\u006f\u0021", 0x3f7));
            }
        三、解决方案
1、最简单:如果不需要其他断点,可以直接全局绕过
2、简单点:使用一律不在此处暂停
3、稍微简单点:添加条件断点
在 JS 代码 debugger 行数位置,鼠标右键添加 条件断点,其中条件 设为 false
4、稍微麻烦点:hook函数
在控制台中注入即可,以下是几种常用的hook函数
- hook构造函数
 
            
            
              javascript
              
              
            
          
          Function.prototype._constructor = Function.prototype.constructor;
Function.prototype.constructor = function () {
  if (arguments.toString().includes("debugger")) {
    return null;
  }
  return Function.prototype._constructor.apply(this, arguments);
};
        - 
hook方法
javascriptF_ = Function Function = function(s){ if (s!=='debugger'){return F_(s)} } - 
hook eval
javascripteval_ = eval; //下面为了过瑞数的 eval.toString 检测 eval = function (a) { if (a == "debugger") { return ""; } else { return eval_(a); } }; eval_back = eval eval = function (args) { if (args.includes('debugger')) { return null } else { return eval_back(args); } } - 
Hook setInterval 函数
 
            
            
              javascript
              
              
            
          
          setInterval_back = setInterval
setInterval = function(a,b){
    if(a.toString().includes('debugger')){
      return null;
    }
    return setInterval_back(a, b);
}
        - 
hook html
javascriptlet oldAppendChild = Node.prototype.appendChild Node.prototype.appendChild = function () { if (arguments[0].innerHTML && arguments[0].innerHTML.indexOf('debugger') !== -1) { arguments[0].innerHTML = '' } return oldAppendChild.apply(this, arguments) } 
5、稍微难点:文件替换的方式
如果以上还不行,则直接改写js文件
- 使用chrome 自带的方式,改写js文件,右键 Override content,然后保存,
 
- 稍微难和麻烦:动态文件替换
 
如果以上还不行,比如文件是动态的,这时候可使用插件或者Fiddler或者charles动态替换文件。先把js下载下来,然后根据规则替换
到这就结束了?不是,这些网上都可以找到,我遇到的这个网站大大滴狡猾,js文件名是接口返回的,每次请求都不一样,而且是混淆过,hook不到debugger代码,不同用途的文件名都是类似md5的名字,没有规则,那怎么解决呢
四、掏它老窝:终极方案
尝试多种方法后,我认为只能编码解决了,自定义的编码几乎可以覆盖以后的任何困难场景。基本思路就是代理所有请求,拿到接口请求数据,解析出文件规则,然后代理js请求,找到debugger的代码特征,根据特征规则静态或者动态替换返回值。
- 
第一步、网页代理
浏览器可以配置,host也可以直接配置,为了使用方便,我直接下载了SwitchyOmega插件
 
 * 第二步、编码抓到所有请求
            
            
              ini
              
              
            
          
              这里使用mitmproxy(<https://mitmproxy.org/)插件,我使用的是mac,安装命令是>
         # 安装命令行
         brew install mitmproxy
         # 安装python包,用于编码
         pip install -i https://pypi.tuna.tsinghua.edu.cn/simple mitmproxy 
    抓包所有请求,示例代码如下:
        def response(flow: http.HTTPFlow):
            if "captxxx/config?" in flow.request.pretty_url:
                response_text = flow.response.text
                print("test:" + response_text)
                res = process(response_text)
                modified_response_text = process_resp(response_text)
                flow.response.text = modified_response_text
        if __name__ == "__main__":
            from mitmproxy.tools.main import mitmdump
            mitmdump(['-s', __file__])
    def response(flow: http.HTTPFlow),用于处理请求,可以根据url区分,既可以改请求,也可以改返回值。
    python代码启动之后安装证书
        彻底解决无限debugger问题
一、背景
这几天准备做个工具,涉及到一些公开数据收集工作,没想到过程曲折超过了想象。其中一个必须解决的问题是无限debugger绕过。本来的原则有困难就躲开,绕过算了,不过这里太有用了,所以对这块进行了一点点研究,掏了它的老窝。

二、无限debugger它到底长啥样

1、直接加到代码里
            
            
              javascript
              
              
            
          
          debugger
eval("debugger")
        这种太简单了,估计没多少网站在用了。啥?还有人用,这人好善良。
2、无限循环
- while 循环
 - for 循环
 - 计时器
 
for和while循环就是方法体里加debugger。JS中的定时器是setInterval ,参数:第一个参数是要定时执行的代码,第二个参数是时间。下面的代码就是利用定时器来实现debugger操作:
            
            
              javascript
              
              
            
          
          setInterval(function () {
         debugger;
    }, 1000
)
        这种稍微有点难度,不过还好。
3、下面上难度:混淆
比较隐蔽的方法时混淆调用debugger的代码,如下面的几种方式
            
            
              javascript
              
              
            
          
          Function("debugger;").call()
Function("debugger;").apply()
或者
variable = Function("debugger;")
variable();
xxx.constructor("debugger").call("action")
Fuction.constructor("debugger").call("action")
Fuction.constructor("debugger").apply("action")
  
(function(){return !![];}["constructor"]("debugger")["call"]("action"))
['constructor']('debugger')['call']('action') : function() {return ![];}
        混淆过后代码可能长这样,文件名可能一样,或者每次都不一样,代码也可能每次请求都不一样。我遇到的是文件名和代码都动态,zhenima!!
            
            
              less
              
              
            
          
            if (('' + _0x6dfb46 / _0x6
                 dfb46)[_0xaed838(-0x340, -0x201, -0x2ba, -0x1b8, -0x84)] !== 0x1 || _0x6dfb46 % 0x14 === 0x0) {
                (function() {
                    return !![];
                }
                )['constructor'](_0x43a3d0(0x4ae, 0x34e, 0x44e, 0x433, "\u0059\u0045\u0073\u002a") + _0xaed838(-0x1f8, -0x31f, -0x24a, -0x413, -0x2b2))["\u0063\u0061\u006c\u006c"](_0x2826e3(-0xc5, -0x1b, -0x4b, -0x96, 0x5));
            } else {
                (function() {
                    return ![];
                }
                )["\u0063\u006f\u006e\u0073\u0074\u0072\u0075\u0063\u0074\u006f\u0072"](_0x6582e1['LeKcb'] + _0x43a3d0(0x436, 0x3c3, 0x470, 0x56c, "\u006b\u005a\u0047\u0046"))['apply'](_0x51784e(0x47d, 0x45b, 0x35c, "\u0075\u0043\u006f\u0021", 0x3f7));
            }
        
三、解决方案
1、最简单:如果不需要其他断点,可以直接全局绕过

2、简单点:使用一律不在此处暂停

3、稍微简单点:添加条件断点
在 JS 代码 debugger 行数位置,鼠标右键添加 条件断点,其中条件 设为 false

4、稍微麻烦点:hook函数
在控制台中注入即可,以下是几种常用的hook函数
- hook构造函数
 
            
            
              javascript
              
              
            
          
          Function.prototype._constructor = Function.prototype.constructor;
Function.prototype.constructor = function () {
  if (arguments.toString().includes("debugger")) {
    return null;
  }
  return Function.prototype._constructor.apply(this, arguments);
};
        - 
hook方法
javascriptF_ = Function Function = function(s){ if (s!=='debugger'){return F_(s)} } - 
hook eval
javascripteval_ = eval; //下面为了过瑞数的 eval.toString 检测 eval = function (a) { if (a == "debugger") { return ""; } else { return eval_(a); } }; eval_back = eval eval = function (args) { if (args.includes('debugger')) { return null } else { return eval_back(args); } } - 
Hook setInterval 函数
 
            
            
              javascript
              
              
            
          
          setInterval_back = setInterval
setInterval = function(a,b){
    if(a.toString().includes('debugger')){
      return null;
    }
    return setInterval_back(a, b);
}
        - 
hook html
javascriptlet oldAppendChild = Node.prototype.appendChild Node.prototype.appendChild = function () { if (arguments[0].innerHTML && arguments[0].innerHTML.indexOf('debugger') !== -1) { arguments[0].innerHTML = '' } return oldAppendChild.apply(this, arguments) } 
5、稍微难点:文件替换的方式
如果以上还不行,则直接改写js文件
- 
使用chrome 自带的方式,改写js文件,右键 Override content,然后保存,

 - 
稍微难和麻烦:动态文件替换
 
如果以上还不行,比如文件是动态的,这时候可使用插件或者Fiddler或者charles动态替换文件。先把js下载下来,然后根据规则替换

到这就结束了?不是,这些网上都可以找到,我遇到的这个网站大大滴狡猾,js文件名是接口返回的,每次请求都不一样,而且是混淆过,hook不到debugger代码,不同用途的文件名都是类似md5的名字,没有规则,那怎么解决呢
四、掏它老窝:终极方案
尝试多种方法后,我认为只能编码解决了,自定义的编码几乎可以覆盖以后的任何困难场景。基本思路就是代理所有请求,拿到接口请求数据,解析出文件规则,然后代理js请求,找到debugger的代码特征,根据特征规则静态或者动态替换返回值。
- 
第一步、网页代理
浏览器可以配置,host也可以直接配置,为了使用方便,我直接下载了SwitchyOmega插件
 
- 
第二步、编码抓到所有请求
这里使用mitmproxy(mitmproxy.org/)插件,我使用的是ma...
bash# 安装命令行 brew install mitmproxy # 安装python包,用于编码 pip install -i https://pypi.tuna.tsinghua.edu.cn/simple mitmproxy抓包所有请求,示例代码如下:
inidef response(flow: http.HTTPFlow): if "captxxx/config?" in flow.request.pretty_url: response_text = flow.response.text print("test:" + response_text) res = process(response_text) modified_response_text = process_resp(response_text) flow.response.text = modified_response_text if __name__ == "__main__": from mitmproxy.tools.main import mitmdump mitmdump(['-s', __file__])def response(flow: http.HTTPFlow),用于处理请求,可以根据url区分,既可以改请求,也可以改返回值。python代码启动之后安装证书
 
- 
记录并解析接口返回值
inidef response(flow: http.HTTPFlow): if "http://xxxxxconfig.json" in flow.request.pretty_url: response_text = flow.response.text print("test:" + response_text) # 保存配置文件 save_file('config_orgin.json', response_text) # 解析配置文件 parser_save(response_text) modified_response_text = response_text flow.response.text = modified_response_text def parser_save(response_text): content = re.sub(r'^[^(]*', '', response_text) content = content.replace("(", '') content = content.replace(");", '') # print('##before load' + content) content_json = json.loads(content) # print('##after load' + json.dumps(content_json)) server_host = content_json["result"]['data']["cdn_server"] js1_path = content_json["result"]['data']["js1_path"] js2_path = content_json["result"]['data']["js2_path"] js1_all = "https://" + server_host + "/" + js1_path + "?v=v3" js2_all = "https://" + server_host + "/" + js2_path + "?v=v3" map = { js1_all: 'js1', js2_all: 'js2' } with open('js_all.json', 'w') as file: json.dump(map, file) - 
根据规则替换js文件
由于不同文件的js代码不同,规则也不一样,然后没种文件特征一样,比如下面对功能1代码的动态替换操作
pythonif "https://cdnxxx.js" in flow.request.pretty_url: response_text = flow.response.text print("#####json resp url:" + flow.request.pretty_url) modified_response_text = process_js_file(flow.request.pretty_url, response_text) flow.response.text = modified_response_text # 是否在配置文件中,有则根据规则替换掉debugger代码 def process_js_file(url, content): print("#####url:" + url) with open('js_all.json', 'r') as file: map = json.load(file) if url in map: print("###包含" + url) file_name = map[url] + '.js' with open(file_name, 'r') as file: return file.read() return content def replace_string(start_str, end_str, s): try: # 查找第一个起始位置 start_idx = s.find(start_str) if start_idx == -1: return s # 查找最后一个结束位置(从起始位置之后开始) end_idx = s.rfind(end_str, start_idx + len(start_str)) if end_idx == -1: return s # 计算有效截取范围 end_len = len(end_str) return s[:start_idx] + "console.log("'debugeer'")" + s[end_idx + end_len:] except IndexError: return s def test_clean(content): res = replace_string("(function(){return!![];})['constructor'](", "));", content) res = replace_string("(function(){return![];})['constructor'](_", "]);", res) return res最后成功跳过所有debugger代码
 
这样,利用代理机制+编码修改,解决了文件名、代码都是动态的问题。实际上能编码的话,可以模拟浏览器的所有请求,模拟网站的规则修改所有代码。
完整代码如下:
            
            
              python
              
              
            
          
          from mitmproxy import http
import json
import os
import re
def save_file(filename, content):
    with open(filename, "w") as file:
        file.write(content)
def process_js_file(url, content):
    print("#####url:" + url)
    with open('js_all.json', 'r') as file:
        map = json.load(file)
    if url in map:
        print("###包含" + url)
        return test_clean(content)
    return content
def replace_string(start_str, end_str, s):
    try:
        # 查找第一个起始位置
        start_idx = s.find(start_str)
        if start_idx == -1:
            return s
        # 查找最后一个结束位置(从起始位置之后开始)
        end_idx = s.rfind(end_str, start_idx + len(start_str))
        if end_idx == -1:
            return s
        # 计算有效截取范围
        end_len = len(end_str)
        return s[:start_idx]  + "console.log("'debugeer'")" + s[end_idx + end_len:]
    except IndexError:
        return s
def test_clean(content):
    res = replace_string("(function(){return!![];})['constructor'](", "));", content)
    res = replace_string("(function(){return![];})['constructor'](_", "]);", res)
    return res
def parser_save(response_text):
    content = re.sub(r'^[^(]*', '', response_text)
    content = content.replace("(", '')
    content = content.replace(");", '')
    # print('##before load' + content)
    content_json = json.loads(content)
    # print('##after load' + json.dumps(content_json))
    server_host = content_json["result"]['data']["cdn_server"]
    js1_path = content_json["result"]['data']["js1_path"]
    js2_path = content_json["result"]['data']["js2_path"]
    js1_all = "https://" + server_host + "/" + js1_path + "?v=v3"
    js2_all = "https://" + server_host + "/" + js2_path + "?v=v3"
    map = {
        js1_all: 'js1',
        js2_all: 'js2'
    }
    print('###save' + json.dumps(map))
    with open('js_all.json', 'w') as file:
        json.dump(map, file)
def response(flow: http.HTTPFlow):
    if "http://xxxxxconfig.json" in flow.request.pretty_url:
        response_text = flow.response.text
        print("test:" + response_text)
        # 保存配置文件
        save_file('config_orgin.json', response_text)
        # 解析配置文件
        parser_save(response_text)
        modified_response_text = response_text
        flow.response.text = modified_response_text
    if "https://xxx/scripts/" in flow.request.pretty_url:
        response_text = flow.response.text
        print("#####json resp url:" + flow.request.pretty_url)
        modified_response_text = process_js_file(flow.request.pretty_url, response_text)
        flow.response.text = modified_response_text
if __name__ == "__main__":
    from mitmproxy.tools.main import mitmdump
    mitmdump(['-s', __file__])
        本人公众号大鱼七成饱,历史文章会在上面同步