Android应用篇 - WebView 与 JS 全解与实战

这章来讲讲 Android 与 JS 的互相调用。

目录:

  1. Android 调用 JS
  2. JS 调用 Android
  3. WebView 漏洞与内存泄漏
  4. Dapp Brower

1. Android 调用 JS

  • 1.1 方式

Android 调用 JS 有两种方式:

  • 通过 WebView 的 loadUrl()。
  • 通过 WebView 的 evaluateJavascript()。

一般是根据版本,结合这两种方式来使用。

扫描二维码关注公众号,回复: 9637127 查看本文章
  • 1.2 demo

activity_android_call_js.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  tools:context=".MainActivity"
  android:layout_height="match_parent"
  android:layout_width="match_parent">

  <WebView
    android:id="@+id/webview"
    android:layout_height="match_parent"
    android:layout_width="match_parent" />

  <Button
    android:id="@+id/button"
    android:layout_centerInParent="true"
    android:layout_height="wrap_content"
    android:layout_width="wrap_content"
    android:text="android call js" />
</RelativeLayout>

js.html:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>android call js</title>
  <script>
       function callJS(){
          alert("android call js");
       }
    </script>
</head>
</html>

 AndroidCallJSActivity.java:

public class AndroidCallJSActivity extends Activity  {

    private static final String TAG = "AndroidCallJSActivity";

    WebView webView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_android_call_js);

        webView = (WebView) findViewById(R.id.webview);
        WebSettings webSettings = webView.getSettings();
        // 设置与 js 交互的权限
        webSettings.setJavaScriptEnabled(true);
        // 设置允许 js 弹窗
        webSettings.setJavaScriptCanOpenWindowsAutomatically(true);

        webView.loadUrl("file:///android_asset/js.html");

        final int version = Build.VERSION.SDK_INT;
        findViewById(R.id.button).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (version < 19) {
                    // 调用 js 的 callJS() 方法
                    webView.loadUrl("javascript:callJS()");
                } else {
                    // 该方法比 loadUrl() 效率更高,因为该方法的执行不会使页面刷新,而第一种方法 loadUrl() 执行则会。
                    // 该方法在Android 4.4 后才可使用
                    webView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
                        @Override
                        public void onReceiveValue(String value) {
                            Log.i(TAG, "onReceiveValue = " + value);
                        }
                    });
                }
            }
        });

        // 由于设置了弹窗检验调用结果,所以需要支持 js 对话框
        // webview 只是载体,内容的渲染需要使用 webviewChromClient 类去实现
        webView.setWebChromeClient(new WebChromeClient() {
            @Override
            public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
                AlertDialog.Builder b = new AlertDialog.Builder(AndroidCallJSActivity.this);
                b.setTitle("Alert");
                b.setMessage(message);
                b.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        result.confirm();
                    }
                });
                b.setCancelable(false);
                b.create().show();
                return true;
            }
        });
    }

    @Override
    protected void onDestroy() {
        if (webView != null) {
            webView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
            webView.clearHistory();
            ((ViewGroup) webView.getParent()).removeView(webView);
            webView.destroy();
            webView = null;
        }
        super.onDestroy();
    }
}

执行结果:

2. JS 调用 Android

  • 2.1 方式

JS 调用 Android 有三种方式:

  • 通过 WebView 的 addJavascriptInterface() 进行对象映射。
  • 通过 WebViewClient 的 shouldOverrideUrlLoading() 方法回调拦截 url。
  • 通过 WebChromeClient 的 onJsAlert()、onJsConfirm()、onJsPrompt() 方法回调拦截JS对话框 alert()、confirm()、prompt() 消息。

  • 2.2 demo

xml 都是一样的:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  tools:context=".MainActivity"
  android:layout_height="match_parent"
  android:layout_width="match_parent">

  <WebView
    android:id="@+id/webview"
    android:layout_height="match_parent"
    android:layout_width="match_parent" />
</RelativeLayout>

(1) 通过 WebView 的 addJavascriptInterface() 进行对象映射

定义一个与 JS 对象映射关系的 Android 类 AndroidtoJs.java:

public class AndroidtoJs {

    private static final String TAG = "AndroidtoJs";

    // 定义 JS 需要调用的方法
    // 被 JS 调用的方法必须加入 @JavascriptInterface 注解
    @JavascriptInterface
    public void helloAndroid(String msg) {
        Log.i(TAG, "js 调用 Android 的 helloAndroid 方法 say --> " + msg);
    }
}

js1.html:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>js call android</title>
  <script>
        function callAndroid(){
            // 由于对象映射,所以调用 testJs 对象等于调用 Android 映射的对象
            testJs.helloAndroid("你好");
        }
  </script>
</head>
<body>
<button type="button" id="button1" content="call android" οnclick="callAndroid()"></button>
</body>
</html>

JSCallAndroidActivity1.java:

public class JSCallAndroidActivity1 extends Activity  {

    WebView webView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_js_call_android);

        webView = (WebView) findViewById(R.id.webview);
        WebSettings webSettings = webView.getSettings();
        // 设置与 js 交互的权限
        webSettings.setJavaScriptEnabled(true);
        // AndroidtoJS 类对象映射到 js 的 testJs 对象
        webView.addJavascriptInterface(new AndroidtoJs(), "testJs");
        webView.loadUrl("file:///android_asset/js1.html");
    }

    @Override
    protected void onDestroy() {
        if (webView != null) {
            webView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
            webView.clearHistory();
            ((ViewGroup) webView.getParent()).removeView(webView);
            webView.destroy();
            webView = null;
        }
        super.onDestroy();
    }
}

执行打印:

I/AndroidtoJs: js 调用 Android 的 helloAndroid 方法 say --> 你好

(2) 通过 WebViewClient 的 shouldOverrideUrlLoading() 方法回调拦截 url

js2.html:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>js call android</title>
  <script>
        function callAndroid(){
            // 约定的 url 协议为:js://webview?arg1=111&arg2=222
            document.location = "js://webview?arg1=111&arg2=222";
        }
  </script>
</head>
<body>
<button type="button" id="button1" content="call android" οnclick="callAndroid()"></button>
</body>
</html>

必须事前约定一个协议。

JSCallAndroidActivity2.java:

public class JSCallAndroidActivity2 extends Activity  {

    private static final String TAG = "JSCallAndroidActivity2";

    WebView webView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_js_call_android);

        webView = (WebView) findViewById(R.id.webview);
        WebSettings webSettings = webView.getSettings();
        // 设置与 js 交互的权限
        webSettings.setJavaScriptEnabled(true);
        webView.loadUrl("file:///android_asset/js2.html");
        webView.setWebViewClient(new WebViewClient() {

            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                Uri uri = Uri.parse(url);
                if ( uri.getScheme().equals("js")) {
                    if (uri.getAuthority().equals("webview")) {
                        Set<String> collection = uri.getQueryParameterNames();
                        Log.i(TAG, "js call android ->" + Arrays.toString(collection.toArray()));
                    }
                }
                return super.shouldOverrideUrlLoading(view, url);
            }
        });
    }

    @Override
    protected void onDestroy() {
        if (webView != null) {
            webView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
            webView.clearHistory();
            ((ViewGroup) webView.getParent()).removeView(webView);
            webView.destroy();
            webView = null;
        }
        super.onDestroy();
    }
}

执行输出:

I/JSCallAndroidActivity2: js call android ->[arg1, arg2]

(3) 通过 WebChromeClient 拦截 JS 对话框

js3.html:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>js call android</title>

  <script>
            function clickprompt(){
            // 调用prompt()
            var result=prompt("js://webview?arg1=111&arg2=222");
            alert("Hi " + result);
            }
  </script>
</head>

<!-- 点击按钮则调用clickprompt()  -->
<body>
<button type="button" id="button1" οnclick="clickprompt()">点击调用Android代码</button>
</body>
</html>

JSCallAndroidActivity3.java:

public class JSCallAndroidActivity3 extends Activity {

    private static final String TAG = "JSCallAndroidActivity3";

    WebView webView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_js_call_android);

        webView = (WebView) findViewById(R.id.webview);
        WebSettings webSettings = webView.getSettings();
        // 设置与 js 交互的权限
        webSettings.setJavaScriptEnabled(true);
        // 设置允许 js 弹窗
        webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
        webView.loadUrl("file:///android_asset/js3.html");
        webView.setWebChromeClient(new WebChromeClient() {
            // 拦截输入框
            // 参数message:代表promt()的内容(不是url)
            @Override
            public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
                Uri uri = Uri.parse(message);
                // 如果url的协议 = 预先约定的 js 协议
                if (uri.getScheme().equals("js")) {
                    // 如果 authority  = 预先约定协议里的 webview,即代表都符合约定的协议
                    // 所以拦截url,下面JS开始调用Android需要的方法
                    if (uri.getAuthority().equals("webview")) {
                        // 可以在协议上带有参数并传递到Android上
                        Set<String> collection = uri.getQueryParameterNames();
                        Log.i(TAG, "js 调用了 Android 的方法,参数为 = " + Arrays.toString(collection.toArray()));
                        // 参数result:代表消息框的返回值(输入值)
                        result.confirm("js调用了Android的方法成功!");
                    }
                    return true;
                }
                return super.onJsPrompt(view, url, message, defaultValue, result);
            }

            // 拦截JS的警告框
            @Override
            public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
                return super.onJsAlert(view, url, message, result);
            }

            // 拦截JS的确认框
            @Override
            public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
                return super.onJsConfirm(view, url, message, result);
            }
        });
    }

    @Override
    protected void onDestroy() {
        if (webView != null) {
            webView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
            webView.clearHistory();
            ((ViewGroup) webView.getParent()).removeView(webView);
            webView.destroy();
            webView = null;
        }
        super.onDestroy();
    }
}

执行输出:

I/JSCallAndroidActivity3: js 调用了 Android 的方法,参数为 = [arg1, arg2]

这种方式相对于第二种方式的优势是可以提供给 JS 返回值。

3. WebView 漏洞与内存泄漏

WebView 的漏洞主要分为三种:

  • 任意代码执行漏洞。
  • 密码明文存储漏洞。
  • 域控制不严格漏洞。
  • 3.1 任意代码执行漏洞

(1) addJavascriptInterface 接口引起远程代码执行漏洞

漏洞产生原因:

 JS 调用 Android 的其中一个方式是通过 addJavascriptInterface 接口进行对象映射:

// 参数1:Android 的本地对象
// 参数2:JS 的对象
webView.addJavascriptInterface(new JSObject(), "obj");

通过对象映射将 Android 中的本地对象和 JS 中的对象进行关联,从而实现 JS 调用 Android 的对象和方法。所以,漏洞产生原因是:当 JS 拿到 Android 这个对象后,就可以调用这个 Android 对象中所有的方法,包括系统类 (Java.lang.Runtime 类),从而进行任意代码执行 (比如可以执行命令获取本地设备的 SD 卡中的文件等信息从而造成信息泄露)。

具体获取系统类的描述 (结合 Java 反射机制):先拿到这个 Android 对象的 Class,然后调用 Class.forName() 加载 Runtime 类,而 Runtime 类是可以执行本地命令的。

比如:

function execute(cmdArgs)  {  
    // 步骤1:遍历 window 对象
    // 目的是为了找到包含 getClass() 的对象
    // 因为 Android 映射的 JS 对象也在 window 中,所以肯定会遍历到
    for (var obj in window) {  
        if ("getClass" in window[obj]) {  
            // 步骤2:利用反射调用 forName() 得到 Runtime 类对象
            alert(obj);          
            return  window[obj].getClass().forName("java.lang.Runtime")  
            // 步骤3:以后,就可以调用静态方法来执行一些命令,比如访问文件的命令
            getMethod("getRuntime", null).invoke(null,null).exec(cmdArgs);  
            // 从执行命令后返回的输入流中得到字符串,有很严重暴露隐私的危险。
            // 如执行完访问文件的命令之后,就可以得到文件名的信息了。
        }  
    }  
} 

当一些 APP 通过扫描二维码打开一个外部网页时,攻击者就可以执行这段 JS 代码进行漏洞攻击。在微信盛行、扫一扫行为普及的情况下,该漏洞的危险性非常大。那么如何规避这个漏洞呢?

Android 4.2 版本之后:Google 在 Android 4.2 版本中规定对被调用的函数以 @JavascriptInterface 进行注解从而避免漏洞攻击。而在 Android 4.2 版本之前:采用拦截 prompt() 进行漏洞修复,这种方式如下:

每次当 WebView 加载页面前加载一段本地的 JS 代码,原理是:

  • (1) 让 JS 调用一个 Javascript 方法:该方法是通过调用 prompt() 把 JS 中的信息 (含特定标识,方法名称等) 传递到 Android 端;
  • (2) 在 Android 的 onJsPrompt() 中 ,解析传递过来的信息,再通过反射机制调用 Java 对象的方法,这样实现安全的 JS 调用 Android 代码。关于 Android 返回给 JS 的值:可通过 prompt() 把 Java 中方法的处理结果返回到 JS 中。

具体需要加载的 JS 代码如下:

javascript:(function JsAddJavascriptInterface_(){  
    // window.jsInterface 表示在 window 上声明了一个 Js 对象
    // jsInterface = 注册的对象名
    // 它注册了两个方法,onButtonClick(arg0) 和 onImageClick(arg0, arg1, arg2)
    // 如果有返回值,就添加上 return
    if (typeof(window.jsInterface)!='undefined') {      
        console.log('window.jsInterface_js_interface_name is exist!!');}   
    else {  
        window.jsInterface = {     
            // 声明方法形式:方法名: function(参数)
            onButtonClick:function(arg0) {   
            // prompt()返回约定的字符串
            // 该字符串可自己定义
            // 包含特定的标识符MyApp和 JSON 字符串(方法名,参数,对象名等)    
                return prompt('MyApp:'+JSON.stringify({obj:'jsInterface',func:'onButtonClick',args:[arg0]}));  
            },  
            onImageClick:function(arg0,arg1,arg2) {   
                return prompt('MyApp:'+JSON.stringify({obj:'jsInterface',func:'onImageClick',
args:[arg0,arg1,arg2]}));  
            },  
        };  
    }  
}  
)()
// 当JS调用 onButtonClick() 或 onImageClick() 时,就会回调到Android中的 onJsPrompt ()
// 我们解析出方法名,参数,对象名
// 再通过反射机制调用 Java 对象的方法

关于采用拦截 prompt() 进行漏洞修复需要注意的两点细节:

(1) 加载上述 JS 代码的时机

由于当 WebView 跳转到下一个页面时,之前加载的 JS 可能已经失效,所以,通常需要在以下方法中加载 JS:
    onLoadResource();
    doUpdateVisitedHistory();
    onPageStarted();
    onPageFinished();
    onReceivedTitle();
    onProgressChanged();

(2) 需要过滤掉 Object 类的方法

由于最终是通过反射得到 Android 指定对象的方法,所以同时也会得到基类的其他方法 (最顶层的基类是 Object 类),为了不把 getClass() 等方法注入到 JS 中,我们需要把 Object 的公有方法过滤掉,需要过滤的方法列表如下:
    getClass()
    hashCode()
    notify()
    notifyAl()
    equals()
    toString()
    wait()


(2) searchBoxJavaBridge_接口引起远程代码执行漏洞

漏洞产生原因:

在 Android 3.0以下,Android 系统会默认通过 searchBoxJavaBridge_ 的 Js 接口给 WebView 添加一个JS 映射对象:searchBoxJavaBridge_对象。而该接口可能被利用,实现远程任意代码。

如何解决呢?

删除 searchBoxJavaBridge_ 接口:removeJavascriptInterface()。

(3) accessibility 和 accessibilityTraversal 接口引起远程代码执行漏洞漏洞产生原因:

漏洞产生原因和解决方案同 (2)。

  • 3.2 密码明文存储漏洞

漏洞产生原因:

// WebView 默认开启密码保存功能 :
webView.setSavePassword(true);
开启后,在用户输入密码时,会弹出提示框:询问用户是否保存密码。如果选择 "是",密码会被明文保到 /data/data/com.package.name/databases/webview.db 中,这样就有被盗取密码的危险。

如何解决?

// 关闭密码保存提醒
WebSettings.setSavePassword(false)。

  • 3.3 域控制不严格漏洞

漏洞产生原因:

A 应用可以通过 B 应用导出的 Activity 让 B 应用加载一个恶意的 file 协议的 url 来获取 B 应用的内部私有文件,从而带来数据泄露威胁。

当 B 应用的 activity 是可被导出的,同时设置允许 WebView 使用 File 协议,则 A 应用可以在外部调起 B 的 activity,同时向 B 传递一个请求内部数据的文件,则可以获取 B 的数据。

如何解决?

  • 1. 设置 activity 不可被导出。
  • 2. 禁止 WebView 使用 File 协议,而且是明确禁止。
  • 3.4 内存泄漏

除了老生常谈的 onDestory() 的释放外,我觉得这篇文章的思路不错,新开一个进程加载 WebView,销毁时直接干掉这个进程即可:webview内存泄漏终极解决方案

4. Dapp Brower

通过注入 js 拦截 web3j 请求:注入钱包地址,拦截交易交付给客户端做签名转账,无需 dapp 项目方做太多适配。

https://github.com/kuangzhongwen/Web3jDappBrowser

发布了126 篇原创文章 · 获赞 215 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/u014294681/article/details/88666966
今日推荐