龙盟编程博客 | 无障碍搜索 | 云盘搜索神器
快速搜索
主页 > 软件开发 > C/C++开发 >

OpenBSD 可加载内核模块编程完全指南

时间:2009-12-22 15:42来源:未知 作者:admin 点击:
分享到:
绪论 这篇文章我说明在openbsd上如何进行内核编程,以下句子来自lkm手册页: "可加载内核模块可以答应系统治理员在一台运行着的系统上动态的增加或删除功能模块,它同时可以帮助软件工

  绪论

  

  

这篇文章我说明在openbsd上如何进行内核编程,以下句子来自lkm手册页: "可加载内核模块可以答应系统治理员在一台运行着的系统上动态的增加或删除功能模块,它同时可以帮助软件工程师们为内核增加新的功能而根本就不需要重起计算机就可以测试他们开发的程序."

  

  

当然,像众多系统的lkm一样,它存在一定的安全隐患,哈哈,其实这也是我写这篇文章给大家的原因:)它提供了更广泛的空间给恶意的superroot,其实也就是已经得到系统治理员权限的我们。我们利用lkm可以驾驭整个系统而不会轻易被发现. 同样的, 假如你系统的securelevel在0级一行的话就不能加载或卸载模块了,假如你要使系统在进入securemode之前可以加载模块,可以编辑/etc/rc.securelevel文件,添加相应的入口.

  

  

总览

  

  

/dev/lkm设备与用户的交互通过ioctl(2)系列系统调用来进行. 主要是一些工具如modload,modunload和modstat等来控制模块的加载

  

  

和卸载以及模块的状态.

  

lkm接口定义了五种不同的模块类型:

  

  

系统调用模块

  

虚拟文件系统模块

  

设备驱动模块

  

可执行程序解释器模块

  

其它模块

  

一个普通的模块包括三个主要部分:

  

1) 内核入口和出口的处理(也就是当模块被加载,被卸载时的动作).

  

  

2) 一个外部入口点, 当模块用modload程序被加载的时候需要用到

  

  

3) 模块的主体, 包含函数代码等.

  

  

对于其他类型的模块来说,它需要开发人员提供严格的控制和当内核模块卸载的时候对内核原有的状态的保存.

  

  

对于模块的支持必须用'option LKM'编译进内核的配置文件.模块需要支持默认的openBSD 2.9的内核.通常,内核空间的数据接口都被提供

  

  

给了模块来操作.后面

  

就有一个lkm设备的例子.

  

  

每个类型的模块的内部数据结构里面都存在一个宏用来加载自己.也就类似模块本身模块名的东东,它被指定在内核数据结构中,和模块的一些

  

  

非凡数据如sysent这样

  

的针对系统调用模块的结构在一起.

  

  

让我们看看一些例子吧.

  

  

★系统调用模块.

  

  

这里我们将增加一个新的系统调用printf()的整型和字符串参数.它的原型如下:

  

  

int syscall(int, char *)

  

  

内核内部定义的一个lkm的syscall结构如下:

  

strUCt lkm_syscall {

  

MODTYPE lkm_type;

  

int lkm_ver;

  

char *lkm_name;

  

u_long lkm_offset; /* 保存/分配 内存空间 */

  

struct sysent *lkm_sysent;

  

struct sysent lkm_oldent; /*保存原调用,用于lkm的卸载 */

  

};

  

  

现在我们已经有了一个简单的模块框架了(应该叫LM_SYSCALL),lkm的版本,模块名,都在系统调用表里存在一个相应的入口.这样我们有

  

  

了一个指向结构sysent的模块框架

  

我们将用MOD_SYSCALL宏来安装它:

  

  

MOD_SYSCALL("ourcall", -1, &newcallent)

  

  

我们来分析一下上面的宏,很明显,模块名为"ourcall",用来标示模块,还有一个作用就是我们利用modstat命令时会显示出来.-1代表我们

  

  

的syscall该插入的位置,在这个

  

宏当中的-1的意思是我们不用关心位置具体在什么地方,它会被分配到一个空的位置.最后一个字段newcallent是一个指向sysent的结构,

  

  

它包含了我们系统调用的相应的数

  

据.

  

除此之外我们还需要一个句柄用来加载和卸载内核模块,好,在这个例子中我用'hi'来加载,用'bye'来卸载.这对我们调试程序很有帮助.句柄可

  

  

以是相同的函数或者单个函数,

  

假如没有定义句柄,那么lkm_nofunc()会简单的返回0,这个模块是没有加载卸载的,也就失去了作用.

  

  

我们模块的外部入口点是ourcall():

  

  

int

  

  

ourcall(lkmtp, cmd, ver)

  

struct lkm_table *lkmtp;

  

int cmd;

  

int ver;

  

{

  

DISPATCH(lkmtp, cmd, ver, ourcall_handler, ourcall_handler, lkm_nofunc)

  

}

  

  

这个句柄可以用来加载,卸载模块.第四个参数我们用作加载操作,第五个参数用作卸载操作,第六个参数是状态函数(在此例中没有用到).

  

ok!完整的系统调用模块代码如下(syscall.c):

  

  

#include

  

#include

  

#include

  

#include

  

#include

  

#include

  

#include

  

#include

  

#include

  

#include

  

  

  

/* 定义我们自己的系统调用原型 */

  

int newcall __P((struct proc *p, void *uap, int *retval));

  

  

/*

  

* 所有的系统调用都有三个参数: 一个指向proc结构的结构指针,一个空指针指向参

  

* 数本身和一个返回指针.下面,我们定义这些参数的结构.假如你只有一个参数,则

  

* 只需要一个入口就可以了.

  

*/

  

  

struct newcall_args{

  

syscallarg(int) value;

  

syscallarg(char *) msg;

  

};

  

  

/*

  

* 下面这个结构定义了我们的系统调用.第一个参数是系统调用的参数数目,第二个参数

  

* 是参数的大小,第三个参数是我们的系统调用的代码了,呵呵:)

  

*/

  

  

static struct sysent newcallent = {

  

2, sizeof(struct newcall_args), newcall

  

};

  

  

/*

  

* 好了,到了我们的syscall的核心结构了,呵呵:)

  

* 第一个参数是syscall的名称,ioctl()调用用它来查询syscall.第二个参数告诉我们

  

* syscall的位置.这里你可以输入数字,或者-1来让系统自动分配.第三个参数指向一个

  

* sysent结构的指针.

  

*/

  

  

MOD_SYSCALL("ourcall", -1, &newcallent);

  

  

/*

  

* 要使我们的模块正常运行我们还要用到以下函数.此函数类似Linux的lkm里面的init_module

  

* 和cleanup_module.

  

* 它通过一个指向lkm_table结构的指针来完成我们给定的动作.检查cmd的值来判定该加载

  

* 什么样的句柄.当我们利用模块来增加一个系统调用的时候,这儿没有专门的句柄来操作.

  

* 当然,我们hacking kernel的时候是不会用例如"hi"和"bye"这样的简单的句柄的,我们

  

* 需要改变系统调用.我们现在是说明原理,其实大同小异:)

  

*/

  

  

static int

  

ourcall_handler(lkmtp, cmd)

  

struct lkm_table *lkmtp;

  

int cmd;

  

{

  

if (cmd == LKM_E_LOAD)

  

printf("hi!n");

  

else if (cmd == LKM_E_UNLOAD)

  

printf("bye!n");

  

  

return(0);

  

}

  

  

/*

  

* 下面就是我们模块的外部入口点,也就是我们的系统调用的主体.

  

* 象上面那样我们通过判定一个cmd所匹配的句柄来描述动作的执行.我们也可以通过一个版本号

  

* 答应一个模块兼容以后版本内核的源码,以保证向下的兼容性.

  

* DISPATCH宏通过三个参数来表示动作的加载,卸载和状态.我们看下面例子,对于加载和卸载

  

* 我们用共享函数ourcall_handler().对于状态(当增加系统调用的时候就用不到它了)我们

  

* 用lkm_nofunc(),该函数仅仅简单的返回0.

  

*/

  

  

int

  

ourcall(lkmtp, cmd, ver)

  

struct lkm_table *lkmtp;

  

int cmd;

  

int ver;

  

{

  

DISPATCH(lkmtp, cmd, ver, ourcall_handler, ourcall_handler, lkm_nofunc)

  

  

}

  

  

/*

  

* 最后对于我们的系统调用应该有主体代码,该调用干了什么之类.

  

*/

  

  

int

  

newcall(p, v, retval)

  

struct proc *p;

  

void *v;

  

int *retval;

  

{

  

struct newcall_args *uap = v;

  

  

printf("%d %sn", SCARG(uap, value), SCARG(uap, msg));

  

return(0);

  

}

  

  

ok!我们编译安装它:

  

# cc -D_KERNEL -I/sys -c syscall.c

  

# modload -o ourcall.o -e ourcall syscall.o

  

Module loaded as ID 0

  

#

  

  

-o参数指定输出文件名,这和gcc的-o选项是一样的.-e参数指定我们的外部标示,最后一个参数就是输入文件.好,我们用modstat看看我们的

  

  

模块有没有被成功加载:

  

# modstat

  

Type Id Off Loadaddr Size Info Rev Module Name

  

SYSCALL 0 210 e0b92000 0002 e0b93008 2 ourcall

  

#

  

  

以上显示需要注重一下'off'字段,它标示了该模块在system call表里面的位置.这在创建系统调用的时候需要用到.我们可以通过dmesg命令

  

  

的输出'hi'来验证我们

  

的模块正确的加载运行了:

  

# dmesg tail -2

  

hi!

  

DDB symbols added: 150060 bytes

  

#

  

  

好,现在让我们来看一个测试我们刚才新的系统调用的简单程序(calltest.c):

  

#include

  

#include

  

#include

  

#include

  

#

  

精彩图集

赞助商链接