Click here to Skip to main content
Click here to Skip to main content

Entering Ring 0 using minimalistic approach

By , 21 Jun 2008
 

What this article is not

This article is meant for people who need easer way of Ring 0 (developing debugging profiling, performance monitoring tools, etc.) and therefore already know what Ring 0 is. Considering how many great tutorials about Protected Mode are already out there. Like Intel 80386 Reference Programmer's Manual or Protected mode programming tutorial it would be like bringing wood to forest. But greatest source are of course Intel and Amd cpu manufacturer manuals themselves.

Introduction

I recently started development of program called SysMon. It is utility simmilar to RegMon or FileMon but it will monitor systemcalls instead. It requires reading some registers (MSRs etc) that are unfortunately accessible only in mysterious ring 0 land. yet thanks to great idea sharing sites like CodeProject I found interesting way how tho get this data without all that inhuman kernel driver debugging galore just to read one register. Plus printf is printf ;)

Background

I was inspired by many great articles for example here on CodeProject those from old Kernel Adventurer Anton Bassov. Unfortunately many of those were relaying on some offsets which as we know change all the time. Since that code was not working on my version of windows I started windows version independent offset free approach plus I am minimalist so I decimated and decimated and this is the last extract of that mysterious kernel manna. I didn't tested it on vista.

The code

If you remember Msdos days you will some day realize that Windows itself is just another oversized protected mode Application. If you look at any protected mode manual you will find that Intel did build 3 ways of switching between ring levels. Interrupts syscalls or Call Gates ( don't bother, he never picks up )... Anyway Call Gate is exactly what this code does since it's simplest. We read Global Descriptor Table and write back address of our function on first free spot + desired ring level. So when we call this function it begins running at desired ring 0 but returns to previous ring 3 level. As an example I read cr0 register and return some text. So Enjoy it. And I am open to any further ideas how to make this code even simpler.

Word of Advice

Use Windows XP images and VirtualPC from here or similar software to test any changes. As is true with any Device Driver Development. This is Ring 0. Any slightest mistake usually means immediate Reset or Frozen computer.

VirtualPc 2007 have Bug thou. it don't have implemented sgdt in ring 3 so you need to replace this instruction with fixed value while testing.

Following values are valid For XP SP2:

// __asm sgdt gdtr
gdtr.base = 0x8003f000;
gdtr.limit = 0x03ff;

#include    <windows.h>
#include    <stdio.h>

#pragma pack(1)

struct  CALL_GATE { WORD addrlo; WORD seg; BYTE arg:5; BYTE u:3; BYTE typ:5; BYTE dpl:2; BYTE pres:1; WORD addrhi; } ;

#define Virtual(a,b,c,d) { struct VIRTUAL{void* A;void* B;DWORD C;}; VIRTUAL v={(void*)(a),b,c}; hr=NtSystemDebugControl(d,&v,sizeof(v),0,0,0); }


void Ring0( DWORD cs, char*& text ) {
    text = "Hello World from Ring 0 \n";        
    __asm mov eax,cr0
    __asm leave
    __asm retf 4
}

int main() {

    LONG (NTAPI *NtSystemDebugControl)     (int,void*,DWORD,void*,DWORD,DWORD*);
    *(DWORD*)   &NtSystemDebugControl    =(DWORD)GetProcAddress(LoadLibrary("ntdll"),"NtSystemDebugControl");

    TOKEN_PRIVILEGES pv={1},po; pv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED ; HANDLE t; int hr; DWORD no;
    
    // This will enable NtSystemDebugControl usage
    hr = LookupPrivilegeValue(  0, SE_DEBUG_NAME, &pv.Privileges[0].Luid );
    hr = OpenProcessToken(      GetCurrentProcess(), TOKEN_ALL_ACCESS,&t);
    hr = AdjustTokenPrivileges( t,0,&pv,sizeof(po),&po, &no);
    
    // This ensures that on multi cpu/core systems we patch the right GDT for right cpu
    hr = SetThreadAffinityMask (GetCurrentThread(),1); Sleep(100);

    // We read GDT table
    LDT_ENTRY gdt[1000]={0}; struct {WORD limit;DWORD base;} gdtr; __asm sgdt gdtr
    
    // Find free spot
    Virtual(gdtr.base, &gdt,gdtr.limit,8);    for(int gate=1;gate<100;gate++)    { if(!gdt[gate].HighWord.Bits.Pres) break; }    
    
    // Construct Call Gate pointing to Ring0 proc and write it there
    DWORD addr=(DWORD)Ring0; CALL_GATE g={addr&0xffff,8,1,0,12,3,1,addr>>16}; Virtual(gdtr.base+gate*8, &g,8,9); 
    
    // Quite ugly way to do far call 
    WORD farcall[3]={0,0,(gate<<3)}; char* param=0,**p=&param; long result=0; 

    // Switch from Ring 3 to Ring 0 is just normal call ;)
    __asm push p
    __asm call fword ptr [farcall]
    __asm mov  result, eax

    // Cleanup Call Gate from GDT
    __int64 c=0; Virtual(gdtr.base+gate*8, &c,8,9); 

    printf("\n %s\n  CR0 = %X ",param,result); getchar();    

    return 0;
}
   

Locations of visitors

Locations of visitors to this page

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Ladislav Nevery
Software Developer (Senior)
Slovakia Slovakia
Member
Past Projects:
[Siemens.sk]Mobile network software: HLR-Inovation for telering.at (Corba)
Medical software: CorRea module for CT scanner
[cauldron.sk]Computer Games:XboxLive/net code for Conan, Knights of the temple II, GeneTroopers, CivilWar, Soldier of fortune II
[www.elveon.com]Computer Games:XboxLive/net code for Elveon game based on Unreal Engine 3
ESET Reasearch.
Looking for job

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralMy vote of 5membergndnet5 Jan '11 - 22:36 
thanks Ladislav
GeneralGreat ArticlememberShaneMcDonald11 Nov '10 - 13:05 
Small changes to work on VS2008 Windows XP included below. No luck with Windows 7. Overall Great Article.
 
#include    <windows.h>
#include    <stdio.h>
 
#pragma pack(1)
 
struct  CALL_GATE { WORD addrlo; WORD seg; BYTE arg:5; BYTE u:3; BYTE typ:5; BYTE dpl:2; BYTE pres:1; WORD addrhi; } ;
 
#define Virtual(a,b,c,d) { struct VIRTUAL{void* A;void* B;DWORD C;}; VIRTUAL v={(void*)(a),b,c}; hr=NtSystemDebugControl(d,&v,sizeof(v),0,0,0); }
 

void Ring0( DWORD cs, char*& text ) {
    text = "Hello World from Ring 0 \n";        
    __asm mov eax,cr0
    __asm leave
    __asm retf 4
}
 
int main() {
 
    LONG (NTAPI *NtSystemDebugControl)     (int,void*,DWORD,void*,DWORD,DWORD*);
    *(DWORD*)   &NtSystemDebugControl    =(DWORD)GetProcAddress(LoadLibrary(TEXT("ntdll")),"NtSystemDebugControl");
 
    TOKEN_PRIVILEGES pv={1},po; pv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED ; HANDLE t; int hr; DWORD no;
    
    // This will enable NtSystemDebugControl usage
    hr = LookupPrivilegeValue(  0, SE_DEBUG_NAME, &pv.Privileges[0].Luid );
    hr = OpenProcessToken(      GetCurrentProcess(), TOKEN_ALL_ACCESS,&t);
    hr = AdjustTokenPrivileges( t,0,&pv,sizeof(po),&po, &no);
    
    // This ensures that on multi cpu/core systems we patch the right GDT for right cpu
    hr = SetThreadAffinityMask (GetCurrentThread(),1); Sleep(100);
 
    // We read GDT table
    LDT_ENTRY gdt[1000]={0}; struct {WORD limit;DWORD base;} gdtr; __asm sgdt gdtr
    
    // Find free spot
    int gate;
    Virtual(gdtr.base, &gdt,gdtr.limit,8);    for(gate=1;gate<100;gate++)    { if(!gdt[gate].HighWord.Bits.Pres) break; }    
    
    // Construct Call Gate pointing to Ring0 proc and write it there
    DWORD addr=(DWORD)Ring0; CALL_GATE g={addr&0xffff,8,1,0,12,3,1,addr>>16}; Virtual(gdtr.base+gate*8, &g,8,9); 
    
    // Quite ugly way to do far call 
    WORD farcall[3]={0,0,(gate<<3)}; char* param=0,**p=&param; long result=0; 
 
    // Switch from Ring 3 to Ring 0 is just normal call ;)
    __asm push p
    __asm call fword ptr [farcall]
    __asm mov  result, eax
 
    // Cleanup Call Gate from GDT
    __int64 c=0; Virtual(gdtr.base+gate*8, &c,8,9); 
 
    printf("\n %s\n  CR0 = %X ",param,result); getchar();    
 
    return 0;
}
   

QuestionHow to go into ring0memberredvc28 May '10 - 16:10 
First, I'm a newer for the ring0.
I have two question.
1. How to into the ring0? where's the code?
2. How to work with function Ring0Call()? Do it execute a function, then go into ring0?
GeneralAwesome!memberBartosz Wojcik11 Jan '10 - 16:02 
I haven't seen anything similar since z0mbie's ring3->ring0 tricks hehe Thumbs Up | :thumbsup:
 
PS. Do you have your GPU related source codes available anywhere?
 

GeneralMore argumentsmemberunix_soul31 Jul '09 - 1:31 
What about if user want to use function Ring0 with 2 or 3 arguments like
 
void Ring0(DWORD cs, char *text, long *, char *text);
QuestionHelp Please?memberTurion18 Jul '09 - 19:09 
Your code is great for entering Ring 0! I just have a problem actually using this ability to call kernel functions.
When I call a kernel function such as ObQueryNameString, it sometimes works correctly, but I typically get an access violation reading 0x00000000 when I run it the first time, and it may BSOD depending on what I'm querying. Here's my entire code; I'm using VC++ 2008 and linking only to NTDLL.dll (I'm delay-loading the kernel and not linking to any VC++ runtimes). I modified your code a bit, to make it more generic.
 
 
#pragma pack (push, 1)
struct CALL_GATE
{
	WORD addrlo;
	WORD seg;
	BYTE arg : 5;
	BYTE u : 3;
	BYTE typ : 5;
	BYTE dpl : 2;
	BYTE pres : 1;
	WORD addrhi;
};
struct SYSDBG_VIRTUAL
{
	void* Address;
	void* Buffer;
	ULONG Length;
};
#pragma pack (pop)
 
struct QUERY_OBJECT_NAME_INFO
{
	PVOID Object;
	PVOID Buffer;
	ULONG Length;
	ULONG* LengthNeeded;
	NTSTATUS Result;
};
 
NTSTATUS NTAPI ObOpenObjectByName(IN POBJECT_ATTRIBUTES ObjectAttributes, IN struct _OBJECT_TYPE* ObjectType, IN KPROCESSOR_MODE AccessMode, IN struct _ACCESS_STATE* PassedAccessState, IN ACCESS_MASK DesiredAccess, IN OUT PVOID ParseContext, OUT PHANDLE Handle)
{
	OPEN_OBJECT_BY_NAME_INFO info = {0};
	info.ObjectAttributes = ObjectAttributes;
	info.ObjectType = ObjectType;
	info.AccessMode = AccessMode;
	info.PassedAccessState = PassedAccessState;
	info.DesiredAccess = DesiredAccess;
	info.ParseContext = ParseContext;
	info.Handle = Handle;
	NTSTATUS result = SystemCall((KERNELPROC)ObOpenObjectByNameInternal, &info);
	if (NT_SUCCESS(result)) { result = info.Result; }
	return result;
}
 

void NTAPI ObQueryNameStringInternal(IN OUT QUERY_OBJECT_NAME_INFO* pInfo)
{ pInfo-&gt;Result = (*pObQueryNameString)(pInfo-&gt;Object, pLocalBuffer, pInfo-&gt;Length, &amp;lengthNeededLocal); }
 
//I don't know assembly to edit the call code, so I created this function so I don't use the first parameter
void Ring0Call(DWORD, SYSCALL_INFO* info)
{
	__try { (*info->Function)(info->Context); } __except (1) { }
	__asm
	{
		mov eax, cr0
			leave
			retf 4
	}
}
 
struct SYSCALL_INFO
{
	KERNELPROC Function;
	PVOID Context;
};
 
__inline NTSTATUS ReadVirtual(void* address, void* buffer, ULONG length) { SYSDBG_VIRTUAL v = { address, buffer, length }; return NtSystemDebugControl(8, &v, sizeof(v), 0, 0, 0); }
__inline NTSTATUS WriteVirtual(void* address, void* buffer, ULONG length) { SYSDBG_VIRTUAL v = { address, buffer, length }; return NtSystemDebugControl(9, &v, sizeof(v), 0, 0, 0); }
NTSTATUS SystemCall(KERNELPROC function, PVOID context)
{
	NTSTATUS status;
	THREAD_BASIC_INFORMATION threadInfo;
	ULONG retLength;
	status = NtQueryInformationThread(GetCurrentThread(), ThreadBasicInformation, &threadInfo, sizeof(threadInfo), &retLength);
	if (!NT_SUCCESS(status)) { return status; }
 
	LDT_ENTRY gdt[1000] = {0};
	short gate;
 
	SYSCALL_INFO info = { function, context };
	DWORD addr = (DWORD)Ring0Call;
	CALL_GATE callGate = { addr & 0xFFFF, 8, 1, 0, 12, 3, 1, addr >> 16 };
#pragma pack (push, 1)
	struct { WORD limit; DWORD base; } gdtr;
#pragma pack (pop)
 

 
	// This ensures that on multi cpu/core systems we patch the right GDT for right cpu
	KAFFINITY one = 1;
	status = NtSetInformationThread(GetCurrentThread(), ThreadAffinityMask, &one, sizeof(one));
	if (!NT_SUCCESS(status)) { return status; }
	__try
	{
		Sleep(100);
 
		int threadPriority = GetThreadPriority(GetCurrentThread());
		//Prevent interruptions between reading & writing the call gate info as much as possible
		if (threadPriority != THREAD_PRIORITY_ERROR_RETURN) { SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL); }
		__try
		{
			// We read GDT table
			__asm sgdt gdtr
				// Find free spot
				status = ReadVirtual((void*)gdtr.base, &gdt, gdtr.limit);
 
			if (!NT_SUCCESS(status)) { return status; }
 
			for (gate = 1; gate < 100; gate++)
			{ if (!gdt[gate].HighWord.Bits.Pres) break; }
 
			// Construct Call Gate pointing to Ring0 proc and write it there
			status = WriteVirtual((void*)(gdtr.base + gate * 8), &callGate, sizeof(callGate));
			if (!NT_SUCCESS(status)) { return status; }
		}
		__finally { SetThreadPriority(GetCurrentThread(), threadPriority); }
	}
	__finally { NtSetInformationThread(GetCurrentThread(), ThreadAffinityMask, &threadInfo.AffinityMask, sizeof(threadInfo.AffinityMask)); }
 
	__try
	{
		// Quite ugly way to do far call
		short farcall[3] = { 0, 0, gate << 3 };
 
		PVOID pInfo = &info;
		// Switch from Ring 3 to Ring 0 is just normal call ;)
		long result; 
		__asm
		{
			push pInfo
				call fword ptr [farcall]
			mov result, eax
		}
	}
	__finally
	{
		// Cleanup Call Gate from GDT
		__int64 c = 0;
		status = WriteVirtual((void*)(gdtr.base + gate * 8), &c, sizeof(c));
	}
 
	return status;
}
 
void mainCRTStartup()
{
	PVOID pSomeObject; //Just hard-code a pointer to an object whose address you know... use a program like ProcExp (from SysInternals) or something.
	hKernel = LoadLibrary(__TEXT("ntkrnlpa.exe"));
	pObQueryNameString = (ObQueryNameStringPointer)GetProcAddress(hKernel, "ObQueryNameString");
	char objectName[4096];
	ULONG needed;
	//I get "Unhandled exception at 0x00000000 in KernelHelper.exe: 0xC000001D: Illegal Instruction."
	NTSTATUS status = ObQueryNameString(pSomeObject, objectName, sizeof(objectName), &needed);
}
 
Just run this in a new project with the settings I mentioned and you'll see what I mean.
Generalproblem occured beacuse of "__asm call fword ptr [farcall]"memberwyybaba28 Mar '09 - 19:03 
hi:
i readed your document and then i try to test your code. i copy all your code to my vc++ project. i build a Test4.exe. but when i run the Test4.exe some problem occured. windows a display a dialog box and show me "Test4.exe has encountered a problem and needs to close. We are sorry for the inconvenience.". i debug this program and find the line
__asm call fword ptr [farcall]
cause this problem. it seems that windows found my bad appempt and terminated my program. i use windows XP sp2.please help me ,what's the problem of this?
GeneralVista/Windows Server 2003 SP1memberfrench_rt1525 Nov '08 - 5:46 
Congratulation ! Works great on my XP SP2 !
 
However, documentation seems say that this code don't work on Vista and Windows Server 2003 SP1... Microsoft don't want let us access phisical memory with NtSystemDebugControl and CreateFile/"\\Device\\PhysicalMemory".
 
Is it the case ?
Is there any workaround ?
GeneralRe: Vista/Windows Server 2003 SP1membermaddin5 Jul '11 - 1:23 
Will NEVER.
 
call gates work only til Windows XP 32 Bit. From Windows XP 64 Bit (Windows 2003 Server Kernel) Microsoft patched many Windows kernel security holes beside entering kernel mode through call gates.
 

Cheers
Maddin
Generalx64... [modified]memberMichael Chourdakis23 Jul '08 - 22:50 
The 32-bit version of course can't run under x64, but how about a x64 version of the article ?
It seems that we need a driver ....
 
modified on Thursday, July 24, 2008 12:25 PM

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web04 | 2.6.130523.1 | Last Updated 21 Jun 2008
Article Copyright 2008 by Ladislav Nevery
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid