Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Demo processor rings - assembly code that runs ring 0 instructions

I want to create a demo to explain processors rings and system calls to students. I was thinking in a presentation doing something like this:

  1. Write some assembly code that tries to execute some code that can only run in ring 0 (for example access disk directly)
  2. see the code failing, and re-write it to use system calls (for example, reading a file using system calls)
  3. if it is not that hard: run the initial code in ring 0

I can use Linux or Windows, whatever is easier

Any ideas on where i can find some code that help me? what protected instructions could I use?

Thanks!

like image 643
Carlos Garcia Avatar asked Dec 29 '25 14:12

Carlos Garcia


1 Answers

One attention-catching demonstration could be powering the computer off.

Unfortunately power management is a complex matter, it involves the ACPI specification that are rather abstract and long, further more they are not the first attempt.
ACPI is complex because different vendors require different actions to manage an aspect of the computer, anyway if you happen to have an Intel chipset (series 200 is recommended but other series should work too) we can skip most of the ACPI layer and use the datasheet instead.

You should check this program on the final hardware before using it in a class.


ACPI defines four global system state, G0-G3, where G3 is mechanical power off (i.e. the plug has been pulled, the battery removed) and G2 is soft-off.
Only G2 can be entered by the software, and this is done by entering the S5 sleep state.

The sleep state is controlled by the PCH (the Intel chipset) through an IO register (PM1a_CNT_BLK), this register is located in the ACPI block defined in the PCI configuration space of the device 31 function 2 (the PM controller).
One should read the base address of the block and then add four (4) to get the address of the register of interest.
I won't do this programmatically, instead the assembly program is expecting a symbol with that address.

To retrieve the address of the register PM1a_CNT_BLK one can use /proc/ioports as follow:

sudo cat /proc/ioports | grep 'PM1a_CNT_BLK' | cut -f3 -d' ' | cut -f1 -d'-'

This gives the hex address of the register. If none is printed out, than it is likely that the chipset is not supported.

In may case the address is 1804.


The sleep state is controlled by bits 10:12 (SLP_TYP) of the register and by bit 13 (SLP_EN).
SLP_TYP is a 3-bit value to select the state to enter (S5 is 7) and bit 13 is the enable bit.
The register has other value that must be preserved, so a read-modify-write operation must be done.

Using the in and out instructions is not possible a Ring 3 unless the TSS of the process has an IOPL (IO Privilege Level) of 3 (or the ports has been enabled in the IO ports map).
The IOPL tell which rings can use in and out, a value of X means all rings at X or less can.

This program attempt to power down the computer and optionally set the IOPL to the given value (through the symbol IOPL):

BITS 64

GLOBAL _start

SECTION .text

_start:
 ;Set the IOPL, only if greater than 0 (since 0 is the default)
%if IOPL > 0
 lea rsi, [rsp-80h]             ;We don't care about the pt_regs struct and we use the RED ZONE
 mov edi, IOPL                  ;IOPL to set
 mov eax, 172                  
 syscall                        ;Set iopl

 and eax, 0fh                   ;Just keep the last nibble, it can be 0 (success), 10 (invalid IOPL) or 15 (insufficient OS permissions)
 test eax, eax                  ;Test for errors
 mov edi, eax                   ;We exit with status 10 or 15 if the iopl syscall failed
 jnz .exit
%endif 

 ;Power off the PC
 mov dx, PM1a_CNT_BLK
 in eax, dx                     ;Read the current value
 and eax, 0ffffc003h            ;Clear SLP_TYP and SLP_EN
 or eax, (7 << 10) | (1 << 13)  ;Set SLP_TYP to 7 and SLP_EN to 1
 out dx, eax                    ;Power off

 ;This is just for safety, execution should STOP BEFORE arriving here. This exits the process with status
 ;0
 xor edi, edi

 ;Exit the process with a numerical status as specified in RDI
.exit:
 mov eax, 60
 syscall

It can be assembled with nasm po.asm -DPM1a_CNT_BLK=$1 -DIOPL=$2 -felf64 -o po.o where $1 is the port address of PM1a_CNT_BLK as found above but prefixed with 0x (in my case it becomes 0x1804) and $2 is a number (0-3) to set the IOPL to.
The IOPL is set iif it is not 0, since 0 is the default value (i.e. only Ring 0 can use in and out)

Note: Pass sensible values to the symbols or the program won't assemble.

This is interesting in the following ways:

  1. If run with an IOPL of 0, the program crashes with a #GP because of the use of in.
    This demonstrates the security mechanism of the CPU.
  2. If run with an IOPL > 0 but not as root it will fail due to insufficient privileges.
    This demonstrates the security mechanism of the OS, allowing only root to change IOPL.
  3. If run with an IOPL > 0 but < 3 and as root it will #GP due to the use of in.
    This demonstrates that user programs run at Ring 3 (the IOPL is not high enough).
  4. If run with IOPL = 3 and as root it will power down the computer (or fail at so and return, possibly leaving the system in an unknown state).
    This demonstrates the risk of allowing user programs access the hardware.
  5. If the IOPL > 3 it will fail due to an invalid IOPL value.
    This demonstrates that there are only four rings.

I've made a git repository with the code and a script built.sh that you can use to build and run different version of the program.
This script is useful because it converts the exit status of po into an user-friendly string, suitable for experimenting.

The script expects the PM1a_CNT_BLK address as the first argument (with the 0x prefix) and the IOPL as the second one.
Use it like:

./build.sh 0x1804 0
./build.sh 0x1804 3
sudo ./build.sh 0x1804 2
sudo ./build.sh 0x1804 4
sudo ./build.sh 0x1804 3

Of course, change the register address.


The IOPL trick is just... a trick. It doesn't really make a program running at Ring 0, it is useful for debugging but not much more.

In order to run a code at Ring 0 you need an LKM (Loadable kernel module).
In the same repository I've included an lkm dir with an example of a LKM.
Upon loading the module attempt to shutdown the computer (instantaneously).

The code is minimal:

#include <linux/module.h>   /* Needed by all modules */
#include <linux/kernel.h>   /* Needed for KERN_INFO */
#include <linux/fs.h>         /* Needed for KERN_INFO */
#include <asm/io.h>       /* Needed for inl and outl */

#define PM1a_CNT_BLK 0x1804

unsigned char bytes[10];

int __init lkm_init(void)
{
  unsigned int pm1a;

    printk(KERN_INFO "I'm going to power the computer off");


  pm1a = inl(PM1a_CNT_BLK);
  pm1a = ( pm1a & 0xffffc003 ) | ( 7 << 10 ) | ( 1 << 13 );
  outl(pm1a, PM1a_CNT_BLK);

  printk(KERN_WARNING "Powering off failed");

    return 0;
}


static void __exit lkm_exit(void)
{
}

module_init(lkm_init);
module_exit(lkm_exit);

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("M.Bloom");
MODULE_DESCRIPTION("Attempt to power down the computer");

To make the LKM, first edit the PM1a_CNT_BLK define, then run make in the same dir (you'll need the kernel headers), the Makefile is standard one for the LKMs.
To load the module use insmod po as root (this is an OS security mechanism).

I've compiled but not tested this LKM since I had already started writing this answer.
You may eventually fix it, use dmesg to check the output of the module.

You can use the LKM as a skeleton to run code a Ring 0, though when dealing with memory you must be aware of how Linux handles virtual memory.


One final note, if you are going to check/use this programs, be sure to close all applications, run a sync and, if willing, switch to run level 1 (with systemd it is systemctl isolate rescue) or at least stop all the critical services.

like image 105
Margaret Bloom Avatar answered Jan 01 '26 06:01

Margaret Bloom



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!