使用 Stunnel 加密——如何在 SSL 内加密任意 TCP 连接

大多数有网络意识的程序都应该使用密码术保护数据,以免数据被偷窥。但许多程序都没有这样做,可能是因为程序本身是旧应用程序,或者因为安全套接字层(Secure Sockets Layer,SSL)很难加入到应用程序中。Stunnel 是一种程序,使程序员和系统管理员可以很轻松地对任意 TCP 会话加密。您可以很轻松地在客户机和服务器上启用 SSL ― 而且这样做不会影响程序源代码。

SSL 挑战

在大多数开发环境中,并不是在产品的整个生命周期中都考虑安全性而是在出现安全性问题后才采取补救措施。从传统的观点来看,这是坏事 ― 事后亡羊补牢很困难,就象向一个写得马马虎虎的软件添加一个可靠性模块来除去错误一样困难。

象这些亡羊补牢技术从安全性立场来看好像是可行的,但实际上不太可靠。例如,您可以写一段代码,建立网络连接,然后连接到一个库 ― 该库用执行加密和认证的版本代替所有的传统网络调用。

实际上,这种集成的容易性是安全套接字层,或称为 SSL(用于 secure HTTP 连接的协议)的最初目的之一。SSL 的几种实现已经在试图顺便替代标准 Berkeley UNIX socket API,或者带有尽可能相似接口的库。

OpenSSL 库(请参阅 参考资料)是尝试后一种方法的一个很好的示例。在 OpenSSL 库中,相似的 API 模仿传统的套接字调用,使用 SSL 上下文对象代替文件描述符。例如,传统的写入套接字的调用具有以下特征:

/* Returns the number of characters successfully written */

size_t write(int file_descriptor, void *buf, size_t len)

 

OpenSSL 更改每个参数的类型,但每个参数的语义不变:

/* Returns the number of characters successfully written */

int SSL_write(SSL *socket_info, char *buf, int len)

 

实际上,除 SSL 对象之外,其它所有的类型与初始调用都是兼容的。理想情况下,开发者能够对程序进行较小的修改,只需添加一些代码从文件描述符初始化 SSL 上下文即可。

而事实上,SSL 库都不容易使用。例如,开发者要写许多附加代码才能使 OpenSSL 在多线程环境下工作。实际上,为将这个库集成到代码中去,大多数开发组织花费的精力都比他们预计的要多的多,而结果还常常是一片混乱。


 

 

 

回页首

Stunnel

幸运的是,有一种方法可以将加密功能无缝添加到网络连接中,而不会将您原来的代码段基址搞乱。Stunnel 是一个程序,可以使用 OpenSSL 库对任意 TCP 会话进行加密。它作为服务器运行在程序外部。Stunnel 服务器主要执行两个功能:一,首先,接收未加密的数据流,进行 SSL 加密,然后将其通过网络发送;二,对已进行 SSL 加密的数据流进行解密,并将其通过网络发送给另一个程序(该程序通常驻留在同一机器上,以避免本地网络上的窥探攻击)。

这样,在必要时,您就可以很容易地运行未加密的程序,当您想要“嗅探”网络,看看到底有什么东西正在通过网络时,这一点很有用。

即使您是一个系统管理员,而不是一个开发者,Stunnel 对您来说也是一个强大的武器,因为它可以向不启用 SSL 的服务器端软件添加 SSL。例如,我已经使用 Stunnel 来保护 POP、SMTP 和 IMAP 服务器。唯一不太尽人意的地方是要使用这些服务器的安全版本,客户机必须是可识别 SSL 的。

Stunnel 要求已经安装了 OpenSSL。它已被移植到了 Windows,以及大多数 UNIX 平台。

一旦安装了 Stunnel,用它来保护服务器就很轻松。例如,您可以通过将常规服务绑定到本地主机使 IMAP 服务器启用 SSL,然后在外部 IP 地址(假设 IMAP 服务器已经在运行,且外部地址为 192.168.100.1)运行 Stunnel:

stunnel -d 192.168.100.1:imap2 -r 127.0.0.1:imap2

 

-d 标志指定我们希望用来运行自己的安全服务的端口。 imap2 字符串指定使用标准 IMAP 端口;我们也可以将其设为 143。Stunnel 检查 "/etc/services" 文件以便将符号名映射到端口号。并非所有的机器都拥有这个文件(有些机器并不列出所有的标准服务),所以使用数字比使用服务名更方便。

-r 标志指定未加密的 IMAP 服务器运行所在的端口。

这个解决方案要求您的 IMAP 服务器只在回送(loopback)接口上侦听。如果 IMAP 服务器绑定到 IP 地址“0.0.0.0”,那么它将侦听机器上每个 IP 地址上的信息,包括 192.168.100.1;这会导致出现一条出错消息,指出我们的安全服务端口已在使用中。大多数服务都可以配置为只绑定到一个接口。不然的话,可能要更改一行代码。

另外,您可以将一个未加密的服务器设在一个非标准端口上。例如,您可以在端口 1143 上运行 IMAP,然后将安全的 IMAP 数据流转发到该端口。一般情况下,您不希望其它机器上的用户访问您未加密的服务。运行服务的机器上的个人防火墙可以加强这种策略。

使用 Stunnel 来保护如 IMAP 等服务面临的一个问题是服务器只接收来自我们提供的 Stunnel 服务器的连接。因此,所有的连接都看起来好象是来自本地机器。在 Linux 上,可以通过传递 -T 标志避开这个问题,传递 -T 标志可以使 Stunnel 服务器透明地代理信息包,这样真正的服务器就可以得到接收到的所有信息包中的正确的源地址。


 

 

 

回页首

用于客户机的 Stunnel

还可以使用客户机的 Stunnel 与服务器连接,不过要多做一些工作。首先,必须生成 Stunnel 作为外部进程。在基于 UNIX 的系统上,执行这个操作的最好方法是 fork() 客户机,并让子进程 execv() stunnel 。父进程必须准备两套文件描述符用来与子进程通信 ― 一对用于从 Stunnel 进程读取数据,另一对用于通过网络发送数据。这个工作量不小。实现这项功能的示例代码,提供一个简单的函数调用 run_cmd ,掩盖潜在的复杂性; run_cmd 使用一个字符串指出要运行的命令,并返回一个 PIPE 对象,该对象有一个文件描述符,套接字使用该文件描述符进行读写操作:

pipe.h:

#ifndef POPEN_H__
#define POPEN_H__

#include <sys/types.h>
#include <stdio.h>

#define EXITVAL        127

typedef struct pipe_st {
  FILE           *read_ptr;
  FILE           *write_ptr;
  pid_t           pid;
} PIPE;

PIPE *run_cmd(char *cmd);
int   pipe_close(PIPE *p);
#endif POPEN_H__

pipe.c:

#include <sys/wait.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <errno.h>
#include <stdlib.h>

#include "pipe.h"

/* We allow double quotes and \ to escape spaces.
 * All backslashes are "processed", despite the value
 * of the next character. (Though \\ -> \).
 * We don't care if there's a missing trailing quote,
 * even if it should really be a syntax error.
 */
static char **
to_words(char *arg) {
  char **arr;
  char  *p = arg;
  int    nw = 1;
  int    slc = 0;
  int    slm = 0;
  char   c;
  short  quote = 0;
  char  *cur;

  /* Build a rough approximation of the number of words,
   * simply so we don't malloc too low.
   */
  while((c = *p++)) {
    if(c == '"' || c == ' ') {
      nw++;
      if(slm < slc) slm = slc;
      slc = 0;
    }
  }
  arr = (char **)malloc(sizeof(char *)*(nw+1));
  quote = nw = slc = 0;
  p = arg;
  cur = (char *)malloc(sizeof(char)*(slm+1));
  arr[nw++] = cur;
  while((c = *p++)) {
    switch(c) {
    case '"':
      quote = !quote;
      continue;
    case ' ':
      if(quote) {
	*cur++ = c;
	slc++;
	continue;
      } else {
	if(!slc) continue;
	*cur = 0;
	cur = (char *)malloc(sizeof(char)*(slm+1));
	arr[nw++] = cur;
	slc = 0;
	continue;
      }
    case '\\':
      if(*p) {
	*cur++ = *p++;
	slc++;
	continue;
      }
    default:
      *cur++ = c;
      slc++;
      continue;
    }
  }
  *cur = 0;
  arr[nw] = 0;
  return arr;
}

PIPE *
run_cmd(char *cmd) {
  int    prpd[2];
  int    pwpd[2];
  pid_t  pid;
  char **args;
  PIPE  *ret;

  args = to_words(cmd);

  if(pipe(prpd) < 0 || pipe(pwpd) < 0) {
    return 0; /* Pipe failed. */
  }

  pid = fork();
  switch(pid) {
  case -1:
    close(prpd[STDIN_FILENO]);
    close(prpd[STDOUT_FILENO]);
    close(pwpd[STDIN_FILENO]);
    close(pwpd[STDOUT_FILENO]);
    return 0; /* Fork failed. */
    /* Here we can only exit on error. */
  case 0:
    /* Child...  */
    if(dup2(pwpd[STDIN_FILENO], STDIN_FILENO) < 0) {
      exit(EXITVAL);
    }
    if(dup2(prpd[STDOUT_FILENO], STDOUT_FILENO) < 0) {
      exit(EXITVAL);
    }

    close(pwpd[STDIN_FILENO]);
    close(pwpd[STDOUT_FILENO]);
    close(prpd[STDIN_FILENO]);
    close(prpd[STDOUT_FILENO]);
    execv(args[0], args);
    exit(EXITVAL);
  default:
    ret = (PIPE *)malloc(sizeof(PIPE));
    ret->read_ptr = ret->write_ptr = 0;
    ret->pid = pid;
    close(pwpd[0]);
    fcntl(pwpd[1], F_SETFD, FD_CLOEXEC);
    ret->write_ptr = fdopen(pwpd[1], "wb");
    if(!ret->write_ptr) {
      int old = errno;
      kill(pid, SIGKILL);
      close(pwpd[1]);
      waitpid(pid, 0, 0);
      errno = old;
      free(ret);
      return 0;
    }
    close(prpd[1]);
    fcntl(prpd[0], F_SETFD, FD_CLOEXEC);
    ret->read_ptr = fdopen(prpd[0], "rb");
    if(!ret->read_ptr) {
      int old = errno;
      kill(pid, SIGKILL);
      close(prpd[0]);
      waitpid(pid, 0, 0);
      errno = old;
      free(ret);
      return 0;
    }
    return ret;
  }
}

int
pipe_close(PIPE *p) {
  int status;

  if(!(p->read_ptr || p->write_ptr)) {
    return -1;
  }
  if(p->read_ptr && fclose(p->read_ptr)) {
    return -1;
  }
  if(p->write_ptr && fclose(p->write_ptr)) {
    return -1;
  }
  if(waitpid(p->pid, &status, 0) != p->pid) {
    return -1;
  }
  p->read_ptr = p->write_ptr = 0;
  return status;
}

 

例如,要使用此函数建立一个连接到上面启用 SSL 的 IMAP 服务器的客户机,我们可以编写如下代码:

PIPE *p;

p = run_cmd("stunnel -c -r 192.168.100.1:imap2 -A /etc/ca_certs -v 3");

 

在上面的代码中,我们这样调用 Stunnel:

stunnel -c -r 192.168.100.1:imap2 -A /etc/ca_certs -v 3

 

最后两个选项并非必需;客户机可以不考虑这两个选项进行连接。但是,如果我们省去了这两个选项,客户机将无法对服务器证书进行充分的验证,会使客户机随时可能遭到 man-in-the-middle 攻击。在这种攻击中,有人创建了一个虚假服务器,使客户机把它当作真正的服务器,而与之进行通信。虚假服务器代理到真正的服务器的连接,读取所有的数据。使用现成的工具如 Dsniff(请参阅 参考资料)很容易发动这种攻击。

v 参数指定验证级别。缺省值为 0,适用于使用其它方法验证客户机的服务器,也是如此。但级别 1 和级别 2 更好一些。级别 1 检查(非强制地)服务器证书是否有效,但可与无证书的服务器连接。级别 2 要求有效的服务器证书,但并不检查证书是否由权威认证中心如 VeriSign 签署。

A 参数指定一个必须包含可信证书列表的文件。一个服务器证书要被接受,它必须是在 A 参数指定的文件内,或者是一个用于签署推荐证书(通常是来自权威认证中心,或称 CA,如 VeriSign 的证书)的证书,该证书必须在指定的文件内。在 参考资料部分,您会找到几个主要认证中心的当前有效证书的链接,您可以将它作为必需内容放在这个文件中。

在使用大型 CA 时,甚至这种方法也会出现问题。例如,确保某个特定的证书是由 VeriSign 签署的是不错的。但您如何确保证书是来自您想要连接的站点呢?

不幸的是,在写这篇文章的时候,Stunnel 还不能使调用程序访问验证过的证书的信息。因此,您无法确定服务器正在使用的是谁的证书。例如,您可能如愿以偿被连接到 Amazon,或者可能被连接到一个服务器,该服务器使用的是偷来的(但有效的)Microsoft 证书。

由于这种局限性,您只能局限于下面四个选择:

  1. 希望没有人发动 man-in-the-middle 攻击(不正确的想法)。
  2. 在客户机端对每个可能的服务器证书进行硬编码(Hardcode)。
  3. 运行自己的认证中心,这样可以动态地添加可信的服务器。
  4. 在应用程序中使用 SSL 代替外部的通道,这样可以通过编程检查。

即使这些解决方案都可使用,也没有一个理想的。


 

 

 

回页首

结论

安全性问题要比我们想象的困难的多。理想情况是,我们单击一下按钮就可保证自己的应用程序安全。在对网络连接加密时,值得称赞的是 Stunnel 接近了“单击 ― 安全”的理想境界,允许我们向在其它方面已经完整的应用程序添加安全性。

不幸的是,Stunnel 有一些局限性。在服务器端,它当前只能够透明地代理 Linux 客户机。在客户机端,不容易执行充分的证书验证。

尽管如此,Stunnel 仍是实用价值很高的实用程序,应该成为程序员,同样也是系统管理员的看家法宝。如果您正在开发自己的软件,应该能够很容易地将 Stunnel 集成到程序中。服务器代码不需修改,使用本文中提供的框架通常可以很容易地对客户机代码进行改写。

参考资料

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文.
  • OpenSSL Project依靠大家的共同努力旨在开发一个健壮的、商务级的、全功能的开放源代码工具箱,用来实现安全套接字层(SSL v2/v3)和传输层安全(TLS v1)协议以及功能强大的通用加密库。
  • 可从 http://www.stunnel.org/获取 Stunnel。
  • 要获取 CA 证书列表,请访问 http://www.columbia.edu/~ariel/good-certs/ns45/
  • 这里有用来导出 Netscape 的 CA 证书的 Perl 脚本
  • 您可以在 monkey.org找到 Dug Song 的 dsniff 2.3 发行版。
  • Larry Loeb 的关于 dsniff 的 developerWorks 系列(分两部分)向您展示了最近的更新是怎样增加 man-in-the-middle 攻击风险的。 第 1 部分介绍了这个网络探测工具的原理,以及如何识别是否有人在“嗅探”您。 第 2 部分提供对抗嗅探器的工具和策略。
  • 查找最新的 IBM security news
  • Intel Common Data Security Architecture可通过减少上市时间和提供测试过性能的互操作性提高您的提供加强安全性解决方案的能力。

关于作者

作者 John Viega ( viega@list.org) 与人合著了 Building Secure Software(Addison-Wesley,2001)和 Java Enterprise Architecture(O'Reilly and Associates, 2001)。John 已经写了 50 多本技术出版物,主要是关于软件安全性方面的技术。 他还写了 Mailman, the GNU Mailing List Manager 和 ITS4,一种在 C 和 C++ 代码中查找安全弱点的工具。

标签: Stunnel, 加密, SSL, TCP 连接

添加新评论