首頁 >web前端 >js教程 >Javascript逆向與反逆向

Javascript逆向與反逆向

小云云
小云云原創
2017-12-05 10:25:516739瀏覽

popunderjs 原來在 github 上是有開源程式碼的,但後來估計作者發現這個需求巨大的商業價值,索性不開源了,直接收費。所以現在要研究它的實作方案,只能上官網扒它源碼了。

檔案結構

script.js 是一個功能主體,實作了popunder 的所有功能以及定義了多個API 方法

license.demo.js 是授權文件,有這個文件你才能順利調用script.js 裡的方法

防止被逆向

這麼有商業價值的程式碼,就這麼公開地給你們用,一定要考慮好被逆向的問題。我們來看看它是怎麼反逆向的。

首先,打開控制台,發現2個問題:

  1. 控制台所有內容都被重複清空,只輸出了這麼一句話: Console was cleared script.js?0.5309098417125133:1

  2. 無法斷點偵錯,因為一旦啟用斷點偵錯功能,就會被導向到一個匿名函數(function() {debugger})

#也就是說,常用的斷點偵錯方法已經無法使用了,我們只能看看原始碼,看能不能理解它的邏輯了。但是,它原始碼是這樣的:

<span style="font-size: 16px;">var a = typeof window === S[0] && typeof window[S[1]] !== S[2] ? window : global;<br>    try {<br>        a[S[3]](S[4]);<br>        return function() {}<br>        ;<br>    } catch (a) {<br>        try {<br>            (function() {}<br>            [S[11]](S[12])());<br>            return function() {}<br>            ;<br>        } catch (a) {<br>            if (/TypeError/[S[15]](a + S[16])) {<br>                return function() {}<br>                ;<br>            }<br>        }<br>    }<br></span>

#  

可見原始碼是根本不可能閱讀的,所以還是得想辦法破掉它的反向措施。

利用工具巧妙破解反逆向

首先在斷點偵錯模式一步步查看它都執行了哪些操作,突然就發現了這麼一段程式碼:

<span style="font-size: 16px;">(function() {<br>    (function a() {<br>        try {<br>            (function b(i) {<br>                if (('' + (i / i)).length !== 1 || i % 20 === 0) {<br>                    (function() {}<br>                    ).constructor('debugger')();<br>                } else {<br>                    debugger ;<br>                }<br>                b(++i);<br>            }<br>            )(0);<br>        } catch (e) {<br>            setTimeout(a, 5000);<br>        }<br>    }<br>    )()<br>}<br>)();<br></span>

這段程式碼主要有2部分,一是透過try {} 區塊內的b() 函數來判斷是否開啟了控制台,如果是的話就進行自我調用,反复進入debugger 這個斷點,從而達到干擾我們調試的目的。如果沒有開啟控制台,那呼叫 debugger 就會拋出異常,這時就在 catch {} 區塊內設定定時器,5秒後再呼叫一下 b() 函數。

這麼說來其實一切的一切都始於setTimeout 這個函數(因為b() 函數全是閉包調用,無法從外界破掉),所以只要在setTimeout 被調用的時候,不讓它執行就可以破解掉這個死循環了。

所以我們只要簡單地覆寫 setTimeout 就可以了…例如:

<span style="font-size: 16px;">window._setTimeout = window.setTimeout;<br>window.setTimeout = function () {};<br></span>

 

但是!這個操作無法在控制台裡面做!因為當你打開控制台的時候,你必然會被吸入到 b() 函數的死循環中。這時再來覆蓋 setTimeout 已經沒有意義了。

這時我們的工具 TamperMonkey 就上場了,把程式碼寫到 TM 的腳本裡,就算不打開主機也能執行了。

TM 腳本寫好之後,重新整理頁面,等它完全載入完,再開啟控制台,這時 debugger 已經不會再出現了!

接下來就輪到控制台刷新程式碼了

#透過Console was cleared 右側的連結點進去定位到具體的程式碼,點擊{} 美化一下被壓縮過的程式碼,發現其實就是用setInterval 重複呼叫console.clear() 清空控制台並輸出了

Console was cleared

訊息,但是注意了,不能直接覆蓋setInterval因為這個函數在其他地方也有重要的用途。

所以我們可以透過覆寫 console.clear() 函數和過濾 log 資訊來阻止它的清屏行為。

同樣寫入到 TamperMonkey 的腳本中,程式碼:

<span style="font-size: 16px;">window.console.clear = function() {};<br>window.console._log = window.console.log;<br>window.console.log = function (e) {<br>    if (e['nodeName'] && e['nodeName'] == 'p') {<br>        return ;<br>    }<br>    return window.console.error.apply(window.console._log, arguments);<br>};<br></span>

#  

之所以用 error 來輸出訊息,是為了檢視它的呼叫堆疊,對理解程式邏輯有幫助。

基本上,做完這些的工作之後,這段程式碼就可以跟普通程式一樣正常調試了。但還有個問題,它主要程式碼是經常混淆加密的,所以調試起來很有難度。下面簡單講講過程。

混淆加密方法一:隱藏方法調用,降低可讀性

從license.demo.js 可以看到開頭有一段程式碼是這樣的:

<span style="font-size: 16px;">var zBCa = function T(f) {<br>    for (var U = 0, V = 0, W, X, Y = (X = decodeURI("+TR4W%17%7F@%17.....省略若干"),<br>    W = '',<br>    'D68Q4cYfvoqAveD2D8Kb0jTsQCf2uvgs'); U < X.length; U++,<br/>    V++) {<br/>        if (V === Y.length) {<br/>            V = 0;<br/>        }<br/>        W += String["fromCharCode"](X["charCodeAt"](U) ^ Y["charCodeAt"](V));<br/>    }<br/>    var S = W.split("&&");<br/></span>

通过跟踪执行,可以发现 S 变量的内容其实是本程序所有要用到的类名、函数名的集合,类似于 var S = ['console', 'clear', 'console', 'log'] 。如果要调用 console.clear() 和 console.log() 函数的话,就这样

<span style="font-size: 16px;">var a = window;<br/>a[S[0]][S[1]]();<br/>a[S[2]][S[3]]();<br/></span>

混淆加密方法二:将函数定义加入到证书验证流程

license.demo.js 中有多处这样的代码:

<span style="font-size: 16px;">a[&#39;RegExp&#39;](&#39;/R[\S]{4}p.c\wn[\D]{5}t\wr/&#39;,&#39;g&#39;)[&#39;test&#39;](T + &#39;&#39;)<br/></span>

这里的 a 代表 window,T 代表某个函数, T + '' 的作用是把 T 函数的定义转成字符串,所以这段代码的意思其实是,验证 T 函数的定义中是否包含某些字符。

每次成功的验证,都会返回一个特定的值,这些个特定的值就是解密核心证书的参数。

可能是因为我重新整理了代码格式,所以在重新运行的时候,这个证书一直运行不成功,所以后来就放弃了通过证书来突破的方案。

逆向思路:输出所有函数调用和参数

通过断点调试,我们可以发现,想一步一步深入地搞清楚这整个程序的逻辑,是十分困难,因为它大部分函数之间都是相互调用的关系,只是参数的不同,结果就不同。

所以我后来想了个办法,就是只查看它的系统函数的调用,通过对调用顺序的研究,也可以大致知道它执行了哪些操作。

要想输出所有系统函数的调用,需要解决以下问题:

  1. 覆盖所有内置变量及类的函数,我们既要覆盖 window.console.clear() 这样的依附在实例上的函数,也要覆盖依附在类定义上的函数,如 window.HTMLAnchorElement.__proto__.click()

  2. 需要正确区分内置函数和自定义函数

经过搜索后,找到了区分内置函数的代码:

<span style="font-size: 16px;">// Used to resolve the internal `[[Class]]` of values<br/>  var toString = Object.prototype.toString;<br/><br/>  // Used to resolve the decompiled source of functions<br/>  var fnToString = Function.prototype.toString;<br/><br/>  // Used to detect host constructors (Safari > 4; really typed array specific)<br>  var reHostCtor = /^\[object .+?Constructor\]$/;<br><br>  // Compile a regexp using a common native method as a template.<br>  // We chose `Object#toString` because there's a good chance it is not being mucked with.<br>  var reNative = RegExp('^' +<br>    // Coerce `Object#toString` to a string<br>    String(toString)<br>    // Escape any special regexp characters<br>    .replace(/[.*+?^${}()|[\]\/\\]/g, '\\$&')<br>    // Replace mentions of `toString` with `.*?` to keep the template generic.<br>    // Replace thing like `for ...` to support environments like Rhino which add extra info<br>    // such as method arity.<br>    .replace(/toString|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$'<br>  );<br><br>  function isNative(value) {<br>    var type = typeof value;<br>    return type == 'function'<br>      // Use `Function#toString` to bypass the value's own `toString` method<br>      // and avoid being faked out.<br>      ? reNative.test(fnToString.call(value))<br>      // Fallback to a host object check because some environments will represent<br>      // things like typed arrays as DOM methods which may not conform to the<br>      // normal native pattern.<br>      : (value && type == 'object' && reHostCtor.test(toString.call(value))) || false;<br>  }<br></span>

 

然后结合网上的资料,写出了递归覆盖内置函数的代码:

<span style="font-size: 16px;">function wrapit(e) {<br>    if (e.__proto__) {<br>        wrapit(e.__proto__);<br>    }<br>    for (var a in e) {<br>        try {<br>            e[a];<br>        } catch (e) {<br>            // pass<br>            continue;<br>        }<br>        var prop = e[a];<br>        if (!prop || prop._w) continue;<br><br>        prop = e[a];<br>        if (typeof prop == 'function' && isNative(prop)) {<br>            e[a] = (function (name, func) {<br>                return function () {<br>                    var args = [].splice.call(arguments,0); // convert arguments to array<br>                    if (false && name == 'getElementsByTagName' && args[0] == 'iframe') {<br>                    } else {<br>                        console.error((new Date).toISOString(), [this], name, args);<br>                    }<br>                    if (name == 'querySelectorAll') {<br>                        //alert('querySelectorAll');<br>                    }<br>                    return func.apply(this, args);<br>                };<br>            })(a, prop);<br>            e[a]._w = true;<br>        };<br>    }<br>}<br></span>

 

使用的时候只需要:

<span style="font-size: 16px;">wrapit(window);<br>wrapit(document);<br></span>

 

然后模拟一下正常的操作,触发 PopUnder 就可以看到它的调用过程了。

参考资料:

A Beginners’ Guide to Obfuscation Detect if function is native to browser Detect if a Function is Native Code with JavaScript

以上内容就是Javascript逆向与反逆向的教程,希望能帮助到大家。

相关推荐:

JavaScript中undefined与null的区别详解

JavaScript中confirm()方法的使用介绍

JavaScript中的后退与刷新的实例详解

以上是Javascript逆向與反逆向的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn