注意:我正在FreeBSD上运行,但是我也将Linux作为标签,因为问题在某种程度上是普遍的,而Linux特定的解决方案对我很感兴趣。
编辑:只是为了确认问题不是特定于FreeBSD,我将模块移植到Linux,并且确实获得了完全相同的行为。下面给出了该模块的Linux版本的代码;它本质上是完全相同的,唯一的主要区别是在Linux中IDT显然具有只读保护,因此我必须禁用cr0中的写保护位才能使代码正常工作。

我正在学习一些有关在x86-64架构上进行内核开发的知识,目前正在阅读《英特尔开发人员手册》中有关中断处理的内容。作为实践,我正在尝试编写一个小的内核模块,该模块挂接IDT中的条目,但是遇到了问题。我的一般问题是:如何确保始终在以下位置显示钩子(Hook)的代码(或新IDT表的数据,如果您使用lidt更改整个idtr而不是仅覆盖IDT的各个条目)内存?我一直遇到的问题是,我将更改IDT条目,触发相应的中断,然后出现双重错误,因为我的 Hook 代码未映射到RAM。总的来说,有什么方法可以避免这个问题?

对于我的情况的详细信息,以下是我编写的FreeBSD LKM的代码,该代码仅覆盖IDT条目中列出的地址以处理零除数故障,并将其替换为asm_hook的地址,目前它只是无条件的jmp s回到原始的中断处理程序中。 (将来,我当然会添加更多功能。)

#include <sys/types.h>
#include <sys/param.h>
#include <sys/proc.h>
#include <sys/module.h>
#include <sys/sysent.h>
#include <sys/kernel.h>
#include <sys/syscall.h>
#include <sys/sysproto.h>
#include <sys/systm.h>


//idt entry
struct idte_t {
    unsigned short offset_0_15;
    unsigned short segment_selector;
    unsigned char ist;              //interrupt stack table
    unsigned char type:4;
    unsigned char zero_12:1;
    unsigned char dpl:2;            //descriptor privilege level
    unsigned char p:1;              //present flag
    unsigned short offset_16_31;
    unsigned int offset_32_63;
    unsigned int rsv; }
    __attribute__((packed))
    *zd_idte;

#define ZD_INT 0x00
unsigned long idte_offset;          //contains absolute address of original interrupt handler

//idt register
struct idtr_t {
    unsigned short lim_val;
    struct idte_t *addr; }
    __attribute__((packed))
    idtr;

__asm__(
    ".text;"
    ".global asm_hook;"
"asm_hook:;"
    "jmp *(idte_offset);");
extern void asm_hook(void);


static int
init() {
    __asm__ __volatile__ (
        "cli;"
        "sidt %0;"
        "sti;"
        :: "m"(idtr));
    uprintf("[*]  idtr dump\n"
            "[**] address:\t%p\n"
            "[**] lim val:\t0x%x\n"
            "[*]  end dump\n\n",
            idtr.addr, idtr.lim_val);
    zd_idte=(idtr.addr)+ZD_INT;

    idte_offset=(long)(zd_idte->offset_0_15)|((long)(zd_idte->offset_16_31)<<16)|((long)(zd_idte->offset_32_63)<<32);
    uprintf("[*]  old idt entry %d:\n"
            "[**] addr:\t%p\n"
            "[**] segment:\t0x%x\n"
            "[**] ist:\t%d\n"
            "[**] type:\t%d\n"
            "[**] dpl:\t%d\n"
            "[**] p:\t\t%d\n"
            "[*]  end dump\n\n",
            ZD_INT, (void *)idte_offset, zd_idte->segment_selector,
            zd_idte->ist, zd_idte->type, zd_idte->dpl, zd_idte->p);
    if(!zd_idte->p) {
        uprintf("[*] fatal: handler segment not present\n");
        return ENOSYS; }

    __asm__ __volatile__("cli");
    zd_idte->offset_0_15=((unsigned long)(&asm_hook))&0xffff;
    zd_idte->offset_16_31=((unsigned long)(&asm_hook)>>16)&0xffff;
    zd_idte->offset_32_63=((unsigned long)(&asm_hook)>>32)&0xffffffff;
    __asm__ __volatile__("sti");
    uprintf("[*]  new idt entry %d:\n"
            "[**] addr:\t%p\n"
            "[**] segment:\t0x%x\n"
            "[**] ist:\t%d\n"
            "[**] type:\t%d\n"
            "[**] dpl:\t%d\n"
            "[**] p:\t\t%d\n"
            "[*]  end dump\n\n",
            ZD_INT, (void *)(\
            (long)zd_idte->offset_0_15|((long)zd_idte->offset_16_31<<16)|((long)zd_idte->offset_32_63<<32)),
            zd_idte->segment_selector, zd_idte->ist, zd_idte->type, zd_idte->dpl, zd_idte->p);

    return 0; }

static void
fini() {
    __asm__ __volatile__("cli");
    zd_idte->offset_0_15=idte_offset&0xffff;
    zd_idte->offset_16_31=(idte_offset>>16)&0xffff;
    zd_idte->offset_32_63=(idte_offset>>32)&0xffffffff;
    __asm__ __volatile__("sti"); }

static int
load(struct module *module, int cmd, void *arg) {
    int error=0;
    switch(cmd) {
        case MOD_LOAD:
            error=init();
            break;
        case MOD_UNLOAD:
            fini();
            break;
        default:
            error=EOPNOTSUPP;
            break; }
    return error; }

static moduledata_t idt_hook_mod = {
    "idt_hook",
    load,
    NULL };

DECLARE_MODULE(idt_hook, idt_hook_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
(我还写了另一个LKM,它使用malloc(9)创建了一个完整的新IDT表并使用lidt将该表加载到idtr中,但是在我看来这是一种劣等的方法,因为它只会更改正在运行的特定CPU内核上的IDT,因此无法在多处理器系统中可靠地工作。除非缺少我的东西,否则这是一项准确的评估吗?)
无论如何,编译代码并加载内核模块不会导致任何问题:
# kldload ./idt_hook.ko
[*]  idtr dump
[**] address:   0xffffffff81fb2c40
[**] lim val:   0xfff
[*]  end dump

[*]  old idt entry 0:
[**] addr:      0xffffffff81080f90
[**] segment:   0x20
[**] ist:       0
[**] type:      14
[**] dpl:       0
[**] p:         1
[*]  end dump

[*]  new idt entry 0:
[**] addr:      0xffffffff8281d000
[**] segment:   0x20
[**] ist:       0
[**] type:      14
[**] dpl:       0
[**] p:         1
[*]  end dump
但是,当我使用以下命令测试 Hook 时,内核挂起:
#include <stdio.h>

int main() {
    int x=1, y=0;
    printf("x/y=%d\n", x/y);
    return 0; }
为了了解发生了什么,我启动了VirtualBox内置调试器,并在IDT的双重故障异常处理程序上设置了一个断点(条目8)。调试显示,我的LKM正确更改了IDT,但是运行上面的零除数代码会引发双重错误。当我尝试访问0xffffffff8281d000(我的asm_hook代码的地址)上的内存时,我意识到了这个原因,这在VirtualBox调试器中触发了VERR_PAGE_TABLE_NOT_PRESENT错误。因此,除非我误解了,否则问题显然是我的asm_hook在某个时候被从内存中删除了。关于如何解决此问题的任何想法?例如,是否有一种方法可以告诉FreeBSD内核永远不要从RAM中取消映射特定页面?

编辑:下面的注释中的Nate Eldredge帮助我找到了我的代码中的一些错误(现已更正),但是不幸的是,问题仍然存在。要提供更多的调试详细信息:首先加载内核模块,然后在VirtualBox调试器中的asm_hook代码(0xffffffff8281d000)列出的地址上设置一个断点。我已通过在该地址处拆解内存来确认它确实包含asm_hook的代码。 (尽管,正如Nate所指出的那样,它恰好位于页面边界上,这有点奇怪-任何人都不知道为什么会这样吗?)
无论如何,当我触发零除数中断时,不幸的是,断点永远不会被击中,而且一旦进入双重故障中断处理程序,当我尝试访问0xffffffff8281d000的内存时,VERR_PAGE_TABLE_NOT_PRESENT错误仍然会标记出来。
的确,从RAM换出/取消映射内核部分是FreeBSD设计的一个不寻常的(?)功能,因此也许更好的问题是“是什么导致此页面错误?”

编辑:这是移植到Linux的模块的版本:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <asm/io.h>

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Hooks the zero divisor IDT entry");
MODULE_VERSION("0.01");


struct idte_t {
    unsigned short offset_0_15;
    unsigned short segment_selector;
    unsigned char ist;              //interrupt stack table
    unsigned char type:4;
    unsigned char zero_12:1;
    unsigned char dpl:2;            //descriptor privilege level
    unsigned char p:1;              //present flag
    unsigned short offset_16_31;
    unsigned int offset_32_63;
    unsigned int rsv; }
    __attribute__((packed))
    *zd_idte;

#define ZD_INT 0x00
unsigned long idte_offset;          //contains absolute address of original interrupt handler
struct idtr_t {
    unsigned short lim_val;
    struct idte_t *addr; }
    __attribute__((packed))
    idtr;

__asm__(
    ".text;"
    ".global asm_hook;"
"asm_hook:;"
    "jmp *(idte_offset);");
extern void asm_hook(void);


static int __init
idt_init(void) {
    __asm__ __volatile__ (
        "cli;"
        "sidt %0;"
        "sti;"
        :: "m"(idtr));
    printk("[*]  idtr dump\n"
           "[**] address:\t%px\n"
           "[**] lim val:\t0x%x\n"
           "[*]  end dump\n\n",
           idtr.addr, idtr.lim_val);
    zd_idte=(idtr.addr)+ZD_INT;

    idte_offset=(long)(zd_idte->offset_0_15)|((long)(zd_idte->offset_16_31)<<16)|((long)(zd_idte->offset_32_63)<<32);
    printk("[*]  old idt entry %d:\n"
           "[**] addr:\t%px\n"
           "[**] segment:\t0x%x\n"
           "[**] ist:\t%d\n"
           "[**] type:\t%d\n"
           "[**] dpl:\t%d\n"
           "[**] p:\t\t%d\n"
           "[*]  end dump\n\n",
           ZD_INT, (void *)idte_offset, zd_idte->segment_selector,
           zd_idte->ist, zd_idte->type, zd_idte->dpl, zd_idte->p);
    if(!zd_idte->p) {
        printk("[*] fatal: handler segment not present\n");
        return ENOSYS; }

    unsigned long cr0;
    __asm__ __volatile__("mov %%cr0, %0" : "=r"(cr0));
    cr0 &= ~(long)0x10000;
    __asm__ __volatile__("mov %0, %%cr0" :: "r"(cr0));
    __asm__ __volatile__("cli");
    zd_idte->offset_0_15=((unsigned long)(&asm_hook))&0xffff;
    zd_idte->offset_16_31=((unsigned long)(&asm_hook)>>16)&0xffff;
    zd_idte->offset_32_63=((unsigned long)(&asm_hook)>>32)&0xffffffff;
    __asm__ __volatile__("sti");
    cr0 |= 0x10000;
    __asm__ __volatile__("mov %0, %%cr0" :: "r"(cr0));
    printk("[*]  new idt entry %d:\n"
           "[**] addr:\t%px\n"
           "[**] segment:\t0x%x\n"
           "[**] ist:\t%d\n"
           "[**] type:\t%d\n"
           "[**] dpl:\t%d\n"
           "[**] p:\t\t%d\n"
           "[*]  end dump\n\n",
           ZD_INT, (void *)(\
           (long)zd_idte->offset_0_15|((long)zd_idte->offset_16_31<<16)|((long)zd_idte->offset_32_63<<32)),
           zd_idte->segment_selector, zd_idte->ist, zd_idte->type, zd_idte->dpl, zd_idte->p);

    return 0; }

static void __exit
idt_fini(void) {
    unsigned long cr0;
    __asm__ __volatile__("mov %%cr0, %0" : "=r"(cr0));
    cr0 &= ~(long)0x10000;
    __asm__ __volatile__("mov %0, %%cr0" :: "r"(cr0));
    __asm__ __volatile__("cli");
    zd_idte->offset_0_15=idte_offset&0xffff;
    zd_idte->offset_16_31=(idte_offset>>16)&0xffff;
    zd_idte->offset_32_63=(idte_offset>>32)&0xffffffff;
    __asm__ __volatile__("sti");
    cr0 |= 0x10000;
    __asm__ __volatile__("mov %0, %%cr0" :: "r"(cr0)); }

module_init(idt_init);
module_exit(idt_fini);

最佳答案

编辑07/18/20 :很抱歉,要复活一个已死的帖子,但实际上,故事还有很多。简而言之,问题实际上不在于VirtualBox,而是由于我的代码无法解决崩溃缓解技术,尤其是内核页表隔离。显然,Qemu默认情况下不启用KPTI,这就是为什么该问题似乎是特定于虚拟机管理程序的原因。但是,通过Qemu启用OS X的“Hypervisor框架”(默认情况下确实启用KPTI)导致该模块再次失败。经过大量调查,我终于意识到问题出在KPTI。像许多内核代码一样,显然可加载的内核模块未包含在用户空间页表中。
为了解决这个问题,我必须编写一个新模块,用一个代码片段覆盖内核现有的IRQ处理程序(包括在用户空间页面表中)的代码,并将cr3更改为一个值,该值将包含我的内核模块的页面条目。 (这是下面代码中的stub。)然后,我跳到asm_hook –现在分页了–增加我的计数器变量,恢复cr3的旧值,然后跳到现有的内核IRQ处理程序。 (因为除法错误处理程序被覆盖,所以我跳到了软断点处理程序。)下面的代码可以用相同的零除程序进行测试。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/mm.h>
#include <linux/kallsyms.h>
#include <asm/io.h>
#include "utilities.h"

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Atticus Stonestrom");
MODULE_DESCRIPTION("Hooks the zero divisor IDT entry");


struct idte_t *idte;                  //points to the start of the IDT

#define ZD_INT 0x00
#define BP_INT 0x03
unsigned long zd_handler;             //contains absolute address of division error IRQ handler
unsigned long bp_handler;             //contains absolute address of soft breakpoint IRQ handler
#define STUB_SIZE 0x2b                //includes extra 8 bytes for the old value of cr3
unsigned char orig_bytes[STUB_SIZE];  //contains the original bytes of the division error IRQ handler
struct idtr_t idtr;                   //holds base address and limit value of the IDT

int counter=0;
__asm__(
    ".text;"
    ".global asm_hook;"
"asm_hook:;"
    "incl counter;"
    "movq (bp_handler), %rax;"
    "ret;");
extern void asm_hook(void);


__asm__(
    ".text;"
    ".global stub;"
"stub:;"
    "push %rax;"    //bp_handler
    "push %rbx;"    //new cr3, &asm_hook
    "push %rdx;"    //old cr3
    "mov %cr3, %rdx;"
    "mov .CR3(%rip), %rbx;"
    "mov %rbx, %cr3;"
    "mov $asm_hook, %rbx;"
    "call *%rbx;"
    "mov %rdx, %cr3;"
    "pop %rdx;"
    "pop %rbx;"
    "xchg %rax, (%rsp);"
    "ret;"
".CR3:;"
    //will be filled with a valid value of cr3 during module initialization
    ".quad 0xdeadbeefdeadbeef;");
extern void stub(void);

static int __init
idt_init(void) {
    READ_IDT(idtr)
    printk("[*]  idtr dump\n"
           "[**] address:\t0x%px\n"
           "[**] lim val:\t0x%x\n"
           "[*]  end dump\n\n",
           idtr.addr, idtr.lim_val);
    idte=(idtr.addr);

    zd_handler=0
        | ((long)((idte+ZD_INT)->offset_0_15))
        | ((long)((idte+ZD_INT)->offset_16_31)<<16)
        | ((long)((idte+ZD_INT)->offset_32_63)<<32);
    printk("[*]  idt entry %d:\n"
           "[**] addr:\t0x%px\n"
           "[**] segment:\t0x%x\n"
           "[**] ist:\t%d\n"
           "[**] type:\t%d\n"
           "[**] dpl:\t%d\n"
           "[**] p:\t\t%d\n"
           "[*]  end dump\n\n",
           ZD_INT, (void *)zd_handler, (idte+ZD_INT)->segment_selector,
           (idte+ZD_INT)->ist, (idte+ZD_INT)->type, (idte+ZD_INT)->dpl, (idte+ZD_INT)->p);
    if(!(idte+ZD_INT)->p) {
        printk("[*] fatal: handler segment not present\n");
        return ENOSYS; }

    bp_handler=0
        | ((long)((idte+BP_INT)->offset_0_15))
        | ((long)((idte+BP_INT)->offset_16_31)<<16)
        | ((long)((idte+BP_INT)->offset_32_63)<<32);
    printk("[*]  breakpoint handler:\t0x%lx\n\n", bp_handler);


    unsigned long cr3;
    __asm__ __volatile__("mov %%cr3, %0":"=r"(cr3)::"memory");
    printk("[*] cr3:\t0x%lx\n\n", cr3);

    memcpy(orig_bytes, (void *)zd_handler, STUB_SIZE);
    DISABLE_RW_PROTECTION
    __asm__ __volatile__("cli":::"memory");
    memcpy((void *)zd_handler, &stub, STUB_SIZE);
    *(unsigned long *)(zd_handler+STUB_SIZE-8)=cr3; //fills the .CR3 data section of stub with a value of cr3 guaranteed to have the code asm_hook paged in
    __asm__ __volatile__("sti":::"memory");
    ENABLE_RW_PROTECTION

    return 0; }

static void __exit
idt_fini(void) {
    printk("[*] counter: %d\n\n", counter);

    DISABLE_RW_PROTECTION
    __asm__ __volatile__("cli":::"memory");
    memcpy((void *)zd_handler, orig_bytes, STUB_SIZE);
    __asm__ __volatile__("sti":::"memory");
    ENABLE_RW_PROTECTION }

module_init(idt_init);
module_exit(idt_fini);
utilities.h仅包含一些相关的IDT宏和structs,例如:
#define DISABLE_RW_PROTECTION         \
__asm__ __volatile__(                 \
    "mov %%cr0, %%rax;"               \
    "and $0xfffffffffffeffff, %%rax;" \
    "mov %%rax, %%cr0;"               \
    :::"rax");

#define ENABLE_RW_PROTECTION          \
__asm__ __volatile__(                 \
    "mov %%cr0, %%rax;"               \
    "or $0x10000, %%rax;"             \
    "mov %%rax, %%cr0;"               \
    :::"rax");

struct idte_t {
    unsigned short offset_0_15;
    unsigned short segment_selector;
    unsigned char ist;              //interrupt stack table
    unsigned char type:4;
    unsigned char zero_12:1;
    unsigned char dpl:2;            //descriptor privilege level
    unsigned char p:1;              //present flag
    unsigned short offset_16_31;
    unsigned int offset_32_63;
    unsigned int rsv; }
    __attribute__((packed));

struct idtr_t {
    unsigned short lim_val;
    struct idte_t *addr; }
    __attribute__((packed));

#define READ_IDT(dst)   \
__asm__ __volatile__(   \
    "cli;"              \
    "sidt %0;"          \
    "sti;"              \
    :: "m"(dst)         \
    : "memory");

#define WRITE_IDT(src)  \
__asm__ __volatile__(   \
    "cli;"              \
    "lidt %0;"          \
    "sti;"              \
    :: "m"(src)         \
    : "memory");
删除模块后,dmesg将显示调用除错处理程序的次数,表示成功。
*显然问题不是与我的代码有关,而是与VirtualBox有关。在VirtualBox调试器中玩耍时,我意识到,一旦进入IDT/IRQ处理程序,尝试访问甚至内核内存的某些区域都会标志VERR_PAGE_TABLE_NOT_PRESENT错误,因此看起来VirtualBox的实现中的某些内容必须定期换出内核内存的区域。对我来说,这似乎很奇怪,但不幸的是,据我所知,VirtualBox并没有太多的文档。如果有人对这里发生的事情有任何见解,我很想听听。
无论如何,我都切换到qemu,内核模块在那里完美运行。为了后代,为确认其正常工作,请对模块代码进行以下修改(特别是我更改了Linux):
int counter=0;
__asm__(
    ".text;"
    ".global asm_hook;"
"asm_hook:;"
    "incl counter;"
    "jmp *(idte_offset);");

...

static void __exit
idt_fini(void) {
    printk("[*] counter:\t%d\n\n", counter);
...
装入内核模块后,请运行几次“除零”程序,然后卸载模块并检查dmesg以确保其正常工作。
因此,总而言之,问题不在于代码,而在于VirtualBox本身。不过,感谢所有尝试提供帮助的人。*

关于c - 避免IDT Hook 中的页面错误,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/62642185/

10-09 20:55