调试器究竟是怎么工作的?如何阻止一个进程attach(挂载)到app上以及又如何破解这些保护(所谓反调试和反反调试)?
关于系统调用
ptrace是一个系统调用。那系统调用是什么东东呢?它是一个系统提供的很强大的底层服务。用户层的框架是构建在system call之上的。
macOS Sierra大约提供了500个系统调用。通过以下命令来了解你系统上的系统调用的个数:
➜ ~ sudo dtrace -ln 'syscall:::entry' | wc -l
这个命令使用了另外一个更强大的工具叫DTrace
,暂不详谈它。
调试的基础--ptrace
在命令行中执行:(注意必须关闭SIP(system integrity protection))
➜ ~ sudo dtrace -qn 'syscall::ptrace:entry { printf("%s(%d, %d, %d, %d) from %s\n", probefunc, arg0, arg1, arg2, arg3, execname); }'
这个命令创建了一个DTrace探针
,它在每次ptrace函数执行时都会得到执行。
在命令行另外一个Tab中执行:
➜ ~ lldb -n Finder
此时dtrace那个tab会输出:
ptrace(14, 283, 0, 0) from debugserver
这看起来看起来像是一个名字叫debugserver的进程调用了ptrace并attach到了Finder进程上。
但debugserver是如何调用的呢?我们通过LLDB来attach到Finder,并不是debugserver。另外,这个debugserver进程是否还存活呢?
➜ ~ pgrep debugserver
==> 43474
既然这个进程存在,我们就来观察下它是如何启动以及有哪些启动参数
➜ ~ ps -fp `pgrep -x debugserver
UID PID PPID C STIME TTY TIME CMD
501 43474 43473 0 4:24PM ttys004 0:00.15 /Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Resources/debugserver --native-regs --setsid --reverse-connect 127.0.0.1:63719
cmd: /path/to/debugserver --native-regs --setsid --reverse-connect 127.0.0.1:63719
接下来看看如果我们去掉或修改一些启动参数会造成什么影响。去掉--reverse-connect 127.0.0.1:63719会发生什么事情呢
查看哪个进程启动了debugserver:
➜ ~ ps -o ppid= $(pgrep -x debugserver)
43473
➜ ~ ps -a 43473
PID TTY TIME CMD
43473 ttys004 0:05.19 /Applications/Xcode.app/Contents/Developer/usr/bin/lldb -n Finder
# 到这里可以看到是LLDB启动了debugserver进程,然后debugserver通过ptrace系统调用把自己attach到Finder上。
ptrace的参数
创建Mac上的命令行应用,执行这样一段Swift代码:
import Foundation
// ptrace(PT_DENY_ATTACH, 0, nil, 0) # 当前并不需要,只是用来帮助跳转到头文件
while true {
sleep(2)
print("Hello, Ptrace")
}
程序执行后又捕捉到2次ptrace调用:
ptrace(14, 45762, 0, 0) from debugserver
ptrace(13, 45762, 5891, 0) from debugserver
点击进入可以看到ptrace所在的头文件,其定义如下:
#define PT_TRACE_ME 0 /* child declares it's being traced */
#define PT_READ_I 1 /* read word in child's I space */
#define PT_READ_D 2 /* read word in child's D space */
#define PT_READ_U 3 /* read word in child's user structure */
#define PT_WRITE_I 4 /* write word in child's I space */
#define PT_WRITE_D 5 /* write word in child's D space */
#define PT_WRITE_U 6 /* write word in child's user structure */
#define PT_CONTINUE 7 /* continue the child */
#define PT_KILL 8 /* kill the child process */
#define PT_STEP 9 /* single step the child */
#define PT_ATTACH ePtAttachDeprecated /* trace some running process */
#define PT_DETACH 11 /* stop tracing a process */
#define PT_SIGEXC 12 /* signals as exceptions for current_proc */
#define PT_THUPDATE 13 /* signal for thread# */
#define PT_ATTACHEXC 14 /* attach to running process with signal exception */
#define PT_FORCEQUOTA 30 /* Enforce quota for root */
#define PT_DENY_ATTACH 31
#define PT_FIRSTMACH 32 /* for machine-specific requests */
int ptrace(int _request, pid_t _pid, caddr_t _addr, int _data);
第一个参数指明要ptrace要做的事情;第二个参数指明了要操作进程的PID,第三个和第四个取决于第一个参数。
可以看到上面ptrace的输出的14,就是PT_ATTACHEXC
。可以通过man ptrace
然后搜索它来查看这个具体是什么意思:
This request allows a process to gain control of an otherwise unrelated process and begin tracing it. It does not need any cooperation from the to-be-traced process. In this case, pid specifies the process ID of the to-be-traced process, and the other two arguments are ignored.
基于这个信息,可以知道为何会发生了第一次的ptrace调用了。
至于13的PT_THUPDATE
,苹果并没有给出更多说明。它大概与控制进程(lldb)如何处理传递给被控制进程(Xcode run的app)的Unix信号和Mach消息有关。
如何反调试
对上面代码中的被注释行取消注释:ptrace(PT_DENY_ATTACH, 0, nil, 0)
Xcode中Run起来后会发现debug console打印出这个:Program ended with exit code: 45
这是因为Xcode默认启动程序的同时用lldb attach上去。如果执行ptrace函数并带上PT_DENY_ATTACH
参数,lldb就会提前退出,程序会中止执行。如果你尝试单独运行程序,稍后再去attach,lldb同样会失败,程序依旧正常地运行下去不受影响。
有很多MacOS的应用就是用了这种方法来达到反调试。然而,这种方式还是非常容易被破解的。
如何反反调试
开发者要达到反调试的目的,必然是在某个地方(大多数还是在main函数)执行了ptrace(PT_DENY_ATTACH, 0, 0, 0)
。所以反反调试的思路非常简单,就是阻止这个执行的发生。
既然lldb有-w这个选项来等待一个进程的启动,你可以使用lldb来捕获到一个进程的启动并在程序执行到ptrace命令之前修改或忽略PT_DENY_ATTACH
命令。
命令行执行:➜ ~ sudo lldb -n "helloptrace" -w
。这里用sudo是因为lldb的一个bug,当你让lldb等待某个进程的启动时不用sudo会出错。
找到上述项目的二进制文件,拖到命令行中执行,然后lldb就应该能够成功attach上去:
➜ ~ sudo lldb -n "helloptrace" -w
(lldb) process attach --name "helloptrace" --waitfor
Process 8336 stopped
* thread #1, stop reason = signal SIGSTOP
frame #0: 0x0000000109522b9a dyld`__ioctl + 10
dyld`__ioctl:
-> 0x109522b9a <+10>: jae 0x109522ba4 ; <+20>
0x109522b9c <+12>: mov rdi, rax
0x109522b9f <+15>: jmp 0x109522325 ; cerror
0x109522ba4 <+20>: ret
Executable module set to "/Users/gogleyin/Library/Developer/Xcode/DerivedData/helloptrace-bjtaxdebpzdyraaogpbcrihdgwku/Build/Products/Debug/helloptrace".
Architecture set to: x86_64h-apple-macosx.
创建如下断点:
(lldb) rb ptrace -s libsystem_kernel.dylib
continue继续执行后你就会在ptrace函数将要执行时停下来。你可以用lldb来让程序不执行那个函数并提前返回:
(lldb) thread return 0
continue继续执行,一个反反调试就达成了!虽然程序进入了ptrace函数,但你是告诉lldb让它提前返回使得函数逻辑没有得到执行。
后续
但我们并不知道一个进程在什么时候执行了ptrace系统调用时,上面的方法就有些捉急了。此时又该怎样反反调试呢?两者孰能技高一筹,请看后续文章。