SSH使用及协议分析 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 >;= n, 将 a 表示成 s (s < n,通常取 s = 2^t) 进制的, 然后对每一位分别编码。 (4) 加密:计算 b == a^m (mod n) (0 <= b < n),b 为 加密结果。 (5) 解密:计算 c == b^r (mod n) (0 <= c < n),c 为 解密结果。 从上面的数学定理可知,最后结果 c = a。 计算 RSA 密钥的方法及过程是,调用下面的函数计算 RSA公钥和 RSA 私钥: _______________________________________________________ void rsa_generate_key ( RSAPrivateKey *prv, RSAPublicKey *pub, RandomState *state, unsigned int bits ) { MP_INT test, aux; unsigned int pbits, qbits; int ret; mpz_init(&prv->;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) < 0) { fprintf(stderr, "The primes are too close together.\n"); goto retry; } /* Make certain p and q are relatively prime (in case one or both were false positives... Though this is quite impossible). */ mpz_gcd(&aux, &prv->;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 < 16) num_primes = 0; /* Don\'t use the table for very small numbers. */ else { for (num_primes = 0; small_primes[num_primes] != 0; num_primes++) { mpz_mod_ui(&aux, &start, small_primes[num_primes]); moduli[num_primes] = mpz_get_ui(&aux); } } /* 寻找一个数,它不能被小质数整除 */ for (difference = 0; ; difference += 2) { unsigned int i; if (difference >; 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 < num_primes; i++) { while (moduli + difference >;= small_primes) moduli -= small_primes; if (moduli + difference == 0) break; } if (i < num_primes) continue; /* Multiple of a known prime. */ /* 检查通过 */ fprintf(stderr, "."); /* Compute the number in question. */ mpz_add_ui(ret, &start, difference); /* Perform the fermat test for witness 2. This means: it is not prime if 2^n mod n != 2. */ mpz_set_ui(&aux, 2); mpz_powm(&aux, &aux, ret, ret); if (mpz_cmp_ui(&aux, 2) == 0) { /* Passed the fermat test for witness 2. */ fprintf(stderr, "+"); /* Perform a more tests. These are probably unnecessary. */ if (mpz_probab_prime_p(ret, 20)) break; /* It is a prime with probability 1 - 2^-40. */ } } /* Found a (probable) prime. It is in ret. */ fprintf(stderr, "+ (distance %ld)\n", difference); /* Free the small prime moduli; they are no longer needed. */ if (moduli != NULL) free(moduli); else exit(1); /* Sanity check: does it still have the high bit set (we might have wrapped around)? */ mpz_div_2exp(&aux, ret, bits - 1); if (mpz_get_ui(&aux) != 1) { fprintf(stderr, "rsa_random_prime: high bit not set, retrying.\n"); goto retry; } mpz_clear(&start); mpz_clear(&aux); } _______________________________________________________ 随机产生一对大质数(p,q)。这对随机大质数要符合的条件是p 必须小于 q。然后调用下面的函数来生成公钥和私钥对的 其他组员: static void derive_rsa_keys ( MP_INT *n, MP_INT *e, MP_INT *d, MP_INT *u, MP_INT *p, MP_INT *q, unsigned int ebits ) { MP_INT p_minus_1, q_minus_1, aux, phi, G, F; assert(mpz_cmp(p, q) < 0); mpz_init(&p_minus_1); mpz_init(&q_minus_1); mpz_init(&aux); mpz_init(&phi); mpz_init(&G); mpz_init(&F); /* 计算 p-1 和 q-1. */ mpz_sub_ui(&p_minus_1, p, 1); mpz_sub_ui(&q_minus_1, q, 1); /* phi = (p - 1) * (q - 1) */ mpz_mul(&phi, &p_minus_1, &q_minus_1); /* G is the number of "spare key sets" for a given modulus n. The smaller G is, the better. The smallest G can get is 2. */ mpz_gcd(&G, &p_minus_1, &q_minus_1); if (mpz_cmp_ui(&G, 100) >;= 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, &phi, &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, &phi); } 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 < q. It is used when doing private key RSA operations using the chinese remainder theorem method. */ mpz_mod_inverse(u, p, q); /* n = p * q (the public modulus). */ mpz_mul(n, p, q); /* Clear auxiliary variables. */ mpz_clear(&p_minus_1); mpz_clear(&q_minus_1); mpz_clear(&aux); mpz_clear(&phi); mpz_clear(&G); mpz_clear(&F); } _______________________________________________________ 最后为检验所生成的一对密钥的有效性,它调用下面的函数产生一个随机整数。 _______________________________________________________ void rsa_random_integer(MP_INT *ret, RandomState *state, unsigned int bits) { unsigned int bytes = (bits + 7) / 8; char *str = xmalloc(bytes * 2 + 1); unsigned int i; /* 生成一个适当大小的16进制随机数,把它转化成mp型整数 */ for (i = 0; i < bytes; i++) sprintf(str + 2 * i, "%02x", random_get_byte(state)); /* 转化到内部表示 */ if (mpz_set_str(ret, str, 16) < 0) { fprintf("Intenal error, mpz_set_str returned error"); exit(1); } /* Clear extra data. */ memset(str, 0, 2 * bytes); if (str != NULL) free(str); else exit(1); /* Reduce it to the desired number of bits. */ mpz_mod_2exp(ret, ret, bits); } _______________________________________________________ 服务密钥生成后,服务器发送一个包把两把密钥发送给客户端,一个是主机密钥的公钥,另一个是服务密钥的公钥。跟 随这个包一起发送的还有服务器支持的加密类型和8个字节即64位的随机字串 cookie。客户端依据这两把密钥计算会话号, 会话号长16字节即128位。计算方法是: 会话号 = MD5(主机公钥模数 n || 服务公钥模数 n || cookie) 计算函数是: void compute_session_id ( unsigned char session_id[16], unsigned char cookie[8], unsigned int host_key_bits, MP_INT *host_key_n, unsigned int session_key_bits, MP_INT *session_key_n ) { unsigned int bytes = (host_key_bits + 7) / 8 + (session_key_bits + 7) / 8 + 8; unsigned char *buf = xmalloc(bytes); struct MD5Context md; mp_linearize_msb_first(buf, (host_key_bits + 7 ) / 8, host_key_n); mp_linearize_msb_first(buf + (host_key_bits + 7 ) / 8, (session_key_bits + 7) / 8, session_key_n); memcpy(buf + (host_key_bits + 7) / 8 + (session_key_bits + 7) / 8, cookie, 8); MD5Init(&md); MD5Update(&md, buf, bytes); MD5Final(session_id, &md); xfree(buf); } void mp_linearize_msb_first ( unsigned char *buf, unsigned int len, MP_INT *value ) { unsigned int i; MP_INT aux; mpz_init_set(&aux, value); for (i = len; i >;= 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 < 32; i++) session_key = random_get_byte(state); 然后用16字节的会话号 xor 这32字的随机字串的前16字节,并安 msb 次序来排列构成一个MP型整数: mpz_init_set_ui(&key, 0); for (i = 0; i < 32; i++) { mpz_mul_2exp(&key, &key, 8); if (i < 16) mpz_add_ui(&key,&key, session_key^session_id); else mpz_add_ui(&key,&key, session_key); } 把结果发给服务器。在用服务器发来主机公钥和服务公钥对这个MP型整数作两次 RSA 加密后,客户端发一个包把这个 MP型整数交给服务器。跟随这个包一起还有客户端选定的加密类型。注意,在客户端,它用上面最初的32字节随机串 session_key 来作为会话密钥进行加密,而不是发给服务器的会话密钥 key。服务器接到上面MP型整数后,把它转换 成32字节即256位的字串。再用自己计算出的16字节的会话号xor 这个字串的前16字节,把结果作为会话密钥。服务器 计算自己的16字节会话号时也是把发给客户端的主机公钥、服务公钥、和16字节随机串 cookie 作为输入,因此它计算 出的会话号与客户端计算出的一样。 在这之后,所有的数据传输都用选用客户端指定的加密方法进行加密了,加密时使用上面的会话密钥。加密使用的代码 在 arcfour.c、des.c、idea.c、blowfish.c 中。 ssh 声称避免了 IP 欺骗,使用的方法在上面的密钥交换中服务器给客户端发了一个64位 cookie,要求客户端原样拷贝 送回。看不出这能避免 IP 欺骗。 第三部分:认证 RSA公钥和RSA私钥数据结构为: typedef struct { unsigned int bits; /* 模数大小 */ MP_INT e; /* 公钥指数 */ MP_INT n; /* 模数 */ } RSAPublicKey; typedef struct { unsigned int bits; /* 模数大小 */ MP_INT n; /* 模数 */ MP_INT e; /* 公钥指数 */ MP_INT d; /* 私钥指数 */ MP_INT u; /* Multiplicative inverse of p mod q. */ MP_INT p; /* 质数 p */ MP_INT q; /* 质数 q */ } RSAPrivateKey; RSA 认证的过程是,客户端向服务器提交自己 RSA公钥的模数成员,服务器先读取用户 .ssh 目录中的公钥文件进行有 效性检验,再生成一个 256 位二进制随机数 cookie。随后把这个随机数 cookie 用从公钥文件读出的公钥加密后传给 客户端,客户端接到 cookie 后,先用自己的私钥解密,再对这个 cookie 和会话号计算出 16 字节的 md5水印,把两 个水印相加后发给服务器。服务器把它收到 md5水印和它自己对 cookie 和会话号计算出的水印和进行比较,如果相等, 则认证通过。 第四部分:shell 和 X11 调用 ssh 提供的一个重要功能就是 X 转发功能,它可以在客户端的显示屏上把服务器端 X 程序的运行结果以图形形式显示 出来显示在客户端的显示屏幕上。例如运行 xterm 程序启动一个 X 终端,该 X 终端窗口显示在客户端的显示屏上。 先来看看 X 窗口系统本身的情况。X 窗口系统是 UNIX的图形用户界面(GUI),它采用"客户/服务器"模式,二者之间的 通讯遵从 X 协议。每台主机运行一个 X 服务器,且只能运行一个 X 服务器,但一个 X 服务器可以控制多个显示屏幕 (显示器)。应用程序要想进行图形显示必须以客户的方式向 X 服务器提交显示请求,由 X 服务器统一控制进行显示。 用户运行 X 程序时,实际是调用 XOpenDisplay 库函数打开一个 PF_UNIX 或 TCP socket 连接到 X 服务器,然后通过 这个连接向它提交显示请求。连接建立后, X 客户所做的第一件事就是:按用户的 $DISPLAY 环境变量的值读取用户 配置文件 .Xauthority 中的显示记录,把这条记录的有关内容提交给 X 服务器进行认证。如果认证通过,就可以提交 显示请求了,这个过程称为打开一个 X 显示。作为客户的 X 程序在提交显示请求时,实际上是把 X 显示数据写入上面 打开的 socket。在打开 X 显示时,必须提供协议号、认证钥(hexkey)、和屏幕号,如果 X 服务器不是在本地运行,还 需要提供运行 X 服务器的远程主机名。这些都记录在用户配置文件 .Xauthority 中,所给的协议号、认证钥、和屏幕 号从这个列表中取出。可以用 xauth 命令来查看显示列表里的内容: [wangdb@ /home/wangdb]>; /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 < 0) { error("socket: %.100s", strerror(errno)); goto fail; } /* Connect it to the display socket. */ ssun.sun_family = AF_UNIX; #ifdef HPSUX_NONSTANDARD_X11_KLUDGE { /* HPSUX release 10.X uses * /var/spool/sockets/X11/0 * for the unix-domain sockets, while earlier * releases stores the socket in * /usr/spool/sockets/X11/0 * with soft-link from * /tmp/.X11-unix/`uname -n`0 */ struct stat st; if (stat("/var/spool/sockets/X11", &st) == 0) { sprintf(ssun.sun_path, "%s/%d", "/var/spool/sockets/X11", display_number); } else { if (stat("/usr/spool/sockets/X11", &st) == 0) { sprintf(ssun.sun_path, "%s/%d", "/usr/spool/sockets/X11", display_number); } else { struct utsname utsbuf; /* HPSUX stores unix-domain sockets in * /tmp/.X11-unix/`hostname`0 * instead of the normal /tmp/.X11-unix/X0. */ if (uname(&utsbuf) < 0) fatal("uname: %.100s", strerror(errno)); sprintf(ssun.sun_path, "%.20s/%.64s%d", X11_DIR, utsbuf.nodename, display_number); } } } #else /* HPSUX_NONSTANDARD_X11_KLUDGE */ { struct stat st; if (stat("/var/X", &st) == 0) { sprintf(ssun.sun_path, "%.80s/X%d", "/var/X/.X11-unix", display_number); } else if (stat(X11_DIR, &st) == 0) { sprintf(ssun.sun_path, "%.80s/X%d", X11_DIR, display_number); } else { sprintf(ssun.sun_path, "%.80s/X%d", "/tmp/.X11-unix", display_number); } } #endif /* HPSUX_NONSTANDARD_X11_KLUDGE */ if (connect(sock, (struct sockaddr *)&ssun, AF_UNIX_SIZE(ssun)) < 0) { error("connect %.100s: %.100s", ssun.sun_path, strerror(errno)); close(sock); goto fail; } /* OK, we now have a connection to the display. */ goto success; } success: /* We have successfully obtained a connection to * the real X display. */ #if defined(O_NONBLOCK) && !defined(O_NONBLOCK_BROKEN) (void)fcntl(sock, F_SETFL, O_NONBLOCK); #else /* O_NONBLOCK && !O_NONBLOCK_BROKEN */ (void)fcntl(sock, F_SETFL, O_NDELAY); #endif /* O_NONBLOCK && !O_NONBLOCK_BROKEN */ ______________________________________________________ 随后客户端用这个 socket 设置一个新通道。注意,如果客户端主机的本地没有终端显示器,在这一步,它也按自己 的环境变量 $DISPLAY 的值,打开一个 TCP socket 与远程 X服务器连接。 最后服务器把前面已经构造出的显示列表记录第一字段和客户端发送来的协议号与认证钥结合在一起构成一条显示记 录,置入用户的.Xauthority 文件中。并把 $DIAPLAY 环境变量的值设置为这条记录第一个字段的显示名。 做了这些之后,就可以进行 X 转发了。服务器运行 X程序时使用这个虚拟的 X 显示提交图形显示请求,把图形显示 数据写入这个虚拟的 X 显示,也即写入上面新建的通道发给客户端。客户端取得这些数据后再把它写入自己刚刚建立 的与 X 服务器连接的通道,也即向 X 服务器提交显示请求。 为什么客户端不直接把自己 .Xauthority 文件中一条显示配置记录交给服务器,由服务器按这条记录直接打开 TCPsocket 与客户端的 X 建立连接呢?ssh 的安全性也就在这里,如果这样做,就把等于把自己的 X 服务器完全奉送 给外界来使用,而 X 服务器本身又是问题多多的。前面伪造一个认证钥也是出于这个考虑,因为如果知道了认证钥, 显示记录里别的几个字段是很容易猜出的。 尽管做了这些,还是存在问题的。如果一个攻击者侵入或掌握着 ssh 服务器运行的主机,那么他/她发现一个 ssh连接 并进行 X 转发服务时,设法获取连接者的 $DISPLAY 环境变量值,再执行一下 "xauth value_of_$DISPLAY" 命令,就 得到显示记录了。随后他/她用 "xauth add" 命令把这条记录加入自己的 .Xauthority 文件中,再把自己的$DISPLAY环 境变量设置成这条记录的显示名。这样他/她就可以在 X转发连接期间运行 X 程序,X 程序的显示请求全部提交给客户 端的 X 服务器了。如果 X 服务器有什么漏洞的话,他/她可以自由运用了。
LZG8056 回复于:2003-11-10 23:43:57 好文章,要珍惜啊!
網中人 回复于:2003-11-11 03:33:48 好文﹗ 不過,用戶在"非個人"使用上,最好留意一下 ssh2 的版權宣告。 要不然,確定你用的是 openssh 版本... |