由于厂商对于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反调试的简单介绍,如有疑问可参考下面的文章
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.
现在我们可以整理一下思路了:
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上下载
最后,希望大家转发能注明出处
更多【绕过iOS 基于svc 0x80的ptrace反调试】相关视频教程:www.yxfzedu.com