PopUnder 研究:Javascript逆向與反逆向

緣起

最近在研究 PopUnder 的實現(xiàn)方案描姚,通過 Google 搜索 js popunder 出來的第一頁中有個網(wǎng)站 popunderjs.com,當(dāng)時看了下,這是個提供 popunder 解決方案的一家公司,而且再翻了幾頁曹鸠,發(fā)現(xiàn)市面上能解決這個問題的,只有2家公司斥铺,可見這個市場基本是屬于壟斷型的彻桃。
popunderjs 原來在 github 上是有開源代碼的,但后來估計作者發(fā)現(xiàn)這個需求巨大的商業(yè)價值晾蜘,索性不開源了邻眷,直接收費。所以現(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個問題:

  1. 控制臺所有內(nèi)容都被反復(fù)清空冯乘,只輸出了這么一句話:Console was cleared script.js?0.5309098417125133:1
  2. 無法斷點調(diào)試洽胶,因為一旦啟用斷點調(diào)試功能,就會被定向到一個匿名函數(shù) (function() {debugger})

也就是說裆馒,常用的斷點調(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à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 這個斷點钦铺,從而達(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è)的鏈接點進(jìn)去定位到具體的代碼稚伍,點擊 {} 美化一下被壓縮過的代碼,發(fā)現(xiàn)其實就是用 setInterval 反復(fù)調(diào)用 console.clear() 清空控制臺并輸出了 <div>Console was cleared</div> 信息巢株,但是注意了槐瑞,不能直接覆蓋 setInterval 因為這個函數(shù)在其他地方也有重要的用途。

所以我們可以通過覆蓋 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.c\wn[\D]{5}t\wr/','g')['test'](T + '')

這里的 a 代表 window填大,T 代表某個函數(shù),T + '' 的作用是把 T 函數(shù)的定義轉(zhuǎn)成字符串俏橘,所以這段代碼的意思其實是允华,驗證 T 函數(shù)的定義中是否包含某些字符。

每次成功的驗證寥掐,都會返回一個特定的值例获,這些個特定的值就是解密核心證書的參數(shù)。

可能是因為我重新整理了代碼格式曹仗,所以在重新運行的時候榨汤,這個證書一直運行不成功,所以后來就放棄了通過證書來突破的方案怎茫。

逆向思路:輸出所有函數(shù)調(diào)用和參數(shù)

通過斷點調(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)用,需要解決以下問題:

  1. 覆蓋所有內(nèi)置變量及類的函數(shù)摊聋,我們既要覆蓋 window.console.clear() 這樣的依附在實例上的函數(shù)鸡捐,也要覆蓋依附在類定義上的函數(shù),如 window.HTMLAnchorElement.__proto__.click()
  2. 需要正確區(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.reibang.com/u/0708f50bcf26
我的知乎:https://www.zhihu.com/people/never-younger
我的公眾號:OutOfRange

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末色迂,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子手销,更是在濱河造成了極大的恐慌歇僧,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,607評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件原献,死亡現(xiàn)場離奇詭異馏慨,居然都是意外死亡埂淮,警方通過查閱死者的電腦和手機(jī)姑隅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,239評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來倔撞,“玉大人讲仰,你說我怎么就攤上這事』居” “怎么了鄙陡?”我有些...
    開封第一講書人閱讀 164,960評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長躏啰。 經(jīng)常有香客問我趁矾,道長,這世上最難降的妖魔是什么给僵? 我笑而不...
    開封第一講書人閱讀 58,750評論 1 294
  • 正文 為了忘掉前任毫捣,我火速辦了婚禮详拙,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蔓同。我一直安慰自己饶辙,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,764評論 6 392
  • 文/花漫 我一把揭開白布斑粱。 她就那樣靜靜地躺著弃揽,像睡著了一般。 火紅的嫁衣襯著肌膚如雪则北。 梳的紋絲不亂的頭發(fā)上矿微,一...
    開封第一講書人閱讀 51,604評論 1 305
  • 那天,我揣著相機(jī)與錄音咒锻,去河邊找鬼冷冗。 笑死,一個胖子當(dāng)著我的面吹牛惑艇,可吹牛的內(nèi)容都是我干的蒿辙。 我是一名探鬼主播,決...
    沈念sama閱讀 40,347評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼滨巴,長吁一口氣:“原來是場噩夢啊……” “哼思灌!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起恭取,我...
    開封第一講書人閱讀 39,253評論 0 276
  • 序言:老撾萬榮一對情侶失蹤泰偿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蜈垮,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體耗跛,經(jīng)...
    沈念sama閱讀 45,702評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,893評論 3 336
  • 正文 我和宋清朗相戀三年攒发,在試婚紗的時候發(fā)現(xiàn)自己被綠了调塌。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,015評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡惠猿,死狀恐怖羔砾,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情偶妖,我是刑警寧澤姜凄,帶...
    沈念sama閱讀 35,734評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站趾访,受9級特大地震影響态秧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜扼鞋,卻給世界環(huán)境...
    茶點故事閱讀 41,352評論 3 330
  • 文/蒙蒙 一申鱼、第九天 我趴在偏房一處隱蔽的房頂上張望空扎。 院中可真熱鬧,春花似錦润讥、人聲如沸转锈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,934評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽撮慨。三九已至,卻和暖如春脆粥,著一層夾襖步出監(jiān)牢的瞬間砌溺,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,052評論 1 270
  • 我被黑心中介騙來泰國打工变隔, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留规伐,地道東北人。 一個月前我還...
    沈念sama閱讀 48,216評論 3 371
  • 正文 我出身青樓匣缘,卻偏偏與公主長得像猖闪,于是被迫代替她去往敵國和親闷叉。 傳聞我的和親對象是個殘疾皇子雌澄,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,969評論 2 355

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,154評論 25 707
  • 前言:調(diào)試技巧,在任何一項技術(shù)研發(fā)中都可謂是必不可少的技能篇裁。掌握各種調(diào)試技巧柑爸,必定能在工作中起到事半功倍的效果吵护。譬...
    騷的掉渣閱讀 336評論 1 4
  • 前言 相信無論是對于身居一線的coder,還是退居多年的老司機(jī)managers來說,對于調(diào)試程序是不陌生的,對于w...
    itclanCoder閱讀 2,576評論 0 7
  • 前言:調(diào)試技巧,在任何一項技術(shù)研發(fā)中都可謂是必不可少的技能表鳍。掌握各種調(diào)試技巧馅而,必定能在工作中起到事半功倍的效果。譬...
    藍(lán)鷗科技閱讀 562評論 1 4
  • 1 外面的雨下的很大很久沒這么下過了 現(xiàn)在的我們各自奔向自己要走的人生方向 認(rèn)識著自己的圈子追求著自己的生活 再...
    弎拾閱讀 194評論 0 0