最近在研究 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