一、什么是API Hook

    见下图所示,API Hook就是对API的正常调用起一个拦截或中间层的作用,这样可以
在调用正常的API之前得到控制权,执行自己的代码。其中Module指映射到内存中的可执
行文件或DLL。

      module0    module1
 |       |
CALL module1!API001 --------------------------------->| API001
 |<-------------------------------------------|
 |       |
  API215 |<----------------------------------CALL module0!API215
 |------------------------------------------->|
 |       |
 *       *
       vs.

      module0    Hooooks.dll    module1
 |        |       |
CALL module1!API001 -------->API001>----------------->| API001
 |<-----------------<HOOOOK<------------------|
 |        |       |
  API215 |<-----------------<API215<---------CALL module0!API215
 |------------------>HOOOOK>----------------->|
 |        |       |
 *        *       *


二、API Hook的原理
   
    这里的API既包括传统的Win32 APIs,也包括任何Module输出的函数调用。熟悉PE文件格
式的朋友都知道,PE文件将对外部Module输出函数的调用信息保存在输入表中,即.idata段。
下面首先介绍本段的结构。
    输入表首先以一个IMAGE_IMPORT_DESCRIPTOR(简称IID)数组开始。每个被PE文件隐式链接
进来的DLL都有一个IID.在这个数组中的最后一个单元是NULL,可以由此计算出该数组的项数。
例如,某个PE文件从两个DLL中引入函数,就存在两个IID结构来描述这些DLL文件,并在两个
IID结构的最后由一个内容全为0的IID结构作为结束。几个结构定义如下:

IMAGE_IMPORT_DESCRIPTOR struct
union{
DWORD Characteristics; ;00h
DWORD OriginalFirstThunk;
};
TimeDateStamp DWORD ;04h
ForwarderChain DWORD ;08h
Name DWORD ;0Ch
FirstThunk DWORD ;10h
IMAGE_IMPROT_DESCRIPTOR ends

typedef struct _IMAGE_THUNK_DATA{
union{
PBYTE ForwarderString;
PDWORD Functions;
DWORD Ordinal;
PIMAGE_IMPORT_BY_NAME AddressOfData;
}u1;
}

IMAGE_IMPORT_BY_NAME结构保存一个输入函数的相关信息:
IMAGE_IMPORT_BY_NAME struct
Hint WORD ? ;本函数在其所驻留DLL的输出表中的序号
Name BYTE ? ;输入函数的函数名,以NULL结尾的ASCII字符串
IMAGE_IMPORT_BY_NAME ends

OriginalFirstThunk(Characteristics):这是一个IMAGE_THUNK_DATA数组的RVA(相对于PE文件
起始处)。其中每个指针都指向IMAGE_IMPORT_BY_NAME结构。
TimeDateStamp:一个32位的时间标志,可以忽略。
ForwarderChain:正向链接索引,一般为0。当程序引用一个DLL中的API,而这个API又引用别的
DLL的API时使用。
NameLL名字的指针。是个以00结尾的ASCII字符的RVA地址,如"KERNEL32.DLL"。
FirstThunk:通常也是一个IMAGE_THUNK_DATA数组的RVA。如果不是一个指针,它就是该功能在
DLL中的序号。
OriginalFirstThunk与FirstThunk指向两个本质相同的数组IMAGE_THUNK_DATA,但名称不同,
分别是输入名称表(Import Name Table,INT)和输入地址表(Import Address Table,IAT)。
IMAGE_THUNK_DATA结构是个双字,在不同时刻有不同的含义,当双字最高位为1时,表示函数以
序号输入,低位就是函数序号。当双字最高位为0时,表示函数以字符串类型的函数名
方式输入,这时它是指向IMAGE_IMPORT_BY_NAME结构的RVA。
三个结构关系如下图:

IMAGE_IMPORT_DESCRIPTOR          INT          IMAGE_IMPORT_BY_NAME    IAT
 --------------------    /-->----------------     ----------     ---------------- <--\
| OriginalFirstThunk |--/   |IMAGE_THUNK_DATA|-->|01| 函数1 |<--|IMAGE_THUNK_DATA|   |
|--------------------|      |----------------|   |----------|   |----------------|   |
|    TimeDateStamp   |      |IMAGE_THUNK_DATA|-->|02| 函数2 |<--|IMAGE_THUNK_DATA|   |
|--------------------|      |----------------|   |----------|   |----------------|   |
|    ForwarderChain  |      |      ...       |-->| n|  ...  |<--|      ...       |   |
|--------------------|       ----------------     ----------     ----------------    |
|        Name        |------>"USER32.dll"                                            |
|--------------------|                                                               |
|      FirstThunk    |---------------------------------------------------------------/
 --------------------

    在PE文件中对DLL输出函数的调用,主要以这种形式出现:
call dword ptr[xxxxxxxx] 或
jmp [xxxxxxxx]
其中地址xxxxxxxx就是IAT中一个IMAGE_THUNK_DATA结构的地址,[xxxxxxxx]取值为IMAGE_THUNK_DATA
的值,即IMAGE_IMPORT_BY_NAME的地址。在操作系统加载PE文件的过程中,通过IID中的Name加载相应
的DLL,然后根据INT或IAT所指向的IMAGE_IMPORT_BY_NAME中的输入函数信息,在DLL中确定函数地址,
然后将函数地址写到IAT中,此时IAT将不再指向IMAGE_IMPORT_BY_NAME数组。这样[xxxxxxxx]取到的
就是真正的API地址。
    从以上分析可以看出,要拦截API的调用,可以通过改写IAT来实现,将自己函数的地址写到IAT中,
达到拦截目的。

    另外一种方法的原理更简单,也更直接。我们不是要拦截吗,先在内存中定位要拦截的API的地址,
然后改写代码的前几个字节为 jmp xxxxxxxx,其中xxxxxxxx为我们的API的地址。这样对欲拦截API的
调用实际上就跳转到了咱们的API调用去了,完成了拦截。不拦截时,再改写回来就是了。


三、实现前的准备

    两种拦截方法,最终目的都是使程序对欲拦截API的调用跳转到自己的API。所以我们的API代码对
欲拦截进程必须是可见的,即我们的代码要映射到欲拦截进程的地址空间中。

    在《隐藏进程》一文中我介绍了远程线程注入代码的技术,这里我们可以采用这种方法向欲拦截进
程中注入我们的API代码。同样有两种注入方式,一种是,直接将代码WriteProcessMemory到欲拦截进
程中,写入的代码包括我们的API代码和远程线程的入口函数代码。这种的缺点是有一些细节问题要解
决,如参数传递、写入代码大小的确定等并且由于多了一个远程线程效率不是很高,如果要拦截所有的
进程,即必须在每个进程中注入代码、插入线程。另一种是,注入DLL,远程线程入口函数为LoadLirary,
当然也存在效率的问题,但免去了一些麻烦。

    这里我们主要介绍通过设置钩子(Hook)来自动注入DLL到欲拦截进程。先简单说明一下钩子是怎么回事。

    Hook指出了系统消息处理机制。利用Hook,可以在应用程序中安装子程序监视系统和进程之间的消息
传递,这个监视过程是在消息到达目的窗口过程之前。系统支持很多不同类型的Hooks,不同的hook提供不
同的消息处理机制。比如,应用程序可以使用WH_MOUSE_hook来监视鼠标消息的传递。系统为不同类型的
Hook提供单独的Hook链。Hook链是一个指针列表,这个列表的指针指向指定的,应用程序定义的,被hook
过程调用的回调函数。当与指定的Hook类型关联的消息发生时,系统就把这个消息传递到Hook过程。一些
Hook过程可以只监视消息,或者修改消息,或者停止消息的前进,避免这些消息传递到下一个Hook过程或
者目的窗口。
    为了利用特殊的Hook类型,可由开发者提供了Hook过程,使用SetWindowsHookEx函数来把Hook过程安
装到关联的Hook链。Hook过程必须按照以下的语法:
    LRESULT CALLBACK HookProc(int nCode, WPARAM wParam, LPARAM lParam);
    HookProc是应用程序定义的名字,nCode参数是Hook代码,Hook过程使用这个参数来确定任务。这个参
数的值依赖于Hook类型,每一种Hook都有自己的Hook代码特征字符集。wParam和lParam参数的值依赖于
Hook代码,但是它们的典型值是包含了关于发送或者接收消息的信息。
    SetWindowsHookEx函数总是在Hook链的开头安装Hook过程。当指定类型的Hook监视的事件发生时,系统
就调用与这个Hook关联的Hook链的开头的Hook过程。每一个Hook链中的Hook过程都决定是否把这个事件传递
到下一个Hook过程。Hook过程传递事件到下一个Hook过程需要调用CallNextHookEx函数。有些类型Hook的
Hook过程只能监视消息,不管是否调用了CallNextHookEx函数,系统都把消息传递到每一个Hook过程。全局
hook监视同一桌面的所有线程。而特定线程的Hook只能监视单独的线程。全局Hook过程可以被同一桌面的任
何应用程序调用,就象调用线程一样,所以这个过程必须和DLL模块分开。特定线程Hook过程只可以被相关
线程调用。只有在有调试目的的时候才使用全局Hook,应该避免使用,全局Hook损害了系统性能。

    本文使用全局的WH_GETMESSAGE Hook,它也是经常用到的Hook,应用程序使用WH_GETMESSAGE Hook来监
视从GetMessage or PeekMessage函数返回的消息。你可以使用WH_GETMESSAGE Hook去监视鼠标和键盘输入,
以及其他发送到消息队列中的消息。关于Hook的详细信息请参考MSDN。

    使用SetWindowsHookEx设置全局的WH_GETMESSAGE Hook,传入DLL的映射到内存时的模块句柄(HANDLE)
和Hook过程,这样系统不但会将此DLL映射到当前所有进程的地址空间,并调用DllMain函数,而且也会将
此DLL映射到新创建的进程的地址空间了。也就是自动完成了代码的注入工作,省了很多力气,调用
UnhookWindowsHookEx卸载钩子。


四、具体实现

    两种实现模式:一是由一个第三方进程负责钩子的设置和卸载,DLL导出设置和卸载函数;二是由一个
第三方进程向某一个进程插入远程线程、注入DLL,然后由DLL负责钩子的设置和卸载,第三方进程退出。

    两种模型的DLL实现差别不大,封装了钩子设置和卸载函数,自己的API的函数等。

    先说改写IAT方法:
定义一个保存拦截信息的结构APIHOOK32_ENTRY:
typedef struct _APIHOOK32_ENTRY
{
LPCTSTR     pszAPIName; //欲拦截API的函数名
LPCTSTR     pszCalleeModuleName;//API所在模块的模块名
PROC        pfnOriginApiAddress;//欲拦截API的函数地址
PROC        pfnDummyFuncAddress;//我们自己的API的函数地址
HMODULE     hModCallerModule; //调用此API的模块名
}APIHOOK32_ENTRY, *PAPIHOOK32_ENTRY;

/////////////////////////////////////////////////////////////////////////////////////////
#include "stdafx.h"
#include "apihook32.h"

HMODULE hModDLL;
HHOOK hHook;
APIHOOK32_ENTRY hkA;
//钩子过程,直接调用CallNextHookEx,而不做任何处理
//因为我们只是利用设置钩子来映射DLL
LRESULT CALLBACK GetMsgProc(int code,WPARAM wParam,LPARAM lParam)
{
return CallNextHookEx(hHook,code,wParam,lParam);
}
//我们自己的API函数
int WINAPI MyMessageBoxA(HWND hwnd,LPCSTR lpText,LPCSTR lpCaption,UINT uType)
{
return MessageBoxA(hwnd,"It's coming from MyMessageBoxA",lpCaption,uType);
}
//设置全局钩子,自动映射DLL到当前所有进程和新创建的进程
HHOOK InsertDll ()
{
hHook = SetWindowsHookEx(WH_GETMESSAGE,&GetMsgProc,hModDLL,0);
return hHook;
}

BOOL APIENTRY DllMain(HANDLE hModule,
  DWORD  ul_reason_for_call,
  LPVOID lpReserved
 
{
hModDLL = (HMODULE)hModule;//在32位windows系统中,DLL的hModule和hHandle是一回事

hkA.hModCallerModule = NULL;
hkA.pszAPIName = "MessageBoxA"; //拦截user32.dll中的MessageBoxA函数
hkA.pszCalleeModuleName = "user32.dll";
hkA.pfnDummyFuncAddress = (PROC) & MyMessageBoxA;
hkA.pfnOriginApiAddress = GetProcAddress(GetModuleHandle("user32.dll","MessageBoxA";

switch(ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
// InsertDll();
SetWindowsAPIHook(&hkW);
SetWindowsAPIHook(&hkA);
break;
case DLL_THREAD_ATTACH:
break;
case DLL_PROCESS_DETACH:
UnhookWindowsAPIHooks(hkW);
UnhookWindowsAPIHooks(hkA);
// UnhookWindowsHookEx(hHook);
break;
case DLL_THREAD_DETACH:
break;
}
return TRUE;

}

/////////////////////////////////////////////////////////////////////////////////////////
//有了一上对PE文件输入表的分析,我们将可以很好的理解下面的代码
void _SetApiHookUp(PAPIHOOK32_ENTRY phk)
{
ULONG size;

//获取指向PE文件中的Import表中IMAGE_DIRECTORY_DESCRIPTOR数组的指针

PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)
ImageDirectoryEntryToData(phk->hModCallerModule,TRUE,IMAGE_DIRECTORY_ENTRY_IMPORT,&size);

if (pImportDesc == NULL)
return;

//查找记录,看看有没有我们想要的DLL
//pImportDesc->Name为空说明IID数组结束
for (;pImportDesc->Name;pImportDesc++)
{
//pImportDesc->Name是DLL名字字符串的RVA,加上Module的基址获得有效指针
LPSTR pszDllName = (LPSTR)((PBYTE)phk->hModCallerModule+pImportDesc->Name);
if (lstrcmpiA(pszDllName,phk->pszCalleeModuleName) == 0)
break;
}

if (pImportDesc->Name == NULL)
{
return;
}

//寻找我们想要的函数
//首先获得IID的FirstThunk指向的IAT
PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA) ((PBYTE)phk->hModCallerModule+pImportDesc->FirstThunk);
for (;pThunk->u1.Function;pThunk++)
{
//ppfn记录了与IAT表项相应的函数的地址

PROC * ppfn= (PROC *)&pThunk->u1.Function;
if (*ppfn == phk->pfnOriginApiAddress)
{
//如果地址相同,也就是找到了我们想要的函数,进行改写,将其指向我们所定义的函数

WriteProcessMemory(GetCurrentProcess(),ppfn,&(phk->pfnDummyFuncAddress),sizeof(phk->pfnDummyFuncAddress),NULL);
return;
}
}
}

//***************************************************************************************//
//        SetWindowsAPIHook    挂接WindowsAPI函数  当phk->hModCallerModule == NULL       //
//                                                 会在整个系统内挂接函数                //
//                             仿照SetWindowsHookEx 建立                                 //
//***************************************************************************************//

BOOL SetWindowsAPIHook(PAPIHOOK32_ENTRY phk)
{
if (phk->pszAPIName == NULL)
{
return FALSE;
}
if (phk->pszCalleeModuleName == NULL)
{
return FALSE;
}
if (phk->pfnOriginApiAddress == NULL)
{
return FALSE;
}
if (phk->hModCallerModule == NULL)
{
MEMORY_BASIC_INFORMATION mInfo;
HMODULE hModHookDLL;
HANDLE hSnapshot;
MODULEENTRY32 me = {sizeof(MODULEENTRY32)};
//根据_SetApiHookUp函数在内存中的位置,确定DLL的映射基址,即hModule
VirtualQuery(_SetApiHookUp,&mInfo,sizeof(mInfo));
hModHookDLL=(HMODULE)mInfo.AllocationBase;
//遍历本进程中的所有Module,除了本DLL模块
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE,0);
BOOL bOk = Module32First(hSnapshot,&me);
while (bOk)
{
if (me.hModule!=hModHookDLL)
{
phk->hModCallerModule = me.hModule;
_SetApiHookUp(phk);
}
bOk = Module32Next(hSnapshot,&me);
}
return TRUE;
}
else
{
_SetApiHookUp(phk);
return TRUE;
}
return FALSE;
}
//拦截某个某块的API调用
void SetMyHooksHere(APIHOOK32_ENTRY hk,HMODULE hMod)
{
hk.hModCallerModule = hMod;
_SetApiHookUp(&hk);
}
//卸载API Hook,就是往IAT中写入原地址
BOOL UnhookWindowsAPIHooks(APIHOOK32_ENTRY & hk)
{
PROC temp;
temp = hk.pfnOriginApiAddress;
hk.pfnOriginApiAddress = hk.pfnDummyFuncAddress;
hk.pfnDummyFuncAddress = temp;
return SetWindowsAPIHook(&hk);
}
///////////////////////////////////////////////////////////////////////////////////////
第三方进程调用InsertDll注入DLL(设置钩子),调用UnhookWindowsHookEx取消拦截。


下面简单介绍jmp xxxxxxxx方法拦截API:
拦截user32.dll中的MessageBoxA函数
FARPROC g_pfMessageBoxA = NULL;
BYTE g_OldMessageBoxACode[5] = {0}, g_NewMessageBoxACode[5] = {0};
DWORD g_dwNewProcessId = 0, g_dwOldProcessId = 0;
BOOL g_bHook = FALSE;
HMODULE g_hDll = FALSE;
HHOOK g_hHook = NULL;

BOOL WINAPI Initialize();
BOOL WINAPI MyMessageBoxA(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType);
void WINAPI HookOn();
void WINAPI HookOff();

LRESULT WINAPI Hook(int nCode, WPARAM wParam, LPARAM lParam);
BOOL WINAPI InstallHook();
BOOL WINAPI UnInstallHook();

BOOL APIENTRY DllMain( HANDLE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
 
{
//char szProcessID[64];
    switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
// _itoa(GetCurrentProcessId(), szProcessID, 10);
// MessageBox(NULL, szProcessID, "Remote Dll", MB_OK);
if(!g_bHook)
{
g_hDll = (HMODULE)hModule;
InstallHook();
Initialize();
// MessageBox(NULL, "Succeeded!", "Hook On", MB_OK);
}
//
MessageBox(NULL, "Process Attach", "Remote Dll", MB_OK);
break;
case DLL_THREAD_ATTACH:
// _itoa(GetCurrentProcessId(), szProcessID, 10);
// MessageBox(NULL, szProcessID, "Remote Dll", MB_OK);
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
// if(g_bHook)
// {
// HookOff();
// MessageBox(NULL, "Off!", "Hook Off", MB_OK);
// UnInstallHook();
// }
// break;
MessageBox(NULL, "Process Detach", "Remote Dll", MB_OK);
break;
    }
    return TRUE;
}

//获得MessageBoxA的地址,然后保存代码起始处的5个字节,并生成跳转代码jmp xxxxxxxx
BOOL WINAPI Initialize()
{
HMODULE hDll = LoadLibrary("user32.dll";
g_pfMessageBoxA = GetProcAddress(hDll, "MessageBoxA";//获得MessageBoxA地址
if(g_pfMessageBoxA == NULL)
return FALSE;
_asm
{
lea edi, g_OldMessageBoxACode
mov esi, g_pfMessageBoxA
cld
movsd //将MessageBoxA地址起始的4个字节(dword)写入g_OldMessageBoxACode
movsb //将MessageBoxA+4地址起始处的1个字节(byte)写入g_OldMessageBoxACode+4
}
//jmp xxxxxxxx的机器码为e9xxxxxxxx,其中e9后的xxxxxxxx为相对跳转偏移,共5个字节
g_NewMessageBoxACode[0] = 0xe9;
_asm
{
lea eax, MyMessageBoxA
mov ebx, g_pfMessageBoxA
sub eax, ebx
sub eax, 5 //获得相对跳转偏移
mov dword ptr [g_NewMessageBoxACode + 1], eax
}

g_dwNewProcessId = GetCurrentProcessId();
g_dwOldProcessId = g_dwNewProcessId;

HookOn();

return TRUE;
}

int WINAPI MyMessageBoxA(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType)
{
int nRet = 0;
char szText[128];
strcpy(szText, lpText);
strcat(szText, "\nYou have been Hooked!";
HookOff();
nRet = MessageBoxA(hWnd, szText, lpCaption, uType);
HookOn();

return nRet;
}

void WINAPI HookOn()
{
g_dwOldProcessId = g_dwNewProcessId;
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, g_dwOldProcessId);
if(hProcess == NULL)
return ;
//申请MessageBoxA地址处的写权限,然后写入跳转代码,然后恢复权限
VirtualProtectEx(hProcess, g_pfMessageBoxA, 5, PAGE_READWRITE, &g_dwOldProcessId);
WriteProcessMemory(hProcess, g_pfMessageBoxA, g_NewMessageBoxACode, 5, NULL);
VirtualProtectEx(hProcess, g_pfMessageBoxA, 5, g_dwOldProcessId, &g_dwOldProcessId);

g_bHook = TRUE;
}

void WINAPI HookOff()
{
g_dwOldProcessId = g_dwNewProcessId;
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, g_dwOldProcessId);
if(hProcess == NULL)
return ;
//写入原MessageBoxA的5个字节代码
VirtualProtectEx(hProcess, g_pfMessageBoxA, 5, PAGE_READWRITE, &g_dwOldProcessId);
WriteProcessMemory(hProcess, g_pfMessageBoxA, g_OldMessageBoxACode, 5, NULL);
VirtualProtectEx(hProcess, g_pfMessageBoxA, 5, g_dwOldProcessId, &g_dwOldProcessId);

g_bHook = FALSE;
}

LRESULT WINAPI Hook(int nCode, WPARAM wParam, LPARAM lParam)
{
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}

BOOL WINAPI InstallHook()
{
g_hHook = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)Hook, g_hDll, 0);
if(!g_hHook)
{
MessageBoxA(NULL, "Set Error!", "ERROR", MB_OK);
return FALSE;
}
return TRUE;
}

BOOL WINAPI UnInstallHook()
{
return UnhookWindowsHookEx(g_hHook);
}

如果让DLL来负责钩子的设置和卸载,就必须设置映射到所有进程的DLL共享的数据段,因为DLL中的
全局数据,每个映射的DLL副本都有自己的副本,互不干扰。为了同步钩子的设置和卸载工作,我们
可以在DLL中设置一个共享段,如下:

#pragma data_seg("shared" //定义段名为shared
BOOL g_bHooked = FALSE;
DWORD g_dwParentProcessID = 0;
//……
#pragma data_seg()
#pragma commnet(lib, "/Section:shared, rws";//设置段属性为read,write and shared

这个共享段在所有的DLL映射副本中共享,完成同步工作。
第三方进程向某个进程中注入远程线程,远程线程注入DLL,然后第三方进程退出。同步代码如下:
BOOL APIENTRY DllMain( HANDLE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
 
{
DWORD dwProcessId = GetCurrentProcessId();
    switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
if(!g_bHooked)
{
g_bHooked = TRUE;
g_dw_ParentProcessID = dwProcessId;
...//在这里设置钩子,hMoudle即为DLL的Handle
}
...//在这里完成API拦截工作
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
...//在这里完成API的恢复工作
if(g_bHooked && (g_dwParentProcessID == dwProcessId))
{
g_bHooked = FALSE;
g_dwParentProcessID = 0;
...//在这里卸载钩子
}
break;
    }
    return TRUE;
}

以上关于远程线程注入技术请参考我的另一篇《隐藏进程》的文章,以上代码在Win2k Professional+SP2
+Visual C++6.0上测试通过,API的具体参数请参考MSDN。

    上面的代码大多是自己机子上的一些零碎代码,加上自己的分析和实际的应用调试,一直找不到方法
卸载DLL,无论是用设置钩子注入DLL,还是用注入远程线程的方法注入DLL。