微信公共平台开放了几个操作webview界面的js接口
示例代码是这样的:

WeixinJSBridge.invoke('getNetworkType',{},
    function(e){
        WeixinJSBridge.log(e.err_msg);
    });

android的webview api中有开放过一个addJavaScriptInterface函数,这个函数的作用是在页面的Window中注入一个JS对象
如果你的应用中使用了这个api,建议先看一下国内安全领域第一人黑哥的这篇文章android webview 漏洞背后的节操,

没耐心的同学直接看结尾的解决方案吧:

第1个方案是设置信任域,这个问题其实是不太靠谱的,在我之前在kcon里演讲《去年跨过的浏览器》里有很多信任域带来的安全问题
第2个方案是使用 shouldOverrideUrlLoading 的方式,据说这个方案还是比较靠谱的,只是可能代价比较大
第3个方案就是教育那些开发商,没有必要用webview的时候就不要用,不要java与js交互就不要用

不过,按黑哥这篇文章的想法,这个漏洞危险等级很低,可以无视之
暂时把安全问题放一边,Js对象的注入,对函数的参数类型有严格要求,它只能传递基本数据类型以及JSON
但微信的JsApi中,参数三是一个函数对象,那他是如果做到的呢

逆向

Android原生的机制既然不能支持函数对象的传递,于是猜测微信是否会对原始的api做了一层包装;
先下载weixin.apk,反编译,全局搜索"WeixinJSBrige",在assets目录找到一个wxjs.js;不知什么原因,微信团队没有对这个js文件进行代码混淆;

wxjs.js分析

wxjs.js有两千多行的代码,不过不必担心,其中有一大部分是jquery的实现
直接找我们想要的,先看'WeixinJSBridge'

   var __WeixinJSBridge = {
               // public
        invoke:_call,
        call:_call,
        on:_on,
        env:_env,
        log:_log,
       _fetchQueue: _fetchQueue,
        _handleMessageFromWeixin: _handleMessageFromWeixin,
   };

看到方法名可以猜测微信JSBridge的大概的逻辑了;应该是消息队列处理机制,

具体还是看一下微信是怎么实现的

先看_call方法的逻辑:

  function _call(func,params,callback) {
        if (!func || typeof func !== 'string') {
            return;
        };
        if (typeof params !== 'object') {
            params = {};
        };

        var callbackID = (_callback_count++).toString();

        if (typeof callback === 'function') {
          _callback_map[callbackID] = callback;
        };

        var msgObj = {'func':func,'params':params};
        msgObj[_MESSAGE_TYPE] = 'call';        
        msgObj[_CALLBACK_ID] = callbackID;

        _sendMessage(JSON.stringify(msgObj));
    }

callbackId是一个自增的ID,_call调用时候将id和回调函数通过_sendMessage存在队列中,

再看一下_sendMessage的逻辑:

       function _sendMessage(message) {
        _sendMessageQueue.push(message);
        _readyMessageIframe.src = _CUSTOM_PROTOCOL_SCHEME + '://' + _QUEUE_HAS_MESSAGE;
      };

事件队列和之前猜测的一样; 但是,为什么会构造一个url? 难道微信没有用addJavaScriptInterface

既然有sendMessage,其他地方必然有一个取消息的逻辑:

      //取出队列中的消息,并清空接收队列
    function _fetchQueue() {
        var messageQueueString = JSON.stringify(_sendMessageQueue);
        _sendMessageQueue = [];
        //window.JsApi && JsApi.keep_setReturnValue && window.JsApi.keep_setReturnValue('SCENE_FETCHQUEUE', messageQueueString);
        _setResultValue('SCENE_FETCHQUEUE', messageQueueString);
        return messageQueueString;
    };

JavaScript调用java的逻辑最终在_setResultValue中

    function _setResultValue(scene, result) {
        if (result === undefined) {
            result = 'dummy';
        }
        _setResultIframe.src = 'weixin://private/setresult/' + scene + '&' + base64encode(UTF8.encode(result));
        //_setResultIframe.src = 'weixin://private/setresult/' + scene + '&' + window.btoa(result);
    }

又出现一个_setResultIfrmae,寻迹查找,最后找到了这个东西

    //创建ifram的准备队列
    function _createQueueReadyIframe(doc) {
        // _setResultIframe 的初始化
        _setResultIframe = doc.createElement('iframe');
        _setResultIframe.id = '__WeixinJSBridgeIframe_SetResult';
        _setResultIframe.style.display = 'none';
        doc.documentElement.appendChild(_setResultIframe);

        _readyMessageIframe = doc.createElement('iframe');
        _readyMessageIframe.id = '__WeixinJSBridgeIframe';
        _readyMessageIframe.style.display = 'none';
        doc.documentElement.appendChild(_readyMessageIframe);
        return _readyMessageIframe;
    }

看到这儿明白了,启鹅的工程师为了规避js注入的安全问题,没有采用JS注入的方式,而是采取的黑哥的方案2:

shouldOverrideUrl

在需要js调用native api的时候,js在页面中创建一个不可见的iframe,设置这个iframe的地址;

在android代码拦截这个url来实现java和js参数;

微信的这个js框架为所有的js函数做了一个统一的入口,

所以编码代码不大,不失为一个好方法

最后瞄一下java调用的js的入口,那就是标准的调用方式了;

  function _handleMessageFromWeixin(message) {
      var msgWrap = message;

      switch(msgWrap[_MESSAGE_TYPE]){
        case 'callback':
        {       
          if(typeof msgWrap[_CALLBACK_ID] === 'string' && typeof _callback_map[msgWrap[_CALLBACK_ID]] === 'function'){
            var ret = _callback_map[msgWrap[_CALLBACK_ID]](msgWrap['__params']); //根据id进行函数 回调
            delete _callback_map[msgWrap[_CALLBACK_ID]]; // 保证调用一次,删除函数
            //window.JsApi && JsApi.keep_setReturnValue && window.JsApi.keep_setReturnValue('SCENE_HANDLEMSGFROMWX', JSON.stringify(ret));
            _setResultValue('SCENE_HANDLEMSGFROMWX', JSON.stringify(ret));
            return JSON.stringify(ret);
          }
        //window.JsApi && JsApi.keep_setReturnValue && window.JsApi.keep_setReturnValue('SCENE_HANDLEMSGFROMWX', JSON.stringify({'__err_code':'cb404'}));
        _setResultValue('SCENE_HANDLEMSGFROMWX', JSON.stringify({'__err_code':'cb404'}));
          return JSON.stringify({'__err_code':'cb404'});          
        }
        break;
        .....

android调用:webview.loadurl("javascript:WeixinJsBridge._handleMessageFromWeixin")

Comments