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

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

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

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

一、原理

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

1
2
3
4
5
6
7
#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. 执行伪装函数。整个程序也分为两部分,一部分是负责查找并打开宿主进程和注入代码的应用程序,另一部分是包含修改代码和伪装函数的注入体。

二、查找指定的进程

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#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;
}

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

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

http://msdn.microsoft.com/en-us/library/windows/desktop/ms686701.aspx

三、代码注入

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

1
2
3
HMODULE  WINAPI LoadLibrary(
   __in           LPCTSTR  lpFileName
);

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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装入指定名称的宿主进程)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
#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;
}

四、挂钩

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

1. 在VS中进行汇编级调试

VS为用户提供了非常强大的调试功能,可以方便的查看注入代码与宿主代码的运行情况。现在需要另创建一个项目作为宿主进程,MFC简单对话框程序是一个不错的选择。下面就以GetTickCount作为目标API进行讲解。先响应对话框的鼠标左键按下事件,并添加GetTickCount代码:

1
2
3
4
5
void  CMyTargetDlg::OnLButtonDown( UINT  nFlags, CPoint point)
{
     GetTickCount();
     CDialog::OnLButtonDown(nFlags, point);
}

 在GetTickCount前设置断点,运行程序后点左键让程序停在这里,然后打开反汇编(调试菜单->窗口),会看到下面的反汇编代码:

 GetTickCount的断点

 上图中有4行汇编指令,第1列是指令所在的内存地址,第2列是汇编指令,第3列是操作数。在不同的机器上编译结果也不同,所以内存地址会不一样,但后面的指令和操作数都大同小异。按一下F10(逐过程),运行到0063E615这一行,再按下F11(逐语句)就会进入到GetTickCount的代码中去,见下图:

 GetTickCount的反汇编

 接下来要执行的指令是:

mov edx, 7FFE0000h

注意这一句代码所在的地址是7C80934A,下一句代码是7C80934F,说明这一行mov指令的长度为5。现在打开内存查看窗口(调试->窗口->内存),并在地址里输入0x7C80934A,显示如下:

 0x7C80934A

可知这条mov指令对应的机器码即是:ba 00 00 fe 7f。此时打开寄存器窗口(调试->窗口->寄存器),可以看到当前各寄存器的值。按下F10执行单步,还可以看到各寄存器的变化(变化的值用红色标出),如下图:

 registers

2. 指令的格式

为了继续要了解x86架构下汇编码和机器码的对应关系,需要参考一部非常重要的文献“Intel® 64 and IA-32 Architectures Software Developer's Manual”(以下简称IA32SDM),这是Intel公司免费提供给开发者的,可以在下面的网址找到3卷合订本:

http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html

在IA32SDM的Vol. 2B - 4.2(总第1257页)可以找到各种mov指令的说明:

 mov_opcode

上表中每一行表示了一种mov指令,第一列是这种指令的操作码格式,第二列是指令格式,最后一列是描述信息。格令格式一列中r*是指位长为*的寄存 器,r/m*是指位长为*的内存地址,Imm*是指位长为*的立即数。上文中的mov指令“mov edx, 7FFE0000h”的操作数有两个:第一个是32位寄存器(r32)edx,第二个是一个32位立即数(Imm32)7FFE0000h。查上表可知该 mov指令就是用红框划出的那一种:“MOV r32, Imm32”,它对应的Opcode(操作码)是“B8+rd”,机器码编码格式为OI。在1258页可以看到各种mov指令的编码格式,第一列与上表中的第三列对应,后面四列是四个操作数。

mov_encode

表中编码格式OI包含两个操作数,先是Opcode加寄存器代码,后面紧跟了一个立即数。而Opcode的编写格式参见IA32SDM的Vol. 2A - 3.1.1.1(总第606页),摘录如下:

The “Opcode” column in the table above shows the object code produced for each form of the instruction. When possible, codes are given as hexadecimal bytes in the same order in which they appear in memory. Definitions of entries other than hexadecimal bytes are as follows:

  • REX.W — Indicates the use of a REX prefix that affects operand size or instruction semantics. The ordering of the REX prefix and other optional/mandatory instruction prefixes are discussed Chapter 2. Note that REX prefixes that promote legacy instructions to 64-bit behavior are not listed explicitly in the opcode column.
  • /digit — A digit between 0 and 7 indicates that the ModR/M byte of the instruction uses only the r/m (register or memory) operand. The reg field contains the digit that provides an extension to the instruction's opcode.(翻译:这是一个0到7的数字,表示指令的ModR/M字节只使用r/m操作数。ModR/M的reg位就是该数,作为操作码的一个附加码)
  • /r — Indicates that the ModR/M byte of the instruction contains a register operand and an r/m operand.
  • cb, cw, cd, cp, co, ct — A 1-byte (cb), 2-byte (cw), 4-byte (cd), 6-byte (cp), 8-byte (co) or 10-byte (ct) value following the opcode. This value is used to specify a code offset and possibly a new value for the code segment register.
  • ib, iw, id, io — A 1-byte (ib), 2-byte (iw), 4-byte (id) or 8-byte (io) immediate operand to the instruction that follows the opcode, ModR/M bytes or scaleindexing bytes. The opcode determines if the operand is a signed value. All words, doublewords and quadwords are given with the low-order byte first.
  • +rb, +rw, +rd, +ro — A register code, from 0 through 7, added to the hexadecimal byte given at the left of the plus sign to form a single opcode byte. See Table 3-1 for the codes(翻译:这是一个寄存器代码,范围由0到7。与+号左边的16进制数代数相加构成一个完整的操作码字节。具体代码参见Table 3-1). The +ro columns in the table are applicable only in 64-bit mode.
  • +i — A number used in floating-point instructions when one of the operands is ST(i) from the FPU register stack. The number i (which can range from 0 to 7) is added to the hexadecimal byte given at the left of the plus sign to form a single opcode byte.

 按照标记为红色的描述可知Opcode“B8+rd”中的B8是基础码值0xB8,rd表示32位寄存器EDX的代号。寄存器的代码表可参见IA32SDM的Vol. 2A - Table 3-1(总第607页),如下图:

 reg_tbl

 从上表中红色线框标出的部分中可以看出,EDX对应的附加码为2,因此这条mov指令的Opcode就是0xB8 + 0x02 = 0xBA。跟据编码格式OI,后面紧跟一个32位的立即数0x7FFE0000,由于Intel的CPU体系是Little Ending,所以字节序为逆序,故在内存查看器中立即数显示为“00 00 fe 7f”。综上所述,该mov指令的完整机器码为:“ba 00 00 fe 7f”,与内存查看器的结果吻合。

2. 准备JMP

上面简单介绍了在VS进行汇编级调试的基本方法,并以mov指令为范例讲解了如何分析机器码。掌握了这些工具和资料,就可以清晰地了解我们下面要完成的代码在系统内部执行的细节。从上节可知,GetTickCount这个API执行的第一条指令是mov,如果能把mov的Opcode改为jmp,那就可以跳转到自定义的函数地址执行任意代码了。从IA32SDM(Vol. 2A - 3.2)中查出jmp指令的机器码:

 jmp_opcode

由于自定义的函数位置随机,且在win32操作系统的保护下,每个进程的段地址都是固定的,程序可以通过CS寄存器访问,但不能够改变。因此我们有两种选择,一是用JMP r/m32指令执行段内绝对跳转,二是用JMP rel32指令执行段内相对跳转。先讲解如何利用JMP r/m32执行绝对跳转。机器码的格式参见IA32SDM的Vol. 2A - 2.1,如下图:

 instruction_format

从上图可知,机器码由6大部分组成,而JMP r/m32指令对应的机器码为“FF /4”(其中/4的含义参见上文中Opcode说明里用蓝色标记的文字),用到了其中3个部分:1个字节的Opcode(即0xFF)、1个字节的ModR/M和4个字节的Displacement操作数。其中的ModR/M指定了CPU的寻址方式以及Opcode的附加码,它又分为三段:Mod、Reg/Opcode和R/M,具体构成可参见IA32SDM的Vol. 2A - 2.1.3和后面的Table 2-2,如下图:

 ModRM

先看一下表头最左边一格,第6行“/digit (Opcode)”就是机器码“FF /4”中的4,所以看红框标记的那一列(4的二进制为100)就可以了。“Effective Address”指定了寻指方式,为了避免对寄存器进行操作,用1条指令就完成跳转,我们选择最简单的“disp32”这一行,它表示仅用指令机器码中的第3部分Displacement表示跳转的目标地址。这样就确定了使用的Mod位为00,Reg/Opcode位为100,R/M位为101。计算可得ModR/M字节为00 100 101(二进制) = 0x25。

Displacement指向一段4字节的内存,这段内存里存放的是最终的目标地址。因此需要先用VirtualAllocEx申请4个字节的空间,将自定义函数的地址存入,然后再将申请的地址填入Displacement。综上所述,完整的机器码应该是FF 25 XX XX XX XX,最后面的4个字节是一个存有目标函数入口地址的内存地址。

用JMP r/m32指令完成跳转是比较复杂的,不仅需要申请和释放内存,且整个机器指令有6个字节。更简单的方法就是利用JMP rel32指令执行相对跳转,而机器码只有5个字节。JMP rel32对应的机器码是E9 cd,其中cd就是相对地址,计算方法为:目标地址 - 当前指令地址 - 5。在准备好JMP指令的机器码后,就可以将其替换到目标API的入口地址处,欺骗宿主进程执行伪装函数。

3. 修改入口点

看完上面的介绍,相信您已经迫不及待的想要尝试如何对目标API挂钩了。虽然还有很多问题没有解决,比如怎样返回,怎样执行原API功能,怎样全身而退等等,但这些问题可以先放一放,先来看看能否利用上面的方法成功挂钩。

首先需要建立一个DLL项目以生成注入体,自定义一个DllMain函数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <windows.h>
 
BOOL  WINAPI DllMain( HINSTANCE  hInstDll,  DWORD  fdwReason,  LPVOID  lpvReserved)
{
     switch  (fdwReason)
     {
     case  DLL_PROCESS_ATTACH:
         DisableThreadLibraryCalls(hInstDll);
         InstallMonitor();
         break ;
 
     case  DLL_PROCESS_DETACH:
         break ;
     }
     return  TRUE;
}

 然后编写挂钩函数InstallMonitor:

1
2
3
4
5
6
7
8
void  InstallMonitor( void )
{
     HANDLE  hProc = GetCurrentProcess();
     BYTE  aOpcode[5] = {0xE9};  //JMP Procudure
     *( DWORD *)(&g_aOpcode[1]) =  DWORD (MonFunc) -  DWORD (GetTickCount) - 5;
     WriteProcessMemory(hProc,  LPVOID (GetTickCount),  LPVOID (aOpcode), 5, NULL);
     CloseHandle(hProc);
}

上面的代码很好理解,aOpcode就是根据前文介绍的方法构造的jmp指令,指定跳转到自定义的伪装函数MonFunc,然后用WriteProcessMemory将jmp指令填写入GetTickCount的代码处。伪装函数MonFunc函数很好写:

1
2
3
4
void  WINAPI MonFunc( void )
{
     MessageBox(NULL, _T( "注入代码" ), _T( "示例" ), 0);
}

 至此,您就可以按上面的代码编译一个注入体DLL了,然后利用本文第三部分的注入程序就可以将此DLL注入到宿主进程执行。

五、完美欺骗

如果您按上文所述的方法执行出成功的结果,那么你很可能会发现在对话框确定后宿主进程崩溃了。原因有下面几条:

  1. 伪装函数没有正确的保持栈的平衡,导致返回时宿主清栈出错;
  2. 伪装函数没有按API执行方式执行出结果,宿主不能正常的调用系统API导致错误;
  3. 伪装函数不是线程安全的,导致宿主在并发调用时出错;
  4. 宿主有安全防护措施,检查到攻击后自动恢复或自我毁灭。

本文只讨论前3条原因的解决方案,不考虑第4条原因。下面逐条解释。

1. 保持栈的平衡

大部分API都是有参数的,而参数是由宿主在call指令执行前压入堆栈。Win32API的调用约定是__stdcall,表示由API负责栈的清理,那么如果伪装函数在返回时没有适当的清栈必将导致出错。因此,伪装函数的参数表一定要与原API相同,才能保证编译器生成的代码能够正确返回到宿主代码。

2. 执行原API的功能

为了能够执行原API的功能,必须在调用它之前恢复它原来的代码,否则就会陷入死循环。当然,应该在改写机器码时保留原先的机器码,这样就可以利用WriteProcessMemory将其恢复原状。ReadProcessMemory这个API函数与WriteProcessMemory的功能相反,可以读取指定位置的机器码。还要记得,在原API调用结束后还要修改它的入口点,否则下次就无法欺骗了。整个伪装函数的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//Monitor Function
DWORD  WINAPI MonFunc()
{
     //Restore the original API before calling it
     ReleaseBase();
 
     //Calling the original API
     DWORD  dw = GetTickCount();
 
     //Monitor the original API again
     MonitorBase();
 
     //You can do anything here
 
     return  dw;
}

3. 线程安全

用EnterCriticalSection和LeaveCriticalSection是保证线程安全的最佳选择,将伪装函数用这对函数包起来就可以解决并发访问的问题。现在的代码应该看起来是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Monitor Function
DWORD  WINAPI MonFunc()
{
     //Thread safety
     EnterCriticalSection(&g_cs);
 
     //Restore the original API before calling it
     ReleaseBase();
     DWORD  dw = GetTickCount();
     MonitorBase();
 
     //You can do anything here
 
     //Thread safety
     LeaveCriticalSection(&g_cs);
     return  dw;
}

4. 完整示例

下面贴出注入体DLL的完整代码,供您参考。这个DLL对GetTickCount挂了钩子,您可以在伪装函数MonFunc中添加任意的自定义代码,并在退出的时候调用UninstallMonitor结束钩子程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#include <tchar.h>
#include <Windows.h>
 
//Handle of current process
HANDLE  g_hProc;
 
//Backup of orignal code of target api
BYTE  g_aBackup[6];
BYTE  g_aOpcode[6];
 
//Critical section, prevent concurrency of calling the monitor
CRITICAL_SECTION g_cs;
 
//Base address of target API in DWORD
DWORD  g_dwApiFunc = ( DWORD )GetTickCount;
 
//Hook the target API
__inline  BOOL  MonitorBase( void )
{
     // Modify the heading 6 bytes opcode in target API to jmp instruction,
     // the jmp instruction will lead the EIP to our fake function
  ReadProcessMemory(g_hProc,  LPVOID (g_dwApiFunc),  LPVOID (g_aBackup),<br>     sizeof (g_aBackup)/  sizeof (g_aBackup[0]), NULL);<br>  return  WriteProcessMemory(g_hProc,  LPVOID (g_dwApiFunc),
         LPVOID (g_aOpcode),  sizeof (g_aOpcode) /  sizeof (g_aOpcode[0]), NULL);
}
 
//Unhook the target API
__inline  BOOL  ReleaseBase( void )
{
     // Restore the heading 6 bytes opcode of target API.
     return  WriteProcessMemory(g_hProc,  LPVOID (g_dwApiFunc),
         LPVOID (g_aBackup),  sizeof (g_aOpcode) /  sizeof (g_aOpcode[0]), NULL);
}
 
//Pre-declare
BOOL  UninstallMonitor( void );
 
//Monitor Function
DWORD  WINAPI MonFunc()
{
     //Thread safety
     EnterCriticalSection(&g_cs);
 
     //Restore the original API before calling it
     ReleaseBase();
     DWORD  dw = GetTickCount();
     MonitorBase();
 
     //You can do anything here, and you can call the UninstallMonitor
     //when you want to leave.
 
     //Thread safety
     LeaveCriticalSection(&g_cs);
     return  dw;
}
 
//Install Monitor
BOOL  InstallMonitor( void )
{
     //Get handle of current process
     g_hProc = GetCurrentProcess();
 
     g_aOpcode[0] = 0xE9;  //JMP Procudure
     *( DWORD *)(&g_aOpcode[1]) = ( DWORD )MonFunc - g_dwApiFunc - 5;
 
     InitializeCriticalSection(&g_cs);
 
     //Start monitor
     return  MonitorBase();
}
 
BOOL  UninstallMonitor( void )
{
     //Release monitor
     if  (!ReleaseBase())
         return  FALSE;
 
     DeleteCriticalSection(&g_cs);
 
     CloseHandle(g_hProc);
 
     //Synchronize to main application, release semaphore to free injector
     HANDLE  hSema = OpenSemaphore(EVENT_ALL_ACCESS, FALSE, _T( "Global\\InjHack" ));
     if  (hSema == NULL)
         return  FALSE;
     return  ReleaseSemaphore(hSema, 1, ( LPLONG )g_hProc);
}
 
BOOL  WINAPI DllMain( HINSTANCE  hInstDll,  DWORD  fdwReason,  LPVOID  lpvReserved)
{
     switch  (fdwReason)
     {
     case  DLL_PROCESS_ATTACH:
         DisableThreadLibraryCalls(hInstDll);
 
         InstallMonitor();
         break ;
 
     case  DLL_PROCESS_DETACH:
         break ;
     }
     return  TRUE;
}

 

 

相关文章
|
2月前
|
机器学习/深度学习 PyTorch 算法框架/工具
揭秘深度学习中的微调难题:如何运用弹性权重巩固(EWC)策略巧妙应对灾难性遗忘,附带实战代码详解助你轻松掌握技巧
【10月更文挑战第1天】深度学习中,模型微调虽能提升性能,但常导致“灾难性遗忘”,即模型在新任务上训练后遗忘旧知识。本文介绍弹性权重巩固(EWC)方法,通过在损失函数中加入正则项来惩罚对重要参数的更改,从而缓解此问题。提供了一个基于PyTorch的实现示例,展示如何在训练过程中引入EWC损失,适用于终身学习和在线学习等场景。
103 4
揭秘深度学习中的微调难题:如何运用弹性权重巩固(EWC)策略巧妙应对灾难性遗忘,附带实战代码详解助你轻松掌握技巧
|
2月前
|
人工智能 机器人 API
【通义】AI视界|谷歌Q3财报:Gemini API六个月增长14倍,公司超25%的新代码由AI生成
本文内容由通义自动生成,涵盖谷歌Q3财报、马斯克xAI融资、九巨头联盟挑战英伟达、Meta加大AI投入及麻省理工研究LLM与人脑相似性等热点资讯。更多精彩内容,请访问通通知道。
|
2月前
|
JSON API 数据格式
低代码实现鸿蒙API返回JSON转TS及快速生成ArkUI代码
低代码实现鸿蒙API返回JSON转TS及快速生成ArkUI代码
49 0
低代码实现鸿蒙API返回JSON转TS及快速生成ArkUI代码
|
2月前
|
存储 数据管理 API
零代码能力:轻松搞定表单和API接口,少写80%后端代码,内含资源
小白接口(果创云 YesApi.cn)是一个零代码和低代码开发平台,提供一站式后端云服务,帮助开发者、学生、业余爱好者、工作室、中小企业及无IT技术人员的传统企业快速搭建应用、接口、服务和网站。平台提供500+免费API接口,支持在线API开发、在线表单、数据库管理、图片文件存储、会员管理等功能,无需后端开发经验,轻松实现数据处理和应用开发。
|
4月前
|
JSON 安全 API
构建高效后端API:最佳实践与代码示例
【8月更文挑战第2天】 在数字化时代,后端API是连接数据与用户的桥梁。本文深入探讨了如何设计并实现高效的后端API,从理论到实践,提供了实用的技巧和代码示例。通过阅读本篇文章,你将学会如何避免常见的陷阱,优化你的API性能,从而提供更加流畅的用户体验。
|
4月前
|
JavaScript 网络协议 API
【Azure API 管理】Azure APIM服务集成在内部虚拟网络后,在内部环境中打开APIM门户使用APIs中的TEST功能失败
【Azure API 管理】Azure APIM服务集成在内部虚拟网络后,在内部环境中打开APIM门户使用APIs中的TEST功能失败
|
4月前
|
API 开发工具 网络架构
【Azure Developer】如何通过Azure Portal快速获取到对应操作的API并转换为Python代码
【Azure Developer】如何通过Azure Portal快速获取到对应操作的API并转换为Python代码
|
4月前
|
Java API 数据格式
Spring Boot API参数读取秘籍大公开!6大神器助你秒变参数处理大师,让你的代码飞起来!
【8月更文挑战第4天】Spring Boot凭借其便捷的开发和配置特性,成为构建微服务的热门选择。高效处理HTTP请求参数至关重要。本文介绍六种核心方法:查询参数利用`@RequestParam`;路径变量采用`@PathVariable`;请求体通过`@RequestBody`自动绑定;表单数据借助`@ModelAttribute`或`@RequestParam`;请求头使用`@RequestHeader`;Cookie则依靠`@CookieValue`。每种方法针对不同场景,灵活运用可提升应用性能与用户体验。
73 9
|
4月前
|
IDE 测试技术 API
使用京东API接口适用于的环境及验证调用合法性的方法
在电商领域,京东API接口支持商品信息查询、订单处理等功能。开发者需确保在稳定服务器端环境使用,选择合适编程语言及框架,并具备足够网络带宽处理能力。开发环境应配备IDE或代码编辑器及所需库。测试环境需充分验证API稳定性与可靠性。合法性验证包括:正确使用App Key和App Secret进行鉴权;掌握签名规则并在请求中添加签名;遵守请求频率限制;理解并遵循数据使用协议。遵循这些指导原则可保证API调用的合法性和稳定性。
|
5月前
|
JSON Shell API
阿里云PAI-Stable Diffusion开源代码浅析之(一)所有api的入参如何看
阿里云PAI-Stable Diffusion开源代码浅析之所有api的入参如何看