x64驱动基础教程 16

栏目: 数据库 · 发布时间: 5年前

内容简介:在 WIN64 上 HOOK SSDT 和 UNHOOK SSDT 在原理上跟 WIN32 没什么不同, 甚至说 HOOK 和 UNHOOK 在本质上也没有不同, 都是在指定的地址上填写一串数字而已(填写代理函数的地址时叫做 HOOK,填写原始函数的地址时叫做 UNHOOK)。 不过实现起来还是很大不同的。 废话不多说,开始分点讲解 HOOK 和 UNHOOK。要挂钩 SSDT,必然要先得到 ServiceTableBase 的地址。和 SSDT 相关的两个结构体 SYSTEM_SERVICE_TABLE

在 WIN64 上 HOOK SSDT 和 UNHOOK SSDT 在原理上跟 WIN32 没什么不同, 甚至说 HOOK 和 UNHOOK 在本质上也没有不同, 都是在指定的地址上填写一串数字而已(填写代理函数的地址时叫做 HOOK,填写原始函数的地址时叫做 UNHOOK)。 不过实现起来还是很大不同的。 废话不多说,开始分点讲解 HOOK 和 UNHOOK。
一、 HOOK SSDT

要挂钩 SSDT,必然要先得到 ServiceTableBase 的地址。和 SSDT 相关的两个结构体 SYSTEM_SERVICE_TABLE 以及 SERVICE_DESCRIPTOR_TABLE 并没有发生什么的变化(除了整个结构体的长度胖了一倍):

typedef struct _SYSTEM_SERVICE_TABLE{
	PVOID ServiceTableBase;
	PVOID ServiceCounterTableBase;
	SIZE_T NumberOfServices;
	PVOID ParamTableBase;
} SYSTEM_SERVICE_TABLE, *PSYSTEM_SERVICE_TABLE;
typedef struct _SERVICE_DESCRIPTOR_TABLE{
	SYSTEM_SERVICE_TABLE ntoskrnl; // ntoskrnl.exe (native api)
	SYSTEM_SERVICE_TABLE win32k; // win32k.sys (gdi/user)
	SYSTEM_SERVICE_TABLE Table3; // not used
	SYSTEM_SERVICE_TABLE Table4; // not used
}SERVICE_DESCRIPTOR_TABLE,*PSERVICE_DESCRIPTOR_TABLE;

得到 ServiceTableBase 的地址后,就能得到每个服务函数的地址了。但和WIN32 不一样,这个表存放的并不是 SSDT 函数的完整地址,而是其相对于ServiceTableBase[Index]>>4 的数据(我称它为偏移地址),每个数据占四个字节,所以计算指定 Index 函数完整地址的公式是: ServiceTableBase[Index]>>4+ ServiceTableBase。代码如下:

ULONGLONG GetSSDTFuncCurAddr(ULONG id)
{
	LONG dwtmp=0;
	PULONG ServiceTableBase=NULL;
	ServiceTableBase=(PULONG)KeServiceDescriptorTable->ServiceTableBase;
	dwtmp=ServiceTableBase[id];
	dwtmp=dwtmp>>4;
	return dwtmp + (ULONGLONG)ServiceTableBase;
}

反之,从函数的完整地址获得函数偏移地址的代码也就出来了:

ULONG GetOffsetAddress(ULONGLONG FuncAddr)
{
	LONG dwtmp=0;
	PULONG ServiceTableBase=NULL;
	ServiceTableBase=(PULONG)KeServiceDescriptorTable->ServiceTableBase;
	dwtmp=(LONG)(FuncAddr-(ULONGLONG)ServiceTableBase);
	return dwtmp<<4;
}

知道了这一套机制, HOOK SSDT 就很简单了,首先获得待 HOOK 函数的序号Index,然后通过公式把自己的代理函数的地址转化为偏移地址,然后把偏移地址的数据填入 ServiceTableBase[Index]。也许有些读者看到这里,已经觉得胜利在望了,我当时也是如此。但实际上我在这里栽了个大跟头,整整郁闷了很长时间!因为我低估了设计这套算法的工程师的智商,我没有考虑一个问题,为什么 WIN64 的 SSDT 表存放地址的形式这么奇怪?只存放偏移地址,而不存放完整地址?难道是为了节省内存?这肯定是不可能的,要知道现在内存白菜价。那么不是为了节省内存,唯一的可能性就是要给试图挂钩 SSDT 的人制造麻烦!要知道, WIN64 内核里每个驱动都不在同一个 4GB 里,而 4 字节的整数只能表示 4GB的范围!所以无论你怎么修改这个值,都跳不出 ntoskrnl 的手掌心。如果你想通过修改这个值来跳转到你的代理函数,那是绝对不可能的。 因为你的驱动的地址不可能跟 ntoskrnl 在同一个 4GB 里。 然而,这位工程师也低估了我们中国人的智商,在中国有两句成语,这位工程师一定没听过,叫“明修栈道,暗渡陈仓”以及“上有政策,下有对策”。虽然不能直接用 4 字节来表示自己的代理函数所在的地址,但是还是可以修改这个值的。要知道, ntoskrnl 虽然有很多地方的代码通常是不会被执行的,比如 KeBugCheckEx。所以我的办法是: 修改这个偏移地址的值,使之跳转到 KeBugCheckEx,然后在 KeBugCheckEx 的头部写一个 12 字节的 mov – jmp,这是一个可以跨越 4GB 的跳转,跳到我们的函数里!代码如下:

VOID FuckKeBugCheckEx()
{
	KIRQL irql;
	ULONGLONG myfun;
	UCHAR jmp_code[]="\x48\xB8\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\xFF\xE0";
	myfun=(ULONGLONG)Fake_NtTerminateProcess;
	memcpy(jmp_code+2,&myfun,8);
	irql=WPOFFx64();
	memset(KeBugCheckEx,0x90,15);
	memcpy(KeBugCheckEx,jmp_code,12);
	WPONx64(irql);
}
VOID HookSSDT()
{
	KIRQL irql;
	ULONGLONG dwtmp=0;
	PULONG ServiceTableBase=NULL;
	//get old address
	NtTerminateProcess=(NTTERMINATEPROCESS)GetSSDTFuncCurAddr(41);
	dprintf("Old_NtTerminateProcess: %llx",(ULONGLONG)NtTerminateProcess);
	//set kebugcheckex
	FuckKeBugCheckEx();
	//show new address
	ServiceTableBase=(PULONG)KeServiceDescriptorTable->ServiceTableBase;
	OldTpVal=ServiceTableBase[41]; //record old offset value
	irql=WPOFFx64();
	ServiceTableBase[41]=GetOffsetAddress((ULONGLONG)KeBugCheckEx);
	WPONx64(irql);
	dprintf("KeBugCheckEx: %llx",(ULONGLONG)KeBugCheckEx);
	dprintf("New_NtTerminateProcess: %llx",GetSSDTFuncCurAddr(41));
}

在代理函数里这么写,保护名为 calc.exe和 loaddrv.exe的程序不被结束:

NTSTATUS __fastcall Fake_NtTerminateProcess(IN HANDLE ProcessHandle, IN NTSTATUSExitStatus)
{
	PEPROCESS Process;
	NTSTATUS st = ObReferenceObjectByHandle (ProcessHandle, 0, *PsProcessType, KernelMode,&Process, NULL);
	DbgPrint("Fake_NtTerminateProcess called!");
	if(NT_SUCCESS(st))
	{
		if(!_stricmp(PsGetProcessImageFileName(Process),"loaddrv.exe")||!_stricmp(PsGetProcessImageFileName(Process),"calc.exe"))
			return STATUS_ACCESS_DENIED;
		else
			return NtTerminateProcess(ProcessHandle,ExitStatus);
	}
	else
		return STATUS_ACCESS_DENIED;
}

注意在代理函数一定要注明是__fastcall,否则会出问题。测试效果如下:

x64驱动基础教程 16

x64驱动基础教程 16

给大家看一下 WINDBG 里的反汇编结果(挂钩前和挂钩后): x64驱动基础教程 16

x64驱动基础教程 16

用 WIN64AST 查看的效果如下(挂钩前和挂钩后):
x64驱动基础教程 16 x64驱动基础教程 16

接下来给出取消 SSDT HOOK 的代码,在这个代码里我没有复原 KeBugCheckEx的原始内容,因为执行到 KeBugCheckEx 就意味着蓝屏,所以是否恢复KeBugCheckEx 的原始机器码都无所谓了:

VOID UnhookSSDT()
{
	KIRQL irql;
	PULONG ServiceTableBase=NULL;
	ServiceTableBase=(PULONG)KeServiceDescriptorTable->ServiceTableBase;
	//set value
	irql=WPOFFx64();
	ServiceTableBase[41]=GetOffsetAddress((ULONGLONG)NtTerminateProcess);
	WPONx64(irql);
	//没必要恢复 KeBugCheckEx 的内容了,反正执行到 KeBugCheckEx 时已经完蛋了。
	dprintf("NtTerminateProcess: %llx",GetSSDTFuncCurAddr(41));
}

网上 SSDT HOOK 的代码,动辄几百行的代码,感觉简直是在吓唬人。现在,我用一行代码凸显出 SSDT HOOK 的本质:

WIN32 内核:

KeServiceDescriptorTable->ServiceTableBase[Index] = 代理函数绝对地址

WIN64 内核:

KeServiceDescriptorTable->ServiceTableBase[Index] = 代理函数偏移地址

也就是说,在 WIN32 下只需要四行代码即可实现 SSDT HOOK,分别是:关闭内存写保护、保存旧地址、设置新地址、打卡内存写保护。而 WIN64 系统显得复杂些,还需要计算偏移地址、找出一块在位于 NTOSKRNL 空间里的废弃内存,并在这块废弃内存里进行二次跳转才能转到自己的处理函数。

二、 UNHOOK SSDT

要恢复 SSDT,首先要获得 SSDT 各个函数的原始地址,而 SSDT 各个函数的原始地址,自然是存储在内核文件里的。于是,有了以下思路:

1.获得内核里 KiServiceTable 的地址(变量名称: KiServiceTable)

2.获得内核文件在内核里的加载地址(变量名称: NtosBase)

3.获得内核文件在 PE32+结构体里的映像基址(变量名称: NtosImageBase)

4.在自身进程里加载内核文件并取得映射地址(变量名称: NtosInProcess)

5.计算出 KiServiceTable 和 NtosBase 之间的“距离”(变量名称: RVA)

6.获得指定 INDEX 函数的地址(计算公式: *(PULONGLONG)(NtosInProcess + RVA+ 8 * index) – NtosImageBase + NtosBase)

思路和 WIN32 下获得 SSDT 函数原始地址差异不大,接下来解释一下第六步的计算公式是怎么得来的。首先看一张 IDA 的截图:

x64驱动基础教程 16

可见,从文件中的 KiServiceTable 地址开始,每 8 个字节,存储一个函数的“理想地址”(之所以说是理想地址,是因为这个地址是基于『内核文件的映像基址 NtosImageBase』的,而不是基于『内核文件的加载基址 NtosBase』的)。因此,得到 8 * index。由于已经获得了 KiServiceTable 和 NtosBase 之间的“距离”(RVA = KiServiceTable – NtosBase),也已知内核文件在自身进程里的映射地址(NtosInProcess),所以就能算出文件中的 KiServiceTable 的地址(NtosInProcess + RVA)。所以, 存储各个函数原始地址的文件地址就是:NtosInProcess + RVA + 8 * index。把这个地址的值取出来(长度为 8),就是: *(PULONGLONG)(NtosInProcess + RVA + 8 * index)。前面说了,由于得到的这个函数地址是理想地址,因为它假设的加载基址是 PE32+结构体里的成员ImageBase(映像基址)的值。而实际上,内核文件的加载基址肯定不可能是这个值,所以还要减去内核文件的映像基址(NtosImageBase)再加上内核文件的实际加载基址(NtosBase)。接下来,给出每一步的具体实现过程的代码。

1.获得 KiServiceTable 的地址其实就是获得 KeServiceDescriptorTable->ServiceTableBase 的地址而已,具体知识之前已经讲过,这里就不赘述了, 直接给出代码:

ULONGLONG GetKeServiceDescriptorTable64()
{
	char KiSystemServiceStart_pattern[13] = "\x8B\xF8\xC1\xEF\x07\x83\xE7\x20\x25\xFF\x0F\x00\x00";
	ULONGLONG CodeScanStart = (ULONGLONG)&_strnicmp;
	ULONGLONG CodeScanEnd = (ULONGLONG)&KdDebuggerNotPresent;
	UNICODE_STRING Symbol;
	ULONGLONG i, tbl_address, b;
	for (i = 0; i < CodeScanEnd - CodeScanStart; i++)
	{
		if (!memcmp((char*)(ULONGLONG)CodeScanStart +i, (char*)KiSystemServiceStart_pattern,13))
		{
			for (b = 0; b < 50; b++)
			{
				tbl_address = ((ULONGLONG)CodeScanStart+i+b);
				if (*(USHORT*) ((ULONGLONG)tbl_address ) == (USHORT)0x8d4c)
					return ((LONGLONG)tbl_address +7) + *(LONG*)(tbl_address +3);
			}
		}
	}
	return 0;
}
ULONG64 ssdt_base_aadress=GetKeServiceDescriptorTable64();
KiServiceTable=*(PULONGLONG)ssdt_base_aadress;

2.获得内核文件在内核里的加载地址

这个本质上属于枚举内核模块,使用 ZwQuerySystemInformation 的SystemModuleInformation 功能号实现。 由于第一个加载的总是内核文件,所以直接获得 0 号模块的基址即可。另外,还要获得内核文件的名称,因为根据 CPU核心数目等硬件条件的不同,内核文件的名称也是不尽相同的。

ULONGLONG GetNtosBaseAndPath(char *ModuleName)
{
	ULONG NeedSize, i, ModuleCount, BufferSize = 0x5000;
	PVOID pBuffer = NULL;
	ULONGLONG qwBase = 0;
	NTSTATUS Result;
	PSYSTEM_MODULE_INFORMATION pSystemModuleInformation;
	do
	{
		pBuffer = malloc( BufferSize );
		if( pBuffer == NULL )
		{
			return FALSE;
		}
		Result = ZwQuerySystemInformation( SystemModuleInformation, pBuffer, BufferSize,
		&NeedSize );
		if( Result == STATUS_INFO_LENGTH_MISMATCH )
		{
			free( pBuffer );
			BufferSize *= 2;
		}
		else if( !NT_SUCCESS(Result) )
		{
			free( pBuffer );
			return FALSE;
		}
	}
	while( Result == STATUS_INFO_LENGTH_MISMATCH );
	pSystemModuleInformation = (PSYSTEM_MODULE_INFORMATION)pBuffer;
	if(ModuleName!=NULL)
		strcpy(ModuleName,pSystemModuleInformation->Module[0].ImageName+pSystemModuleInformat
	ion->Module[0].ModuleNameOffset);
	qwBase=(ULONGLONG)pSystemModuleInformation->Module[0].Base;
	free(pBuffer);
	return qwBase;
}

3.获得内核文件的映像基址这个直接解析 PE32+文件的结构即可。

DWORD FileLen(char *filename)
{
	WIN32_FIND_DATAA fileInfo={0};
	DWORD fileSize=0;
	HANDLE hFind;
	hFind = FindFirstFileA(filename ,&fileInfo);
	if(hFind != INVALID_HANDLE_VALUE)
	{
		fileSize = fileInfo.nFileSizeLow;
		FindClose(hFind);
	}
	return fileSize;
}
CHAR *LoadDllContext(char *filename)
{
DWORD dwReadWrite, LenOfFile=FileLen(filename);
	HANDLE hFile = CreateFileA(filename, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ |
	FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0);
	if (hFile != INVALID_HANDLE_VALUE)
	{
		PCHAR buffer=(PCHAR)malloc(LenOfFile);
		SetFilePointer(hFile, 0, 0, FILE_BEGIN);
		ReadFile(hFile, buffer, LenOfFile, &dwReadWrite, 0);
		CloseHandle(hFile);
		return buffer;
	}
	return NULL;
}
VOID GetNtosImageBase()
{
	PIMAGE_NT_HEADERS64 pinths64;
	PIMAGE_DOS_HEADER pdih;
	char *NtosFileData=NULL;
	NtosFileData=LoadDllContext(NtosName);
	pdih=(PIMAGE_DOS_HEADER)NtosFileData;
	pinths64=(PIMAGE_NT_HEADERS64)(NtosFileData+pdih->e_lfanew);
	NtosImageBase=pinths64->OptionalHeader.ImageBase;
	printf("ImageBase: %llx\n",NtosImageBase);
}

4/5/6.获得 SSDT 函数的原始地址原理已经在前面解释过,这里直接给出代码。

ULONGLONG GetFunctionOriginalAddress(DWORD index)
{
	if ( NtosInProcess==0 )
		NtosInProcess = (ULONGLONG)LoadLibraryExA(NtosName,0,DONT_RESOLVE_DLL_REFERENCES);
	ULONGLONG RVA=KiServiceTable-NtosBase;
	ULONGLONG temp=*(PULONGLONG)(NtosInProcess+RVA+8*(ULONGLONG)index);
	ULONGLONG RVA_index=temp-NtosImageBase;
	return RVA_index+NtosBase;
}

接下来测试一下效果,在测试前,运行 SSDT HOOK NtTerminateProcess 的DEMO(检测出了 SSDT 的异常项)。

x64驱动基础教程 16

检测出了异常的项目就需要恢复。其实恢复 SSDT 本质上和挂钩 SSDT 本质上没有不同,都是在 KiServiceTable 的指定偏移处写入一个 INT32 值。代码如下:

LONG GetOffsetAddress(ULONGLONG FuncAddr)
{
	LONG dwtmp=0;
	PULONG ServiceTableBase=NULL;
	if(KeServiceDescriptorTable==NULL)
		KeServiceDescriptorTable=(PSYSTEM_SERVICE_TABLE)GetKeServiceDescriptorTable64();
	ServiceTableBase=(PULONG)KeServiceDescriptorTable->ServiceTableBase;
	dwtmp=(LONG)(FuncAddr-(ULONGLONG)ServiceTableBase);
	return dwtmp<<4;
}
VOID UnHookSSDT(ULONG id, ULONGLONG FuncAddr) //传入正确的地址
{
	KIRQL irql;
	LONG dwtmp;
	PULONG ServiceTableBase=NULL;
	dwtmp=GetOffsetAddress(FuncAddr);
	ServiceTableBase=(PULONG)KeServiceDescriptorTable->ServiceTableBase;
	irql=WPOFFx64();
	ServiceTableBase[id]=dwtmp; //核心就这一句
	WPONx64(irql);
}

接下来测试效果(输入要恢复的函数的 Index): x64驱动基础教程 16


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

调试九法

调试九法

David J.Agans / 赵俐 / 人民邮电出版社 / 2010-12-7 / 35.00元

硬件缺陷和软件错误是“技术侦探”的劲敌,它们负隅顽抗,见缝插针。本书提出的九条简单实用的规则,适用于任何软件应用程序和硬件系统,可以帮助软硬件调试工程师检测任何bug,不管它们有多么狡猾和隐秘。 作者使用真实示例展示了如何应用简单有效的通用策略来排查各种各样的问题,例如芯片过热、由蛋酒引起的电路短路、触摸屏失真,等等。本书给出了真正能够隔离关键因素、运行测试序列和查找失败原因的技术。 ......一起来看看 《调试九法》 这本书的介绍吧!

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具