避免 IDT 挂钩中的页面错误
Avoiding page faults in IDT hooking
注意:我在 FreeBSD 上 运行,但我也包含了 Linux 作为标签,因为这个问题有点普遍并且 Linux我对特定的解决方案很感兴趣。
编辑:只是为了确认问题不是 FreeBSD 特有的,我将模块移植到 Linux,并且确实得到了完全相同的结果行为。 Linux 版本模块的代码如下;它本质上是完全一样的,唯一的主要区别是 IDT 显然在 Linux 中被赋予了只读保护,因此我必须禁用 cr0
中的写保护位才能使代码正常工作.
我正在学习一些有关 x86-64 架构上的内核开发的知识,目前正在阅读英特尔开发人员手册中有关中断处理的内容。作为实践,我正在尝试编写一个小的内核模块来挂钩 IDT 中的条目,但是 运行 遇到了问题。我的一般问题是:如果您使用 lidt
来更改整个 idtr
而不是,您如何确保钩子的代码(或新 IDT table 的数据)只是覆盖 IDT 的单个条目)总是存在于 RAM 中?我一直 运行 遇到的问题是,我将更改 IDT 条目,触发相应的中断,然后出现双重错误,因为我的挂钩代码未映射到 RAM 中。一般有什么方法可以避免这个问题?
对于我的具体情况,以下是我编写的 FreeBSD LKM 代码,它只是覆盖了 IDT 条目中列出的地址以处理零除数错误,并将其替换为 asm_hook
,目前只是无条件地 jmp
返回到原始中断处理程序。 (以后我当然会增加更多的功能。)
#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 table 并使用 lidt
将 table 加载到 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
然而,当我用下面的代码测试钩子时,内核挂起:
#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 指出的那样,它恰好放在页面边界上有点奇怪——有人知道为什么会这样吗?)
无论如何,当我触发零除数中断时,不幸的是断点从未命中,而且,一旦我进入双重故障中断处理程序,当我尝试访问 [=22= 处的内存时] VERR_PAGE_TABLE_NOT_PRESENT
错误仍然存在。
的确,FreeBSD 设计的一个不寻常(?)特征是从 RAM 交换其内核的 out/unmap 部分,所以也许更好的问题是“是什么导致了这个页面错误?”
编辑: 这是移植到 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);
编辑 2020 年 7 月 18 日:很抱歉让一个死人复活 post,但事实上这个故事还有更多内容。简而言之,问题实际上不是出在 VirtualBox 上,而是我的代码没有考虑崩溃缓解技术,尤其是内核页面 Table 隔离。显然,默认情况下 Qemu 不启用 KPTI,这就是为什么问题看起来是特定于管理程序的原因。但是,启用 OS X 的“Hypervisor Framework”与 Qemu(默认情况下启用 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 [=11=]xfffffffffffeffff, %%rax;" \
"mov %%rax, %%cr0;" \
:::"rax");
#define ENABLE_RW_PROTECTION \
__asm__ __volatile__( \
"mov %%cr0, %%rax;" \
"or [=11=]x10000, %%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 的实现中的某些东西必须定期交换out 内核内存区域。这对我来说似乎很奇怪,但不幸的是,据我所知,VirtualBox 没有太多文档;如果有人对这里发生的事情有任何了解,我很想听听。
无论如何,我切换到 qemu
,内核模块在那里完美地工作。为了 posterity,为了确认它的工作,对模块代码进行以下修改(我更改了 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 本身;尽管如此,还是要感谢所有试图提供帮助的人。*
注意:我在 FreeBSD 上 运行,但我也包含了 Linux 作为标签,因为这个问题有点普遍并且 Linux我对特定的解决方案很感兴趣。
编辑:只是为了确认问题不是 FreeBSD 特有的,我将模块移植到 Linux,并且确实得到了完全相同的结果行为。 Linux 版本模块的代码如下;它本质上是完全一样的,唯一的主要区别是 IDT 显然在 Linux 中被赋予了只读保护,因此我必须禁用 cr0
中的写保护位才能使代码正常工作.
我正在学习一些有关 x86-64 架构上的内核开发的知识,目前正在阅读英特尔开发人员手册中有关中断处理的内容。作为实践,我正在尝试编写一个小的内核模块来挂钩 IDT 中的条目,但是 运行 遇到了问题。我的一般问题是:如果您使用 lidt
来更改整个 idtr
而不是,您如何确保钩子的代码(或新 IDT table 的数据)只是覆盖 IDT 的单个条目)总是存在于 RAM 中?我一直 运行 遇到的问题是,我将更改 IDT 条目,触发相应的中断,然后出现双重错误,因为我的挂钩代码未映射到 RAM 中。一般有什么方法可以避免这个问题?
对于我的具体情况,以下是我编写的 FreeBSD LKM 代码,它只是覆盖了 IDT 条目中列出的地址以处理零除数错误,并将其替换为 asm_hook
,目前只是无条件地 jmp
返回到原始中断处理程序。 (以后我当然会增加更多的功能。)
#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 table 并使用 lidt
将 table 加载到 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
然而,当我用下面的代码测试钩子时,内核挂起:
#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 指出的那样,它恰好放在页面边界上有点奇怪——有人知道为什么会这样吗?)
无论如何,当我触发零除数中断时,不幸的是断点从未命中,而且,一旦我进入双重故障中断处理程序,当我尝试访问 [=22= 处的内存时] VERR_PAGE_TABLE_NOT_PRESENT
错误仍然存在。
的确,FreeBSD 设计的一个不寻常(?)特征是从 RAM 交换其内核的 out/unmap 部分,所以也许更好的问题是“是什么导致了这个页面错误?”
编辑: 这是移植到 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);
编辑 2020 年 7 月 18 日:很抱歉让一个死人复活 post,但事实上这个故事还有更多内容。简而言之,问题实际上不是出在 VirtualBox 上,而是我的代码没有考虑崩溃缓解技术,尤其是内核页面 Table 隔离。显然,默认情况下 Qemu 不启用 KPTI,这就是为什么问题看起来是特定于管理程序的原因。但是,启用 OS X 的“Hypervisor Framework”与 Qemu(默认情况下启用 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 [=11=]xfffffffffffeffff, %%rax;" \
"mov %%rax, %%cr0;" \
:::"rax");
#define ENABLE_RW_PROTECTION \
__asm__ __volatile__( \
"mov %%cr0, %%rax;" \
"or [=11=]x10000, %%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 的实现中的某些东西必须定期交换out 内核内存区域。这对我来说似乎很奇怪,但不幸的是,据我所知,VirtualBox 没有太多文档;如果有人对这里发生的事情有任何了解,我很想听听。
无论如何,我切换到 qemu
,内核模块在那里完美地工作。为了 posterity,为了确认它的工作,对模块代码进行以下修改(我更改了 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 本身;尽管如此,还是要感谢所有试图提供帮助的人。*