2008年3月3日星期一

SSH使用及协议分析

SSH使用及协议分析
本文出自:http://xfocus.org 作者: yeye (2001-09-12 07:00:01)

SSH是一个用来替代TELNET、FTP以及R命令的工具包,主要是想解决口令在网上明文传输的问题。为了系统安全和用户
自身的权益,推广SSH是必要的。SSH有两个版本,我们现在介绍的是版本2。



安装SSH

具体步骤如下:


获得SSH软件包。 (ftp://ftp.pku.edu.cn:/pub/unix/ssh-2.3.0.tar.gz)


成为超级用户(root).


# gzip –cd ssh-2.3.0.tar.gz |tar xvf –


# cd ssh-2.3.0


# ./configure

注意,如果你希望用tcp_wrappers来控制SSH,那么在configure时需要加上选项“--with-libwrap=/path/to/libwrap/”,
用来告诉SSH关于libwrap.a 和tcpd.h的位置。


# make


# make install

和SSH有关的程序都放置在/usr/local/bin下,包括ssh,sftp,sshd2, ssh-keygen等。


二、配置

SSH的配置文件在/etc/ssh2下,其中包括sshd2的主机公钥和私钥:hostkey和hostkey.pub。这两个文件通常是在安装SSH时自动生成的。你可以通过下面的命令重新来生成它们:

# rm /etc/ssh2/hostkey*

# ssh-keygen2 –P /etc/ssh2/hostkey

而ssh2_config 文件一般情形下无需修改。


三、启动sshd2

每个要使用SSH的系统都必须在后台运行sshd2。用手工启动:

# /usr/local/bin/sshd2&

可以在“/etc/rc2.d/S99local”中加入该命令,这样系统每次启动时会自动启动sshd2。


四、用tcp_wrappers控制SSH

安装SSH的站点可以用tcp_wrappers来限制哪些IP地址可以通过ssh来访问自己。比如,在/etc/hosts.allow中加入

sshd,sshd2: 10.0.0.1

那么只有10.0.0.1可以通过ssh来访问该主机。


以上都是系统管理员完成的工作。下面我们说说普通用户如何使用SSH。


五、基本应用

每个用户在使用SSH之前,都要完成以下步骤:


在本地主机(比如,local.pku.edu.cn)上生成自己的ssh公钥和私钥。命令如下:

local# ssh-keygen

Generating 1024-bit dsa key pair

1 oOo.oOo.o

Key generated.

1024-bit dsa, teng@ns, Fri Oct 20 2000 17:27:05

Passphrase :************ /*在此输入你的口令,以后访问这台主机时要用。

Again :************ /*

Private key saved to /home1/teng/.ssh2/id_dsa_1024_a

Public key saved to /home1/teng/.ssh2/id_dsa_1024_a.pub


生成的私钥和公钥(id_dsa_1024_a和id_dsa_1024_a.pub)存放在你家目录的~/.ssh2目录下。和用户相关的SSH配
置文件都在~/.ssh2下。私钥由用户保存在本地主机上,而公钥需传送到远地主机的你自己的帐号的~/.ssh2下,如
果你要用ssh2访问本地主机的话。


在~/.ssh2下创建“identification”文件用来说明进行身份认证的私钥。命令如下:


local:~/.ssh2# echo "IdKey id_dsa_1024_a" > identification


3.同样地,在远地主机(比如,remote.pku.edu.cn)上完成上面步骤。

4.将本地(local.pku.edu.cn)下你自己(这里是“teng”)的公钥(id_dsa_1024_a.pub)拷贝到远地主机
(remote.pku.edu.cn)上你自己家目录下的.ssh2目录下,可命名为“local.pub”,一般用ftp上传即可。


在远地主机上,你自己家目录的.ssh2目录下,创建“authorization”文件,其中指定用来进行身份认证的公钥文件。
命令如下:

remote:~/.ssh2# echo “Key local.pub” > authorization


现在你可以从本地用ssh2登录到远地系统了。命令如下:

local# ssh remote.pku.edu.cn

Passphrase for key "/home1/teng/.ssh2/id_dsa_1024_a" with comment "1024-bit dsa,

teng@ns, Fri Oct 20 2000 17:27:05":***********


这时会要你输入你的ssh口令(Passphrase)。验证通过后,即登录到remote主机上。




第一部分:协议概览

整个通讯过程中,经过下面几个阶段协商实现认证连接。

第一阶段:

由客户端向服务器发出 TCP 连接请求。TCP 连接建立后,客户端进入等待,服务器向客户端发送第一个报文,宣告自己
的版本号,包括协议版本号和软件版本号。协议版本号由主版本号和次版本号两部分组成。它和软件版本号一起构成形如:

"SSH-<主协议版本号>.<次协议版本号>-<软件版本号>\n"

的字符串。其中软件版本号字符串的最大长度为40个字节,仅供调试使用。客户端接到报文后,回送一个报文,内容也
是版本号。客户端响应报文里的协议版本号这样来决定:当与客户端相比服务器的版本号较低时,如果客户端有特定的
代码来模拟,则它发送较低的版本号;如果它不能,则发送自己的版本号。当与客户端相比服务器的版本号较高时,客
户端发送自己的较低的版本号。按约定,如果协议改变后与以前的相兼容,主协议版本号不变;如果不相兼容,则主主
协议版本号升高。

服务器接到客户端送来的协议版本号后,把它与自己的进行比较,决定能否与客户端一起工作。如果不能,则断开TCP
连接;如果能,则按照二进制数据包协议发送第一个二进制数据包,双方以较低的协议版本来一起工作。到此为止,这
两个报文只是简单的字符串,你我等凡人直接可读。

第二阶段:

协商解决版本问题后,双方就开始采用二进制数据包进行通讯。由服务器向客户端发送第一个包,内容为自己的 RSA主
机密钥(host key)的公钥部分、RSA服务密钥(server key)的公钥部分、支持的加密方法、支持的认证方法、次协议版本
标志、以及一个 64 位的随机数(cookie)。这个包没有加密,是明文发送的。客户端接收包后,依据这两把密钥和被称
为cookie的 64 位随机数计算出会话号(session id)和用于加密的会话密钥(session key)。随后客户端回送一个包给服
务器,内容为选用的加密方法、cookie的拷贝、客户端次协议版本标志、以及用服务器的主机密钥的公钥部分和服务密钥
的公钥部分进行加密的用于服务器计算会话密钥的32 字节随机字串。除这个用于服务器计算会话密钥的 32字节随机字串
外,这个包的其他内容都没有加密。之后,双方的通讯就是加密的了,服务器向客户端发第二个包(双方通讯中的第一个
加密的包)证实客户端的包已收到。

第三阶段:

双方随后进入认证阶段。可以选用的认证的方法有:

(1) ~/.rhosts 或 /etc/hosts.equiv 认证(缺省配置时不容许使用它);
(2) 用 RSA 改进的 ~/.rhosts 或 /etc/hosts.equiv 认证;
(3) RSA 认证;
(4) 口令认证。

如果是使用 ~/.rhosts 或 /etc/hosts.equiv 进行认证,客户端使用的端口号必须小于1024。

认证的第一步是客户端向服务器发 SSH_CMSG_USER 包声明用户名,服务器检查该用户是否存在,确定是否需要进行认证。
如果用户存在,并且不需要认证,服务器回送一个SSH_SMSG_SUCCESS 包,认证完成。否则,服务器会送一个
SSH_SMSG_FAILURE 包,表示或是用户不存在,或是需要进行认证。注意,如果用户不存在,服务器仍然保持读取从客户端
发来的任何包。除了对类型为 SSH_MSG_DISCONNECT、SSH_MSG_IGNORE 以及 SSH_MSG_DEBUG 的包外,对任何类型的包都以
SSH_SMSG_FAILURE 包。用这种方式,客户端无法确定用户究竟是否存在。

如果用户存在但需要进行认证,进入认证的第二步。客户端接到服务器发来的 SSH_SMSG_FAILURE 包后,不停地向服务器
发包申请用各种不同的方法进行认证,直到时限已到服务器关闭连接为止。时限一般设定为 5 分钟。对任何一个申请,
如果服务器接受,就以 SSH_SMSG_SUCCESS 包回应;如果不接受,或者是无法识别,则以 SSH_SMSG_FAILURE 包回应。

第四阶段:

认证完成后,客户端向服务器提交会话请求。服务器则进行等待,处理客户端的请求。在这个阶段,无论什么请求只要成
功处理了,服务器都向客户端回应 SSH_SMSG_SUCCESS包;否则回应 SSH_SMSG_FAILURE 包,这表示或者是服务器处理请求
失败,或者是不能识别请求。会话请求分为这样几类:申请对数据传送进行压缩、申请伪终端、启动 X11、TCP/IP 端口
转发、启动认证代理、运行 shell、执行命令。到此为止,前面所有的报文都要求 IP 的服务类型(TOS)使用选项
IPTOS_THROUGHPUT。

第五阶段:

会话申请成功后,连接进入交互会话模式。在这个模式下,数据在两个方向上双向传送。此时,要求 IP 的服务类型(TOS)
使用 IPTOS_LOWDELAY 选项。当服务器告知客户端自己的退出状态时,交互会话模式结束。

(注意:进入交互会话模式后,加密被关闭。在客户端向服务器发送新的会话密钥后,加密重新开始。用什么方法加密由
客户端决定。)

第二部分:密钥的交换和加密的启动

在服务器端有一个主机密钥文件,它的内容构成是这样的:

1. 私钥文件格式版本字符串;
2. 加密类型(1 个字节);
3. 保留字(4 个字节);
4. 4 个字节的无符号整数;
5. mp 型整数;
6. mp 型整数;
7. 注解字符串的长度;
8. 注解字符串;
9. 校验字(4 个字节);
10. mp 型整数;
11. mp 型整数;
12. mp 型整数;
13. mp 型整数;

其中 4、5、6 三个字段构成主机密钥的公钥部分;10、11、12、13 四个字段构成主机密钥的私钥部分。9、10、11、12、13
五个字段用字段 2 的加密类型标记的加密方法进行了加密。4 个字节的校验字交叉相等,即第一个字节与第三个字节相等,
第二个字节与第四个字节相等。在服务器读取这个文件时进行这种交叉相等检查,如果不满足这个条件,则报错退出。

服务器程序运行的第一步,就是按照上面的字段划分读取主机密钥文件。随后生成一个随机数,再调用函数

void rsa_generate_key
(
RSAPrivateKey *prv,
RSAPublicKey *pub,
RandomState *state,
unsigned int bits
);

生成服务密钥,服务密钥也由公钥和私钥两部分组成。上面的这个函数第一个指针参数指向服务密钥的私钥部分,第二个指
向公钥部分。然后把主机密钥的公钥部分和服务密钥的公钥部分发送给客户端。在等到客户端回应的包后,服务器用自己的
主机密钥的私钥部分和服务密钥的私钥部分解密得到客户端发来的 32 字节随机字串。然后计算自己的会话号,并用会话号
的前 16字节 xor 客户端发来的 32 字节随机字串的前 16 字节,把它作为自己的会话密钥。注意,服务器把8个字节的
cookie、主机密钥的公钥部分、和服务密钥的公钥部分作为参数来计算自己的会话号。

再来看客户端。客户端启动后的第一步骤也是读取主机密钥。然后等待服务器主机密钥、服务密钥、和 8个字节的cookie。
注意,服务器发送来的只是主机密钥和服务密钥的公钥部分。接到包后,客户端立即把从服务器端收到cookie、主机密钥、
和服务密钥作为参数计算出会话号。从上面可以看出,服务器和客户端各自计算出的会话号实际是一样的。

随后,客户端检查用户主机列表和系统主机列表,查看从服务器收到的主机密钥是否在列表中。如果不在列表中,则把它加
入列表中。然后就生成 32 字节的随机字串,这个32 字节的随机字串就是客户端的会话密钥。客户端用 16字节的会话密钥
xor 它的前 16 字节,把结果用服务器的主机密钥和服务密钥进行双重加密后发送给服务器。产生 32字节随机字串时,随
机数种子由两部分组成,其中一部分从系统随机数种子文件中得到,这样来避免会话密钥被猜出。从上面服务器和客户端各
自计算会话密钥的过程可以看出,服务器和客户端计算出的会话密钥是一样的。

上面的这几步,总结起来就要交换确定会话密钥,因为无论是 des、idea、3des、arcfour、还是 blowfish 都是对称加密方
法,只有一把密钥,双方都知道了会话密钥才能启动加密。但会话密钥不能在网络上明文传送,否则加密就失去意义了。于
是使用 RSA 公钥体系对会话密钥进行加密。

RSA 公钥体系的办法是用公钥加密私钥解密,它依据这样的数学定理:

若 p、q 是相异的两个质数,整数 r 和 m 满足
rm == 1 (mod (p-1)(q-1))
a 是任意的整数,整数 b、c 满足 b == a^m (mod pq),
c == b^r (mod pq)。则
c == a (mod pq)。

具体实现是这样的:

(1) 找三个正整数 p、q、r,其中 p、q 是相异的质数,
r 是与(p-1)、(q-1)互质的数。这三个数 p、q、r
就是私钥(private key)。
(2) 再找一个正整数 m 满足 rm == 1 (mod(p-1)(q-1))。
计算 n = pq,m、n 就是公钥(public key)。
(3) 被加密对象 a 看成是正整数,设 a <>= n,
将 a 表示成 s (s < s =" 2^t)" b ="="" c ="="" c =" a。">q);
mpz_init(&prv->p);
mpz_init(&prv->e);
mpz_init(&prv->d);
mpz_init(&prv->u);
mpz_init(&prv->n);
mpz_init(&test);
mpz_init(&aux);

/* 计算质数 p、q 的位数 */
pbits = bits / 2;
qbits = bits - pbits;

retry0:

fprintf(stderr, "Generating p: ");

/* 生成随机质数 p */
rsa_random_prime(&prv->p, state, pbits);

retry:

fprintf(stderr, "Generating q: ");

/* 生成随机质数 q */
rsa_random_prime(&prv->q, state, qbits);

/* 判断是否 p == q,如果是返回重新生成 */
ret = mpz_cmp(&prv->p, &prv->q);
if (ret == 0)
{
fprintf(stderr,
"Generated the same prime twice!\n");
goto retry;
}
if (ret > 0)
{
mpz_set(&aux, &prv->p);
mpz_set(&prv->p, &prv->q);
mpz_set(&prv->q, &aux);
}

/* 确定 p、q 是否很接近 */
mpz_sub(&aux, &prv->q, &prv->p);
mpz_div_2exp(&test, &prv->q, 10);
if (mpz_cmp(&aux, &test) <>p, &prv->q);
if (mpz_cmp_ui(&aux, 1) != 0)
{
fprintf(stderr,
"The primes are not relatively prime!\n");
goto retry;
}

/* 从质数 p、q 导出私钥 */
fprintf(stderr, "Computing the keys...\n");
derive_rsa_keys(&prv->n, &prv->e, &prv->d,
&prv->u, &prv->p, &prv->q, 5);
prv->bits = bits;

/* 从质数 p、q 导出公钥 */
pub->bits = bits;
mpz_init_set(&pub->n, &prv->n);
mpz_init_set(&pub->e, &prv->e);

/* 测试公钥和密钥是否有效 */
fprintf(stderr, "Testing the keys...\n");
rsa_random_integer(&test, state, bits);
mpz_mod(&test, &test, &pub->n); /* must be less than n. */
rsa_private(&aux, &test, prv);
rsa_public(&aux, &aux, pub);
if (mpz_cmp(&aux, &test) != 0)
{
fprintf(stderr,
"**** private+public failed to decrypt.\n");
goto retry0;
}

rsa_public(&aux, &test, pub);
rsa_private(&aux, &aux, prv);
if (mpz_cmp(&aux, &test) != 0)
{
fprintf(stderr,
"**** public+private failed to decrypt.\n");
goto retry0;
}

mpz_clear(&aux);
mpz_clear(&test);

fprintf(stderr, "Key generation complete.\n");
}
_______________________________________________________

在上面的函数成一对密钥时,首先调用函数
_______________________________________________________

void rsa_random_prime
(
MP_INT *ret, RandomState *state,
unsigned int bits
)
{
MP_INT start, aux;
unsigned int num_primes;
int *moduli;
long difference;

mpz_init(&start);
mpz_init(&aux);

retry:

/* 挑出一个随机的足够大的整数 */
rsa_random_integer(&start, state, bits);

/* 设置最高的两位 */
mpz_set_ui(&aux, 3);
mpz_mul_2exp(&aux, &aux, bits - 2);
mpz_ior(&start, &start, &aux);
/* 设置最低的两位为奇数 */
mpz_set_ui(&aux, 1);
mpz_ior(&start, &start, &aux);

/* 启动小质数的 moduli 数 */
moduli = malloc(MAX_PRIMES_IN_TABLE * sizeof(moduli[0]));
if (moduli == NULL)
{
printf(stderr, "Cann't get memory for moduli\n");
exit(1);
}
if (bits < num_primes =" 0;" num_primes =" 0;" difference =" 0;"> 0x70000000)
{
fprintf(stderr, "rsa_random_prime: "
"failed to find a prime, retrying.\n");
if (moduli != NULL)
free(moduli);
else
exit(1);
goto retry;
}

/* 检查它是否是小质数的乘积 */
for (i = 0; i <>= small_primes[i])
moduli[i] -= small_primes[i];
if (moduli[i] + difference == 0)
break;
}
if (i < phi =" (p">= 0)
{
fprintf(stderr, "Warning: G=");
mpz_out_str(stdout, 10, &G);
fprintf(stderr,
" is large (many spare key sets); key may be bad!\n");
}

/* F = phi / G; the number of relative prime
numbers per spare key set. */
mpz_div(&F, φ, &G);

/* Find a suitable e (the public exponent). */
mpz_set_ui(e, 1);
mpz_mul_2exp(e, e, ebits);
mpz_sub_ui(e, e, 1); /*make lowest bit 1, and substract 2.*/
/* Keep adding 2 until it is relatively prime
to (p-1)(q-1). */
do
{
mpz_add_ui(e, e, 2);
mpz_gcd(&aux, e, φ);
}
while (mpz_cmp_ui(&aux, 1) != 0);

/* d is the multiplicative inverse of e, mod F.
Could also be mod (p-1)(q-1); however, we try to
choose the smallest possible d. */
mpz_mod_inverse(d, e, &F);

/* u is the multiplicative inverse of p, mod q,
if p < n =" p" bytes =" (bits" str =" xmalloc(bytes" i =" 0;" bytes =" (host_key_bits" buf =" xmalloc(bytes);" i =" len;">= 4; i -= 4)
{
unsigned long limb = mpz_get_ui(&aux);
PUT_32BIT(buf + i - 4, limb);
mpz_div_2exp(&aux, &aux, 32);
}
for (; i > 0; i--)
{
buf[i - 1] = mpz_get_ui(&aux);
mpz_div_2exp(&aux, &aux, 8);
}
mpz_clear(&aux);
}

随后客户端计算会话密钥,计算过程是首先生成32个字节即256位随机字串:

for (i = 0; i < i =" 0;"> /usr/openwin/bin/xauth list
***.***.***/unix:10 MIT-MAGIC-COOKIE-1 \
92b404e556588ced6c1acd4ebf053f68
***.***.***/unix:11 MIT-MAGIC-COOKIE-1 \
92b404e556588ced6c1acd4ebf053f68
***.***.***:10 MIT-MAGIC-COOKIE-1 \
92b404e556588ced6c1acd4ebf053f68
***.***.***/unix:10 MIT-MAGIC-COOKIE-1 \
92b404e556588ced6c1acd4ebf053f68
***.***.***:11 MIT-MAGIC-COOKIE-1 \
92b404e556588ced6c1acd4ebf053f68
***.***.***/unix:11 MIT-MAGIC-COOKIE-1 \
92b404e556588ced6c1acd4ebf053f68
[wangdb@ /home/wangdb]> echo $DISPLAY
***.***.***:10.0
[wangdb@ /home/wangdb]> /usr/openwin/bin/xauth
Using authority file /home/wangdb/.Xauthority
xauth> list ***.***.***:10.0
***.***.***:10 MIT-MAGIC-COOKIE-1 \
92b404e556588ced6c1acd4ebf053f68
xauth> quit
[wangdb@ /home/wangdb]>

.Xauthority 文件的显示记录里各个字段的含义如下,第一个字段的***.***.*** 是主机名,":"号后的"."前面的数字
是 X 服务器标号,"."后面的数字是显示屏幕(显示器)标号。这个字段称为显示名,$DISPLAY 环境变量里填入这个字段。
第二个字段是协议标号,第三个字段是十六进制的认证钥。认证钥是由系统给的,打开 X 显示时如果认证钥给的不对,
X 服务器拒绝处理显示请求。

ssh 实现 X 转发的第一步是,客户端调用 popen 函数执行 "xauth list $DISPLAY" 命令,读取 X 显示的屏幕号、协议
号、和认证钥,然后把协议号和认证钥保存在内存中。客户端并不把自己的认证钥发送给服务器,而是生成一个 8位二进
制随机数序列,以十六进制打印,把这个十六进制数字串发送给服务器作为认证钥。等到服务器发来打开 X 显示请求时,
客户端使用自己真正的认证钥打开 X 显示。采用这种方法,客户保证了自己的认证钥不会泄露给外界,安全性得到保证。

服务器接到客户端的 X 转发请求后,读取客户端发来的屏幕号、协议号、和认证钥,然后打开一个 socket 并绑定它,
设置成侦听模式,并用这个 socket 设置一个通道。随后就从服务器自己的配置文件读出 X 服务器标号,调用
gethostname函数获取本机主机名,把这两者和客户发来的屏幕号结合在一起构成显示列表记录的第一字段。

在服务器处理客户端执行命令或启动 shell 的请求时,它用前面设置的通道接受一个 TCP 连接,返回一个 socket,
再用这个 socket 设置一个新通道。然后发一个包给客户端要求它打开一个 X 显示。客户端接到这个包后打开一个
socket 与本地 X 服务器连接,即打开一个 X 显示:
_____________________________________________________
int display_number, sock;
const char *display;
struct sockaddr_un ssun;

/* Try to open a socket for the local X server. */
display = getenv("DISPLAY");
if (!display)
{
error("DISPLAY not set.");
goto fail;
}

/* Now we decode the value of the DISPLAY variable
* and make a connection to the real X server.
*/

/* Check if it is a unix domain socket. Unix domain
* displays are in one of the following formats:
* unix:d[.s], :d[.s], ::d[.s]
*/
if (strncmp(display, "unix:", 5) == 0 ||
display[0] == ':')
{
/* Connect to the unix domain socket. */
if (sscanf(strrchr(display, ':') + 1,
"%d", &display_number) != 1)
{
error("Could not parse display number "
"from DISPLAY: %.100s", display);
goto fail;
}
/* Create a socket. */
sock = socket(AF_UNIX, SOCK_STREAM, 0);
if (sock < sun_family =" AF_UNIX;">

没有评论: