这章来讲讲 Android 与 JS 的互相调用。
目录:
- Android 调用 JS
- JS 调用 Android
- WebView 漏洞与内存泄漏
- Dapp Brower
1. Android 调用 JS
- 1.1 方式
Android 调用 JS 有两种方式:
- 通过 WebView 的 loadUrl()。
- 通过 WebView 的 evaluateJavascript()。
一般是根据版本,结合这两种方式来使用。
- 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 项目方做太多适配。