Extracting the SSDT directly from ntoskrnl.exe

Extracting the SSDT directly from ntoskrnl.exe

Guide and sample code for extracting SSDT/KiServiceTable from the Windows kernel binary without a debugger

For any developers wanting to make their apps hard to hook and analyze, making direct syscalls to the kernel is a useful approach to look into. There are countless resources for making this happen, from j00ru's work building a full table, to the library SysWhispers2 for taking advantage of this in your own application. But how do these syscalls get extracted?

Debugging the kernel

If we start a windows install in a virtual machine we can attach WinDbg and get access to a number of things, including a completely built System Service Descriptor Table (SSDT) at KiServiceTable, and a ton of extra clues from the symbols that Microsoft provides us. We can construct our own SSDT for a specific kernel version using the following steps:

  1. Iterate over each address in the SSDT and convert to a real address
  2. Look up each address in the symbol table
  3. Add the combination of index in the SSDT and name in the symbol table to map syscall numbers to function names

This works, but it requires us to debug or drop a driver into a running windows instance. This isn't super hard, but it is a little more than "download this script and run it" level of easy. The other thing is that we don't get to find out what happens under the covers, and this is something I find interesting.

So... I started by firing up WinDbg and seeing what KiServiceTable pointed to, and came up with this:

kd> dd nt!KiServiceTable L1D0
fffff806`58808450  fc721b04 fc7c8800 0256ef02 04538500
fffff806`58808460  02a07c00 fdb89c00 02739905 022ce306
fffff806`58808470  0272c005 022c8d01 027e8000 01a95200
fffff806`58808480  01a85300 02a70300 027dc800 029f9800
fffff806`58808490  02010a01 02742601 0297f300 01f69202
fffff806`588084a0  0282ec00 02435800 02786d01 0278d302
fffff806`588084b0  02937902 01dd9f01 01c2b101 025f4505
fffff806`588084c0  01e05e00 01938503 021e1700 044bcf00
fffff806`588084d0  022e2f00 02a35e01 023ab700 02953402
fffff806`588084e0  0283c800 027de901 0234d000 fd2d2001
fffff806`588084f0  027fee06 02240e07 01ac8a00 01a95401
fffff806`58808500  01e5de00 04d1b500 025ff805 0283ca01
fffff806`58808510  02843e00 022cd700 01f54c02 01f00202
fffff806`58808520  0299ee00 02483107 02a0a000 0233ab00
fffff806`58808530  04d19f01 0236c606 01e38701 02421e00
fffff806`58808540  01f3bd03 01f0bb00 022e3c00 01e38a01
fffff806`58808550  01cb5000 01ec6402 02849e02 fdb58c00
fffff806`58808560  0301f800 01e41b01 fc109900 04d90f00
fffff806`58808570  0286c201 02023701 027ee303 023bd800
fffff806`58808580  022d6b00 0488a005 0488a904 01cf7a00
fffff806`58808590  02946a01 02495201 023dc300 01e35100
fffff806`588085a0  044bdf02 01f00907 024f5701 044bf002
fffff806`588085b0  01cb5b00 0224170c 04cf2f00 01c2c201
fffff806`588085c0  01db3b00 0241b900 fcd99200 02186301
fffff806`588085d0  01fefe02 fcc9bd00 fd3cd603 fc800607
fffff806`588085e0  fefe7707 04a2630c 04a26e0d 02bd7b00
fffff806`588085f0  02af4100 04d55c00 04d55f00 0248d102
fffff806`58808600  02bb340c 048ecf00 048ee100 02069900
fffff806`58808610  022d9b00 0250ee00 04528300 02584200
fffff806`58808620  01e2b303 01b02a05 0260c400 01a87507
fffff806`58808630  01a8ac07 0249a800 01b16702 023a7800
fffff806`58808640  01abd100 01ace300 02442500 044d2300
fffff806`58808650  02434500 01ad0100 024a6f00 044c1000
fffff806`58808660  0292c000 01a8dc02 024a4f02 022b6701
fffff806`58808670  01aa2d02 044c3200 028d4b04 021ea900
fffff806`58808680  02ec1800 01cf8e00 fc3a2b04 fdbda400
fffff806`58808690  0241d400 041f9c00 fc8f5200 fc39f400
fffff806`588086a0  fd901a00 fd901c00 01888400 fd901e00
fffff806`588086b0  03eb4300 0251ae00 025fb400 02237400
fffff806`588086c0  025b5e00 03eb7100 023c8804 04dc0b00
fffff806`588086d0  04121900 02423400 02423201 045cb105
fffff806`588086e0  fd902004 02bb3400 03060200 02b93100
fffff806`588086f0  01cedb00 02bb3300 018ade04 033bbd00
fffff806`58808700  01c43605 0182cb04 02b33200 024c600a
fffff806`58808710  03300800 048f3a00 02c31801 01c58600
fffff806`58808720  04889704 04dc1d05 04dc2b06 02599000
fffff806`58808730  fd902203 0450a605 0286a401 0249fd00
fffff806`58808740  01bd2207 020d1e00 021f2f01 04a3bb09
fffff806`58808750  01e9b30d fd902406 fd902602 01ef0207
fffff806`58808760  0236db00 03057701 01a52003 021ef906
fffff806`58808770  04123800 04125800 0233cf00 04d56200
fffff806`58808780  04d57b00 02f64f00 018d2800 02ee6500
fffff806`58808790  02ee4200 0193e600 0343a100 01a4a500
fffff806`588087a0  02ee1600 04cfa900 ff19bd00 02ed1200
fffff806`588087b0  04d59400 04d5f900 04d64400 fd902801
fffff806`588087c0  02619100 04a4ef01 02283a02 02bb340a
fffff806`588087d0  023bdb01 0346c200 025b5e00 02525a00
fffff806`588087e0  fc1b4300 02331400 045d9500 04530b00
fffff806`588087f0  03eb8d00 fd902a00 02451502 01a59802
fffff806`58808800  0257ef00 048a9200 048a9700 04719c00
fffff806`58808810  02b14200 02ffef01 0490c302 02579c01
fffff806`58808820  fd902c03 fcb37703 02234100 02305800
fffff806`58808830  045cf801 01e59d00 03043100 02c55600
fffff806`58808840  02c94100 02622a00 0348a300 0263c000
fffff806`58808850  045d2505 02f6b600 02f69b00 0192d104
fffff806`58808860  01c02506 024f7800 022b1500 fca88500
fffff806`58808870  02bdf000 02592400 045a6c00 01a3e901
fffff806`58808880  02ede702 04535600 025d1205 04d66f00
fffff806`58808890  04d67200 024cbd05 024cc306 02028306
fffff806`588088a0  01ffeb08 03029f04 fd902e01 02bb3400
fffff806`588088b0  041f7100 048b7f00 02987400 03eb9600
fffff806`588088c0  018ac901 04dc6e00 01cb4500 02c0a708
fffff806`588088d0  03465300 0254e400 02843c00 03eb9800
fffff806`588088e0  fd903001 01cb3a00 02c27c00 022d1900
fffff806`588088f0  02064800 04d19400 fd903201 fd903402
fffff806`58808900  020e7900 fd903600 fd903800 fd903a00
fffff806`58808910  fd903c00 01ec4600 03025502 0222ac01
fffff806`58808920  fd903e00 fd904000 01993900 04dc3200
fffff806`58808930  04d67500 04d69c00 fc7e9300 01c06106
fffff806`58808940  02a97d03 04d6cc00 023f3b05 01efc600
fffff806`58808950  0233f001 041faa01 fd904201 01d48701
fffff806`58808960  044bd201 fd904401 fd904601 fd904801
fffff806`58808970  ff1f1401 0257f900 02be2900 041f8301
fffff806`58808980  022bbd01 0194b002 04dce701 03ebad00
fffff806`58808990  03ebd100 02bb2a00 0420cf05 01dcef02
fffff806`588089a0  022ec501 049d1702 04d8f601 028d2900
fffff806`588089b0  04d6ff00 025d2b01 02409f02 025d1a00
fffff806`588089c0  01a68a02 02483f01 01e3ff02 fdb5b800
fffff806`588089d0  04d8c202 fd904a00 fd904c00 fd904e00
fffff806`588089e0  fd905000 fd90a201 025d2200 02564000
fffff806`588089f0  fcb4c700 022c7102 04127700 03ec0600
fffff806`58808a00  fd90a400 03ec5200 ff2c6800 044be500
fffff806`58808a10  02520800 0238a400 01b6a200 03ec8a00
fffff806`58808a20  048ee900 feea0a00 fd905200 fd905400
fffff806`58808a30  01887400 fd905600 fd90aa00 03ecb400
fffff806`58808a40  03ecb600 03ece000 023c8d05 03474300
fffff806`58808a50  04d73000 04d75100 049d4801 049d4b02
fffff806`58808a60  048db900 032b5b00 0346ec00 03018000
fffff806`58808a70  0301a100 04d77200 04206900 02bb3400
fffff806`58808a80  02bb3400 fc914100 04128c01 fd905800
fffff806`58808a90  01d29b00 028ce900 fd905a00 04636d00
fffff806`58808aa0  01e68800 fd905c00 fd90a600 01bb2402
fffff806`58808ab0  fc3b0d00 02be3500 021f5b01 027dd602
fffff806`58808ac0  fd8fed02 02bb3400 02bb3400 04214200
fffff806`58808ad0  0223a000 04d79300 04d7c301 019a4b00
fffff806`58808ae0  016ea700 04cf3700 021d5400 fd2f2900
fffff806`58808af0  fcc91c00 019a2300 033db900 02e71a01
fffff806`58808b00  0246a800 04cfc400 fc8cdb00 fed2e100
fffff806`58808b10  fd90a800 04dc3900 04dc5f00 01a64600
fffff806`58808b20  048ef100 0254b700 04dc7e02 045d8a00
fffff806`58808b30  02532500 02498900 03ed0300 fd905e00
fffff806`58808b40  02a82f02 04d7ed00 0447e000 02ee7000
fffff806`58808b50  02e9d500 03060800 01894800 0230df01
fffff806`58808b60  fcb7c400 01ac9100 01a08300 01a60903
fffff806`58808b70  02bb3400 02a95a00 0412a400 02560c00
fffff806`58808b80  fd1e9201 02bb3400 02bb3400 000001cf

by the way if you want to follow along at home I'm working with Windows 10 x64 kernel 1809 (build 10.0.17763.379)

A couple of things stand out about this collection:

  • It's a bunch of 32-bit numbers that are roughly between +0x03000000 and -0x03000000
  • It finishes with the size of the collection (0x1CF), something we can search for when confirming whether we've found the SSDT

Once we have this address we can search for it in the disassembly and see what references it directly. I found two references, one of which looks like this:

// sub_16CCEC
KeCompactServiceTable(&KiServiceTable, &KiArgumentTable, (unsigned int)*(&KiServiceTable + 0x1CF), 0i64, 0x140000000i64);

We can look inside this and see how this gets prepared:

pCurrentEntry = pKiServiceTable;
if ( numEntries )
  numEntriesRemaining = numEntries;
    *pCurrentEntry = ((imageBase + *pCurrentEntry - (unsigned int)pKiServiceTable ) << 4) | (*pNumArguments >> 2);
  while ( numEntriesRemaining );

Let's break down that big pCurrentEntry line:

// the whole line
((imageBase + *pCurrentEntry - (unsigned int)pKiServiceTable ) << 4) | (*pNumArguments >> 2);
// first half
((imageBase + *pCurrentEntry - (unsigned int)pKiServiceTable ) << 4)
// let's step in, it's this value shifted left by 4:
imageBase + *pCurrentEntry - (unsigned int)pKiServiceTable
// if the original values are just offsets from the base then adding imageBase just relocates it (pKiServiceTable is already relocated)
relocatedFunctionPointer - pKiServiceTable
// this gives us an offset to the function from KiServiceTable...

So this explains what our entries are in the table:

  • Bits 0-4 are the number of arguments that each function takes on the stack (for some reason stored in multiples of 4?)
  • Bits 5-32 are an offset from KiServiceTable

We can test this by running it in reverse. Let's look for a function like NtWaitForSingleObject (present in every windows 10 kernel), at offset 0x04. This value is 0x02a07c00, and we break it down as follows:

  • Number of arguments is 02a07c00 ^ 0x0F = 0
  • Offset from KiServiceTable is 02a07c00 >> 4 = 02a07c0
  • KiServiceTable is at 1403FE450 so we should find this function at 1403FE450 + 02a07c0 = 14069EC10

And this is correct: NtWaitForSingleObject is at 14069EC10 in the binary (or 69EC10 + baseOffset).

We need to do this arithmetic when we lift the SSDT from memory, but it looks like the base source of data is just offsets from the base image. Sure enough, if we look at the binary on disk, this is exactly what we see:


This is all we need to lift the SSDT! I've put together some sample code for you that does the following:

  1. Looks up the export table to find the RVA of a function we know we'll have a syscall for (NtWaitForSingleObject)
  2. Scans through the binary to find mentions of this RVA
  3. Does some checks to make sure we're actually in the SSDT (scans through until we find a small number, then checks we've got something like the NumArguments table immediately following)
  4. Renders this table with checks against the export table to extract names where we can.

Here's the result:


You'll note there are quite a few gaps, there are reasons for this. Apart from the fact that system calls are part of Windows internals, and any undocumented feature like these is likely to change in the future, there are also a lot of system calls that have similar names and use slightly different endpoints. For example, a call to NtSetContextThread in usermode will result in a system call, but it hits a different entrypoint to the PsSetContextThread function that is exported in kernel mode. They are both facades to the same internal function, but it means we're limited to only rendering names for functions that use the same entrypoint for both usermode and kernel mode invocations.

In any case, this is a starting point, and the next step is to extract kernel symbols and use them to render a more complete version of the SSDT.

Happy hacking.