当前位置: 首页 > 技术干货 > 浅谈热补丁的钩取方式

浅谈热补丁的钩取方式

发表于:2024-06-25 11:19 作者: hope 阅读数(839人)

前言

热补丁的钩取方式是为了解决内联钩取在多线程情况下会出错的情况,使用热补丁的钩取可以避免重复读写指令造成问题。

内联钩取潜在问题

正常情况下,在每次跳转到自定义函数时需要将原始的指令(mov edi,edi)写回CreateProcessW函数内,为了后续正确调用CreateProcesW函数,在调用完毕之后,又需要进行挂钩的处理,即将mov指令修改为jmp指令。

image-20240508111729098

但是在多线程的情况下就可能会出现下列问题,在进行mov指令篡改时可能会发生线程的切换,因为篡改指令的操作不是原子操作。那么在线程2时可能调用了CreateProcessW函数时可能跳转指令还没写完成,例如下图的jmp 0x12xx,而不是原本的jmp 0x1234就导致了执行出错。

image-20240508112437672

为了解决此问题采用了热补丁钩取。

热补丁钩取

热补丁是指在不中断系统运行进行应用。即不中断程序运行也能够修改系统库或程序中的执行逻辑。

这里以CreateProcessW为例子

windbg中使用以下指令在CreateProcessW函数中打下断点

.reload /f
bp CreateProcessW

可以看到CreateProcessW函数入口点是mov edi,edi指令,而在该指令上方有一段没用用到的空间,在windbg中使用int 3指令填充了。

image-20240508134834761

mov edi,edi指令本身没有实际意义,这就是微软在系统库预留的空间,用于打上热补丁。因为这个指令无论被修改成什么都不会影响程序的执行。

接着可以发现这跳指令的长度为2字节,因此可以使用任意的2字节长的指令替换mov edi,edi

那么这里就需要寻找可以完成跳转的指令,并且仅占用2字节完成对mov指令的替换。

在汇编中存在着短跳转指令可以完成跳转并且仅占用2字节,用以下例子来观察一下短跳转的指令。

int main()
{
   // 使用标签作为跳转目标
   __asm {
       jmp short label;
  };

   // 标签处定义跳转目标
label:
   // 这里是跳转目标后的代码
   return 0;
}

可以看到在跳转到标签label上时,采用的跳转指令机器码是EB开头的,而不是E9,并且指令长度也只有2字节。

image-20240508135609706

那么00是跳转的偏移值,根据该例子分析一下跳转偏移的计算

跳转偏移 = 目标地址 - 当前地址 - 当前指令的长度
00     = 00731005 - 00731003 - 2

可以看到计算偏移的公式与jmp指令一致,只是跳转的指令的长度为5字节,而短跳转的指令长度为2字节,因此jmp指令也被称之为长跳转。

那么怎么配合短跳转进行一个钩取操作,如下图。我们可以借助短跳转使得指令执行到上述填充的区域,然后再使用jmp指令完成钩取的操作。这里需要注意的是空闲区域的空间大小需要大于5个字节,不然无法容纳jmp指令。

image-20240508140251069

最终修改后钩取的效果如下图,在自定义函数中不在需要钩取与脱钩的操作,因为我们修改的指令不会影响正常的CreateProcessW函数执行。那么在既然不存在写操作,那么在多线程中也不会因为条件竞争导致还没写完就切换线程的情况。

image-20240508140519160

那么代码实现部分如下,这里需要注意长跳转的指令0xE9,短跳转的指令为0xEB,这里先把偏移计算好了0xF9,因此写好了,但是这个偏移值不是唯一值,只要找到的地址存在大于5字节的空闲区域都是可以的。紧接着就是修改函数内部的指令,将初始的指令修改为短跳转,然后再空闲区中填充长跳转即可。

...
   //长跳转指令
   BYTE pBuf[5] = { 0xE9, 0 };
   //短跳转指令 + 偏移值
   BYTE pShortJmp[2] = { 0xEB, 0xF9};
   //获取模块地址
   HMODULE hModule = GetModuleHandleA(szDllName);
   //获取函数地址
   FARPROC pfnOld = GetProcAddress(hModule, szFuncName);
   //选中长跳转指令填充的地址,这里选择恰好能容纳jmp指令的位置
   DWORD target = (DWORD)pfnOld - 5;
   //计算跳转的偏移
   DWORD dwAddress = (DWORD)pfnNew - target - 5;
   //修改区域的权限
   VirtualProtect((LPVOID)target, 7, PAGE_EXECUTE_READWRITE, &dwOldProtect);
   //将偏移填充到指令中
   memcpy(&pBuf[1], &dwAddress, 4);
   //将长跳转指令填充
   memcpy((LPVOID)target, pBuf, 5);
   //保存原始的两个字节
   memcpy(pOldBytes, pfnOld, 2);
   //将短跳转指令填充
   memcpy(pfnOld, pShortJmp, 2);
   VirtualProtect((LPVOID)target, 7, dwOldProtect, &dwOldProtect);
...

在自定义函数中,只需要直接调用CreatePorcessW + 2的指令就可以完成原始CreateProcessW函数,不再需要挂钩脱钩的处理。

...
   //调用CreateProcessW + 2
BOOL ret = ((LPFN_CreateProcessW)((DWORD)pfnOld + 2))(
       applicationName,
       lpCommandLine,
       lpProcessAttributes,
       lpThreadAttributes,
       bInheritHandles,
       dwCreationFlags,
       lpEnvironment,
       lpCurrentDirectory,
       lpStartupInfo,
       lpProcessInformation
      );
...

完整代码:

https://github.com/h0pe-ay/HookTechnology/tree/main/Hook-HotPatch

总结

优点:避免多线程出错

缺点:不一定有热补丁的条件,就是不一定存在有垃圾指令

如64位程序的CreateProcessW函数的第一条指令是mov r11,rsp,但是后续的指令都需要用到r11寄存器的值,因此该指令不是无用指令。就不能上述热补丁的方法。

image-20240508142953237