赞
踩
前面写过 移动安全-Android安全测试框架Drozer 一文,记录了如何使用 Drozer 对 Android 四大组件进行安全测试,本文补充下其他一些零散的测试知识。
adb shell 提供了很多实用的命令可供测试者使用,如 am、pm 等命令。
am 指令是 activity manager 的缩写,可以启动 Activity、Service、Broadcast,杀进程,监控等功能,这些功能都非常便捷调试程序。
目的 | 指令 |
---|---|
指定 action 启动拨号 Activity | am start -a android.intent.action.CALL -d tel:10086 |
指定 action 打开网页 Activity | am start -a android.intent.action.VIEW -d https://www.baidu.com |
指定组件名启动应用 Activity | am start -n com.android.example/.MainActivity |
启动一个服务 | am startservice -n/-a XXX |
发送一个广播 | am broadcast -a <广播动作> |
监控 Crash 与 ANR | am monitor |
强杀进程/强制停止某应用 | am force-stop com.android.example |
更多命令和功能请执行 adb shell am -h
查看完整的指令帮助信息。
【补充】参数释义
1、Intent 参数
参数名 | 解释 |
---|---|
-a <ACTION> | 指定 Intent action, 实现原理 Intent.setAction() |
-n <COMPONENT> | 指定组件名( 包名/.类名 或 包名/类的全名 ),实现原理 Intent.setComponent() |
-d <DATA_URI> | 传入数据,指定 Intent data URI |
-p <PACKAGE> | 指定包名,实现原理 Intent.setPackage() |
-f <FLAGS> | 添加 flags,实现原理 Intent.setFlags(int ),紧接着的参数必须是 int 型 |
2、Extra 参数
来看一个示例:
am start -n com.android.example/.MainActivity -es website www.baidu.com
此处-es website www.baidu.com
,等价于 Intent.putExtra(“website”, “www.baidu.com”)
。其他类型的参数如下:
参数 | 类型 |
---|---|
-e/-es | String |
-esn | (String)null |
-ez | boolean |
-ei | int |
-el | long |
-ef | float |
-eu | uri |
-ecn | component |
-esa | String[] 数组 |
-eia | int[] 数组 |
补充一句,移动安全-Activity劫持检测与防护 提到的关于 Activity 劫持的快速验证方法,可以使用如下 am 指令:
adb shell am start -n com.test.uihijack com.test.uihijack.MainActivity
pm 工具为包管理 (Package Manager) 的简称,可以使用 pm 工具来执行应应用包的信息、系统权限查询,控制应用等。pm 工具是 Android 开发与测试过程中必不可少的工具,通常放置在 system/bin/pm
下。
常用指令比如查看指定应用版本信息:
pm dump com.tencent.mm|findstr versionName
More:
目的 | 指令 |
---|---|
列出所有应用 | pm list package |
列出第三方应用 | pm list package -3 |
列出所有系统应用 | pm list package -s |
列出所有已知权限 | pm list permissions |
查找应用安装包路径 | pm path com.tencent.mm |
清除应用的数据与缓存 | pm clear com.tencent.mm |
授予应用某个权限 | pm grant com.tencent.mm android.permission.CAMERA |
撤销应用某个权限 | pm revoke com.tencent.mm android.permission.CAMERA |
卸载应用 | pm uninstall com.tencent.mm |
隐藏应用桌面图标 | pm hide com.tencent.mm |
恢复隐藏的应用 | pm unhide com.tencent.mm |
以上列出了日常测试中常用的 pm 指令,更多指令信息可查看 adb shell pm
帮助信息。
其他常用 adb shell 指令,如 :
//查看当前在运行的 Activity
adb shell dumpsys activity top | grep ACTIVITY
//抓取指定应用的错误日志
adb shell logcat *:E --pid `pidof com.XX.XX`> /data/local/tmp/1.log
目的 | 指令 |
---|---|
抓取错误日志 | logcat -c && logcat *:E > /data/local/tmp/1.log |
手机截屏 | adb shell screencap -p /data/local/tmp/1.png |
手机录屏 | adb shell screenrecord /data/local/tmp/1.mp4 |
查看应用的详细权限、版本等信息 | adb shell dumpsys package com.tencent.mm |
content 指令查询数据库 | adb shell content query --uri content://contacts/people |
【补充】Android 的 Uri 由以下三部分组成: “content://”、数据的路径、标示ID(可选)。举些例子,如:
所有联系人的Uri: content://contacts/people
某个联系人的Uri: content://contacts/people/5
所有图片Uri: content://media/external
某个图片的Uri: content://media/external/images/media/4
来看一下 Android 是如何管理多媒体文件(音频、视频、图片)的信息。通过 DDMS,可以在/data/data/com.android.providers.media
下找到数据库文件:
打开 external.db
文件进一步查看:在 media 表格下,可以看到文件路径(_data) 和 Uri 的标示 ID(_id) 的对应关系。
安全测试经常需要编写 POC 程序调用其他应用程序 exported="true"
且无权限保护的可导出组件,以下是借助 Intent 调用其他应用程序的组件的方法:
//启动其他应用程序的Activity Intent intent = new Intent(); intent.setComponent(new ComponentName("com.Tr0e.example", "com.Tr0e.example.MyExampleActivity")); startActivity(intent); //启动其他应用程序的Service Intent intent = new Intent(); intent.setComponent(new ComponentName("com.Tr0e.example", "com.Tr0e.example.MyExampleService")); startService(intent); //发送广播给其他应用程序的Receiver Intent intent = new Intent(); //Android 8以后想给静态注册的广播接收器(应用无需启动即可接收广播)发送广播,必须指定接收器的包名类名 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){ intent.setComponent(new ComponentName("com.Tr0e.example", "com.Tr0e.example.MyExampleReciver")); } intent.setAction("android.intent.action.Mybroadcast"); sendBroadcast(intent);
此处补充一下,Android 8.0 开始之所以要限制静态注册的广播接收器所能接收的广播,是因为静态注册的广播接收器具有无需启动应用即可接收广播的特点,那么试想一下以下场景:一个恶意 APP 完成可以通过静态注册一个监听系统开机广播的广播接收器,来达到手机开机后立马执行恶意脚本的目的(典型的触发开机自启动的场景)。关于广播接收器和广播的更多基础知识可以参见:Android-广播。
以上提到可以通过 intent.setComponent(pkg,cls)
接口来限制广播接收方,其实 Intent 还提供如下接口同样也可以达到限制广播接收方的目的:
//https://developer.android.com/reference/android/content/Intent#setPackage(java.lang.String)
public Intent setPackage (String packageName)
通过 intent.setPackage(com.Tr0e.example);
即可限定只能由指定的应用来接收、解析该 Intent 实例。
故给非系统应用静态注册的广播接收器发送广播的方式也可以如下:
Intent intent = new Intent();
intent.setPackage("com.Tr0e.example");
intent.setAction("android.intent.action.Mybroadcast");
sendBroadcast(intent);
众所皆知,Intent 可分为隐式(implicitly) 和显式 (explicitly) 两种:
1)显式Intent
即在构造 Intent 对象时就指定接收者,它一般用在知道目标组件名称的前提下,一般是在相同的应用程序内部实现的,如下:
Intent intent = new Intent(MainActivit.this, NewActivity.class);
startActivity(intent);
2)隐式Intent
即 Intent 的发送者在构造 Intent 对象时,并不知道也不关心接收者是谁,有利于降低发送者和接收者之间的耦合,它一般用在没有明确指出目标组件名称的前提下,一般是用于不同应用程序之间,如下:
Intent intent = new Intent();
intent.setAction("com.wooyun.test");
startActivity(intent);
对于显式 Intent,Android 不需要去做解析,因为目标组件已经很明确,Android 需要解析的是那些隐式 Intent,通过解析将 Intent 映射给可以处理此 Intent 的 Activity、Receiver 或 Service。
攻击场景
如果使用隐式 Intent 启动 Activity 组件,系统中凡是满足 action="XXX"
的组件否有可能被启动,系统会弹出选项框,由用户决定启动哪个 Activity。此时如果 Intent 中如果携带敏感数据且用户选错组件的话,攻击者便能劫持该 Intent 并获取敏感数据。整体的攻击场景如下图所示:
Demo 案例
1)在受害者 A 应用(Test)中发送 Intent
Intent intent = new Intent();
intent.setAction("com.bwshen.intent.action_test");
intent.putExtra("password","admin123");
startActivity(intent);
同时在 A 应用(Test)中定义目标组件:
<activity android:name=".LoginActivity">
<intent-filter>
<action android:name="com.bwshen.intent.action_test" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
2)在攻击者 B 应用 (Attack)中同样定义相同 Action 的组件:
<activity android:name=".LoginActivity">
<intent-filter>
<action android:name="com.bwshen.intent.action_test" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
此时 A 应用发生 Intent 时将弹出如下选项框供用户选择:
以上如果用户选择错应用,那么 password 信息将被攻击者获取。综上所述,应尽量避免使用隐式启动的方式来调用组件,特别是传递了敏感数据的 Intent。开发者可以使用 setPackage(pkgName)
或 setComponent(pkg,cls)
来明确指定接收方,从而防止 Intent 劫持和敏感数据泄露。
Intent 可以通过 putExtra(String name, XXX value)
来给目标组件传递各种类型的数据,如下图所示:
攻击场景
如果目标组件在使用 getXXXExtra() 接口接收数据时没有对异常、畸形数据进行异常捕获,那么攻击者向应用发送空数据、异常或者畸形数据可达到致使受害者应用崩溃、形成拒绝服务攻击的效果。此类本地拒绝服务漏洞对于锁屏应用、安全防护类软件危害是巨大的。
漏洞示例
注意到传递的一类特殊数据—— Bundle:
(1)先来看下 MainActivity 正常情况下如何在 Intent 中传递 Bundle 对象:
Intent intent = new Intent(MainActivity.this,LoginActivity.class);
Bundle bundle = new Bundle();
bundle.putString("name","admin");
bundle.putString("password","admin123");
intent.putExtra("data",bundle);
startActivity(intent);
LoginActivity 接收数据:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
Intent intent = getIntent();
Bundle bundle_data = intent.getBundleExtra("data");
String password = (String)bundle_data.get("password");
Log.e(TAG,"password="+password);
}
程序运行效果:
(2)接下来看看如果 MainActivity 中不传递名字为 data 的 Bundle 对象的话,将导致什么结果:
Intent intent = new Intent(MainActivity.this,LoginActivity.class);
Bundle bundle = new Bundle();
bundle.putString("name","admin");
bundle.putString("password","admin123");
intent.putExtra("data111",bundle);
startActivity(intent);
运行程序并出发 Intent 发送事件,发现程序崩溃,日志如下:
2022-05-11 22:30:20.759 3873-3873/com.bwshen.test E/AndroidRuntime: FATAL EXCEPTION: main Process: com.bwshen.test, PID: 3873 java.lang.RuntimeException: Unable to start activity ComponentInfo{com.bwshen.test/com.bwshen.test.LoginActivity}: java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.Object android.os.Bundle.get(java.lang.String)' on a null object reference at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2817) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2892) at android.app.ActivityThread.-wrap11(Unknown Source:0) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1593) at android.os.Handler.dispatchMessage(Handler.java:105) at android.os.Looper.loop(Looper.java:164) at android.app.ActivityThread.main(ActivityThread.java:6541) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767) Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.Object android.os.Bundle.get(java.lang.String)' on a null object reference at com.bwshen.test.LoginActivity.onCreate(LoginActivity.java:18) at android.app.Activity.performCreate(Activity.java:6975) at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1213) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2770) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2892) at android.app.ActivityThread.-wrap11(Unknown Source:0) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1593) at android.os.Handler.dispatchMessage(Handler.java:105) at android.os.Looper.loop(Looper.java:164) at android.app.ActivityThread.main(ActivityThread.java:6541) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
可以看到触发了空指针引用的异常,修复方法很简单,增加异常捕获:
Intent intent = getIntent();
try {
Bundle bundle_data = intent.getBundleExtra("data");
String password = (String)bundle_data.get("password");
Log.e(TAG,"password="+password);
}catch (Exception e){
Log.e(TAG, String.valueOf(e));
}
除了传递 Bundle 对象外,Intent 中传递如下指针类型的数据时,接收方均应该进行异常捕获:
接收方对应的获取数据的接口(重点关注)如:getSerializable、getParcelable、getDoubleArray 等。
在 Android 设备中,可以通过点击 Deeplink 可以打开指定应用的 Activity 组件,该特性可以用于发现存在本地 Activity 存在漏洞(如上述拒绝服务攻击)后发起网络钓鱼链接攻击,将漏洞的攻击向量由本地攻击转为远程攻击(有利于 CVSS 漏洞评分),从而提高漏洞危害。
为了能够正确定位到需要打开的应用,并正确打开指定的 Activity,需要应用开发过程中对 Intent 进行过滤接收进行配置(就是intent-filter)。具体做法是在 AndroidManifest.xml 中对 Activity 声明的时候添加<intent-filter>
的<data>
节点,配置schema
和一些必要的区分属性参数(如:host、path 等)即可,配置的属性参数越多越详细,越能保证唯一性,准确打开需要打开的应用,而不是弹出打开应用选择框。
DeepLink 唤起 APP 示例
继续以上面的 LoginActivity 为例,修改 AndroidMainfest.xml 中其配置如下:
<activity android:name=".LoginActivity"> <intent-filter> <action android:name="com.bwshen.intent.action_test" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> <intent-filter> <!--ACTION_VIEW:支持被检索--> <action android:name="android.intent.action.VIEW" /> <!--CATEGORY_DEFAULT:响应隐式Intent--> <category android:name="android.intent.category.DEFAULT" /> <!--CATEGORY_BROWSABLE:可被Web浏览器唤起--> <category android:name="android.intent.category.BROWSABLE" /> <!--data:一个或多个,必须含有scheme标签,决定被唤起的URL格式--> <data android:scheme="rsdkdemo" android:host="rs.com" android:pathPrefix="/test"/> </intent-filter> </activity>
上述添加的<intent-filter>
标签包含以下属性:
属性 | 作用 |
---|---|
action 动作 | 外部打开必须配置成ACTION_VIEW ,这样外部的打开指令才能到达 |
category 范畴 | (1)必须包含 DEFAULT,这个 category 允许你的 Activity 可以接收隐式 Intent,如果没有配置这个,Activity 只能通过指定应用程序容器名称打开; (2)必须包含 BROWSABLE,这个 category 允许你的 intent-filter 可以在 Web 浏览器中访问,如果没有配置这个,点击Web浏览器中的 Deeplink 链接将无法解析并打开 Activity。 |
data 数据 | 需要添加一个或者多个<data> 标签,每一个<data> 标签都描述了什么样格式的 URI 将会分派到 Activity 进行处理,同时每一个<data> 标签至少且必须包含一个android:schema 属性。 |
Deeplink 的链接类型一般是 schema://host/path?params
样式,上述链接即为 rsdkdemo://rs.com/test
或 rsdkdemo://rs.com/test?referer=Deeplink_Test
。
(1)命令行 adb 测试 deeplink
adb shell am start -a android.intent.action.VIEW -d "rsdkdemo://rs.com/test"
效果如下:
(2)原生代码访问 deeplink
对应的如果是原生 Android 代码启动上述组件的代码则是:
Intent intent = new Intent();
intent.setData(Uri.parse("rsdkdemo://rs.com/test"));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
(3)测试网页点击 deeplink
编写 html 页面 poc.html 如下:
<!DOCTYPE html>
<html>
<head>
<title>MyPoc</title>
</head>
<body>
<input type="button" value="点击我打开Deeplink" onclick="javascrtpt:window.location.href='rsdkdemo://rs.com/test?referer=Deeplink_Test'">
</body>
</html>
搭建 Python Web 服务提供远程服务,手机浏览器访问 poc.html :
点击按钮成功访问目标 APP 组件:
对应的在 h5 页面上唤起上述组件的方式还有:
<!--1.通过a标签打开,点击标签是启动,注意这里的 href 格式-->
<a href="rsdkdemo://rs.com/test">open android app</a>
<!--2.通过iframe打开,设置iframe.src即会启动-->
<iframe src="rsdkdemo://rs.com/test"></iframe>
<!--3.直接通过window.location 进行跳转-->
window.location.href= "rsdkdemo://rs.com/test";
(4)传递数据的 DeepLink
修改 LoginActivity 组件的配置:
<activity android:name=".LoginActivity"> <intent-filter> <action android:name="com.bwshen.intent.action_test" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> <intent-filter> <!--ACTION_VIEW:支持被检索--> <action android:name="android.intent.action.VIEW" /> <!--CATEGORY_DEFAULT:响应隐式Intent--> <category android:name="android.intent.category.DEFAULT" /> <!--CATEGORY_BROWSABLE:可被Web浏览器唤起--> <category android:name="android.intent.category.BROWSABLE" /> <!--data:一个或多个,必须含有scheme标签,决定被唤起的URL格式--> <data android:scheme="rsdkdemo"/> </intent-filter> </activity>
以下 POC 可唤醒组件同时传递数据:
<!DOCTYPE html>
<html>
<head>
<title>MyPoc</title>
</head>
<body>
<h1><a href="intent://externalapp/#Intent;scheme=rsdkdemo;component=com.bwshen.test/.LoginActivity;S.password=admin123;end">open android app</a>
</body>
</html>
访问效果如下:
成功在浏览器启动目标 APP 的 Activity 组件并传递数据:
读者如果细心的话肯定发现我传递的是字符串而不是原先示例程序的 Bundle 对象,因为 Bundle 对象我暂不知道如何传递……知道的可以告诉我,谢谢!
导出组件一般有以下三种形式:
android:exported=“true”
;android:exported=“false”
,但是其存在 intent-filter 以及 action ,则也为导出组件;android:exported
属性默认值为 true,17 及以上默认值为 false。任意第三方 App 都可以访问导出组件。
Android 提供了许多权限(权限列表可参见:Android 危险权限与所有权限大全)来限制第三方应用程序的行为,比如相机权限 android.permission.CAMERA
可防止三方应用程序在用户未授权情况下访问相机。
除了系统自带的权限之外,Android 应用程序也可以自定义权限来保护自身组件。自定义的权限在 AndroidMainfest.xml 文件中声明,格式如下:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.Tr0e.example"> <permission android:name="com.Tr0e.permission.TEST" android:description="@string/test_permission_description" android:permissionGroup="@string/test_permissionGroup" android:protectionLevel="signature" /> ... </manifest> <!-- 需定义字符串资源 src/main/res/values/strings.xml --> <resources> <string name="app_name">Test</string> <string name="test_permission_description">this is a test permission</string> <string name="test_permissionGroup">android.permission-group.COST_MONEY</string> </resources>
解释下各个属性:
Android 将权限分为若干个保护级别,比如:
【注意】自定义权限一般不指定 protectionLevel=normal
,因为保护级别为正常权限的话,其他三方应用可随意申请该权限,那么就没法实现自定义权限所预期想要的保护特定组件的效果了。
我们继续以上面讨论过的广播和广播接收器权限为例,看看如何通过自定义保护组件。
(1)谁有权给我发广播?
首先可以在 AndroidMainfest.xml 中定义静态广播接收器的时候指定组件的保护权限:
<receiver
android:name="MyExampleReciver"
android:permission="com.Tr0e.permission.TEST" >
<intent-filter >
<actionandroid:name="android.intent.action.Mybroadcast" />
</intent-filter>
</receiver>
以上可以限定广播发送方应具有特点权限后才能给该静态注册的广播接收器发送广播。如果是动态注册的广播接收器则可以通过在 registerReceiver()
函数中添加权限字段的方式来添加权限保护:
public class MainActivity extends AppCompatActivity { private IntentFilter intentFilter; private NetworkChangeReceiver networkChangeReceiver; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //创建IntentFilter实例,并添加action intentFilter = new IntentFilter(); //当网络发生变化时,系统发出的广播为android.net.conn.CONNECTIVITY_CHANGE intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE"); //创建NetworkChangeReceiver实例 networkChangeReceiver = new NetworkChangeReceiver(); //调用registerReceiver()方法进行注册,同时声明广播发送者应具有的权限(可选项,默认为空) registerReceiver(networkChangeReceiver, intentFilter,"com.Tr0e.permission.TEST"); } @Override protected void onDestroy() { super.onDestroy(); //活动结束时,调用unregisterReceiver方法实现取消注册 unregisterReceiver(networkChangeReceiver); } //创建一个类,让它继承自BroadcastReceiver,并重写父类的onReceive()方法即可。 //具体的处理逻辑是在onReceive()中执行(不支持多线程,所以不要处理太复杂的信息) class NetworkChangeReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { ConnectivityManager connectionManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connectionManager.getActiveNetworkInfo(); if (networkInfo != null && networkInfo.isAvailable()) { Toast.makeText(context, "network is available", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(context, "network is unavailable", Toast.LENGTH_SHORT).show(); } } } }
(2)谁有权收我的广播?
以上是针对广播接收器组件的保护,而对于广播的保护,则可以通过 sendBroadcast(Intent, String)
的接口在发送广播时指定接收者必须具备的 permission 权限:
Intent intent = new Intent();
intent.setAction("android.intent.action.Mybroadcast");
sendBroadcast(intent, "com.Tr0e.permission.TEST");
如果担心反编译后,权限被窃取导致限制失效。可以在声明权限时,提高权限的 leverl 为签名验证,即只有相同签名的应用且有该权限才能够接收,这样就能够满足产品簇的问题。
自定义权限虽然能有效保护组件不被非法访问,但是权限管理又成为了一个必须考虑的问题。
经常会遇到某 apk 使用了未定义的权限来保护组件,这将使得防护形同虚设,恶意应用可以在自身 apk 中定义受害者应用所引用的未定义的权限,从而成功获得访问受害者应用该类“受保护”的组件。
未定义权限的排查,需关注以下两种场景:
判断权限是否已定义的方法(未返回任何权限定义信息则为未定义权限):
adb shell pm list permissions | findstr com.Tr0e.permission.TEST
同时可以通过命令查询权限的完整信息:
C:\Users\True
λ adb shell pm list permissions -f | grep -A4 com.Tr0e.permission.TEST
+ permission:com.Tr0e.permission.TEST
package:com.bwshen.test
label:null
description:this is a test permission
protectionLevel:signature
C:\Users\True
λ
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。