绕过iOS 基于svc 0x80的ptrace反调试
本文为看雪论坛优秀文章
看雪论坛作者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 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
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
__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
现在我们可以整理一下思路了:
三
方案实现
有了思路,那接下来我们要如何找到kernproc的内核地址呢?
通过阅读源码,我们知道kernproc的是一个全局变量,所以判断他的地址偏移一定是固定了,而且应该位于kernelcache,并且会在bsd_init过程中被初始化。
根据上边的线索,我们可以通过逆向kernelcache镜像文件找到他的偏移。
找到偏移后,下一个问题来了,由于ASLR的存在,我们必须要获取到kernbase才能配合偏移量定位kernproc位置,进行进一步操作。
索性GeoSn0w大神已经在github上提供了这个功能的代码,其原理是通过扫描kernel heap 找到指向内核镜像的指针,再根据这个内核景象向上回溯machO的head。详细的可以通过阅读源码来了解。
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;
}
好了,万事俱备了,现在需要的是通过代码将其实现:
// ---- 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
# 往期推荐
3.House of apple 一种新的glibc中IO攻击方法
球分享
球点赞
球在看
点击“阅读原文”,了解更多!