CPUPrivilege

- 4 mins read

Prologue

It’s a fundamental concept and design rule that all the CPU resources should not be accessible/programmable by any random process or function in a machine. This is crucial for data protection and avoiding functionality impairment and improving fault tolerance of a CPU. This protection is enforced by a mechanism called as protection ring with different level of access provided to different rings. More popularly though, the protection rings are referred as CPU privilege levels.

At software level, there can be multiple privilege levels which can be created and enforced to be used with an application or process using the mechanisms like Discretionary Access Control (DAC) in standard Linux or Mandatory Access Control (MAC) in Security Enhanced Linux (SELinux). However, at the processor (our discussion is around x86_64 processor) level, there are only two levels (also called modes): Supervisor (Kernel) mode (ring 0 in x86 documentation); and User mode(ring 3 in x86 documentation). The OS code runs in ring 0 in kernel mode has the highest level of privilege while all the applications and processes run in ring 3, user mode.

NOTE: Originally, x86 processors have four rings (ring 0 through ring 3), as per the Intel® 64 and IA-32 Architectures Software Developer Manuals. But most systems use only two of them. Ring 1 and 2 were intended to be used by device drivers, but modern Operating Systems run them in ring 0. Ring 1 and 2 are still used, for example type 2 hypervisors like VirtualBox and VMware Fusion put guest OS in ring 1.

We will be testing privilege level of a code in C language which includes inline assembly language to see what registers it can read and write.

Inline Assembly language in C

Inline assembly language can be embedded in a C code using the asm keyword. GNU assembler uses AT&T Assembly syntax which can be referred quickly here.

Few important sytatctial notes that will be used in this post:

  • Register names prefixed by a ‘%’ sign, eg. %al,%bx, %ds, %cr0 etc. mov %ax, %bx > mov instruction moves the value from the 16-bit register AX to 16-bit register BX.

  • Literal values prefixed by a ‘$’ sign. mov $100, %bx > moves the value 100 into the register AX.

  • mov $A, %al > moves the numerical value of the ascii A into the AL register.

  • The mov instruction can be suffixed with (b, w, l, or q) indicating how many bytes are being copied (1, 2, 4, or 8 respectively).

Now, it’s time to get our hands dirty with an actual code in C. We are going to start with an exteremely basic assembly embedded in C to copy a variable into another, increment, and print:

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int src = 9;
    int dst;

    asm ("mov %0, %1\n\t"
        "add $1, %1"
        : "=r" (dst)
        : "r" (src));

    printf("%d\n", dst);
    exit(0);
}

It will print “10” in the output.

Let’s try to access some registers on the processor.n

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

typedef unsigned long u64;

static u64 getRcx(void)
{
    /** Note: [RAX] register in x86 processors contains a function return
     value, so we can query a register's value by moving its value into RAX.
    * */
    __asm__ __volatile__(
            "push %rcx\n\t" 
            "movq $5, %rcx\n\t" 
            "movq %rcx, %rax"); 
    __asm__ __volatile__("pop %rcx");
}

int main(void)
{
    printf("Inline assembly with register value:\n [RCX] = 0x%lx\n",  
                getRcx());
    exit(0);
}

It prints 0x5, as the expected output. So, the C program execution can read the values of RCS as well as RAX (well, program returned value is stored in RAX so it should be accessible).

We will now try to play with control registers. Contorl registers perform fundamental operations like interrput control and addressing mode et al. A detailed reading on specific functions can be found here. Here, we are going to try reading the CR0 control register. CR0 register has control flags which determines the basic processor opearations; toggling the bits enables/disables paging, cache and protected mode, among others.

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

typedef unsigned long u64;

static u64 getRcx(void)
{
    __asm__ __volatile__("movq %cr0, %rax");
}

int main(void)
{
    printf("Inline assembly with register value:\n [RCX] = 0x%lx\n",  
                getRcx());
    exit(0);
}

Out of the above code execution:

Segmentation fault (core dumped)

Here, the application code - which is essentially at application level (user mode) — tried to access a control register. As we discussed earlier, control registers control fundamental functionalities of the processor - operating modes and states of the processor. A nasty application could easily impair the processor functionality. So, none of these registers can be accessed only in kernel mode of operation, which is OS. It now makes sense why need different privilege levels.