Win32环境下代码注入与API钩子的实现

澳门新葡亰网站注册 4

1、背景

仅针对JVM的模板解释器:

如何根据opcode和寻址模式,将bytecode生成汇编码。

本文的示例中所使用的字节码和汇编码,请参见上篇博文:按值传递还是按引用?

本文详细的介绍了在Visual
Studio(以下简称VS)下实现API钩子的编程方法,阅读本文需要基础:有操作系统的基本知识(进程管理,内存管理),会在VS下编写和调试Win32应用程序和动态链接库(以下简称DLL)。

2、寻址模式

本文不打算深入展开寻址模式的阐述,我们聚焦Intel的IA32-64架构的指令格式:

澳门新葡亰网站注册 1

简要说明下,更多的请参考intel的手册:

– Prefixes : 用于修饰操作码Opcode,赋予其lock、repeat等的语义.
– REX Prefix
—- Specify GPRs and SSE registers.
—- Specify 64-bit operand size.
—- Specify extended control registers.
Opcode澳门新葡亰网站注册,:操作码,如mov、push.
Mod R/M:寻址相关,具体见手册。
SIB:和Mod R/M结合起来指定寻址。
Displacement:配合Mod R/M和SIB指定寻址。
Immediate:立即数。

对上面的Opcode、Mod R/W、SIB、disp、imm如果不明白,看句汇编有个概念:

%mov %eax , %rax,-0x18(%rcx,%rbx,4)

如果这句汇编也不太明白,那么配合下面的:

– Base + (Index ∗ Scale) + Displacement – Using all the addressing
components together allows efficient
indexing of a two-dimensional array when the elements of the array are
2, 4, or 8 bytes in size.

API钩子是一种高级编程技巧,常常用来完成一些特别的功能,比如词典软件的屏幕取词,游戏修改软件的数据修改等。当然,此技术更多的是被黑客或是病毒用来攻击其它程序,截获需要的数据或改变目标程序的行为。本文不探讨此技术的应用,只讲实现。同时希望掌握此技术的人都能够合法的应用它,不要去做危险或违法的事情,害人害己。

3、合法的值(64位)

关注下这4个参数的合法取值:

• Displacement — An 8-bit, 16-bit, or 32-bit value.
• Base — The value in a 64-bit general-purpose register.
• Index — The value in a 64-bit general-purpose register.
• Scale factor — A value of 2, 4, or 8 that is multiplied by the index
value.

一、原理

每一个程序在操作系统中运行,都必须调用操作系统提供的函数——也就是API(应用程序编程接口)——来实现程序的各种功能。在Windows操作系统下,API就是那几千个系统函数。在有些程序中并没直接调用API的代码,比如下面的程序:

#include <iostream>
using namespace std;
int main(void)
{
    cout << "Hello World!" << endl;
    return 0;
}

事实上,cout对象的内部处理函数已经替你调用API。就算你的main函数是空的,里面什么代码都不写,只要程序被操作系统启动,也会调用一些基本的API,比如LoadLibrary。这个函数是用来加载DLL的,也就是在进程运行的过程中,把DLL中的程序指令和数据读入当前进程并执行启动代码,我们后面会用到这个函数。

如果能够设法用自定义函数替换宿主进程调用的目标API函数,那么就可以截获宿主进程传入目标API的参数,并可以改变宿主进程的行为。但要想修改目标API函数必须先查找并打开宿主进程,并让自定义代码能在宿主进程中运行。因此挂API钩子分为四步:1.
查找并打开宿主进程,2. 将注入体装入宿主进程中运行,3.
用伪装函数替换目标API,4.
执行伪装函数。整个程序也分为两部分,一部分是负责查找并打开宿主进程和注入代码的应用程序,另一部分是包含修改代码和伪装函数的注入体。

4、Mod R/M(32位寻址)

我们在后文将会用到Mod R/M字节,所以将32位寻址的格式贴在这里:

澳门新葡亰网站注册 2

上表的备注,其中第1条将在我们的示例中用到,所以这里留意下:

  1. The [–][–] nomenclature means a SIB follows the ModR/M
    byte.
  2. The disp32 nomenclature denotes a 32-bit displacement that follows
    the ModR/M byte (or the SIB byte if one is present) and that is
    added to the index.
  3. The disp8 nomenclature denotes an 8-bit

二、查找指定的进程

查找指定的进程有很多方法,下面简单的介绍三种:

5、SIB(32位寻址)

同样,因为用到了Mod R/M字节,那么SIB字节也可能要用到:

澳门新葡亰网站注册 3

1. 找到鼠标所指窗体的进程句柄

DWORD GetProcIDFromCursor(void)
{
    //Get current mouse cursor position
    POINT ptCursor;
    if (!GetCursorPos(&ptCursor))
    {
        cout << "GetCursorPos Error: " << GetLastError() << endl;
        return 0;
    }

    //Get window handle from cursor postion
    HWND hWnd = WindowFromPoint(&ptCursor);
    if (NULL == hWnd)
    {
        cout << "No window exists at the given point!" << endl;
        return 0;
    }

    //Get the process ID belong to the window.
    DWORD dwProcId;
    GetWindowThreadProcessId(hWnd, &dwProcId);

    return dwProcId;
} 

6、示例

2. 查找指定文件名的进程

#include <Psapi.h>
#pragma comment(lib, "Psapi.lib")
DWORD GetProcIDFromName(LPCTSTR lpName)
{
    DWORD aProcId[1024], dwProcCnt, dwModCnt;
    HMODULE hMod;
    TCHAR szPath[MAX_PATH];

    //枚举出所有进程ID
    if (!EnumProcesses(aProcId, sizeof(aProcId), &dwProcCnt))
    {
        cout << "EnumProcesses error: " << GetLastError() << endl;
        return 0;
    }

    //遍例所有进程
    for (DWORD i = 0; i < dwProcCnt; ++i)
    {
        //打开进程,如果没有权限打开则跳过
        HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, aProcId[i]);
        if (NULL != hProc)
        {
            //打开进程的第1个Module,并检查其名称是否与目标相符
            if (EnumProcessModules(hProc, &hMod, sizeof(hMod), &dwModCnt))
            {
                GetModuleBaseName(hProc, hMod, szPath, MAX_PATH);
                if (0 == lstrcmpi(szPath, lpName))
                {
                    CloseHandle(hProc);
                    return aProcId[i];
                }
            }
            CloseHandle(hProc);
        }
    }
    return 0;
}

6.1、准备工作

来看个实际的例子。

下面的代码是生成mov汇编码:

void Assembler::movl(Address dst, Register src) {
  InstructionMark im(this);
  prefix(dst, src);
  emit_int8((unsigned char)0x89);
  emit_operand(src, dst);
}

prefix(dst,src)就是处理prefix和REX prefix,这里我们不关注。

emit_int8((unsigned char) 0x89)顾名思义就是生成了一个字节,那字节的内容0×89代表什么呢?

先不急,还有一句emit_operand(src,dst),这是一段很长的代码,我们大概看下:

void Assembler::emit_operand(Register reg, Register base, Register index,
                 Address::ScaleFactor scale, int disp,
                 RelocationHolder const& rspec,
                 int rip_relative_correction) {
  relocInfo::relocType rtype = (relocInfo::relocType) rspec.type();

  // Encode the registers as needed in the fields they are used in

  int regenc = encode(reg) << 3;
  int indexenc = index->is_valid() ? encode(index) << 3 : 0;
  int baseenc = base->is_valid() ? encode(base) : 0;

  if (base->is_valid()) {
    if (index->is_valid()) {
      assert(scale != Address::no_scale, "inconsistent address");
      // [base + index*scale + disp]
      if (disp == 0 && rtype == relocInfo::none  &&
          base != rbp LP64_ONLY(&& base != r13)) {
        // [base + index*scale]
        // [00 reg 100][ss index base]

        /**************************
        * 关键点:关注这里
        **************************/

        assert(index != rsp, "illegal addressing mode");
        emit_int8(0x04 | regenc);
        emit_int8(scale << 6 | indexenc | baseenc);
      } else if (is8bit(disp) && rtype == relocInfo::none) {
        // ...
      } else {
        // [base + index*scale + disp32]
        // [10 reg 100][ss index base] disp32
        assert(index != rsp, "illegal addressing mode");
        emit_int8(0x84 | regenc);
        emit_int8(scale << 6 | indexenc | baseenc);
        emit_data(disp, rspec, disp32_operand);
      }
    } else if (base == rsp LP64_ONLY(|| base == r12)) {
      // ... 
    } else {

      // ... 
    }
  } else {
    // ... 
  }
}

上面的代码的关注点已经标出,这里我们将其抽出,并将前文中的emit_int8((unsigned char) 0x89)结合起来:

emit_int8((unsigned char) 0x89)
emit_int8(0x04 | regenc);
emit_int8(scale << 6 | indexenc | baseenc);

最终其生成了如下的汇编代码(64位机器):

mov    %eax,(%rcx,%rbx,1)

好了,问题来了:

上面这句汇编怎么得出的?

 3. 查找其它指定信息的进程

通过CreateToolhelp32Snapshot枚举系统中正在运行的所有进程,并通过相关数据结构得到进程的信息,具体用法可以参见:

6.2、计算过程

我们给个下面的值:

regenc = 0x0,scale << 6 | indexenc | baseenc = 25

进行简单的运算就可以得到:

emit_int8((unsigned char) 0x89) //得到0x89
emit_int8(0x04 | regenc); //得到0x04
emit_int8(scale << 6 | indexenc | baseenc); //得到0x19

合起来就是三个字节:

0x89 0x04 0x19

1、0×89对应什么?

澳门新葡亰网站注册 4

从上表可以看出因为JVM工作在64位下,所以需要配合REX.W来“起头”,不过在我们这个例子中,其恰好是0。

主要看那个89/r:

MOV r/m64,r64 //64位,将寄存器中的值给到寄存器或者内存地址中

2、0×04代表什么?

现在我们要用到上面的Mod R/M表和SIB表了。

用第二个字节0×04查Mod
R/M表,可知源操作数是寄存器EAX,同时可知寻址类型是[–][–]类型,含义为:

The [–][–] nomenclature means a SIB follows the ModR/M byte.

3、0×19代表什么?

继续查SIB表,对应字节0×19的是:

base = ECX
scaled index = EBX

4、汇编代码:

//32位
mov %eax,%(ecx,ebx,1)

//64位
mov %rax,%(rcx,rbx,1)

三、代码注入

上面提到过LoadLibrary可以将指定的DLL代码注入当前进程,如果能让宿主进程来执行这个函数,并把我们自己的DLL的文件名传入,那么我们的代码就可以在宿主进程中运行了。

HMODULE WINAPI LoadLibrary(
  __in          LPCTSTR lpFileName
);

再看另一个函数:CreateRemoteThread,它可以让宿主进程新开一个线程,但是新线程的处理函数(LPTHREAD_START_ROUTINE)必须是宿主进程中的函数地址或系统API。

HANDLE WINAPI CreateRemoteThread(
  __in          HANDLE hProcess,
  __in          LPSECURITY_ATTRIBUTES lpThreadAttributes,
  __in          SIZE_T dwStackSize,
  __in          LPTHREAD_START_ROUTINE lpStartAddress,
  __in          LPVOID lpParameter,
  __in          DWORD dwCreationFlags,
  __out         LPDWORD lpThreadId
);

//其中LPTHREAD_START_ROUTINE的定义如下
typedef DWORD (WINAPI *PTHREAD_START_ROUTINE)(
    LPVOID lpThreadParameter
    );

如果可以用让宿主进程新开一个线程,执行LoadLibrary函数,而参数是注入体DLL的文件名,就大功告成了。不过要完成这些操作,我们先来分析一下可行性。

我们知道,所有系统API函数的调用方式都是__stdcall,即参数采用从右到左的压栈方式,自己在退出时清空堆栈。这一类函数的具体调用过程如下:在调用前先由调用者将所有参数以地址或数值的形式从右向左压入栈中,然后用call指令调用该函数;进入函数后,先从栈中取出这些参数再进行运算,并在函数返回前将之前压入的栈数据全部弹出以维持栈平衡,最后用eax寄存储传递返回值(地址或数值)给调用者。这也就是说在指令层面上讲,API函数的基本调用方式都相同,然而调用者必须在栈中压入确定数量的参数,若压入的参数数量不匹配,函数内的取栈和弹栈操作将会使得栈数据错乱,最终导致程序崩溃。

通过观察发现LoadLibrary的参数数量刚好与LPTHREAD_START_ROUTINE都只有一个参数,那么如果能够获取LoadLibrary函数在宿主进程中的地址,作为lpStartAddress传入CreateRemoteThread,并将我们的注入体DLL的文件名作为lpParameter传入,那么就可以让宿主进程执行注入体代码了。为了将DLL的文件名传入宿主进程,我们还需要以下四个API:VirtualAllocEx和VirtualFreeEx可以在宿主进程中分配和释放一段内存空间;ReadProcessMemory和WriteProcessMemory可以在宿主进程中的指定内存地址读出或写入数据。

在注入的代码执行完毕后,还要完成清理工作。首先是卸载刚刚载入的DLL,需要使用另一个系统API:FreeLibrary。过程与上面的代码注入一样,使用CreateRemoteThread,将FreeLibrary的地址作为lpStartAddress参数传入。注意到FreeLibrary的参数是一个HMODULE,该句柄其实是一个Module的全局ID,一般由LoadLibrary的返回值给出。因此可以调用GetExitCodeThread获取前面执行的LoadLibrary线程的返回值,再作为CreateRemoteThread的lpParameter参数传入,这样就完成了DLL的卸载。还要记得用VirtualFreeEx释放VirtualAllocEx申请到的内存,并关闭打开的所有句柄,完成最后的清理工作。

现在注入代码的步骤就比较清晰了:

  1. 调用OpenProcess获取宿主进程句柄;
  2. 调用GetProcAddress查找LoadLibrary函数在宿主进程中的地址;
  3. 调用VirtualAllocEx和WriteProcessMemory将DLL文件名字符串写入宿主进程的内存;
  4. 调用CreateRemoteThread执行LoadLibrary在宿主进程中运行DLL;
  5. 调用VirtualFreeEx释放刚申请的内存;
  6. 调用WaitForSingleObject等待注入线程结束;
  7. 调用GetExitCodeThread获取前面加载的DLL的句柄;
  8. 调用CreateRemoveThead执行FreeLibrary卸载DLL;
  9. 调用CloseHandle关闭打开的所有句柄。

代码注入的所有代码整理如下。(注意:这个程序需要在win32控制台模式下编译生成一个exe文件。在控制台下运行时需要两个参数:第1个参数为宿主进程的映象名称,可以在任务管理器中查看;第2个参数为注入体DLL的完整路径文件名。程序运行后就会将指定的DLL装入指定名称的宿主进程)

#include <tchar.h>
#include <Windows.h>
#include <atlstr.h>
#include <Psapi.h>
#pragma comment(lib, "Psapi.lib") 

#include <iostream>
#include <string>
using namespace std;

DWORD FindProc(LPCSTR lpName)
{
    DWORD aProcId[1024], dwProcCnt, dwModCnt;
    char szPath[MAX_PATH];
    HMODULE hMod;

    //枚举出所有进程ID
    if (!EnumProcesses(aProcId, sizeof(aProcId), &dwProcCnt))
    {
        //cout << "EnumProcesses error: " << GetLastError() << endl;
        return 0;
    }

    //遍例所有进程
    for (DWORD i = 0; i < dwProcCnt; ++i)
    {
        //打开进程,如果没有权限打开则跳过
        HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, aProcId[i]);
        if (NULL != hProc)
        {
            //打开进程的第1个Module,并检查其名称是否与目标相符
            if (EnumProcessModules(hProc, &hMod, sizeof(hMod), &dwModCnt))
            {
                GetModuleBaseNameA(hProc, hMod, szPath, MAX_PATH);
                if (0 == _stricmp(szPath, lpName))
                {
                    CloseHandle(hProc);
                    return aProcId[i];
                }
            }
            CloseHandle(hProc);
        }
    }
    return 0;
}

//第一个参数为宿主进程的映象名称,可以任务管理器中查看
//第二个参数为需要注入的DLL的完整文件名
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        cout << "Invalid parameters!" << endl;
        return -1;
    }
    //查找目标进程,并打开句柄
    DWORD dwProcID = FindProc(argv[1]);
    if (dwProcID == 0)
    {
        cout << "Target process not found!" << endl;
        return -1;
    }
    HANDLE hTarget = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcID);
    if (NULL == hTarget)
    {
        cout << "Can't Open target process!" << endl;
        return -1;
    }

    //获取LoadLibraryW和FreeLibrary在宿主进程中的入口点地址
    HMODULE hKernel32 = GetModuleHandle(_T("Kernel32"));
    LPTHREAD_START_ROUTINE pLoadLib = (LPTHREAD_START_ROUTINE)
        GetProcAddress(hKernel32, "LoadLibraryW");
    LPTHREAD_START_ROUTINE pFreeLib = (LPTHREAD_START_ROUTINE)
        GetProcAddress(hKernel32, "FreeLibrary");
    if (NULL == pLoadLib || NULL == pFreeLib)
    {
        cout << "Library procedure not found: " << GetLastError() << endl;
        CloseHandle(hTarget);
        return -1;
    }

    WCHAR szPath[MAX_PATH];
    MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, argv[2], -1,
        szPath, sizeof(szPath) / sizeof(szPath[0]));

    //在宿主进程中为LoadLibraryW的参数分配空间,并将参数值写入
    LPVOID lpMem = VirtualAllocEx(hTarget, NULL, sizeof(szPath),
        MEM_COMMIT, PAGE_READWRITE);
    if (NULL == lpMem)
    {
        cout << "Can't alloc memory block: " << GetLastError() << endl;
        CloseHandle(hTarget);
        return -1;
    }

    // 参数即为要注入的DLL的文件路径
    if (!WriteProcessMemory(hTarget, lpMem, (void*)szPath, sizeof(szPath), NULL))
    {
        cout << "Can't write parameter to memory: " << GetLastError() << endl;
        VirtualFreeEx(hTarget, lpMem, sizeof(szPath), MEM_RELEASE);
        CloseHandle(hTarget);
        return -1;
    }

    //创建信号量,DLL代码可以通过ReleaseSemaphore来通知主程序清理
    HANDLE hSema = CreateSemaphore(NULL, 0, 1, _T("Global\InjHack"));

    //将DLL注入宿主进程
    HANDLE hThread = CreateRemoteThread(hTarget, NULL, 0, pLoadLib, lpMem, 0, NULL);

    //释放宿主进程内的参数内存
    VirtualFreeEx(hTarget, lpMem, sizeof(szPath), MEM_RELEASE);

    if (NULL == hThread)
    {
        cout << "Can't create remote thread: " << GetLastError() << endl;
        CloseHandle(hTarget);
        return -1;
    }

    //等待DLL信号量或宿主进程退出
    WaitForSingleObject(hThread, INFINITE);
    HANDLE hObj[2] = {hTarget, hSema};
    if (WAIT_OBJECT_0 == WaitForMultipleObjects(2, hObj, FALSE, INFINITE))
    {
        cout << "Target process exit." << endl;
        CloseHandle(hTarget);
        return 0;
    }
    CloseHandle(hSema);

    //根据线程退出代码获取DLL的Module ID
    DWORD dwLibMod;
    if (!GetExitCodeThread(hThread, &dwLibMod))
    {
        cout << "Can't get return code of LoadLibrary: " << GetLastError() << endl;
        CloseHandle(hThread);
        CloseHandle(hTarget);
        return -1;
    }

    //关闭线程句柄
    CloseHandle(hThread);

    //再次注入FreeLibrary代码以释放宿主进程加载的注入体DLL
    hThread = CreateRemoteThread(hTarget, NULL, 0, pFreeLib, (void*)dwLibMod, 0, NULL);
    if (NULL == hThread)
    {
        cout << "Can't call FreeLibrary: " << GetLastError() << endl;
        CloseHandle(hTarget);
        return -1;
    }
    WaitForSingleObject(hThread, INFINITE);
    CloseHandle(hThread);

    CloseHandle(hTarget);
    return 0;
}

7、结语

本文简要探讨了:

如何根据opcode和寻址模式,将bytecode生成汇编码。

四、挂钩

上面的程序已经可以将自编代码注入到宿主进程中了,下面就要进一步讨论如何来编写注入体(动态链接库)以实现对目标API进行拦截。这一部分的内容比上面要深一些,需要一点汇编基础知识。