摘要:原來在上是有開源代碼的,但后來估計作者發(fā)現(xiàn)這個需求巨大的商業(yè)價值,索性不開源了,直接收費(fèi)。所以現(xiàn)在要研究它的實現(xiàn)方案,只能上官網(wǎng)扒它源碼了。但是,它源代碼是這樣的可見源代碼是根本不可能閱讀的,所以還是得想辦法破掉它的反逆向措施。
緣起
最近在研究 PopUnder 的實現(xiàn)方案,通過 Google 搜索 js popunder 出來的第一頁中有個網(wǎng)站 popunderjs.com,當(dāng)時看了下,這是個提供 popunder 解決方案的一家公司,而且再翻了幾頁,發(fā)現(xiàn)市面上能解決這個問題的,只有2家公司,可見這個市場基本是屬于壟斷型的。
popunderjs 原來在 github 上是有開源代碼的,但后來估計作者發(fā)現(xiàn)這個需求巨大的商業(yè)價值,索性不開源了,直接收費(fèi)。所以現(xiàn)在要研究它的實現(xiàn)方案,只能上官網(wǎng)扒它源碼了。
這是它的示例頁:http://code.ptcong.com/demos/bjp/demo.html
分別加載了幾個重要文件:
http://code.ptcong.com/demos/bjp/script.js?0.3687041198903791 http://code.ptcong.com/demos/bjp/license.demo.js?0.31109710863616447文件結(jié)構(gòu)
script.js 是功能主體,實現(xiàn)了 popunder 的所有功能以及定義了多個 API 方法
license.demo.js 是授權(quán)文件,有這個文件你才能順利調(diào)用 script.js 里的方法
這么具有商業(yè)價值的代碼,就這么公開地給你們用,肯定要考慮好被逆向的問題。我們來看看它是怎么反逆向的。
首先,打開控制臺,發(fā)現(xiàn)2個問題:
控制臺所有內(nèi)容都被反復(fù)清空,只輸出了這么一句話:Console was cleared script.js?0.5309098417125133:1
無法斷點(diǎn)調(diào)試,因為一旦啟用斷點(diǎn)調(diào)試功能,就會被定向到一個匿名函數(shù) (function() {debugger})
也就是說,常用的斷點(diǎn)調(diào)試方法已經(jīng)無法使用了,我們只能看看源代碼,看能不能理解它的邏輯了。但是,它源代碼是這樣的:
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() {} ; } } }
可見源代碼是根本不可能閱讀的,所以還是得想辦法破掉它的反逆向措施。
利用工具巧妙破解反逆向首先在斷點(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部分,一是通過 try {} 塊內(nèi)的 b() 函數(shù)來判斷是否打開了控制臺,如果是的話就進(jìn)行自我調(diào)用,反復(fù)進(jìn)入 debugger 這個斷點(diǎn),從而達(dá)到干擾我們調(diào)試的目的。如果沒有打開控制臺,那調(diào)用 debugger 就會拋出異常,這時就在 catch {} 塊內(nèi)設(shè)置定時器,5秒后再調(diào)用一下 b() 函數(shù)。
這么說來其實一切的一切都始于 setTimeout 這個函數(shù)(因為 b() 函數(shù)全是閉包調(diào)用,無法從外界破掉),所以只要在 setTimeout 被調(diào)用的時候,不讓它執(zhí)行就可以破解掉這個死循環(huán)了。
所以我們只需要簡單地覆蓋掉 setTimeout 就可以了……比如:
window._setTimeout = window.setTimeout; window.setTimeout = function () {};
但是!這個操作無法在控制臺里面做!因為當(dāng)你打開控制臺的時候,你就必然會被吸入到 b() 函數(shù)的死循環(huán)中。這時再來覆蓋 setTimeout 已經(jīng)沒有意義了。
這時我們的工具 TamperMonkey 就上場了,把代碼寫到 TM 的腳本里,就算不打開控制臺也能執(zhí)行了。
TM 腳本寫好之后,刷新頁面,等它完全加載完,再打開控制臺,這時 debugger 已經(jīng)不會再出現(xiàn)了!
接下來就輪到控制臺刷新代碼了
通過 Console was cleared 右側(cè)的鏈接點(diǎn)進(jìn)去定位到具體的代碼,點(diǎn)擊 {} 美化一下被壓縮過的代碼,發(fā)現(xiàn)其實就是用 setInterval 反復(fù)調(diào)用 console.clear() 清空控制臺并輸出了
所以我們可以通過覆蓋 console.clear() 函數(shù)和過濾 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 來輸出信息,是為了查看它的調(diào)用棧,對理解程序邏輯有幫助。
基本上,做完這些的工作之后,這段代碼就可以跟普通程序一樣正常調(diào)試了。但還有個問題,它主要代碼是經(jīng)?;煜用艿?,所以調(diào)試起來很有難度。下面簡單講講過程。
混淆加密方法一:隱藏方法調(diào)用,降低可讀性從 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("&&");
通過跟蹤執(zhí)行,可以發(fā)現(xiàn) S 變量的內(nèi)容其實是本程序所有要用到的類名、函數(shù)名的集合,類似于 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ù)定義加入到證書驗證流程
license.demo.js 中有多處這樣的代碼:
a["RegExp"]("/R[S]{4}p.cwn[D]{5}twr/","g")["test"](T + "")
這里的 a 代表 window,T 代表某個函數(shù),T + "" 的作用是把 T 函數(shù)的定義轉(zhuǎn)成字符串,所以這段代碼的意思其實是,驗證 T 函數(shù)的定義中是否包含某些字符。
每次成功的驗證,都會返回一個特定的值,這些個特定的值就是解密核心證書的參數(shù)。
可能是因為我重新整理了代碼格式,所以在重新運(yùn)行的時候,這個證書一直運(yùn)行不成功,所以后來就放棄了通過證書來突破的方案。
逆向思路:輸出所有函數(shù)調(diào)用和參數(shù)通過斷點(diǎn)調(diào)試,我們可以發(fā)現(xiàn),想一步一步深入地搞清楚這整個程序的邏輯,是十分困難,因為它大部分函數(shù)之間都是相互調(diào)用的關(guān)系,只是參數(shù)的不同,結(jié)果就不同。
所以我后來想了個辦法,就是只查看它的系統(tǒng)函數(shù)的調(diào)用,通過對調(diào)用順序的研究,也可以大致知道它執(zhí)行了哪些操作。
要想輸出所有系統(tǒng)函數(shù)的調(diào)用,需要解決以下問題:
覆蓋所有內(nèi)置變量及類的函數(shù),我們既要覆蓋 window.console.clear() 這樣的依附在實例上的函數(shù),也要覆蓋依附在類定義上的函數(shù),如 window.HTMLAnchorElement.__proto__.click()
需要正確區(qū)分內(nèi)置函數(shù)和自定義函數(shù)
經(jīng)過搜索后,找到了區(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)上的資料,寫出了遞歸覆蓋內(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; }; } }
使用的時候只需要:
wrapit(window); wrapit(document);
然后模擬一下正常的操作,觸發(fā) PopUnder 就可以看到它的調(diào)用過程了。
參考資料:
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
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/88884.html
摘要:到現(xiàn)在,已經(jīng)不能隨意地給用戶彈出窗口了?,F(xiàn)在網(wǎng)上能明確實現(xiàn)這個功能的,都開了公司在賣代碼了,分別是這兩個價格非常之貴,美元年起步。如果是廣告平臺用,那就是美元年。這么一個小小的功能,就能賣如此之貴,可見其商業(yè)價值。 popunder 技術(shù)是用來在用戶正在瀏覽網(wǎng)頁時,后臺偷偷打開一個窗口并加載廣告。 這項技術(shù)看似簡單,實際上卻很復(fù)雜,涉及到多瀏覽器兼容,以及相同瀏覽器不同版本的實現(xiàn)方式也...
摘要:在代碼審計中,按業(yè)務(wù)流程審計當(dāng)然是必須的,人工的流程審計的優(yōu)點(diǎn)是能夠更加全面的發(fā)現(xiàn)漏洞,但是缺點(diǎn)是查找漏洞效率低下。代碼審計學(xué)習(xí)之旅總有人問我代碼審計該怎么學(xué)習(xí),該從哪學(xué)習(xí),現(xiàn)在統(tǒng)一回復(fù),表示我也不知道。。。 在代碼審計中,按業(yè)務(wù)流程審計當(dāng)然是必須的,人工的流程審計的優(yōu)點(diǎn)是能夠更加全面的發(fā)現(xiàn)漏洞,但是缺點(diǎn)是查找漏洞效率低下。如果要定向的查找漏洞,逆向跟蹤變量技術(shù)就顯得更加突出,如查找X...
閱讀 1968·2021-09-07 09:59
閱讀 2529·2019-08-29 16:33
閱讀 3709·2019-08-29 16:18
閱讀 2860·2019-08-29 15:30
閱讀 1689·2019-08-29 13:52
閱讀 2054·2019-08-26 18:36
閱讀 547·2019-08-26 12:19
閱讀 712·2019-08-23 15:23