演示处理器环 - 运行环 0 指令的汇编代码

Demo processor rings - assembly code that runs ring 0 instructions

我想创建一个演示来向学生解释处理器环和系统调用。我想在演示文稿中做这样的事情:

  1. 编写一些汇编代码,尝试执行一些只能 运行 在 ring 0 中的代码(例如直接访问磁盘)
  2. 看到代码失败,并重新编写它以使用系统调用(例如,使用系统调用读取文件)
  3. 如果不是那么难:运行环0中的初始代码

我可以使用 Linux 或 Windows,哪个更简单

关于在哪里可以找到对我有帮助的代码的任何想法? 我可以使用哪些受保护的指令?

谢谢!

一个 attention-catching 演示可能会关闭计算机电源。

不幸的是,电源管理是一件复杂的事情,它涉及 ACPI specification that are rather abstract and long, further more they are not the first attempt
ACPI 很复杂,因为不同的供应商需要不同的操作来管理计算机的某个方面,无论如何,如果您 碰巧拥有 Intel 芯片组 (推荐使用 200 系列,但其他系列也应该可以)我们可以跳过大部分 ACPI 层并改用数据表。

在 class.

中使用它之前,您应该在最终硬件上检查该程序

ACPI定义了四种全局系统状态,G0-G3,其中G3是机械状态关机(即插头已拔,电池已取出),G2soft-off.
软件只能进入G2,进入S5休眠状态即可。

睡眠状态由PCH(Intel芯片组)通过IO寄存器(PM1a_CNT_BLK)控制,这个寄存器位于PCI配置space中定义的ACPI块中设备 31 功能 2(PM 控制器)。
应该读取块的基地址,然后加上四 (4) 以获得感兴趣的寄存器的地址。
我不会以编程方式执行此操作,而是汇编程序需要一个具有该地址的符号。

要检索寄存器 PM1a_CNT_BLK 的地址,可以使用 /proc/ioports 如下:

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

这给出了寄存器的十六进制地址。如果打印出 none,则可能是芯片组不受支持。

在可能的情况下,地址是 1804


睡眠状态由寄存器的位 10:12 (SLP_TYP) 和位 13 (SLP_EN).
控制 SLP_TYP 是 select 要进入的状态的 3 位值(S5 是 7),第 13 位是使能位。
寄存器有其他值必须保存,所以必须进行read-modify-write操作。

使用 inout 指令是不可能的 Ring 3 除非进程的 TSS 具有 IOPL(IO 特权级别)3(或者端口已在IO端口图)。
IOPL 告诉哪些环可以使用 inout,值 X 表示所有 X 或更小的环都可以。

此程序尝试关闭计算机并可选择将 IOPL 设置为给定值(通过符号 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

可以是assembled和nasm po.asm -DPM1a_CNT_BLK= -DIOPL= -felf64 -o po.o其中</code>是上面找到的<code>PM1a_CNT_BLK的端口地址但是前缀是0x(在我的例子中,它变为 0x1804)并且 </code> 是一个数字 (0-3),用于将 IOPL 设置为。<br> 如果 IOPL 不为 0,则设置 IOPL,因为 0 是默认值(即只有 Ring 0 可以使用 <code>inout

注意:将合理的值传递给符号,否则程序不会assemble。

这在以下方面很有趣:

  1. 如果 运行 的 IOPL 为 0,程序会因使用 in.
    而崩溃并显示 #GP 这演示了 CPU 的安全机制。
  2. 如果 运行 IOPL > 0 但不是作为 root,它将因权限不足而失败。
    这演示了 OS 的安全机制,只允许 root 更改 IOPL。
  3. 如果 运行 的 IOPL > 0 但 < 3 并且作为 root 它将 #GP 由于使用 in
    这表明用户程序 运行 在 Ring 3(IOPL 不够高)。
  4. 如果 运行 IOPL = 3 并且作为 root 用户,它将关闭计算机(或者在 return 时失败,可能使系统处于未知状态)。
    这表明允许用户程序访问硬件的风险。
  5. 如果 IOPL > 3,它将因 IOPL 值无效而失败。
    这表明只有四个环。

我制作了 a git repository with the code 和一个您可以用来构建的脚本 built.sh 和 运行 不同版本的程序。
这个脚本很有用因为它将po的退出状态转换成user-friendly字符串,适合做实验。

脚本需要 PM1a_CNT_BLK 地址作为第一个参数(带有 0x 前缀)和 IOPL 作为第二个参数。
像这样使用它:

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

当然是改寄存器地址。


IOPL 技巧只是……一个技巧。它并没有真正在 Ring 0 上创建一个程序 运行ning,它对调试很有用,但仅此而已。

为了 运行 Ring 0 的代码,您需要一个 LKM(可加载内核模块)。
在同一个存储库中,我包含了一个 lkm 目录和一个 LKM 示例。
加载模块后尝试关闭计算机(立即)。

代码最少:

#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");

要制作 LKM,首先编辑 PM1a_CNT_BLK 定义 ,然后运行 make 在同一个目录中(你需要内核头文件),Makefile 是 LKM 的标准。
要加载模块,请使用 insmod po 作为根用户(这是一个 OS 安全机制)。

因为我已经开始写这个答案,所以我已经编译但没有测试这个 LKM
您最终可能会修复它,使用 dmesg 检查模块的输出。

您可以使用 LKM 作为框架来 运行 编写 Ring 0,但在处理内存时您必须了解 Linux 如何处理虚拟内存。


最后一点,如果你要check/use这个程序,一定要关闭所有应用程序,运行一个sync,如果愿意,切换到运行 级别 1(对于 systemd,它是 systemctl isolate rescue)或至少停止所有关键服务。