SSDT(系统服务描述符表 system services descriptor table)

SSDT表介绍

ntdll.dll模块中的函数有些以nt或zw开头的函数为了完成相应的功能需要进入内核,调用内核中以nt开头的函数来完成相应的功能。ntdll.dll里的函数在进入内核层之前首先将系统服务号传入eax寄存器中,然后调用KiSystemService函数进入内核层。进入内核后会根据eax值索引ssdt表里的函数进行执行相应地址的函数。

SSDT的每一项是一个系统服务函数的地址,可以通过HOOK这些函数完成特定的功能。

32位系统上SSDT是导出的,64位是不会导出的。

通过PCHunter查看win7 x64系统的SSDT表:

如何获得SSDT表的地址和每一个项对应的服务名称呢?

结论:每一个版本的windows操作系统的系统服务函数的编号都是固定的,例如所有32位的windows 7的系统服务函数的编号都是固定的,无论系统版本如何变化。这主要是因为一旦更新操作系统后,如果系统服务函数的编号发生变化会导致系统不稳定。基于以上事实,我们只需要针对win7和win10定义四份函数表即可。而函数表的获得只需要依靠PC hunter直接导出即可。

下面介绍SSDT表的基本知识

32位系统

32系统上ntdll.dll使用mov eax,xxx传入索引值,因此也可以通过遍历ntdll.dll查看每一个函数对应的服务号,从而找到服务函数名和服务编号的关系。另外,内核中32位的SSDT的起始地址是直接在ntoskrnl.exe中通过KeServiceDescriptorTable符号导出,不需要使用工具来获得,可以直接在驱动程序中引用该符号的地址。注意:在代码实现上应当引入头文件#include <ntimage.h>之后使用语句

extern SSDTEntry __declspec(dllimport) KeServiceDescriptorTable;

来获得KeServiceDescriptorTable的地址。

32位系统中KeServiceDescriptorTable结构如下图所示

#pragma pack(1)
typedef struct _SERVICE_DESCRIPTOR_TABLE
{
    PULONG ServiceTableBase;//SSDT的起始地址
    PULONG ServiceCounterTableBase;//
    ULONG  NumberOfService;//SSDT表中服务函数的总数
    PUCHAR ParamTableBase;//服务函数的参数个数数组的起始地址,数组的每一个成员占1字节,记录的值是对应函数的参数个数*4
} SSDTEntry, *PSSDTEntry;
#pragma pack()

ServiceTableBase的内容是SSDT表的起始地址,然后从ServiceTableBase开始是一个长度为NumberOfService的指针数组,每一项是4个字节,是SSDT表中每一个服务的函数地址。

在内核调试器windbg中使用dd KeServiceDescriptorTable命令查看KeServiceDescriptorTable数据,就可以看到SSDTEntry结构的每一项数据。

64位系统

64位系统上ntdll.dll使用 mov r10,rcx;mov eax,xxx传入索引值,因此也可以通过遍历64位的ntdll.dll查看每一个函数对应的服务号。从而找到服务函数名和服务编号的关系。

如下图所示,使用IDA查看windows 7 x64的64位ntdll.dll的函数

通过SSDT表可以看出二者却是相互对应(这是因为,系统就是通过ntdll.dll来逆推出SSDT表的每一项对应的函数名的)

由于64位的内核文件并未导出KeServiceDescriptorTable的信息,所以64位系统的SSDT表的起始地址无法直接获得。但是却同样可以在windbg中使用dd KeServiceDescriptorTable命令查看KeServiceDescriptorTable内容:

但是,我们却无法使用某种方式直接获得KeServiceDescriptorTable的地址,于是常采用间接方式。

使用windbg观察nt!KiSystemServiceRepeat函数的反汇编如下

可以发现,x64系统会在KiSystemServiceRepeat函数使用lea r10,[nt!KeServiceDescriptorTable]指令获得KeServiceDescriptorTable的地址,因此只要在KiSystemServiceRepeat函数内搜索4c 8d 15 得到这条指令的起始地址。

KeServiceDescriptorTable地址保存在这条指令之后的地址即fffff800`03e98779+指令操作数(是偏移量)得到的新地址中(指令反汇编的知识,用四个字节作为偏移量),即

pKeServiceDescriptorTable= fffff800`03e98779+2320c7= FFFFF800`040CA840

通过windbg验证KeServiceDescriptorTable的地址

而KeServiceDescriptorTable结构如下:

#pragma pack(1)
typedef struct _SERVICE_DESCIPTOR_TABLE
{
	PULONG ServiceTableBase;		// SSDT基址,8字节大小
	PVOID ServiceCounterTableBase;	// SSDT中服务被调用次数计数器,8字节大小
	ULONGLONG NumberOfService;		// SSDT服务函数的个数,8字节大小
	PVOID ParamTableBase;			// 系统服务参数表基址,8字节大小。实际指向的数组是以字节为单位的记录着对应服务函数的参数个数
}SSDTEntry, *PSSDTEntry;
#pragma pack()

通过以上信息,可以看出SSDT的首地址是fffff800`03e9a300,SSDT中以每4个字节为单位描述一个服务的地址项信息,不过其内容并非实际的地址,因为4个字节无法保存64位下的地址,实际上其内容左移4位得到的是地址相对偏移量,是真实服务地址相对SSDT起始地址的偏移量。下面的内容可以帮助理解真实服务地址的计算过程。

通过windbg查看fffff800`03e9a300的数据

通过以上信息,验证第一项和第二个项指向的地址,SSDT的首地址是fffff800`03e9a300:

fffff800`03e9a300+040d9a00>>4= fffff800`03e9a300+040d9a0= FFFFF800`042A7C0

fffff800`03e9a300+02f55c00>>4= fffff800`03e9a300+02f55c0= FFFFF800`0418F8C0

很明显这与PCHunter显示的一致

上面的操作需要直到KiSystemServiceRepeat地址,然而KiSystemServiceRepeat函数也没有导出。所以无法知道其地址。

上面介绍了那么多,要想获得指定编号的服务函数的名称是非常简单的

如何获得64位Windows系统的SSDT表的基址

方式一:硬编码,对每一个系统的不同版本,分别使用windbg工具获得KiSystemServiceRepeat函数的地址。

方式二:读取msr寄存器(特别模块寄存器)的0xc0000082的值,即在微软的C语言中使用__readmsr(0xc0000082)获得KiSystemCall64函数的地址。

pKiSystemCall64=(PVOID)__readmsr(0xc0000082);

获得地址后遍历该地址后的数据会经过KiSystemServiceRepeat函数,就可以通过特征码4c 8d 15找到目标指令。

具体计算公式是:

特征码起始地址是pKiSystemCall64+i则(i表示相对KiSystemCall64函数开始处偏移i个字节)

pKeServiceDescriptorTable=(PVOID)(pKiSystemCall64+i+7+*(PLONG)( pKiSystemCall64+i+3));

pSSDT=*(PLONG)pKeServiceDescriptorTable

注意:上述代码中数字7表示lea r10,[xxx]指令的长度是7个字节,还需要注意的是:windows 10的1809版本(10.0.17763.xxx)后的系统的SSDT表的获得又发生了新的变化。因为它通过jmp指令跳转到其它地址后才执行到上述特征指令。

方式三:解析本操作系统下的内核文件的符号文件。

使用VS2017和WDK编写64位C语言驱动程序示例代码

typedef struct _SYSTEM_SERVICE_DESCIPTOR_TABLE_SR							//系统描述符表(SSDT)占用32字节
{
	PULONG ServiceTableBase;												//SSDT数组的基址,8字节大小,数组中每个元素占4字节,保存的内容左移4位得到的是地址相对偏移量,是服务函数地址相对SSDT起始地址的偏移量
	PVOID ServiceCounterTableBase;											//SSDT中服务被调用次数计数器,8字节大小
	ULONGLONG NumberOfService;												//SSDT服务函数的个数,8字节大小
	PVOID ParamTableBase;													//系统服务参数表基址,8字节大小。实际指向的数组是以字节为单位的记录着对应服务函数的参数个数
}SYSTEM_SERVICE_DESCIPTOR_TABLE_SR, *PSYSTEM_SERVICE_DESCIPTOR_TABLE_SR;

#ifdef DBG
#define KDbgPrint DbgPrint													//debug生成模式下将输出调试信息
#else
#define KDbgPrint(format,...)												//Release生成模式下不会输出调试信息
#endif


PEPROCESS find_process(IN const char* process_image_file_name, OUT OPTIONAL PDWORD pprocess_pid)
{
	ULONG pid;
	NTSTATUS status;
	PEPROCESS peprocess_find,peprocess_ret=NULL;
	for (pid = 0; pid <= 240000; pid += 4)
	{
		status = PsLookupProcessByProcessId((HANDLE)pid, &peprocess_find);
		if (NT_SUCCESS(status))
		{
			if (strcmp((CHAR*)PsGetProcessImageFileName(peprocess_find), process_image_file_name) == 0)
			{
				peprocess_ret = peprocess_find;
				if(pprocess_pid) *pprocess_pid = pid;
			}
			ObDereferenceObject(peprocess_find);
		}
		if (peprocess_ret) return peprocess_ret;
	}
	return NULL;
}


PVOID get_system_service_descriptor_table_addr_x64()
{
	PUCHAR start_search_addr = NULL;
	PUCHAR end_search_addr = NULL;
	PUCHAR i = NULL;
	UCHAR b1 = 0, b2 = 0, b3 = 0;
	LONG tmp_long = 0;
	ULONGLONG addr = 0;
	RTL_OSVERSIONINFOW ver = { 0 };

	ver.dwOSVersionInfoSize = sizeof(ver);

	RtlGetVersion(&ver);
	start_search_addr = (PUCHAR)__readmsr(0XC0000082);
	if (ver.dwBuildNumber >= 17763)//1809版本(10.0.17763.xxx)以后jmp 过去了
	{

		for (i = start_search_addr; i < start_search_addr + 0x500; i++)
		{
			if (MmIsAddressValid(i) && MmIsAddressValid(i + 5))
			{
				b1 = *i;
				b2 = *(i + 5);
				if (b1 == 0xe9 && b2 == 0xc3)
				{
					memcpy(&tmp_long, i + 1, 4);
					start_search_addr = i + 5 + tmp_long;
					break;
				}
			}
		}
	}
	for (i = start_search_addr; i < start_search_addr + 0x500; i++)
	{
		if (MmIsAddressValid(i) && MmIsAddressValid(i + 2))
		{
			b1 = *i;
			b2 = *(i + 1);
			b3 = *(i + 2);
			if (b1 == 0x4c && b2 == 0x8d && b3 == 0x15)
			{
				memcpy(&tmp_long, i + 3, 4);
				addr = (ULONGLONG)tmp_long + (ULONGLONG)i + 7;
				break;
			}
		}
	}
	KDbgPrint("%s SSDT addr=0x%p\n", __FUNCTION__, addr);
	return (PVOID)addr;
}

PVOID get_ssdt_fun_addr_by_index_x64(ULONG index)
{
	static PSYSTEM_SERVICE_DESCIPTOR_TABLE_SR pssdt = NULL;
	if (pssdt == NULL)pssdt = (PSYSTEM_SERVICE_DESCIPTOR_TABLE_SR)get_system_service_descriptor_table_addr_x64();
	if (pssdt == NULL) return NULL;
	PULONG fun_array = pssdt->ServiceTableBase;
	if (index >= pssdt->NumberOfService) return NULL;
	LONG tmp = fun_array[index];
	tmp = tmp >> 4;
	PVOID ret = (PVOID)((UINT64)tmp + (UINT64)fun_array);
	KDbgPrint("%s line=%d fun_index=%d fun_addr=0x%p\n", __FUNCTION__, __LINE__, index, ret);
	return ret;
}

PVOID get_shadow_system_service_descriptor_table_addr_x64()
{
	PUCHAR start_search_addr = NULL;
	PUCHAR end_search_addr = NULL;
	PUCHAR i = NULL;
	UCHAR b1 = 0, b2 = 0, b3 = 0;
	LONG tmp_long = 0;
	ULONGLONG addr = 0;
	RTL_OSVERSIONINFOW ver = { 0 };

	ver.dwOSVersionInfoSize = sizeof(ver);

	RtlGetVersion(&ver);
	start_search_addr = (PUCHAR)__readmsr(0XC0000082);
	if (ver.dwBuildNumber >= 17763)//1809版本以后
	{

		for (i = start_search_addr; i < start_search_addr + 0x500; i++)
		{
			if (MmIsAddressValid(i) && MmIsAddressValid(i + 5))
			{
				b1 = *i;
				b2 = *(i + 5);
				if (b1 == 0xe9 && b2 == 0xc3)
				{
					memcpy(&tmp_long, i + 1, 4);
					start_search_addr = i + 5 + tmp_long;
					break;
				}
			}
		}
	}
	for (i = start_search_addr; i < start_search_addr + 0x500; i++)
	{
		if (MmIsAddressValid(i) && MmIsAddressValid(i + 2))
		{
			b1 = *i;
			b2 = *(i + 1);
			b3 = *(i + 2);
			if (b1 == 0x4c && b2 == 0x8d && b3 == 0x1d)
			{
				memcpy(&tmp_long, i + 3, 4);
				addr = (ULONGLONG)tmp_long + (ULONGLONG)i + 7;
				break;
			}
		}
	}
	KDbgPrint("%s Shadow SSDT addr=0x%p\n", __FUNCTION__, addr);
	//当前获得了shadow ssdt的地址但是里面保存的是ssdt的描述符,不过没关系,增加一个结构体大小后就是shadow ssdt描述符的地址
	return (PVOID)(addr+sizeof(SYSTEM_SERVICE_DESCIPTOR_TABLE_SR));
}

PVOID get_shadow_ssdt_fun_addr_by_index_x64(ULONG index)
{
	static PSYSTEM_SERVICE_DESCIPTOR_TABLE_SR pshadow_ssdt = NULL;
	//win10中不能正确的获得SSSDT表的地址里的函数原因:
	//win32k.sys的内存地址只有GUI线程才会被映射该内存
	//windows中获得的shadow ssdt描述符地址后只有在GUI线程中才会映射真正的数据,否则该地址映射的实际上依然是ssdt描述符表的内容
	//因此获得之前应当切换到一个GUI进程中,然后再读取地址内的数据
	//切换到目标进程的方法是:首先调用KeStackAttachProcess切换到目标进程,然后再调用KeUnstackDetachProcess返回到当前进程
	//当获得了地址后,还需要做函数的地址的转换(如果需要的话),因为对于来自用户层的函数调用实际上会通过jmp跳转到win32kfull.sys中进行后续任务
	if (pshadow_ssdt == NULL)pshadow_ssdt = (PSYSTEM_SERVICE_DESCIPTOR_TABLE_SR)get_shadow_system_service_descriptor_table_addr_x64();

	PEPROCESS peprocess_csrss=find_process("csrss.exe");
	KAPC_STATE apc_old = { 0 ,};
	KeStackAttachProcess(peprocess_csrss, &apc_old);
	if (pshadow_ssdt == NULL) return NULL;
	PULONG fun_array = pshadow_ssdt->ServiceTableBase;
	if (index >= pshadow_ssdt->NumberOfService) return NULL;
	LONG tmp = fun_array[index];
	tmp = tmp >> 4;
	PVOID ret = (PVOID)((UINT64)tmp + (UINT64)fun_array);
	KeUnstackDetachProcess(&apc_old);
	KDbgPrint("%s line=%d fun_index=%d fun_addr=0x%p\n", __FUNCTION__,__LINE__, index, ret);
	return ret;
}

  

posted @ 2019-03-02 13:58  J坚持C  阅读(2740)  评论(0编辑  收藏  举报