OpenBSD可加載內核模塊編程完全向導
發表時間:2024-01-18 來源:明輝站整理相關軟件相關文章人氣:
[摘要]緒論 這篇文章我說明在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_longlkm_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_longlkm_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 rootwheel 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_longlkm_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_longlkm_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 intlkmexists __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后門等等.當然我們還可以利用
模塊來制作一些內核級的安全工具.發揮想象力,留給大家了,呵呵