最近看了《加密与解密》,跟着大佬们的思路学习了Hook相关知识,如理解有误请不吝赐教,以免误导他人。
API函数都保存在操作系统提供的DLL文件中,当在程序中调用某个API函数并运行程序后,程序会隐式地将API函数所在的DLL文件加载入内存中,这样,程序就会像调用自己的函数一样调用API。Inline Hook这种方法是在程序流程中直接进行嵌入jmp指令来改变流程的。
简而言之,就是将函数开头修改为jmp指令,跳转到我们自定义的函数上去。
首先用CreateProcessA API写一个测试程序,功能很简单,程序启动后,按下任意键,调用CreateProcessA创建进程,为了直观这里是直接弹一个计算器:
下面就HOOK CreateProcessA:
将生成的EXE拖到Xdbg中,定位到CreateProceessA这里,在调用CreateProcessA前有一段汇编代码:
mov edi edi,
push ebp
mov ebp,esp
16进制:8B FF 55 8B EC
HOOK的话肯定要准备一个自定义的函数, call addr ,假如它的地址为12345678,那么我们HOOK的操作就是将上面的命令替换为:JMP 12345678,也就是e9 addr。
解除的HOOK的话就是将替换的字节恢复。
Inline Hook流程
代码如下,关键步骤已注释:
效果如下:
IAT Hook是 Address Hook的一种方式,顾名思义就是通过修改函数的地址进行Hook。
IAT(Import Address Table,输入表)是PE中的一种结构,如图:
再用一张图来理解导入表结构:
需要注意的是:因为IAT具体指某个PE模块的IAT,所以他的作用范围只针对被Hook的模块,且必须在以静态链接的方式调用API时才会被Hook,所以它的作用范围只针对被Hook的模块,且必须以静态链接的方式调用API时才会被Hook,在使用Loadlibrary或GetProcAddress进行动态调用时不受影响。要想对已加载的所有模块起作用,就必须遍历进程内的所有模块,对目标API进行Hook。
现在开始Hook,这里是通过注入来Hook其他的程序,将代码写在DLL里面
为了保证原来的函数不受影响,我们先将要被Hook的函数的地址保存下来:
然后就是解析PE文件,获取函数导入表中的函数地址表,并替换:
解析PE文件前面已经学习过了,下面过程直接走一遍:
pDosHeader->e_lfanew的数据类型为DWORD,前面定义了pDosHeader的数据类型为PIMAGE_DOS_HEADER,要和pDosHeader相加需将pDosHeader类型转换为DWORD,最后再将得到的结果转换为PIMAGE_NT_HEADERS。
获取扩展头
从扩展头中获取数据目录表中的导入表
获取导入表前,要先找到它的偏移:
获取导入表:
有多个导入表结构,所以要遍历每个导入表:
因为导入表是依靠一个全零的结构来判断结束的,所以我们就采取对比pImportTable->Characteristics为0和pImportTable->FirstThunk为NULL时,来判断结束,然后在其中判断函数地址是否与我们所得到的原函数地址一致,如果一致说明找到了:
最后,为了保证程序的稳定,我们需要构造与被 HOOK 的函数一样结构的函数,同时为了保证原函数功能的正常运行,再定义一个函数指针,在自己的功能执行完成后,调用原来程序正常的功能:
要HOOK的API是MessageBoxA (X32),写一个简单的程序:
要达到的目的是当测试程序运行时,注入DLL,Hook住MessageBoxA,使其指向CreateProcessA api:
正常运行:
Hook后:
在代码编译为程序后,虚函数表就是一个固定的表了,它位于PE的.data段。在对虚函数表进行Hook时,虽然原理也是查找原函数的位置,修改页面属性,写入Detour函数这样的过程,但是虚函数有些特殊。
同样是Hook地址,它不能像IAT Hook那样直接定义一个函数来替换目标函数,而必须把它定义为类的成员函数。我们知道面向对象的三要素:封装、继承、多态 。在多态里有类特殊的是虚函数(以virtual修饰), 32位系统下,对象里有4个字节保存虚表的数组,其值为每一项虚函数的地址。
针对虚函数的HOOK就是通过保存对象中的虚表的值,针对每一项进行替换。
这次虚函数的Hook就在程序本身执行了,dll注入的方式需要对程序进行逆向分析,暂时放一下:
效果如下:
热补丁Hook是微软提供的一种安全的Hook的机制,也是将函数开头修改为jmp指令,跳转到自定义的函数地址执行,和IAT Hook类似却又有不同。
以IAT Hook中的测试程序为例:
我们可以看到CreateProcessA函数的首字节为 mov edi,edi(88 FF),这句汇编意思就是将edi的值放入edi,实际上并没有什么用。
我们还看到在这个API上边有大段的int3 中断。这就给了我们一种新的Hook思路,即将前两个字节改为短跳转指令(EB E9),使其跳到函数上边五字节处,这五个字节的int3中断实际上就是一段空闲空间:
然后再将这五个字节改为长跳转指令(E9 xxxxxxxx)。这样,即使Hook失败,也不影响函数的继续执行。
这样,hook函数的时候,先是一个短跳跳到自定义的函数然后执行。如果要恢原流程,则找到函数地址并+2,直接跳过E8 F9 ,从push ebp开始执行。
然后开始写代码,有了其他Hook方式的经验,这次直接写一个通用的Hook:
还是以这个测试程序为例:
将生成的dll注入,hook后,当执行到MessageboxA时会劫持住它原来流程去执行CreateProcessA,如图弹出了notepad.exe:
补充:并不是所有的api都能使用HotPatch的方式进行Hook,比如CreateProcess,当然或许是我代码写错了:)
简单介绍一下windows的异常机制:
Intel在386开始的IA-32家族处理器中引入了异常和中断。中断是指外部硬件设备或异步事件引发的,而异常是由内部事件产生的,又可分为故障,陷阱和终止三类。故障和陷阱是可恢复的,终止是不可恢复的,如果出现了了终止异常,则需要重启操作系统解决。
Windows中主要的异常处理机制:VEH、SEH、C++EH。
SEH:结构化异常处理。就是平时用的__try
__finally
__try
__except
,是对c的扩展。
VEH:向量异常处理。一般来说用AddVectoredExceptionHandler
去添加一个异常处理函数,可以通过第一个参数决定是否将VEH函数插入到VEH链表头,插入到链表头的函数先执行,如果为1,则会最优先执行。
C++EH是C++提供的异常处理方式,执行顺序将排在最后。
在用户模式下发生异常时,异常处理分发函数在内部会先调用遍历 VEH 记录链表的函数, 如果没有找到可以处理异常的注册函数,再开始遍历 SEH 注册链表。
主要区分一下SEH和VEH:
然后开始写代码,主要流程如下:
在调试模式下,当MessageBox被调用时,会触发int3异常:
将项目编译为PE文件再次运行,已经成功Hook:
代码如下:
如果要实现Hook其他程序,只需将函数写到dll文件中然后进行注入:
BOOL
CreateProcessR(char
*
szExePath) {
SECURITY_ATTRIBUTES psa
=
{
0
};
SECURITY_ATTRIBUTES tsa
=
{
0
};
STARTUPINFO si
=
{ sizeof(si) };
PROCESS_INFORMATION pi;
BOOL
Ret;
Ret
=
CreateProcessA(szExePath, NULL, &psa, &tsa, false,
0
, NULL, NULL, &si, &pi);
/
/
TerminateProcess(pi.hProcess,
0
);
/
/
结束进程
return
Ret;
}
int
main(
int
argc,char
*
argv[]) {
system(
"pause"
);
CreateProcessR(EXE_PATH);
return
0
;
}
BOOL
CreateProcessR(char
*
szExePath) {
SECURITY_ATTRIBUTES psa
=
{
0
};
SECURITY_ATTRIBUTES tsa
=
{
0
};
STARTUPINFO si
=
{ sizeof(si) };
PROCESS_INFORMATION pi;
BOOL
Ret;
Ret
=
CreateProcessA(szExePath, NULL, &psa, &tsa, false,
0
, NULL, NULL, &si, &pi);
/
/
TerminateProcess(pi.hProcess,
0
);
/
/
结束进程
return
Ret;
}
int
main(
int
argc,char
*
argv[]) {
system(
"pause"
);
CreateProcessR(EXE_PATH);
return
0
;
}
/
/
Myhook.cpp
CHOOK::CHOOK()
{
MyFuncaAddress
=
NULL;
memset(MyOldBytes,
0
,
5
);
memset(MyNewBytes,
0
,
5
);
}
CHOOK::~CHOOK()
{
UnHOOK();
MyFuncaAddress
=
NULL;
memset(MyOldBytes,
0
,
5
);
memset(MyNewBytes,
0
,
5
);
}
BOOL
CHOOK::Hook(LPSTR pszModuleName, LPSTR pszFuncName, PROC pfnHookFunc)
{
HMODULE hModule
=
GetModuleHandle(pszModuleName);
MyFuncaAddress
=
(PROC)GetProcAddress(hModule, pszFuncName);
if
(MyFuncaAddress
=
=
NULL) {
return
FALSE;
}
/
/
读地址 将原来的
5
个字节的数据保存
ReadProcessMemory(GetCurrentProcess(), MyFuncaAddress, MyOldBytes,
5
,
0
);
/
/
JMP ADDRESS JMP
123456789
E9
MyNewBytes[
0
]
=
'\xE9'
;
*
(DWORD
*
)(MyNewBytes
+
1
)
=
(DWORD)pfnHookFunc
-
(DWORD)MyFuncaAddress
-
5
;
WriteProcessMemory(GetCurrentProcess(), MyFuncaAddress, MyNewBytes,
5
,
0
);
return
TRUE;
}
VOID CHOOK::UnHOOK()
{
if
(MyFuncaAddress !
=
NULL) {
WriteProcessMemory(GetCurrentProcess(), MyFuncaAddress, MyOldBytes,
5
,
0
);
}
return
VOID();
}
BOOL
CHOOK::ReHook()
{
if
(MyFuncaAddress !
=
NULL) {
WriteProcessMemory(GetCurrentProcess(), MyFuncaAddress, MyNewBytes,
5
,
0
);
}
return
0
;
}
/
/
Myhook.cpp
CHOOK::CHOOK()
{
MyFuncaAddress
=
NULL;
memset(MyOldBytes,
0
,
5
);
memset(MyNewBytes,
0
,
5
);
}
CHOOK::~CHOOK()
{
UnHOOK();
MyFuncaAddress
=
NULL;
memset(MyOldBytes,
0
,
5
);
memset(MyNewBytes,
0
,
5
);
}
BOOL
CHOOK::Hook(LPSTR pszModuleName, LPSTR pszFuncName, PROC pfnHookFunc)
{
HMODULE hModule
=
GetModuleHandle(pszModuleName);
MyFuncaAddress
=
(PROC)GetProcAddress(hModule, pszFuncName);
if
(MyFuncaAddress
=
=
NULL) {
return
FALSE;
}
/
/
读地址 将原来的
5
个字节的数据保存
ReadProcessMemory(GetCurrentProcess(), MyFuncaAddress, MyOldBytes,
5
,
0
);
/
/
JMP ADDRESS JMP
123456789
E9
MyNewBytes[
0
]
=
'\xE9'
;
*
(DWORD
*
)(MyNewBytes
+
1
)
=
(DWORD)pfnHookFunc
-
(DWORD)MyFuncaAddress
-
5
;
WriteProcessMemory(GetCurrentProcess(), MyFuncaAddress, MyNewBytes,
5
,
0
);
return
TRUE;
}
VOID CHOOK::UnHOOK()
{
if
(MyFuncaAddress !
=
NULL) {
WriteProcessMemory(GetCurrentProcess(), MyFuncaAddress, MyOldBytes,
5
,
0
);
}
return
VOID();
}
BOOL
CHOOK::ReHook()
{
if
(MyFuncaAddress !
=
NULL) {
WriteProcessMemory(GetCurrentProcess(), MyFuncaAddress, MyNewBytes,
5
,
0
);
}
return
0
;
}
/
/
dllmain.cpp : 定义 DLL 应用程序的入口点。
CHOOK MyHookObject;
BOOL
WINAPI
MyCreateProcessA(
_In_opt_ LPCSTR lpApplicationName,
_Inout_opt_ LPSTR lpCommandLine,
_In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_
BOOL
bInheritHandles,
_In_ DWORD dwCreationFlags,
_In_opt_ LPVOID lpEnvironment,
_In_opt_ LPCSTR lpCurrentDirectory,
_In_ LPSTARTUPINFOA lpStartupInfo,
_Out_ LPPROCESS_INFORMATION lpProcessInformation
)
{
if
(MessageBox(NULL,
"是否拦截"
,
"Notice"
, MB_YESNO)
=
=
IDYES)
{
MessageBox(NULL,
"程序已拦截"
,
"Notice"
, MB_OK);
}
else
{
MyHookObject.UnHOOK();
CreateProcessA(
lpApplicationName,
lpCommandLine,
lpProcessAttributes,
lpThreadAttributes,
bInheritHandles,
dwCreationFlags,
lpEnvironment,
lpCurrentDirectory,
lpStartupInfo,
lpProcessInformation
);
MyHookObject.ReHook();
}
return
true;
}
BOOL
APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
MyHookObject.Hook((LPSTR)
"Kernel32.dll"
, (LPSTR)
"CreateProcessA"
,(PROC)MyCreateProcessA);
break
;
case DLL_THREAD_ATTACH:
break
;
case DLL_THREAD_DETACH:
break
;
case DLL_PROCESS_DETACH:
MyHookObject.UnHOOK();
break
;
}
return
TRUE;
}
/
/
dllmain.cpp : 定义 DLL 应用程序的入口点。
CHOOK MyHookObject;
BOOL
WINAPI
MyCreateProcessA(
_In_opt_ LPCSTR lpApplicationName,
_Inout_opt_ LPSTR lpCommandLine,
_In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_
BOOL
bInheritHandles,
_In_ DWORD dwCreationFlags,
_In_opt_ LPVOID lpEnvironment,
_In_opt_ LPCSTR lpCurrentDirectory,
_In_ LPSTARTUPINFOA lpStartupInfo,
_Out_ LPPROCESS_INFORMATION lpProcessInformation
)
{
if
(MessageBox(NULL,
"是否拦截"
,
"Notice"
, MB_YESNO)
=
=
IDYES)
{
MessageBox(NULL,
"程序已拦截"
,
"Notice"
, MB_OK);
}
else
{
MyHookObject.UnHOOK();
CreateProcessA(
lpApplicationName,
lpCommandLine,
lpProcessAttributes,
lpThreadAttributes,
bInheritHandles,
dwCreationFlags,
lpEnvironment,
lpCurrentDirectory,
lpStartupInfo,
lpProcessInformation
);
MyHookObject.ReHook();
}
return
true;
}
BOOL
APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
MyHookObject.Hook((LPSTR)
"Kernel32.dll"
, (LPSTR)
"CreateProcessA"
,(PROC)MyCreateProcessA);
break
;
case DLL_THREAD_ATTACH:
break
;
case DLL_THREAD_DETACH:
break
;
case DLL_PROCESS_DETACH:
MyHookObject.UnHOOK();
break
;
}
return
TRUE;
}
OldMesageBoxA
=
(FncMessageBoxA)GetProcAddress(GetModuleHandleA(
"user32.dll"
),
"MessageBoxA"
);
OldMesageBoxA
=
(FncMessageBoxA)GetProcAddress(GetModuleHandleA(
"user32.dll"
),
"MessageBoxA"
);
PIMAGE_DOS_HEADER pDosHeader
=
(PIMAGE_DOS_HEADER)GetModuleHandleA(NULL);
/
/
当传入参数为NULL时,获取的是PE文件的imagebase,通过类型强制转换就获取到了dos_header
PIMAGE_DOS_HEADER pDosHeader
=
(PIMAGE_DOS_HEADER)GetModuleHandleA(NULL);
/
/
当传入参数为NULL时,获取的是PE文件的imagebase,通过类型强制转换就获取到了dos_header
PIMAGE_NT_HEADERS pNtHeader
=
(PIMAGE_NT_HEADERS)((DWORD)pDosHeader
+
pDosHeader
-
>e_lfanew);
PIMAGE_NT_HEADERS pNtHeader
=
(PIMAGE_NT_HEADERS)((DWORD)pDosHeader
+
pDosHeader
-
>e_lfanew);
PIMAGE_OPTIONAL_HEADER pOptionalHeader
=
(PIMAGE_OPTIONAL_HEADER)&pNtHeader
-
>OptionalHeader;
/
/
扩展头是NT头的一个成员
PIMAGE_OPTIONAL_HEADER pOptionalHeader
=
(PIMAGE_OPTIONAL_HEADER)&pNtHeader
-
>OptionalHeader;
/
/
扩展头是NT头的一个成员
DWORD dwImportTableOffset
=
pOptionalHeader
-
>DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
/
/
获取导入表偏移
DWORD dwImportTableOffset
=
pOptionalHeader
-
>DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
/
/
获取导入表偏移
PIMAGE_IMPORT_DESCRIPTOR pImportTable
=
(PIMAGE_IMPORT_DESCRIPTOR)(DWORD)pDosHeader
+
dwImportTableOffset;
PIMAGE_IMPORT_DESCRIPTOR pImportTable
=
(PIMAGE_IMPORT_DESCRIPTOR)(DWORD)pDosHeader
+
dwImportTableOffset;
DWORD
*
pFirstThunk;
/
/
遍历导入表结构
while
(pImprotTable
-
>Characteristics && pImprotTable
-
>FirstThunk !
=
NULL)
{
pFirstThunk
=
(DWORD
*
)(pImprotTable
-
>FirstThunk
+
(DWORD)pDosHeader);
while
(
*
(DWORD
*
)pFirstThunk !
=
NULL)
{
/
/
如果相当了,就说明当前的数组元素就是我们要找的函数地址表中的函数地址
if
(
*
(DWORD
*
)pFirstThunk
=
=
(DWORD)OldMesageBoxA)
{
DWORD oldProtected;
VirtualProtect(pFirstThunk,
0x1000
, PAGE_EXECUTE_READWRITE, &oldProtected);
DWORD dwFuncAddr
=
(DWORD)MyMessageBoxA;
memcpy(pFirstThunk, (DWORD
*
)&dwFuncAddr,
4
);
VirtualProtect(pFirstThunk,
0x1000
, oldProtected, &oldProtected);
}
pFirstThunk
+
+
;
}
pImprotTable
+
+
;
}
DWORD
*
pFirstThunk;
/
/
遍历导入表结构
while
(pImprotTable
-
>Characteristics && pImprotTable
-
>FirstThunk !
=
NULL)
{
pFirstThunk
=
(DWORD
*
)(pImprotTable
-
>FirstThunk
+
(DWORD)pDosHeader);
while
(
*
(DWORD
*
)pFirstThunk !
=
NULL)
{
/
/
如果相当了,就说明当前的数组元素就是我们要找的函数地址表中的函数地址
if
(
*
(DWORD
*
)pFirstThunk
=
=
(DWORD)OldMesageBoxA)
{
DWORD oldProtected;
VirtualProtect(pFirstThunk,
0x1000
, PAGE_EXECUTE_READWRITE, &oldProtected);
DWORD dwFuncAddr
=
(DWORD)MyMessageBoxA;
memcpy(pFirstThunk, (DWORD
*
)&dwFuncAddr,
4
);
VirtualProtect(pFirstThunk,
0x1000
, oldProtected, &oldProtected);
}
pFirstThunk
+
+
;
}
pImprotTable
+
+
;
}
typedef
int
(WINAPI
*
FncMessageBoxA)(
_In_opt_ HWND hWnd,
_In_opt_ LPCSTR lpText,
_In_opt_ LPCSTR lpCaption,
_In_ UINT uType);
FncMessageBoxA OldMesageBoxA
=
NULL;
int
WINAPI MyMessageBoxA(
_In_opt_ HWND hWnd,
_In_opt_ LPCSTR lpText,
_In_opt_ LPCSTR lpCaption,
_In_ UINT uType)
{
SECURITY_ATTRIBUTES psa
=
{
0
};
SECURITY_ATTRIBUTES tsa
=
{
0
};
STARTUPINFO si
=
{ sizeof(si) };
PROCESS_INFORMATION pi;
CreateProcessA(EXE_PATH, NULL, &psa, &tsa, false,
0
, NULL, NULL, &si, &pi);
return
0
;
}
typedef
int
(WINAPI
*
FncMessageBoxA)(
_In_opt_ HWND hWnd,
_In_opt_ LPCSTR lpText,
_In_opt_ LPCSTR lpCaption,
_In_ UINT uType);
FncMessageBoxA OldMesageBoxA
=
NULL;
int
WINAPI MyMessageBoxA(
_In_opt_ HWND hWnd,
_In_opt_ LPCSTR lpText,
_In_opt_ LPCSTR lpCaption,
_In_ UINT uType)
{
SECURITY_ATTRIBUTES psa
=
{
0
};
SECURITY_ATTRIBUTES tsa
=
{
0
};
STARTUPINFO si
=
{ sizeof(si) };
PROCESS_INFORMATION pi;
CreateProcessA(EXE_PATH, NULL, &psa, &tsa, false,
0
, NULL, NULL, &si, &pi);
return
0
;
}
int
main() {
system(
"pause"
);
MessageBoxA(
0
,
0
,
0
,
0
);
system(
"pause"
);
return
0
;
}
int
main() {
system(
"pause"
);
MessageBoxA(
0
,
0
,
0
,
0
);
system(
"pause"
);
return
0
;
}
class
MyClass
{
public:
MyClass();
~MyClass();
virtual void
print
();
private:
};
MyClass::MyClass()
{
}
MyClass::~MyClass()
{
}
void MyClass::
print
()
{
printf(
"hello\r\n"
);
}
void Myfunc() {
MessageBoxA(NULL,
"hello"
,
"title"
, NULL);
}
int
main() {
MyClass obj;
MyClass& vobj
=
obj;
vobj.
print
();
/
/
寻找虚表指针,虚表指针通常情况下位于对象的头
4
字节上
int
nAddr
=
*
(
int
*
)&obj;
/
/
更改内存属性
DWORD dwOldProtect
=
0
;
VirtualProtect((void
*
)nAddr,
0x100
, PAGE_EXECUTE_READWRITE,&dwOldProtect);
/
/
将自己的函数地址替换过去,前面说到了数组的值就是虚函数的地址,我们写的测试例子中就写了一个虚函数,所以其地址就是第一个值
(
*
(
int
*
)nAddr)
=
(
int
)Myfunc;
VirtualProtect((void
*
)nAddr,
0x100
, dwOldProtect,&dwOldProtect);
vobj.
print
();
system(
"pause"
);
}
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2021-12-23 11:10
被soloz编辑
,原因: