最近在研究 PopUnder 的实现方案,经过 Google 搜索 js popunder
出来的第一页中有个网站 popunderjs.com
,当时看了下,这是个提供 popunder 解决方案的一家公司,并且再翻了几页,发现市面上能解决这个问题的,只有2家公司,可见这个市场基本是属于垄断型的。
popunderjs 原来在 github 上是有开源代码的,但后来估计做者发现这个需求巨大的商业价值,索性不开源了,直接收费。因此如今要研究它的实现方案,只能上官网扒它源码了。 javascript
这是它的示例页:http://code.ptcong.com/demos/bjp/demo.html
分别加载了几个重要文件:html
http://code.ptcong.com/demos/bjp/script.js?0.3687041198903791 http://code.ptcong.com/demos/bjp/license.demo.js?0.31109710863616447
script.js 是功能主体,实现了 popunder 的全部功能以及定义了多个 API 方法
license.demo.js 是受权文件,有这个文件你才能顺利调用 script.js 里的方法java
这么具备商业价值的代码,就这么公开地给大家用,确定要考虑好被逆向的问题。咱们来看看它是怎么反逆向的。
首先,打开控制台,发现2个问题:node
Console was cleared script.js?0.5309098417125133:1
(function() {debugger})
也就是说,经常使用的断点调试方法已经没法使用了,咱们只能看看源代码,看能不能理解它的逻辑了。可是,它源代码是这样的:git
var a = typeof window === S[0] && typeof window[S[1]] !== S[2] ? window : global; try { a[S[3]](S[4]); return function() {} ; } catch (a) { try { (function() {} [S[11]](S[12])()); return function() {} ; } catch (a) { if (/TypeError/[S[15]](a + S[16])) { return function() {} ; } } }
可见源代码是根本不可能阅读的,因此仍是得想办法破掉它的反逆向措施。github
首先在断点调试模式一步步查看它都执行了哪些操做,忽然就发现了这么一段代码:api
(function() { (function a() { try { (function b(i) { if (('' + (i / i)).length !== 1 || i % 20 === 0) { (function() {} ).constructor('debugger')(); } else { debugger ; } b(++i); } )(0); } catch (e) { setTimeout(a, 5000); } } )() } )();
这段代码主要有2部分,一是经过 try {} 块内的 b() 函数来判断是否打开了控制台,若是是的话就进行自我调用,反复进入 debugger 这个断点,从而达到干扰咱们调试的目的。若是没有打开控制台,那调用 debugger 就会抛出异常,这时就在 catch {} 块内设置定时器,5秒后再调用一下 b() 函数。闭包
这么说来其实一切的一切都始于 setTimeout 这个函数(由于 b() 函数全是闭包调用,没法从外界破掉),因此只要在 setTimeout 被调用的时候,不让它执行就能够破解掉这个死循环了。app
因此咱们只须要简单地覆盖掉 setTimeout 就能够了……好比:ide
window._setTimeout = window.setTimeout; window.setTimeout = function () {};
可是!这个操做没法在控制台里面作!由于当你打开控制台的时候,你就必然会被吸入到 b() 函数的死循环中。这时再来覆盖 setTimeout 已经没有意义了。
这时咱们的工具 TamperMonkey 就上场了,把代码写到 TM 的脚本里,就算不打开控制台也能执行了。
TM 脚本写好以后,刷新页面,等它彻底加载完,再打开控制台,这时 debugger 已经不会再出现了!
接下来就轮到控制台刷新代码了
经过 Console was cleared
右侧的连接点进去定位到具体的代码,点击 {}
美化一下被压缩过的代码,发现其实就是用 setInterval 反复调用 console.clear() 清空控制台并输出了 <div>Console was cleared</div>
信息,可是注意了,不能直接覆盖 setInterval 由于这个函数在其余地方也有重要的用途。
因此咱们能够经过覆盖 console.clear() 函数和过滤 log 信息来阻止它的清屏行为。
一样写入到 TamperMonkey 的脚本中,代码:
window.console.clear = function() {}; window.console._log = window.console.log; window.console.log = function (e) { if (e['nodeName'] && e['nodeName'] == 'DIV') { return ; } return window.console.error.apply(window.console._log, arguments); };
之因此用 error 来输出信息,是为了查看它的调用栈,对理解程序逻辑有帮助。
基本上,作完这些的工做以后,这段代码就能够跟普通程序同样正常调试了。但还有个问题,它主要代码是常常混淆加密的,因此调试起来颇有难度。下面简单讲讲过程。
从 license.demo.js 能够看到开头有一段代码是这样的:
var zBCa = function T(f) { for (var U = 0, V = 0, W, X, Y = (X = decodeURI("+TR4W%17%7F@%17.....省略若干"), W = '', 'D68Q4cYfvoqAveD2D8Kb0jTsQCf2uvgs'); U < X.length; U++, V++) { if (V === Y.length) { V = 0; } W += String["fromCharCode"](X["charCodeAt"](U) ^ Y["charCodeAt"](V)); } var S = W.split("&&");
经过跟踪执行,能够发现 S 变量的内容实际上是本程序全部要用到的类名、函数名的集合,相似于 var S = ['console', 'clear', 'console', 'log']
。若是要调用 console.clear() 和 console.log() 函数的话,就这样
var a = window; a[S[0]][S[1]](); a[S[2]][S[3]]();
license.demo.js 中有多处这样的代码:
a['RegExp']('/R[\S]{4}p.c\wn[\D]{5}t\wr/','g')['test'](T + '')
这里的 a 表明 window,T 表明某个函数,T + ''
的做用是把 T 函数的定义转成字符串,因此这段代码的意思实际上是,验证 T 函数的定义中是否包含某些字符。
每次成功的验证,都会返回一个特定的值,这些个特定的值就是解密核心证书的参数。
多是由于我从新整理了代码格式,因此在从新运行的时候,这个证书一直运行不成功,因此后来就放弃了经过证书来突破的方案。
经过断点调试,咱们能够发现,想一步一步深刻地搞清楚这整个程序的逻辑,是十分困难,由于它大部分函数之间都是相互调用的关系,只是参数的不一样,结果就不一样。
因此我后来想了个办法,就是只查看它的系统函数的调用,经过对调用顺序的研究,也能够大体知道它执行了哪些操做。
要想输出全部系统函数的调用,须要解决如下问题:
window.console.clear()
这样的依附在实例上的函数,也要覆盖依附在类定义上的函数,如 window.HTMLAnchorElement.__proto__.click()
通过搜索后,找到了区份内置函数的代码:
// Used to resolve the internal `[[Class]]` of values var toString = Object.prototype.toString; // Used to resolve the decompiled source of functions var fnToString = Function.prototype.toString; // Used to detect host constructors (Safari > 4; really typed array specific) var reHostCtor = /^\[object .+?Constructor\]$/; // Compile a regexp using a common native method as a template. // We chose `Object#toString` because there's a good chance it is not being mucked with. var reNative = RegExp('^' + // Coerce `Object#toString` to a string String(toString) // Escape any special regexp characters .replace(/[.*+?^${}()|[\]\/\\]/g, '\\$&') // Replace mentions of `toString` with `.*?` to keep the template generic. // Replace thing like `for ...` to support environments like Rhino which add extra info // such as method arity. .replace(/toString|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$' ); function isNative(value) { var type = typeof value; return type == 'function' // Use `Function#toString` to bypass the value's own `toString` method // and avoid being faked out. ? reNative.test(fnToString.call(value)) // Fallback to a host object check because some environments will represent // things like typed arrays as DOM methods which may not conform to the // normal native pattern. : (value && type == 'object' && reHostCtor.test(toString.call(value))) || false; }
而后结合网上的资料,写出了递归覆盖内置函数的代码:
function wrapit(e) { if (e.__proto__) { wrapit(e.__proto__); } for (var a in e) { try { e[a]; } catch (e) { // pass continue; } var prop = e[a]; if (!prop || prop._w) continue; prop = e[a]; if (typeof prop == 'function' && isNative(prop)) { e[a] = (function (name, func) { return function () { var args = [].splice.call(arguments,0); // convert arguments to array if (false && name == 'getElementsByTagName' && args[0] == 'iframe') { } else { console.error((new Date).toISOString(), [this], name, args); } if (name == 'querySelectorAll') { //alert('querySelectorAll'); } return func.apply(this, args); }; })(a, prop); e[a]._w = true; }; } }
使用的时候只须要:
wrapit(window); wrapit(document);
而后模拟一下正常的操做,触发 PopUnder 就能够看到它的调用过程了。
参考资料:
A Beginners’ Guide to Obfuscation
Detect if function is native to browser
Detect if a Function is Native Code with JavaScript
接下来是广告时间:
个人简书:http://www.jianshu.com/u/0708f50bcf26
个人知乎:https://www.zhihu.com/people/never-younger
个人公众号:OutOfRange