WINDOWS上的 API HOOK 技术
预备知识
- 每个进程都拥有独立的地址空间
- dll动态链接库是所有进程共享的,但是需要注意,这里有个前提。前提是,所有的进程都不会去修改dll的内容,如果有进程A修改了dll的内容,则该dll就变成了进程A私有的了(系统使用copy-on-write方法,复制一份dll到进程A的私有地址空间,然后修改dll内容)。试想,如果任何情况都是共享的,一个进程只需要自己加载的dll中的内容,那么其余的所有使用了该dll的进程都会受到影响。
- 如果进程A加载的dll和进程B加载的dll是同一个dll(不是同一个的拷贝,必须是同一个路径下的同一个dll),那么dll加载后映射到进程A地址空间和进程B地址空间的虚拟地址是相同的
API HOOK
API HOOK 有很多方式,本文只介绍最常用的dll注入方式进程API HOOK。
dll注入方式大体分为两步:1、dll注入,2、修改API入口
dll注入
假设注入进程为A,被注入的进程为B,注入的dll名为virus.dll。则进程A注入dll到进程B的大体步骤如下
- 调整当前进程(A进程)权限
if ( OpenProcessToken(GetCurrentProcess(),TOKEN_ADJUST_PRIVILEGES,&hToken) ) { TOKEN_PRIVILEGES tkp; LookupPrivilegeValue( NULL,SE_DEBUG_NAME,&tkp.Privileges[0].Luid );//修改进程权限 tkp.PrivilegeCount=1; tkp.Privileges[0].Attributes=SE_PRIVILEGE_ENABLED; AdjustTokenPrivileges( hToken,FALSE,&tkp,sizeof tkp,NULL,NULL );//通知系统修改进程权限 CloseHandle(hToken); }
- 打开B进程
OpenProcess( PROCESS_CREATE_THREAD | //允许远程创建线程 PROCESS_VM_OPERATION | //允许远程VM操作 PROCESS_VM_WRITE| //允许远程VM写 PROCESS_ALL_ACCESS, FALSE, dwRemoteProcessId ) )
- 在B进程中申请一块内存,申请的区域假设为REDATA
pszLibFileRemote = (char *) VirtualAllocEx( hRemoteProcess, NULL, lstrlen(DllFullPath)+1,MEM_COMMIT, PAGE_READWRITE);
- 将virus.dll的路径写入到REDATA区域
WriteProcessMemory(hRemoteProcess,pszLibFileRemote, (void *) DllFullPath, lstrlen(DllFullPath)+1, NULL)
- 计算Kernel32.dll中的LoadLibraryA API的地址,记为pfnStartAddr
PTHREAD_START_ROUTINE pfnStartAddr = (PTHREAD_START_ROUTINE) GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryA");
- 创建远程线程,传入pfnStartAddr的地址作为线程执行的函数,REDATA的地址作为参数
hRemoteThread = CreateRemoteThread( hRemoteProcess, NULL, 0, pfnStartAddr, pszLibFileRemote, 0, NULL)
经过了上述步骤,virus.dll就能被成功注入到进程B的地址空间中,上面的步骤就是为了干一件事情,让B进程调用LoadLibraryA("virus.dll"),为了做这件事情需要上述6个步骤。,此处需要有几点说明:
- 提权是为了有权限访问远程线程,读写远程线程
- 为什么要申请空间,然后将virus.dll路径写入到B进程空间中,不能直接传一个dll路径的字符串呢?因为远程空间调用LoadLibraryA时,需要读取参数,A进程中写的dll路径的字符串存储在A进程的地址空间中,A把这个地址告诉B,B在自己的进程空间中找这个地址是找不到内容的,所以,需要将申请的空间地址告诉B。
- 你要问了,既然路径的地址需要B进程内存空间的地址,pfnStartAddr的地址是A进程空间相对位置,这个地址直接给B有问题吗?问得好,这个是没有问题的,这里有一个编程经验:
a、任何应用进程将Kerner32.dll加载到内存的虚拟地址位置都是一样 b、dll中的方法在dll中的相对位置是固定的
所以,A进程找到的函数地址位置和在B进程中的位置是一样的。
至此,我们将virus.dll加载到了B进程中,接下来就是B进程中的virus.dll怎么对自己进程空间中的API函数做HOOK的问题了。这里涉及到两个问题:
- 什么时候执行API HOOK这件事情
- API HOOK之后,什么时候可以执行HOOK之后的内容(不知道你们听懂我在说什么没有。。)
修改API入口代码
我们先看怎么修改API函数的入口代码,再讨论执行时机的问题。这件事情是在virus.dll中做的,执行的步骤如下
- 获取API函数所在的模块句柄
HMODULE hmod=::LoadLibrary(_TEXT("add.dll"));
- 获取该模块中API函数的地址
add=(AddProc)::GetProcAddress(hmod,"add");
- 修改API函数的汇编代码,让他跳转到我们自己写的函数(这个函数才是真正的干坏事的地方)
// 将add()的入口代码保存到OldCode里 _asm { lea edi,OldCode mov esi,pfadd cld movsd movsb } NewCode[0]=0xe9;//第一个字节0xe9相当于jmp指令 //获取Myadd()的相对地址 _asm { lea eax,Myadd mov ebx,pfadd sub eax,ebx sub eax,5 mov dword ptr [NewCode+1],eax } //填充完毕,现在NewCode[]里面就相当于指令 jmp Myadd HookOn();
void HookOn() { ASSERT(hProcess!=NULL); DWORD dwTemp=0; DWORD dwOldProtect; //将内存保护模式改为可写,老模式保存入dwOldProtect VirtualProtectEx(hProcess,pfadd,5,PAGE_READWRITE,&dwOldProtect); //将所属进程中add的前5个字节改为Jmp Myadd WriteProcessMemory(hProcess,pfadd,NewCode,5,0); //将内存保护模式改回为dwOldProtect VirtualProtectEx(hProcess,pfadd,5,dwOldProtect,&dwTemp); bHook=true; }
这段代码将add函数(API函数)的前5个字节存储到OldCode里面(为了以后恢复add函数),然后生成一个jmp指令到NewCode中,都存储好之后,NewCode的5个字节的内容,替换到原来add函数入口处的5个字节内容。
注意几个问题:
- 为什么是5个字节?
因为jmp XXX 指令占用5个字节(32位体系结构中)
- jmp XXX 中的跳转目标是计算的
xxx = Myadd - pfadd - 5
为什么是这个公式呢,先引用《深入理解计算机系统-第三版》中一段话:
当CPU计算跳转指令的目标地址时,程序计数器的值是当前指令的后一条指令的地址,而不是跳转指令的地址举个例子
pfadd: 01FF0000 : ?? ?? ?? ?? 01FF0004 : ?? ?? ?? ?? 01FF0008 : ?? ?? ?? ?? ... Myadd: 01FF00A0 : ?? ?? ?? ?? 01FF00A4 : ?? ?? ?? ?? 01FF00A8 : ?? ?? ?? ?? ... relpalce the first 5 bytes pfadd as jmp(E9 xx xx xx xx),the content of pfadd: pfadd: 01FF0000 : E9 xx xx xx 01FF0004 : xx ?? ?? ?? 01FF0008 : ?? ?? ?? ?? ... satisfy : 01FF00A0 = PC + XXX PC = 01FF0000 + 5 then the addr : XXX = 01FF00A0 - 01FF0000 - 5
以上就是修改api入口代码的方法
执行时机问题就比较简单了,我们还记得进程A(起始就是注入器)创建了一个远程线程,让进程B执行了LoadLibraryA("virus.dll")函数。dll中有个入口函数
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved) { HANDLE g_hModule; switch(dwReason) { case DLL_PROCESS_ATTACH: cout<<"Dll is attached!"<<endl; g_hModule = (HINSTANCE)hModule; break; case DLL_PROCESS_DETACH: cout<<"Dll is detached!"<<endl; g_hModule=NULL; break; } return true; }
当dll被加载之后,就会执行DLL_PROCESS_ATTACH后面的代码,如果我们将修改API入口代码的动作放到这里面做,则创建远程线程加载dll后,HooK的动作就会被执行,API函数的入口代码就会被修改。而当进程B再次调用该API时,该API就会跳转到我们自己写的函数了。
总结
以上就是API HOOK 的dll远程注入技术的详细步骤,如果有不明白的地方,可以参考:
远程注入 : http://blog.csdn.net/ithzhang...
api修改 : http://blog.csdn.net/friendan...
还可以参考我的代码 : https://github.com/Jasey/hook...
里面有大量的例子和解释。