网络安全编程
这本书直到这个地方,我们一直在注意如何编写使用套接口的程序,而不论其是客户端还是服务器程序。但是我们却并没有级出安全编程的考虑来对抗外在的威胁,这些威胁可以是来自Internet或是我们局域网内部的一些别有用心的人。在这一章,我们将会介绍以下内容:
inetd守护进程如何与TCP包装器概念配合来提供客户的检查
TCP包装器概念是如何工作的
当我们结束这一章,我们将会理解TCP包装器概念是如何工作的,并且会了解到如何将其应用到我们管理或是自已编写的服务器上。
定义安全Merriam Webster的学院字典用各各方式定义了安全。而我们感兴趣的是下面的两种定义方式:
当受危险控制的质量或状态
对抗危险的采取的方法
当讨论网络安全时,第一点是指:我们希望远离各种潜在的威胁。第二点是,我们必须保证我们的系统资源不被非法检查,以某种方式的破坏,以及其他的各种攻击。我们编写的文章在没有公开的情况下不能从我们的系统未经授权的丢失。
这个任务的复杂性以及其程序的边界使得其不可能在一章中解决全部的问题。然而,网络安全的某些方面直接适用于套接口编程,而我们不应忽略所存在的危险。在这一章我们所提到的一些简单的方法可以帮助我们防止攻击。我们将会了解弱点存在的地方,而我们应怎么样来处理。
标识朋友或敌人第十七章,"传递证书与文件描述符",将会向我们展示我们可以通过使用证书来我们服务器的一个本地用户标识一个高可信度。然而,当我们服务器的这个用户是一个远程用户时,这样的可信度并不容易达到。
在这一章,我们将会依据对等网络地址来标识一个资源的用户。然而,这并不是一个防护技术,因为对等的地址在一定的条件可以被欺骗。但是,他确实提供了一个简单的预防攻击的初级防护。
在我们拥了IP地址以后,我们可以查看客户的注册主机与域名。这提供了一个筛选的额外级别,而这在攻击者部分需要更多的描述。
解析主机与域名允许我们的服务器应用下列类型的访问策略:
允许访问指定主机
允许访问指定域名
拒绝访问指定主机
拒绝访问指定域名
拒绝访问不能解析为名字的IP地址
下面的章节将会讨论这些不同的策略。
通过主机名或是域名加强安全当一个客户连接到我们的服务器时,我们也许会回忆起服务器由accep函数调用来接受客户端套接口地址。如下面的代码片段所示:
struct sockaddr_in adr_clnt;/* AF_INET */
int len_inet; /* length */
int c; /* Client socket */
. . .
len_inet = sizeof adr_clnt;
c = accept(s,
(struct sockaddr *)&adr_clnt,
&len_inet);
数据报服务器由recvfrom函数得到这个客户的地址。如下面的代码所示:
int z;
struct sockaddr_in adr_clnt;/* AF_INET */
int len_inet; /* length */
int s; /* Socket */
char dgram[512]; /* Recv buffer */
len_inet = sizeof adr_clnt;
z = recvfrom(s, /* Socket */
dgram, /* Receiving buffer */
sizeof dgram, /* Max recv buf size */
0, /* Flags: no options */
(struct sockaddr *)&adr_clnt,/* Addr */
&len_inet); /* Addr len, in & out */
在任何一种服务器类型中得到了客户端地址以后,我们可以应用第九章中的技术,"主机名与网络名查询",使用gethostbyaddr函数。下面的代码片段显示了一个客户端IP地址如何解析为一个主机名:
struct sockaddr_in adr_clnt;/* AF_INET */
struct hostent *hp; /* Host entry ptr */
hp = gethostbyaddr(
(char *)&adr_clnt.sin_addr,
size of adr_clnt.sin_addr,
adr_clnt.sin_family);
if ( !hp )
fprintf(logf," Error: %s\n",
hstrerror(h_errno));
else
fprintf(logf," %s\n",
hp->h_name);
服务器在hp->h_name中得到主机名以后,他就可以就用设计者所希望的允许或是拒绝策略。
通过IP地址标识尽管使用系统主机名或是域名进行工作很方便,但是这种标识方法却存在安全问题。当服务器接收到IP地址时,他必须使用另外一个网络进程(通过gethostbyaddr进行初始化)来将其解析为一个主机名。我们可以很容易想到一个攻击者可以其IP地址谎设置为他的私有名字服务器。因这这个原因,有时我们更喜欢只依据IP地址来进行安全决定。
另一个折中的策略就是只依据IP地址确定访问权限,但是在我们的服务器日志记录中要同时记录IP地址与所解析的域名。这样在查看历史日志时除了提供了一定的方便也提供了很好的安全策略。当出现一些莫名的事情时,我们可以同时查看IP地址与所解析的域名。
使用IP地址作为安全策略为服务器管理提出了额外的要求。如果客户改变了其IP地址,那么系统管理员必须更新IP地址,尽管他的主机名与域名并没有改变。
当使用IP地址安全策略时,对于系统管理员还有另一个挑战,也许他会遇到并不知道自己IP地址的用户。然而,也许我们可以从他们那里得到主机名与域名的信息。使用这些信息,我们可以使用nslookup(1)命令来确定这个客户的IP地址,然后配置这个地址。
最后,一些客户每次启动他们的机器时也许会使用不同的IP地址--例如,使用DHCP。他们的IP地址是由他们的系统管理员所确定的IP地址范围内一个变化的IP地址。在这种情况下我们最好的解决办法就是与他们的系统管理员联系,从而得到更多的关于他们的IP地址变化范围内容的信息。在这些情况下,我们通常会在网络ID级限制访问。
安全化inetd服务器到目前为止,我们的讨论围绕着在我们要实现安全策略的每一台服务器中的自定义代码。这有许多缺点:
管理安全策略的代码必须嵌入到提供给客户的每一台服务器中
每台服务器必须全程测试来确认其精度并且防御攻击
多个访问点就会暴露多个弱点
前两点很好的解释了他们自身。最后一点可以通过一个超市关门时要锁许多门的问题来演示。当一些门必须锁住时,就会很容易忽略其中的一个。另外,也许更有可能的是其中一个有会被攻破的弱点。正是由于这些原因,就需要将一个安全策略集中到一个模块中。
集中网络策略在上一章中,我们看到inetd守护进程如何使得服务器的设计更为简单。inetd守护进程提供了监听客户端请求所必须的所有服务器代码,并且只在必须的时候才会启动服务器。inetd守护进程提供了另外的一级方便:他允许安装一个集中的网络安全模块。
如果我们正在使用Linux较新的版本,例如RH6,我们就已经有了一个可以由inetd来调用的TCP包装程序。要验证这一点,可以用grep查找telnetd实体,如下所示:
# grep telnet /etc/inetd.conf
telnet stream tcp nowait root /usr/sbin/tcpd in.telnetd
#
Y
由这个例子输出,我们可以看到当一个telnet请求到达时,inetd调用可执行的/usr/sbin/tcpd程序。如果我们同时用grep检测ftp,我们就会发现/usr/sbin/tcpd也会为这个程序而调用。那么tcpd程序是做什么的呢?
应该强调的一点就是tcpd程序在inetd与服务器(例如telnetd)之间插入其自身。这是以透明的方式来完成的,因为他并不会在套接口上产生任何输入或是输出。tcpd程序只是简单的应用其网络安全策略,如果允许访问就会调用相应的服务器。
从前面的grep输入的例子,字符串in.telnet作为tcpd的命令名(他的argv[0]的值)。这会通知tcpd如果允许访问应调用哪个服务器。如果因为某些原因拒绝访问,这样的尝试将会被记入日志,而套接口将会关闭(当tcpd退出时),而不会调用服务器。
理解TCP包装器概念下图向我们显示了tcpd程序的角色,以及他与inetd的交互和最后的服务器。
下面我们来回顾一下远程客户端连接到我们的in.telnetd服务器的过程:
1 客户端使用telnet客户端命令向我们机器的telnet守护进程发送一个连接请求。
2 我们的Linux主机使用inetd,他被配置为在23号端口监听telnet请求。他接受第一步的连接请求。
3 /etc/inetd.conf配置文件指导我们的inetd服务器fork一个新进程。而父进程返回继续监听连接请求。
4 第三步的子进程调用exec命令来执行/usr/sbin/tcpd TCP包装器程序。
5 tcpd程序将会决定是否应为客户端指定一定的访问权限。这是由所调用的套接口地址与/etc/hosts.deny和/etc/hosts.allow配置文件的组合来决定的。
6 如果拒绝访问,tcpd只会简单的结束(这会使得文件单元0,1,2关闭,而他们是套接口文件描述符)。
7 如果允许访问,要启动的可执行文件是由tcpd的argv[0]值来确定的。在这个例子中为in.telnetd程序。这指定的可执行路径名,/usr/sbin/in.telnetd,他将会传递给exec函数来载入并执行。
8 服务器现在以tcpd之前所有的相同的进程ID代替tcpd运行。服务器现在在套接口(文件单元0,1,2)执行输入与输出操作。
第七步是很重要的,这是由tcpd内部通过exec函数启动服务器进程的地方。这在inetd与(子)服务器进程之间维护重要的父/子关系。当使用了wait标记时,inetd守护进程就会在检测到当前子进程结束时启动下一个服务器。这只有当服务器是inetd父进程的直接子进程时才会正常工作。下面的几点也许会有助于我们来理解:
1 例如在这个例子中inetd守护进程的ID为124。
2 inetd守护进程调用fork来启动一个子进程。在这个例子中子进程ID现在为1243。
3 inetd子进程(PID 1243)现在调用exec来启动/usr/sbin/tcpd。
4 注意到tcpd现在以PID 1243来运行(我们可以回想一下exec使用相同的进程资源来启动一个新程序,而忽略调用exec的原始程序)。
5 当允许访问时,tcpd实际会再次调用exec。这会启动新服务器,在这个例子中为/usr/sbin/in.telnetd。
6 注意到服务器/usr/sbin/in.telnetd仍然是1243,因为exec并没有创建一个新进程。
7 服务器in.telnetd实际退出(PID 1243结束)。
8 父进程inetd(PID 1243)接收到一个SIGCHLD信号来表明其子进程ID 1243已经结束。这会使得inetd调用wait来确定哪一个子进程结束了。
从这些步骤我们可以看出tcpd包装程序是多么的精巧。这个程序绝不会在套接口上执行I/O操作--这会打乱正使用的协议(telnet或是其他)。
确定访问现在我们也许仍有两个问题:
1 TCP包装程序如何确定他要保证的服务(telnet,ftp)?
2 他如何确定客户端是谁?
现在,我们将会在下面的部分简要的描述各个解决方案。
确定服务tcpd程序可以通过调用getsockname函数来确定他所保护的服务。还记得这个函数?他不仅返回客户端所连接到的套接口地址,而且还会指明服务的端口。在前面的例子中,端口号为23(telnet服务)。
确定客户端标识因为tcpd程序并不执行accept函数调用(这是由inetd来完成的),他必须确定客户端是谁。也许正如我们所猜想的,这是由getpeername函数来完成的。我们也许还会回忆起这个函数取得远程客户端的地址和端口号,其工作方式与getsockname相类似。
确定数据报客户端标识确定一个数据报客户端的标识有一些麻烦。敏锐的读者也许在前面的章节中已经想到了这个问题,因为数据报并不使用accept函数。也不可以在数据报套接口上使用getpeername函数,因为第一个数据报实际上是由不同的客户端发来的。客户端地址是由recvfrom函数调用返回的。然而,tcpd程序可以在不实际读取服务器数据的情况下确定客户端标识吗?
结果证明tcpd可以欺骗。客户端地址与端口号可以由使用MSG_PEEK标记选项的recvfrom函数调用来确定。如下面的示例代码所示:
int z;
struct sockaddr_in adr_clnt;/* AF_INET */
int len_inet; /* length */
int s; /* Socket */
char dgram[512]; /* Recv buffer */
len_inet = sizeof adr_clnt;
z = recvfrom(s, /* Socket */
dgram, /* Receiving buffer */
sizeof dgram, /* Max recv buf size */
MSG_PEEK, /* Flags: Peek at data */
(struct sockaddr *)&adr_clnt,/* Addr */
&len_inet); /* Addr len, in & out */
在这里我们需要注意MSG_PEEK标记选项。这个选项会引导内核以类似通常的方式来执行recvfrom调用,所不同的是数据并不会由读队列中移除。如果允许访问,这会允许tcpd程序监视服务器接下来所要读取的数据报。
注意,数据本身在这里并不重要。MSG_PEEK操作所完成的是他会返回客户端的IP地址(在这个例子中,这会放置在adr_clnt中)。包装程序会由adr_clnt变量决定数据报是否应被处理。
到现在为止,我们已经理解了TCP包装概念后面的理论。下面,我们将会看到这样的思想以例子程序的具体形式进行演示。
安装包装器与服务器程序这一节将会提供一个简单的数据报服务器与一个相对应的TCP包装程序。这个包装程序实现了一个非常简单的安全策略。
检测服务器与包装器日志代码
服务器与包装程序共享一些日志函数。日志函数代码在下面给出:
/*
* log.c
*
* Logging Functions:
*/
#include
#include
#include
#include
#include
#include
static FILE *logf = NULL; /* Log File */
/*
* Open log file for append:
*
* Returns:
* 0 Success
* -1 Failed
*/
int log_open(const char *pathname)
{
logf = fopen(pathname,"a");
return logf ? 0 : -1;
}
/*
* Log information to a file:
*/
void log(const char *format,...)
{
va_list ap;
if(!logf)
return ; /* No log file open */
fprintf(logf,"[PID %ld] ",(long)getpid());
va_start(ap,format);
vfprintf(logf,format,ap);
va_end(ap);
fflush(logf);
}
/*
* Close the log file:
*/
void log_close(void)
{
if(logf)
fclose(logf);
logf = NULL;
}
/*
* This function reports the error to
* the log file and calls exit(1)
*/
void bail(const char *on_what)
{
if(logf) /* Is log open? */
{
if(errno) /* Error ? */
log("%s:",strerror(errno));
log("%s\n",on_what); /* Log msg */
log_close();
}
exit(1);
}
由引用程序所包含的文件代码如下:
/*
* log.h
* log.c externs:
*/
extern int log_open(const char *pathname);
extern void log(const char *format,...);
extern void log_close(void);
extern void bail(const char *on_what);
检测数据报服务器代码
这里将会演示一个程序,这个程序会处理第一个数据报,然后会轮询更多的数据报。如果在8秒内没有数据报到达,服务器就会超时退出。inetd守护进程直到被通知服务器结束时才会启动另一个服务器(/etc/inetd.conf记录实体必须使用wait标识字)。
下面是数据报服务程序所使用的代码:
/*
* dgramisrvr.c
*
* Example inetd datagram server:
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "log.h"
#define LOGPATH "/tmp/dgramisrvr.log"
int main(int argc,char **argv)
{
int z;
int s; /* Socket */
int alen; /* Length of address */
struct sockaddr_in adr_clnt; /* Client */
char dgram[512]; /* Receive buffer */
char dtfmt[512]; /* Date/Time Result */
time_t td; /* Current Time and Date */
struct tm dtv; /* Date time values */
fd_set rx_set; /* Incoming req. set */
struct timeval tmout; /* Timeout value */
/*
* Open a log file for append:
*/
if(log_open(LOGPATH) == -1)
exit(1); /* No log file ! */
log("dgramisrvr started. \n");
/*
* Other initialization:
*/
s = 0; /* Our socket is on std input */
FD_ZERO(&rx_set); /* Initialize */
FD_SET(s,&rx_set); /* Notice fd=0 */
/*
* Now wait for incoming datagrams:
*/
for(;;)
{
/*
* Blog until a datagram arrives:
*/
alen = sizeof adr_clnt;
z = recvfrom(s, /* Socket */
dgram, /* Receiving buffer */
sizeof dgram, /* Max recv size */
(struct sockaddr *)&adr_clnt,
&alen); /* Addr len,in&out*/
if(z<0)
bail("recvfrom(2)");
dgram[z]=0; /* NULL terminate dgram */
/*
* Log the request:
*/
log("Got request '%s' from %s port %d\n",
dgram,
inet_ntoa(adr_clnt.sin_addr),
ntohs(adr_clnt.sin_port));
/*
* Get the current date and time:
*/
time(&td); /* Current time & date */
dtv = *localtime(&td);
/*
* Formate a new date and time string,
* based upon the input format string:
*/
strftime(dtfmt, /* Formatted result */
sizeof dtfmt, /* Max size */
dgram, /* date/time format */
&dtv); /* Input values */
/*
* Send the formatted result back to the
* client program:
*/
z = sendto(s, /* Socket */
dtfmt, /* datagram result */
strlen(dtfmt), /* length */
0, /* Flags:no options */
(struct sockaddr *)&adr_clnt,
alen);
if(z<0)
bail("sendto(2)");
/*
* Wait for next packet or timeout:
*
* This is easily accomplished with the
* use of select(2).
*/
do
{
/* Establish Timeout = 8.0 secs */
tmout.tv_sec = 8; /* 8 seconds */
tmout.tv_usec = 0; /* + 0 usec */
/* Wait for read event or timeout */
z = select(s+1,&rx_set,NULL,NULL,&tmout);
}while(z==-1 && errno == ENITR);
/*
* Exit if select(2) return an error
* or if it idictes a timeout:
*/
if(z<=0)
break;
}
/*
* Close the socket and exit:
*/
if(z == -1)
log("%s:select(2) \n",strerror(errno));
else
log("Time out:server exiting.\n");
close(s);
log_close();
return 0;
}
服务器代码只是简单的组织为一个main()程序。
服务器代码显示了一个UDP服务器如何轮询,并且读取更多的数据报直到发生超时。还有其他的方法来实现超时,但是这也许是其中最简单的一个了。
检测简单的TCP包装程序
现在到了介绍我们将会用到的简单的TCP包装程序的源代码了。其程序代码如下:
/*
* wrapper.c
*
* Simple wrappher example:
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "log.h"
#define LOGPATH "/tmp/wrapper.log"
int main(int argc,char **argv,char **envp)
{
int z;
struct sockaddr_in adr_clnt; /* Client */
int alen; /* Address length */
char dgram[512]; /* Receive buffer */
char *str_addr; /* String form of addr */
/*
* We must log denied attempts:
*/
if(log_open(LOGPATH) == -1)
exit(1); /* Can't open log file! */
log("wrapper started.\n");
/*
* Peek at datagram using MSG_PEEK:
*/
alen = sizeof adr_clnt; /* length */
z = recvfrom(0, /* Socket on std input */
dgram, /* Receiving buffer */
sizeof dgram, /* Max recv size */
MSG_PEEK, /* Flags:Peek */
(struct sockaddr *)&adr_clnt,
&alen); /* Addr len, in & out */
if(z<0)
bail("recvfrom(2),peeking at client"
"address.");
/*
* Covert IP address to string form:
*/
str_addr = inet_ntoa(adr_clnt.sin_addr);
if(strcmp(str_addr,"127.7.7.7") != 0)
{
/*
* Not our special 127.7.7.7 address:
*/
log("Address %s port %d rejected.\n",
str_addr,ntohs(adr_clnt.sin_port));
/*
* We must read this packet now without
* the MSG_PEEK option to discard dgram:
*/
z = recvfrom(0, /* Socket */
dgram, /* Receiving buffer */
sizeof dgram, /* Max rcv size */
0, /* No flags */
(struct sockaddr *)&adr_clnt,
&alen);
if(z<0)
bail("recvfrom(2),eating dgram");
exit(1);
}
/*
* Accpet this dgram request,and
* launch the server:
*/
log("Address %s port %d accepted.\n",
str_addr,ntohs(adr_clnt.sin_port));
/*
* inetd has provied argv[0] from the
* config file /etc/inetd.conf:we have
* used this to indicate the server's
* full pathname for this exampel.we
* simply pass any other arguments and
* environment as is.
*/
log("Starting '%s'\n",argv[0]);
log_close(); /* No longer need this */
z = execve(argv[0],argv,envp);
/*
* If control returns,the execve(2)
* failed for some reason:
*/
log_open(LOGPATH); /* Re log */
bail("execve(2),starting server");
return 1;
}
上面所提供的包装程序代码实现了下面的一些简单的安全策略:
如果客户端为127.7.7.7,他的请求可以到达数据报服务器。并没有在客户端的端口上进行限制。
如果客户端地址是其他的IP地址,请求被拒绝并记入日志。
在这里需要注意的一点是,数据报包装程序并不会使用getpeername函数来确定数据报地址。相反,他必须使用MSG_PEEK标记位来调用recvfrom函数。MSG_PEEK标记位返回客户端地址并存入地址结构adr_clnt中,而并不会真正的由这个套接口的输入队列中移除数据报。
简介客户端程序
在这里提供了一个以前的客户端程序的修改版本。这个客户端参数需要两个命令行参数:服务器地址以及要使用的客户端地址。这个额外客户端IP地址的指定可以允许我们使用我们的TCP包装器程序执行更多的试验。修改后的数据报客户端程序如下:
/*
* dgramcln2.c:
*
* Modified datagram client:
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/*
* This function reports the error and
* exits back to the shell:
*/
static void bail(char *on_what)
{
if(errno)
{
fputs(strerror(errno),stderr);
fputs(": ",stderr);
}
fputs(on_what,stderr);
fputc('\n',stderr);
exit(1);
}
int main(int argc,char **argv)
{
int z;
char *srvr_addr = NULL; /* Srvr addr */
char *clnt_addr = NULL; /* Clnt addr */
struct sockaddr_in adr_srvr; /* Server */
struct sockaddr_in adr_clnt; /* Client */
struct sockaddr_in adr; /* AF_INET */
int alen; /* Socket addr lenth */
int s; /* Socket */
char dgram[512]; /* Recv buffer */
/*
* Insist on two command-line arguments
* (without port numbers):
*
* dgramcln2
*/
if(argc != 3)
{
fputs("Usage: dgramclnt "
"\n",stderr);
return 1;
}
srvr_addr = argv[1]; /* 1st arg is srv */
clnt_addr = argv[2]; /* 2nd arg is clnt */
/*
* Create a server socket addrss:
*/
memset(&adr_srvr,0,sizeof adr_srvr);
adr_srvr.sin_family = AF_INET;
adr_srvr.sin_port = htons(9090);
adr_srvr.sin_addr.s_addr = inet_addr(srvr_addr);
if(adr_srvr.sin_addr.s_addr == INADDR_NONE)
bail("bad server address.");
/*
* Create a UDP socket:
*/
s = socket(AF_INET,SOCK_DGRAM,0);
if(s==-1)
bail("socket()");
/*
* Create the specific client address:
*/
memset(&adr_clnt,0,sizeof adr_clnt);
adr_clnt.sin_family = AF_INET;
adr_clnt.sin_port = 0; /* Any port */
adr_clnt.sin_addr.s_addr = inet_addr(clnt_addr);
if(adr_clnt.sin_addr.s_addr == INADDR_NONE)
bail("bad client address.");
/*
* Bind the specific client address:
*/
z = bind(s,(struct sockaddr *)&adr_clnt,sizeof adr_clnt);
if(z==-1)
bail("bind(2) of client address");
/*
* Enter input client loop:
*/
for(;;)
{
/*
* Prompt user for a date formate string:
*/
fputs("\nEnter format string: ",stdout);
if(!fgets(dgram,sizeof dgram,stdin))
break; /* EOF */
z = strlen(dgram);
if(z>0 && dgram[--z] == '\n')
dgram[z] = 0; /* Stomp out newline */
/*
* Send format string to server:
*/
z = sendto(s, /* Socket */
dgram, /* datagram to send */
strlen(dgram), /* dgram length */
0, /* Flags:no options */
(struct sockaddr *)&adr_srvr,
sizeof adr_srvr);
if(z<0)
bail("sendto(2)");
/*
* Wait for a response:
*
* NOTE: Control will hang here if the
* wrapper decides we lack access (no
* response will arrive).
*/
alen = sizeof adr;
z = recvfrom(s, /* Socket */
dgram, /* Receiving buffer */
sizeof dgram, /* Max recv size */
0, /* Flags no options */
(struct sockaddr *)&adr,
&alen); /* Addr len, in & out */
if(z<0)
bail("recfrom(2)");
dgram[z]=0; /* NULL terminate */
/*
* Report Result:
*/
printf("Result from %s port %u :"
"\n\t'%s'\n",
inet_ntoa(adr.sin_addr),
(unsigned)ntohs(adr.sin_port),
dgram);
}
/*
* Close the socket and exit:
*/
close(s);
putchar('\n');
return 0;
}
注意,如果包装器程序认为不可以访问服务器,那么dgramcln2程序就会被挂起。这是因为recvfrom函数并不会从服务器得到应答。当出现这种情况时,我们需要中断我们的程序。
安装与测试包装器
现在我们可以检测我们要用到的所有代码。要开始我们的包装器实验,首先我们需要这些代码:
AMF
$ make
gcc -c -D_GNU_SOURCE -Wall -Wreturn-type dgramisrvr.c
gcc -c -D_GNU_SOURCE -Wall -Wreturn-type log.c
gcc dgramisrvr.o log.o -o dgramisrvr
gcc -c -D_GNU_SOURCE -Wall -Wreturn-type wrapper.c
gcc wrapper.o log.o -o wrapper
TE
gcc -c -D_GNU_SOURCE -Wall -Wreturn-type dgramcln2.c
gcc dgramcln2.o -o dgramcln2
$
通常我们需要root权限才可以安装服务器。然而为了简单的测试,我们可以不用root权限来进行这个实验。要在/tmp目录下创建我们的文件,我们可以用下面的命令:
$ make install
rm -f /tmp/wrapper.log /tmp/dgramisrvr.log
rm -f /tmp/inetd.conf /tmp/wrapper /tmp/dgramisrvr
cp dgramisrvr wrapper /tmp/.
chmod 500 /tmp/wrapper /tmp/dgramisrvr
chmod 600 /tmp/inetd.conf
/usr/sbin/inetd /tmp/inetd.conf
make命令在/tmp目录下放置了一系列的文件,包括一个简单的/tmp/inetd.conf文件。其内容如下:
$ cat /tmp/inetd.conf
9090 dgram udp wait studnt1 /tmp/wrapper /tmp/dgramisrvr
$
也许我们希望再检测一下其他的文件是否正确的安装到了/tmp目录:
$ ls -Itr /tmp | tail
-rw-r----- 1 stdnt1 class1 29454 Feb 19 22:59 xprnKg3RSc
-rw-r--r-- 1 root root 11 Feb 21 23:18 1pq.0002621c
-r-x------ 1 stdnt1 class1 14202 Feb 22 22:48 wrapper
-rw------- 1 stdnt1 class1 53 Feb 22 22:48 inetd.conf
-r-x------ 1 stdnt1 class1 15237 Feb 22 22:48 dgramisrvr
$
从输出中,我们可以看到TCP包装程序wrapper已经被安装了,而dgramisrvr可执行程序也安装到了/tmp目录。
监视日志文件
如果我们正使用X Window会话,那么推荐启动一个终端会话,其命令如下:
$ >/tmp/wrapper.log
$ tail -f /tmp/wrapper.log
这会创建并监视包装器日志文件。我们将会看到当包装器程序向日志文件中写入记录时窗口会进行更新。
在一个新启动的窗口,执行下面命令:
$ >/tmp/dgraimsrvr.log
$ tail -f /tmp/dgramisrvr.log
这会创建并监视服务器日志文件。
如果我们并没有使用X Window会活,我们可以使用多个终端会话来达到同样的目的。
启动我们的inetd守护进程
在我们启动我们的客户端之前,我们必须准备好我们的inetd守护进程。这个守护进程会运行在我们自己的用户ID下,而不需要任何特殊的root权限。然而,我们必须小心的命令行为其指定正确的配置文件,如下所示:
$ /usr/sbin/inetd /tmp/inetd.conf
$
这个命令行参数会通知我们的inetd守护进程使用我们所提供的/tmp/inetd.conf配置文件,而不是/etc/inetd.conf。这个程序会自动的进入后台运行,而我们可以用下面的命令来查看:
$ ps -ef | grep inetd
root 313 1 0 Feb15 ? 00:00:00 inetd
studnt1 12763 1 0 23:04 ? 00:00:00 /usr/sbin/inetd /tmp/inetd.conf
studnt1 12765 11739 0 23:08 pts/3 00:00:00 grep inetd
$
测试包装器
完成了日志的监视之后,现在我们就可以启动客户端并且试验一些内容了。首先,我们测试一些包装器程序可以接受的内容:
$ ./dgramcln2 127.0.0.1 127.7.7.7
Enter format string: %A %B %D
Result from 127.7.7.7 port 9090 :
'Tuesday November 11/09/99'
Enter format string:
这会启动客户端程序,并且将套接口的客户端绑定到IP地址 127.7.7.7,而这正是wrapper程序所可以接受的。包装器日志如下:
$ tail -f /tmp/wrapper.log
[PID 1279] wrapper started.
[PID 1279] Address 127.7.7.7 port 1027 accepted.
[PID 1279] Starting '/tmp/dgramisrvr'
日志表明进程ID为1279,而请求来自127.7.7.7,端口号为1027。因为请求可以接受,服务器/tmp/dgramisrvr来执行请求。
而服务器的日志输出类似下面的内容:
$ tail -f /tmp/dgramisrvr.log
[PID 1279] dgramisrvr started.
[PID 1279] Got request '%A %B %D from 127.7.7.7 port 1027
[PID 1279] Timed out: server exiting.
注意,服务器的进程ID与包装器的相同(包装器进程是由服务器使用execve启动的)。日志记录表明服务器启动并处理请求。最后的记录表明服务器超时退出。
拒绝请求
关闭我们的客户端程序,并使用一个新的地址启动,如下所示:
$ ./dgramcln2 127.0.0.1 127.13.13.13
Enter format string: %D (%B %A)
我们将会看到我们的客户端程序并不会有任何响应。他会挂起,因为wrapper程序拒绝这个请求到在服务器。我们可以中断退出。
wrapper日志文件内容如下:
$ tail -f /tmp/wrapper.log
[PID 1279] wrapper started.
[PID 1279] Address 127.7.7.7 port 1027 accepted.
[PID 1279] Starting '/tmp/dgramisrvr'
[PID 1289] wrapper started.
[PID 1289] Address 127.13.13.13 port 1027 rejected.
我们将会看到下一个数据报请求是由一个新的wrapper进程ID 1289来处理的。最后一行日志记录表明地址127.13.13.13被拒绝。客户端程序挂起,因为wrapper程序丢弃了数据报,从而阻止其被服务器处理。然后wrapper程序退出。
测试服务器超时
要测试服务器的轮询能力,我们必须快速的输入两个日期格式请求(在8秒以内)。如下面的一个会话例子:
$ ./dgramcln2 127.0.0.1 127.7.7.7
Enter format string: %x
Result from 127.7.7.7 port 9090 :
'11/09/99'
Enter format string: %x %X
Result from 127.7.7.7 port 9090 :
'11/09/99 19:11:32'
Enter format string: CTRL+D
$
如果我们足够快,服务器可以在单一的服务器进程内同时处理这两个请求。要检测是否如此,我们可以查看服务器日志:
$ tail -f /tmp/dgramisrvr.log
[PID 1279] dgramisrvr started.
[PID 1279] Got request '%A %B %D' from 127.7.7.7 port 1027
[PID 1279] Timed out: server exiting.
[PID 1294] dgramisrvr started.
[PID 1294] Got request '%x' from 127.7.7.7 port 1027
[PID 1294] Got request '%x %X' from 127.7.7.7 port 1027
[PID 1294] Timed out: server exiting.
最后四行日志记录表明服务器进程ID 1294 可以在超时之前处理两个数据请求。
御载演示程序
要御载演示程序,可以执行下面的命令:
$ make clobber
rm -f *.o core a.out
rm -f /tmp/wrapper.log /tmp/dgramisrvr.log
rm -f /tmp/inetd.conf /tmp/wrapper /tmp/dgramisrvr
rm -f dgramisrvr wrapper dgramcln2
studnt1 12763 1 0 23:04 ? 00:00:00 /usr/sbin/inetd /tmp/inetd.conf
If you see your inetd process running above, you may want to kill it now.
$
Makefile所提供的clobber目标会移除/tmp目录下的所有文件,并且试着显示我们inetd守护进程的ID。在所显示的例子输出中,守护进程的ID为12763。这可以由kill命令来结束:
$ kill 12763
$
数据报弱点
在我们为数据报服务器所设计的包装器中有一个弱点。我们是否看出了这个问题呢?提示:他与服务器轮询有关。
正如在前面所显示的数据报服务器的轮询有一个可以使用包装器概念进行攻击的弱点。当没有服务器进程运行时,包装器程序总是可以在服务器读取数据报之前进行筛选。然而,如果服务器等待更多的数据报,并且只在超时时退出,包装器程序就会被用来筛选这些额外的数据报。这个过程可以总结如下:
1 一个数据报到达,通知。
2 inetd守护进程启动wrapper程序。
3 wrapper程序允许这个数据报,调用exec来启动数据报服务器。
4 数据报服务顺读取并处理数据报。
5 服务器等待其他的数据报。
6 如果一个数据报到达,重复4。
7 否则,服务器超时并退出。
8 重复1。
当服务器继续运行时,进程重复4。这就在步骤3留下了安全检测。如果我们足够快,我们就可以使用前面所提供的例子程序进行演示。
为了更安全,我们可以有下面的一些选择:
使用nowait风格的数据报服务器(这些服务器处理一个数据并退出)。这会强制包装器程序仔细检查所有的请求。
在我们的数据报服务器中使用自定义的代码在接受处理之前测试每一个数据报。
一个折中的办法就是缩短超时间隔,但是这还会留下许多漏洞。
因为这些原因,如果一个同样服务的TCP版本存在,许多站点就会选择禁止数据报服务。如果必须提供数据报服务,安全站点并不会允许他们的数据报服务器循环。这要求改变服务器源代码,或是编写一个自定义的服务器程序。相对应的,服务器程序本身会检测每一个客户端请求的访问权限,而不依赖于TCP包装器的概念。