查看原文
其他

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

xiaohang 看雪学苑 2022-08-18


本文为看雪论坛优秀文章

看雪论坛作者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源码阅读






球分享

球点赞

球在看



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

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存