现在让我们看看当我们跟随Stalker的线程时的选项。Stalker在跟踪线程执行时生成事件,这些事件被放置到队列中并定期或由用户手动刷新。这不是由Stalker自己完成的,但是EventSink::process
vfunc重新进入JavaScript运行时一次处理一个事件将会非常昂贵。大小和时间段可以通过选项配置。事件可以在每个指令的基础上为调用、返回或所有指令生成。或者,它们可以在块的基础上生成,或者在执行块时,或者当它被Stalker引擎检测时。
我们还可以提供onReceive
或onCallSummary
两个回调之一。前者将非常简单地交付一个二进制blob,其中包含由Stalker
生成的原始事件,事件按照它们生成的顺序排列。(可以使用Stalker.parse()
将其转换为表示事件的元组的JS数组。)。第二个聚合这些结果,只是返回每个函数被调用的次数。这比onReceive更有效,但数据的粒度要小得多。
在继续描述Stalker的详细实现之前,我们首先需要理解设计中使用的一些关键术语和概念。
Interceptor.attach()
在调用给定函数时获取回调。但是,当线程在Stalker中运行时,这些拦截器可能不起作用。这些拦截器通过修补目标函数的前几条指令(序言)来重新指导Frida的执行。Frida复制并将前几个指令重新定位到其他地方,以便在onEnter回调完成后,它可以将控制流重新定向到原始函数。Stalker.addCallProbe(address, callback[, data])
来提供此功能。如果我们的拦截器在块被检测之前已经附加,或者跟踪者的trustThreshold被配置,以便我们的块将被重新检测,那么我们的拦截器将工作(因为修补的指令将被复制到新的检测块)。否则就不会。当然,我们希望在不满足这些条件时能够支持钩子函数。API的普通用户可能不熟悉这种级别的设计细节,因此调用探测解决了这个问题。Stalker
引擎中。此外,还需要存储地址,以便在遇到调用该函数的指令时,可以将代码改为首先调用该函数。由于多个函数可能调用您添加探测的函数,因此许多检测块可能包含调用探测函数的额外指令。因此,无论何时添加或删除探测,缓存的插入块都将被销毁,因此所有代码都必须重新插入。注意,这个数据参数仅在回调是C回调时使用——例如,使用CModule实现——当使用JavaScript时,使用闭包来捕获任何所需的状态会更简单。instrumentation
块?好吧,这只有在我们检测的代码没有改变的情况下才有效。在自修改代码的情况下(通常用作反调试/反反汇编技术,试图阻止对安全关键代码的分析),代码可能会更改,因此无法重用被检测的代码块。那么,我们如何检测块是否发生了变化呢?我们只需在数据结构中保留原始代码的副本以及测试版本。然后,当我们再次遇到一个块时,我们可以将将要检测的代码与上次检测的版本进行比较,如果它们匹配,我们就可以重用该块。但是每次运行块时执行比较可能会降低速度。所以,这又是一个Stalker可以自定义的领域。Stalker.trustThreshold: 一个整数,指定在假定一段代码不会发生变化之前需要执行多少次。
指定-1表示不信任(缓慢),0表示从一开始就信任代码,N表示在执行N次之后信任代码。默认值为1。
实际上,N的值是块需要重新执行的次数,在我们停止执行比较之前,匹配之前检测的块(例如不变)。请注意,即使信任阈值设置为-1或0,代码块的原始副本仍然存储。虽然这些值实际上并不需要它,但为了保持简单,保留了它。在任何情况下,这两个都不是默认设置。
Stalker.exclude(range)
,它通过了一个基础和限制,用于防止Stalker在这些区域内检测代码。例如,考虑线程在libc中调用malloc()。您很可能不关心堆的内部工作方式,这不仅会降低性能,还会生成大量您不关心的无关事件。但是,需要考虑的一点是,一旦对一个排除的范围进行调用,跟踪该线程就会停止,直到它返回为止。这意味着,如果线程调用一个不在限制范围内的函数,可能是一个回调,那么Stalker就不会捕获这个函数。正如这可以用来停止跟踪整个库一样,它也可以用来停止跟踪给定函数(及其被调用者)。如果目标应用程序是静态链接的,这可能特别有用。在这里,was不能简单地忽略所有对libc的调用,但是我们可以使用Module.enumerateSymbols()
找到malloc()
的符号,并忽略那个单独的函数。