Notebook
Search…
Hook SSDT(Shadow)
[TOC]

Basic struct

1
struct SSDTStruct
2
{
3
LONG* pServiceTable;
4
PVOID pCounterTable;
5
#ifdef _WIN64
6
ULONGLONG NumberOfServices;
7
#else
8
ULONG NumberOfServices;
9
#endif
10
PCHAR pArgumentTable;
11
};
Copied!

Find SSDT(Shadow) base address

x86

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

x64

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

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
1
// Get SystemInfo size first
2
ZwQuerySystemInformation(SystemModuleInformation,
3
&SystemInfoBufferSize,
4
0,
5
&SystemInfoBufferSize);
6
// Allocate buffer for system info
7
pSystemInfoBuffer = (PSYSTEM_MODULE_INFORMATION)ExAllocatePool(NonPagedPool, SystemInfoBufferSize * 2);
8
// Query SystemIfno
9
status = ZwQuerySystemInformation(SystemModuleInformation,
10
pSystemInfoBuffer,
11
SystemInfoBufferSize * 2,
12
&SystemInfoBufferSize);
Copied!
while the struct of SYSTEM_MODULE_INFORMATION is
1
typedef struct _SYSTEM_MODULE_INFORMATION
2
{
3
ULONG Count;
4
SYSTEM_MODULE_ENTRY Module[0];
5
} SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;
6
7
typedef struct _SYSTEM_MODULE_ENTRY
8
{
9
HANDLE Section;
10
PVOID MappedBase;
11
PVOID ImageBase;
12
ULONG ImageSize;
13
ULONG Flags;
14
USHORT LoadOrderIndex;
15
USHORT InitOrderIndex;
16
USHORT LoadCount;
17
USHORT OffsetToFileName;
18
UCHAR FullPathName[256];
19
} SYSTEM_MODULE_ENTRY, *PSYSTEM_MODULE_ENTRY;
Copied!
Usually the module ntoskrnl.exe is always the first module in the buffer, which isModule[0], so:
1
PVOID ntBase = pSystemInfoBuffer->Module[0].ImageBase;
Copied!
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"
1
PSYSTEM_MODULE_ENTRY moduleInfo = pSystemInfoBuffer->Module;
2
UNICODE_STRING win32kPath = RTL_CONSTANT_STRING(L"\\SystemRoot\\system32\\win32k.sys");
3
UNICODE_STRING moduleName;
4
ANSI_STRING ansiModuleName;
5
while ( TRUE )
6
{
7
RtlInitAnsiString( &ansiModuleName, (PCSZ)moduleInfo->FullPathName );
8
RtlAnsiStringToUnicodeString( &moduleName, &ansiModuleName, TRUE );
9
if ( RtlCompareUnicodeString( &win32kPath, &moduleName, TRUE ) == 0)
10
{
11
PVOID win32kBase = moduleInfo->ImageBase;
12
break;
13
}
14
moduleInfo = moduleInfo + 1; // Iterate to next module entry
15
}
Copied!

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)
1
0: kd> u ntdll!NtCreateFile
2
ntdll!ZwCreateFile:
3
00000000`774d1860 4c8bd1 mov r10,rcx
4
00000000`774d1863 b852000000 mov eax,52h # This is the SSDT index
5
00000000`774d1868 0f05 syscall
6
00000000`774d186a c3 ret
7
00000000`774d186b 0f1f440000 nop dword ptr [rax+rax]
Copied!
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
1
0: kd> dd /c 1 nt!KiServiceTable l53 # Function index begin from 0
2
fffff800`03e97300 040d9a00
3
fffff800`03e97304 02f55c00
4
fffff800`03e97308 fff6ea00
5
fffff800`03e9730c 02e87805
6
fffff800`03e97310 031a4a06
7
......
8
fffff800`03e97438 031bab01
9
fffff800`03e9743c 02efec80
10
fffff800`03e97440 02d52300
11
fffff800`03e97444 04637102
12
fffff800`03e97448 03071007 # This is the index of nt!NtCreateFile (52h)
13
14
0: kd> u FFFFF8000419E400
15
nt!NtCreateFile:
16
fffff800`0419e400 4c8bdc mov r11,rsp
17
fffff800`0419e403 4881ec88000000 sub rsp,88h
18
fffff800`0419e40a 33c0 xor eax,eax
19
fffff800`0419e40c 498943f0 mov qword ptr [r11-10h],rax
20
fffff800`0419e410 c744247020000000 mov dword ptr [rsp+70h],20h
Copied!
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
1
UNICODE_STRING FileName;
2
OBJECT_ATTRIBUTES ObjectAttributes;
3
RtlInitUnicodeString(&FileName, L"\\SystemRoot\\system32\\ntdll.dll");
4
InitializeObjectAttributes(&ObjectAttributes, &FileName,
5
OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE,
6
NULL, NULL);
7
8
if(KeGetCurrentIrql() != PASSIVE_LEVEL)
9
{
10
return STATUS_UNSUCCESSFUL;
11
}
12
13
HANDLE FileHandle;
14
IO_STATUS_BLOCK IoStatusBlock;
15
// Open ntdll.dll file
16
NTSTATUS NtStatus = ZwCreateFile(&FileHandle,
17
GENERIC_READ,
18
&ObjectAttributes,
19
&IoStatusBlock, NULL,
20
FILE_ATTRIBUTE_NORMAL,
21
FILE_SHARE_READ,
22
FILE_OPEN,
23
FILE_SYNCHRONOUS_IO_NONALERT,
24
NULL, 0);
25
if(NT_SUCCESS(NtStatus))
26
{
27
// Get ntdll.dll file size
28
FILE_STANDARD_INFORMATION StandardInformation = { 0 };
29
NtStatus = ZwQueryInformationFile(FileHandle, &IoStatusBlock, &StandardInformation, sizeof(FILE_STANDARD_INFORMATION), FileStandardInformation);
30
if(NT_SUCCESS(NtStatus))
31
{
32
FileSize = StandardInformation.EndOfFile.LowPart;
33
FileData = (unsigned char*)RtlAllocateMemory(true, FileSize);
34
35
LARGE_INTEGER ByteOffset;
36
ByteOffset.LowPart = ByteOffset.HighPart = 0;
37
// Read ntdll.dll into buffer
38
NtStatus = ZwReadFile(FileHandle,
39
NULL, NULL, NULL,
40
&IoStatusBlock,
41
FileData,
42
FileSize,
43
&ByteOffset, NULL);
44
}
45
46
ZwClose(FileHandle);
47
}
Copied!
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
1
//Verify DOS Header
2
PIMAGE_DOS_HEADER pdh = (PIMAGE_DOS_HEADER)FileData;
3
//Verify PE Header
4
PIMAGE_NT_HEADERS pnth = (PIMAGE_NT_HEADERS)(FileData + pdh->e_lfanew);
5
//Verify Export Directory
6
PIMAGE_DATA_DIRECTORY pdd = NULL;
7
if(pnth->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC)
8
pdd = ((PIMAGE_NT_HEADERS64)pnth)->OptionalHeader.DataDirectory;
9
else
10
pdd = ((PIMAGE_NT_HEADERS32)pnth)->OptionalHeader.DataDirectory;
11
ULONG ExportDirRva = pdd[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
12
ULONG ExportDirSize = pdd[IMAGE_DIRECTORY_ENTRY_EXPORT].Size;
13
ULONG ExportDirOffset = RvaToOffset(pnth, ExportDirRva, FileSize);
14
15
//Read Export Directory
16
PIMAGE_EXPORT_DIRECTORY ExportDir = (PIMAGE_EXPORT_DIRECTORY)(FileData + ExportDirOffset);
17
ULONG NumberOfNames = ExportDir->NumberOfNames;
18
ULONG AddressOfFunctionsOffset = RvaToOffset(pnth, ExportDir->AddressOfFunctions, FileSize);
19
ULONG AddressOfNameOrdinalsOffset = RvaToOffset(pnth, ExportDir->AddressOfNameOrdinals, FileSize);
20
ULONG AddressOfNamesOffset = RvaToOffset(pnth, ExportDir->AddressOfNames, FileSize);
21
22
ULONG* AddressOfFunctions = (ULONG*)(FileData + AddressOfFunctionsOffset);
23
USHORT* AddressOfNameOrdinals = (USHORT*)(FileData + AddressOfNameOrdinalsOffset);
24
ULONG* AddressOfNames = (ULONG*)(FileData + AddressOfNamesOffset);
Copied!
and get the file offset of the function address from its name
1
// Find RVA of exported function whose name is ExportName
2
ULONG ExportOffset = PE_ERROR_VALUE;
3
for(ULONG i = 0; i < NumberOfNames; i++)
4
{
5
ULONG CurrentNameOffset = RvaToOffset(pnth, AddressOfNames[i], FileSize);
6
if(CurrentNameOffset == PE_ERROR_VALUE)
7
continue;
8
const char* CurrentName = (const char*)(FileData + CurrentNameOffset);
9
ULONG CurrentFunctionRva = AddressOfFunctions[AddressOfNameOrdinals[i]];
10
if(CurrentFunctionRva >= ExportDirRva && CurrentFunctionRva < ExportDirRva + ExportDirSize)
11
continue; //we ignore forwarded exports
12
//compare the export name to the requested export
13
if(!strcmp(CurrentName, ExportName))
14
{
15
// Convert function RVA to offset
16
ExportOffset = RvaToOffset(pnth, CurrentFunctionRva, FileSize);
17
break;
18
}
19
}
Copied!
Then get the real address of exported function in memory ( in ntdll.dll )
1
ExportFunctionAddress = (PVOID)((ULONG_PTR)FileData + ExportOffset)
Copied!
finally search the whole function for pattern mov eax, xx, retrieve the SSDT index
1
for(int i = 0; i < 32 && ExportOffset + i < FileSize; i++)
2
{
3
if(ExportData[i] == 0xC2 || ExportData[i] == 0xC3) //RET
4
break;
5
if(ExportData[i] == 0xB8) //mov eax,XX -- b8 FFFFFFFF
6
{
7
SsdtOffset = *(int*)(ExportData + i + 1);
8
break;
9
}
10
}
Copied!

Combine index with SSDT(Shadow)

x64
1
realNtFunction = (PVOID)(ntTable[SsdtOffset] >> 4 + ntTable);
Copied!
x86
1
realNtFunction = (PVOID)(ntTable[SsdtOffset]);
Copied!
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)
1
#pragma pack(push,1) // This is very important !!!!
2
struct HOOKOPCODES
3
{
4
#ifdef _WIN64
5
unsigned short int mov;
6
#else
7
unsigned char mov;
8
#endif
9
ULONG_PTR addr;
10
unsigned char push;
11
unsigned char ret;
12
};
13
#pragma pack(pop)
14
15
typedef struct HOOKSTRUCT
16
{
17
ULONG_PTR addr;
18
HOOKOPCODES hook;
19
unsigned char orig[sizeof(HOOKOPCODES)];
20
//SSDT extension
21
int SSDTindex;
22
LONG SSDTold;
23
LONG SSDTnew;
24
ULONG_PTR SSDTaddress;
25
}HOOK, *PHOOK;
Copied!

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
1
NTSTATUS SuperCopyMemory(
2
IN VOID UNALIGNED* Destination,
3
IN CONST VOID UNALIGNED* Source,
4
IN ULONG Length)
5
{
6
//Change memory properties.
7
PMDL g_pmdl = IoAllocateMdl(Destination, Length, 0, 0, NULL);
8
if(!g_pmdl)
9
return STATUS_UNSUCCESSFUL;
10
MmBuildMdlForNonPagedPool(g_pmdl);
11
unsigned int* Mapped = (unsigned int*)MmMapLockedPages(g_pmdl, KernelMode);
12
if(!Mapped)
13
{
14
IoFreeMdl(g_pmdl);
15
return STATUS_UNSUCCESSFUL;
16
}
17
KIRQL kirql = KeRaiseIrqlToDpcLevel();
18
RtlCopyMemory(Mapped, Source, Length);
19
KeLowerIrql(kirql);
20
//Restore memory properties.
21
MmUnmapLockedPages((PVOID)Mapped, g_pmdl);
22
IoFreeMdl(g_pmdl);
23
return STATUS_SUCCESS;
24
}
Copied!
Also, we can modify the "WP" flags of the cr0register to enable or disable kernel memory write protection
To disable WP:
1
KIRQL DisableWP(){
2
KIRQL irql=KeRaiseIrqlToDpcLevel();
3
ULONG_PTR cr0=__readcr0();
4
#ifdef _AMD64_
5
cr0 &= 0xfffffffffffeffff;
6
#else
7
cr0 &= 0xfffeffff;
8
#endif
9
__writecr0(cr0);
10
_disable(); // Disable interrupts
11
return irql;
12
}
Copied!
there we can modify the kernel memory without access violation.
🧐
To enable WP after writing:
1
void EnableWP(KIRQL irql){
2
ULONG_PTR cr0=__readcr0();
3
cr0 |= 0x10000;
4
_enable(); // Enable interrupts
5
__writecr0(cr0);
6
KeLowerIrql(irql);
7
}
Copied!

x86 Hook

Just replace the SSDT index with your own function
1
PVOID realNtFunction = ntTable[FunctionIndex];
2
3
hHook = (HOOK)RtlAllocateMemory(true, sizeof(HOOK));
4
hHook.SSDTold = realNtFunction;
5
hHook.SSDTnew = YourFunction;
6
hHook.SSDTindex = FunctionIndex;
7
hHook.SSDTaddress = realNtFunction;
8
9
SuperCopyMemory(&realNtFunction, YourFunction);
Copied!

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
1
movabs rax, xxxxxxxxxxxxxxxx ; 48B8 XXXXXXXXXXXXXXXX (move our real function to rax)
2
push rax ; 50
3
ret ; c3 (return to rax, which is our function)
Copied!
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
1
ULONG dwRva = (ULONG)((unsigned char*)realNtFunction - (unsigned char*)ntBase);
2
IMAGE_DOS_HEADER* pdh = (IMAGE_DOS_HEADER*)ntBase;
3
if(pdh->e_magic != IMAGE_DOS_SIGNATURE)
4
return 0;
5
IMAGE_NT_HEADERS* pnth = (IMAGE_NT_HEADERS*)((unsigned char*)ntBase + pdh->e_lfanew);
6
if(pnth->Signature != IMAGE_NT_SIGNATURE)
7
return 0;
8
IMAGE_SECTION_HEADER* psh = IMAGE_FIRST_SECTION(pnth);
Copied!
find out in which section the real function is
1
static ULONG RvaToSection(IMAGE_NT_HEADERS* pNtHdr, ULONG dwRVA)
2
{
3
USHORT wSections;
4
PIMAGE_SECTION_HEADER pSectionHdr;
5
pSectionHdr = IMAGE_FIRST_SECTION(pNtHdr);
6
wSections = pNtHdr->FileHeader.NumberOfSections;
7
for(int i = 0; i < wSections; i++)
8
{
9
if(pSectionHdr[i].VirtualAddress <= dwRVA &&
10
(pSectionHdr[i].VirtualAddress + pSectionHdr[i].Misc.VirtualSize) > dwRVA)
11
return i;
12
}
13
return (ULONG) - 1;
14
}
15
16
int section = RvaToSection(pnth, dwRva);
17
if(section == -1)
18
return 0;
19
20
CodeSize = psh[section].SizeOfRawData;
21
CodeStart = (PVOID)((ULONG_PTR)ntBase + psh[section].VirtualAddress);
Copied!
then try to find a at least 12 bytes large nop block in the code page
1
unsigned char* Code = (unsigned char*)CodeStart;
2
unsigned int CaveSize = sizeof(HOOKOPCODES);
3
for(unsigned int i = 0, j = 0; i < CodeSize; i++)
4
{
5
if(Code[i] == 0x90 || Code[i] == 0xCC) //NOP or INT3
6
j++;
7
else
8
j = 0;
9
if(j == CaveSize)
10
ShellcodeCave = (PVOID)((ULONG_PTR)CodeStart + i - CaveSize + 1);
11
}
Copied!
Next we need to write the shellcode to the cave address we just got, and insert our own function address
initialize the hook struct
1
//allocate structure
2
PHOOK hook = (PHOOK)RtlAllocateMemory(true, sizeof(HOOK));
3
//set hooking address
4
hook->addr = ShellcodeCave; // Store the cave address
5
//set hooking opcode
6
#ifdef _WIN64
7
hook->hook.mov = 0xB848;
8
#else
9
hook->hook.mov = 0xB8;
10
#endif
11
hook->hook.addr = (ULONG_PTR)YourFunction; // Insert our own function
12
hook->hook.push = 0x50;
13
hook->hook.ret = 0xc3;
14
//set original data
15
RtlCopyMemory(&hook->orig, (const void*)addr, sizeof(HOOKOPCODES));
Copied!
write to the cave address
1
SuperCopyMemory((void*)ShellcodeCave, &hook->hook, sizeof(HOOK));
Copied!
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 !!
1
oldOffset = ntTable[FunctionIndex];
2
newOffset = (LONG)((ULONG_PTR)ShellcodeCave - ntTable);
3
newOffset = (newOffset << 4) | oldOffset & 0xF;
Copied!
update SSDT
1
hook.SSDTold = oldOffset;
2
hook.SSDTnew = newOffset;
3
hook.SSDTIndex = FunctionIndex;
4
hook.SSDTaddress = realNtFunction;
5
6
SuperCopyMemory(&ntTable[FunctionIndex], newOffset);
Copied!
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.
1
// Get the process id of the "winlogon.exe" process
2
GetProcessIdByName(ProcessId, "winlogon.exe");
3
PsLookupProcessByProcessId( ProcessId, &Process );
4
APC_STATE oldApc;
5
KeStackAttachProcess(Process, &oldApc);
6
7
// Find Shadows SSDT and do dirty things
8
9
KeUnstackDetachProcess(&oldApc);
Copied!