第14章:你好,Ptrace

正如本书介绍中所提到的,调试并不完全是把东西修好。调试是对幕后发生的事情获得更好理解的过程。在本章中,你将探索调试的基础,即负责一个进程附加到另一个进程的系统调用:ptrace。

此外,你将学习一些开发者使用trace的常见安全技巧,以防止一个进程附加到他们的程序中。你还将学习一些简单的变通方法来解决这些开发者强加的限制。

系统调用

等等......ptrace是一个系统调用。什么是系统调用?

系统调用是由内核提供的强大的、低级别的服务。系统调用是用户地框架的基础,如C的stdlib、Cocoa、UIKit,甚至是你自己的辉煌框架都是建立在这个基础上的。

macOS Mojave Sierra有大约533个系统调用。打开一个终端窗口,运行以下命令,可以非常接近地估计出你的系统中可用的系统调用的数量。

sudo dtrace -ln 'syscall:::entry' | wc -l

这个命令使用了一个名为DTrace的非常强大的工具来检查你的MacOS机器上存在的系统调用。

注意:记住,如果你想使用 DTrace,你需要禁用 SIP(见第 1 章)。此外,你还需要sudo来使用DTrace命令,因为DTrace可以监控多个用户的进程,并执行一些令人难以置信的强大操作。巨大的权力伴随着巨大的责任--这就是为什么你需要sudo。

你将在本书的第五节中了解更多关于如何使DTrace服从你的意志。现在,你将使用简单的DTrace命令从ptrace中获取系统调用信息。

附件的基础,ptrace

你现在要更深入地看看trace的系统调用。打开一个终端控制台。在你开始之前,确保按⌘ + K清除终端控制台。接下来,在终端中执行下面的DTrace内联脚本,看看ptrace是如何被调用的。

sudo dtrace -qn 'syscall::ptrace:entry { printf("%s(%d, %d, %d, %d) from %s\n", probefunc, arg0, arg1, arg2, arg3, execname); }'

这将创建一个DTrace探针,在每次ptrace函数执行时执行;它将吐出ptrace系统调用的参数以及负责调用的可执行文件。

不要担心这个 DTrace 脚本的语义;在后面的章节中,你会对这个工具产生不舒服的感觉。现在,只需关注从终端返回的内容。

在终端中用快捷键⌘+T创建一个新的标签。

注意:如果你还没有禁用Rootless,你需要查看第1章,了解如何禁用它,否则,当附加到Finder时,ptrace会失败,你的DTrace脚本也不会工作。

一旦Rootless被禁用,在新的终端标签中输入以下内容。

lldb -n Finder

一旦你连接到Finder应用程序,你在第一个终端标签上设置的DTrace探针将吐出一些类似于以下的信息。

ptrace(14, 459, 0, 0) from debugserver

似乎一个名为 debugserver 的进程负责调用 ptrace 并附加到 Finder 进程。但是debugserver是如何被调用的呢?你是用 LLDB 连接到 Finder 的,而不是 debugserver。还有,这个debugserver进程还活着吗?

是时候回答这些问题了。在终端中创建一个新标签(⌘ + T)。接下来,在终端窗口中输入以下内容。

pgrep debugserver

如果LLDB已经成功连接并且正在运行,你会收到一个代表debugserver进程ID的整数输出,或者说PID,表明debugserver还活着,并且在你的电脑上运行。

由于debugserver目前正在运行,你可以找出debugserver是如何启动的。输入以下内容。

ps -fp `pgrep -x debugserver`

请注意,上述命令使用了反斜线,而不是单引号,以使命令生效。

这将给你提供debugserver位置的完整路径,以及用于启动该进程的所有参数。

你会看到类似于下面的内容。

爽啊! 这可能让你想知道,当你减去或修改某些启动参数时,功能会发生什么变化。例如,如果你去掉了 --reverse-connect 127.0.0.1:59297,会发生什么?

那么是哪个进程启动了debugserver?输入以下内容。

ps -o ppid= $(pgrep -x debugserver)

这将倒出负责启动 debugserver 的父 PID。你会得到一个与下面类似的整数。

82122

在使用PID时,它们在你的计算机上很可能与你在这里看到的不同(以及在不同的运行中)。

好吧,数字很有趣,但你很想知道与这个PID相关的实际名称。你可以通过在终端执行以下操作来获得这一信息,用你在上一步发现的PID替换数字。

ps -a 82122

你会得到负责启动 debugserver 的进程的名称、全路径和启动参数。

PID TT STAT TIME COMMAND 
82122 s000 S+ 0:05.35 /Applications/Xcode.app/Contents/Developer/ 
usr/bin/lldb -n Finder

正如你所看到的,LLDB负责启动debugserver进程,然后它使用ptrace系统调用将自己连接到Finder。现在你知道了这个调用的来源,你可以更深入地研究传入ptrace的函数参数。

ptrace参数

你能够推断出ptrace被调用时执行的进程和参数。不幸的是,它们只是一些数字,目前对你来说相当无用。现在是时候使用<sys/ptrace.h>头文件来理解这些数字了。

为了做到这一点,你将使用一个macOS应用程序来指导你的理解。

打开helloptrace应用程序,你可以在本章的资源文件夹中找到它。这是一个macOS终端命令应用程序,是最基本的。它所做的只是启动然后完成,根本没有输出到stdout。

这个项目中唯一值得关注的是一个桥接头,用于将ptrace系统调用API导入Swift中。

打开main.swift,在文件末尾添加以下代码。

while true { 
  sleep(2) 
  print("helloptrace") 
}

接下来,定位Xcode和DTrace终端窗口,使它们在同一屏幕上都能看到。

构建并运行该应用程序。一旦你的应用程序启动,并且debugserver已经连接,观察DTrace脚本产生的输出。

请注意DTrace终端窗口的情况。当helloptrace进程开始运行时,将发生两个新的ptrace调用。DTrace脚本的输出将类似于这样。

ptrace(14, 50121, 0, 0) from debugserver   
ptrace(13, 50121, 5891, 0) from debugserver

使用Xcode的快速打开功能(⌘ + Shift + O)并输入/usr/include/sys/ ptrace.h。

在ptrace.h中可以看到以下ptrace的函数原型。

int ptrace(int _request, pid_t _pid, caddr_t _addr, int _data);

第一个参数是你希望ptrace做什么。第二个参数是你想执行的PID。第三和第四个参数取决于第一个参数。

回顾一下你先前的DTrace输出。你的第一行输出与下面的内容相似。

ptrace(14, 50121, 0, 0) from debugserver

比较一下第一个参数和ptrace.h头,你会发现第一个参数14实际上代表PT_ATTACHEXC。这个PT_ATTACHEXC是什么意思?要获得这个参数的信息,首先要打开一个终端窗口。最后,输入man ptrace并搜索PT_ATTACHEXC。

注意:你可以通过按"/"来执行大小写搜索,然后再按你的搜索查询。你可以按N键向下搜索,或者按Shift + N键向上搜索到上一个关键词。

你会发现一些关于PT_ATTACHEXC的相关信息,下面的输出是从ptrace man页中获得的。

有了这些信息,第一次调用ptrace的原因应该很清楚。这个调用说 "嘿,附加到这个进程",并附加到第二个参数中提供的进程。

从你的DTrace输出进入下一个trace调用。

ptrace(13, 50121, 5891, 0) from debugserver

这个调用有点难理解,因为苹果决定不给任何关于这个调用的人工文档。这个调用与一个进程附加到另一个进程的内部有关。

如果你看一下ptrace API的标题,13代表PT_THUPDATE,与控制进程(这里是debugserver)如何处理UNIX信号和传递给被控制进程的Mach消息有关;这里是helloptrace。

内核需要知道如何处理来自另一个进程控制的信号传递,如第1节中的信号项目。控制进程可以说它不希望向被控制进程发送任何信号。

这个特定的ptrace动作是Mach内核如何在内部处理ptrace的一个实现细节;没有必要纠结于此。

幸运的是,还有其他记录在案的信号绝对值得通过人去探索。其中之一是PT_DENY_ATTACH动作,你现在要学习的就是这个。

创建附加问题

一个进程实际上可以通过调用ptrace并提供PT_DENY_ATTACH参数来指定它不希望被附加。这通常被用作一种反调试机制,以防止不受欢迎的反向工程师发现程序的内部结构。

现在你将对这个参数进行实验。打开main.swift,在while循环前添加以下一行代码。

ptrace(PT_DENY_ATTACH, 0, nil, 0)

建立并运行,注意观察调试器控制台,看看会发生什么。

程序将退出并向调试器控制台输出以下内容。

Program ended with exit code: 45

注意:你可能需要通过点击查看▸调试区▸激活控制台(或⌘ + Shift + Y,如果你是那些很酷的快捷方式的开发者之一)来打开调试控制台,看看这个。

发生这种情况是因为Xcode默认启动helloptrace程序时自动附加了LLDB。如果你用PT_DENY_ATTACH执行ptrace函数,LLDB将提前退出,程序将停止执行。

如果你试图执行helloptrace程序,并在之后试图附加到它,LLDB将无法附加,helloptrace程序将愉快地继续执行,对debugserver的附加问题视而不见。

有许多macOS(和iOS)程序在他们的生产构建中执行这个动作。然而,要规避这一安全防范措施是相当容易的。忍者调试模式启动!

绕过PT_DENY_ATTACH

一旦一个进程执行了带有PT_DENY_ATTACH参数的ptrace,制作附件的复杂性就会大大增加。然而,有一个更简单的方法来解决这个问题。

通常情况下,开发者会在主可执行程序代码的某个地方执行ptrace(PT_DENY_ATTACH, 0, 0, 0)--很多时候,就在主函数里。

由于LLDB有-w参数来等待进程的启动,你可以使用LLDB来 "捕捉 "进程的启动,并在进程有机会执行ptrace之前执行逻辑来增加或忽略PT_DENY_ATTACH命令。

打开一个新的终端窗口,输入以下内容。

sudo lldb -n "helloptrace" -w

这将启动一个ldb会话并附加到helloptrace程序,但这次-w告诉ldb等待,直到一个名称为helloptrace的新进程启动。

你需要使用sudo,因为当你告诉LLDB等待终端程序启动时,LLDB和macOS的安全有一个持续的错误。

在项目导航器中,打开产品文件夹,右击helloptrace可执行文件。接下来,选择在Finder中显示。

接下来,把helloptrace可执行文件拖到一个新的终端标签。最后,按回车键启动该可执行程序。

现在,打开先前创建的终端标签,你让LLDB坐在那里,等待helloptrace的可执行文件。

如果一切按预期进行,LLDB将看到helloptrace已经开始,并将启动自己,附加到这个新创建的helloptrace进程。

在LLDB中,创建以下的regex断点,在任何包含ptrace一词的函数类型上停止。

(lldb) rb ptrace -s libsystem_kernel.dylib

这将在用户区的网关上添加一个断点到实际的内核ptrace函数。接下来,在终端窗口输入continue。

(lldb) continue

你将在ptrace函数即将执行之前中断。然而,你可以简单地使用LLDB来提前返回,不执行该函数。现在就这样做吧。

(lldb) thread return 0

接下来,干脆直接继续。

(lldb) continue

尽管程序进入了ptrace用户区网关函数,你告诉LLDB提前返回,不执行将执行内核ptrace系统调用的逻辑。

导航到helloptrace输出标签,验证它是否在反复输出 "helloptrace"。如果是这样,你已经成功地绕过了PT_DENY_ATTACH,并且在运行LLDB的同时仍然连接到helloptrace命令

在几章中,你将通过检查Mach-O的__DATA.__la_symbol_ptr部分和可爱的DYLD_INSERT_LIBRARIES环境变量,来探索削弱外部函数如ptrace的另一种方法。

其他反调试技术

既然我们谈到了反调试的话题,让我们把iTunes放在现场:在很长一段时间里,iTunes实际上使用了ptrace的PT_DENY_ATTACH。然而,当前版本的iTunes(在撰写本文时为12.7.0)选择了一种不同的技术来防止调试。

iTunes现在会使用强大的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) 
}

我现在还不打算讨论sysctl的预期参数的细节,我们将把它留到另一章。只要知道有不止一种方法可以剥掉一只猫的皮。

接下来该怎么做呢?

用你在本章中使用的DTrace转储脚本,探索你系统的某些部分,看看trace何时被调用。

如果你觉得自己很自负,可以阅读一下ptrace的手册,看看你是否能创建一个程序,让它自动附加到你系统中的另一个程序。

还有精力吗?去读一下man sysctl。这将是一些不错的夜间阅读。

记住,有附件问题并不总是一件坏事!


上一章 目录 下一章