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

/ns/wz/comp/data/20011116043148.htm

来源:www.whitecell.org

整理翻译:e4gle <par@whitecell.org> from www.whitecell.org

绪论

这篇文章我说明在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 <sys/param.h>
#include <sys/systm.h>
#include <sys/ioctl.h>
#include <sys/cdefs.h>
#include <sys/conf.h>
#include <sys/mount.h>
#include <sys/exec.h>
#include <sys/lkm.h>
#include <sys/proc.h>
#include <sys/syscallargs.h>


/* 定义我们自己的系统调用原型 */
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 <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <err.h>
#include <sys/lkm.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>

int
main(argc, argv)
int argc;
char **argv;
{
int error, fd;
struct lmc_stat modstat;

if (argc != 3)
erro(1, "%s ", argv[0]);

modstat.name = "ourcall";

fd = open("/dev/lkm", O_RDONLY);
if (fd == -1)
err(1, "open");

error = ioctl(fd, LMSTAT, &modstat);
if (error == -1)
err(1, "ioctl");

printf("syscall no: %lun", modstat.offset);

error = syscall(modstat.offset, atoi(argv[1]), argv[2]);
if (error == -1)
err(1, "syscall");

exit(0);
}

注意我们是怎么从module的modstat结构来利用ioctl调用获得syscall的偏移量的.一般的用户权限是不允许访问/dev/lkm设备的,同样,

我们也可以从modstat来获得象以上那
样的信息.
所以我们的程序需要一个整数和字符串参数提交给新系统调用,好,编译运行我们的程序:

# cc -o calltest calltest.c
# ./calltest 4 beers
syscall no: 210
# dmesg | tail -1
4 beers
#

我们用unloadmod来卸载内核模块:
# modunload -n ourcall
#

再用dmesg命令可以看出我们的模块被成功卸载了:
# dmesg | tail -1
bye!
#

好,现在让我们来看看设备驱动的编写.
★设备驱动程序模块

设备驱动模块和系统调用的模块的编写方法有很大的相同之处.他们有一个外部入口点,且句柄关联着特殊的模块代码.在下面的这段特殊的模

块代码会直接操作我们的设备.在这个
例子中我们简单的演示了一个字符设备的例子,只能支持open,close,read和ioctl操作.在我们剖析它的内部机理之前,让我们先来看看lkm

是如何来解释设备的.

下面这段代码定义了一个可加载的设备驱动:

struct lkm_dev {
MODTYPE lkm_type;
int lkm_ver;
char *lkm_name;
u_long lkm_offset;
DEVTYPE lkm_devtype;
union {
void *anon;
struct bdevsw *bdev;
struct cdevsw *cdev;
} lkm_dev;
union
{
struct bdevsw bdev;
struct cdevsw cdev;
} lkm_olddev;
};

首先我们需要一个模块的类型(这里是LM_DEV),然后是lkm的版本号,再就是它的名称和它在cdevsw[]或者bdevsw[]表中的位置

(lkm_offset).然后我们到了DEVTYPE定义的
lkm_devtype成员,它定义了我们设备的类型,或者是一个字符型设备或者是一个块设备,分别被LM_DT_CHAR或者LM_DT_BLOCK宏指

定.再下面定义了两个枚举类型的结构,在模
快被加载的时候分别定义了新的设备的操作空间以及保留了老的设备结构,此结构通过MOD_DEV宏来初始化:
MOD_DEV("ourdev", LM_DT_CHAR, -1, &cdev_ourdev)

首先我们通过我们的模块名以及设备类型,在此例中我们得知我们创建的是一个字符型的设备.接下来需要在cdevsw[]中有个入口,就象上面

的系统调用的例子那样,-1代表我们可以
不去关心放置的确切位置,让系统自己去寻找可用的入口.如果没有空闲的入口,函数ENFILE ("Too many open files in system")将会被

返回.最后我们通过初始化cdevsw
结构来对我们的设备进行操作.
我们的字符设备将会支持四种操作:open,close,read和ioctl.不能干再多的事情了,它将存储一个字符串和一个数字,该数字可以被ioctl调用

设置和返回,字符串也可以用read
调用返回.
我们定义的内部结构如下:

#define MAXMSGLEN 100

struct ourdev_io {
int value;
char msg[MAXMSGLEN];
};

当模块第一次被加载的时候,我们设置value为13并且为我们的字符串赋值"hello world!".我们定义了两个简单的ioctl调用来设置或获取内

部结构的当前的value的值.这些
都利用ourdev_io结构作为一个参数,然后利用ioctl执行一个相应的动作.
在模块的入口指针中,我这里再次用了IDSPATH宏.

以下是我们自定义的设备程序的完整代码(chardev.c):

#include <sys/param.h>
#include <sys/fcntl.h>
#include <sys/systm.h>
#include <sys/ioctl.h>
#include <sys/exec.h>
#include <sys/conf.h>
#include <sys/lkm.h>

#include "common.h"

/*
* 导入我们支持的操作:open,read,close,ioctl等
*/

int ourdevopen __P((dev_t dev, int oflags, int devtype, struct proc *p));
int ourdevclose __P((dev_t dev, int fflag, int devtype, struct proc *p));
int ourdevread __P((dev_t dev, struct uio *uio, int ioflag));
int ourdevioctl __P((dev_t dev, u_long cmd, caddr_t data, int fflag,
struct proc *p));
int ourdev_handler __P((struct lkm_table *lkmtp, int cmd));

/*
* outdev_io结构定义在头文件common.h中,我们的设备会通过ioctl调用来获取和设置它的值.
*/

static struct ourdev_io dio;

/*
* 这里我们初始化我们的设备的操作向量
*/

cdev_decl(ourdev);
static struct cdevsw cdev_ourdev = cdev_ourdev_init(1, ourdev);


/*
* 初始化lkm接口的内部结构.第一个参数是模块名,第二个参数是设备的类型,在我的例子里标记为
* LM_DT_CHAR表示是一个字符设备.第三个参数是我们存储在cdevsw[]表中的操作结构.就象系统
* 调用的例子中一样,值为-1的话系统自动找寻空闲的位置存储.最后我们初始化cdevsw结构
*/

MOD_DEV("ourdev", LM_DT_CHAR, -1, &cdev_ourdev)

/*
* 以下的动作在设备被打开的时候执行,这里打印"hello",哈,仅做测试之用:)
*/

int
ourdevopen(dev, oflags, devtype, p)
dev_t dev;
int oflags, devtype;
struct proc *p;
{
printf("device opened, hi!n");
return(0);
}

/*
* 以下动作在设备被关闭的时候执行,这里打印一段信息
*/

int
ourdevclose(dev, fflag, devtype, p)
dev_t dev;
int fflag, devtype;
struct proc *p;
{
printf("device closed! bye!n");
return(0);
}

/*
* 定义我们设备执行的read动作,这里它把存储在内部结构ourdev_io里的string的当前值读出来
*/

int
ourdevread(dev, uio, ioflag)
dev_t dev;
struct uio *uio;
int ioflag;
{
int resid = MAXMSGLEN;
int error = 0;

do {
if (uio->uio_resid < resid)
resid = uio->uio_resid;

error = uiomove(dio.msg, resid, uio);

} while (resid > 0 && error == 0);

return(error);
}

/*
* ioctl操作的代码.这里定义了两个操作,一个负责从ourdev_io中读取当前值,一个负责设置当前值.
*/

int
ourdevioctl(dev, cmd, data, fflag, p)
dev_t dev;
u_long cmd;
caddr_t data;
int fflag;
struct proc *p;
{
struct ourdev_io *d;
int error = 0;

switch(cmd) {
case ODREAD:

d = (struct ourdev_io *)data;
d->value = dio.value;
error = copyoutstr(&dio.msg, d->msg, MAXMSGLEN - 1, NULL);

break;

case ODWRITE:

if ((fflag & FWRITE) == 0)
return(EPERM);

d = (struct ourdev_io *)data;
dio.value = d->value;
bzero(&dio.msg, MAXMSGLEN);
error = copyinstr(d->msg, &dio.msg, MAXMSGLEN - 1, NULL);

break;

default:
error = ENOTTY;
break;
}

return(error);
}

/*
* 我们的外部入口点.非常象前面介绍的系统调用的例子,用来控制模块的加载,这里和系统调用模块不
* 同的是我们在模块卸载的时候没有制定特殊的动作
*/

int
ourdev(lkmtp, cmd, ver)
struct lkm_table *lkmtp;
int cmd;
int ver;
{
DISPATCH(lkmtp, cmd, ver, ourdev_handler, lkm_nofunc, lkm_nofunc)
}

/*
* 控制加载模块的代码.我们为我们的内部结构设置一些初始值,这些值以后会被ioctl改变.它仅仅
* 在模块被加载的时候用到.
*/

int
ourdev_handler(lkmtp, cmd)
struct lkm_table *lkmtp;
int cmd;
{
struct lkm_dev *args = lkmtp->private.lkm_dev;

if (cmd == LKM_E_LOAD) {
dio.value = 13;
strncpy(dio.msg,"hello world!n", MAXMSGLEN - 1);
printf("loading module %sn", args->lkm_name);
}

return 0;
}

好了,最后我们可以用modload的-p参数来安装我们的设备模块,我可以写一个脚本来完成编译安装我们的设备的任务.脚本利用mknod在

/dev目录里面创建了一个设备,就叫
'/dev/ourdev'.在此安装脚本中,我们用模块号作为第一个参数,模块的类型作为第二个参数.如果模块是一个系统调用,我们还需要指定系统

调用号作为第三个参数这里,我
们的第三个参数是主设备号.
以下就是该安装脚本(dev-install.sh):

#!/bin/sh
MAJOR=`modstat -n ourdev | tail -1 | awk '{print $3}'`
mknod -m 644 /dev/ourdev c $MAJOR 0
echo "created device /dev/ourdev, major number $MAJOR"
ls -l /dev/ourdev

好,开始安装.
首先编译源码:
[e4gle@openbsd29]# gcc -D_KERNEL -I/sys -c chardev.c
[e4gle@openbsd29]#

安装模块:
[e4gle@openbsd29]# modload -o ourdev.o -eourdev -p ./dev-install.sh chardev.o
Module loaded as ID 0
created device /dev/ourdev, major number 29
crw-r--r-- 1 root wheel 29, 0 Jul 10 05:16 /dev/ourdev
[e4gle@openbsd29]#

看看日志确定模块是否被正常加载:
[e4gle@openbsd29]# dmesg | tail -2
loading module ourdev
DDB symbols added: 140232 bytes
[e4gle@openbsd29]#

好,我们测试一下我们新创建的设备,用dd命令来测试:
[e4gle@openbsd29]# dd if=/dev/ourdev of=/dev/fd/1 count=1 bs=100
hello world!
1+0 records in
1+0 records out
100 bytes transferred in 1 secs (100 bytes/sec)
[e4gle@openbsd29]#

现在我来通过一个测试程序来测试一下我们的ioctl调用是否工作.测试程序必须包括模块代码和头文件common.h:

#define MAXMSGLEN 100

struct ourdev_io {
int value;
char msg[MAXMSGLEN];
};

#define ODREAD _IOR('O', 0, struct ourdev_io)
#define ODWRITE _IOW('O', 1, struct ourdev_io)

#ifdef _KERNEL

/* open, close, read, ioctl */
#define cdev_ourdev_init(c,n) {
dev_init(c,n,open), dev_init(c,n,close), dev_init(c,n,read),
(dev_type_write((*))) lkmenodev, dev_init(c,n,ioctl),
(dev_type_stop((*))) lkmenodev, 0, (dev_type_select((*))) lkmenodev,
(dev_type_mmap((*))) lkmenodev }

#endif /* _KERNEL */

Now this is the program we'll use to test (chardevtest.c):
#include <sys/types.h>
#include <sys/ioctl.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <err.h>

#include "common.h"

int
main(void)
{
struct ourdev_io a;
int error, fd;

fd = open("/dev/ourdev", O_WRONLY);
if (fd == -1)
err(1, "open");

error = ioctl(fd, ODREAD, &a);
if (error == -1)
err(1, "ioctl");

printf("%d %s", a.value, a.msg);

bzero(a.msg, MAXMSGLEN);

strlcpy(a.msg, "cowsn", sizeof(a.msg));
a.value = 42;

error = ioctl(fd, ODWRITE, &a);
if (error == -1)
err(1, "ioctl");

bzero(&a, sizeof(struct ourdev_io));

error = ioctl(fd, ODREAD, &a);
if (error == -1)
err(1, "ioctl");

printf("%d %s", a.value, a.msg);

close(fd);

exit(0);
}

首先它读取存在的值,然后自己替换掉.最后它读取这个新的值并且打印出来,用来确定它们替换成功.

编译测试程序:

[e4gle@openbsd29]# gcc -o chardevtest chardevtest.c
[e4gle@openbsd29]#

运行:
[e4gle@openbsd29]# ./chardevtest
13 hello world!
42 cows
[e4gle@openbsd29]#

再用dd命令看看现在的内部字符应该是'cows'了.

★虚拟文件系统模块

增加一个虚拟文件系统是非常简单的.假如你要开发一个新的文件系统或者支持现存的文件系统,就需要写一个模块作为接口.同样的,假如需

要调试已经存在的文件系统,也需要那样
一个接口.必须确定你的内核不支持目标文件系统.

一个虚拟文件系统的模块的结构应该象如下定义:

struct lkm_vfs {
MODTYPE lkm_type;
int lkm_ver;
char *lkm_name;
u_long lkm_offset;
struct vfsconf *lkm_vfsconf;
};

和前面的例子差不多,我们也有个模块类型(LM_VFS),一个版本号,一个模块名和一个偏移值.在这个vfs模块的例子中,offset值是用不到的.

最后我们需要一个指向vfsconf结构
的指针,它包括了虚拟文件系统的操作向量以及一些其他信息(vfsconf结构在头文件/usr/include/sys/mount.h中定义).
此结构通过MOD_VFS宏来初始化:

MOD_VFS("nullfs", -1, &nullfs_vfsconf)

我们看看上面的代码,第一个参数是我们的模块名,第二个参数offset,这个参数在我们的vfs模块中无关紧要(前面说过,可以不用).最后一个参

数是我们的文件系统的结构.
在你的模块的外部接口中,你必须调用vfs_opv_init_explicit和vfs_opv_init_default来分配和初始化默认操作向量.因为文件系统被编译

进内核,所以通过定义在
/usr/src/sys/kern/vfs_conf.c里的vfs_opv_desc[]来在系统启动的时候装载.

一个需要注意的是当用需要用ld程序来链接多个源代码文件来为modload提供目标文件时,你必须用-r标记来创建一个可重定位的目标文件.

因为modload在把你的模块链接进
内核的同时需要用到ld程序.可以用modload的-d标记来察看ld运行的内部参数.
这儿是一个fs模块的完整代码 (nullmod.c):


#include <sys/param.h>
#include <sys/ioctl.h>
#include <sys/systm.h>
#include <sys/conf.h>
#include <sys/mount.h>
#include <sys/exec.h>
#include <sys/lkm.h>
#include <sys/file.h>
#include <sys/errno.h>

/*
* 文件系统的操作结构
* 参考:/usr/src/sys/miscfs/nullfs/
*/

extern struct vfsops null_vfsops;
extern struct vnodeopv_desc null_vnodeop_opv_desc;

struct vfsconf nullfs_vfsconf = {
&null_vfsops, MOUNT_NULL, 9, 0, 0, NULL, NULL
};

/*
* 声明我们的模块结构,通过我们文件系统的模块名,offset和初始的vfsconf结构
*/

MOD_VFS("nullfs", -1, &nullfs_vfsconf)

/*
* 我们的外部接口.我们初始化文件系统并且用到了DISPATCH宏,在此例中没有用到句柄
*/

int
nullfsmod(lkmtp, cmd, ver)
struct lkm_table *lkmtp;
int cmd;
int ver;
{
vfs_opv_init_explicit(&null_vnodeop_opv_desc);
vfs_opv_init_default(&null_vnodeop_opv_desc);

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

好,编译安装它:
(一些其他的附加代码在/usr/src/sys/miscfs/nullfs里)

[e4gle@openbsd29]# gcc -D_KERNEL -I/sys -c null_subr.c
[e4gle@openbsd29]# gcc -D_KERNEL -I/sys -c null_vfsops.c
[e4gle@openbsd29]# gcc -D_KERNEL -I/sys -c null_vnops.c
[e4gle@openbsd29]# gcc -D_KERNEL -I/sys -c nullmod.c
[e4gle@openbsd29]# ld -r -o nullfs.o null_vfsops.o null_vnops.o null_subr.o nullmod.o
[e4gle@openbsd29]# modload -o nullfsmod -enullfsmod nullfs.o
[e4gle@openbsd29]# modstat
Type Id Off Loadaddr Size Info Rev Module Name
VFS 0 -1 e0b84000 0003 e0b860d0 2 nullfs
[e4gle@openbsd29]#

ok,虚拟文件系统模块就说到这.

★其他类型的模块

这些模块被用来执行一些预定的模块类型所没有定义的操作.在我这个例子中我们将为网络协议栈里加入控制代码,然后打印出我们接收到的

tcp包的一些信息.

当我们在书写其他类型的模块时,我们必须要完整的检查一遍,确定没有预定的操作.例如,同样的操作模块不能被加载两次.这等于我们在往内

核中去写入模块.当然,
我们都会在模块加载和卸载的控制函数里去控制.

一个其他类型的模块结构象下面这样定义:

struct lkm_misc {
MODTYPE lkm_type;
int lkm_ver;
char *lkm_name;
u_long lkm_offset;
};

同样,我们首先有一个模块的类型(在这个例子中试LM_MISC),然后是lkm的版本,再接着是模块名和offset的值.在我的这个例子中offset值

没有用到,但在/usr/share/lkm/misc
提供的例子中(增加一个系统调用)offset被用来在系统调用表里面标记一个新的系统调用的位置.
用MOD_MISC宏来初始化该结构:

MOD_MISC("tcpinfo")

这里只有一个参数,指定了模块名.
当我们的模块被加载后,该模块把tcp_input函数的指针改为我们制定的new_input函数.新的函数会打印出mbuf里的包头的一些信息,然后

再调用原来的tcp_input函数.在做这些
之前,我们一定要确定同类的模块没有被加载.

对于这个模块一些值得注意的地方:首先运行这个模块时不适合传输大量的tcp包,printf()会变的很慢.大家试一下就知道.这个例子只是做测

试之用,其实大家可以想想我们既然
可以改变tcp协议栈里的函数指针,我们用模块来做一个tcp的内核后门也应该很容易,就留给大家思考吧,呵呵.第二,此代码原来是运行在

freebsd之上的,稍微修改了一下而已,
bsd系列的内核真是很相像.

以下是该模块的完整代码(tcpmod.c):

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/mbuf.h>
#include <sys/exec.h>
#include <sys/conf.h>
#include <sys/lkm.h>
#include <sys/socket.h>
#include <sys/protosw.h>
#include <net/route.h>
#include <net/if.h>
#include <netinet/in.h>
#include <netinet/in_systm.h>
#include <netinet/ip.h>
#include <netinet/in_pcb.h>

/*
* 我们将改变protosw结构中的TCP入口.
*/

extern struct protosw inetsw[];

/*
* 我们自定义的函数
*/

extern int lkmexists __P((struct lkm_table *));
extern char *inet_ntoa __P((struct in_addr));

static void new_input __P((struct mbuf *, ...));
static void (*old_tcp_input) __P((struct mbuf *, ...));

/*
* 声明我们的模块结构
*/

MOD_MISC("tcpinfo")

/*
* 我们的句柄函数,用来加载和卸载模块.
*/

int
tcpmod_handler(lkmtp, cmd)
struct lkm_table *lkmtp;
int cmd;
{
int s;

switch(cmd) {

case LKM_E_LOAD:

/*
* 确定此模块是第一次加载使用
*/

if (lkmexists(lkmtp))
return(EEXIST);

/*
* 阻赛网络协议进程,我们把tcp_input函数指针改成我们自己的包装函数.
*/

s = splnet();
old_tcp_input = inetsw[2].pr_input;
inetsw[2].pr_input = new_input;
splx(s);

break;

case LKM_E_UNLOAD:

/*
* 当模块退出时返回原来的结构
*/

s = splnet();
inetsw[2].pr_input = old_tcp_input;
splx(s);

break;
}

return(0);
}

/*
* 我们的外部接口,没有做什么,用到了DISPATCH宏
*/

int
tcpinfo(lkmtp, cmd, ver)
struct lkm_table *lkmtp;
int cmd;
int ver;
{
DISPATCH(lkmtp, cmd, ver, tcpmod_handler, tcpmod_handler, lkm_nofunc)
}

/*
* 定义我们自己的包装的tcp_input函数.假如mbuf里有包头,则打印出网络接口接收到的包
* 的总长度以及包的源地址.然后使原来的tcp_input函数正常运行.
*/

static void
new_input(struct mbuf *m, ...)
{
va_list ap;
int iphlen;
struct ifnet *ifnp;
struct ip *ip;

va_start(ap, m);
iphlen = va_arg(ap, int);
va_end(ap);

if (m->m_flags & M_PKTHDR) {
ifnp = m->m_pkthdr.rcvif;
ip = mtod(m, struct ip *);
printf("incoming packet: %d bytes ", m->m_pkthdr.len);
printf("on %s from %sn", ifnp->if_xname, inet_ntoa(ip->ip_src));

}

(*old_tcp_input)(m, iphlen);

return;
}

好,我们编译安装它:
[e4gle@openbsd29]# gcc -D_KERNEL -I/sys -c tcpmod.c
[e4gle@openbsd29]# modload -o tcpinfo.o -etcpinfo tcpmod.o

产生一些tcp连接,用dmesg来看看是否正常工作:
[e4gle@openbsd29]# dmesg | tail -3
incoming packet: 1500 bytes on ne3 from 129.128.5.191
incoming packet: 1205 bytes on ne3 from 129.128.5.191
incoming packet: 52 bytes on ne3 from 129.128.5.191
[e4gle@openbsd29]#

ok,到这里结束,足以说明问题了.

★结束语

写这篇文章的目的还是为了让大家如们bsd系列的内核编程,驱动程序编程的入门,当然,作为一个网络安全的专业人员应该可以从这篇文章里

面看到一些东西,就是一些内核级别
的后门和截获技术,例如,我们可以通过增加和重定向系统调用的模块来截获系统调用,我们可以用刚才的最后一种模块来做一个内核级别的

tcp后门等等.当然我们还可以利用
模块来制作一些内核级的安全工具.发挥想象力,留给大家了,呵呵.

★参考资料

lkm(4), modload(8), modstat(8), modunload(8)
/usr/src/sys/kern/kern_lkm.c
/usr/src/sys/sys/lkm.h
/usr/share/lkm