文章分类 | 推荐文章 | 最新文章 | 热点文章 | 最新软件 | 精品软件 | 下载排行 | 推荐下载 | 免费看大片 | WPS | 杀毒软件
清风网络
首 页 软件下载 网络学院 数码学院
QQ 电脑入门 游戏 操作系统 图形处理 办公软件 媒体动画 精文荟萃 工具软件 网络编程 程序开发 网络技术 认证考试 网站建设 文章专栏
当前位置:清风网络学院网络编程PHPOpenBSD可加载内核模块编程完全指南
精品推荐
特别推荐
·菜鸟学PHP之Smarty入门
·PHP环境安全配置教程
·PHP入门指导:如何学习PHP?
·Php+Mysql注入专题
·PHP:实现给上传图片加水印的程序代码
·PHP动态网站开发中常用的8个小技巧
·PHP初学者:配置Apache服务器并且设置DNS
·初学PHP指导:php.ini 配置详细选项
·好文分享:PHP入门的学习方法
·急速搭建属于自己的Discuz!6论坛
·PHP入门:初学来看配置文件PHP.INI的中文注释
·php.ini配置,php.ini 中的 php-5.2.0 配置指令详解
·总结:一些PHP学习过程中的心得和经验
·新手入门:JSP初学者必须掌握的语法点
·PHP技巧:PHP脚本编程中的文件系统函数库
·详细讲解PHP编程中分页显示的制作
·成为PHP编程高手应该遵循的三个准则
·谈谈生成静态页面的一些经验
·新手入门:详细解述七个软件开发的原则
·简单的webservice开发例子
热点TOP10
·显示照片exif信息PHP代码
·Windows XP下PHP+MySQL环境搭建
·EasyJWeb Tools业务引擎中分页的设计及实现
·PHP环境下配置在线编辑器FCKeditor
·PHP5对Mysql5的任意数据库表的管理代码示例(三)
·初学者学习PHP开发应该掌握的几段精华代码
·win2003 iis整合php后 环境变量无法读取到
·SQL jdbc解决自动自动增长列统一处理问题纪实
·JDBC连接Oracle数据库常见问题及解决方法
·彻底解决中文名文件下载和下载文件内容乱码问题
·用JFreeChart画统计分析柱状图
·技巧文集:php的mysql性能优化
·动态网页制作学习:PHP预定义变量
·php下免费的加速器,PHP Accelerator
·模板用xml的思路
·用PHP模拟登陆
·PHP中轻型的数据库抽象类:ADODB Lite
·表单验证类 Validator for PHP
·PHP 会话 (Session) 使用入门
·技巧文集:PHP如何禁止图片文件的被盗链

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

日期:2005年3月18日 作者:清风网络学院 查看:[大字体 中字体 小字体]



LINUX安全管理的基本技巧=版权所有  软件 下载  学院  版权所有=
中华信息阵地 http:<!--StartFragment-->OpenBSD可加载内核模块编程完全指南
整理:e4gle(大鹰)
来源: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 //www.cnxxz.net 作者:greenice
Linux作为是一个开放源代码的免费操作系统,以其高效隐定的优秀品质,越来越受到用户们的欢迎,并在全世界不断普及开来。相信在不久的将来Linux还会得到更大更快的发展。虽然,Linux和Unix很相似,但是它们之间还是有不少重要的差别。对于很多习惯了UNIX和Windows的系统管理员来讲,如何保证Linux操作系统的安全可靠将面临许多新的挑战。本文在此将给大家介绍一些Linux管理安全的基本技巧,希望能对大家有所帮助(甚感欣慰)。
首先我想从系统的安全配置,开始我们的话题。因为我个人认为一位管理员要能充分利用系统提供的安全机制、挖掘系统自身的潜力来对服务器进行高效安全的维护,才能称得上优秀。我并不是完全反对使用防火墙等工具,但是正如人一样,我们可以消毒、可以带口罩,却没有我们自身体魄强健、有抗体来得好。Linux是完全开放源代码的免费操作系统,其可开发的潜力极大,有能力的管理员甚至可以通过自行改编内核来满足自己服务器优良工作的需要。当然,在此我们只讲一些基本,但实用的配置技巧。
一、ILO的安全设置
LILO是Linux Loader的缩写,它是LINUX的启动模块!
。我们可通过修改“/etc/lilo.conf”文件中的内容来对它进行配置。在文件中加上,如下两个参数:restricted,password。这两个参数可以使你的系统在启动lilo时就要求密码验证。
boot=/dev/hda
  map=/boot/map
  install=/boot/boot.b
  prompt
  timeout=00 #把这行该为00,这样系统启动时将不在等待,而直接启动LINUX
  message=/boot/message
  linear
  default=linux
  restricted #加入这行
  password= #加入这行并设置自己的密码
  image=/boot/vmlinuz-2.4.2-2
  label=linux
  root=/dev/hda6
  read-only
因为"/etc/lilo.conf"文件中包含明文密码,所以要把它设置为root权限读取。
  # chmod 0600 /etc/lilo.conf
还要使用“chattr”命令使"/etc/lilo.conf"文件变为不可改变。
  # chattr +i /etc/lilo.conf
  这样可以对“/etc/lilo.conf”文件起到很好的保护作用。(对其它文件的保护也可以采用此方法)
最后要使lilo.conf文件生效要用
# /sbin/lilo -v
&nbs!
p;更新一下系统。
二、口令安全
口令可以说是系统的第一道防!
线,目前
网络上大部分的系统入侵都是从猜测口令或者截获口令开始的,所以口令安全至关重要。
首先要杜绝不设口令的帐号存在。这可以通过查看/etc/passwd文件来发现。例如,
存在用户名为test的帐号,没有设置口令,则在/etc/passwd文件中就有如下一行:
test::100:9::/home/test:/bin/bash
其第二项为空,说明test这个帐号没有设置口令,这是非常危险的!应将该类帐号删除或者设置口令。
其次,在旧版本的linux中,在/etc/passwd文件中是包含有加密的密码的,这就给系统的安全性带来了很大的隐患,最简单的方法就是可以用暴力破解的方法来获得口令(如,用John等工具)。可以使用命令/usr/sbin/pwconv或者/usr/sbin/grpconv来建立/etc/shadow或者/etc/gshadow文件,这样在/etc/passwd文件中不再包含加密的密码,而是放在/etc/shadow文件中,该文件只有超级用户root可读!
第三点是修改一些系统帐号的Shell变量,例如uucp,ftp和news等,还有一些仅仅需要FTP功能的帐号,一定不要给他们设置/bin/bash或者/bin/sh等Shell变量。可以在/etc/passwd中将它们的Shell变量置空,例如设为/bin/false或者/dev/null等,也可以使用usermod!
-s /dev/null username命令来更改username的Shell为/dev/null。这样使用这些帐号将无法Telnet远程登录到系统中来!
第四点是修改缺省的密码长度:在你安装linux时默认的密码长度是5个字节。但这并不够,要把它设为8。修改最短密码长度需要编辑login.defs文件(vi/etc/login.defs),把下面这行
  PASS_MIN_LEN 5
  改为
  PASS_MIN_LEN 8
  login.defs文件是login程序的配置文件。
最后别忙了为root加上一个强壮的密码,8位以上,最好包含特殊字符。
三、登录安全
1、自动注销帐号的登录,在unix系统中root账户是具有最高特权的。如果系统管理员在离开系统之前忘记注销root账户,那将会带来很大的安全隐患,应该让系统会自动注销。通过修改账户中“TMOUT”参数,可以实现此功能。TMOUT按秒计算。编辑你的profile文件(vi /etc/profile),在"HISTFILESIZE="后面加入下面这行:
  TMOUT=300
  300,表示300秒,也就是表示5分钟。这样,如果系统中登陆的用户在5分钟内都没有动作,那么系统会自动注销这个账户。你可以在个别用户的“.bashrc”文件中添加该值,以便系统对该用?
敌刑厥獾淖远⑾奔洹?
  改变这项设置后,必须先注销用!
户,再用
该用户登陆才能激活这个功能。
2、使用PAM(可插拔认证模块)禁止任何人通过su命令改变为root用户su(Substitute
  User替代用户)命令允许你成为系统中其他已存在的用户。如果你不希望任何人通过su命令改变为root用户或对某些用户限制使用su命令,你可以在su配置文件(在"/etc/pam.d/"目录下)的开头添加下面两行:
编辑su文件(vi /etc/pam.d/su),在开头添加下面两行:
  auth sufficient /lib/security/pam_rootok.so
  auth required /lib/security/Pam_wheel.so group=wheel
  这表明只有"wheel"组的成员可以使用su命令成为root用户。你可以把用户添加到“wheel”组,以使它可以使用su命令成为root用户。添加方法可以用这个命令:chmod -G10 username 。

  四、控制台访问安全
1、取消普通用户的控制台访问权限,你应该取消普通用户的控制台访问权限。
比如shutdown、reboot、halt等命令。
  # rm -f /etc/security/console.apps/
  是你要注销的程序名。
2、不允许从不同的控制台进行root登陆
  "/etc/securetty"文件允许你定义root用户可以从那个TTY设?
傅锹健D憧梢员嗉?quot;/etc/securetty"文件,再不需要登陆的TTY设备前添加“#”标志,来禁止从该TTY设备进行root登陆。
  在/etc/inittab文件中有如下一段话:
  # Run gettys in standard runlevels
  1:2345:respawn:/sbin/mingetty tty1
  2:2345:respawn:/sbin/mingetty tty2
  #3:2345:respawn:/sbin/mingetty tty3
  #4:2345:respawn:/sbin/mingetty tty4
  #5:2345:respawn:/sbin/mingetty tty5
  #6:2345:respawn:/sbin/mingetty tty6
  系统默认的可以使用6个控制台,即Alt+F1,Alt+F2...,这里在3,4,5,6前面加上“#”,注释该句话,这样现在只有两个控制台可供使用,最好保留两个。然后重新启动init进程,改动即可生效!

五、服务安全
取消并反安装所有不用的服务
  取消并反安装所有不用的服务,这样你的担心就会少很多。察看“/etc/inetd.conf”文件,通过注释取消所有你不需要的服务(在该服务项目之前加一个“#”)。然后用“sighup”命令升级“inetd.conf”文件。
第一步:
  更改“/etc/inetd.conf”权限为600,只允许root来读写该文件!

  # chmod 600 /etc/inetd.conf
  第二步:
 !
 确定“
/etc/inetd.conf”文件所有者为root。
  第三步:
  编辑 /etc/inetd.conf文件(vi /etc/inetd.conf),取消下列服务(你不需要的):ftp, telnet, shell, login, exec, talk, ntalk, imap, pop-2, pop-3, finger, auth等等。把不需要的服务关闭可以使系统的危险性降低很多。

  第四步:
  给inetd进程发送一个HUP信号:
  # killall -HUP inetd
  第五步:
  用chattr命令把/ec/inetd.conf文件设为不可修改,这样就没人可以修改它:
  # chattr +i /etc/inetd.conf
   这样可以防止对inetd.conf的任何修改(意外或其他原因)。唯一可以取消这个属性的人只有root。如果要修改inetd.conf文件,首先要是取消不可修改性质:
  # chattr -i /etc/inetd.conf
  同时修“/etc/services”文件的属性,防止未经许可的删除或添加服务:
  # chattr +i /etc/services
别忘了以后要修改时,再把它们的性质改为可修改的就行了。
六、其它综合设置安全
1、TCP_WRAPPERS
  使用TCP_WRAPPERS可以使你的系统安全面对外部入侵。最好的策略就是阻止所有的主机("/etc/hosts.deny!
"文件中加入"ALL: ALL@ALL, PARANOID" ),然后再在"/etc/hosts.allow" 文件中加入所有允许访问的主机列表。
  第一步:
  编辑hosts.deny文件(vi /etc/hosts.deny),加入下面这行
  # Deny access to everyone.
  ALL: ALL@ALL, PARANOID
  这表明除非该地址包在允许访问的主机列表中,否则阻塞所有的服务和地址。
  第二步:
  编辑hosts.allow文件(vi /etc/hosts.allow),加入允许访问的主机列表,比如:
  ftp: 202.54.15.99 foo.com
  202.54.15.99和 foo.com是允许访问ftp服务的ip地址和主机名称。
  第三步:
  tcpdchk程序是tepd wrapper设置检查程序。它用来检查你的tcp wrapper设置,并报告发现的潜在的和真实的问题。设置完后,运行下面这个命令:
  # tcpdchk
  2、修改“/etc/host.conf”文件
  “/etc/host.conf”说明了如何解析地址。编辑“/etc/host.conf”文件(vi /etc/host.conf),加入下面这行:
  # Lookup names via DNS first then fall back to /etc/hosts.
  order bind,hosts
  # We have mach!
ines with multiple IP addresses.
  multi on
  # Ch!
eck for
IP address spoofing.
  nospoof on
  第一项设置首先通过DNS解析IP地址,然后通过hosts文件解析。第二项设置检测是否“/etc/hosts”文件中的主机是否拥有多个IP地址(比如有多个以太口网卡)。第三项设置说明要注意对本机未经许可的电子欺骗。
  3、Shell logging Bash
  shell在“~/.bash_history”(“~/”表示用户目录)文件中保存了500条使用过的命令,这样可以使你输入使用过的长命令变得容易。每个在系统中拥有账号的用户在他的目录下都有一个“.bash_history”文件。bash
shell应该保存少量的命令,并且在每次用户注销时都把这些历史命令删除。
  第一步:
  “/etc/profile”文件中的“HISTFILESIZE”和“HISTSIZE”行确定所有用户的“.bash_history”文件中可以保存的旧命令条数。强烈建议把把“/etc/profile”文件中的“HISTFILESIZE”和“HISTSIZE”行的值设为一个较小的数,比如30。编辑profile文件(vi/etc/profile),把下面这行改为:
  HISTFILESIZE=30
  HISTSIZE=30
  这表示每个用户的“.bash_history”文件只可以保存30条旧命令。
  第二步:
  网管还应该在"/e!
tc/skel/.bash_logout" 文件中添加下面这行"rm -f $HOME/.bash_history" 。这样,当用户每次注销时,“.bash_history”文件都会被删除。
  编辑.bash_logout文件(vi /etc/skel/.bash_logout) ,添加下面这行:
  rm -f $HOME/.bash_history
  4、禁止Control-Alt-Delete键盘关闭命令
  在"/etc/inittab" 文件中注释掉下面这行(使用#):
  ca::ctrlaltdel:/sbin/shutdown -t3 -r now
  改为:
  #ca::ctrlaltdel:/sbin/shutdown -t3 -r now
  为了使这项改动起作用,输入下面这个命令:
  # /sbin/init q

  5、给"/etc/rc.d/init.d" 下script文件设置权限
  给执行或关闭启动时执行的程序的script文件设置权限。
  # chmod -R 700 /etc/rc.d/init.d/*
  这表示只有root才允许读、写、执行该目录下的script文件。
6、隐藏系统信息
  在缺省情况下,当你登陆到linux系统,它会告诉你该linux发行版的名称、版本、内核版本、服务器的名称。对于黑客来说这些信息足够它入侵你的系统了。你应该只给它显示一!
个“login:”提示符。
  首先编辑"/etc/rc.d/rc.local&!
quot; 文
件,在下面显示的这些行前加一个“#”,把输出信息的命令注释掉。
  # This will overwrite /etc/issue at every boot. So, make any changes you
  # want to make to /etc/issue here or you will lose them when you reboot.
  #echo "" > /etc/issue
  #echo "$R" >> /etc/issue
  #echo "Kernel $(uname -r) on $a $(uname -m)" >> /etc/issue
  #
  #cp -f /etc/issue /etc/issue.net
  #echo >> /etc/issue
  其次删除"/etc"目录下的“isue.net”和"issue"文件:
  # rm -f /etc/issue
  # rm -f /etc/issue.net
  7、禁止不使用的SUID/SGID程序
  如果一个程序被设置成了SUID
  root,那么普通用户就可以以root身份来运行这个程序。网管应尽可能的少使用SUID/SGID 程序,禁止所有不必要的SUID/SGID程序。
  查找root-owned程序中使用s位的程序:
  # find / -type f ( -perm -04000 -o -perm -02000 ) -exec ls -lg {} ;
  用下面命令禁止选中的带有s位的程序:
  # chmod a-s [program!
]

以上是一些基本的安全设置技巧,俗话说:“道高一尺,魔高一丈”。只要是连上网的计算机,就有可被入侵。因此系统的定期检查和维护是相当重要的,对于及时发现入侵很有帮助,有助于我们亡羊补牢,赶在入侵者还没有破坏系统和数据之前把它们清理出去。所以接着就给大家讲一下这方面的技巧。
一、优化分区结构
这对维护很有好处,我们应该把Linux的文件系统分成几个主要的分区,每个分区分别进行不同的配置和安装,一般情况下至少要建立/、/usr/local、/var和/home等分区。/usr可以安装成只读并且可以被认为是不可修改的。如果/usr中有任何文件发生了改变,那么系统将立即发出安全报警。当然这不包括用户自己改变/usr中的内容。/lib、/boot和/sbin的安装和设置也一样。在安装时应该尽量将它们设置为只读,并且对它们的文件、目录和属性进行的任何修改都会导致系统报警。
  当然将所有主要的分区都设置为只读是不可能的,有的分区如/var等,其自身的性质就决定了不能将它们设置为只读,但应该不允许它具有执行权限。
二、保护log文件
当与log文件和log备份一起使用时不可变和只添加这两种文件属性特别有用。这通常需要在log更新脚本中添加一些控制命令

(出处:清风网络学院






上一篇:通过/proc和gdb来实现感染进程的病毒程序分析

下一篇:关于多操作系统共存的研究

OpenBSD可加载内核模块编程完全指南 相关文章:
·socket编程原理
·魔兽争霸3完全作弊码
·红色警报2 完全秘技
·Windows系统进程列表完全解析
·完全记录攻略--轻松刻录不求人
·MPEG、RM、WMV电影文件格式转换指南
·QQ空间大图模块:性感蔡依林
·QQ空间最新大图模块:谁都不是谁的谁
·QQ空间动漫大图模块:火影忍者系列【7】
·从零开始学黑客:网络黑客新手入门指南
OpenBSD可加载内核模块编程完全指南 相关软件:
·完全精通局域网手册(PDF)
·Adobe Photoshop CS 2 简体中文使用指南
·盟军敢死队II(Commandos 2)完全图文并茂攻略
·创业完全手册
·启动光盘制作完全手册(菜鸟先飞 图文教材系列)
·最好的Photoshop6完全自学手册第二章
·winxp sp2 完全优化版 GHO 免刻录直接克隆
·极品飞车 9 头号通缉黑名单 完全版
·炒股赚钱一本通(完全版)
·班得瑞全集 9张专辑完全系列

特别声明:本站除部分特别声明禁止转载的专稿外的其他文章可以自由转载,但请务必注明出处和原始作者。文章版权归文章原始作者所有。对于被本站转载文章的个人和网站,我们表示深深的谢意。如果本站转载的文章有版权问题请联系编辑人员,我们尽快予以更正。
[打印本页] [关闭窗口] 转载请注明来源:http://www.vipcn.net
| 帮助(?) | 版权声明 | 友情连接 | 关于我们 | 信息发布
Copyright 2007 www.vipcn.net All Rights Reserved. 鄂ICP备05000083号Powered by:viphot