PopUnder 研究:Javascript逆向与反逆向

缘起

最近在研究 PopUnder 的实现方案,通过 Google 搜索 js popunder 出来的第一页中有个网站 popunderjs.com,当时看了下,这是个提供 popunder 解决方案的一家公司,而且再翻了几页,发现市面上能解决这个问题的,只有2家公司,可见这个市场基本是属于垄断型的。
popunderjs 原来在 github 上是有开源代码的,但后来估计作者发现这个需求巨大的商业价值,索性不开源了,直接收费。所以现在要研究它的实现方案,只能上官网扒它源码了。

这是它的示例页: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

文件结构

script.js 是功能主体,实现了 popunder 的所有功能以及定义了多个 API 方法
license.demo.js 是授权文件,有这个文件你才能顺利调用 script.js 里的方法

防止被逆向

这么具有商业价值的代码,就这么公开地给你们用,肯定要考虑好被逆向的问题。我们来看看它是怎么反逆向的。
首先,打开控制台,发现2个问题:

  1. 控制台所有内容都被反复清空,只输出了这么一句话:Console was cleared script.js?0.5309098417125133:1
  2. 无法断点调试,因为一旦启用断点调试功能,就会被定向到一个匿名函数 (function() {debugger})

也就是说,常用的断点调试方法已经无法使用了,我们只能看看源代码,看能不能理解它的逻辑了。但是,它源代码是这样的:

    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() {}
                ;
            }
        }
    }

可见源代码是根本不可能阅读的,所以还是得想办法破掉它的反逆向措施。

利用工具巧妙破解反逆向

首先在断点调试模式一步步查看它都执行了哪些操作,突然就发现了这么一段代码:

(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 被调用的时候,不让它执行就可以破解掉这个死循环了。

所以我们只需要简单地覆盖掉 setTimeout 就可以了……比如:

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 函数的定义中是否包含某些字符。

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

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

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

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

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

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

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

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

  // 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,544评论 25 707
  • 前言:调试技巧,在任何一项技术研发中都可谓是必不可少的技能。掌握各种调试技巧,必定能在工作中起到事半功倍的效果。譬...
    骚的掉渣阅读 324评论 1 4
  • 前言 相信无论是对于身居一线的coder,还是退居多年的老司机managers来说,对于调试程序是不陌生的,对于w...
    itclanCoder阅读 2,497评论 0 7
  • 前言:调试技巧,在任何一项技术研发中都可谓是必不可少的技能。掌握各种调试技巧,必定能在工作中起到事半功倍的效果。譬...
    蓝鸥科技阅读 531评论 1 4
  • 1 外面的雨下的很大很久没这么下过了 现在的我们各自奔向自己要走的人生方向 认识着自己的圈子追求着自己的生活 再...
    弎拾阅读 182评论 0 0