首页   

绕过iOS 基于svc 0x80的ptrace反调试

看雪学苑  · 互联网安全  · 1 周前


本文为看雪论坛优秀文章

看雪论坛作者ID:xiaohang





ptrace反调试的原理与实现


由于厂商对于app安全方面的认识不断提升,当前iOS上的调试对抗愈演愈烈。而ptrace attach deny作为比较常用的反调试手段,其原理是将相关进程proc的p_lflag加上一个P_LNOATTACH标识位,当外部调试器想要再加载进程时,会返回一个Segmentation fault: 11 的错误标识:

iPhone8k:/usr/local root# debugserver 127.0.0.1:6666 -a Xxxxdebugserver-@(#)PROGRAM:LLDB  PROJECT:lldb-900.3.104 for arm64.Attaching to process Xxxx...Segmentation fault: 11


ptrace源码,摘自xnu-6153.101.6/bsd/kern/mach_process.c

intptrace(struct proc *p, struct ptrace_args *uap, int32_t *retval){//....     if (uap->req == PT_DENY_ATTACH) {//....        proc_lock(p);        if (ISSET(p->p_lflag, P_LTRACED)) {            proc_unlock(p);            //...            exit1(p, W_EXITCODE(ENOTSUP, 0), retval);             thread_exception_return();            /* NOTREACHED */        }        SET(p->p_lflag, P_LNOATTACH);//p_lflag |=0x00001000        proc_unlock(p);         return 0;    }....}


厂商为了防止API hook使其失效,开始大量使用基于svc 0x80的服务调用方式,并伴随着代码混淆以及代码膨胀,使得想要快速定位svc 0x80调用并将其patch掉也变得难以实现。

 

使用svc方式调用ptrace attach deny

__asm__("mov X0, #31"        "mov X1, #0"        "mov X2, #0"        "mov X3, #0"        "mov X16, #26"        "svc #0x80"        );

以上是ptrace反调试的简单介绍,如有疑问可参考下面的文章:

https://blog.it-securityguard.com/itunes-exploit-development/
https://cardaci.xyz/blog/2018/02/12/a-macos-anti-debug-technique-using-ptrace/





对抗方案


ptrace实现PT_DENY_ATTACH,就是对相关进程proc的p_lflag加上P_LNOATTACH标示位。那么要想使得进程和被调试器加载,只需要取消这个标志位。现在的问题是,proc链表结构,是位于iOS内核中,所以我们必须要拥有读写iOS内核的能力,要获取这个能力,第一个想到的办法是对iOS的漏洞利用,毕竟,iOS越狱也是基于这些漏洞,对特定内核位置进行读写。


所幸的是,当前一些越狱工具,提供了tfp0(task for pid 0)接口,可供我们读写iOS内核。


那什么是tfp0呢?theiphonewiki上给出的说明如下:


In the XNU kernel, task_for_pid is a function that allows a (privileged) process to get the task port of another process on the same host, except the kernel task (process ID 0). A tfp0 patch (or task_for_pid(0) patch) removes this restriction, allowing any executable running as root to call task_for_pid for pid 0 (hence the name) and then use vm_read and vm_write to modify the kernel VM region. The entitlements get-task-allow and task_for_pid-allow are required to make AMFI happy.


https://www.theiphonewiki.com/wiki/Tfp0_patch

 

现在我们可以整理一下思路了:

1、找到kernproc在内核的地址,然后通tfp0调用读取kernproc。
2、找到当前系统所有的进程信息,所有进程都放在了kernproc指向的链表中。
3、找到相当进程的proc,对p_lflag,进行修改。





方案实现


有了思路,那接下来我们要如何找到kernproc的内核地址呢?
通过阅读源码,我们知道kernproc的是一个全局变量,所以判断他的地址偏移一定是固定了,而且应该位于kernelcache,并且会在bsd_init过程中被初始化。


根据上边的线索,我们可以通过逆向kernelcache镜像文件找到他的偏移。




找到偏移后,下一个问题来了,由于ASLR的存在,我们必须要获取到kernbase才能配合偏移量定位kernproc位置,进行进一步操作。


索性GeoSn0w大神已经在github上提供了这个功能的代码,其原理是通过扫描kernel heap 找到指向内核镜像的指针,再根据这个内核景象向上回溯machO的head。详细的可以通过阅读源码来了解。

boolkernel_base_init_with_unsafe_heap_scan() {    uint64_t kernel_region_base = 0xfffffff000000000;    uint64_t kernel_region_end  = 0xfffffffbffffc000;    // Try and find a pointer in the kernel heap to data in the kernel image. We'll take the    // smallest such pointer.    uint64_t kernel_ptr = (uint64_t)(-1);    mach_vm_address_t address = 0;    for (;;) {        // Get the next memory region.        mach_vm_size_t size = 0;        uint32_t depth = 2;        struct vm_region_submap_info_64 info;        mach_msg_type_number_t count = VM_REGION_SUBMAP_INFO_COUNT_64;        kern_return_t kr = mach_vm_region_recurse(kernel_task_port, &address, &size,                &depth, (vm_region_recurse_info_t) &info, &count);        if (kr != KERN_SUCCESS) {            break;        }        // Skip any region that is not on the heap, not in a submap, not readable and        // writable, or not fully mapped.        int prot = VM_PROT_READ | VM_PROT_WRITE;        if (info.user_tag != 12            || depth != 1            || (info.protection & prot) != prot            || info.pages_resident * 0x4000 != size) {            goto next;        }        // Read the first word of each page in this region.        for (size_t offset = 0; offset < size; offset += 0x4000) {            uint64_t value = 0;            bool ok = kernel_read(address + offset, &value, sizeof(value));            if (ok                && kernel_region_base <= value                && value < kernel_region_end                && value < kernel_ptr) {                kernel_ptr = value;            }        }next:        address += size;    }    // If we didn't find any such pointer, abort.    if (kernel_ptr == (uint64_t)(-1)) {        return false;    }    printf("found kernel pointer %p\n", (void *)kernel_ptr);    // Now that we have a pointer, we want to scan pages until we reach the kernel's Mach-O    // header.    uint64_t page = kernel_ptr & ~0x3fff;    for (;;) {        bool found = is_kernel_base(page);        if (found) {            kernel_base = page;            return true;        }        page -= 0x4000;    }    return false;}


好了,万事俱备了,现在需要的是通过代码将其实现:

// ---- Main -------------------------------------------------------------------------------------- //iphone8  ios 13.4  kernel#define TARGET_KERNELCACHE_VERSION_STRING "@(#)VERSION: Darwin Kernel Version 19.4.0: Mon Feb 24 22:04:29 PST 2020; root:xnu-6153.102.3~1/RELEASE_ARM64_T8015" int main() {    kernel_task_init();    uint64_t kb = kernel_base_init();    for (size_t i = 0; i < 8; i++) {        printf("%016llx\n", kernel_read64(kb + 8 * i));    }    uint64_t versionstraddr = kb + 0x2FB64;    char versionstr[256];    if(kernel_read(versionstraddr, (void *)&versionstr, sizeof(versionstr)))    {        printf("%s\n", versionstr);        if(strcmp(TARGET_KERNELCACHE_VERSION_STRING,versionstr) == 0)        {            printf("kernel cache hit\n"


    
);            //226AF60  kernproc            uint64_t kernel_proc0 = kernel_read64(kb + 0x226AF60);             struct proc * proc0 =  (void *)malloc(sizeof(struct proc));             if(!kernel_read(kernel_proc0, (void *)proc0, sizeof(struct proc)))            {                printf("proc0 read failed\n");                return -1;            }             printf("uniqueid offset 0x%llx  comm offset 0x%llx \n",(int64_t)&(proc0->p_uniqueid) - (int64_t)proc0, (int64_t)&(proc0->p_comm)- (int64_t)proc0);             struct proc * proc1 =  (struct proc *)malloc(sizeof(struct proc));            uint64_t preptr = (uint64_t)(proc0->p_list.le_prev);            while(preptr){                if(!kernel_read(preptr, (void *)proc1, sizeof(struct proc)))                {                    printf("procnext read failed\n");                    return -1;                }else{                    if(proc1->p_list.le_prev == 0)                    {                        printf("proc1->p_list.le_prev == 0\n");                        break;                    }                    int64_t lflagoffset = (int64_t)&(proc1->p_lflag) - (int64_t)proc1;                    int lflagvalue = proc1->p_lflag;                    printf("(%llu)%s  proc = 0x%llx   lflag = 0x%x  lflag offset = 0x%llx"                        ,proc1->p_uniqueid,                        proc1->p_comm,//(char *)((int64_t)proc1 + 0x258),                        preptr,lflagvalue,lflagoffset);                         if(ISSET(lflagvalue, P_LNOATTACH))                        {                            printf(" !!!P_LNOATTACH set");                            CLR(lflagvalue, P_LNOATTACH);                            KERNEL_WRITE32(preptr + lflagoffset, lflagvalue);                        }                        printf("\n");                     preptr = (uint64_t)(proc1->p_list.le_prev);                }            }             printf("end\n");            free(proc0);            free(proc1);        }else{            printf("kernel cache version mismatch\n");        }    }else{        printf("failed to read kernel version string\n");    }    return 0;}


完整代码可到github上下载:

https://github.com/xiaohang99/iOSFuckDenyAttach




看雪ID:xiaohang

https://bbs.pediy.com/user-home-12238.htm

*本文由看雪论坛 xiaohang 原创,转载请注明来自看雪社区



# 往期推荐

1.CobaltStrike ShellCode详解

2.Android APP漏洞之战——SQL注入漏洞初探

3.House of apple 一种新的glibc中IO攻击方法

4.so文件分析的一些心得

5.从PWN题NULL_FXCK中学到的glibc知识

6.指令级工具Dobby源码阅读






球分享

球点赞

球在看



点击“阅读原文”,了解更多!

© 2022 51好读