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

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;
}

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);

Last updated