×
广告

Ptrace在iOS上反调试的利用和破解

96
blueshadow
2017.07.17 10:58* 字数 1316

调试器究竟是怎么工作的?如何阻止一个进程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系统调用时,上面的方法就有些捉急了。此时又该怎样反反调试呢?两者孰能技高一筹,请看后续文章。

LLDB
Web note ad 1