内核层的三种Hook技术的使用
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
内核层的三种Hook技术的使⽤
1.前⾔
本⽂是在Win7 X86系统上进⾏实验,实验的内容是在内核层通过三种不同的Hook⽅法来实现进程保护,让运⾏的程序不被其他的程序关闭。
这⾥⽤来被保护的程序是⼀个简单的HelloWord弹窗程序,程序名是demo.exe。
2.实现原理
⼀个程序要想关闭⼀个进程,⾸先就要获取这个进程的句柄,并且获得的这个句柄还需要具备进程关闭的访问掩码。
也就是说,在⽤户层我们想要关闭⼀个进程,⾸先就需要使⽤以下的代码来获得⼀个进程的句柄。
HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, dwPid);
其中第⼀个参数PROCESS_TERMINATE就是让我们获得的进程句柄具有关闭进程的访问掩码,它在⽂档中的定义如下:
#define PROCESS_TERMINATE (0x0001)
那也就是说,如果HOOK了打开进程的函数,就可以在我们编写的函数中判断所打开的进程是否是我们要保护的进程,以及所要获得的进程句柄是否是要具备进程关闭的访问掩码,如果是的话就把它去掉。
这个时候,程序如果使⽤这个被删除掉进程关闭访问掩码的句柄来关闭进程,就会失败,这就完成了对进程的保护作⽤。
⽽在⽤户层所调⽤的OpenProcess到内核层以后,其实在内核中是调⽤ZwOpenProcess,所以只要在内核中监控这个函数就可以完成实验。
该函数在⽂档中的定义如下:NTSTATUS
ZwOpenProcess(
__out PHANDLE ProcessHandle,
__in ACCESS_MASK DesiredAccess,
__in POBJECT_ATTRIBUTES ObjectAttributes,
__in_opt PCLIENT_ID ClientId
);
参数2所代表的就是要打开的这个进程要具备的访问掩码,通过对它的判断与修改就可以实现进程保护。
参数4的⼀个指向CLIENT_ID的指针,CLIENT_ID在⽂档中的定义如下:
typedef struct _CLIENT_ID {
HANDLE UniqueProcess;
HANDLE UniqueThread;
} CLIENT_ID;
typedef CLIENT_ID *PCLIENT_ID;
其中的UniqueProcess代表的就是进程的PID,根据它就可以⽤来判断是否是要保护的进程。
3.代码框架
由于三个实验只是HOOK的⽅法的代码不同,⽽其他的⽐如⽤户层与内核层的通信等等是相同的。
所以为了避免重复,这⾥先给出代码框架,后续的HOOK代码只需要加进去就好。
⽤户层所要做的事情就是:
安装和卸载驱动
根据要保护的进程名获取进程PID,以传给内核层供内核层使⽤
根据⽤户输⼊选择安装或者卸载HOOK,向内核层发送相应的IoControlCode并把返回结果告诉⽤户
⽤户层的代码如下:
#include <cstdio>
#include <Windows.h>
#include <TlHelp32.h>
#define IOCTRL_BASE 0x800
#define MYIOCTRL_CODE(i) CTL_CODE(FILE_DEVICE_UNKNOWN, IOCTRL_BASE + i, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define CTL_HOOK MYIOCTRL_CODE(0)
#define CTL_UNHOOK MYIOCTRL_CODE(1)
#define LINK_NAME "\\\\.\\HookDeviceLink" //符号名
#define DRIVER_NAME "HookDriver" //驱动名称
#define OUT_BUFFER_LENGTH 1 //输出缓冲区的长度
#define INPUT_BUFFER_LENGTH 4 //输⼊缓冲区的长度
#define PROTECTED_NAME "demo.exe" //要保护的进程名
BOOL InstallService(); //安装驱动服务
BOOL UnInstallService(); //卸载驱动
DWORD GetPid(); //获得要保护的进程PID
int main()
{
DWORD dwAns; //⽤来输⼊控制程序执⾏
HANDLE hDevice = NULL;
BOOLEAN bRes = FALSE; //输出缓冲区,⽤来判断是否Hook成功
DWORD dwRetLength = 0; //接受缓冲区长度
if (InstallService())
{
// 获取设备句柄
hDevice = CreateFile(LINK_NAME, GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if (hDevice == INVALID_HANDLE_VALUE)
{
printf("CreateFile Error %d.\n", GetLastError());
goto exit;
while (TRUE)
{
dwAns = 0;
bRes = TRUE;
printf("选择操作,(输⼊1 2 3):\n");
printf("1是开启HOOK,2是关闭HOOK,3是退出程序\n");
scanf("%d", &dwAns);
if (dwAns == 3) break;
else if (dwAns == 1)
{
DWORD dwPid = GetPid();
if (dwPid == 0)
{
printf("没有找到进程\n");
continue;
}
//开启HOOK
if (!DeviceIoControl(hDevice, CTL_HOOK, &dwPid, INPUT_BUFFER_LENGTH,
&bRes, OUT_BUFFER_LENGTH, &dwRetLength, NULL))
{
printf("DeviceIoControl CTL_HOOK Error\n");
break;
}
if (bRes) printf("Hook 成功\n");
else printf("Hook 失败\n");
}
else if (dwAns == 2)
{
//关闭HOOK
if (!DeviceIoControl(hDevice, CTL_UNHOOK, NULL, 0,
&bRes, OUT_BUFFER_LENGTH, &dwRetLength, NULL))
{
printf("DeviceIoControl CTL_UNHOOK Error\n");
break;
}
if (bRes) printf("UnHook 成功\n");
else printf("UnHook 失败\n");
}
}
exit:
if (hDevice) CloseHandle(hDevice);
if (!UnInstallService())
{
printf("卸载驱动失败\n");
}
}
else
{
printf("驱动加载失败\n");
}
system("pause");
return 0;
}
BOOL UnInstallService()
{
BOOL bRet = TRUE;
SC_HANDLE hService = NULL, hSCMHandle = NULL;
SERVICE_STATUS ServiceStatus = { 0 };
hSCMHandle = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); //建⽴服务控制管理器连接 if (hSCMHandle == NULL)
{
printf("OpenSCManager error %d\n", GetLastError());
bRet = FALSE;
goto exit;
}
hService = OpenService(hSCMHandle, DRIVER_NAME, SERVICE_ALL_ACCESS);
if (hService == NULL)
{
printf("CreateService error %d\n", GetLastError());
bRet = FALSE;
goto exit;
}
if (!ControlService(hService, SERVICE_CONTROL_STOP, &ServiceStatus)) //停⽌驱动服务
{
printf("ControlService error %d\n", GetLastError());
bRet = FALSE;
goto exit;
}
if (!DeleteService(hService)) //删除驱动
printf("DeleteService error %d\n", GetLastError());
bRet = FALSE;
goto exit;
}
printf("驱动卸载成功\n");
exit:
if (hSCMHandle) CloseServiceHandle(hSCMHandle);
if (hService) CloseServiceHandle(hService);
return bRet;
}
BOOL InstallService()
{
SC_HANDLE hSCMHandle = NULL, hService = NULL;
BOOL bRet = TRUE;
CHAR szDriverPath[MAX_PATH] = { 0 };
hSCMHandle = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); //建⽴服务控制管理器连接 if (hSCMHandle == NULL)
{
printf("OpenSCManager error %d\n", GetLastError());
bRet = FALSE;
goto exit;
}
//获取当前程序运⾏⽬录
GetCurrentDirectory(MAX_PATH, szDriverPath);
strcat(szDriverPath, "\\");
strcat(szDriverPath, DRIVER_NAME);
strcat(szDriverPath, ".sys");
//创建⼀个服务对象
hService = CreateService(hSCMHandle,
DRIVER_NAME, //驱动名称
DRIVER_NAME, //显⽰的名称
SERVICE_ALL_ACCESS,
SERVICE_KERNEL_DRIVER, //指定内核驱动程序
SERVICE_DEMAND_START, //需要时候开启
SERVICE_ERROR_NORMAL,
szDriverPath, //驱动的路径
NULL, NULL, NULL, NULL, NULL);
if (hService == NULL)
{
printf("CreateService error %d\n", GetLastError());
bRet = FALSE;
goto exit;
}
if (!StartService(hService, 0, NULL)) //启动服务
{
printf("StartService error %d\n", GetLastError());
bRet = FALSE;
goto exit;
}
printf("驱动安装成功\n");
exit:
if (hSCMHandle) CloseServiceHandle(hSCMHandle);
if (hService) CloseServiceHandle(hService);
return bRet;
}
DWORD GetPid()
{
DWORD dwPid = 0;
PROCESSENTRY32 pe32 = { 0 };
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
BOOL bRet = FALSE;
if (hSnap == INVALID_HANDLE_VALUE)
{
printf("CreateToolhelp32Snapshot process %d\n", GetLastError());
return 0;
}
pe32.dwSize = sizeof(pe32);
bRet = Process32First(hSnap, &pe32);
while (bRet)
{
if (lstrcmp(pe32.szExeFile, PROTECTED_NAME) == 0)
{
dwPid = pe32.th32ProcessID;
break;
}
bRet = Process32Next(hSnap, &pe32);
}
CloseHandle(hSnap);
return dwPid;
}
⽽驱动程序就要接受⽤户层传下来的IoControlCode来进⾏Hook或者UnHook,并把返回结果传回⽤户层,具体代码如下:
#include <ntifs.h>
#define IOCTRL_BASE 0x800
#define MYIOCTRL_CODE(i) CTL_CODE(FILE_DEVICE_UNKNOWN, IOCTRL_BASE + i, METHOD_BUFFERED, FILE_ANY_ACCESS) #define CTL_HOOK MYIOCTRL_CODE(0)
#define CTL_UNHOOK MYIOCTRL_CODE(1)
#define DEVICE_NAME L"\\Device\\HookDevice"
#define SYMBOL_LINK_XP L"\\DosDevices\\HookDeviceLink"
#define SYMBOL_LINK_WIN7 L"\\DosDevices\\Global\\HookDeviceLink"
// 进程访问权限,允许关闭进程
#define PROCESS_TERMINATE (0x0001)
VOID DriverUnload(IN PDRIVER_OBJECT driverObject);
NTSTATUS DispatchCommon(PDEVICE_OBJECT pObj, PIRP pIrp);
NTSTATUS DispatchIoctrl(PDEVICE_OBJECT pObj, PIRP pIrp);
BOOLEAN Hook();
BOOLEAN UnHook();
ULONG g_uPID = 0; //要保护的进程的PID
ULONG g_uOrgFuncAddr = 0; //保存原函数地址
NTSTATUS DriverEntry(IN PDRIVER_OBJECT driverObject, IN PUNICODE_STRING registryPath)
{
NTSTATUS status = STATUS_SUCCESS;
UNICODE_STRING uDeviceName, uSymbolLinkName;
ULONG i = 0;
PDEVICE_OBJECT pDeviceOjb = NULL;
//创建设备
RtlInitUnicodeString(&uDeviceName, DEVICE_NAME);
status = IoCreateDevice(driverObject, NULL, &uDeviceName, FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN, FALSE, &pDeviceOjb);
if (!NT_SUCCESS(status))
{
DbgPrint("IoCreateDevice %X\r\n", status);
goto exit;
}
//设置数据交互⽅式
pDeviceOjb->Flags |= DO_BUFFERED_IO;
//创建符号链接
if (IoIsWdmVersionAvailable(1, 0x10)) //根据操作系统版本来初始化符号名
{
RtlInitUnicodeString(&uSymbolLinkName, SYMBOL_LINK_WIN7);
}
else
{
RtlInitUnicodeString(&uSymbolLinkName, SYMBOL_LINK_XP);
}
status = IoCreateSymbolicLink(&uSymbolLinkName, &uDeviceName);
if (!NT_SUCCESS(status))
{
DbgPrint("IoCreateSymbolicLink %X\r\n", status);
goto exit;
}
for (i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
{
driverObject->MajorFunction[i] = DispatchCommon;
}
driverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchIoctrl;
DbgPrint("驱动成功加载\r\n");
exit:
driverObject->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}
NTSTATUS DispatchIoctrl(PDEVICE_OBJECT pObj, PIRP pIrp)
{
BOOLEAN bRes = FALSE;
NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
PIO_STACK_LOCATION pIrpStack;
ULONG uIoControlCode = 0, uInformation = 0;
PVOID pIoBuffer = NULL;
//获取设备栈
pIrpStack = IoGetCurrentIrpStackLocation(pIrp);
//获取控制码
uIoControlCode = pIrpStack->Parameters.DeviceIoControl.IoControlCode;
//获取输⼊输出缓冲区
pIoBuffer = pIrp->AssociatedIrp.SystemBuffer;
switch(uIoControlCode)
{
case CTL_HOOK:
{
memcpy(&g_uPID, pIoBuffer, sizeof(g_uPID)); //保存⽤户层传⼊的进程PID
bRes = Hook();
memcpy(pIoBuffer, &bRes, sizeof(bRes));
uInformation = sizeof(bRes);
if (bRes)
{
DbgPrint("Hook 成功\r\n");
DbgPrint("要保护的进程PID是:%d\r\n", g_uPID);
status = STATUS_SUCCESS;
}
break;
}
case CTL_UNHOOK:
{
bRes = UnHook();
g_uPID = 0;
memcpy(pIoBuffer, &bRes, sizeof(bRes));
uInformation = sizeof(bRes);
status = STATUS_SUCCESS;
if (bRes) DbgPrint("UnHook 成功\r\n");
break;
}
default:
{
uInformation = 0;
DbgPrint("Unknown IoControlCode\r\n");
status = STATUS_SUCCESS;
break;
}
}
pIrp->IoStatus.Status = status;
pIrp->rmation = uInformation;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
BOOLEAN Hook()
{
BOOLEAN bRes = TRUE;
return bRes;
}
BOOLEAN UnHook()
{
BOOLEAN bRes = TRUE;
return bRes;
}
NTSTATUS DispatchCommon(PDEVICE_OBJECT pObj, PIRP pIrp)
{
pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->rmation = 0;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
VOID DriverUnload(IN PDRIVER_OBJECT driverObject)
{
UNICODE_STRING uSymbolLinkName;
if (driverObject->DeviceObject)
{
IoDeleteDevice(driverObject->DeviceObject);
if (IoIsWdmVersionAvailable(1, 0x10))
{
RtlInitUnicodeString(&uSymbolLinkName, SYMBOL_LINK_WIN7);
}
else
{
RtlInitUnicodeString(&uSymbolLinkName, SYMBOL_LINK_XP);
}
IoDeleteSymbolicLink(&uSymbolLinkName);
}
DbgPrint("驱动卸载完成\r\n");
}
4.InlineHook
InlineHook的实现原理是:将原函数要执⾏的代码改为jmp跳转指令,跳转到我们要执⾏的代码中执⾏中。
⽽在我们的代码中,为了保证Hook以后原本的功能可以实现就需要恢复被修改的字节然后再次调⽤,可是在多线程的环境中频繁的修改与删除很容易带来错误,造成蓝屏。
为了避免这种情况,可以采取热补丁的技术来实现,⾸先看看NtOpenProcess函数的反汇编结果。
可以看到在函数最开始执⾏了⼀句mov edi, edi这么⼀个⽆意义的指令,⽽这个指令对应的硬编码是两字节0x8BFF,⽽在这个函数上⾯有5个字节的0x90。
所以可以通过修改最开始的两个字节实现短跳转跳到上⾯的5个字节的0x90的开始地址执⾏,⽽这五个字节的0x90就可以修改为跳转到要真正执⾏的我们的函数的地址。
⽽在我们的函数中,如果要执⾏原来的代码,只需要直接跳转到NtOpenProcess函数2个字节偏移处,也就是略过最开始的mov edi, edi这条指令,直接从push ebp开始执⾏,这样就不⽤频繁的对原来的函数进⾏修改。
这⾥还有⼀点需要注意,由于页保护的存在,所以不能直接通过函数地址对函数中的字节进⾏修改。
需要通过内存描述符(MDL)描述⼀块包含该内存区域的起始地址,拥有者进程,字节数量,标记等信息的内存区域,并通过将它修改具有可写的属性来实现对函数的读写。
具体实现代码如下:
VOID NtMyOpenProcess(PHANDLE ProcessHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PCLIENT_ID ClientId); //Hook以后要执⾏的函数
PVOID g_pNtOpenProcessMap_5 = NULL; //NtOpenProcess函数地址-5的地址映射得指针
PMDL g_pMDL = NULL; //内存描述符指针
UCHAR g_szOrgBytes[7]; //存储HOOK之前函数中的七个字节
BOOLEAN Hook()
{
BOOLEAN bRes = TRUE;
UCHAR szShellCode[7] = { 0xE9, 0x0, 0x0, 0x0, 0x0, 0xEB, 0xF9 }; //构造热补丁
PVOID pNtOpenProcessAddr_5 = NULL; //NtOpenProcess-5的地址
UNICODE_STRING uStrFuncName = RTL_CONSTANT_STRING(L"NtOpenProcess");
ULONG i = 0;
pNtOpenProcessAddr_5 = (PVOID)((UINT32)MmGetSystemRoutineAddress(&uStrFuncName) - 5); //获得NtOpenProcess的函数地址-5的数
g_uOrgFuncAddr = (PVOID)((UINT32)pNtOpenProcessAddr_5 + 7); //取得要执⾏得原函数得代码地址
//创建MDL,并为内存属性添加可写属性
g_pMDL = MmCreateMdl(NULL, pNtOpenProcessAddr_5, sizeof(szShellCode));
if (g_pMDL == NULL)
{
bRes = FALSE;
goto exit;
}
MmBuildMdlForNonPagedPool(g_pMDL); //建⽴内存页的MDL描述
g_pMDL->MdlFlags |= MDL_MAPPED_TO_SYSTEM_VA; //改变MDL的标记为可写
g_pNtOpenProcessMap_5 = MmMapLockedPages(g_pMDL, KernelMode); //映射MDL空间
*(PUINT32)(szShellCode + 1) = (UINT32)NtMyOpenProcess - ((UINT32)pNtOpenProcessAddr_5 + 5); //填充szShellCode使其跳转到⽬标地址
__asm cli;
for (i = 0; i < 7; i++)
{
g_szOrgBytes[i] = *((PUCHAR)g_pNtOpenProcessMap_5 + i); //将原来的字节保存起来
*((PUCHAR)g_pNtOpenProcessMap_5 + i) = szShellCode[i]; //将ShellCode写⼊到地址中
}
__asm sti;
exit:
return bRes;
}
BOOLEAN UnHook()
{
BOOLEAN bRes = TRUE;
ULONG i = 0;
if (g_pNtOpenProcessMap_5)
{
__asm cli;
for (i = 0; i < 7; i++)
{
*((PUCHAR)g_pNtOpenProcessMap_5 + i) = g_szOrgBytes[i]; //恢复原来的字节
g_szOrgBytes[i] = 0;
}
__asm sti;
}
//释放MDL,将函数的内存空间改为初始状态
if (g_pMDL)
{
MmUnmapLockedPages(g_pNtOpenProcessMap_5, g_pMDL);
IoFreeMdl(g_pMDL);
g_pMDL = NULL;
}
return bRes;
}
VOID __declspec(naked) NtMyOpenProcess(PHANDLE ProcessHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PCLIENT_ID ClientId)
{
__asm
{
push ebp
mov ebp, esp //把ebp挪上来,为了后⾯⽅便使⽤参数
pushfd //保存寄存器环境
pushad
}
if (ClientId)
{
if ((UINT32)ClientId->UniqueProcess == g_uPID) //操作得是否是要保护得进程
{
if (DesiredAccess & PROCESS_TERMINATE) //是否是要关闭进程如果是删掉它
{
DesiredAccess &= ~PROCESS_TERMINATE;
}
}
}
__asm
{
popad //恢复寄存器环境
popfd
mov esp, ebp
pop ebp
jmp g_uOrgFuncAddr
}
}
5.SysenterHook
程序员使⽤OpenProcess打开进程的时候,这个函数其实是Kernel32.dll的导出函数,⽽在Kernel32.dll中这个函数的作⽤就是检测参数,随后将参数⼊栈以后在调⽤ntdll.dll中的ZwOpenProcess,该函数的具体实现如下:
.text:77F05D88 ; NTSTATUS __stdcall ZwOpenProcess(PHANDLE ProcessHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID ClientId) .text:77F05D88 public ZwOpenProcess
.text:77F05D88 ZwOpenProcess proc near ; CODE XREF: RtlQueryProcessDebugInformation+8F↑p
.text:77F05D88 ; sub_77EDD774+64C3A↓p ...
.text:77F05D88
.text:77F05D88 ProcessHandle = dword ptr 4
.text:77F05D88 DesiredAccess = dword ptr 8
.text:77F05D88 ObjectAttributes= dword ptr 0Ch
.text:77F05D88 ClientId = dword ptr 10h
.text:77F05D88
.text:77F05D88 mov eax, 0BEh ; NtOpenProcess
.text:77F05D8D mov edx, 7FFE0300h
.text:77F05D92 call dword ptr [edx]
.text:77F05D94 retn 10h
.text:77F05D94 ZwOpenProcess endp
可以看到这个函数⾸先将0xBE赋给eax,此时这个0xBE就叫做调⽤号,它⽤来指定在内核中你要调⽤的那个内核函数是哪个。
随后对0x7FFE0300这个地址中保存的地址进⾏了调⽤。
⽽0x7FFE03000这个地址中究竟保存的是什么就要取决于你的CPU是否⽀持快速调⽤,如果⽀持这个地址中保存的就是ntdll.dll中的KiFastCallSystemCall,如果不⽀持那么保存的就
是ntdll.dll中的KiIntSystemCall。
⽽检测系统是否⽀持快速调⽤的⽅法是,为eax赋值为1,随后调⽤cpuid指令,那么处理器的信息就会被放到ecx和edx寄存器中,此时判断edx的第11位是否为1,如果为1那么当前
CPU就⽀持快速调⽤,否则不⽀持。
检测代码如下:
BOOL CanSysenter()
{
BOOL bRet = FALSE;
__asm
{
mov eax, 1
cpuid
and edx, 0x0800 //与第11位为1的⼆进制进⾏与
mov bRet, edx
}
return bRet;
}
⽽要实现SysenterHook的电脑,它的CPU是要⽀持快速调⽤的,所以此时继续跟进ntdll.dll中的KiFastCallSystemCall,函数实现如下:
.text:77F070B0 public KiFastSystemCall
.text:77F070B0 KiFastSystemCall proc near ; DATA XREF: .text:off_77EF61B8↑o
.text:77F070B0 mov edx, esp
.text:77F070B2 sysenter
.text:77F070B2 KiFastSystemCall endp
可以看到,函数⾸先将esp赋值给edx,也就是说此时edx指向的是包含返回地址和参数的栈地址。
但要注意,由于调⽤了ZwOpenProcess以后⼜调⽤了KiFastSystemCall。
所以此时
edx所指的地址保存了两个返回地址,具体如下图所⽰。
随后函数会调⽤sysenter指令,此时这个指令就会从特殊模块寄存器(MSRs)寄存器中取出在内核中要运⾏的代码地址以及要设置的代码段等信息取出,并将它们赋给相应的寄存器。
MSRs中包含的不同功能的寄存器有上百种,但是与sysenter配合的MSRs⼀共有下表的三个:
名称偏移功能
SYSENTER_CS_MSR0x174指定切换到内核层的CS
SYSENTER_ESP_MSR0x175指定切换到内核层的ESP
SYSENTER_EIP_MSR0x176指定切换到内核层的EIP
那也就是说,进⼊内核层要执⾏的代码是有SYSENTER_EIP_MSR来指定的,⽽程序员可以⽤rdmsr/wrmsr来分别实现对SYSENTER_EIP_MSR的读取和修改。
当使⽤rdmsr指令的时候,程序会将ecx所指的MSRs的指定寄存器中的值写⼊edx:eax,⽽调⽤wrmsr的时候,程序会将edx:eax的值写⼊到ecx所指的MSRs寄存器中。
那也就是说可以通过将ecx赋值为0x176,再将eax赋值为要调⽤的函数,就可以实现SysenterHook。
当程序进⼊内核的时候,⾸先执⾏的就是我们所指定的函数。
具体的实现代码如下:
VOID MyKiFastCallEntry(); //Hook以后要执⾏的函数
ULONG g_uSSDT_Index = 0; //保存调⽤的SSDT函数的下标
PACCESS_MASK g_pAccessMask = 0; //保存调⽤ZwOpenProcess时候传⼊的访问权限
PCLIENT_ID g_ClientId = 0; //保存调⽤ZwOpenProcess时候传⼊的
//edx保存的参数和返回地址的栈地址,但是注意这时候有两个返回地址,详细如上图所⽰
__declspec(naked) VOID MyKiFastCallEntry()
{
__asm
{
mov g_uSSDT_Index, eax; //获取调⽤号
push EDX
add DWORD PTR [ESP], 0x0C
pop g_pAccessMask //取得保存AccessMask的栈地址
push DWORD PTR [EDX + 0x14]
pop g_ClientId; //取出参数ClientId
}
__asm
{
pushfd //保存寄存器环境
pushad
}
if (g_uSSDT_Index == 0xBE) //根据调⽤号判断调⽤的是否是ZwOpenProcess
{
if (g_ClientId)
{
if ((ULONG)g_ClientId->UniqueProcess == g_uPID ) //是否是要保护的进程
{
if (*g_pAccessMask & PROCESS_TERMINATE)
{
*g_pAccessMask = (*g_pAccessMask) & (~PROCESS_TERMINATE); //具有关闭进程的权限,删除它
}
}
}
}
__asm
{
popad //恢复寄存器环境
popfd
jmp g_uOrgFuncAddr //跳回原来的代码执⾏
}
}
BOOLEAN Hook()
{
BOOLEAN bRes = TRUE;
__asm
{
cli
mov ecx, 0x176 //指向SYSENTER_EIP_MSR
rdmsr
mov g_uOrgFuncAddr, eax //将原来要执⾏得代码地址保存起来
mov eax, MyKiFastCallEntry //Hook为要新执⾏的代码
wrmsr
sti
}
return bRes;
}
BOOLEAN UnHook()
{
BOOLEAN bRes = TRUE;
__asm
{
cli
mov ecx, 0x176 //指向SYSENTER_EIP_MSR
xor edx, edx
mov eax, g_uOrgFuncAddr //将原来执⾏的代码的地址恢复回去
wrmsr
sti
}
return bRes;
}
6.SSDT Hook
上⾯说过,进⼊内核的之前,程序会给eax赋值⼀个调⽤号,⽤来在进⼊内核以后找到要执⾏的函数。
⽽找到的办法其实是通过找系统服务描述服表来找到的,在这张表中就保存了系统的所有函数,系统就是通过这张表来调⽤相应的函数。
这张表的地址在32位的系统中是导出的,可以直接获取。
⽽获取了该表所保存的内容如下:
typedef struct _KSERVICE_TABLE_DESCRIPTOR
{
KSYSTEM_SERVICE_TABLE ntoskrnl;
KSYSTEM_SERVICE_TABLE win32k;
KSYSTEM_SERVICE_TABLE NotUsed1; //未使⽤
KSYSTEM_SERVICE_TABLE NotUsed2;
} KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;
打开进程的函数在ntoskrnl中,称为系统服务表,这是⼀个KSYSTEM_SERVICE_TABLE的变量,定义如下:
typedef struct _KSYSTEM_SERVICE_TABLE
{
PULONG pServiceTableBase; //保存函数地址的表
ULONG pServiceCounterTable; //通常为0
ULONG uNumberFunc; //保存函数个数
PUCHAR pParamTableBase; //保存了参数个数的表,以字节为单位(除4得到参数个数)
} KSYSTEM_SERVICE_TABLE, *PKSYSTEM_SERVICE_TABLE;
其中的pServiceTableBase指向的地址就是⼀张保存了各个函数地址的表,通过替换表中相应位置的内容就可以实现HOOK。
因为当⽤户层的函数调⽤进⼊到内核的时候,系统通过这张表找到的函数地址就是我们指定的地址。
另外这张表同样是因为页保护的存在⽽不可写,上⾯介绍了通过MDL的⽅式映射⼀块内存来进⾏写操作。
其实还有另⼀个办法,那就是修改CR0寄存器中的WP位(第16位)。
当它为0的时候,页保护就会被关闭,这样就可以实现对SSDT表的修改。
以下是具体的代码实现:
#pragma pack(1)
// 系统服务表
typedef struct _KSYSTEM_SERVICE_TABLE
{
PULONG pServiceTableBase; //保存函数地址的表
ULONG pServiceCounterTable; //通常为0
ULONG uNumberFunc; //保存函数个数
PUCHAR pParamTableBase; //保存了参数个数的表,以字节为单位(除4得到参数个数)
} KSYSTEM_SERVICE_TABLE, *PKSYSTEM_SERVICE_TABLE;
typedef struct _KSERVICE_TABLE_DESCRIPTOR
{
KSYSTEM_SERVICE_TABLE ntoskrnl;
KSYSTEM_SERVICE_TABLE win32k;
KSYSTEM_SERVICE_TABLE unUsedTable1; //系统未使⽤
KSYSTEM_SERVICE_TABLE unUsedTable2; //系统未使⽤
} KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;
#pragma pack()
VOID OpenPageProtect(); //开起页保护
VOID ClosePageProtect(); //关闭页保护
typedef NTSTATUS (*pFnOpenProcess)(PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, PCLIENT_ID);
NTSTATUS MyZwOpenProcess(PHANDLE ProcessHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PCLIENT_ID ClientId); //HOOK以后要执⾏的函数
extern PKSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable; //获取导出的地址
BOOLEAN Hook()
{
BOOLEAN bRes = TRUE;
ClosePageProtect(); //关闭页保护
g_uOrgFuncAddr = KeServiceDescriptorTable->ntoskrnl.pServiceTableBase[0xBE]; //原函数进⾏备份
KeServiceDescriptorTable->ntoskrnl.pServiceTableBase[0xBE] = (ULONG)MyZwOpenProcess; //修改为要执⾏的函数地址
OpenPageProtect(); //开启页保护
return bRes;
}
BOOLEAN UnHook()
{
BOOLEAN bRes = TRUE;
ClosePageProtect(); //关闭页保护
KeServiceDescriptorTable->ntoskrnl.pServiceTableBase[0xBE] = g_uOrgFuncAddr; //还原成原来的函数
OpenPageProtect(); //开启页保护
return bRes;
}
NTSTATUS MyZwOpenProcess(PHANDLE ProcessHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID ClientId) {
if ((ULONG)ClientId->UniqueProcess == g_uPID) //是否是要保护的进程
{
if (DesiredAccess & PROCESS_TERMINATE) //是否具有关闭进程的权限
{
DesiredAccess &= ~PROCESS_TERMINATE;
}
}
return ((pFnOpenProcess)g_uOrgFuncAddr)(ProcessHandle, DesiredAccess, ObjectAttributes, ClientId); //调⽤原函数
}
// 关闭页保护
VOID ClosePageProtect()
{
__asm
{
cli
mov eax, cr0 //将cr0中的数值赋给eax
and eax, ~0x10000 // 将第16位,也就是WP位置0
mov cr0, eax
}
}
// 开起页保护
VOID OpenPageProtect()
{
__asm
{
mov eax, cr0 // 将cr0中的数值赋给eax
or eax, 0x10000 // WP位置1
mov cr0, eax
sti
}
}
7.实验结果
⽽当卸载了钩⼦以后就可以顺利关闭进程。