首页
论坛
课程
招聘
[原创]绕过iOS 基于svc 0x80的ptrace反调试
2022-7-24 12:39 7361

[原创]绕过iOS 基于svc 0x80的ptrace反调试

2022-7-24 12:39
7361

ptrace反调试的原理与实现

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

1
2
3
4
5
iPhone8k:/usr/local root# debugserver 127.0.0.1:6666 -a Xxxx
debugserver-@(#)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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int
ptrace(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

1
2
3
4
5
6
7
__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。详细的可以通过阅读源码来了解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
bool
kernel_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;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// ---- 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

 

最后,希望大家转发能注明出处


[2022夏季班]《安卓高级研修班(网课)》月薪三万班招生中~

最后于 2022-7-24 12:59 被xiaohang编辑 ,原因:
收藏
点赞5
打赏
分享
最新回复 (7)
雪    币: 2800
活跃值: 活跃值 (1991)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
caolinkai 活跃值 2022-7-29 09:00
2
0
牛皮
雪    币: 2800
活跃值: 活跃值 (1991)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
caolinkai 活跃值 2022-7-29 09:04
3
0
 only support under checkra1n and iphone8 iOS 13.4
雪    币: 1060
活跃值: 活跃值 (1121)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mb_cwlqrkbj 活跃值 2022-7-29 20:34
4
0
雪    币: 1000
活跃值: 活跃值 (35)
能力值: ( LV3,RANK:29 )
在线值:
发帖
回帖
粉丝
wx_突突兔 活跃值 6天前
5
0
为什么都已经有tfp0还要扫内存去获取aslr,直接这样不香么。
uint64_t get_kslide(void)
{
    task_t tfp0;
    kern_return_t ret = task_for_pid(mach_task_self(), 0, &tfp0);
    if(ret != KERN_SUCCESS) return 0;
    task_dyld_info_data_t info;
    uint32_t count = TASK_DYLD_INFO_COUNT;
    ret = task_info(tfp0, TASK_DYLD_INFO, (task_info_t)&info, &count);
    if(ret != KERN_SUCCESS) return 0;
    return info.all_image_info_size;
}
雪    币: 1000
活跃值: 活跃值 (35)
能力值: ( LV3,RANK:29 )
在线值:
发帖
回帖
粉丝
wx_突突兔 活跃值 6天前
6
0
https://github.com/tututu-patch/iOSFuckDenyAttach
iPhone 8 Plus iOS 13.6 , Darwin Kernel Version 19.6.0: Sat Jun 27 04:36:08 PDT 2020; root:xnu-6153.142.1~4/RELEASE_ARM64_T8015
雪    币: 1661
活跃值: 活跃值 (1380)
能力值: (RANK:260 )
在线值:
发帖
回帖
粉丝
xiaohang 活跃值 3 5天前
7
0
wx_突突兔 为什么都已经有tfp0还要扫内存去获取aslr,直接这样不香么。 uint64_t get_kslide(void) { task_t tfp0; kern_return_t ...
有空我也尝试一下
雪    币: 1000
活跃值: 活跃值 (35)
能力值: ( LV3,RANK:29 )
在线值:
发帖
回帖
粉丝
wx_突突兔 活跃值 5天前
8
0
我已经移植到了unc0ver,目前工作一切正常https://github.com/tututu-patch/iOSFuckDenyAttach/tree/unc0ver
游客
登录 | 注册 方可回帖
返回