一篇文章带你理解HOOK技术

0x0 前言

         1.这是《一篇文章带你….》系列的第三篇,也是自己的学习总结,很多不懂的地方找的论坛前辈的资料。代码都是自己手敲,边敲边注释,对一些细节做了微调。前两篇是一篇文章带你学会Armadillo脱壳一篇文章带你理解PE三表

         2.几个驱动相关的HOOK,从代码层面上讲理解的不是很深刻。但是手敲了一边除了一些驱动相关的知识,HOOK原理上应该有所领悟。

         3.还是一样,不提供源码下载,驱动相关的HOOK,前辈们都写有源码。不作伸手党。

         4.想来想去,这篇文章已经停滞1个多月了,今天草草结笔。关于HOOK检测部分估计需要鸽了。有空再学习吧。看雪第四版有源码。

         5.这篇文章是新瓶装旧酒,大佬别喷我。下一篇文章预计写注入,代码已经写完了,就剩下总结了,同样的还是新瓶装旧酒。旨在学习。下下一篇我也不知道写什么?加壳?花指令??管他呢。

         6.唉,我什么时候才能成为像各位一样的大佬啊^_^

0x1 AddressHook

0x1.1 IAT_HOOK

         IAT是程序中存储导入函数地址的数据结构,如果HOOK了导入函数地址。就可以在函数调用的时候,将函数流程HOOK到我们指定的流程。但是我个人觉得这种方式最好要结合DLL注入的方式,如果单纯的使用HOOK,那么就需要将需要执行的操作的shellcode写入目标进程,如果操作复杂,可能需要的shellcode量特别大,所以我们需要借助DLL注入,这样就将我们需要执行的代码写入进程内部,在HOOK的Detour函数只需要实现LoadLibrary的操作。

         IATHOOK的基本原理就是通过修改程序IAT数据结构,将原始调用API函数地址Target函数地址修改为Detour函数地址。所以IAT_HOOK需要实现以下几个步骤:

  • 构造Detour函数
  • 获取Target函数地址
  • 通过PE获取Target函数所在的IAT的地址
  • 保存原始的IAT地址和IAT地址所存储的内容
  • 修改IAT地址中的数据
  • 如果需要调用原来API函数,可以直接使用保存的API地址,可以就保证了HOOK的有效性

         首先需要构造Detour函数,为了堆栈平衡和一些其他原因,最好Detour函数的函数原型和Target函数原型保持一致。

1
2
3
4
5
6
7
typedef int(WINAPI *PFN_MessageBoxA)(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType);
int WINAPI My_MessageBoxA(
HWND hWnd,
LPCTSTR lpText,
LPCTSTR lpCaption,
UINT uType
)

         这个函数的内容可以任意设置,主要执行两个方面的操作,一是执行我们想进行的操作,二是控制Target函数的返回值。为了简单期间,设置MessageBox作为HOOK,标志,MessageBox函数地址可以使用保存的API函数

1
2
3
4
bReturn = OldMessageBox(NULL, "You Are Hooked", "Warning", MB_OK);
//2.你可以控制API函数的返回值
BOOL bReturn = FALSE;
return bReturn;

         第二歩是获取Target函数地址,这一步的目的是为了遍历IAT的时候比较IAT中所存储的Target函数地址。以便找到存放目标函数的IAT地址。

1
2
3
4
5
6
7
8
9
10
||#ifdef _WIN64
ULONG_PTR TargetFunAddr = (ULONG_PTR)GetProcAddress(hModule, szFuncName);
PULONG_PTR lpAddr = NULL;
SIZE_T size = sizeof(PULONG_PTR);
||#else
ULONG32 TargetFunAddr = (ULONG32)GetProcAddress(hModule, szFuncName);
PULONG32 lpAddr = NULL;
SIZE_T size = sizeof(PULONG32);
||#endif
//MARKDOWN原因,不能以#开头,所以加个||符号,实际代码不存在||符号

         第三步是获取Target函数的IAT地址,首先需要获取导入表的RVA,这里可以使用函数ImageDirectoryEntryToData获取

1
2
3
4
PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR) ImageDirectoryEntryToData(hModToHook,
TRUE,
IMAGE_DIRECTORY_ENTRY_IMPORT,
&ulSize);

         PE装载器已经将PE文件载入内存,可以使用IAT获取函数地址,所以通过FirstThunk指向的IAT遍历Target函数。需要判断DLLNAME是否是Target函数所在的模块,也就是说需要确定IID,毕竟一个IID对应一个DLL

1
2
3
4
5
6
7
8
9
10
11
12
13
while (pImportDescriptor->FirstThunk)
{
//存放DllName
szModName = (char*)((PBYTE)hModToHook + pImportDescriptor->Name);
printf("[*]Cur Module Name:%s\n", szModName);
//比较DLLName与目标DLL是否相同 使用stricmp函数是不区分大小写的
if (stricmp(szModName, szModuleName) != 0)
{
pImportDescriptor++;
continue;
}
...
}

         First指向的是一个IAT结构,存储的API函数地址。所以可以利用PIMAGE_THUNK_DATA(IAT)遍历Target函数。如果找到了最好保存修改的IAT地址和数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PIMAGE_THUNK_DATA pThunkData = (PIMAGE_THUNK_DATA)((BYTE*)hModToHook + pImportDescriptor->FirstThunk);
while (pThunkData->u1.Function)
{
if ((*lpAddr) == TargetFunAddr)
{
//保存数据,修改为Detour函数地址
}
}
//最好保存一下
if (pThunkPointer != NULL) //保存修改内存的地址
{
*pThunkPointer = lpAddr;
}
if (pOriginalFuncAddr != NULL) //保存修改内存的数据,也就是Target函数的地址
{
*pOriginalFuncAddr = *lpAddr;
}

         这里我实现了X64和X86的兼容,但是在实现X86的时候,出现了内存访问异常在VirtuallProtest处,但是在debug模式下程序没奔溃,需要那位大佬可以解答一下

0x1.2 EAT_HOOK

         使用EAT_HOOK需要注意一下两点:第一:EAT存储的是函数地址的偏移,所以在HOOK EAT的时候需要加上基地址,在写入EAT的时候,Detour地址需要减去BaseAddress。第二,EAT不对隐式链接起作用,只对显示链接起作用,也就是说对于那种GetProcAddress的那种调用起作用。

         EAT_HOOK的原理和IAT_HOOK类似,都是通过修改函数地址数据从而HOOK。EAT_HOOK,也需要进行以下步骤:

  • 1.获取Target函数在HookModule上的RVA
  • 2.获取导出函数数组首地址
  • 3.遍历查找Target函数RVA
  • 4.切记在修改函数地址之前,需要保存EAT地址和原函数地址、
  • 5.将Detour函数地址写入EAT

         首先是获取Target函数RVA,因为EAT存的是函数的RVA,所以,我们需要获取Target函数RVA

1
2
3
4
5
//1.获取Target函数在HookModule上的RVA
ULONG_PTR TargetFunAddr = NULL;
TargetFunAddr = (ULONG_PTR)GetProcAddress(hModToHook, szFuncName);
ULONG_PTR TargetFunRVA = NULL;
TargetFunRVA = (ULONG_PTR)(TargetFunAddr - (ULONG_PTR)hModToHook);

         第二歩是获取导出函数数组首地址,在EAT->AddressOfFunctions可以获取导出函数地址数组AddressOfFunctions的首地址

1
2
3
4
5
6
7
PIMAGE_EXPORT_DIRECTORY pExportDir = NULL;
pExportDir = (PIMAGE_EXPORT_DIRECTORY)ImageDirectoryEntryToData(hModToHook,//BaseAddress
TRUE,
IMAGE_DIRECTORY_ENTRY_EXPORT, //Type
&ulSize); //接收数据的大小
ULONG* FuncAddr = NULL;
FuncAddr = (ULONG*)((BYTE*)hModToHook + pExportDir->AddressOfFunctions); //导出函数数组首地址

         第三步是在EAT在寻找Target函数的RVA

1
2
3
4
5
6
7
8
9
if (FuncAddr[i] == TargetFunRVA)
{
//修改内存保护属性
DWORD OldProtect = NULL;
if (VirtualProtect(&FuncAddr[i], sizeof(ULONG*), PAGE_EXECUTE_READWRITE, &OldProtect))
{
//修改保存数据
}
}

         第四步是保存EAT地址和Target函数RVA

1
2
*pAddrPointer = (PULONG_PTR)&FuncAddr[i];
*pOriginalFuncAddr = FuncAddr[i];

         EAT保存的是Target函数的RVA,这一定要记住。所以在写入Detour函数的时候,是需要减去BaseAddress的。

1
2
3
//5.将Detour函数地址写入EAT
//因为EAT里面保存的是函数地址RVA值,所以在写入Detour函数地址需要减去BaseAddress
FuncAddr[i] = (ULONG)((ULONG_PTR)DetourFunc-(ULONG_PTR)hModToHook);

0x1.3 VirtualFunctionHook

         C++虚函数存在的意义是为了方便使用多态性。在实现虚函数Hook的时候需要注意如下问题:1.在构建DetourFun函数的时候,一定要构造DetourClass,因为在调用虚函数的时候使用了Thiscall的函数调用约定,如果直接调用detourfun函数应该使用的标准调用约定,两者不统一,会出错。2.当使用Trampolinefun回调的时候,需要重新实例化一个TrampolineClass。

         第一步:仍然是构造DetourClass类和TrampolineClass类

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
//因为使用了This的调用方法,所以在Hook的时候同时需要创建DetourClass类,保证函数约定是一致的
class DetourClass
{
public:
virtual int DetourFun(int a, int b);
};
class TrampolineClass
{
public:
virtual int TrampolineFun(int a,int b)
{
printf("TrampolineFun");
return a + b;
}
};
//此处构造DetourFun
int DetourClass::DetourFun(int a, int b)
{
//此处执行自定义操作
MessageBox(NULL, "Hooked", "warning", MB_OK);
//调用TrampolineFun,首先需要将TrampolineClass实例化
TrampolineClass *pTrampoline = new TrampolineClass;
int iRet = pTrampoline->TrampolineFun(a, b);
delete pTrampoline;
return iRet+10;
}

         第二歩:将Target函数地址保存在TableTrampoline虚表中,方便回调。这时候需要获取两个值,第一个TableTrampoline虚表,第二个TargetFun地址。由于TableTrampoline虚函数表在类的起始位置。所以类的地址就是虚函数表的地址,第二,TargetFun函数地址位于虚函数中,存储在类似于数组的结构,可以用其索引指向获取虚函数地址

1
2
3
4
5
6
7
8
9
10
11
12
//获取虚表地址vfTableToHook
base base;
printf("[*]pBase=0x%x\n", &base);
ULONG_PTR *vfTableToHook = (ULONG_PTR*)*(ULONG_PTR*)&base;
//获取Trampoline虚表地址,用于回调
ULONG_PTR *vfTableTrampoline = (ULONG_PTR*)*(ULONG_PTR*)&Trampoline;
//第一次修改,用于保存原始的Target函数地址
//修改内存保护属性
VirtualProtect(vfTableTrampoline, sizeof(ULONG_PTR), PAGE_EXECUTE_READWRITE, &dwOldProtect);
vfTableTrampoline[0] = (ULONG_PTR)GetClassVirtualFnAddress(&base, 0);
printf("[*]vfTableTrampoline=0x%x\n", vfTableTrampoline[0]);
VirtualProtect(vfTableTrampoline, sizeof(ULONG_PTR), dwOldProtect, &dwOldProtect);

         第三步,将Detour函数地址写入到TargetClass的原始虚表TableToHook中

1
2
3
4
5
//第二次修改,为了HookTarget函数,修改原始虚表
VirtualProtect(vfTableToHook, sizeof(ULONG_PTR), PAGE_EXECUTE_READWRITE, &dwOldProtect);
vfTableToHook[0] = (ULONG_PTR)GetClassVirtualFnAddress(&Detour, 0);
printf("[*]vfTableTrampoline=0x%x\n", vfTableToHook[0]);
VirtualProtect(vfTableToHook, sizeof(ULONG_PTR), dwOldProtect, &dwOldProtect);

0x2 InlineHook

0x2.1 InlineHook(A)

         这一类InlineHook是一类较为特殊的InlineHook,他修改的不是开始的多个字节,而是修改Target函数中call指令的地址。比如说VirtualAlloc函数中调用了VirtualAllocEx函数,这次inlineHook其实就是修改了VirtualAllocEx的地址(调用处的地址),这样做的好处是可以避免被一些Hook检测工具检测。但是这样的缺点是兼容性不是很好,因为一些API的函数可能会因为系统的改变而改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
do
{
if (pTargetFun[0] == 0xE8)
{
//获取VirtualAllocEx地址
addrTemp = (ULONG)pTargetFun + 5 + *(LONG*)(pTargetFun + 1);
//比较是否相同
if (addrTemp == addrTargetFun)
{
bResult = TRUE;
break;
}
}
i++;
pTargetFun++;
} while (i < 0x30);

         如果比较无误后修改DetourFun

1
2
3
4
5
6
7
8
//保存修改的地址
g_PointerToRawData = (ULONG)(pTargetFun + 1);
//保存修改的内容
g_RawOffset = *(ULONG*)(pTargetFun + 1);
//保存Detour函数到Target函数的偏移量
addrTemp= (LONG)DetourVirtualAllocEx - (LONG)pTargetFun - 5
//修改
bResult = WriteProcessMemory(GetCurrentProcess(), pTargetFun + 1, &addrTemp, sizeof(LONG), NULL);

0x2.2 InlineHook(B)

         这种InlineHook修改的是Target函数前5个字节,这种做的好处是能够多版本进行HOOK,因为如果是采用上一种InlineHook,可能内部调用流程随着版本不同而不同,所以不容易Hook。而这种就没有这种缺点。

         使用InlineHook,需要了解到三种函数:

  • Target函数:目标函数,我们选定的HOOK的函数
  • Detour函数:我们构造的函数,用于搭载HOOK完Target函数后,我们制定的操作
  • TrampolineFun函数:负责回调Target函数,在回调的时候,需要注意的时候重新执行HOOK修改的三条指令,并绕过HOOK的地方

         InlineHook主要的步骤就是修改Target函数的前五个字节。大概有以下几个步骤:

  • Step1:构造Detour函数
  • Step2:构造TrampolineFun函数
  • Step3:获取TrampolineFun和HookPoint的地址。
  • Step4:填充需要修改的指令
  • Step5:使用ReadProcessMemory保存原指令
  • Step5:使用WriteProcessMemory修改Target函数指令

         虽然步骤看着简单,但是里面坑还是很多的。首先是构造构造Detour函数,这里需要注意的是Detour函数声明需要和Target函数保持一致,否则函数返回会异常,而且还要在DetourFun中还要调用TrampolineFun。

1
2
3
4
5
6
7
8
9
10
11
12
//第一步设置Detour函数
//Detour函数的函数声明需要和Target函数保持一致,否则函数返回会异常
int WINAPI My_MessageBoxA(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType)
{
//修改操作
int iResult = 0;
lpText = "Hooked";
iResult = OriginalMessageBox(hWnd, lpText, lpCaption, uType);
//修改返回值
iResult = 0;
return iResult;
}

         第二歩是构造TrampolineFun函数,Trampoline函数是用于在Detour回调Target函数,在使用Trampoline首先执行Target被修改的三条指令,为了避免调用Target函数堆栈异常。然后使用jmp的方式跳转到Target函数中第四条指令,绕过被修改的指令,这是为了实现永久化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//77D5050B > 8BFF mov edi,edi
//77D5050D 55 push ebp
//77D5050E 8BEC mov ebp,esp
/////////////
__declspec( naked )
int WINAPI OriginalMessageBox(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType)
{
_asm
{
//再次执行之前被修改的三条指令,避免堆栈异常
mov edi,edi
push ebp
mov ebp,esp
jmp MsgBoxHookData.JmpBackAddr //跳转到Hook之后的地方,跳过自己安装的HOOK,实现持续化
}
}

         在正式HOOK开始,还需要填充一些关键的参数,为此,我们构造一个结构体。以便管理参数。

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct _HOOK_DATA
{
char szApiName[128]; //TargetFun
char szModuleName[64]; //TargetModule
int HookCodelen; //HOOK长度
BYTE oleEntry[16]; //保存HOOK原始指令
BYTE newEntry[16]; //保存HOOK新指令
ULONG_PTR HookPoint; //被HOOK的地址
ULONG_PTR JmpBackAddr; //回跳的地址,可以多次使用
ULONG_PTR pfnTrampolineFun; //跳转到原函数执行的函数
ULONG_PTR pfnDetourFun; //Detour函数
}HOOK_DATA,*PHOOK_DATA;

         第三步是获取HookPoint和pfnTrampolineFun的地址,这一步的目的我也不是很清楚,但是我删除这两个指令,程序也是正常的。,接着设置回调点,这是为了在TrampolineFun中,设置跳转。回调点为了被修改指令之后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//如果是跳转指令,获取跳转指令跟随的地址
//如果不是跳转指令,直接返回参数
pHookData->pfnTrampolineFun = SkipJmpAddress(pHookData->pfnTrampolineFun);
//HOOK点,是mov指令
pHookData->HookPoint = SkipJmpAddress(pHookData->HookPoint);
pHookData->JmpBackAddr = pHookData->HookPoint + pHookData->HookCodelen;
ULONG_PTR SkipJmpAddress(ULONG_PTR uAddress)
{
·······
if (pFn[0] == 0xE9)
{
//目标地址-当前地址-5 = 偏移量
//(ULONG_PTR)pFn为当前地址
//*(ULONG_PTR*)(pFn + 1)为偏移量
TrueAddress = (ULONG_PTR)pFn + *(ULONG_PTR*)(pFn + 1) + 5;
return TrueAddress;
}
}

         第四步是填充我们修改的指令

1
2
3
//填充需要修改的内容
pHookData->newEntry[0] = 0xE9; //jmp
*(ULONG*)(pHookData->newEntry + 1) = (ULONG)pHookData->pfnDetourFun - (ULONG)pHookData->HookPoint - 5;

         第五步:使用ReadProcessMemory保存原指令以便恢复HOOK

1
2
3
4
5
6
//保存原始数据到pHookData->oldEntry
if (!ReadProcessMemory(hProcess, (LPCVOID)pHookData->HookPoint, pHookData->oleEntry, pHookData->HookCodelen, &dwBtyeReturned))
{
printf("[*]ReadProcessMemory:%d\n", GetLastError());
return FALSE;
}

         第六步:使用WriteProcessMemory修改前三条指令

1
2
3
4
5
if (!WriteProcessMemory(hProcess, (LPVOID)pHookData->HookPoint, pHookData->newEntry, pHookData->HookCodelen, &dwBtyeReturned))
{
printf("[*]WriteProcessMemory:%d\n", GetLastError());
return FALSE;
}

         题外话:例程中给的是当前进程HOOK,如果是需要跨进程的话,需要将InlineHook包装成dll,然后使用注入技术注入到目标进程,才能实现HOOK。

0x2.3 InlineHook(C)

         在InlineHook(B)中,我们使用jmp指令跳转到Detour函数,这部分我们使用mov-jmp和push-ret,以及HotPatch的方法跳转到DetourFun。

         首先将跳转分为两种,第一种是一次性跳转,例如jmp,push-retn.mov-jmp等,第二种是HotPatch这种长短跳。对于第一种跳转比较简单,根据Hook的指令不同,可以选择长度为5.6.7不同的Hook指令。象jmp指令对应的是修改5个字节,而push-ret修改6个字节,mov-jmp修改的是七个字节。具体填充到HookPoint的数据如下:

1
2
3
4
5
6
7
8
9
10
11
//jmp (5个字节)
MsgBoxHookData->newEntry[0] = '\xE9';
*(ULONG_PTR*)(MsgBoxHookData->newEntry + 1) = (ULONG_PTR)MsgBoxHookData->pfnDetourFun - (ULONG_PTR)MsgBoxHookData->HookPoint - 5;
//
//push-retn (6个字节)
memcpy(MsgBoxHookData->newEntry, "\x68\x44\x33\x22\x11\xC3",5);
*(LONG_PTR*)(MsgBoxHookData->newEntry + 1) = (ULONG)MsgBoxHookData->pfnDetourFun;
//
//mov-jmp (7个字节)
memcpy(MsgBoxHookData->newEntry, "\xB8\x44\x33\x22\x11\xFF\xE0 ", 7);
*(LONG_PTR*)(MsgBoxHookData->newEntry + 1) = (ULONG)MsgBoxHookData->pfnDetourFun;

         第二种跳转是长短跳,也就是HotPatch的方法,由于标准函数调用存在两种形式,分别是不存在SEH,和存在SEH的。对于第二种12个字节指令,我们推荐使用Hotpatch的方法,原理如下:因为在API上面存在nop或者int3,这些指令通常是微软用于实现HotPatch的。可以使用长短跳结合的方式占用上方的nop实现Hook,步骤是这样的:1.使用短跳到HookPoint上面5个字节HotPatchCode处, 2.然后使用长跳到DetourFun。

1
2
3
4
5
6
7
8
//1.不存在SEH
mov edi,edi
push ebp
mov ebp,esp5个字节)
//2.存在SEH
push 10
push xxxx
call xxx(2+5+5

1
2
3
4
5
6
7
//77D507E5 >-/E9 66086B88 jmp InlineHo.00401050
//77D507EA > $^\EB F9 jmp short USER32.77D507E5
//
MsgBoxHookData->newEntry[0] = 0xEB; //Jmp -5
MsgBoxHookData->newEntry[1] = 0xF9;
MsgBoxHookData->HotPatchCode[0] = 0xE9; //Jmp
*(ULONG*)(MsgBoxHookData->HotPatchCode + 1) = (ULONG)MsgBoxHookData->pfnDetourFun - ((ULONG)MsgBoxHookData->HookPoint - 5) - 5;

         InlineHook(C)属于InlineHook(B)的进阶版。所以基本步骤也是相同的。

  • Step1:构造Detour函数和TrampolineFun
  • Step2:获取Detour和HookPoint的地址。
  • Step3:修改TrampolineFun处初始化的原指令
  • Step4:填充需要修改的指令
  • Step5:使用ReadProcessMemory保存原指令
  • Step5:使用WriteProcessMemory修改Target函数指令

         构造Detour函数和构造TrampolineFun函数,以及填充修改的指令和之前是一致的,第三步是保存原始数据,这样做是为了以后能够将头几条指令填充TrampolineFun

1
2
3
4
5
6
7
8
//Step3 保存原始数据
//jmp mov-jmp,push-ret三种方法和HotPatch大有不同,分开讨论
SIZE_T lpNumberOfBytesRead = 0;
if (!ReadProcessMemory(GetCurrentProcess(), (LPCVOID)MsgBoxHookData->HookPoint, MsgBoxHookData->oldEntry, 8, &lpNumberOfBytesRead))
{
printf("[*]ReadProcessMemory:%d", GetLastError());
return FALSE;
}

         第四步填充TrampolineFun函数

1
2
3
4
5
6
7
8
9
if (MsgBoxHookData->HookCodeLen != 2)
{
SIZE_T lpNumberOfBytesWrite = 0;
if (!WriteProcessMemory(GetCurrentProcess(), (LPVOID)MsgBoxHookData->pfnTrampolineFun, MsgBoxHookData->oldEntry, MsgBoxHookData->HookCodeLen, &lpNumberOfBytesWrite))
{
printf("[*]WriteProcessMemory:%d", GetLastError());
return FALSE;
}
}

         第五步是向向HookPoint写入跳转数据,对于一次性跳转和之前是一致的,不再说明,重点是HotPatch。根据原理,在HookPaint处写入\xE8\xF9是跳转到EIP-5处也就是HotPatch处。可以在HookPoint前5个指令写入Hotpatch用于跳转到Detourfun,

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
//一次性跳转
pAddrToWrite = (PBYTE)MsgBoxHookData->HookPoint;
if (!WriteProcessMemory(GetCurrentProcess(), pAddrToWrite, MsgBoxHookData->newEntry, MsgBoxHookData->HookCodeLen, &lpNumberOfBytesWrite))
{
printf("[*]WriteProcessMemory:%d", GetLastError());
return FALSE;
}
//
//[重点]HotPatch
if (MsgBoxHookData->HookCodeLen == 2) //[重点]HotPatch
{
pAddrToWrite = (PBYTE)MsgBoxHookData->HookPoint - 5;
if (!WriteProcessMemory(GetCurrentProcess(), pAddrToWrite, MsgBoxHookData->HotPatchCode, 5, &lpNumberOfBytesWrite))
{
printf("[*]WriteProcessMemory:%d", GetLastError());
return FALSE;
}
//达到需要写入的地址
pAddrToWrite += 5;
if (!WriteProcessMemory(GetCurrentProcess(), pAddrToWrite, MsgBoxHookData->newEntry, MsgBoxHookData->HookCodeLen, &lpNumberOfBytesWrite))
{
printf("[*]WriteProcessMemory:%d", GetLastError());
return FALSE;
}
}

0x2.4 InlineHook(D)(x64)

         这一节主要讲x64下面的HOOK技术,和之前的x86下HOOK一样,都需要经历一下步骤:这一部分主要讲一下在X64位下面HOOK需要注意的地方。

  • Step1:构造Detour函数
  • Step2:获取Detour和HookPoint的地址。
  • Step3:修改TrampolineFun处初始化的原指令
  • Step4:填充需要修改的指令
  • Step5:使用ReadProcessMemory保存原指令
  • Step5:使用WriteProcessMemory修改Target函数指令

         第一点,就是在X86下可以直接写入一段shellcode到TrampolineFun。但是在x64下不能内联汇编了,所以申请一块内存用做TrampolineFun的shellcode。然后使用才能填充Trampoline。但是教主给的例程使用第二段代码做重定位,不知道作用是什么,但是使用这段代码在WIN10下HOOK MessageBoxA是不正确的,可能是填充Trampoline出现了意外。或者在填写跳转地址的时候少了一个字节,跳到了在正常的代码上面的一个int 3处,导致异常。

1
2
3
4
5
6
7
8
MsgBoxHookData.pfnTrampolineFun = (ULONG_PTR)VirtualAlloc(NULL, 128, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
//...
PBYTE pFun = (PBYTE)pHookData->pfnTrampolineFun;
memcpy(pFun, (PVOID)(pHookData->HookPoint), 14);
pFun += 14;
pFun[0] =0xFF;
pFun[1] = 0x25;
*(ULONG_PTR*)(pFun + 6) = pHookData->JmpBackAddr;

1
2
3
4
5
6
7
8
9
10
//由于第三行指令中有重定位数据,所以这里需要修复一下
//更好的办法是使用反汇编引擎来判断是否有重定位数据
//////////////////////////////////////////////////////
// 不懂 //
//////////////////////////////////////////////////////
ULONG DataOffset = 0;
ULONG_PTR pData = (ULONG_PTR)pHookData->HookPoint + 7 + 7 + *(ULONG*)(pHookData->HookPoint + 10);
printf("pData = 0x%p\n", pData);
DataOffset = (ULONG)(pData - ((ULONG_PTR)pFun + 14));
*(ULONG*)(pFun + 10) = DataOffset;

         第二点,就是在X86下面,修改指令的长度最大是7个字节,在X64下变成了14个字节,而且地址长度变成了8个字节。并且使用的指令E9变成了FF25这种长跳转指令。

1
2
3
4
5
6
//64位jmp
memset(pHookData->newEntry, 0, 14);
pHookData->newEntry[0] = 0xFF;
pHookData->newEntry[1] = 0x25;
//2-5是全0
*(ULONG_PTR*)(pHookData->newEntry + 6) = (ULONG_PTR)pHookData->pfnDetourFun;

0x3 VEH_HOOK

         VEH技术的主要原理是利用异常处理改变程序指令流程。通过主动抛出异常,使程序触发异常,控制权交给异常处理例程的这一系列操作来实现HOOK。

         这里简单提一下VEH,向量异常处理,基于VEH链表而不是栈,这样的话其作用范围是进程全局,而不是线程。且优先级也高于SEH,这也是VEH_HOOK的优势所在。

          VEH_HOOK通过异常机制实现HOOK,必不可少需要构造异常处理函数,同时也需要人为的构造异常,同时为了实现永久化机制,保证执行原操作需要实现TrampolineFun函数。所以总结VEH_HOOK步骤如下:

  • 1.构造TrampolineFun
  • 2.构造异常处理函数,即Detour函数
  • 3.人为构造异常。

          构造TrampolineFun的目的1是为了执行原有流程,2是实现永久化。因为64位不支持内联汇编,所以需要开辟空间来存放shellcode。复制前四个指令,实现堆栈平衡,然后为了永久化,特定将跳转点定在函数开头后四个字节处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pFun = (PBYTE)VirtualAlloc(NULL, 128, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
uResult = (ULONG_PTR)pFun;
if (NULL == pFun)
{
printf("VirtualAlloc%d\n", GetLastError());
return NULL;
}
//
memset(pFun, 0, 128);
memcpy(pFun, g_AddressOfMsgBox, 4); //复制MsgBox前四个字节
pFun += 4;
pFun[0] = 0xFF;
pFun[1] = 0x25;
*(ULONG_PTR*)(pFun + 6) = (ULONG_PTR)g_AddressOfMsgBox + 4;

         构造异常处理函数,这个函数就是Detour函数,同时也是我们异常处理的函数,所以他的参数是一个_EXCEPTION_POINTERS结构。根据看雪加密解密所介绍,他是一个陷进帧,用来存放EXCEPTION_RECORD和 CONTEXT_RECORD。EXCEPTION_RECORD保存发生异常的基本信息,如异常类型,发生异常的地址。而二是CONTEXT_RECORD用于保存上下文。

1
2
3
4
5
6
7
8
9
10
11
12
LONG WINAPI VectoredHandler1(struct _EXCEPTION_POINTERS *ExceptionInfo)
{
//初始化异常信息
pExceptionRecord = ExceptionInfo->ExceptionRecord;
pContextRecord = ExceptionInfo->ContextRecord;
//如果异常发生在Msgbox,且异常原因是断点异常。
if (pExceptionRecord->ExceptionAddress == g_AddressOfMsgBox
&& pExceptionRecord->ExceptionCode == EXCEPTION_BREAKPOINT)
{
//此处执行你想要的操作。
}
}

         因为X64是采用了类似于FastCall的调用约定,所以压栈顺序为RCX,RDX,R8,R9。同时也是从右到左的传参方式。所以,修改RDX就可以修改MsgBox的第二个参数。同时,别忘记修改Eip到TrampolineFun。对于x86平台下的,只需要修改栈顶第二个参数就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
||#ifdef _WIN64
pContextRecord->Rdx = (ULONG_PTR)szText;
pContextRecord->Rip = g_OriginalMessageBoxA;
||#else
/*
0012FF70 0040105A /CALL 到 MessageBoxA 来自 VEHHook.00401054
0012FF74 00000000 |hOwner = NULL
0012FF78 00407030 |Text = "VEH Hook"
0012FF7C 0040703C |Title = "Test"
0012FF80 00000000 \Style = MB_OK|MB_APPLMODAL
0012FF84 00401225 返回到 VEHHook.<ModuleEntryPoint>+0B4 来自 VEHHook.00401000
*/
ULONG_PTR* uEsp = (ULONG_PTR*)pContextRecord->Esp; //截断栈
uEsp[2] = (ULONG_PTR)szText;
pContextRecord->Eip = (ULONG_PTR)g_OriginalMessageBoxA; //跳过函数开头
||#endif

         设置异常,这里选择的是简单的断点异常。直接将Target原始代码修改一个字节为0xCC即可!

1
2
3
4
5
6
7
8
9
10
11
12
13
g_OldCode[0] = *pTarget;
if (!VirtualProtect(pTarget, sizeof(BYTE), PAGE_EXECUTE_READWRITE, &dwOldProtect))
{
printf("VirtualProtect:%d\n", GetLastError());
return FALSE;
}
//修改CC
pTarget[0] = 0xCC;
if (!VirtualProtect(pTarget, sizeof(BYTE), dwOldProtect, &dwOldProtect))
{
printf("VirtualProtect:%d\n", GetLastError());
return FALSE;
}

0x4 SSDT_HOOK

         SSDT中文全称为系统服务描述符表,其作用是作为R3和R0层的通道,将用户态API函数和内核函数联系起来。用简单的API函数举例子,我们调用了CreateFile,其会调用ZwCreateFile,然后调用NtCreateFile,经过参数和模式的检查,然后调用系统服务分发函数KiSystemService进入内核。在R0中通过传入的系统服务号(函数索引)得到系统服务的地址,然后调用该系统服务即可。

         所以,根据上述,我们可以知道SSDT其实是一个存储系统服务的数组。SSDT_HOOK其实就是在内核层的AddressHook。只不过他修改是系统服务描述符表数据。

         因为SSDT的索引号和系统服务内核地址是一一对应的,所以不需要向普通的AddressHook一一对比函数地址。所以让我们来屡一下执行SSDT的操作。我们有目的向原因开始。如果我们需要执行SSDT_HOOK的话,首先需要修改为与SSDT中的系统服务地址,但又由于系统服务地址是和服务索引是保持对应关系的,所以我们还需要获取索引号。

         根据上面的分析,我们知道首先需要获取服务索引号。但是服务索引号和函数地址对应的,在X86系统中,相对于导出函数偏移量1的地址往后读四个字节就是SSDT服务索引号。但是对于X64位的系统,却是函数地址偏移为4的地址读取四个字节。所以需要得到服务索引号,就需要得到导出函数地址。

         我们现在总结一下得到服务索引的步骤:

  • Step1:将Ntdll.dll载入内存
  • Step2:获取导出函数地址
  • Step3:计算函数索引

         SSDT适用于R0内,在内核层映射文件到内存和在应用层是一致的。只是使用的函数不一样,首先使用InitializeObjectAttributes初始化文件对象,然后使用ZwOpenFile获得映射文件句柄,接着使用ZwCreateSection创建创建一个节对象。最后使用ZwMapViewOfSection,这些都是固定的模板,代码如下。

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
//初始化文件对象
InitializeObjectAttributes(&objectAttributes,
&ustrDllFileName,
OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE,
NULL, NULL);
//获得映射文件句柄
status = ZwOpenFile(&hFile,
GENERIC_READ,
&objectAttributes,
&iosb,
FILE_SHARE_READ,
FILE_SYNCHRONOUS_IO_NONALERT);
if (!NT_SUCCESS(status))
{
//DbgPrint宏定义
KdPrint(("ZwOpenFile Error! [error code: 0x%X]", status));
return status;
}
//创建一个节对象
status = ZwCreateSection(&hSection, SECTION_MAP_READ | SECTION_MAP_WRITE, NULL, 0, PAGE_READWRITE, 0x100000, hFile);
if (!NT_SUCCESS(status))
{
//DbgPrint宏定义
ZwClose(hFile);
KdPrint(("ZwCreateSection Error! [error code: 0x%X]", status));
return status;
}
//将文件映射到内存
status = ZwMapViewOfSection(hSection, GetCurrentProcess(), &pBaseAddress, 0, 1024,0, &viewSize,ViewShare, MEM_TOP_DOWN, PAGE_READWRITE);
if (!NT_SUCCESS(status))
{
//DbgPrint宏定义
ZwClose(hFile);
ZwClose(hSection);
KdPrint(("ZwMapViewOfSection Error! [error code: 0x%X]", status));
return status;
}

         将Ntdll映射到内存中,然后就想普通的获取导出函数地址的方式获取对应的函数地址,然后根据公式获取服务索引。

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
ULONG GetIndexFromExportTable(PVOID pBaseAddress, PCHAR pszFunctionName)
{
ULONG ulFunctionIndex = 0;
// Dos Header
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddress;
// NT Header
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew);
// Export Table
PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((PUCHAR)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress);
// 有名称的导出函数个数
ULONG ulNumberOfNames = pExportTable->NumberOfNames;
// 导出函数名称地址表
PULONG lpNameArray = (PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfNames);
PCHAR lpName = NULL;
// 开始遍历导出表
for (ULONG i = 0; i < ulNumberOfNames; i++)
{
lpName = (PCHAR)((PUCHAR)pDosHeader + lpNameArray[i]);
// 判断是否查找的函数
if (0 == _strnicmp(pszFunctionName, lpName, strlen(pszFunctionName)))
{
// 获取导出函数地址
USHORT uHint = *(USHORT *)((PUCHAR)pDosHeader + pExportTable->AddressOfNameOrdinals + 2 * i);
ULONG ulFuncAddr = *(PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfFunctions + 4 * uHint);
PVOID lpFuncAddr = (PVOID)((PUCHAR)pDosHeader + ulFuncAddr);
// 获取 SSDT 函数 Index
|#ifdef _WIN64
ulFunctionIndex = *(ULONG *)((PUCHAR)lpFuncAddr + 4);
|#else
ulFunctionIndex = *(ULONG *)((PUCHAR)lpFuncAddr + 1);
|#endif
break;
}
}
return ulFunctionIndex;
}

         因为SSDT在x86系统上是由Ntoskrnl.exe导出,导出符号为KeServiceDesriptorTable,我们很容易获取SSDT的地址。只需要获取KeServiceDesriptorTable即可!也就是使用以下语句,同时给出_SERVICE_DESCIPTOR_TABLE结构,可以看到结构体第一个成员是SSDT基址,所以可以使用(PVOID)KeServiceDescriptorTable.ServiceTableBase[ulSSDTFunctionIndex];获取函数基地址。

1
2
3
4
5
6
7
8
9
10
11
extern SSDTEntry __declspec(dllimport) KeServiceDescriptorTable;
//_SERVICE_DESCIPTOR_TABLE结构
typedef struct _SERVICE_DESCIPTOR_TABLE
{
PULONG ServiceTableBase; // SSDT基址
PULONG ServiceCounterTableBase; // SSDT中服务被调用次数计数器
ULONG NumberOfService; // SSDT服务个数
PUCHAR ParamTableBase; // 系统服务参数表基址
}SSDTEntry, *PSSDTEntry;
//获取服务地址
pFunctionAddress = (PVOID)KeServiceDescriptorTable.ServiceTableBase[ulSSDTFunctionIndex];

         我们已经找到了目标函数在SSDT的地址,最后,我们只需要在该地址处填写我们构造的函数地址即可!!但是这块内存是有保护属性的,所以我们需要使用MDL方式绕过写保护属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 使用 MDL 方式修改 SSDT
pMdl = MmCreateMdl(NULL, &KeServiceDescriptorTable.ServiceTableBase[ulSSDTFunctionIndex], sizeof(ULONG));
if (NULL == pMdl)
{
DbgPrint("MmCreateMdl Error!\n");
return FALSE;
}
MmBuildMdlForNonPagedPool(pMdl);
pNewAddress = MmMapLockedPages(pMdl, KernelMode);
if (NULL == pNewAddress)
{
IoFreeMdl(pMdl);
DbgPrint("MmMapLockedPages Error!\n");
return FALSE;
}
// 写入新函数地址
ulNewFuncAddr = (ULONG)New_ZwQueryDirectoryFile;
RtlCopyMemory(pNewAddress, &ulNewFuncAddr, sizeof(ULONG));
// 释放
MmUnmapLockedPages(pNewAddress, pMdl);
IoFreeMdl(pMdl);

         同样的,卸载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
BOOLEAN SSDTUnhook()
{
UNICODE_STRING ustrDllFileName;
ULONG ulSSDTFunctionIndex = 0;
PVOID pSSDTFunctionAddress = NULL;
PMDL pMdl = NULL;
PVOID pNewAddress = NULL;
ULONG ulOldFuncAddr = 0;
RtlInitUnicodeString(&ustrDllFileName, L"\\??\\C:\\Windows\\System32\\ntdll.dll");
// 从 ntdll.dll 中获取 SSDT 函数索引号
ulSSDTFunctionIndex = GetSSDTFunctionIndex(ustrDllFileName, "ZwQueryDirectoryFile");
// 使用 MDL 方式修改 SSDT
pMdl = MmCreateMdl(NULL, &KeServiceDescriptorTable.ServiceTableBase[ulSSDTFunctionIndex], sizeof(ULONG));
if (NULL == pMdl)
{
DbgPrint("MmCreateMdl Error!\n");
return FALSE;
}
MmBuildMdlForNonPagedPool(pMdl);
pNewAddress = MmMapLockedPages(pMdl, KernelMode);
if (NULL == pNewAddress)
{
IoFreeMdl(pMdl);
DbgPrint("MmMapLockedPages Error!\n");
return FALSE;
}
// 写入原函数地址
ulOldFuncAddr = (ULONG)g_pOldSSDTFunctionAddress;
RtlCopyMemory(pNewAddress, &ulOldFuncAddr, sizeof(ULONG));
// 释放
MmUnmapLockedPages(pNewAddress, pMdl);
IoFreeMdl(pMdl);
return TRUE;
}

0x5 IRP_Hook

         IRP全称是IO请求包,发送到设备驱动程序的大多数请求都打包在IRP中。操作系统组件或驱动程序通过调用IoCallDriver将IRP发送给驱动程序。

         大概的执行流程是这样的:IO管理器创建一个IRP来代表一个IO操作,并且将该IRP传递给正确的驱动程序,当此IO操作完成时再处理该请求包。相对的,驱动程序(上层的虚拟设备驱动或者底层的真实设备驱动)接收一个IRP,执行该IRP指定的操作,然后将IRP传回给IO管理器,告诉它,该操作已经完成,或者应该传给另一个驱动以进行进一步处理。

         IO管理器可以使用一下三个函数创建IRP。但此时,IRP堆栈还没有被初始化,难以进行拦截。然后使用你可以调用IoGetNextIrpStackLocation函数获得该IRP第一个堆栈单元的指针。然后初始化这个堆栈单元。当初始化完成之后,就可以调用IoCallDriver函数把IRP发送到设备驱动程序了。这就可以在中途进行拦截啦。

  • IoBuildAsynchronousFsdRequest 创建异步IRP
  • IoBuildSynchronousFsdRequest 创建同步IRP
  • IoBuildDeviceIoControlRequest 创建一个同步IRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL请求。

         根据上述流程,执行IrpHook可以在三个地址进行,第一:在Irp初始化之后,第二:在发往派遣例程过程中,第三,直接修改需要拦截驱动对象派遣例程函数表。

         通过查看 IofCallDriver函数发现,在函数开头存在一个jmp指令。ff2500c85480其中ff25是jmp的机器码,后面的机器码是跳转的绝对地址。可以使用InlineHook直接修改跳转地址即可

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
void HookpIofCallDriver()
{
KIRQL oldIrql;
ULONG addr = (ULONG)IofCallDriver;
//保存原始的IofCallDriver函数地址
__asm
{
mov eax, addr
mov esi, [eax + 2]
mov eax, [esi]
mov old_piofcalldriver, eax
}
//引发硬件优先IRQL
oldIrql = KeRaiseIrqlToDpcLevel();
__asm
{
mov eax, cr0
mov oData, eax
and eax, 0xffffffff
mov cr0, eax
mov eax, addr; IofCallDriver
mov esi, [eax + 2]
mov dword ptr[esi], offset NewpIofCallDriver; 写入新的数据
mov eax, oData;恢复cr0的数据
mov cr0, eax
}
KeLowerIrql(oldIrql);
return;
}

0x6 Object Hook

         首先讲解一个重要的结构体_OBJECT_HEADER,使用WINDBG用dt _OBJECT_HEADER命令即可显示如下:
mark

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct _OBJECT_HEADER {
LONG PointerCount;
union {
LONG HandleCount;
PSINGLE_LIST_ENTRY SEntry;
};
POBJECT_TYPE Type; //这个很重要HOOK就靠它,对象类型结构也是一个对象,TYPE它是系统第一个创建出来的对象类型
UCHAR NameInfoOffset; //OBJECT_HEADER_NAME_INFO 偏移
UCHAR HandleInfoOffset; //OBJECT_HEADER_HANDLE_INFO 偏移
UCHAR QuotaInfoOffset;
UCHAR Flags;
union
{
POBJECT_CREATE_INFORMATION ObjectCreateInfo;
PVOID QuotaBlockCharged;
};
PSECURITY_DESCRIPTOR SecurityDescriptor;
QUAD Body;//对象本身
} OBJECT_HEADER, *POBJECT_HEADER;

        接着,我们来看一下OBJECT_TYPE,同样的使用windbgdt _OBJECT_TYPE即可查看
mark

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//对象类型结构
typedef struct _OBJECT_TYPE {
ERESOURCE Mutex;
LIST_ENTRY TypeList; //队列
UNICODE_STRING Name;
PVOID DefaultObject;
ULONG Index;
ULONG TotalNumberOfObjects;
ULONG TotalNumberOfHandles;
ULONG HighWaterNumberOfObjects;
ULONG HighWaterNumberOfHandles;
OBJECT_TYPE_INITIALIZER TypeInfo; //这个很重要,下面讲这个结构
||#ifdef POOL_TAGGING
ULONG Key;
||#endif
} OBJECT_TYPE, *POBJECT_TYPE;

        对于对象类型结构,主要的层次结构像一个树形或者说目录形。其主要的对象类型比如IoFileObjectType,PsProcessType,*PsThreadType。都是存在于ObjectTypes\Device。所以,只要生成对象就会创建指定的对象类型结构。

        最后讲解一下关于最后一个结构体OBJECT_TYPE_INITIALIZER,使用dt _OBJECT_TYPE_INITIALIZER就可以查看_OBJECT_TYPE_INITIALIZER的数据。在这个结构体中,最后8个函数指针是关乎HOOK的,这些函数能够决定对象的操作,比如说打开,创建,删除等。
mark

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef struct _OBJECT_TYPE_INITIALIZER {
USHORT Length;
BOOLEAN UseDefaultObject;
BOOLEAN CaseInsensitive;
ULONG InvalidAttributes;
GENERIC_MAPPING GenericMapping;
ULONG ValidAccessMask;
BOOLEAN SecurityRequired;
BOOLEAN MaintainHandleCount;
BOOLEAN MaintainTypeList;
POOL_TYPE PoolType;
ULONG DefaultPagedPoolCharge;
ULONG DefaultNonPagedPoolCharge;
PVOID DumpProcedure;
PVOID OpenProcedure; //这几个函数指针就是我们最需要的
PVOID CloseProcedure; //这些函数都是决定你的对象的的一些
PVOID DeleteProcedure; //操作或者叫方法,比如打开 创建 删除
PVOID ParseProcedure; //不同的对象类型(OBJECT_TYPE)操作也不同
PVOID SecurityProcedure;
PVOID QueryNameProcedure;
PVOID OkayToCloseProcedure;
} OBJECT_TYPE_INITIALIZER, *POBJECT_TYPE_INITIALIZER

        当你调用NtCreateFile->IoCreateFile->ObOpenObjectByName->ObpLookupObjectName->IopParseFile->IopParseDevice
IopParseFile最终也会调用IopParseDevice
ObjectHook其实就是比如你要HOOK 创建打开就是OBJECT_TYPE_INITIALIZER->ParseProcedure,所以ObjectHook的关键就是Hook OBJECT_TYPE_INITIALIZER最后几个关键的函数。

        实现代码如下

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
NTSTATUS Hook()
{
NTSTATUS Status;
HANDLE hFile;
UNICODE_STRING Name;
OBJECT_ATTRIBUTES Attr;
IO_STATUS_BLOCK ioStaBlock;
PVOID pObject = NULL;
RtlInitUnicodeString(&Name, L"\\Device\\HarddiskVolume1\\1.txt");
InitializeObjectAttributes(&Attr,
&Name,
OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE,
0, NULL);
Status = ZwOpenFile(&hFile,
GENERIC_ALL,
&Attr,
&ioStaBlock,
0, FILE_NON_DIRECTORY_FILE);
if (!NT_SUCCESS(Status))
{
KdPrint(("File is Null\n"));
return Status;
}
//获取访问对象的句柄
Status = ObReferenceObjectByHandle(hFile, GENERIC_ALL, NULL, KernelMode, &pObject, NULL);
if (!NT_SUCCESS(Status))
{
KdPrint(("Object is Null\n"));
return Status;
}
KdPrint(("pobject is %08X\n", pObject));
addrs = OBJECT_TO_OBJECT_HEADER(pObject);//获取对象头
//POBJECT_TYPE
pType = addrs->Type;//获取对象类型结构 object-10h
KdPrint(("pType is %08X\n", pType));
//保存原始地址
//POBJECT_TYPE->OBJECT_TYPE_INITIALIZER.ParseProcedure
OldParseProcedure = pType->TypeInfo.ParseProcedure;//获取服务函数原始地址OBJECT_TYPE+9C位置为打开
KdPrint(("OldParseProcedure addrs is %08X\n", OldParseProcedure));
KdPrint(("addrs is %08X\n", addrs));
//MDL去掉内存保护
__asm
{
cli;
mov eax, cr0;
and eax, not 10000h;
mov cr0, eax;
}
//hook
pType->TypeInfo.ParseProcedure = NewParseProcedure;
__asm
{
mov eax, cr0;
or eax, 10000h;
mov cr0, eax;
sti;
}
Status = ZwClose(hFile);
return Status;
}

0x7 sysenterHook

        sysenter是由目态进入管态的CPU支持的快速系统调用的一条指令。在此之前,系统的切换是使用int 0x2E系统中断实现的。但是这样做的弊端是操作是非原子的,因为要进行大量的栈切换,需要多次访问内存。所以在后来使用了新的切换指令—sysenter/sysexit

        因为sysenter的原子性,这决定了管态和目态的无论是堆栈还是指令上的切换都是可以通过一条指令来实现,当然,同时,CPU也为其配备了相对应的寄存器。分别是SYSENTER_CS_MSR:0x174,SYSENTER_ESP_MSR:0x175,SYSENTER_EIP_MSR:0x176。并且我们可以通过rdmsr和wrmsr进行读写这三个寄存器。由于CS和EIP可以决定程序的流程,所以我们如何修改了SYSENTER_CS_MSR和SYSENTER_EIP_MSR的数据,将流程劫持到我们想要的路径,这样就实现了一次Hook。

        Hook流程大概是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
_asm
{
//读取IA32_SYSENTER_EIP
mov ecx, 0x176
rdmsr
//保存原始数据
//作用无非有二,第一为了回调该函数,第二为了卸载Hook的时候方便恢复。
mov d_origKiFastCallEntry eax
//Hook
mov eax,MyKiFastCallEntry
wrmsr
}
//摘录自:https://bbs.pediy.com/thread-60247.htm

        但是上面的方法直接修改寄存器数据,这样容易被Hook检测工具检测,一般检测工具对于常见sysenterHook检测基于寄存器的值是否超过本模块范围,对于InlineHook一般检测函数起始数据是否是0xE9,然后检测后面的地址是否超过当前模块范围。如果我们使用FF25这类的转移指令,这样是不容易被察觉的。起始接下来的方法并不是严格意义上的sysenterHook,更像是属于InlineHook。

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
OID HookSysenter()
{
UCHAR cHookCode[8] = { 0x57, //push edi 第一跳,从KiFastCall跳到MyKiFastCallEntry.并绕过rootkit检测工具检测
0xBF,0,0,0,0, //mov edi,0000 0000需要被填充
0xFF,0xE7 }; //jmp edi
UCHAR JmpCode[] = {0xE9,0,0,0,0}; //jmp 0000 第三跳,从KiFastCall函数头代码跳转到原来KiFastCall+N
int nCopyLen = 0;
int nPos = 0;
//得到KiFastCallEntry地址
//但是也存在使用rdmsr读取的IP并不是KiFastCallEntry地址
ULONG uSysenter=NULL;
__asm {
mov ecx, 0x176
rdmsr
mov uSysenter, eax
}
DbgPrint("sysenter:0x%08X", uSysenter);
//我们要改写的函数头至少需要8字节 这里计算实际需要COPY的代码长度 因为我们不能把一条完整的指令打断
nPos = uSysenter;
while (nCopyLen < 8) {
nCopyLen += GetOpCodeSize((PVOID)nPos);
nPos = uSysenter + nCopyLen;
}
//保存原是的前八个字节代码
ULONG uOrigSysenterHead[8];
DbgPrint("copy code lenght:%d", nCopyLen);
PVOID pMovedSysenterCode = ExAllocatePool(NonPagedPool, 20);
memcpy(uOrigSysenterHead, (PVOID)uSysenter, 8);
//计算跳转地址
*((ULONG*)(JmpCode + 1)) = (uSysenter + nCopyLen) - ((ULONG)pMovedSysenterCode + nCopyLen) - 5;
//保存函数其实不妨原始数据
memcpy(pMovedSysenterCode, (PVOID)uSysenter, nCopyLen);
//把跳转代码COPY上去
memcpy((PVOID)(pMovedSysenterCode + nCopyLen), JmpCode, 5);
//HOOK地址,其实填充的是第二条语句的地址,其实就是InlineHook(A)
*((ULONG*)(cHookCode + 2)) = (ULONG)MyKiFastCallEntry;
DbgPrint("Saved sysenter code:0x%08X", pMovedSysenterCode);
DbgPrint("MyKiFastCallEntry:0x%08X", MyKiFastCallEntry);
__asm {
cli
mov eax, cr0
and eax, not 10000h
mov cr0, eax
}
memcpy((PVOID)uSysenter, cHookCode, 8);//把改写原来函数头
__asm {
mov eax, cr0
or eax, 10000h
mov cr0, eax
sti
}
}
//摘录自https://bbs.pediy.com/thread-42705.htm

        但是,rdmsr对于的IP地址并不一定是KiFastCallEntry,按道理来说其地址应该是KiFastCallEntry,但是我的机器上显示的不是!看看哪位师傅可以给解释一下

1
2
3
4
5
6
7
8
9
10
11
12
0: kd> rdmsr 176
msr[176] = 00000000`80542520
0: kd> u 80542520
nt!KeReleaseInStackQueuedSpinLockFromDpcLevel+0xa78:
80542520 b923000000 mov ecx,23h
80542525 6a30 push 30h
80542527 0fa1 pop fs
80542529 8ed9 mov ds,cx
8054252b 8ec1 mov es,cx
8054252d 648b0d40000000 mov ecx,dword ptr fs:[40h]
80542534 8b6104 mov esp,dword ptr [ecx+4]
80542537 6a23 push 23h

0x8 相关事项

        这一部分主要讲一下Hook的注意事项和部分大厂关于Hook的面经。部分面经之前讲解了,在这里不做赘述。

        首先是二次HOOK,就是被别人HOOK了之后自己再次HOOK,这里可以提供4种方法,第一可以换个位置HOOK第二就是替换原HOOK,也就是说将别人HOOK的指令修改为自己HOOK的指令。这样应该是比较有效的,但是需要注意的是修改指令数量一定要和对方的一致,或者修改之前将原来的HOOK还原,不然容易产生错误。第三,在Detour函数中HOOK第四,在Target函数中的原来HOOK的地址后面HOOK

        第二是X64下HOOK应该注意什么?首先X64和X86本质区别就是地址总线上的差别,一个是2^64次,一次传输64位数据,一个是2^32次,一次传输32位数据。由此造成的差异就是内存地址大小问题,在32位机器上主要是4个字节,64位机器上就变成了8个字节。这样的话对于指针的使用就需要考虑到两个架构上的兼容性和差异性。例如在32下可以使用ULONG,但是在64位下使用ULONG_PTR。这样就可以有效避免由于编码问题产生的异常(或者统一使用ULONG_PTR)。第二就是PE格式上,由于x86和x64PE结构上存在微小差异,所以在进行AddressHook的时候需要注意。第三可能涉及到跳转的问题

        由于地址长度导致跳转指令长度变化。想mov-jmp就需要利用2+8+2的长度进行跳转,又像push-ret的方法,在32位系统下直接push就是32位数据,但是64位下只能push32位数据,这样的话,只能先push低位数据,然后修改高位数据,例如这样:push 55667788h;mov [esp+4],11223344。再如使用jmp[addr]方法。FF25类型jmp在X86平台下是一种绝对偏移的跳转,但是在x64下也是一种相对偏移的跳转。计算公式为当前EIP+0x6(指令长度)

        检测HOOK:

  • 1.HOOK修改的是内存中的数据,本地文件却没有修改。可以将本地文件加载到内存中,然后进行对比
  • 2.对内存模块进行CRC校验
  • 3.设置回调函数,检测某个IAT或者函数的前几个指令是否被修改
  • 4.对VirtualProtect函数和WriteProcess函数进行HOOK,检测修改内容的合法性
  • 5.利用PsSetCreateProcessNotifyRoutineEx注册回调函数,监控进程创建,对比特定的进程,如果创建,设置创建标志为假,创建失败
  • 6.利用PsSetCreateThreadNotifyRoutine注册回调函数,监控线程创建,通过进程路径.找到对应进程名.判断是否符合,如果是的话.找到回调函数地址( pWin32Address = (UCHAR**)((UCHAR)Thread + 0x410);)并改为C3
  • 7.利用PsSetLoadImageNotifyRoutine拦截模块,首先需要获取模块基地址(让其载入),PE寻找基地址,解析到OEP,修改oep为ret即可
  • https://bbs.pediy.com/thread-224514.htm