一、背景
这几天准备做个工具,涉及到一些公开数据收集工作,没想到过程曲折超过了想象。其中一个必须解决的问题是无限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__])
本人公众号大鱼七成饱,历史文章会在上面同步