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:
- Iterate over each address in the SSDT and convert to a real address
- Look up each address in the symbol table
- 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;
do
{
*pCurrentEntry = ((imageBase + *pCurrentEntry - (unsigned int)pKiServiceTable ) << 4) | (*pNumArguments >> 2);
*pNumArguments++;
pCurrentEntry++;
numEntriesRemaining--;
}
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:
- Looks up the export table to find the RVA of a function we know we'll have a syscall for (NtWaitForSingleObject)
- Scans through the binary to find mentions of this RVA
- 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)
- 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.