0 - 前言 1 - 绪论 2 - 介绍 2.1 - 什么是中断(interrupt)? 2.2 - 中断和异常(exception) 2.3 - 中断向量 2.4 - 什么是IDT? 3 - 异常 3.1 - 异常列表 3.2 - 当异常出现时会发生什么 ? 3.3 - 中断钩子(Hooking) by mammon 3.4 - 一般中断钩子 3.5 - profit钩子 : 我们第一个后门 3.6 - fun钩子 4 - 硬件中断 4.1 - 它是如何工作的 ? 4.2 - 初始化和半底(bottom half)激活过程 4.3 - 键盘中断钩子 5 - 为系统调用安排的异常 5.1 - 系统调用列表 5.2 - 系统调用是如何工作的 ? 5.3 - profit钩子 5.3.1 - sys_setuid钩子 5.3.2 - sys_write钩子 5.4 - fun钩子 6 - CheckIDT 7 - 参考 & 致谢 8 - 附录 前言: 看到这片文章就让我想到LSD在5th Argus Hacking Challenge上的精彩表演。 只不过当时玩的是系统LDT漏洞,现在玩的是系统IDT后门。翻译不妥的地方还请斧正,如果您 的英文比较好的话,还是看原文吧。 --[ 1 - 绪论 众所周知,Intel x86 CPU能够运行在两种模式下:一种是实模式,一种是保护模式。 实模式我们就不讨论了,现在所有的操作系统都使用的是保护模式来使内核和一般进程隔离。 保护模式提供4个不同的权限等级(ring0...ring3)。用户应用程序在ring3,系统内核运行 在ring0.这使内核获得了访问所有CPU寄存器和硬件内存的权力。 在文中,我们将演示如何在Linux/x86上修改IDT。再进一步,我们将演示如何使用 一些技术重定向系统调用(象LKM做到的那样)。 本文中的例子只来说明使用LKM把可执行代码装载到内核空间是件容易的事情。其他超出 本文讨论范围的技术也可以用来把可执行代码装载到内核空间或者用来隐藏内核模块(就象 Spacewalker的方法一样)。 CheckIDT是个有用的工具,它检查IDT并且每5分钟避免内核panic一次。 --[ 2 - 介绍 ----[ 2.1 - 什么是中断(interrupt)? "中断被定义为当一个事件发生时,改变处理器的指令序列。这样的事件可由CPU芯片 内部或者外部硬件产生电信号产生" (摘自: "Understanding the Linux kernel," O'Reilly publishing.) ----[ 2.2 - 中断和异常(exception) Intel参考手册上指出“同步中断”(在一个指令执行完成后,由CPU控制单元产生的)作为“异常”。 异步中断(可能会在任意时刻由其他硬件产生的)才称为“中断”。中断被外部的I/O设备产生。 但是异常是由编程错误或者是由反常情况(必须由内核来处理)触发的。在该文档中, 术语“中断信号”既指异常又指中断。 中断分为两种类型:可屏蔽中断--它在短时间片段里可被忽略;不可屏蔽中断--它必须被立即处理。 不可屏蔽中断是由紧急事件产生例如硬件失败。著名的IRQS(中断请求)失败划为可屏蔽中断。 异常被分为不同的两类:处理器产生的异常(Faults, Traps, Aborts)和编程安排的 异常(用汇编指令int or int3 触发)。后一种就是我们经常说到的软中断。 ----[ 2.3 - 中断向量 每个中断或者异常用一个0-255的数字识别。Intel称这个数字为向量(vector).这些 数字如下分类: - From 0 to 31 : 异常和不可屏蔽中断 - From 32 to 47 : 可屏蔽中断 - From 48 to 255 : 软中断 Linux只使用一个软中断(0x80)作为调用系统内核函数的系统调用接口。 硬件IRQs从IRQ0...IRQ15分别被关联到了中断向量32..47。 ----[ 2.4 - 什么是IDT? IDT = Interrupt Descriptor Table 中断描述表 IDT是一个有256个入口的线形表,每个中断向量关联了一个中断处理过程。 每个IDT的入口是个8字节的描述符,所以整个IDT表的大小为256*8=2048 bytes IDT有三种不同的描述符或者说是入口: - 任务门描述符 Task Gate Descriptor Linux 没有使用该类型描述符 - 中断门描述符 Interrupt Gate Descriptor 63 48|47 40|39 32 +------------------------------------------------------------ | | |D|D| | | | | | | | | | HANDLER OFFSET (16-31) |P|P|P|0|1|1|1|0|0|0|0| RESERVED | | |L|L| | | | | | | | | ============================================================= | | SEGMENT SELECTOR | HANDLER OFFSET (0-15) | | | ------------------------------------------------------------+ 31 16|15 0 - bits 0 to 15 : handler offset low - bits 16 to 31 : segment selector - bits 32 to 37 : reserved - bits 37 to 39 : 0 - bits 40 to 47 : flags/type - bits 48 to 63 : handler offset high - 陷阱门描述符 Trap Gate Descriptor 同上,只是flag不同 flag 组成如下 : - 5 bits for the type interrupt gate : 1 1 1 1 0 trap gate : 0 1 1 1 0 - 2 bits for DPL DPL = descriptor privilege level - 1 bit reserved Offset low和offset high组成了处理中断函数的地址。当中断发生时会直接跳到该 地址运行。本文的目标是改变那些地址并且让我们自己的中断处理函数执行 DPL=Descriptor Privilege Level DPL等于0或者是3. 0是特权等级(内核模式). 当前的执行等级被保存在CPL寄存器中 (Current Privilege Level). 控制单元UC (Unit Of Control) 比较CPL中的值和IDT中断 描述符中的DPL字段。假如DPL值大于(较小权限)或者等于CPL值,那么中断处理过程被执行。 用户应用程序在ring3(CPL==3)中执行。某些中断在用户态是不能够被调用的。 IDT被BIOS程序首先初始化,但是当Linux得到控制权后,Linux自己又重新设置了IDT。 汇编指令lidt提供了初始化idtr寄存器---它包含了IDT的大小和IDT的地址。 然后setup_idt函数填充了256个IDT入口--使用了同样的中断门(ignore_int)。然后按照需要, 安装正确的中断门。 linux/arch/i386/kernel/traps.c::set_intr_gate(n, addr) 在idt寄存器指向的地址n位置插入一个中断门。'addr'中存放中断处理地址。 linux/arch/i386/kernel/irq.c 所有可屏蔽中断和软中断被set_intr_gate初始化: set_intr_gate : #define FIRST_EXTERNAL_VECTOR 0x20 for (i = 0; i trap_gate [root@redhat73 root]# grep c0108abc /boot/System.map c0108abc T overflow overflow -> system_gate [root@redhat73 root]# grep c0100200 /boot/System.map c0100200 t ignore_int 18到31 Intel保留 [root@redhat73 root]# grep c021e2ac /boot/System.map c021e2ac r IRQ0x00_interrupt device keyboard ->intr_gate [root@redhat73 root]# grep c01088f0 /boot/System.map c01088f0 T system_call system call -> system_gate 注: checkIDT有个选项解析标号 --[ 3 - 异常 ----[ 3.1 - 异常列表 --------------------------------------------------------------------------+ number | Exception | Exception Handler | --------------------------------------------------------------------------+ 0 | Divide Error | divide_error() | 1 | Debug | debug() | 2 | Nonmaskable Interrupt | nmi() | 3 | Break Point | int3() | 4 | Overflow | overflow() | 5 | Boundary verification | bounds() | 6 | Invalid operation code | invalid_op() | 7 | Device not available | device_not_available() | 8 | Double Fault | double_fault() | 9 | Coprocessor segment overrun | coprocesseur_segment_overrun() | 10 | TSS not valid | invalid_tss() | 11 | Segment not present | segment_no_present() | 12 | stack exception | stack_segment() | 13 | General Protection | general_protection() | 14 | Page Fault | page_fault() | 15 | Reserved by Intel | none | 16 | Calcul Error with float virgul| coprocessor_error() | 17 | Alignement check | alignement_check() | 18 | Machine Check | machine_check() | --------------------------------------------------------------------------+ 异常被分为两类: - 处理器侦测的异常(DPL为0) - 软中断(aka programmed exceptions) (DPL为3). 后者我们可在用户态调用。 ----[ 3.2 - 当异常出现时会发生什么 ? 当一个中断发生,当前中断的中断处理函数被执行。该处理函数不是真正的处理异常函数, 它仅仅做个跳转,跳转到更好的处理函数。 异常 -----> 中间处理函数 -----> 真正的处理异常函数 entry.S 定义了所有的中间处理函数,也称为通用处理函数或者是stub. 中间处理函数用asm写的,后面真正的处理函数是用C写的。 让我们看看entry.S : entry.S : --------- ************************************************** ENTRY(nmi) pushl $0 pushl $ SYMBOL_NAME(do_nmi) jmp error_code ENTRY(int3) pushl $0 pushl $ SYMBOL_NAME(do_int3) jmp error_code ENTRY(overflow) pushl $0 pushl $ SYMBOL_NAME(do_overflow) jmp error_code ENTRY(divide_error) pushl $0 # no error value/code pushl $ SYMBOL_NAME(do_divide_error) ALIGN error_code: pushl %ds pushl