1可装载模块
1.1 模块事件处理程序
1.2 DECLARE_MODULE 宏
1.3 "Hello, world!"
1.4 系统调用模块
1.4.1 系统调用函数
1.4.2 sysent 结构
1.4.3 Offset 值
1.4.4 SYSCALL_MODULE 宏
1.4.5 示例
1.4.6 modfind 函数
1.4.7 modstat 函数
1.4.8 syscall函数
1.4.9 执行系统调用
1.4.10不用C代码执行系统调用的方法
1.5 Kernel/User 空间数据交换
1.5.1 copyin 和copyinstr 函数
1.5.2 copyout 函数
1.5.3 copystr 函数
1.6 字符设备模块
1.6.1 cdevsw 结构
1.6.2 字符设备函数
1.6.3 设备注册例程
1.6.4 例子
1.6.5 测试字符设备
1.7 链接文件和模块
1.8 小结
1
可装载内核模块
The
simplest way to introduce code into a running kernel is through a
loadable kernel module (LKM), which is a kernel subsystem that can be
loaded and unloaded after bootup,allowing a system administrator to
dynamically add and remove functionality from a live system. This makes
LKMs an ideal platform for kernel-mode rootkits. In fact, the vast
majority of modern rootkits are simply LKMs.
向一个正在运行中的内核插入代码,最简易的途径是通过可装载内核模块(LKM)。LKM是内核的一种子系统,它可以在系统启动后进行装载或卸载。这样系统管理员可以动态地增加或去除运行中的操作系统中子功能模块。这使得LKM成了实现内核模式的理想平台。实际上,大多数现代的rootkit都是LKM.
NOTE In
3.0, substantial changes were made to the kernel module subsystem, and
the LKM Facility was renamed the Dynamic Kernel Linker (KLD) Facility.
Subsequently, the term KLD is commonly used to describe LKMs under
FreeBSD.
注意 在FreeBSD 3.0中, 内核模块子系统发生了实质性的变化,并且LKM工具改称为动态内核链接器(KLD)工具。之后,在FreeBSD系统中普遍用术语KLD来描述LKM。
In this chapter we’ll discuss LKM (that is, KLD) programming within FreeBSD for programmers to kernel hacking.
在以后的章节中,我们讨论在FreeBSD环境中的LKM(也就是 KLD)编程。本教程面向内核新手。
NOTE Throughout this book, the terms device driver, KLD, LKM, loadable module, and module are all used interchangeably.
注意 在本书中交替使用设备驱动程序,KLD,LKM,可装载模块,还有模块这些术语.
1.1 Module Event Handler
1.1 模块事件处理程序
Whenever
a KLD is loaded into or unloaded from the kernel, a function known as
the module event handler is called. This function handles the
initialization and shutdown routines for the KLD. Every KLD must
include an event handler.1 The prototype for the event handler function
is defined in the header as follows:
无论什么时候,一个KLD被装载进内核或从内核中卸载时,必须调用一个被称为模块事件处理程序的函数。这个函数为KLD执行初始化和关闭例子程。每个KLD必须包含一个事件处理程序.(注1)事件处理程序函数的原型在 中定义如下:
--------------------------------------------------------------------------------
typedef int (*modeventhand_t)(module_t, int /* modeventtype_t */, *);
--------------------------------------------------------------------------------
where module_t is a pointer to a module structure and modeventtype_t is defined in the header as follows:
module_t 是指向module结构体的,modeventtype_t 在 头文件中定义如下:
--------------------------------------------------------------------------------
typedef enum modeventtype {
MOD_LOAD, /* Set when module is loaded. */
MOD_UNLOAD, /* Set when module is unloaded. */
MOD_SHUTDOWN, /* Set on shutdown. */
MOD_QUIESCE /* Set on quiesce. */
} modeventtype_t;
--------------------------------------------------------------------------------
Here is an example of an event handler function:
下面是一个事件处理程序函数的例子:
--------------------------------------------------------------------------------
static int
load(struct module *module, int cmd, void *arg)
{
int error = 0;
switch (cmd) {
case MOD_LOAD:
uprintf("Hello, world!\n");
break;
case MOD_UNLOAD:不
uprintf("Good-bye, cruel world!\n");
break;
default:
error = EOPNOTSUPP;
break;
}
return(error);
}
--------------------------------------------------------------------------------
----------------------
1
Actually, this isn’t entirely true. You can have a KLD that just
includes a sysctl. You can also dispense with module handlers if you
wish and just use SYSINIT and SYSUNINIT directly to register functions
to be invoked on load and unload, respectively. You can’t, however,
indicate failure in those.
实际上,这不完全正确。只要包含sysctl就可以编写KLD.只要你愿意,你也可以省去模块处理程序,仅仅分别利用SYSINIT和SYSUNINIT直接去注册在装载和卸载KLD时被调用的函数。然而,你无法利用它们指示错误。
This
function will print “Hello, world!” when the module loads, “Goodbye,
cruel world!” when it unloads, and will return with an error
(EOPNOTSUPP)2 on shutdown and quiesce.
在装载模块时,这个函数将打印出"Hello, world!".当卸载模块时,打印出"Goodbye, cruel world!"。在shutdown和quiesce时返回错误((EOPNOTSUPP)(注2)。
1.2 DECLARE_MODULE 宏
When
a KLD is loaded (by the kldload(8) command, described in Section 1.3),
it must link and register itself with the kernel. This can be easily
accomplished by calling the DECLARE_MODULE macro, which is defined in
the header as follows:
装载KLD时(通过命令kldload(8), 在1.3节介绍),它必须把自己链接以及注册到内核中。这通过调用 DECLARE_MODULE 宏可以轻松地完成。DECLARE_MODULE 宏在 头文件中定义如下:
--------------------------------------------------------------------------------
#define DECLARE_MODULE(name, data, sub, order) \
MODULE_METADATA(_md_##name, MDT_MODULE, &data, #name); \
SYSINIT(name##module, sub, order, module_register_init, &data) \
struct __hack
--------------------------------------------------------------------------------
Here is a brief description of each parameter:
下面是各个参数的简要描述:
name
This specifies the generic module name, which is passed as a character string.
它指定普通的模块名称,作为字符串传递。
data
This
parameter specifies the official module name and event handler
function, which is passed as a moduledata structure. struct moduledata
is defined in the header as follows:
这个参数指定正式的模块名称和事例处理程序。它作为moduledata 结构进行传递。moduledata 结构在头文件中定义如下:
--------------------------------------------------------------------------------
typedef struct moduledata {
const char *name; /* module name */
modeventhand_t evhand; /* event handler */
void *priv; /* extra data */
} moduledata_t;
--------------------------------------------------------------------------------
sub
This
specifies the system startup interface, which identifies the module
type. Valid entries for this parameter can be found in the
header within the sysinit_sub_id enumeration list.
它指定系统启动接口,定义了模块的类型。这个参数的有效项可以通 头文件中的sysinit_sub_id枚举列表中查看。
For our purposes, we’ll always set this parameter to SI_SUB_DRIVERS, which is used when registering a device driver.
根据我们的目的,我们总是设置这个参数为SI_SUB_DRIVERS。SI_SUB_DRIVERS是在注册一个设备驱动程序时使用的。
order
This
specifies the KLD’s order of initialization within the subsystem.
You’ll find valid entries for this parameter in the
header within the sysinit_elem_order enumeration
list.
这个参数指定模块在模块子系统中初始化的次序。你可以在头文件中的sysinit_elem_order 枚举列表中查看它的有效项。
For our purposes, we’ll always set this parameter to SI_ORDER_MIDDLE, which will initialize the KLD somewhere in the middle.
根据我们的目的,我们总是设置这个参数为SI_ORDER_MIDDLE,它在KLDP初始化过程中处于中间位置。
-------------------
2 EOPNOTSUPP 代表错误: 不支持的操作.
1.3 "Hello, world!"
You now know enough to write your first KLD. Listing 1-1 is a complete “Hello, world!” module.
现在,你完全可以编写你第一个KLD了。清单 1-1 是一个完整的"Hello, world!"模块。
--------------------------------------------------------------------------------
#include
#include
#include
#include
/* The function called at load/unload. */
static int
load(struct module *module, int cmd, void *arg)
{
int error = 0;
switch (cmd) {
case MOD_LOAD:
uprintf("Hello, world!\n");
break;
case MOD_UNLOAD:
uprintf("Good-bye, cruel world!\n");
break;
default:
error = EOPNOTSUPP;
break;
}
return(error);
}
/* The second argument of DECLARE_MODULE. */
static moduledata_t hello_mod = {
"hello", /* module name */
load, /* event handler */
NULL /* extra data */
};
DECLARE_MODULE(hello, hello_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
--------------------------------------------------------------------------------
Listing 1-1: hello.c
清单 1-1: hello.c
As
you can see, this module is simply a combination of the sample event
handler function from Section 1.1 and a filled-out DECLARE_MODULE
macro. To compile this module, you can use the system Makefile3
bsd.kmod.mk.
Listing 1-2 shows the complete Makefile for hello.c.
正如你看到的,这个模块是1.1节的事件处理程序和填充好的DECLARE_MODULE宏两者组合而成的。为了编译这个模块,你可以使用系统Makefile3 bsd.kmod.mk.
清单 1-2 显示了完整的hello.c文件的Makefile.
3
A Makefile is used to simplify the process of converting a
file or files from one form to another by describing the dependencies
and build scripts for a given output. For more on Makefiles, see the
make(1) manual page.
------------------------
3 A
Makefile is used to simplify the process of converting a file or files
from one form to another by describing the dependencies and build
scripts for a given output. For more on Makefiles, see the make(1)
manual page.
--------------------------------------------------------------------------------
KMOD= hello # Name of KLD to build.
SRCS= hello.c # List of source files.
.include
--------------------------------------------------------------------------------
清单 1-2: Makefile
NOTE
Throughout this book, we’ll adapt this Makefile to compile every KLD by
filling out KMOD and SRCS with the appropriate module name and source
listing(s), respectively.
注意 本书中,我们分别用适当的模块名称和代码列表填充这个Makefile文件中KMOD和SPCS,并用这个Makefile来编译各个KLD.
Now,
assuming the Makefile and hello.c are in the same directory, simply
type make and (if we haven’t botched anything) the compilation should
proceed—very verbosely—and produce an executable file named hello.ko,
as shown here:
现在,假设Makefile和hello.c位于同个文件夹里,简单地敲打make(如果我们没有候补任何东西),编译将会进行,非常明显,并产生一个名为hello.ko的可执行文件。编译过程如下:
--------------------------------------------------------------------------------
$ make
Warning: Object directory not changed from original /usr/home/ghost/hello
@ -> /usr/src/sys
machine -> /usr/src/sys/i386/include
cc -O2 -pipe -funroll-loops -march=athlon-mp -fno-strict-aliasing -Werror -D_
KERNEL -DKLD_MODULE -nostdinc -I- -I. -I@ -I@/contrib/altq -I@/../include -
I/usr/include -finline-limit=8000 -fno-common -mno-align-long-strings -mpref
erred-stack-boundary=2 -mno-mmx -mno-3dnow -mno-sse -mno-sse2 -ffreestanding
-Wall -Wredundant-decls -Wnested-externs -Wstrict-prototypes -Wmissing-prot
otypes -Wpointer-arith -Winline -Wcast-qual -fformat-extensions -std=c99 -c
hello.c
ld -d -warn-common -r -d -o hello.kld hello.o
touch export_syms
awk -f /sys/conf/kmod_syms.awk hello.kld export_syms | xargs -J% objcopy % h
ello.kld
ld -Bshareable -d -warn-common -o hello.ko hello.kld
objcopy --strip-debug hello.ko
$ ls –F
@@ export_syms hello.kld hello.o
Makefile hello.c hello.ko* machine@
--------------------------------------------------------------------------------
You can load and unload hello.ko with the kldload(8) and kldunload(8) utilities,4 as shown below:
利用 kldload(8) 和 kldunload(8)工具,就可以装载或卸载hello.ko。如下:
--------------------------------------------------------------------------------
$ sudo kldload ./hello.ko
Hello, world!
$ sudo kldunload hello.ko
Good-bye, cruel world!
--------------------------------------------------------------------------------
Excellent—you have successfully loaded and unloaded code into a running kernel. Now, let’s try something a little more advanced.
棒极了--你已经成功地把代码装载到一个运行中的内核中并又把它卸载掉。现在,让我们尝试一下稍稍高级一点的东西。
-----------------------
4
With a Makefile that includes , you can also use
make load and make unload to load and unload the module once you have
built it.
1.4 System Call Modules
1.4 系统调用模块
System
call modules are simply KLDs that install a system call. In operating
systems, a system call, also known as a system service request, is the
mechanism an application uses to request service from the operating
system’s kernel.
系统调用模块是系统调用的KLD。在操作系统中,系统调用,也称为系统服务请求,是应用程序用来向操作系统内核请求服务的一种机制。
NOTE
In Chapters 2, 3, and 6, you’ll be writing rootkits that either hack
the existing system calls or install new ones. Thus, this section
serves as a primer.
注意 在章2,3和6中,你编写的rootkit,不是通过hack已存在的系统调用,就是通过安装一个新的系统调用的方式实现的。所以,本章的内容是基础的知识。
There
are three items that are unique to each system call module: the system
call function, the sysent structure, and the offset value.
每个系统调用模块都有三个项目,每个系统调用模块中的这三个项目都是唯一的。它们是系统调用函数,sysent 结构,和offset 值。
1.4.1 The System Call Function
1.4.1 系统调用函数
The system call function implements the system call. Its function prototype is defined in the header as:
系统调用函数实现了系统调用,它的函数原型在头文件中定义如下:
--------------------------------------------------------------------------------
typedef int sy_call_t(struct thread *, void *);
--------------------------------------------------------------------------------
where
struct thread * points to the currently running thread, and void *
points to the system call’s arguments’ structure, if there is any.
指针(struct thread *)指向当前运行的线程,如果系统调用具有包含参数的数据结构,指针(void *)用于指向这写数据结构。
Here
is an example system call function that takes in a character pointer
(i.e., a string) and outputs it to the system console and logging
facility via printf(9).
下面是一个系统调用的例子。它接受一个字符指针(比如,一个字符串)并且把它输出到系统控制台and logging facility via printf(9).
--------------------------------------------------------------------------------
/*1*/ struct sc_example_args {
char *str;
};
static int
sc_example(struct thread *td, void *syscall_args)
{
/*2*/ struct sc_example_args *uap;
/*3*/ uap = (struct sc_example_args *)syscall_args;
printf("%s\n", uap->str);
return(0);
}
--------------------------------------------------------------------------------
Notice
that the system call’s arguments are /*1*/ declared within a structure
(sc_example_args). Also, notice that these arguments are accessed
within the system call function by /*2*/first declaring a struct
sc_example_args pointer (uap) and then assigning /*3*/the coerced void
pointer (syscall_args) to that pointer.
注意到系统调用的变量声明在一个结构内部。我们也注意到访问这些变量的方式:在系统调用函数的内部,首先声明一个sc_example_args结构的指针(uap),然后把void指针强制转换并赋给该指针(uap).
Keep
in mind that the system call’s arguments reside in user space but that
the system call function executes in kernel space.5 Thus, when you
access the
我们记得,系统调用的变量位于用户空间,但是系统调用函数是在内核中执行的。所以,当你通过uap
---------------------------
5
FreeBSD segregates its virtual memory into two parts: user space and
kernel space. User space is where all user-mode applications run, while
kernel space is where the kernel and kernel extensions(i.e., LKMs) run.
Code running in user space cannot access kernel space directly (but
code running in kernel space can access user space). To access kernel
space from user space, an application issues a system call.
5.FreeBSD把它的虚拟分为两部分:用户空间和内核空间.用户模式应用程序运行于用户空间,内核以及内核的外延(比如LKM)运行于内核空间。运行在用户空间的代码不能直接访问内核空间(但是运行在内核空间的代码可以访问用户空间)。为了从用户空间访问内核空间,应用程序必须发出一个系统调用。
arguments
via uap, you are actually working by value, not reference. This means
that, with this approach, you aren’t able to modify the actual
arguments.
访问这些变量的时候,实际上你是操作的是参数的值而不是引用。这意味着,通过这个方法,你无法修改实际的参数。
NOTE In Section 1.5, I’ll detail how to modify data residing in user space while in kernel space.
注意,在章节1.5,我们将详细介绍在内核中如何修改位于用户空间的数据。
It
is probably worth mentioning that the kernel expects each system call
argument to be of size register_t (which is an int on i386, but is
typically a long on other platforms) and that it builds an array of
register_t values that are then cast to void * and passed as the
arguments. For this reason, you might need to include explicit padding
in your arguments’ structure to make it work correctly if it has any
types that aren’t of size register_t (e.g., char, or int on a 64-bit
platform). The header provides some macros to do
this, along with examples.
值得一提的是,内核期望每一个系统调用的参数都是register_t的大小(在
i386中是int,在其他平台中一般是long),然后构建一个元素大小都是register_t的数组,转换为指针(void
*)并作为参数传递给系统调用函数。基于这个原因,如果你的参数不是大小是register_t的数据类型(比如,char,或者在64位平台的
int),你可能需要在你的参数内部添加额外的补足成分(padding),这样它才能正常工作。头文件提供了一些宏完成这个任务,它还提供了一些例子。
1.4.2 The sysent Structure
1.4.2 sysent 结构
System calls are defined by their entries in a sysent structure, which is defined in the header as follows:
系统调用在sysent结构体中通过一个项目定义。sysent结构体在头文件中定义如下:
--------------------------------------------------------------------------------
struct sysent {
int sy_narg; /* number of arguments */
sy_call_t *sy_call; /* implementing function */
au_event_t sy_auevent; /* audit event associated with system call */
};
struct sysent {
int sy_narg; /* 参数的个数 */
sy_call_t *sy_call; /* 执行函数 */
au_event_t sy_auevent; /* 与系统调用相关的审计事件 */
};
--------------------------------------------------------------------------------
Here is the complete sysent structure for the example system call (shown in Section 1.4.1):
下面是针对示例系统调用(见章节1.4.1)写的完整的sysent结构。
--------------------------------------------------------------------------------
static struct sysent sc_example_sysent = {
1, /* number of arguments */
sc_example /* implementing function */
};
static struct sysent sc_example_sysent = {
1, /* 参数的个数 */
sc_example /* 执行函数 */
};
--------------------------------------------------------------------------------
Recall that the example system call has only one argument (a character pointer) and is named sc_example.
记得示例的系统调用只有一个参数(一个字符指针),而且系统调用函数的名称是sc_example。
One additional point is also worth mentioning. In FreeBSD, the system
call table is simply an array of sysent structures, and it is declared
in the header as follows:
还有一个指针也值得一提。在FreeBSD中,系统调用表只是一个sysent结构的数组,它在头文件中声明如下:
--------------------------------------------------------------------------------
extern struct sysent sysent[];
--------------------------------------------------------------------------------
Whenever
a system call is installed, its sysent structure is placed within an
open element in sysent[]. (This is an important point that will come
into play in Chapters 2 and 6.)
每当一个安装一个系统调用,它的sysent结构就被放置于sysent[]内一个开放的元素中(sysent是一个重要的指针,在章2和章6中将运用到它)
NOTE Throughout this book, I’ll refer to FreeBSD’s system call table as sysent[].
注意 以后,我在本书中也把FreeBSD的系统调用表叫做sysent[].
1.4.3 The Offset Value
1.4.3 Offset 值
The offset value (also known as the system call number) is a unique
integer between 0 and 456 that is assigned to each system call to
indicate its sysent structure’s offset within sysent[].
offset(也叫做系统调用号)在0到456之间的一个唯一的整数。它分配给每一个系统调用,指出系统调用的sysent结构在sysent[]中的偏移量.
Within a system call module, the offset value needs to be explicitly declared. This is typically done as follows:
在系统调用模块中,这个offset必须显式地进行声明。典型的做法如下:
--------------------------------------------------------------------------------
static int offset = NO_SYSCALL;
--------------------------------------------------------------------------------
The constant NO_SYSCALL sets offset to the next available or open element in sysent[].
常数NO_SYSCALL把offset设置为sysent[]中下一个可用或开放的元素。
Although you could manually set offset to any unused system call
number, it’s considered good practice to avoid doing so when
implementing something dynamic, like a KLD.
虽然你可以手工把offset设置为任何一个没有使用的系统调用号,但是当实现一些动态的东西时(比如KLD)避免那样做是一个好的习惯.
NOTE For a list of used and unused system call numbers, see the file /sys/kern/syscalls.master.
注意 想查看已使用和没使用的系统调用号的清单,看文件/sys/kern/syscalls.master
1.4.4 The SYSCALL_MODULE Macro
1.4.4 SYSCALL_MODULE 宏
Recall from Section 1.2 that when a KLD is loaded, it must link and
register itself with the kernel and that you use the DECLARE_MODULE
macro to do so.
章节1.2提到,在装载KLD时,KLD必须把它自身链接并注册到内核中。这个过程用DECLARE_MODULE来完成。
However, when writing a system call module, the DECLARE_MODULE macro is
somewhat inconvenient, as you’ll soon see. Thus, we use the
SYSCALL_MODULE macro instead, which is defined in the
header as follows:
但是,在编写一个系统调用模块是,DECLARE_MODULE宏使用起来有点不方便,这点你很快就能看到。所以,代替它的是我们使用SYSCALL_MODULE宏。SYSCALL_MODULE宏在头文件中定义如下:
--------------------------------------------------------------------------------
#define SYSCALL_MODULE(name, offset, new_sysent, evh, arg) \
static struct syscall_module_data name##_syscall_mod = { \
evh, arg, offset, new_sysent, { 0, NULL } \
}; \
\
static moduledata_t name##_mod = { \
#name, \
syscall_module_handler, \
&name##_syscall_mod \
}; \
DECLARE_MODULE(name, name##_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE)
--------------------------------------------------------------------------------
As you can see, if we were to use the DECLARE_MODULE macro, we would’ve
had to set up a syscall_module_data and moduledata structure first;
thankfully, SYSCALL_MODULE saves us this trouble.
可以看出,如果我们使用的是DECLARE_MODULE宏,我门首先得构造syscall_module_data 和 moduledata 结构体。而现在幸亏有了SYSCALL_MODULE宏,使得避免了那些麻烦。
The following is a brief description of each parameter in SYSCALL_MODULE:
下面是SYSCALL_MODULE中每个参数的简单描述:
name
This specifies the generic module name, which is passed as a character string.
它指明一般模块的名称,作为字符串进行传递。
offset
This specifies the system call’s offset value, which is passed as an integer pointer.
指定系统调用的偏移值,以一个整数的指针进行传递。
new_sysent
This specifies the completed sysent structure, which is passed as a struct sysent pointer.
指定一个完整的sysent结构,作为一个sysent指针进行传递。
evh
This specifies the event handler function.
指定事件处理程序函数
arg
This
specifies the arguments to be passed to the event handler function. For
our purposes, we’ll always set this parameter to NULL.
指定传递到事件处理程序函数的参数。根据我们的目的,我们总是设置它为NULL.
1.4.5 Example
1.4.5 例子
Listing 1-3 is a complete system call module.
清单1-3是一个完整的系统调用模块.
--------------------------------------------------------------------------------
#include
#include
#include
#include
#include
#include
#include
/* The system call's arguments. */
/* 系统调用的参数. */
struct sc_example_args {
char *str;
};
/* The system call function. */
/* 系统调用函数. */
static int
sc_example(struct thread *td, void *syscall_args)
{
struct sc_example_args *uap;
uap = (struct sc_example_args *)syscall_args;
printf("%s\n", uap->str);
return(0);
}
/* The sysent for the new system call. */
/* 为新的系统调用准备的sysent结构. */
static struct sysent sc_example_sysent = {
1, /* number of arguments */
sc_example /* implementing function */
};
/* The offset in sysent[] where the system call is to be allocated. */
/* 系统调用在sysent[]中所处的偏移量. */
static int offset = NO_SYSCALL;
/* The function called at load/unload. */
/* 装载/卸载阶段调用的函数. */
static int
load(struct module *module, int cmd, void *arg)
{
int error = 0;
switch (cmd) {
case MOD_LOAD:
uprintf("System call loaded at offset %d.\n", offset);
break;
case MOD_UNLOAD:
uprintf("System call unloaded from offset %d.\n", offset);
break;
default:
error = EOPNOTSUPP;
break;
}
return(error);
}
SYSCALL_MODULE(sc_example, &offset, &sc_example_sysent, load, NULL);
--------------------------------------------------------------------------------
清单 1-3: sc_example.c
As you can see, this module is simply a combination of all the
components described throughout this section, with the addition of an
event handler function. Simple, no?
Here are the results of loading this module:
正像你看到的,这个模块是本章描述的各部分加上事件处理函数的简单组合。的确很简单,不是吗?
下面是装载这个模块的结果:
--------------------------------------------------------------------------------
$ sudo kldload ./sc_example.ko
System call loaded at offset 210.
--------------------------------------------------------------------------------
So far, so good. Now, let’s write a simple user space program to
execute and test this new system call. But first, an explanation of the
modfind, modstat, and syscall functions is required.
到目前为止,一切顺利。现在,让我们写些个简单的用户空间程序,运行和测试一个这个新的系统调用。但是,首先有必要介绍函数modfind, modstat, 和syscall.
1.4.6 The modfind Function
1.4.6 modind函数
The modfind function returns the modid of a kernel module based on its
module name.
modind函数依据它的模块名称返回内核模块的modid
--------------------------------------------------------------------------------
#include
#include
int
modfind( char *modname);
--------------------------------------------------------------------------------
Modids are integers used to uniquely identify each loaded module in
the system.
modid是用来唯一地标志系统中已装载模块的整数。
1.4.7 The modstat Function
1.4.7 modstat 函数
The modstat function returns the status of a kernel module referred to by
its modid.
modstat函数返回由modid指定的内核模块的状态.
--------------------------------------------------------------------------------
#include
#include
int
modstat(int modid, struct module_stat *stat);
--------------------------------------------------------------------------------
The returned information is stored in stat, a module_stat structure, which
is defined in the header as follows:
返回的信息存储在stat,一个module_stat的结构体中。module_stat在头文件中定义如下:
--------------------------------------------------------------------------------
struct module_stat {
int version;
char name[MAXMODNAME]; /* module name */
int refs; /* number of references */
int id; /* module id number */
modspecific_t data; /* module specific data */
};
typedef union modspecific {
int intval; /* offset value */
u_int uintval;
long longval;
u_long ulongval;
} modspecific_t;
--------------------------------------------------------------------------------
1.4.8 The syscall Function
1.4.8 syscall函数
The syscall function executes the system call specified by its system call
number.
syscall函数执行由系统调用号指定的系统调用.
--------------------------------------------------------------------------------
#include
#include
int
syscall(int number, ...);
--------------------------------------------------------------------------------
1.4.9 Executing the System Call
1.4.9 执行系统调用
Listing
1-4 is a user space program designed to execute the system call in
Listing 1-3 (which is named sc_example). This program takes one
command-line argument: a string to be passed to sc_example.
清单1-4是一个用户空间程序,它用来执行清单1-3的系统调用(名称叫sc_example).该程序接受一个命令行参数:一个传递给sc_example的字符串。
--------------------------------------------------------------------------------
#include
#include
#include
#include
int
main(int argc, char *argv[])
{
int syscall_num;
struct module_stat stat;
if (argc != 2) {
printf("Usage:\n%s \n", argv[0]);
exit(0);
}
/* Determine sc_example's offset value. */
stat.version = sizeof(stat);
/*1*/ modstat(modfind("sc_example"), &stat);
syscall_num = stat.data.intval;
/* Call sc_example. */
return(/*2*/ syscall(syscall_num, argv[1]));
}
--------------------------------------------------------------------------------
Listing 1-4: interface.c
清单 1-4: interface.c
As you can see, we first call /*1*/ modfind and modstat to determine
sc_example’s offset value. This value is then passed to /*2*/ syscall, along with
the first command-line argument, which effectively executes sc_example.
Some sample output follows:
就如你看到的,我们调用modfind和modstat来确定sc_example的偏移值。然后这个值以及第一个命令行参数一起传递给syscall。syscall执行sc_example。
输出结果如下:
--------------------------------------------------------------------------------
$ ./interface Hello,\ kernel!
$ dmesg | tail -n 1
Hello, kernel!
--------------------------------------------------------------------------------
1.4.10 Executing the System Call Without C Code
1.4.10不用C代码就能执行系统调用的方法
While
writing a user space program to execute a system call is the “proper”
way to do it, when you just want to test a system call module, it’s
annoying to have to write an additional program first. To execute a
system call without writing a user space program, here’s what I do:
当你想去测试一个系统调用模块时,编写一个用户空间的程序来执行一个系统调用是种“正规”的方法。但首先要编写额外的程序,这是恼人的事情。怎样才可以执行一个系统调用而又不用编写用户空间的程序呢?我是这样做的,如下:
--------------------------------------------------------------------------------
$ sudo kldload ./sc_example.ko
System call loaded at offset 210.
$ perl -e '$str = "Hello, kernel!";' -e 'syscall(210, $str);'
$ dmesg | tail -n 1
Hello, kernel!
--------------------------------------------------------------------------------
As the preceding demonstration shows, by taking advantage of Perl’s
command-line execution (i.e., the -e option), its syscall function, and
the fact that you know your system call’s offset value, you can quickly
test any system call module. One thing to keep in mind is that you
cannot use string literals with Perl’s syscall function, which is why I
use a variable ($str) to pass the string to sc_example.
前面这个示范显示,利用Perl的命令行执行的优点和它的syscall函数以及你知道了系统调用的偏移值,你可以快速地测试任何一个系统调用模块。但有
一件事你得记住,不能在Perl的syscall函数中使用字符串,这就是我使用一个变量($str)来传递字符串给sc_example的原因.
1.5 Kernel/User Space Transitions
1.5 内核/用户空间数据传递
I’ll
now describe a set of core functions that you can use from kernel space
to copy, manipulate, and overwrite the data stored in user space. We’ll
put these functions to much use throughout this book.
下面我将描述一组核心函数,可以在内核空间使用它们来复制,操作,重写存储在用户空间的数据。在本书中,这些函数经常用到。
1.5.1 The copyin and copyinstr Functions
1.5.1 copyin 和 copyinstr 函数
The copyin and copyinstr functions allow you to copy a continuous region of data from user space to kernel space.
copyin 和 copyinstr 函数用来拷贝用户空间中一段连续区域的数据到内核空间.
--------------------------------------------------------------------------------
#include
#include
int
copyin(const void *uaddr, void *kaddr, size_t len);
int
copyinstr(const void *uaddr, void *kaddr, size_t len, size_t *done);
--------------------------------------------------------------------------------
The copyin function copies len bytes of data from the user space address uaddr to the kernel space address kaddr.
copyin函数从用户空间的地址uaddr拷贝len字节的数据到内核空间的地址kaddr.
The
copyinstr function is similar, except that it copies a null-terminated
string, which is at most len bytes long, with the number of bytes
actually copied returned in done.6
copyinstr函数与copyin类似,但它是拷贝一个null结束的字符串(至多len字节长),实际拷贝的字节数返回到done中。
1.5.2 The copyout Function
1.5.2 copyout 函数
The
copyout function is similar to copyin, except that it operates in the
opposite direction, copying data from kernel space to user space.
copyout函数与copyin类似,不过它以相反的方向操作,从内核空间拷贝数据到用户空间。
--------------------------------------------------------------------------------
#include
#include
int
copyout(const void *kaddr, void *uaddr, size_t len);
--------------------------------------------------------------------------------
1.5.3 The copystr Function
The copystr function is similar to copyinstr, except that it copies a string from one kernel space address to another.
copystr函数与copyinstr类似,不过它是从内核空间的一个地址拷贝数据到另一个地址。
--------------------------------------------------------------------------------
#include
#include
int
copystr(const void *kfaddr, void *kdaddr, size_t len, size_t *done);
--------------------------------------------------------------------------------
---------------------
6
In Listing 1-3, the system call function should, admittedly, first call
copyinstr to copy in the user space string and then print that. As is,
it prints a userland string directly from kernel space, which can
trigger a fatal panic if the page holding the string is unmapped (i.e.,
swapped out or not faulted in yet). That’s why it’s just an example and
not a real system call.
6
清单1-3中,系统调用函数无可否认应当首先调用copyinstr函数来拷贝用户空间的字符串,然后再打印它们。直接从内核空间打印用户空间的字符串可
能会触发致命的panic,如果拥有该字符串的页面给unmapped了(比如,该页面给页交换出去了,或者缺页后页面还没调进内存)的话。这就是清单1
-3仅仅是一个示例的原因,它还不是一个真正的系统调用。
1.6 Character Device Modules
1.6 字符设备驱动模块
Character
device modules are KLDs that create or install a character device. In
FreeBSD, a character device is the interface for accessing a specific
device within the kernel. For example, data is read from and written to
the system console via the character device /dev/console.
字符设备模块是一种用于创建或安装字符设备的KLD。在FreeBSD中,字符设备是访问某种内核中特殊设备的界面。例如,通过字符设备/dev/console可以从系统控制台读取数据或把数据写到控制台去。
NOTE
In Chapter 4 you’ll be writing rootkits that hack the existing
character devices on the system. Thus, this section serves as a primer.
注意 在章节4中,你将要编写hack在系统中已经存在的字符设备rootkit。因此,本节介绍的是基础知识.
There
are three items that are unique to each character device module: a
cdevsw structure, the character device functions, and a device
registration routine. We’ll discuss each in turn below.
每个字符设备模块都有三个唯一的项目:cdevsw 结构体,字符设备函数,设备注册例程。下面我们依次讨论它们。
1.6.1 The cdevsw Structure
1.6.1 cdevsw 结构体
A
character device is defined by its entries in a character device switch
table, struct cdevsw, which is defined in the header
as follows:
字符设备在一个字符设备转换表cdevsw结构体中通过它的表项定义。cdevsw在头文件中定义如下:
--------------------------------------------------------------------------------
struct cdevsw {
int d_version;
u_int d_flags;
const char *d_name;
d_open_t *d_open;
d_fdopen_t *d_fdopen;
d_close_t *d_close;
d_read_t *d_read;
d_write_t *d_write;
d_ioctl_t *d_ioctl;
d_poll_t *d_poll;
d_mmap_t *d_mmap;
d_strategy_t *d_strategy;
dumper_t *d_dump;
d_kqfilter_t *d_kqfilter;
d_purge_t *d_purge;
d_spare2_t *d_spare2;
uid_t d_uid;
gid_t d_gid;
mode_t d_mode;
const char *d_kind;
/* These fields should not be messed with by drivers */
/* 这些域不应该让驱动程序 messed with?? */
LIST_ENTRY(cdevsw) d_list;
LIST_HEAD(, cdev) d_devs;
int d_spare3;
struct cdevsw *d_gianttrick;
};
--------------------------------------------------------------------------------
Table 1-1 provides a brief description of the most relevant entry points.
表格 1-1 提供相关项的简要描述
Table 1-1: Entry Points for Character Device Drivers
表格 1-1: 提供给字符设备驱动程序的项
--------------------------------------------------------------------------------
Entry Point Description
--------------------------------------------------------------------------------
d_open Opens a device for I/O operations
d_close Closes a device
d_read Reads data from a device
d_write Writes data to a device
d_ioctl Performs an operation other than a read or a write
d_poll Polls a device to see if there is data to be read or space available for writing
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
项 描述
--------------------------------------------------------------------------------
d_open 为I/O操作打开一个设备
d_close 关闭一个设备
d_read 从设备读数据
d_write 写数据到设备
d_ioctl 执行read和write以外的其他操作
d_poll 轮询问一个设备,看看有没有数据可供读取或有没有效的空间写数据
--------------------------------------------------------------------------------
Here is an example cdevsw structure for a simple read/write character
device module:
下面是简单的读/写字符设备模块的一个cdevsw结构的示例
--------------------------------------------------------------------------------
static struct cdevsw cd_example_cdevsw = {
.d_version = D_VERSION,
.d_open = open,
.d_close = close,
.d_read = read,
.d_write = write,
.d_name = "cd_example"
};
--------------------------------------------------------------------------------
Notice
that I do not define every entry point or fill out every attribute.
This is perfectly okay. For every entry point left null, the operation
is considered unsupported. For example, when creating a write-only
device, you would not declare the read entry point.
注意到我没有定义全部的入口点或填充每一个属性。这完全没问题。对于没一个保留null的入口点来说,它的操作被视为不被支持。例如,要创建一个只写的设备,你就没必要声明一个读的入口点。
Still,
there are two elements that must be defined in every cdevsw structure:
d_version, which indicates the versions of FreeBSD that the driver
supports, and d_name, which specifies the device’s name.
但是,有两个元素在每个cdevsw结构中是必须定义的:d_version,它指出驱动程序支持的FreeBSD版本.还有,d_name,它指定了驱动程序的名称。
NOTE The constant D_VERSION is defined in the header, along with other version numbers.
注意 常量D_VERSION 定义在头文件。该头文件也包含其他的版本号。
1.6.2 Character Device Functions
1.6.2 字符设备函数
For
every entry point defined in a character device module’s cdevsw
structure, you must implement a corresponding function. The function
prototype for each entry point is defined in the
header.
对于每一个在字符设备驱动模块的cdevsw结构中定义了的入口点,你必须实现一个相应的函数。每一个入口点的函数原型定义在头文件。
Below is an example implementation for the write entry point.
下面是写入口点的一个实现示例。
--------------------------------------------------------------------------------
/* Function prototype. */
/* 函数原型 */
d_write_t write;
int
write(struct cdev *dev, struct uio *uio, int ioflag)
{
int error = 0;
error = copyinstr(uio->uio_iov->iov_base, &buf, 512, &len);
if (error != 0)
uprintf("Write to \"cd_example\" failed.\n");
return(error);
}
--------------------------------------------------------------------------------
As
you can see, this function simply calls copyinstr to copy a string from
user space and store it in a buffer, buf, in kernel space.
你可以看出,这个函数简单地调用copyinstr把一个字符串从用户空间拷贝出来然后保存到内核空间的一个缓冲区buf中。
NOTE In Section 1.6.4 I’ll show and explain some more entry-point implementations.
注意 在章节 1.6.4 ,我将演示和解释一些其他的入口点的实现。
1.6.3 The Device Registration Routine
1.6.3 设备注册例程
The
device registration routine creates or installs the character device on
/dev and registers it with the device file system (DEVFS). You can
accomplish this by calling the make_dev function within the event
handler function as follows:
设备注册例程创建或安装字符设备到/dev,并注册到设备文件系统(DEVFS)。你可以在在事件处理程序函数里面调用make_dev函数完成这个任务。
--------------------------------------------------------------------------------
static struct cdev *sdev;
/* The function called at load/unload. */
/* 该函数在装载/卸载模块时调用 */
static int
load(struct module *module, int cmd, void *arg)
{
int error = 0;
switch (cmd) {
case MOD_LOAD:
sdev = make_dev(&cd_example_cdevsw, 0, UID_ROOT, GID_WHEEL,
0600, "cd_example");
uprintf("Character device loaded\n");
break;
case MOD_UNLOAD:
destroy_dev(sdev);
uprintf("Character device unloaded\n");
break;
default:
error = EOPNOTSUPP;
break;
}
return(error);
}
--------------------------------------------------------------------------------
This
example function will register the character device, cd_example, when
the module loads by calling the make_dev function, which will create a
cd_example device node on /dev. Also, this function will unregister the
character device when the module unloads by calling the destroy_dev
function, which takes as its sole argument the cdev structure returned
from a preceding make_dev call.
这个示例函数将在装载模块是通过调用make_dev
函数注册字符设备cd_example,make_dev 将在/dev创建一个cd_example
设备。同样这个函数在卸载模块是通过调用destroy_dev注销字符设备,前面调用make_dev时会返回一个cdev结构,
destroy_dev函数接收这个cdev结构做为它唯一变量。
1.6.4 Example
1.6.4 示例
Listing
1-5 shows a complete character device module (based on Rajesh
Vaidheeswarran’s cdev.c) that installs a simple read/write character
device. This device acts on an area of kernel memory, reading and
writing a single character string from and to it.
清单1-5显示了一个完整的字符设备模块(基于Rajesh Vaidheeswarran的cdev.c)。这个字符设备模块安装一个简单的读/写字符设备。这个设备操作内核内存的一块区域,从这个区域读取单个字符串或写单个字符串到这个区域中。
--------------------------------------------------------------------------------
#include
#include
#include
#include
#include
#include
#include
/* Function prototypes. */
/* 函数原型. */
d_open_t open;
d_close_t close;
d_read_t read;
d_write_t write;
static struct cdevsw cd_example_cdevsw = {
.d_version = D_VERSION,
.d_open = open,
.d_close = close,
.d_read = read,
.d_write = write,
.d_name = "cd_example"
};
static char buf[512+1];
static size_t len;
int
open(struct cdev *dev, int flag, int otyp, struct thread *td)
{
/* Initialize character buffer. */
/* 初始化字符缓冲 */
memset(&buf, '\0', 513);
len = 0;
return(0);
}
int
close(struct cdev *dev, int flag, int otyp, struct thread *td)
{
return(0);
}
int
write(struct cdev *dev, struct uio *uio, int ioflag)
{
int error = 0;
/*
* Take in a character string, saving it in buf.
* Note: The proper way to transfer data between buffers and I/O
* vectors that cross the user/kernel space boundary is with
* uiomove(), but this way is shorter. For more on device driver I/O
* routines, see the uio(9) manual page.
*/
/*
* 接收一个字符串,保存到缓冲中。
* 注意:在跨越了用户/内核空间边界的缓冲和I/O向量之间传递数据的
* 正规途径是使用uiomove(),但是本例的方法更简短。你可以在uio(9)
* 手册查看关于设备驱动程序I/O例程的更多知识
*/
error = copyinstr(uio->uio_iov->iov_base, &buf, 512, &len);
if (error != 0)
uprintf("Write to \"cd_example\" failed.\n");
return(error);
}
int
read(struct cdev *dev, struct uio *uio, int ioflag)
{
int error = 0;
if (len <= 0)
error = -1;
else
/* Return the saved character string to userland. */
/* 返回保存到用户空间的字符串长度 */
copystr(&buf, uio->uio_iov->iov_base, 513, &len);
return(error);
}
/* Reference to the device in DEVFS. */
/* 在DEVFS中对设备的引用 */
static struct cdev *sdev;
/* The function called at load/unload. */
/* 该函数在装载/卸载模块时调用 */
static int
load(struct module *module, int cmd, void *arg)
{
int error = 0;
switch (cmd) {
case MOD_LOAD:
sdev = make_dev(&cd_example_cdevsw, 0, UID_ROOT, GID_WHEEL,
0600, "cd_example");
uprintf("Character device loaded.\n");
break;
case MOD_UNLOAD:
destroy_dev(sdev);
uprintf("Character device unloaded.\n");
break;
default:
error = EOPNOTSUPP;
break;
}
return(error);
}
DEV_MODULE(cd_example, load, NULL);
--------------------------------------------------------------------------------
Listing 1-5: cd_example.cBSD爱好者乐园U(M#M"t$yK}W2`
清单 1-5: cd_example.cBSD爱好者乐园7TBF4B
T!Nq8~6^ n
BSD爱好者乐园
sOYXm
The
following is a breakdown of the above listing. First, at the beginning,
we declare the character device’s entry points (open, close, read, and
write). Next, we appropriately fill out a cdevsw structure. Afterward,
we declare two global variables: buf, which is used to store
thecharacter string that this device will be reading in, and len, which
is used to store the string length. Next, we implement each entry
point. The open entry point simply initializes buf and then returns.
The close entry point does nothing, more or less, but it still needs to
be implemented in order to close the device. The write entry point is
what is called to store the character string (from user space) in buf,
and the read entry point is what is called to return it. Lastly, the
event handler function takes care of the character device’s
registration routine.
+`M+w+J
阅读(1773) | 评论(0) | 转发(0) |