本文基于这篇论文和这个代码仓库(及其 Pages 服务)。
有些人绕了反调试就没管了,有些整理之后直接顶会顶刊,这就是人与人的差距吧。
ShortCut(妨碍分析过程)
工作原理
用 JS 禁用常用快捷键触发的事件,如 F12、Ctrl+Shift+I、Ctrl+Shift+J 等。
应对方式
从浏览器菜单栏里面打开 DevTools,或者本地 mitmproxy 一下把响应里不让开 DevTools 的代码去掉;此外可以通过 Ctrl+U(一般不会禁用这个)或修改 URL 前缀将协议改为 view-source
查看网页源代码。
参考代码
下面的代码没有禁用 Ctrl+U:
window.addEventListener('keydown', function(event) {
console.log(event);
if (event.key == "F12" || ((event.ctrlKey || event.altKey) && (event.code == "KeyI" || event.key == "KeyJ" || event.key == "KeyU"))) {
event.preventDefault();
return false;
}
});
window.addEventListener('contextmenu', function(event) {
event.preventDefault();
return false;
});
TrigBreak(妨碍分析过程)
工作原理
通过 debugger
语句可在 JS 中自动下断点;通过 setInterval
高频执行包含 debugger
语句的函数即可阻止用户下断点调试。
应对方式
在 DevTools 里停用断点可禁用所有断点;在 DevTools 里 debugger
语句所在的行号右键可禁用这一行的断点(一些毒瘤的变种会持续创造包含debugger
语句的匿名函数并执行,导致这种方法失效);此外也可以使用支持去掉 debugger
语句的浏览器扩展或本地 mitmproxy 一下把响应里干坏事的代码去掉。
参考代码
下面的代码是一个简单的实现:
function debug() {
debugger;
setTimeout(debug, 1);
}
debug();
下面的代码是一个更加阴暗(使用 obfuscator 混淆)的实现:
var _0x1452cb = function () {
var _0x373b34 = !![];
return function (_0x5bd40f, _0x424dd9) {
var _0x502238 = _0x373b34 ? function () {
if (_0x424dd9) {
var _0x476265 = _0x424dd9['apply'](_0x5bd40f, arguments);
_0x424dd9 = null;
return _0x476265;
}
} : function () {
};
_0x373b34 = ![];
return _0x502238;
};
}();
(function () {
_0x1452cb(this, function () {
var _0xd0dec9 = new RegExp('function\x20*\x5c(\x20*\x5c)');
var _0x4c1d0d = new RegExp('\x5c+\x5c+\x20*(?:[a-zA-Z_$][0-9a-zA-Z_$]*)', 'i');
var _0x257572 = _0x448e86('init');
if (!_0xd0dec9['test'](_0x257572 + 'chain') || !_0x4c1d0d['test'](_0x257572 + 'input')) {
_0x257572('0');
} else {
_0x448e86();
}
})();
}());
function _0x448e86(_0x3d32ad) {
function _0x596cba(_0x4f5e6e) {
if (typeof _0x4f5e6e === 'string') {
return function (_0x43d248) {
}['constructor']('while\x20(true)\x20{}')['apply']('counter');
} else {
if (('' + _0x4f5e6e / _0x4f5e6e)['length'] !== 0x1 || _0x4f5e6e % 0x14 === 0x0) {
(function () {
return !![];
}['constructor']('debu' + 'gger')['call']('action'));
} else {
(function () {
return ![];
}['constructor']('debu' + 'gger')['apply']('stateObject'));
}
}
_0x596cba(++_0x4f5e6e);
}
try {
if (_0x3d32ad) {
return _0x596cba;
} else {
_0x596cba(0x0);
}
} catch (_0x4c5b3a) {
}
}
setInterval(function () {
_0x448e86();
}, 0xfa0);
ConClear(妨碍分析过程)
工作原理
不断调用 console.clear
函数使运行期间,如果不用调试器设置断点,几乎不可能检查输出。
应对方式
在控制台设置里面把“保留日志”打开,或者把 console.clear
覆盖成空函数;当然也可以 mitmproxy 一下把响应里的相关代码去掉。
参考代码
下面的代码是一个简单的实现:
function clear() {
console.clear();
setTimeout(clear, 10);
}
clear();
ModBuilt(改变分析结果)
工作原理
出于支持旧版浏览器等原因,JS 中所有的内置函数都可以被任意地重新定义,因此可重新定义分析者常用的 console
、String
和 JSON
等对象及内置的函数。
应对方式
相比于妨碍分析过程,改变分析结果更加隐蔽;可在执行待测试的代码前,先保存一份 JS 的内置函数的引用,之后将使用 JS 内置函数改为使用保存的引用。
参考代码
下面的代码是一个简单的实现(仅示意,未考虑递归的情况):
let originalStringify = JSON.stringify;
JSON.stringify = function(obj) {
if (typeof obj != "object") {
return originalStringify(obj);
}
let newObj = {};
for (let key of Object.keys(obj)) {
if (typeof obj[key] == "string") {
newObj[key] = obj[key].replace("shellcode", "benign code").replace("want to hide", "do not want to hide");
} else {
newObj[key] = obj[key];
}
}
return originalStringify(newObj);
}
let originalLog = console.log;
console.log = function(arg) {
arg = arg.replace("shellcode", "benign code").replace("want to hide", "do not want to hide");
originalLog(arg);
}
下面的代码使用 obfuscator 生成,禁用了控制台输出:
var _0x4bde55 = function () {
var _0x16e614 = !![];
return function (_0x41e722, _0xf342eb) {
var _0x280f64 = _0x16e614 ? function () {
if (_0xf342eb) {
var _0x24a5ce = _0xf342eb['apply'](_0x41e722, arguments);
_0xf342eb = null;
return _0x24a5ce;
}
} : function () {
};
_0x16e614 = ![];
return _0x280f64;
};
}();
var _0x54fe5d = _0x4bde55(this, function () {
var _0xb720ec = function () {
};
var _0x217a18;
try {
var _0xc3cc4a = Function('return\x20(function()\x20' + '{}.constructor(\x22return\x20this\x22)(\x20)' + ');');
_0x217a18 = _0xc3cc4a();
} catch (_0x228a20) {
_0x217a18 = window;
}
if (!_0x217a18['console']) {
_0x217a18['console'] = function (_0x2266c8) {
var _0x104cf8 = {};
_0x104cf8['log'] = _0x2266c8;
_0x104cf8['warn'] = _0x2266c8;
_0x104cf8['debug'] = _0x2266c8;
_0x104cf8['info'] = _0x2266c8;
_0x104cf8['error'] = _0x2266c8;
_0x104cf8['exception'] = _0x2266c8;
_0x104cf8['table'] = _0x2266c8;
_0x104cf8['trace'] = _0x2266c8;
return _0x104cf8;
}(_0xb720ec);
} else {
_0x217a18['console']['log'] = _0xb720ec;
_0x217a18['console']['warn'] = _0xb720ec;
_0x217a18['console']['debug'] = _0xb720ec;
_0x217a18['console']['info'] = _0xb720ec;
_0x217a18['console']['error'] = _0xb720ec;
_0x217a18['console']['exception'] = _0xb720ec;
_0x217a18['console']['table'] = _0xb720ec;
_0x217a18['console']['trace'] = _0xb720ec;
}
});
_0x54fe5d();
WidthDiff(检测分析行为)
工作原理
更多的情况下,打开 DevTools 将水平或垂直地分割浏览器窗口,因而可以同时获得包括所有工具栏在内的整个浏览器窗口的大小(外部大小)和没有任何工具栏的内容区域的大小(内部大小),当差异超出阈值时认为开启了 DevTools。
应对方式
把 DevTools 在单独的窗口打开即可(感觉身边不少人都默认单独窗口打开了),此外其它侧边栏的存在可能导致这种方式出现误判。
参考代码
下面的代码是一个参考的实现:
function detect() {
const devtools = { isOpen: false, orientation: undefined };
const threshold = 160;
const emitEvent = (isOpen, orientation) => {
let string = "<p>DevTools are " + (isOpen ? "open" : "closed") + "</p>";
console.log(string);
document.write(string);
};
setInterval(() => {
const widthThreshold = window.outerWidth - window.innerWidth > threshold;
const heightThreshold = window.outerHeight - window.innerHeight > threshold;
const orientation = widthThreshold ? 'vertical' : 'horizontal';
if (
!(heightThreshold && widthThreshold) &&
((window.Firebug && window.Firebug.chrome && window.Firebug.chrome.isInitialized) || widthThreshold || heightThreshold)
) {
if (!devtools.isOpen || devtools.orientation !== orientation) {
emitEvent(true, orientation);
}
devtools.isOpen = true;
devtools.orientation = orientation;
} else {
console.log(devtools.isOpen);
if (devtools.isOpen) {
emitEvent(false, undefined);
}
devtools.isOpen = false;
devtools.orientation = undefined;
}
}, 500);
if (typeof module !== 'undefined' && module.exports) {
module.exports = devtools;
} else {
window.devtools = devtools;
}
}
detect();
LogGet(检测分析行为)
工作原理
在一些控制台输出的方式中,被输出对象的 toString
方法在打开 DevTools 查看控制台的时候才被调用(如 console.profile + console.profileEnd
),可以此进行区分。
应对方式
这是检测 DevTools 是否打开的一个非常可靠的方法,比较难应对;除了删除控制台对象的所有记录功能外,只能尝试本地 mitmproxy 来修改响应了。
参考代码
下面的代码是一个参考的实现(本文发布时有效):
var devtools = function() {};
devtools.toString = function() {
if (this.open) {
document.clear();
document.write("DevTools were open! (You can close them again, but this text will stay)");
clearInterval(interval);
}
this.open = true;
return '-';
}
var interval = setInterval(() => {
console.profile(devtools);
console.profileEnd(devtools);
console.clear();
}, 100);
下面的代码是一个曾经有效的方式(利用 console.log
,参考 Stack Overflow):
function check() {
var devtools = function() {};
devtools.toString = function() {
this.opened = true;
};
console.log('%c', devtools);
if (devtools.opened) {
document.write("This technique is broken since Chrome 77. Only included for historical reasons");
}
}
setTimeout(check, 300);
下面的代码是另一个曾经有效的方式(利用 requestAnimationFrame
,参考 Stack Overflow):
var checkStatus;
var element = new Image();
Object.defineProperty(element, 'id', {
get: function() {
checkStatus = true;
throw new Error("Dev tools checker");
}
});
requestAnimationFrame(function check() {
console.dir(element);
console.log(checkStatus);
if (checkStatus) {
document.write("DevTools were open! (You can close them again, but this text will stay)");
console.clear();
return;
}
requestAnimationFrame(check);
});
MonBreak(基于时间的复杂反调试)
工作原理
由于 debugger
语句只有在开启了 DevTools 的情况下才会停止执行代码,我们可以简单地比较执行 debugger
语句前后的时间,如果超出阈值就认为 DevTools 已被打开;不同于 TrigBreak,MonBreak 的目标不是扰乱用户,而是推断出 DevTools 的状态,因而只要触发断点一次即可。
应对方式
由于存在(可疑的)debugger
语句,与 TrigBreak 类似,可在 DevTools 里停用断点以禁用所有断点,或在 DevTools 里 debugger
语句所在的行号右键禁用这一行的断点。
参考代码
下面的代码是一个参考的实现:
addEventListener("load", () => {
var threshold = 500;
const measure = () => {
const start = performance.now();
debugger;
const time = performance.now() - start;
if (time > threshold) {
document.write("<p>DevTools were open since page load</p>");
}
}
setInterval(measure, 300);
});
NewBreak(基于时间的复杂反调试)
工作原理
为 MonBreak 一个更加隐蔽的变种,注意到分析者可能下断点调试,断点被命中时可以通过时间信息来区分,只要反复调用一个函数并记录时间。如果发现此函数突然花了很长的时间来执行,就很有可能是因为一个断点被命中了。
应对方式
虽然这种方法更隐蔽,但由于只检测了是否有人打开 DevTools,然后触发了一个断点,只要分析者在使用 DevTools 时根本不设置/触发断点,就没有什么效果了。
参考代码
下面的代码是一个简单的实现(仅示意,未考虑失去焦点时对 setInterval
产生的影响并使用 hasFocus
进行判断与处理):
var timeSinceLast;
addEventListener("load", () => {
var threshold = 1000;
const measure = () => {
if (!timeSinceLast) {
timeSinceLast = performance.now();
}
const diff = performance.now() - timeSinceLast;
if (diff > threshold) {
document.write("<p>A breakpoint was hit</p>");
}
timeSinceLast = performance.now();
}
setInterval(measure, 300);
});
ConSpam(基于时间的复杂反调试)
工作原理
利用浏览器关闭与打开 DevTools 时的性能差异进行判断,一个曾经有效的做法是创建许多内容较长的文本元素,并快速地在 DOM 中反复添加和删除它们;一个目前有效的做法是向控制台写大量的输出,并检查这需要多长时间。
应对方式
这个方法比较难处理;除了删除控制台对象的所有记录功能外,只能尝试本地 mitmproxy 来修改响应了。
此外这个方法在实现上也存在一定难处,如果采用了固定的阈值,具有缓慢硬件的访问者可能被误判,如果采用了可变的阈值,就要首先测量开始时的几轮时间,因而只能应对 DevTools 在页面加载后被打开,而不是一开始就被打开的情况。
参考代码
下面的代码是一个参考的实现(本文发布时有效):
var baseline;
function measure() {
const start = performance.now();
for (let i = 0; i < 100; i++) {
console.log(i);
console.clear();
}
const time = performance.now() - start;
if (baseline === undefined) {
baseline = time;
}
else if (time > baseline * 2) {
document.write("DevTools were opened");
return;
}
setTimeout(measure, 1000);
}
measure();
结语
A backdoor could always be cleverly disguised as a “bugdoor”.
和全文相比,还是论文作者这里说的有水平呀,学到了学到了。
原文地址:http://www.cnblogs.com/Chenrt/p/16899145.html