浏览器调试与常用 Hook
Hook 相关内容参考视频 https://www.bilibili.com/video/BV1gQ4mzMEA4/
定位加密参数
使用请求重放,一步步移除cookie、headers、query等参数中无关紧要内容。定位最终的加密参数
注意响应是否存在 set-cookie。一些 cookie 与加密 js 一一对应
断点
根据不同情况,可选用不同断点
dom断点
通过 ajax 请求拿到数据后,渲染到页面中时可通过 dom 断点定位页面元素被修改的代码
Elements tap ->指定标签右键 -> Break on(发生中断的条件)
事件监听断点
在调试时常用的 Event Listener Breakpoints 事件监听器断点 介绍
- Script断点: 读取到脚本时暂停 Script 脚本
- Timer 断点: 设置或清除计时器
- Control 断点: 元素焦点,元素失焦,submit提交数据
- Mouse 断点: 鼠标操作断点
xhr断点
发送指定请求时的断点,可通过请求地址暂停到指定请求之前
源码断点
源码断点常用的是以下几种
普通代码断点:蓝色
- 搜索关键词: 如
sign=,sign",sign:等 open函数: xhr请求初始化函数- etc.
- 搜索关键词: 如
条件断点:橙色。右键添加条件断点。运行到条件断点处,会执行设置的内容。当设置内容返回为true时中断
编写条件,返回true时断下
- 参数数量为 N 时断下
arguments.callee.length === N - window 上绑定变量用于控制是否启动断点后,启动断点
window.enableBreakpoints === true
- 参数数量为 N 时断下
可以编写普通代码(无返回值代码,如Statement),则会执行而不中断(代码不返回true时,不会触发断点机制)
Eg1: 修改断点行的参数值
var id = 1; var a = id; // 此行加断点,编写 id = 999,则此时id被修改为999,a会被赋值id的当前值,即999Eg2: 计时
for (let i = 1; i < 9999999; i++) { // 断点内容 console.time('calc time') window.getComputedStyle(document.body) } // 断点内容 console.timeEnd('calc time')
日志断点:粉色。自动在控制台输出断点所设置的内容(监控当前代码位置,某个变量的值,即使当前代码为使用该变量)。通常设置为字符串拼接变量,例如
"当前i的值为" + i
调用堆栈
在调试时,常看的调用堆栈信息分为 Network 数据包的 Initiator 启动器 和代码断点的 Call Stack 调用栈
两个地方堆栈显示都遵循了上方是栈顶的策略。即最下方的是先触发的,最上方的是最后调用的
禁控制台
开源项目 disable-devtool 会检测控制台情况并触发以下行为
- 禁用 F12 与右键,减少控制台打开的方式
- 检测到控制台打开后,调用
window.close()关闭当前标签页 - 检测到控制台打开后,跳转到其他安全页面
- 检测到控制台打开后,反复使用
console.clear()清空控制台输出 - 检测到控制台打开后,反复使用
console.table()控制台输出大量内容 - 检测到控制台打开后,反复使用
window.history.go()与window.history.back()进行跳转 - etc(其他行为可详见项目说明)
处理手段
- hook
window.close()为空函数,防止自动关闭 - hook
window.onbeforeunload = () => {debugger;return false}, 在堆栈中定位跳转代码,移除跳转代码并 overrides - hook
console.clear()为空函数,防止清楚控制台输出 - hook
console.table() - hook
window.history的go和back为空方法
过 debugger
debugger 干扰调试通过与定时器配合实现反复触发 debugger 但不影响主进程执行业务逻辑
常规的 debugger 可通过断点不暂停过掉。
出现一些debugger无法通过断点不暂停过掉的情况,如: 使用 Function 构造 debugger
// 通过自定义函数构造器,调用 Function 构造 debugger 并调用
func.constructor('debugger').call()
// 通过匿名函数
(function(){}.constructor('debugger').call())
可在发生 debugger 时查看堆栈,定位触发 debugger 的代码块(函数)。在执行该代码块前下断点。刷新后重写该代码,再放行(HOOK时机: 函数定义之后,执行之前,进行重写)
或通过 hook 方式过 debugger (修改内置可能会被检测)。以下是 hook 代码
- 重写定时器
_setInterval = setInterval setInterval = function(a, b){ // 判断无 debugger 关键字,再正常设置定时器 if (a.toSting().indexOf('debugger') === -1) { return _setInterval(a, b) } } - 重写构造器
_constructor = Function.prototype.constructor Function.prototype.constructor = function() { if(arguments[0] === 'debugger') { // 什么都不做 } else { return _constructor.apply(this, arguments) } } - 重写 eval: 处理使用
eval进行 debugger 的方式_eval = eval eval = function(s) { // 检测到 eval 内容不存在 debugger if (s.indexOf('debugger') === -1) { return _eval(s) } }
内存爆破
通过格式化检测,代码执行时间差,浏览器指纹等方式,检查代码是否正常运行或处于调试状态。再通过死循环进行内存爆破
格式化检测手段: 正则或 toString ,检测目标函数是否被格式化
Hook
可用于快速定位指定函数或属性的调用点
注意点:
Hook 时机: Hook 指定函数,需在函数定义之后,函数使用之前。属性也是如此。如
- 浏览器内置函数或属性: 已经在浏览器中被定义,因此可在新建标签页时进行 Hook
- 代码中定义的函数或属性: 通过断点,使代码停在该函数或属性定义之后(防止被重写回原本内容),函数调用之前(防止被调用的依然是原本内容),再进行 Hook
Hook 内置方法时,注意使用 toString保护代码 对修改后的方法进行保护,防止
toString检测
Hook 模版
- 函数
_<函数名> = <函数名> <函数名> = function(argument) { // 需要增加的操作,如 打印,debugger 等 // 正常返回 return _<函数名>.apply(argument) } // 重写原型链,避免原型链检测 Hook // <函数名>.prototype.xxx = xxx <函数名>.toString = function(){return 'function <函数名>() { [naive code] }'} - 对象的属性: 使用
Object.defineProperty方法。注意: 此方法只能绑定一次。如果网站使用此函数定义对象属性,则无法再通过该函数 Hook<对象名>._<属性名> = <对象名>.<属性名> Object.defineProperty(<对象名>, '<属性名>', { get: function() { // 需要增加的操作,如 打印,debugger 等 // 正常返回 return <对象名>._<属性名> }, set: function (value) { // 需要增加的操作,如 打印,debugger 等 return ... } })
Tips: 如果 Hook 的内容是对象在原型链上的内容,如字符串、数组、XHR 的相关函数,为了使该函数顺利执行,可在返回时使用 this
Array.prototype._pop = Array.prototype.pop
Array.prototype.pop = function() {
debugger;
return this._pop()
}
// 此处使用 toString 保护
safeFunction(Array.prototype.pop)
// 测试是否正常
console.log(Array.prototype.pop.toString())
arr = [1,2,3,4,5]
arr.pop(6)
XMLHttpRequest.prototype._setRequestHeader = XMLHttpRequest.prototype.setRequestHeader
XMLHttpRequest.prototype.setRequestHeader = function(){...}
油猴脚本
可通过油猴脚本将 js 代码进行注入,从而实现 hook
需要注意的配置项 @run-at。该配置项有以下几种选项
@run-at document-start: 开始渲染 document 时注入(hook 内置对象或函数时使用此方法)@run-at document-body: body渲染时注入@run-at document-idle(默认值):OOMContentLoaded事件后注入@run-at context-menu: 手动点击脚本时注入
Hook 脚本示例
https://github.com/0xsdeo/Hook_JS
扣代码
流程:
定位到加密位置,将最终生成代码取出
取出加密位置涉及的一个内容,查看内部是否存在未定义的内容,进行补充,直到加密位置涉及的这个内容完全可用(深度优先)
再取出下一个内容进行补充
Tips:
- 在控制台中活用
copy()函数,将目标变量复制到剪切板,便于直接粘贴 - 在补环境或扣代码时,如果在浏览器中正常运行但 node 中异常,只有两种可能性: 浏览器指纹 或 格式化检测
- 进一步判断: 将代码在浏览器控制台运行。 如果此时顺利运行,说明检测设备指纹。 如果依然存在问题,说明存在格式化检测
- 如何处理格式化检测
- 此时在代码行首
debugger,在浏览器控制台 使用 单步调试 与 跳过函数调用 定位内存爆破的调用位置 - 通过
debugger与堆栈定位哪个分支进入了内存爆破的代码 - 通过阅读分支条件了解是检测了哪个函数的格式化
- 为实现最小影响,仅修改被检测函数为非格式化状态
- 后续再遇到问题时,接着通过浏览器控制台执行代码判断是指纹检测还是格式化检测
- 如果依然格式化检测,从之前已排查的位置开始
debugger向后排查
- 进一步判断: 将代码在浏览器控制台运行。 如果此时顺利运行,说明检测设备指纹。 如果依然存在问题,说明存在格式化检测
混淆框架处理手段: jsfuck、eval 等内容,通过源码得知检测内容。再手动过检测后,检查上下文是否有其他改动内容。如果有则保持原本内容在原本位置或替换成源码
- OB: 明确数组、位移、解密函数的位置,不格式化这三部分代码。在其余的代码逻辑(主逻辑)中定位加密参数生成位置
- jsfuck: 将代码在浏览器控制台执行,查看是否报错
- 报错: 报错信息内会包含去编码后的源码
- 未报错: 在代码末尾删减部分,使其报错,从而可在报错信息内查看到源码
- 移除括号
- 末尾括号内(
eval)拼接一个任意字符
- AAEncode(表情包): 将代码在浏览器控制台执行,查看输出
- undefined: 删除部分结尾字符
- eval: 可能与某个混淆手段配合,
eval主逻辑中很长一串内容。将代码中的eval替换成console.log打印拿到代码内容
蜜罐
当nodejs顺利输出结果但结果不可用,可能走到了错误的分支。此时重点排查的产生分支的地方
tryif||或&&逻辑符控制代码是否执行?+:检查nodejs执行分支是否与浏览器内一致
插桩
面对 VMP 时,可使用 overide 手动增加 console.log 或使用 日志断点 进行插桩
如果没输出,检查分支情况,console.log 是否被重写
恢复log输出
当 console.log 被重写时,日志断点也无法正常输出
- 使用
script断点,使代码停在执行js之前 - 使用自定义函数接收输出功能
console._log = console.log - 使用断点停在插桩位置前,恢复输出功能
console.log = console._log
插桩重点位置
call与apply:eval或 wasm 等调用时会使用到- 栈内弹出元素的
^+-*%>>等二元计算: 吐出核心计算流程