1. 你好Ptrace
1.1 系统调用
系统调用是由系统核心提供的强大底层服务,是用户系统框架(比如C的stdlib
、Cocoa、UIKit和你自己的框架)的建立基础。
1.2 附着的基础:ptrace
我们利用dtrace
来监视ptrace
的调用,并把参数打印出来。
sudo dtrace -qn 'syscall::ptrace:entry { printf("%s(%d, %d, %d, %d) from %s\n", probefunc, arg0, arg1, arg2, arg3, execname); }'
之后我们新建一个tab用LLDB附着Finder
。
lldb -n Finder
(lldb) process attach --name "Finder"
//之前的tab输出
ptrace(14, 358, 0, 0) from debugserver
我们看到有一个叫debugserver
的进程调用了ptrace
,并且附着到了Finder
的进程上。debugserver
是如何被调用的?我们用的是LLDB
而不是debugserver
,现在debugserver
进程还是活跃的么?
要回答这些问题,我们新开一个tab,再输入
~> pgrep debugserver
2917
LLDB如果成功附着且还在运行,我们看到了一个输出2917
,代表了debugserver
的进程ID,即PID
,说明debugserver
还在运行。我们来看看debugserver
是如何启动起来的。
~> ps -fp `pgrep -x debugserver`
UID PID PPID C STIME TTY TIME CMD
502 2917 2916 0 10:28AM ttys001 0:00.21 /Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Resources/debugserver --fd=5 --native-regs --setsid
这个命令打印了debugserver
的完整路径和启动这个进程的所有参数。那哪个进程启动了它呢?
~> ps -o ppid= $(pgrep -x debugserver)
2916
~> ps -a 2916
PID TTY TIME CMD
2916 ttys001 0:08.60 /Applications/Xcode.app/Contents/Developer/usr/bin/lldb -n Finder
正如你所看到的,LLDB
启动了debugserver
这个进程,然后用ptrace
系统调用把它自己附着到了Finder
上。
1.3 ptrace的参数
虽然我们可以看到ptrace
的执行,但这些都是些数字,我们需要在<sys/ptrace.h>中查看它们的具体含义。
//mac项目中运行下面的代码
while true {
sleep(2)
print("helloptrace")
}
在ptrace
监听tab窗我们可以看到
ptrace(14, 3365, 0, 0) from debugserver
ptrace(13, 3365, 7939, 0) from debugserver
暂停工程,我们来看看<sys/ptrace.h>头文件,ptrace
的方法定义。
//$1: 是你想ptrace做什么
//$2: 你想应用到哪个pid上
//$3、$4取决于$1是什么
int ptrace(int _request, pid_t _pid, caddr_t _addr, int _data);
那么
// #define PT_ATTACHEXC 14 /* attach to running process with signal exception */
ptrace(14, 3365, 0, 0) from debugserver
关于PT_ATTACHEXC
的详细信息可以通过man ptrace
命令在终端中查看。这个请求可以让一个进程附着到另一个不相关的进程,它只需要要跟踪的pid
,其他参数不需要。
//#define PT_THUPDATE 13 /* signal for thread# */
ptrace(13, 3365, 7939, 0) from debugserver
至于13的PT_THUPDATE
是和怎么控制进程相关的,在我们的例子里面就是debugserver
;处理传递给被控制进程的UNIX信号和Mach消息,我们的例子里面就是,helloptrace
。这个特殊的ptrace
行为涉及到Mach核心怎么在内部实现ptrace
处理的实现细节有关。
1.4 创建一个附着问题
进程实际上可以使用PT_DENY_ATTACH
请求来拒绝被附着。这通常被用来作为一种反调试机制来保护我们的程序被逆向。
我们加上ptrace(PT_DENY_ATTACH, 0, nil, 0)
来试一试。
ptrace(PT_DENY_ATTACH, 0, nil, 0)
while true {
sleep(2)
print("helloptrace")
}
//运行结果
Program ended with exit code: 45
这是因为Xcode启动时helloptrace
会自动启动LLDB
附着。如果你拒绝附着,那么LLDB
就会提前退出,程序结束运行。
1.5 玩一玩PT_DENY_ATTACH
通常,开发者会在程序中加入ptrace(PT_DENY_ATTACH, 0, 0, 0)
,通常就在main
函数里面。
LLDB
有一个参数-w
来等待一个进程启动。那么就可以在程序启动还没有执行到ptrace(PT_DENY_ATTACH, 0, 0, 0)
之前进行附着,修改或忽略ptrace
。
~> sudo lldb -n "helloptrace" -w
Password:
(lldb) process attach --name "helloptrace" --waitfor
然后我们在终端的新tab中运行helloptrace
,就会发现LLDB
成功附着了。
Process 3575 stopped
* thread #1, stop reason = signal SIGSTOP
frame #0: 0x00000001142cc34e dyld`getattrlist + 10
dyld`getattrlist:
-> 0x1142cc34e <+10>: jae 0x1142cc358 ; <+20>
0x1142cc350 <+12>: mov rdi, rax
0x1142cc353 <+15>: jmp 0x1142ca769 ; cerror_nocancel
0x1142cc358 <+20>: ret
Target 0: (helloptrace) stopped.
我们在libsystem_kernel.dylib
中下一个ptrace
的断点,然后继续运行。
(lldb) rb ptrace -s libsystem_kernel.dylib
Breakpoint 1: no locations (pending).
WARNING: Unable to resolve breakpoint to any actual locations.
(lldb) continue
Process 3575 resuming
1 location added to breakpoint 1
Process 3575 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x00007fff662caa44 libsystem_kernel.dylib`__ptrace
libsystem_kernel.dylib`__ptrace:
-> 0x7fff662caa44 <+0>: xor rax, rax
0x7fff662caa47 <+3>: lea r11, [rip + 0x29cd7eda] ; errno
0x7fff662caa4e <+10>: mov dword ptr [r11], eax
0x7fff662caa51 <+13>: mov eax, 0x200001a
Target 0: (helloptrace) stopped.
我们断在了ptrace
将要执行的时候,我们可以直接用LLDB
提前返回,不执行这个函数。
(lldb) thread return 0
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x000000010e0d2a35 helloptrace`main at main.swift:31:1
28
29 import Foundation
30
-> 31 ptrace(PT_DENY_ATTACH, 0, nil, 0)
32
33 while true {
34 sleep(2)
(lldb) continue
Process 3575 resuming
我们可以看到,helloptrace
正常打印了。
我们也可以修改寄存器的值,下面是前四个参数的值,可以看到RDI
中是31(PT_DENY_ATTACH)
。再把RDI
中的值替换为0
,再继续运行,可以达到同样的效果。
(lldb) p $rdi
(unsigned long) $1 = 31
(lldb) p $rsi
(unsigned long) $2 = 0
(lldb) p $rdx
(unsigned long) $3 = 0
(lldb) p $rcx
(unsigned long) $4 = 0
(lldb) p $rdi = 0
(unsigned long) $6 = 0
(lldb) register read rdi
rdi = 0xffffffffffffffff
(lldb) continue
Process 3635 resuming
当然,如果你还会其他的方法也可以,比如利用
fishhook
替换ptrace
的行为。
1.6 其他反调试技术
长久以来,iTunes
都是用PT_DENY_ATTACH
的方式来反调试的。iTunes v12.7.0
的时候,开始用另一种方式来反调试了。
它使用sysctl
函数来检测它是否在被反调试,如果是的话就关闭程序。sysctl
像ptrace
一样是另一个系统调用。iTunes
在运行时利用一个NSTimer
来反复调用sysctl
执行检测。
下面是一个简单的Swift代码来展示iTunes
做了什么:
let mib = UnsafeMutablePointer<Int32>.allocate(capacity: 4)
mib[0] = CTL_KERN
mib[1] = KERN_PROC
mib[2] = KERN_PROC_PID
mib[3] = getpid()
var size: Int = MemoryLayout<kinfo_proc>.size
var info: kinfo_proc? = nil
sysctl(mib, 4, &info, &size, nil, 0)
if (info.unsafelyUnwrapped.kp_proc.p_flag & P_TRACED) > 0 {
exit(1)
}