PopUnder 研究:Javascript逆向與反逆向
最近在研究 PopUnder 的實(shí)現(xiàn)方案,通過(guò) Google 搜索 js popunder 出來(lái)的第一頁(yè)中有個(gè)網(wǎng)站 popunderjs.com ,當(dāng)時(shí)看了下,這是個(gè)提供 popunder 解決方案的一家公司,而且再翻了幾頁(yè),發(fā)現(xiàn)市面上能解決這個(gè)問(wèn)題的,只有2家公司,可見(jiàn)這個(gè)市場(chǎng)基本是屬于壟斷型的。
popunderjs 原來(lái)在 github 上是有開(kāi)源代碼的,但后來(lái)估計(jì)作者發(fā)現(xiàn)這個(gè)需求巨大的商業(yè)價(jià)值,索性不開(kāi)源了,直接收費(fèi)。所以現(xiàn)在要研究它的實(shí)現(xiàn)方案,只能上官網(wǎng)扒它源碼了。
這是它的示例頁(yè): http://code.ptcong.com/demos/bjp/demo.html 分別加載了幾個(gè)重要文件:
http://code.ptcong.com/demos/bjp/script.js?0.3687041198903791http://code.ptcong.com/demos/bjp/license.demo.js?0.31109710863616447 文件結(jié)構(gòu)
script.js 是功能主體,實(shí)現(xiàn)了 popunder 的所有功能以及定義了多個(gè) API 方法
license.demo.js 是授權(quán)文件,有這個(gè)文件你才能順利調(diào)用 script.js 里的方法
防止被逆向這么具有商業(yè)價(jià)值的代碼,就這么公開(kāi)地給你們用,肯定要考慮好被逆向的問(wèn)題。我們來(lái)看看它是怎么反逆向的。
首先,打開(kāi)控制臺(tái),發(fā)現(xiàn)2個(gè)問(wèn)題:
控制臺(tái)所有內(nèi)容都被反復(fù)清空,只輸出了這么一句話: Console was cleared script.js?0.5309098417125133:1 無(wú)法斷點(diǎn)調(diào)試,因?yàn)橐坏﹩⒂脭帱c(diǎn)調(diào)試功能,就會(huì)被定向到一個(gè)匿名函數(shù) (function() {debugger})也就是說(shuō),常用的斷點(diǎn)調(diào)試方法已經(jīng)無(wú)法使用了,我們只能看看源代碼,看能不能理解它的邏輯了。但是,它源代碼是這樣的:
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() {}; }} }
可見(jiàn)源代碼是根本不可能閱讀的,所以還是得想辦法破掉它的反逆向措施。
利用工具巧妙破解反逆向首先在斷點(diǎn)調(diào)試模式一步步查看它都執(zhí)行了哪些操作,突然就發(fā)現(xiàn)了這么一段代碼:
(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部分,一是通過(guò) try {} 塊內(nèi)的 b() 函數(shù)來(lái)判斷是否打開(kāi)了控制臺(tái),如果是的話就進(jìn)行自我調(diào)用,反復(fù)進(jìn)入 debugger 這個(gè)斷點(diǎn),從而達(dá)到干擾我們調(diào)試的目的。如果沒(méi)有打開(kāi)控制臺(tái),那調(diào)用 debugger 就會(huì)拋出異常,這時(shí)就在 catch {} 塊內(nèi)設(shè)置定時(shí)器,5秒后再調(diào)用一下 b() 函數(shù)。
這么說(shuō)來(lái)其實(shí)一切的一切都始于 setTimeout 這個(gè)函數(shù)(因?yàn)?b() 函數(shù)全是閉包調(diào)用,無(wú)法從外界破掉),所以只要在 setTimeout 被調(diào)用的時(shí)候,不讓它執(zhí)行就可以破解掉這個(gè)死循環(huán)了。
所以我們只需要簡(jiǎn)單地覆蓋掉 setTimeout 就可以了……比如:
window._setTimeout = window.setTimeout;window.setTimeout = function () {};
但是!這個(gè)操作無(wú)法在控制臺(tái)里面做!因?yàn)楫?dāng)你打開(kāi)控制臺(tái)的時(shí)候,你就必然會(huì)被吸入到 b() 函數(shù)的死循環(huán)中。這時(shí)再來(lái)覆蓋 setTimeout 已經(jīng)沒(méi)有意義了。
這時(shí)我們的工具 TamperMonkey 就上場(chǎng)了,把代碼寫(xiě)到 TM 的腳本里,就算不打開(kāi)控制臺(tái)也能執(zhí)行了。
TM 腳本寫(xiě)好之后,刷新頁(yè)面,等它完全加載完,再打開(kāi)控制臺(tái),這時(shí) debugger 已經(jīng)不會(huì)再出現(xiàn)了!
接下來(lái)就輪到控制臺(tái)刷新代碼了
通過(guò) Console was cleared 右側(cè)的鏈接點(diǎn)進(jìn)去定位到具體的代碼,點(diǎn)擊 {} 美化一下被壓縮過(guò)的代碼,發(fā)現(xiàn)其實(shí)就是用 setInterval 反復(fù)調(diào)用 console.clear() 清空控制臺(tái)并輸出了 <div>Console was cleared</div> 信息,但是注意了,不能直接覆蓋 setInterval 因?yàn)檫@個(gè)函數(shù)在其他地方也有重要的用途。
所以我們可以通過(guò)覆蓋 console.clear() 函數(shù)和過(guò)濾 log 信息來(lái)阻止它的清屏行為。
同樣寫(xiě)入到 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 來(lái)輸出信息,是為了查看它的調(diào)用棧,對(duì)理解程序邏輯有幫助。
基本上,做完這些的工作之后,這段代碼就可以跟普通程序一樣正常調(diào)試了。但還有個(gè)問(wèn)題,它主要代碼是經(jīng)常混淆加密的,所以調(diào)試起來(lái)很有難度。下面簡(jiǎn)單講講過(guò)程。
混淆加密方法一:隱藏方法調(diào)用,降低可讀性從 license.demo.js 可以看到開(kāi)頭有一段代碼是這樣的:
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('&&');
通過(guò)跟蹤執(zhí)行,可以發(fā)現(xiàn) S 變量的內(nèi)容其實(shí)是本程序所有要用到的類(lèi)名、函數(shù)名的集合,類(lèi)似于 var S = [’console’, ’clear’, ’console’, ’log’] 。如果要調(diào)用 console.clear() 和 console.log() 函數(shù)的話,就這樣
var a = window;a[S[0]][S[1]]();a[S[2]][S[3]](); 混淆加密方法二:將函數(shù)定義加入到證書(shū)驗(yàn)證流程
license.demo.js 中有多處這樣的代碼:
a[’RegExp’](’/R[S]{4}p.cwn[D]{5}twr/’,’g’)[’test’](T + ’’)
這里的 a 代表 window,T 代表某個(gè)函數(shù), T + ’’ 的作用是把 T 函數(shù)的定義轉(zhuǎn)成字符串,所以這段代碼的意思其實(shí)是,驗(yàn)證 T 函數(shù)的定義中是否包含某些字符。
每次成功的驗(yàn)證,都會(huì)返回一個(gè)特定的值,這些個(gè)特定的值就是解密核心證書(shū)的參數(shù)。
可能是因?yàn)槲抑匦抡砹舜a格式,所以在重新運(yùn)行的時(shí)候,這個(gè)證書(shū)一直運(yùn)行不成功,所以后來(lái)就放棄了通過(guò)證書(shū)來(lái)突破的方案。
逆向思路:輸出所有函數(shù)調(diào)用和參數(shù)通過(guò)斷點(diǎn)調(diào)試,我們可以發(fā)現(xiàn),想一步一步深入地搞清楚這整個(gè)程序的邏輯,是十分困難,因?yàn)樗蟛糠趾瘮?shù)之間都是相互調(diào)用的關(guān)系,只是參數(shù)的不同,結(jié)果就不同。
所以我后來(lái)想了個(gè)辦法,就是只查看它的系統(tǒng)函數(shù)的調(diào)用,通過(guò)對(duì)調(diào)用順序的研究,也可以大致知道它執(zhí)行了哪些操作。
要想輸出所有系統(tǒng)函數(shù)的調(diào)用,需要解決以下問(wèn)題:
覆蓋所有內(nèi)置變量及類(lèi)的函數(shù),我們既要覆蓋 window.console.clear() 這樣的依附在實(shí)例上的函數(shù),也要覆蓋依附在類(lèi)定義上的函數(shù),如 window.HTMLAnchorElement.__proto__.click() 需要正確區(qū)分內(nèi)置函數(shù)和自定義函數(shù)經(jīng)過(guò)搜索后,找到了區(qū)分內(nèi)置函數(shù)的代碼:
// 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; }
然后結(jié)合網(wǎng)上的資料,寫(xiě)出了遞歸覆蓋內(nèi)置函數(shù)的代碼:
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;}; }}
使用的時(shí)候只需要:
wrapit(window);wrapit(document);
然后模擬一下正常的操作,觸發(fā) PopUnder 就可以看到它的調(diào)用過(guò)程了。
參考資料:
A Beginners’ Guide to Obfuscation Detect if function is native to browser Detect if a Function is Native Code with JavaScript
來(lái)自:http://www.jianshu.com/p/9148d215c119
相關(guān)文章:
1. msxml3.dll 錯(cuò)誤 800c0019 系統(tǒng)錯(cuò)誤:-2146697191解決方法2. jsp+servlet簡(jiǎn)單實(shí)現(xiàn)上傳文件功能(保存目錄改進(jìn))3. 解析原生JS getComputedStyle4. 輕松學(xué)習(xí)XML教程5. HTML DOM setInterval和clearInterval方法案例詳解6. 阿里前端開(kāi)發(fā)中的規(guī)范要求7. xpath簡(jiǎn)介_(kāi)動(dòng)力節(jié)點(diǎn)Java學(xué)院整理8. jsp EL表達(dá)式詳解9. css代碼優(yōu)化的12個(gè)技巧10. jsp實(shí)現(xiàn)登錄驗(yàn)證的過(guò)濾器
