黄玮
MVVM
非「唯一」选择,只是典型架构 之一
Model
负责全部的业务逻辑,提供泛化数据,确保业务逻辑独立完整,被不同页面(View
)共享View
负责展示数据,拥有 ViewModel
的引用, 而 ViewModel
独立于 View
View
观察 ViewModel
提供的属性,监视到 ViewModel
有变化可以及时更新 UIViewModel
是为事件驱动的页面提供数据流,以生命周期方式存储与管理页面相关数据,供 View
绑定,是 一对多 关系
View
)知道生产者(ViewModel
),而生产者只提供数据, 并不关心谁消费Repository
是唯一依赖于其他多个类的类
SQLite
RESTful API Service
Shared Preferences
ViewModel
保存到「内存」中,支持保存复杂对象(但存储空间受可用内存限制),读写速度快,在配置更改后继续存在ViewModel
不复存在Activity
关闭或触发 onFinish()
方法调用后 ViewModel
不复存在Activity
、Fragment
或 Service
)的生命周期。这种感知能力可确保 LiveData
仅更新处于活跃生命周期状态的应用组件观察者相比较于直接调用 SQLite API 和使用 SQL 语句操作本地数据库
Room
支持返回 LiveData
对象的可观察查询Room
会生成更新 LiveData
对象所需的所有代码。在需要时,生成的代码会在后台线程上异步运行查询A type-safe HTTP client for Android and Java
RESTful
接口Retrofit
和 OkHttp
都是 Square
公司的开源解决方案Android
官方推荐的面向「标准化开发模式」可以使用的一套组件库,独立于 Android
操作系统和开发工具包发布
Java API
和 Kotlin API
Android
官方开发团队推荐的 最佳安全实践 ,不要局限于本章内容中的一些可能已过时的『经典安全编程实践范式』攻击手段 | |
---|---|
Activity | 构造Intent直接调⽤,实现⾮授权访问 后台守护进程通过进程枚举,直接启动新Activity覆盖到当前Activity进⾏『点击』劫持,实现钓鱼攻击(零权限要求) |
Service | 构造Intent直接调⽤,实现⾮授权访问 |
Broadcast Receiver | 构造Intent直接发送虚假⼴播消息,实现⾮授权访问 注册同名IntentFilter,实现⼴播消息监听和劫持 |
Content Provider | 直接访问暴露的URI,实现⾮授权访问 |
adb shell am start -n cuc.edu.cn/.MainActivity
android:exported="true"
<activity
android:name=".LoginActivity"
android:label="@string/app_name"
android:exported="true">
# 获取系统中所有应用
adb shell pm list packages
# 获取系统中所有第 3 方应用
adb shell pm list packages -3
# 查看指定应用的所有可访问 Main Activity 类名
adb shell dumpsys package | grep Activity | grep -i cuc.edu.cn
# 查看当前打开应用的活动 Activity
adb shell dumpsys window windows | grep -i cuc.edu.cn
# ref: https://programmer.help/blogs/dumpsys-command-in-android.html
ContentProvider
组件包含的 android:exported
属性值默认设置为 false
android:exported
默认设置为 false
,即使子元素包含 intent-filter
定义也需要显示设置 android:exported=true
,否则依然为 false
Android 12
以前版本三大组件声明时的 android:exported
默认值的一般规则:如果包含 intent-filter
子元素定义,则没有声明 android:exported
属性也视为隐式声明该属性值为 true
。否则,默认值均为 false
activity
、service
、广播接收器
,尤其是 Content Provider
android:exported
属性,以尽量避免混淆默认值Android Permission
机制对被访问 Activity 设置访问控制规则
adb shell am start -n cuc.edu.cn/.DisplayMessageActivity
# Starting: Intent { cmp=cuc.edu.cn/.DisplayMessageActivity }
# Security exception: Permission Denial: starting Intent { flg=0x10000000 cmp=cuc.edu.cn/.DisplayMessageActivity } from null (pid=3804, uid=2000) not exported from uid 10104
#
# java.lang.SecurityException: Permission Denial: starting Intent { flg=0x10000000 cmp=cuc.edu.cn/.DisplayMessageActivity } from null (pid=3804, uid=2000) not exported from uid 10104
# at com.android.server.wm.ActivityStackSupervisor.checkStartAnyActivityPermission(ActivityStackSupervisor.java:1043)
# at com.android.server.wm.ActivityStarter.startActivity(ActivityStarter.java:760)
# at com.android.server.wm.ActivityStarter.startActivity(ActivityStarter.java:583)
# at com.android.server.wm.ActivityStarter.startActivityMayWait(ActivityStarter.java:1288)
# at com.android.server.wm.ActivityStarter.execute(ActivityStarter.java:514)
# at com.android.server.wm.ActivityTaskManagerService.startActivityAsUser(ActivityTaskManagerService.java:1058)
# at com.android.server.wm.ActivityTaskManagerService.startActivityAsUser(ActivityTaskManagerService.java:1032)
# at com.android.server.am.ActivityManagerService.startActivityAsUser(ActivityManagerService.java:3504)
# at com.android.server.am.ActivityManagerShellCommand.runStartActivity(ActivityManagerShellCommand.java:518)
# at com.android.server.am.ActivityManagerShellCommand.onCommand(ActivityManagerShellCommand.java:172)
# at android.os.ShellCommand.exec(ShellCommand.java:104)
# at com.android.server.am.ActivityManagerService.onShellCommand(ActivityManagerService.java:9774)
# at android.os.Binder.shellCommand(Binder.java:881)
# at android.os.Binder.onTransact(Binder.java:765)
# at android.app.IActivityManager$Stub.onTransact(IActivityManager.java:4498)
# at com.android.server.am.ActivityManagerService.onTransact(ActivityManagerService.java:2741)
# at android.os.Binder.execTransactInternal(Binder.java:1021)
# at android.os.Binder.execTransact(Binder.java:994)
# 如果在 AndroidManifest.xml 中没有显式设置 android:exported="true"
# Android 6.0 系统默认值视为 true
# 当 Activity 显式设置 android:exported="false" 时
# Android 6.0 系统并不会在终端输出如上异常信息,只是静默唤起 Activity 失败
# Android 10.0 11.0 系统默认值视为 false
# 获取 Android 系统版本信息
adb shell getprop ro.build.version.release
# 10
# 获取 Android 系统版本 API Level
adb shell getprop ro.build.version.sdk
# 29
# 获取目标 Android 系统所有运行时信息
adb shell getprop
# 发送 Intent 时传参数
adb shell am start -n cuc.edu.cn/.DisplayMessageActivity --es cuc.edu.cn.MESSAGE hello
<!-- 被访问 Activity 添加 `android:permission` 属性 -->
<activity android:name=".Another" android:label="@string/app_name"
android:permission="com.test.custompermission">
</activity>
<!-- AndroidManifest.xml 中声明自定义权限 -->
<permission android:description="test"
android:label="test"
android:name="com.test.custompermission"
android:protectionLevel="normal">
</permission>
protectionLevel
有四种级别 normal
、dangerous
、signature
、signatureOrSystem
signature
、signatureOrSystem
时,只有调用者 apk
和被调用者 apk
具备相同签名时才能调用<uses-permission android:name="com.test.custompermission" />
if (context.checkCallingOrSelfPermission("com.test.custompermission")
!= PackageManager.PERMISSION_GRANTED) {
// The Application requires permission to access the Internet
} else {
// OK to access the Internet
}
所「点」非所得
Promon
根据掌握的「在野」漏洞利用案例情况,报告了该漏洞,并宣称扩展了上述 2015 年发现的任务劫持理论模型,影响 Android 10 及更低版本,Android 开发团队却并不认可该漏洞StrandHogg
维京海盗突袭战术
lanchMode
为 standard
或 singleTop
时,通过设定恶意程序 taskAffinity
与目标 Activity
所属应用包名一致,并配合使用 android:allowTaskReparenting="true"
,将恶意 Activity
置于目标任务栈的内部或顶部,当用户点击受害者应用图标时恶意程序会伪装成正常应用程序的界面迷惑用户实现界面劫持攻击taskAffinity
与目标程序保持一致,且 lanchMode
设定为 singleInstance
,则恶意程序运行后可以构建与目标程序同名的返回栈,并以不同任务 ID 存在于后台,从而可将任务隐藏于后台adb shell getprop ro.build.version.release
# 8.1.0
adb shell dumpsys activity | grep -i "running activities" -A6
# Running activities (most recent first):
# TaskRecord{a771cb #19 A=cuc.edu.cn U=0 StackId=1 sz=2}
# Run #5: ActivityRecord{8e9b1ab u0 cuc.edu.cn/.DisplayMessageActivity t19}
# Run #4: ActivityRecord{8ba91c8 u0 cuc.edu.cn/.MainActivity t19}
# TaskRecord{b0ed9f0 #11 A=com.android.settings U=0 StackId=1 sz=3}
# Run #3: ActivityRecord{6ec9472 u0 com.android.settings/.SubSettings t11}
# Run #2: ActivityRecord{f817f74 u0 com.android.settings/.Settings$NetworkDashboardActivity t11}
adb shell dumpsys activity activities | head -n 70
# ACTIVITY MANAGER ACTIVITIES (dumpsys activity activities)
# Display #0 (activities from top to bottom):
# Stack #1:
# mFullscreen=true
# isSleeping=false
# mBounds=null
# Task id #19
# mFullscreen=true
# mBounds=null
# mMinWidth=-1
# mMinHeight=-1
# mLastNonFullscreenBounds=null
# * TaskRecord{a771cb #19 A=cuc.edu.cn U=0 StackId=1 sz=2}
# userId=0 effectiveUid=u0a79 mCallingUid=2000 mUserSetupComplete=true mCallingPackage=null
# affinity=cuc.edu.cn
# intent={act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 cmp=cuc.edu.cn/.MainActivity}
# realActivity=cuc.edu.cn/.MainActivity
# autoRemoveRecents=false isPersistable=true numFullscreen=2 taskType=0 mTaskToReturnTo=1
# rootWasReset=false mNeverRelinquishIdentity=true mReuseTask=false mLockTaskAuth=LOCK_TASK_AUTH_PINNABLE
# Activities=[ActivityRecord{8ba91c8 u0 cuc.edu.cn/.MainActivity t19}, ActivityRecord{8e9b1ab u0 cuc.edu.cn/.DisplayMessageActivity t19}]
# askedCompatMode=false inRecents=true isAvailable=true
# lastThumbnail=null lastThumbnailFile=/data/system_ce/0/recent_images/19_task_thumbnail.png
# stackId=1
# hasBeenVisible=true mResizeMode=RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION mSupportsPictureInPicture=false isResizeable=true firstActiveTime=1622768158264 lastActiveTime=1622768169700 (inactive for 54s)
# * Hist #1: ActivityRecord{8e9b1ab u0 cuc.edu.cn/.DisplayMessageActivity t19}
# packageName=cuc.edu.cn processName=cuc.edu.cn
# launchedFromUid=10079 launchedFromPackage=cuc.edu.cn userId=0
# app=ProcessRecord{9f97da8 9061:cuc.edu.cn/u0a79}
# Intent { cmp=cuc.edu.cn/.DisplayMessageActivity (has extras) }
# frontOfTask=false task=TaskRecord{a771cb #19 A=cuc.edu.cn U=0 StackId=1 sz=2}
# taskAffinity=cuc.edu.cn
# realActivity=cuc.edu.cn/.DisplayMessageActivity
# baseDir=/data/app/cuc.edu.cn-qLptM63CTI1Wt0oFxe2RRg==/base.apk
# dataDir=/data/user/0/cuc.edu.cn
# stateNotNeeded=false componentSpecified=true mActivityType=0
# compat={560dpi} labelRes=0x7f0e001b icon=0x7f0c0001 theme=0x7f0f019c
# mLastReportedConfigurations:
# mGlobalConfig={1.0 310mcc260mnc [en_US] ldltr sw411dp w411dp h659dp 560dpi nrml port finger qwerty/v/v -nav/h appBounds=Rect(0, 0 - 1440, 2392) s.6}
# mOverrideConfig={1.0 310mcc260mnc [en_US] ldltr sw411dp w411dp h659dp 560dpi nrml port finger qwerty/v/v -nav/h appBounds=Rect(0, 0 - 1440, 2392) s.6}
# CurrentConfiguration={1.0 310mcc260mnc [en_US] ldltr sw411dp w411dp h659dp 560dpi nrml port finger qwerty/v/v -nav/h appBounds=Rect(0, 0 - 1440, 2392) s.6}
# taskDescription: iconFilename=null label="null" primaryColor=ff6200ee
# backgroundColor=ffffffff
# statusBarColor=ff3700b3
# navigationBarColor=ff000000
# launchFailed=false launchCount=1 lastLaunchTime=-54s318ms
# haveState=false icicle=null
# state=RESUMED stopped=false delayedResume=false finishing=false
# keysPaused=false inHistory=true visible=true sleeping=false idle=true mStartingWindowState=STARTING_WINDOW_NOT_SHOWN
# fullscreen=true noDisplay=false immersive=false launchMode=0
# frozenBeforeDestroy=false forceNewConfig=false
# mActivityType=APPLICATION_ACTIVITY_TYPE
# waitingVisible=false nowVisible=true lastVisibleTime=-53s785ms
# resizeMode=RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION
# mLastReportedMultiWindowMode=false mLastReportedPictureInPictureMode=false
# * Hist #0: ActivityRecord{8ba91c8 u0 cuc.edu.cn/.MainActivity t19}
# packageName=cuc.edu.cn processName=cuc.edu.cn
# launchedFromUid=2000 launchedFromPackage=null userId=0
# app=ProcessRecord{9f97da8 9061:cuc.edu.cn/u0a79}
# Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 cmp=cuc.edu.cn/.MainActivity }
# frontOfTask=true task=TaskRecord{a771cb #19 A=cuc.edu.cn U=0 StackId=1 sz=2}
# taskAffinity=cuc.edu.cn
# realActivity=cuc.edu.cn/.MainActivity
# baseDir=/data/app/cuc.edu.cn-qLptM63CTI1Wt0oFxe2RRg==/base.apk
# dataDir=/data/user/0/cuc.edu.cn
# stateNotNeeded=false componentSpecified=true mActivityType=0
# compat={560dpi} labelRes=0x7f0e001b icon=0x7f0c0001 theme=0x7f0f019c
# mLastReportedConfigurations:
# mGlobalConfig={1.0 310mcc260mnc [en_US] ldltr sw411dp w411dp h659dp 560dpi nrml port finger qwerty/v/v -nav/h appBounds=Rect(0, 0 - 1440, 2392) s.6}
# mOverrideConfig={1.0 310mcc260mnc [en_US] ldltr sw411dp w411dp h659dp 560dpi nrml port finger qwerty/v/v -nav/h appBounds=Rect(0, 0 - 1440, 2392) s.6}
# CurrentConfiguration={1.0 310mcc260mnc [en_US] ldltr sw411dp w411dp h659dp 560dpi nrml port finger qwerty/v/v -nav/h appBounds=Rect(0, 0 - 1440, 2392) s.6}
StrandHogg
攻击方法的恶意应用被安装到用户手机上Home
键退出应用
Android
多任务调度机制特性,后台恶意应用可以被唤起到前台
Lookout
发现 36 款恶意软件有利用该漏洞进行攻击的事件Promon
公司的研究人员测试了 Top 500
应用均证明受该漏洞影响
42matters
提供的应用排名榜单StrandHogg 2.0
无法完全依赖于应用商店审核机制发现恶意应用
StrandHogg
会在恶意应用的清单文件中留下明显痕迹
StrandHogg 2.0
无需在应用清单文件中申请任何特殊权限或设置设置 taskAffinity
属性,而是在代码运行时利用 Java 的反射机制执行恶意代码Google
之所以不认可针对 StrandHogg
为代表的 taskAffinity
属性字段 滥用 导致的 UI 欺骗与劫持
风险为有效漏洞,是因为 Google
认为这是一个正常的 feature
,Google
认为开发者无需进行代码级别安全加固(官方自然也不会发布什么代码级别修复补丁,无需代码级别补丁的缺陷报告是不会被 Google
认定为是安全漏洞的),只需要 应用商店 在审核应用时对第三方应用设置 taskAffinity
为非自家开发应用时给予禁止上架处理即可
Google
观点:用户只要没有渠道能下载安装包含 StrandHogg
攻击方式的恶意应用,则不存在上述安全风险Android
系统中,taskAffinity
与 launchMode
均为正常的属性,且在 Android
应用中有较多的应用,尤其在第三方插件中使用较为广泛,实际依赖于人工检测工作量肯定太大,必定要依赖于自动化检测程序每次启动新应用
之前清理一遍所有后台任务方式来自保了Activity
设置属性 android:taskAffinity=""
Activity
添加 singleTask
或 singleInstance
属性字段Android
系统到不受该漏洞影响版本是从根本上进行安全加固Room
持久化库将结构化数据存储在专用数据库中Android
使用路径(例如 /sdcard
)表示这些存储设备⚠️ 注意:可用于保存文件的确切位置可能因设备而异。因此,请勿使用硬编码的文件路径。
Android
系统中,应用需要声明这些权限才能访问位于外部存储空间中应用专属目录之外的任何文件<!-- API Level >= 19 需要声明以下权限才能读取外部存储空间中的数据 -->
<!-- API Level < 19 无需声明以下权限就能读取外部存储空间中的数据 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE ">
<!-- 应用获得了 WRITE_EXTERNAL_STORAGE 授权则自动获得 READ_EXTERNAL_STORAGE 授权 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE">
root
或代码编写错误,受保护数据将被其他程序访问到try {
// 确保该文件被保存到应用专属目录下,且文件属主被设置为应用属主,其他人无法访问
FileOutputStream fos = openFileOutput("config.txt", MODE_PRIVATE);
fors.write("hello world".getBytes());
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
AOSP
源代码编译出来的系统镜像AOSP
源代码修改定制之后的系统独立于设备厂商的个人或社区团队组织基于 AOSP 代码修改编译出来的系统镜像
⚠️ 小心第三方 ROM 内置的后门
Google Play
对已知重要漏洞和缺陷代码模式建立的 改进指南 案例库