hook与注入系列

前言

HOOK(钩子) 其实就是改变程序执行流程的一种技术的统称

Inline hook

 在windows系统下编程,应该会接触到api函数的使用,常用的api函数大概有2000个左右。今天随着控件,stl等高效编程技术的出现,api的使用概率在普通的用户程序上就变得越来越小了。当诸如控件这些现成的手段不能实现的功能时,我们还需要借助api。最初有些人对某些api函数的功能不太满意,就产生了如何修改这些api,使之更好的服务于程序的想法,这样api hook就自然而然的出现了。我们可以通过api hook,改变一个系统api的原有功能。基本的方法就是通过hook“接触”到需要修改的api函数入口点,改变它的地址指向新的自定义的函数

exe 调用函数,正常情况下直接找到dll导出函数,那么如果是hook,就把dll导出的这个函数进行一顿操作,把他首地址5个字节改了,exe因为这个被跳转了,跳转到我们的hook函数里面,

利用这个技术,我们可以监控API,比如应用程序会调用loadLibrary,那我们把它Hook了,

把Dll路径改成我们的,那加载的就是我们的dll了,当然 Hook的API很多,因为只要是Windows的API都能HOOK因为JMP的时候占五个字节,而WindowsAPI也是头部弄了五个字节,猜想可能是Windows自己留作Hook的.

inline Hook的步骤

我们先看一个简单的例子,我这里人为改变程序的流程,在add函数开头添加hook, add函数调用之前先调用fake函数 , 这里要提到两个函数。VirtualProtect和WriteProcessMemory.

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
#include "stdio.h"
#include "windows.h"
#include "ras.h"
#pragma warning(disable:4996)

int fake_add(int a, int b){
return 1 + a + b;
}

int add(int a, int b){
return a + b;
}

int main(int argc, char* argv[]){

int x = add(1, 2);

printf("result of 1+2 is %d\n" , x);

printf("Now HOOKING add using fake_add\n");

DWORD dwOldProtect, dwTempProtect;
//获取HOOK函数和原始函数地址
LPVOID originAdd = add, fakeAdd = fake_add;
printf("\tModifying Memory Protection to Read/Write\n");
if (VirtualProtect(originAdd, 5, 64, &dwOldProtect)){
//获取当前的进程句柄。类似于一种“对象”
HANDLE currentProcessHandle = GetCurrentProcess();
//构造字节码
BYTE shellCode[5] = { 0 };
shellCode[0] = 0xe9;//jmp指令

//jmp跳转的目的地址计算:HOOK函数地址 - 原始函数地址 - 5
*((DWORD*)(&shellCode[1])) = (DWORD)fake_add - (DWORD)add - 5;

DWORD dwWritten = 0;
printf("\tWriting Shell Code to add funcion\n");
//写入该段内存
if (WriteProcessMemory(currentProcessHandle, originAdd, shellCode, 5, &dwWritten) && dwWritten == 5){
MessageBoxA(NULL, "Write Hook Success !", "Hook add", 0);
}

//进程句柄使用完毕后务必关闭。
CloseHandle(currentProcessHandle);
}
printf("\tModifying Memory Protection Back to original\n");
VirtualProtect(originAdd, 5, dwOldProtect, &dwTempProtect);

printf("Now Testing our fake functions\n");

x = add(1, 2);

printf("result of 1+2 is %d\n", x);
system("pause");
return 0;
}

方式以下:

  1. 我们要获取messagebos的函数地址

  2. 调用GetModuleHandlle,获取Dll模块(user32.dll的)的基地址

  3. 通过基地址,调用GetProcAddress获得Msg的函数地址

  4. 重定位跳转地址 Dest - MsgCode = offset -5 jmp 跳转到这个偏移即可

  5. .跳转回来的时候则是 dest + offset + 5 = Msgcode +5的位置

IAT hook

改 IAT 表中对应的地址,来达到我们执行自己但是代码的目的

我们构造导入表的时候,将 IAT 表和 INT 表都指向的是函数名称所在的位置,然后在运行的时候,IAT 表中的内容会被替换成对应函数的地址,在调用的时候使用间接 CALL ,来调用其中所储存的地址。

IAT表在pe文件加载前他和INT表结构一样,都指向IMAGE_IMPORT_BY_NAME这个表,

首先操作系统会通过 Name 字段找到当前导入表的名字,然后调用 LoadLibrary 得到句柄,如果没有找到的话会提示找不到 dll 文件, 接着会根据 OriginalFirstThunk 找名字,OriginalFirstThunk 所指向的也就是咱们前面所说的 INT 表,通过 INT 表中的 RVA 地址来找函数的名字。

当然,这里也不一定存储的就是名字,也可能是导出序号,如果是导出序号的话,就会直接调用 GetProcAddress 得到函数地址,然后填写到 FirstThunk 所指向的对应位置,也就是 IAT 表中的对应位置。

如果存储的是名字的话,也就是指向了_IMAGE_IMPORT_BY_NAME结构,跳过第一个的Hint字节,就得到了调用函数的名字,接着还是调用 GetProcAddress 得到函数地址,填写到对应的位置。

pe文件加载前

PE加载后

既然我们明白了 IAT 表的填写方法,那么就可以通过更改 IAT 表中对应的地址,来达到我们执行自己但是代码的目的,同时为了保证原函数的功能不受影响,还需要进行一些其他的处理。 为了能更好的理解原理,我们采用在函数内部 HOOK 自己的方式来进行,如果想要 HOOK 其他程序的话,可以通过注入等方式来进行。

IAT hook思路

我们使用MessageBox函数来举例,为了保证原来的函数不受影响,我们先将函数的地址保存下来,获得函数地址的方法就是前面所描述的过程

接下来就需要找到MessageBox函数所对应在IAT表中的位置,因为已经得到了函数的地址

,那么我们就可以通过对比 IAT 表中所指向的地址与当前地址是否一致,来判断是不是我们想要找的函数。

在对比之前,首先要做的就是找到导入表所在的位置,这个就比较简单了,需要注意的是 Optional 头的大小不是固定的,需要根据 File 头中的SizeOfOptionalHeader来确定,然后通过数据目录中的第二项,就可以找到导入表所在的位置了
在获取句柄以后,把它赋值给了ImageBase变量,是因为所谓的句柄,实际上就是当前进程的起始位置,也就是在不考虑没有抢占到建议装载地址时候的基址。

在找到导入表之后,就需要进行遍历和对比了,因为导入表是依靠一个全零的结构来判断结束的,所以我们就采取对比FirstThunk和OriginalFirstThunk都为0时,来判断结束,然后在其中判断函数地址是否与我们所得到的原函数地址一致,如果一致说明找到了

接下来,还需要处理的就是我们 HOOK 以后需要进行的操作,为了保证程序的稳定,我们需要构造与被 HOOK 的函数一样结构的函数,同时为了保证原函数功能的正常运行,再定义一个函数指针,在自己的功能执行完成后,调用原来程序正常的功能。

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
#include <stdio.h>
#include <Windows.h>
int OldAddr;
int SetIATHook(int OldAddr, int NewAddr)
{
DWORD ImageBase = 0;
PDWORD pFunAddr = 0;
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS pNtHeader = NULL;
PIMAGE_FILE_HEADER pFileHeader = NULL;
PIMAGE_OPTIONAL_HEADER pOptionalHeader = NULL;
PIMAGE_DATA_DIRECTORY pDataDirectory = NULL;
PIMAGE_IMPORT_DESCRIPTOR pImportDirectory = NULL;
ImageBase = (DWORD)GetModuleHandle(NULL);
pDosHeader = (PIMAGE_DOS_HEADER)ImageBase;
pNtHeader = (PIMAGE_NT_HEADERS)(ImageBase + pDosHeader->e_lfanew);
pFileHeader = (PIMAGE_FILE_HEADER)((DWORD)pNtHeader + 4);
pOptionalHeader = (PIMAGE_OPTIONAL_HEADER)((DWORD)pFileHeader + sizeof(_IMAGE_FILE_HEADER));
pDataDirectory = pOptionalHeader->DataDirectory;
pImportDirectory = (PIMAGE_IMPORT_DESCRIPTOR)(ImageBase + (pDataDirectory + 1)->VirtualAddress);
while (pImportDirectory->FirstThunk != 0 && pImportDirectory->OriginalFirstThunk != 0)
{
pFunAddr = (PDWORD)(ImageBase + pImportDirectory->FirstThunk);
while (*pFunAddr)
{
if (OldAddr == *pFunAddr)
{
*pFunAddr = NewAddr;
break;
}
pFunAddr++;
}
pImportDirectory++;
}
return 0;
}
int WINAPI NewMessageBox
(
HWND hWnd,
LPCSTR lpText,
LPCSTR lpCaption,
UINT uType
)
{
typedef int (WINAPI* BOX)(HWND hWnd,LPCSTR lpText,LPCSTR lpCaption,UINT uType);
((BOX)OldAddr)(0, "IAT Hook", "New", 0);
return 0;
}

int main(int argc, char* argv[])
{
OldAddr = (int)GetProcAddress(LoadLibrary("user32.dll"), "MessageBoxA");
MessageBox(0, "test", "Old", 0);
SetIATHook(OldAddr, (int)NewMessageBox);
MessageBox(0, "test", "Old", 0);
return 0;
}

SEH Hook

SSDT Hook

DLL注入技术

本身Hook本身一般来讲,没有太大意义,那么如何Hook其它进程?

  • ​ 若是使用的是SSDT hook,因为是在内核层,对全部的进程都是有效的。
  • ​ 若是是在应用层,如何Hook其它进程?咱们能够将咱们的函数放置到一个DLL中,将此DLL注入到其余进程中,这样其余进程调用DLL中的函数,将不会访问失败。

注入的方式有:

  • 1、远程线程注入

    2、APC注入

    3、注册表注入

    4、ComRes注入

    5、劫持进程创建注入

    6、输入法注入

    7、消息钩子注入

    8、依赖可信任进程注入

使用注册表来注入

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\ 中:

有一个键:AppInit_DLLs,能够包含一组DLL(逗号或空格分隔),将咱们的DLL路径写入此键值中。

建立一个名为LoadAppInit_DLLs,将其值设置为1.

当user32.dll被映射到一个新的进程时,会收到DLL_PROCESS_ATTACH通知。当user32.dll对它进行处理时,会取得上述注册表键值,并调用LoadLibrary载入这个键值存储的全部DLL。

缺点:

基于CUI的应用程序没有用到user32.dll,所以没法加载DLL。

我可能只想注入某些应用中,映射的越多,崩溃的可能性越大。

使用window 系统消息挂钩来注入

为了能让系统消息挂钩正常运行,Microsoft被迫设计出一种机制,让咱们能够将DLL注入到另外一进程空间中(想象一下:调用SetWindowsHookEx安装一个WH_GETMESSAGE钩子,若是让其余进程运行SetWindowsHookEx设置的函数?)

SetWindowsHookEx(WH_GETMESSAGE,GetMsgProc,hInstDll,0);

GetMsgProc在调用此函数的进程空间中,假设为A进程。第三个参数为hInstDll,为当前进程空间中的一个DLL的句柄,此DLL包含了GetMsgProc函数,最后一个参数表示要给哪儿个线程安装,0表示给系统中全部的GUI线程安装挂钩。

接下来会发生什么:

  1. ​ B进程的一个线程准备向一个窗口发送一条消息。
  2. ​ 系统检测到已经安装了一个WH_GETMESSAGE钩子。
  3. ​ 系统检测GetMsgProc所在的DLL是否已经被映射到进程B地址空间中。
  4. ​ 若是还没有映射,系统会强制调用LoadLibrary强制将DLL映射到B进程地址空间中,DLL引用计数加一。
  5. ​ DLL映射到B中的地址可能和A相同,也可能不一样。若是不一样,须要调整GetMsgProc的地址。
  6. ​ GetMsgProc = hInstDll B + (GetMsgProcA – hInstDll A)
  7. ​ 系统在进程B中地址该DLL的引用计数。
  8. ​ 在B空间调用GetMsgProc,在此函数里,能够作API Hook处理,还能够建立一些消息机制,经过A进程进行功能控制。

使用远程线程来注入

线程注入,是通过开启远程线程的方式,将DLL加载到目标宿主进程中的常用方式。

首先Windows中链接库分为两种:

动态链接库DLL、静态链接库LIB。

① 静态链接库:在运行的时候就直接把代码全部加载到程序中,调用方式比如:

#prama comment (lib,”Psapi.lib”)

② 动态链接库:在需要的时候加载,加载方式为:

——使用LoadLibrary动态加载DLL

——使用GetProcAddress获取DLL中导出函数的指针

——最后用FreeLibrary卸载指定的DLL

动态链接库设计的目的是为了动态加载功能,动态地释放内存,节约内存,方便每一个程序的4GB内存的管理,4GB有2GB用来放系统内核,即系统大量的DLL。其本质上也是一个可以被加载的程序。同时系统对于DLL只会保存一份。

编写注入dll

vs 创建桌面向导,选择dll

关于DLL入口主函数第二个参数ul_reason_for_call,即DLL四种当前的状态:

创建头文件

1
2
3
4
5
#pragma once
#define ExportFunc _declspec(dllexport)

extern "C" ExportFunc int Add(int a, int b);
extern "C" ExportFunc int Add(int a, int b);

Dllmain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"

BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
MessageBox(NULL, L"Hello daji!", L"Welcome!", NULL);
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}


导出函数

1
2
3
4
5
6
7
8
9
10
11
#include "ExportDLL.h"
#include"pch.h"
int Add(int a, int b)
{
return a + b;
}

int Sub(int a, int b)
{
return a + b;
}

这里需要说一下注入的可行性:

流程:目标进程-传入DLL地址-开启远程线程-加载DLL-实现DLL注入

在另外一个进程中建立远程线程:

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
);

只要使用这个远程线程调用LoadLibrary,来加载咱们的DLL,就能达到DLL注入的目的。例如:

CreateRemoteThread(hProcessRemote,NULL, 0, LoadLibraryW,L”C:\MyLib.dll”, 0, NULL);

当远程线程在远程进程执行时,会当即调用LoadLibraryW,并传入DLL路径。

但并无这么简单

CreateRemoteThread(hProcessRemote,NULL, 0, LoadLibraryW,L”C:\MyLib.dll”, 0, NULL);

但存在两个问题:

  1. ​ 第一个问题是:

上面咱们已经讲过LoadLibraryW是个导入函数,使用地址引用IAT的方式来调用,显然这个地址在不一样的PE文件里是不一样的。在其余进程中,这个地址是不同的。

对CreateRemoteThread调用,假定本进程和远程进程中,Kernel32.dll被映射到地址空间是同一内存。(虽然是假定,到目前为止,都是同一地址,重启会变,但启动后不变)。

能够改变为:

PTHREAD_START_ROUNTINE pfnThreadRtn = (PTHREAD_START_ROUNTINE)GetProcAddress(GetModuleHandle(_T(“Kernel32”)), “LoadLibraryW”);

CreateRemoteThread(hProcessRemote,NULL, 0, pfnThreadRtn,L”C:\MyLib.dll”, 0, NULL);

  1. ​ 第二个问题:

L”C:\MyLib.dll”字符串在调用进程中,目标进程并无这个字符串,若是在目标程序执行,此地址则是执行一个未知地址,可能会崩溃。

解决这个问题的办法,须要把本地字符串放到远程进程去。调用VirtualAllocEx可让一个进程在另外一个进程中分配一块空间。WriteProcessMemory能够将字符串从本进程复制到远程进程中。

突破session 0的远线程注入


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1204342476@qq.com

💰

×

Help us with donation