WINDOWS上的 API HOOK 技术

stdjkdblom 2019-06-26

预备知识

  1. 每个进程都拥有独立的地址空间
  2. dll动态链接库是所有进程共享的,但是需要注意,这里有个前提。前提是,所有的进程都不会去修改dll的内容,如果有进程A修改了dll的内容,则该dll就变成了进程A私有的了(系统使用copy-on-write方法,复制一份dll到进程A的私有地址空间,然后修改dll内容)。试想,如果任何情况都是共享的,一个进程只需要自己加载的dll中的内容,那么其余的所有使用了该dll的进程都会受到影响。
  3. 如果进程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个步骤。,此处需要有几点说明:

  1. 提权是为了有权限访问远程线程,读写远程线程
  2. 为什么要申请空间,然后将virus.dll路径写入到B进程空间中,不能直接传一个dll路径的字符串呢?因为远程空间调用LoadLibraryA时,需要读取参数,A进程中写的dll路径的字符串存储在A进程的地址空间中,A把这个地址告诉B,B在自己的进程空间中找这个地址是找不到内容的,所以,需要将申请的空间地址告诉B。
  3. 你要问了,既然路径的地址需要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个字节内容。
注意几个问题:

  1. 为什么是5个字节?

因为jmp XXX 指令占用5个字节(32位体系结构中)

  1. 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...
里面有大量的例子和解释。