有关于多核下安全地 N 字节 Patch 内核的问题。我们都知道 Patch 是有风险的,可有时候我们又不得不冒着风险。我想知道究竟这个风险有多大,又分为哪些情况,它们可不可以尽量避免,如何避免。请您谈谈您对此问题的理解,越深刻越好,越详细越好,谢谢您。为了不涉及公司的代码利益,我自主编写了我的 Patch 引擎,做法如下图(恕我不过多涉及细节):简言之 即其它核空转(DISPATCH_LEVEL),本核操作(DISPATCH_LEVEL),本核完成(PASSIVE_LEVEL),其它核继续工作。请您参考讨论。
上面的贴是2:06 AM发出的,王宇勤奋呀!天道酬勤,我看到了中国的Alex在崛起^_^很喜欢王宇的这个提议。这个问题虽然很久以前就思考过,但是始终没有深究。
先抛块砖吧:
首先,下面这篇来自微软官方的公开paper介绍了Windows系统中hotpatch的基础,没有读过的朋友不妨先读一读。
http://technet.microsoft.com/en-us/library/cc787843.aspx
第二,在NT内核中有一系列包含hotpatch的函数,虽然没有仔细考察每一个,但想必与hotpatch有关。
lkd> x nt!*hotpatch*805df092 nt!RtlFreeHotPatchData = <no type information>805ae9e0 nt!MmHotPatchRoutine = <no type information>80552fe8 nt!MiHotPatchList = <no type information>805ae84a nt!MiPerformHotPatch = <no type information>805dfd6c nt!RtlInitializeHotPatch = <no type information>805dfdf6 nt!RtlCreateHotPatch = <no type information>805defb4 nt!RtlpFreeHotpatchMemory = <no type information>805df022 nt!RtlGetHotpatchHeader = <no type information>805def90 nt!RtlpAllocateHotpatchMemory = <no type information>805ae80a nt!MiRundownHotpatchList = <no type information>
第三,大多数内核服务的入口函数的开头两个字节都是mov edi,edi,这个是为hotpatch准备的。
例如:
lkd> u nt!NtCreateFilent!NtCreateFile:8056d62e 8bff mov edi,edi8056d630 55 push ebp8056d631 8bec mov ebp,esp
注意,上面的mov edi,edi指令的起始地址是xxxxxxxe,这不是一个按四字节对齐的地址,但是它的前两个字节通常是无用的INT 3指令:
lkd> u 8056d62c nt!NtRemoveIoCompletion+0x1a8:8056d62c cc int 38056d62d cc int 3nt!NtCreateFile:8056d62e 8bff mov edi,edi8056d630 55 push ebp8056d631 8bec mov ebp,esp8056d633 33c0 xor eax,eax8056d635 50 push eax8056d636 50 push eax
注意,8056d62c是一个按四字节对齐的地址,这样,hotpatch时,就可以利用32位CPU的一条指令以原子方式(LOCK前缀)把这里的指令替换掉,不需要任何软件方面的锁。因为这个原因,hotpatch时把要patch函数的起始指令替换为新指令是很安全的,不管是单核还是多核。
但是!NtReadFile的开头几个字节不是:
lkd> u nt!NtReadFilent!NtReadFile:80570afa 6a68 push 68h80570afc 68908a4d80 push offset nt!GUID_DOCK_INTERFACE+0x374 (804d8a90)80570b01 e89a69fcff call nt!_SEH_prolog (805374a0)80570b06 33f6 xor esi,esi
最后,nt!MiPerformHotPatch函数应该是实际进行hotpatch的内核函数。
上一个帖子是有些关键点没能点明。王宇的Patch N字节是个大题目。实际操作中,一种可行的办法(Nt系统实用的)是把新的函数(A')复制到要修补模块的地址空间中,因为这时不会有人调用A',所以根本不用考虑多核和并发的问题。
接下来需要把原来函数(A)的入口点Patch进一条跳转指令。因为原函数是在使用的,可能有CPU正在执行或者即将执行它,所以这时就要格外注意并发和多核问题了。如何注意呢?干净利索的办法就是我说的四字节对齐方法。以原子方式写入一条跳转指令,那么新的调用便会被导向到新的函数A'。
因为在今天广泛使用的平坦地址模型下,使用只有两个字节的近跳转指令就可以跳到新的函数了。所以,要做的就是把两个字节的跳转指令和两个补位用的INT 3(或者NOP)合成四个字节,然后写过去。
离 Alex 的水平还差十万八千里... >_<我提出讨论的是一个大的问题,我先抛块砖。(恕我不会过多谈论代码,我只发两张截图)首先,我并不关心 Hook 的细节。nightxie 所说的“短跳+长跳”法,或者是常见的前 5 字节 Inline Hook,再或者是微点爱用的 Call Hook 都只是“论术”,OK 我们不回避,先“论术”:在我的 Hook 引擎DEMO 里可以自动选择 Hook 点(默认是 Call Hook),上述的都支持:/* ff15 Call Hook *//* 短跳 + 长跳 Hook */但是,这些伎俩有一个使用前提,就是如何 Patch 才是安全的? 这是“论道”:
问题可以分两类:01. 你在 Ptach 的同时(或间隙),有别的线程(本核上的,其它核上的)也执行过来;02. 你在 Ptach 之前,已经有线程执行在你要 ptach 的长度里(假设你要 Patch 100 字节,该线程执行在这之间),然后系统线程调度到了你这,你开始强制修改。
对于单核来说,我们可以选择关中断,或者提升 IRQL 保证线程不被切换以解决问题一。可是对于多核来说,这些做法无法控制全部的CPU。有些人想到了自旋锁,但是,系统根本不和你自旋。
DPC+空转法似乎成了唯一解,它可以最终解决问题一。
有人会说没必要,因为 Intel 有 F0 指令前缀 (lock prefix),但是锁总线也是有应用前提的:
• Reading or writing a byte• Reading or writing a word aligned on a 16-bit boundary• Reading or writing a doubleword aligned on a 32-bit boundary
显而易见的前提就是对齐 + 长度限制,试想我如果 Patch 5 字节、11 字节、103 字节 或是 N 字节,我该怎么用 F0 前缀呢?它没法保证原子性一次性操作那么多字节(且不一定对齐)。
反过来,我们可以思考为什么 N 字节 Patch 就是不安全的?(问题二)
或许是有一个线程A正好执行在 5 字节 或 103 字节中,此时,系统强制发生了线程切换,它的执行环境被保存,然后系统调度到了我这,我疯狂并开心地改写了这 103 字节,等到线程A再次获得执行时,它会发现——世界变了——随即 BSOD。
接着我又想到去扫描全部的系统线程,分析他们的执行点,但问题更多——如果这103字节里有 call、有等待的话.....
或许还有什么别的情况等或解决方法您补充...
如果上述假设成立,那么只能说明 多指令多字节 Patch 问题是无解的,而我们平时之所以不发生问题只能说明这是一个类似于10亿分之一的小概率事件问题?