繞過iOS 基于svc 0x80的ptrace反調試
一、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", (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", kernel_read64(kb + 8 * i)); } uint64_t versionstraddr = kb + 0x2FB64; char versionstr[256]; if(kernel_read(versionstraddr, (void *)&versionstr, sizeof(versionstr))) { printf("%s", versionstr); if(strcmp(TARGET_KERNELCACHE_VERSION_STRING,versionstr) == 0) { printf("kernel cache hit"); //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"); return -1; } printf("uniqueid offset 0x%llx comm offset 0x%llx ",(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"); return -1; }else{ if(proc1->p_list.le_prev == 0) { printf("proc1->p_list.le_prev == 0"); 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(""); preptr = (uint64_t)(proc1->p_list.le_prev); } } printf("end"); free(proc0); free(proc1); }else{ printf("kernel cache version mismatch"); } }else{ printf("failed to read kernel version string"); } return 0;}
完整代碼可到github上下載:
https://github.com/xiaohang99/iOSFuckDenyAttach