Notebook
Search…
Hook SSDT(Shadow)
[TOC]

Basic struct

struct SSDTStruct
{
LONG* pServiceTable;
PVOID pCounterTable;
#ifdef _WIN64
ULONGLONG NumberOfServices;
#else
ULONG NumberOfServices;
#endif
PCHAR pArgumentTable;
};

Find SSDT(Shadow) base address

x86

SSDT:
1: kd> dps nt!KeServiceDescriptorTable
83f86b00 83e7ccbc nt!KiServiceTable
83f86b04 00000000
83f86b08 00000191
83f86b0c 83e7d304 nt!KiArgumentTable
1: kd> dd /c 1 nt!KiServiceTable l2
83e7ccbc 84098ffa # Function offset
83e7ccc0 83ed5217
SSDT Shadow:
1: kd> dps nt!KeServiceDescriptorTableShadow
83f86b40 83e7ccbc nt!KiServiceTable # SSDT
83f86b44 00000000
83f86b48 00000191
83f86b4c 83e7d304 nt!KiArgumentTable
83f86b50 9be58000 win32k!W32pServiceTable # SSDT Shadow
83f86b54 00000000
83f86b58 00000339
83f86b5c 9be5902c win32k!W32pArgumentTable
1: kd> dd /c 1 win32k!W32pServiceTable l2
9be58000 9bde19df
9be58004 9bdfa31b
Before dump SSDT Shadow, you have to attach to any user-mode GUI process (like winlogon.exe)
1: kd> !process 0 0 winlogon.exe
PROCESS 882d3d20 SessionId: 1 Cid: 01d4 Peb: 7ffdc000 ParentCid: 0188
DirBase: bf153080 ObjectTable: 9b33f300 HandleCount: 55.
Image: winlogon.exe
1: kd> .process /p 882d3d20
Implicit process is now 882d3d20
.cache forcedecodeuser done
Function Index to real function address:
realAddress = (PVOID)ntTable[FunctionIndex]
Find table address in a driver
Because nt!KeServiceDescriptorTable is exported by kernel, we can get its address in the program
UNICODE_STRING routineName;
RtlInitUnicodeString(&routineName, L"KeServiceDescriptorTable");
pServiceDescriptorTable = (SSDTStruct*)MmGetSystemRoutineAddress(&routineName);
Then we can get the address of nt!KeServiceDescriptorTableShadow by add the offset (like previously windbg shows)
pServiceDescriptorTableShadow = (PVOID)((ULONG_PTR)pServiceDescriptorTable + 0x40)
Then Get the addresses of two tables
pNtTable = pServiceDescriptorTable;
pW32kTable = (PVOID)((ULONG_PTR)pServiceDescriptorTable + 0x10)

x64

SSDT:
1: kd> dps nt!KeServiceDescriptorTable
fffff800`040c7840 fffff800`03e97300 nt!KiServiceTable # SSDT base address
fffff800`040c7848 00000000`00000000
fffff800`040c7850 00000000`00000191
fffff800`040c7858 fffff800`03e97f8c nt!KiArgumentTable
1: kd> dd /c 1 nt!KiServiceTable l10
fffff800`03e97300 040d9a00 # Function offset
fffff800`03e97304 02f55c00
fffff800`03e97308 fff6ea00
fffff800`03e9730c 02e87805
fffff800`03e97310 031a4a06
fffff800`03e97314 03116a05
fffff800`03e97318 02bb9901
...
SSDT Shadow:
1: kd> dps nt!KeServiceDescriptorTableShadow
fffff800`040c7880 fffff800`03e97300 nt!KiServiceTable # SSDT base address
fffff800`040c7888 00000000`00000000
fffff800`040c7890 00000000`00000191
fffff800`040c7898 fffff800`03e97f8c nt!KiArgumentTable
fffff800`040c78a0 fffff960`00111f00 win32k!W32pServiceTable # SSDT Shadow base address
fffff800`040c78a8 00000000`00000000
fffff800`040c78b0 00000000`0000033b
fffff800`040c78b8 fffff960`00113c1c win32k!W32pArgumentTable
1: kd> dd /c 1 win32k!W32pServiceTable l10
fffff960`00111f00 fff3a740 # GDI Function offset
fffff960`00111f04 fff0b501
fffff960`00111f08 000206c0
fffff960`00111f0c 001021c0
...
Before dump SSDT Shadow, you have to attach to any user-mode GUI process (like winlogon.exe)
1: kd> !process 0 0 winlogon.exe
PROCESS fffffa80042a8b30
SessionId: 1 Cid: 01ec Peb: 7fffffde000 ParentCid: 0198
DirBase: 1bf29000 ObjectTable: fffff8a000a01580 HandleCount: 111.
Image: winlogon.exe
1: kd> .process /p fffffa80042a8b30
Implicit process is now fffffa80`042a8b30
.cache forcedecodeuser done
1: kd> .reload
Function Index to real function address:
readAddress = (ULONG_PTR)(ntTable[FunctionIndex] >> 4) + SSDT(Shadow)BaseAddress;
Find table address in a driver
Find nt!KeServiceDescriptorTable and nt!KeServiceDescriptorTableShadow in kernel function nt!KiSystemServiceStart
0: kd> u nt!KiSystemServiceStart
nt!KiSystemServiceStart:
fffff800`03e9575e 4889a3d8010000 mov qword ptr [rbx+1D8h],rsp
fffff800`03e95765 8bf8 mov edi,eax
fffff800`03e95767 c1ef07 shr edi,7
fffff800`03e9576a 83e720 and edi,20h
fffff800`03e9576d 25ff0f0000 and eax,0FFFh
nt!KiSystemServiceRepeat:
fffff800`03e95772 4c8d15c7202300 lea r10,[nt!KeServiceDescriptorTable (fffff800`040c7840)]
fffff800`03e95779 4c8d1d00212300 lea r11,[nt!KeServiceDescriptorTableShadow
As shown above, we can search the whole kernel address from the kernel base for KiSystemServiceStart 's pattern
const unsigned char KiSystemServiceStartPattern[] = {
0x8B, 0xF8, // mov edi,eax
0xC1, 0xEF, 0x07, // shr edi,7
0x83, 0xE7, 0x20, // and edi,20h
0x25, 0xFF, 0x0F, 0x00, 0x00 // and eax,0fffh
};
bool found = false;
ULONG KiSSSOffset;
for(KiSSSOffset = 0; KiSSSOffset < kernelSize - signatureSize; KiSSSOffset++)
{
if(RtlCompareMemory(
((unsigned char*)kernelBase + KiSSSOffset),
KiSystemServiceStartPattern, signatureSize) == signatureSize)
{
found = true;
break;
}
}
if(!found)
return NULL;
After find the target function, we can retrieve the address of nt!KeServiceDescriptorTableShadow in the instruction:
4c8d1d00212300 lea r11,[nt!KeServiceDescriptorTableShadow
ULONG_PTR addressAfterPattern = kernelBase + kiSSSOffset + signatureSize
ULONG_PTR address = addressAfterPattern + 7 // Skip lea r10,[nt!KeServiceDescriptorTable]
LONG relativeOffset = 0;
// lea r11, KeServiceDescriptorTableShadow
if((*(unsigned char*)address == 0x4c) &&
(*(unsigned char*)(address + 1) == 0x8d) &&
(*(unsigned char*)(address + 2) == 0x1d))
{
relativeOffset = *(LONG*)(address + 3);
}
if(relativeOffset == 0)
return NULL;
SSDTStruct* shadow = (SSDTStruct*)( address + relativeOffset + 7 );
Then we can get the addresses of nt!KiServiceTable(SSDT) and win32k!W32pServiceTable(SSDT Shadow)
PVOID ntTable = (PVOID)shadow;
PVOID win32kTable = (PVOID)((ULONG_PTR)shadow + 0x20); // Offset showed in Windbg

Find the base address of ntoskrnl.exe and win32k.sys

First, we use the undocumented function ZwQuerySystemInformation with parameter of SystemModuleInformation to get information of system modules
// Get SystemInfo size first
ZwQuerySystemInformation(SystemModuleInformation,
&SystemInfoBufferSize,
0,
&SystemInfoBufferSize);
// Allocate buffer for system info
pSystemInfoBuffer = (PSYSTEM_MODULE_INFORMATION)ExAllocatePool(NonPagedPool, SystemInfoBufferSize * 2);
// Query SystemIfno
status = ZwQuerySystemInformation(SystemModuleInformation,
pSystemInfoBuffer,
SystemInfoBufferSize * 2,
&SystemInfoBufferSize);
while the struct of SYSTEM_MODULE_INFORMATION is
typedef struct _SYSTEM_MODULE_INFORMATION
{
ULONG Count;
SYSTEM_MODULE_ENTRY Module[0];
} SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;
typedef struct _SYSTEM_MODULE_ENTRY
{
HANDLE Section;
PVOID MappedBase;
PVOID ImageBase;
ULONG ImageSize;
ULONG Flags;
USHORT LoadOrderIndex;
USHORT InitOrderIndex;
USHORT LoadCount;
USHORT OffsetToFileName;
UCHAR FullPathName[256];
} SYSTEM_MODULE_ENTRY, *PSYSTEM_MODULE_ENTRY;
Usually the module ntoskrnl.exe is always the first module in the buffer, which isModule[0], so:
PVOID ntBase = pSystemInfoBuffer->Module[0].ImageBase;
But the position of win32k.sys in the buffer is not sure, so we have to traverse the buffer to find it by compare the FullPathName field of each SYSTEM_MODULE_ENTRY with "win32k.sys"
PSYSTEM_MODULE_ENTRY moduleInfo = pSystemInfoBuffer->Module;
UNICODE_STRING win32kPath = RTL_CONSTANT_STRING(L"\\SystemRoot\\system32\\win32k.sys");
UNICODE_STRING moduleName;
ANSI_STRING ansiModuleName;
while ( TRUE )
{
RtlInitAnsiString( &ansiModuleName, (PCSZ)moduleInfo->FullPathName );
RtlAnsiStringToUnicodeString( &moduleName, &ansiModuleName, TRUE );
if ( RtlCompareUnicodeString( &win32kPath, &moduleName, TRUE ) == 0)
{
PVOID win32kBase = moduleInfo->ImageBase;
break;
}
moduleInfo = moduleInfo + 1; // Iterate to next module entry
}

Find SSDT function index by function name

Index in ntdll.dll

Almost all SSDT function have a stub function which has the same name and is exported by NTDLL.DLL
eg. the first few lines of user-mode function NtCreateFile exported by NTDLL.DLL (You have to attach to user-mode process to view symbols in ntdll)
0: kd> u ntdll!NtCreateFile
ntdll!ZwCreateFile:
00000000`774d1860 4c8bd1 mov r10,rcx
00000000`774d1863 b852000000 mov eax,52h # This is the SSDT index
00000000`774d1868 0f05 syscall
00000000`774d186a c3 ret
00000000`774d186b 0f1f440000 nop dword ptr [rax+rax]
As above shows, mov eax, 52h transfers the system call number to EAX, and call syscall to trap into the kernel, then kernel will use the number 52h to find corresponding kernel function in SSDT
0: kd> dd /c 1 nt!KiServiceTable l53 # Function index begin from 0
fffff800`03e97300 040d9a00
fffff800`03e97304 02f55c00
fffff800`03e97308 fff6ea00
fffff800`03e9730c 02e87805
fffff800`03e97310 031a4a06
......
fffff800`03e97438 031bab01
fffff800`03e9743c 02efec80
fffff800`03e97440 02d52300
fffff800`03e97444 04637102
fffff800`03e97448 03071007 # This is the index of nt!NtCreateFile (52h)
0: kd> u FFFFF8000419E400
nt!NtCreateFile:
fffff800`0419e400 4c8bdc mov r11,rsp
fffff800`0419e403 4881ec88000000 sub rsp,88h
fffff800`0419e40a 33c0 xor eax,eax
fffff800`0419e40c 498943f0 mov qword ptr [r11-10h],rax
fffff800`0419e410 c744247020000000 mov dword ptr [rsp+70h],20h
SO, we can get any SSDT function's index from its name by looking into its user-mode stub function exported by NTDLL.DLL!

Get SSDT index from ntdll.dll

First we must read ntdll.dll into memory
UNICODE_STRING FileName;
OBJECT_ATTRIBUTES ObjectAttributes;
RtlInitUnicodeString(&FileName, L"\\SystemRoot\\system32\\ntdll.dll");
InitializeObjectAttributes(&ObjectAttributes, &FileName,
OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE,
NULL, NULL);
if(KeGetCurrentIrql() != PASSIVE_LEVEL)
{
return STATUS_UNSUCCESSFUL;
}
HANDLE FileHandle;
IO_STATUS_BLOCK IoStatusBlock;
// Open ntdll.dll file
NTSTATUS NtStatus = ZwCreateFile(&FileHandle,
GENERIC_READ,
&ObjectAttributes,
&IoStatusBlock, NULL,
FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ,
FILE_OPEN,
FILE_SYNCHRONOUS_IO_NONALERT,
NULL, 0);
if(NT_SUCCESS(NtStatus))
{
// Get ntdll.dll file size
FILE_STANDARD_INFORMATION StandardInformation = { 0 };
NtStatus = ZwQueryInformationFile(FileHandle, &IoStatusBlock, &StandardInformation, sizeof(FILE_STANDARD_INFORMATION), FileStandardInformation);
if(NT_SUCCESS(NtStatus))
{
FileSize = StandardInformation.EndOfFile.LowPart;
FileData = (unsigned char*)RtlAllocateMemory(true, FileSize);
LARGE_INTEGER ByteOffset;
ByteOffset.LowPart = ByteOffset.HighPart = 0;
// Read ntdll.dll into buffer
NtStatus = ZwReadFile(FileHandle,
NULL, NULL, NULL,
&IoStatusBlock,
FileData,
FileSize,
&ByteOffset, NULL);
}
ZwClose(FileHandle);
}
Then get the real function address of any exported function .
To do this, we have to get its Export Address Table from its PE header
//Verify DOS Header
PIMAGE_DOS_HEADER pdh = (PIMAGE_DOS_HEADER)FileData;
//Verify PE Header
PIMAGE_NT_HEADERS pnth = (PIMAGE_NT_HEADERS)(FileData + pdh->e_lfanew);
//Verify Export Directory
PIMAGE_DATA_DIRECTORY pdd = NULL;
if(pnth->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC)
pdd = ((PIMAGE_NT_HEADERS64)pnth)->OptionalHeader.DataDirectory;
else
pdd = ((PIMAGE_NT_HEADERS32)pnth)->OptionalHeader.DataDirectory;
ULONG ExportDirRva = pdd[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
ULONG ExportDirSize = pdd[IMAGE_DIRECTORY_ENTRY_EXPORT].Size;
ULONG ExportDirOffset = RvaToOffset(pnth, ExportDirRva, FileSize);
//Read Export Directory
PIMAGE_EXPORT_DIRECTORY ExportDir = (PIMAGE_EXPORT_DIRECTORY)(FileData + ExportDirOffset);
ULONG NumberOfNames = ExportDir->NumberOfNames;
ULONG AddressOfFunctionsOffset = RvaToOffset(pnth, ExportDir->AddressOfFunctions, FileSize);
ULONG AddressOfNameOrdinalsOffset = RvaToOffset(pnth, ExportDir->AddressOfNameOrdinals, FileSize);
ULONG AddressOfNamesOffset = RvaToOffset(pnth, ExportDir->AddressOfNames, FileSize);
ULONG* AddressOfFunctions = (ULONG*)(FileData + AddressOfFunctionsOffset);
USHORT* AddressOfNameOrdinals = (USHORT*)(FileData + AddressOfNameOrdinalsOffset);
ULONG* AddressOfNames = (ULONG*)(FileData + AddressOfNamesOffset);
and get the file offset of the function address from its name
// Find RVA of exported function whose name is ExportName
ULONG ExportOffset = PE_ERROR_VALUE;
for(ULONG i = 0; i < NumberOfNames; i++)
{
ULONG CurrentNameOffset = RvaToOffset(pnth, AddressOfNames[i], FileSize);
if(CurrentNameOffset == PE_ERROR_VALUE)
continue;
const char* CurrentName = (const char*)(FileData + CurrentNameOffset);
ULONG CurrentFunctionRva = AddressOfFunctions[AddressOfNameOrdinals[i]];
if(CurrentFunctionRva >= ExportDirRva && CurrentFunctionRva < ExportDirRva + ExportDirSize)
continue; //we ignore forwarded exports
//compare the export name to the requested export
if(!strcmp(CurrentName, ExportName))
{
// Convert function RVA to offset
ExportOffset = RvaToOffset(pnth, CurrentFunctionRva, FileSize);
break;
}
}
Then get the real address of exported function in memory ( in ntdll.dll )
ExportFunctionAddress = (PVOID)((ULONG_PTR)FileData + ExportOffset)
finally search the whole function for pattern mov eax, xx, retrieve the SSDT index
for(int i = 0; i < 32 && ExportOffset + i < FileSize; i++)
{
if(ExportData[i] == 0xC2 || ExportData[i] == 0xC3) //RET
break;
if(ExportData[i] == 0xB8) //mov eax,XX -- b8 FFFFFFFF
{
SsdtOffset = *(int*)(ExportData + i + 1);
break;
}
}

Combine index with SSDT(Shadow)

x64
realNtFunction = (PVOID)(ntTable[SsdtOffset] >> 4 + ntTable);
x86
realNtFunction = (PVOID)(ntTable[SsdtOffset]);
Shadow is similar

Hook SSDT

!!! Yon have to first disable PatchGuard or enable DebugMode on your system !!!
😀
In order to unhook SSDT in future, define some hook structs as below (HOOKOPCODES is our shellcode for x64 hook)
#pragma pack(push,1) // This is very important !!!!
struct HOOKOPCODES
{
#ifdef _WIN64
unsigned short int mov;
#else
unsigned char mov;
#endif
ULONG_PTR addr;
unsigned char push;
unsigned char ret;
};
#pragma pack(pop)
typedef struct HOOKSTRUCT
{
ULONG_PTR addr;
HOOKOPCODES hook;
unsigned char orig[sizeof(HOOKOPCODES)];
//SSDT extension
int SSDTindex;
LONG SSDTold;
LONG SSDTnew;
ULONG_PTR SSDTaddress;
}HOOK, *PHOOK;

Disable Write Protection

Because SSDT are read-only kernel memory, so you have to disable write protection before modifying it , we use MDL to do this. Below is the helper function to copy data to any read-only kernel memory
NTSTATUS SuperCopyMemory(
IN VOID UNALIGNED* Destination,
IN CONST VOID UNALIGNED* Source,
IN ULONG Length)
{
//Change memory properties.
PMDL g_pmdl = IoAllocateMdl(Destination, Length, 0, 0, NULL);
if(!g_pmdl)
return STATUS_UNSUCCESSFUL;
MmBuildMdlForNonPagedPool(g_pmdl);
unsigned int* Mapped = (unsigned int*)MmMapLockedPages(g_pmdl, KernelMode);
if(!Mapped)
{
IoFreeMdl(g_pmdl);
return STATUS_UNSUCCESSFUL;
}
KIRQL kirql = KeRaiseIrqlToDpcLevel();
RtlCopyMemory(Mapped, Source, Length);
KeLowerIrql(kirql);
//Restore memory properties.
MmUnmapLockedPages((PVOID)Mapped, g_pmdl);
IoFreeMdl(g_pmdl);
return STATUS_SUCCESS;
}
Also, we can modify the "WP" flags of the cr0register to enable or disable kernel memory write protection
To disable WP:
KIRQL DisableWP(){
KIRQL irql=KeRaiseIrqlToDpcLevel();
ULONG_PTR cr0=__readcr0();
#ifdef _AMD64_
cr0 &= 0xfffffffffffeffff;
#else
cr0 &= 0xfffeffff;
#endif
__writecr0(cr0);
_disable(); // Disable interrupts
return irql;
}
there we can modify the kernel memory without access violation.
🧐
To enable WP after writing:
void EnableWP(KIRQL irql){
ULONG_PTR cr0=__readcr0();
cr0 |= 0x10000;
_enable(); // Enable interrupts
__writecr0(cr0);
KeLowerIrql(irql);
}

x86 Hook

Just replace the SSDT index with your own function
PVOID realNtFunction = ntTable[FunctionIndex];
hHook = (HOOK)RtlAllocateMemory(true, sizeof(HOOK));
hHook.SSDTold = realNtFunction;
hHook.SSDTnew = YourFunction;
hHook.SSDTindex = FunctionIndex;
hHook.SSDTaddress = realNtFunction;
SuperCopyMemory(&realNtFunction, YourFunction);

x64 Hook

In x64 platform, because realFunction = ntTable[FunctionIndex] >> 4 + ntTable, the only thing you can modify is the value of ntTable[FunctionIndex], and it's long type(32 bit), which means you can only jump within the range -2GB + ntTable ~ 2GB + ntTable, Obviously the address is inside the ntoskrnl.exe module address space. So to jump to the function located in our own module, we have to make a "indirect jump".
Jump from SSDT to our stub function, then jump to our real function. Below is the stub shellcode
movabs rax, xxxxxxxxxxxxxxxx ; 48B8 XXXXXXXXXXXXXXXX (move our real function to rax)
push rax ; 50
ret ; c3 (return to rax, which is our function)
As you can see, the shellcode is only 12 bytes, so we only need to find a executable memory block which is larger than 12 bytes. Well in every code page(4KB size) there are many nop instruction where we can insert our shellcode, So we try to insert the shellcode into the code page where the real kernel function to be hooked is.
Get the ntoskrnl.exe's section header first
ULONG dwRva = (ULONG)((unsigned char*)realNtFunction - (unsigned char*)ntBase);
IMAGE_DOS_HEADER* pdh = (IMAGE_DOS_HEADER*)ntBase;
if(pdh->e_magic != IMAGE_DOS_SIGNATURE)
return 0;
IMAGE_NT_HEADERS* pnth = (IMAGE_NT_HEADERS*)((unsigned char*)ntBase + pdh->e_lfanew);
if(pnth->Signature != IMAGE_NT_SIGNATURE)
return 0;
IMAGE_SECTION_HEADER* psh = IMAGE_FIRST_SECTION(pnth);
find out in which section the real function is
static ULONG RvaToSection(IMAGE_NT_HEADERS* pNtHdr, ULONG dwRVA)
{
USHORT wSections;
PIMAGE_SECTION_HEADER pSectionHdr;
pSectionHdr = IMAGE_FIRST_SECTION(pNtHdr);
wSections = pNtHdr->FileHeader.NumberOfSections;
for(int i = 0; i < wSections; i++)
{
if(pSectionHdr[i].VirtualAddress <= dwRVA &&
(pSectionHdr[i].VirtualAddress + pSectionHdr[i].Misc.VirtualSize) > dwRVA)
return i;
}
return (ULONG) - 1;
}
int section = RvaToSection(pnth, dwRva);
if(section == -1)
return 0;
CodeSize = psh[section].SizeOfRawData;
CodeStart = (PVOID)((ULONG_PTR)ntBase + psh[section].VirtualAddress);
then try to find a at least 12 bytes large nop block in the code page
unsigned char* Code = (unsigned char*)CodeStart;
unsigned int CaveSize = sizeof(HOOKOPCODES);
for(unsigned int i = 0, j = 0; i < CodeSize; i++)
{
if(Code[i] == 0x90 || Code[i] == 0xCC) //NOP or INT3
j++;
else
j = 0;
if(j == CaveSize)
ShellcodeCave = (PVOID)((ULONG_PTR)CodeStart + i - CaveSize + 1);
}
Next we need to write the shellcode to the cave address we just got, and insert our own function address
initialize the hook struct
//allocate structure
PHOOK hook = (PHOOK)RtlAllocateMemory(true, sizeof(HOOK));
//set hooking address
hook->addr = ShellcodeCave; // Store the cave address
//set hooking opcode
#ifdef _WIN64
hook->hook.mov = 0xB848;
#else
hook->hook.mov = 0xB8;
#endif
hook->hook.addr = (ULONG_PTR)YourFunction; // Insert our own function
hook->hook.push = 0x50;
hook->hook.ret = 0xc3;
//set original data
RtlCopyMemory(&hook->orig, (const void*)addr, sizeof(HOOKOPCODES));
write to the cave address
SuperCopyMemory((void*)ShellcodeCave, &hook->hook, sizeof(HOOK));
After that, we can change the corresponding SSDT offset to point to shellcode cave.
You have to convert cave address to the relative offset to SSDT first !!
oldOffset = ntTable[FunctionIndex];
newOffset = (LONG)((ULONG_PTR)ShellcodeCave - ntTable);
newOffset = (newOffset << 4) | oldOffset & 0xF;
update SSDT
hook.SSDTold = oldOffset;
hook.SSDTnew = newOffset;
hook.SSDTIndex = FunctionIndex;
hook.SSDTaddress = realNtFunction;
SuperCopyMemory(&ntTable[FunctionIndex], newOffset);
DONE!

Hook Shadow SSDT

The steps of hooking Shadow is similar with SSDT hook, except that you have to attach to a user-mode GUI process before getting the base address of the Shadow.
// Get the process id of the "winlogon.exe" process
GetProcessIdByName(ProcessId, "winlogon.exe");
PsLookupProcessByProcessId( ProcessId, &Process );
APC_STATE oldApc;
KeStackAttachProcess(Process, &oldApc);
// Find Shadows SSDT and do dirty things
KeUnstackDetachProcess(&oldApc);
Copy link
On this page
Basic struct
Find SSDT(Shadow) base address
x86
x64
Find the base address of ntoskrnl.exe and win32k.sys
Find SSDT function index by function name
Hook SSDT
Disable Write Protection
x86 Hook
x64 Hook
Hook Shadow SSDT