一种非常常见的技术,它是由多线程引起的问题,它是内联钩子,它用对钩子过程的跳转指令替换了正常函数的序言代码,然后根据需要通过蹦床调用原始函数。
内联挂钩技术存在多个固有问题,这使其成为使用和调试非常复杂的方法。正如我所提到的,一个主要问题是,在现实世界中的正统多线程环境中,它并不安全。这是由于更改函数的字节时,您不能保证指令指针不会在新注入的代码中间,这可能导致目标应用程序由于执行旧的无效混合而崩溃。操作码与您插入的操作码混合在一起。
有一些解决方案,一种方法是挂起进程中的所有线程,然后检查每个线程中的指令指针,以确保当前没有任何线程在执行您要替换的目标指令。 。然后,如果碰巧有一个或两个线程在执行该特定功能,那么您可以通过执行诸如执行堆栈跟踪以将断点放置在返回地址处,恢复该线程,然后在该线程处理异常时进行相应的响应。从目标函数返回。
当然,此方法仍然是不安全的,因为在挂起进程中的所有正在运行的线程之前,并没有阻止任何偷偷摸摸的线程使用
CreateThread()
(计算机上的某些应用程序一次运行40多个线程)。甚至可能有一个相关过程在目标应用程序中使用CreateRemoteThread()
,然后在安全之前调用您要挂接的函数。 解决该问题的方法可能是尝试调试进程并接收有关进程何时创建新线程的通知,然后通过挂起该线程进行响应。当然,Windows API或第三方API提供的许多事件通知系统将不会实时发送,这可能允许该线程在挂起之前执行不安全的操作。
另一种解决方案可能是在启动进程之前使用钩子函数对可执行文件进行了静态修补,大概是通过钩住EAT / IAT。这对我来说不是一个选择,因为我需要一个可以在整个过程中工作的实现,而不管如何解析函数,或者是否有新的未钩连模块调用该函数。
有我没有提到的内联钩子技术需要克服的许多其他问题。这又使我回到我的问题:
可以使用什么方法在程序执行期间自动改变代码流?
我很想知道是否有更健壮的解决方案,以克服我在本文中介绍的方法的某些缺点。
请没有第三方库建议的挂钩函数。我想实现自己的学习目的。
我更喜欢使用C编程语言的挂钩技术文档和示例。与x86-x64兼容,并且我的操作系统是Windows7。
#1 楼
有关如何实现自己的修补系统的草图,其中替换指令的长度小于或等于您要修补的指令的长度:请确保您的代码都不依赖于您要修补的任何代码。这是重新进入的问题。如果您对补丁程序将使用的代码进行补丁,则可能会死锁/阻止/引发无数个信号。常见的解决方案是执行诸如重新实现所需的
libc
的子集或静态链接到libc
的某些其他实现的操作。符号版本控制可以帮助确保在运行时没有其他库链接到您的libc
版本(或类似libc
的函数)。具有专用的修补“主”线程。如果您使用的是Linux,则此修补线程可以是通过
ptrace
控制程序的外部进程。另一种选择是从发出信号的线程中动态选择一个主线程(在下一点中提到)。安装信号处理程序,以便您的修补线程可以发出信号通知所有其他线程在条件下停止和阻塞变量。显然,请确保您的修补程序线程未发出信号。您可能需要仔细检查一下,在用信号通知所有线程(您知道的时间)时,是否再也没有创建新线程。更改包含要修补的代码页的内存保护,以使它们可读,可写但不可执行。确保您使用的代码没有出现在同一页面上!更改代码。将该代码的内存保护更改回可读,可执行但不可写的状态。
给条件变量/线程发送信号以唤醒它。
当您的线程从其信号处理程序中的条件变量中被阻止唤醒时,它们需要执行同步指令,例如
CPUID
,在执行修补的代码之前。这样一来,旧版本的代码就不会保留在任何指令预取缓存/缓冲区/任何内容中。英特尔的软件优化手册在此处详细介绍。顺便提一下,请注意修补用于实现信号/等待/等的并发/信号机制!然后线程将从信号处理程序返回,以在发出信号的地方恢复执行。
现在,如果要用
N
字节指令对M
字节指令进行补丁,使M > N
怎么办?我们将采用大致相同的技术,但是我们将修改信号线程的返回地址,以指向包含您的补丁P
的原始指令的副本。例如,假设您如果有说明I1; I2; I3; I4; ...
,则补丁P
(如果已放置)将最终显示为:P; I3_tail_garbage; I4; ...
。然后,您可以在地址P; I1_copy; I2_copy; I3_copy; jmp &I4;
上创建补丁入口patch
。您将修改信号处理程序中的某些返回地址,如下所示:如果
RA == &I1
,则将其指向:P; I1_copy; I2_copy; I3_copy; jmp &I4;
。如果
RA == &I2
,则将其指向到:I2_copy; I3_copy; jmp &I4;
如果
RA == &I3
,则将其指向:I3_copy; jmp &I4;
修补
I1; I2; I3; I4; ...
以执行以下操作:jmp patch; int3; ...; int3; I4; ...
。注意:复制代码时,如果您的指令以某种方式从指令指针中读取,则需要重新相对。例如,如果
I1
,I2
或I3
是分支指令,或计算RIP
的相对地址,则可能需要用等效指令扩展/修改/替换它们。另一种方法是修补I1
,I2
和I3
的每个。如果这样做,则必须首先仅修补每个指令的第一个字节,并且仅使用int3
。即使其他线程正在执行正在修补的代码,也可以安全地完成此操作。但是,如果其他线程正在同时执行这些指令,则无法安全地修改这些指令的其他字节。这是因为这些指令可能已经被预取,并且一旦发生,它们就不再是一个内聚单元。 ,但我认为可以采用与上述复制前几条指令的方法类似的方法来处理,以确保不会丢失这些指令,但您还可以捕获执行补丁区域内代码的线程。 /> 我不熟悉Windows环境,因此
int3
问题听起来很棘手,但是我认为在代码中安装CreateRemoteThread
指令以及在搜索线程发出信号时保护代码免于执行。您可能还考虑让您的主修补程序线程在短时间内进入睡眠状态。 最后,还有一些不错的参考文献是Kprobes和RCU的东西,因为某些“额外”线程在看到旧版本或新版本(或中间版本)时遇到的问题RCU的主要问题。最后,请注意有关英特尔手册的语言。缓存一致性和icache。可以解释很多文本,就像在icache中表示对数据缓存的原子写入一样,但是实际上,这不能保证是正确的(尤其是在涉及预取的情况下),并且存在一些重要的CPU错误。使问题变得比最初更难的问题。
#2 楼
这是一个很好的问题,我认为没有一种100%安全的方法可以修补正在运行的Windows进程,除非您主动对其进行调试,而且即使那样也可能存在一些极端情况。您可以消除许多潜在的问题,但是我认为出于通用目的并不能完全消除潜在的线程问题。我认为这有两个实用的选择:
1.)暂停过程,修补代码,然后继续执行。所有线程都已挂起或没有被挂起,如果您有权对进程进行补丁,则很容易检测到。这是我的首选方法,尽管基于计时器的防调试措施以及防御性挂钩可以检测到这一点。总体来说,尽管我会说这是非常可靠的。
2.)很好地了解您的目标,而不依赖于“通用”的“一刀切”的所有修补技术。您应该事先知道多线程是否会妨碍针对特定目标的特定地址处的特定修补程序,以及执行可靠的实时修补的可行性。
如果您知道或怀疑您的目标代码是线程化的,找到使用的同步方法(锁,互斥锁,互锁的操作等),然后从线程安全代码开始打补丁,最好在强制执行临时线程争用/死锁以防止在打补丁时执行。可靠地执行此操作很可能是针对特定目标的,因此至少需要对目标有一点点熟悉的知识。没有这些知识,您就不可能创建原子补丁。
您将面临进行一系列(原子指令的)原子写入的问题,从而使得中间补丁无法执行崩溃/挂起/以意外方式执行。这不是要解决的琐碎问题。暂停过程并安全进行。
编辑:我只是意识到我上了诱饵,并以仅考虑挂钩的方式进行了回答,即补丁,即使您的问题专门询问如何在执行过程中自动更改代码流。在大多数情况下,正确的DLL注入应该可以使您相当可靠地执行此操作,尽管像往常一样,在修改正在运行的进程时从来都不是确定的事情。
评论
在内核模式下有...还是符合作弊条件? ;)
– 0xC0000022L♦
18年6月19日在12:06
#3 楼
这是有关Windows中如何实现热修补的较旧的文章。如果要绝对原子地完成它,则必须从内核模式完成。没有办法解决。这是演练:将线程的IRQL设置为
CLOCK1_LEVEL
(哎呀,您甚至可以尝试HIGH_LEVEL
),该值高于2,将几乎停止该系统中所有任务的切换补丁代码运行的时间。它也足够高,可以抢占大多数中断。或者,您可以尝试使用CLI
指令来禁用它们。 (对于多核CPU,这是必需的。)基本上可以将您的修补线程在短时间内转变为单线程环境。为确保在应用5字节JMP的那段短时间的可执行内存中没有其他正在运行的线程停止,请遍历每个线程的上下文并检查其RIP值。万一发生重叠,在极少数情况下,要么取消补丁,然后片刻后重试,要么将线程的IRQL升高一小段时间,然后将其降低。然后再次检查。重复N次,直到其RIP与修补程序不重叠。
最后应用修补程序。确保尽快进行。尽量不要调用任何外部函数。只需在准备好的内存blob上快速执行
REP MOVS
。您可能需要清除处理器的指令缓存。 (如果有补丁之前的旧代码。)
然后撤消上述所有步骤,将系统恢复到工作状态。
PS。应该在理论上起作用。在实践中,调试这将是一个活生生的麻烦。显然,它是在VM中完成的,并准备好重启(很多)。
编辑:这是一个实际的示例,取自DebugView工具。如果您知道它的作用,它将尝试捕获程序的调试器输出。如果在较旧的操作系统上启用内核调试器输出,则该工具没有其他选择,只能在DebugView启动时在实时系统的DbgPrint函数上安装蹦床。
这是它的工作方式(它使用了一些较旧的内核功能,但仍然可以实现这一点):获取CPU内核数(它使用较旧的
KeNumberProcessors
全局变量,例如):占用的CPU数大于32/64。)2.。通过调用
KeQueryActiveProcessors()
确保当前线程在CPU内核0上运行:(然后检查全局变量
KeQueryGroupAffinity()
,以防我们不需要设置此变量蹦床并通过跳到步骤6来恢复线程的亲和力。但是这种情况并不有趣。)3。然后检查是否只有一个CPU内核,如果是,则跳到步骤5。(也不太有趣。)否则,将全局变量
KeSetAffinityThread
设置为CPU内核数,并将全局bDontSet_FuncTrampoline
标志重置为0: br /> 4。为我们的
nCountCpuCores
的bAllowToContinue_CoreNon0_DeferredRoutine
DPC(递延过程调用)设置每个CPU内核(非0,即rsi
指针从索引1开始,或0x8字节偏移),并将high importance
设置为CPU内核编号: > 基本上,这将取代其他内核所做的一切,并将其定向到我们的DPC。
5。然后对于在核心0中运行的线程,执行DPC例程
DeferredRoutine
:6。之后,将该线程的亲和性恢复到以前的状态:
现在,有趣的是
context
中发生了什么: br />(请注意,此例程将在每个CPU内核上执行。)答:第一步,将线程的IRQL设置为
DeferredRoutine
(对于x64代码,则设置为DeferredRoutine
。)这是通过使用CLOCK_LEVEL
CPU寄存器来完成的。这样做将阻止大多数中断的处理:B。然后减少
13
全局变量中的计数器,但是使用cr8
CPU前缀来确保所有CPU内核之间的同步:C。检查此线程在哪个CPU内核上运行,并相应地进入自旋循环:
C.1。 (上面代码流中的右侧块。)对于非0内核,继续循环运行,而全局变量
nCountCpuCores
为0。C.2。 (上面代码流中的左侧块。)对于核心0,继续循环运行,而全局变量
lock
中已处理核心的数量未达到0。(我个人会添加每个循环都有一个
bAllowToContinue_CoreNon0_DeferredRoutine
指令,以确保CPU在“旋转”时不会浪费太多功率。)C.3。一旦条件C.2。已经满足,这意味着我们拥有自己的核心0,而所有其他核心都在C.1的循环中忙于旋转。然后我们可以调用
nCountCpuCores
函数来安装所需的蹦床。C.4。当我们完成蹦床操作后,请记住从C.1的自旋循环中释放所有核心。通过将
pause
设置为1. D。最后,非常重要,将IRQL恢复到以前的样子:
返回错误
install_func_Trampoline
代码(如果是)。否则,我们就完成了!
评论
查看本文;特别是在5.2捕获CPU以安全更新的部分中有趣,但我不知道在Windows上用于linux函数stop_machine的等效操作。
我认为动态代码检测框架(例如PIN,DynInst)可能会有所帮助,因为检测将从程序代码“透明地”实现。例如,有关DynInst用于运行时代码修补的API的论文说:“ ... API的设计目的是使单个检测过程可以将摘要插入到在一台计算机上执行的多个过程中。在这里,代码片段是注入的代码。