SSH通信协议浅析

发表于:2007-06-23来源:作者:点击数: 标签:
第一部分:协议概览 整个通讯过程中,经过下面几个阶段协商实现认证连接。 第一阶段: 由客户端向服务器发出 TCP 连接请求。TCP 连接建立后,客户端进入等待,服务器向客户端发送第一个报文,宣告自己的版本号,包括协议版本号和软件版本号。协议版本号由主版

   
  第一部分:协议概览

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

第一阶段:

由客户端向服务器发出 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 选项。当服务器告知客户端自己的退出状态时,交互会话模式结束。

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

第二部分:数据包格式和加密类型

二进制数据包协议:

包 = 包长域(4字节:u_int32_t) + 填充垫(1-7字节)

+ 包类型域(1字节:u_char) + 数据域

+ 校验和域(4字节)

加密部分 = 填充垫 + 包类型 + 数据 + 校验和

包长 = 1(包类型) + 数据字节长度 + 4(校验和)

数据包压缩:

如果支持压缩,包类型域和数据域用 gzip 压缩算法进行压缩。压缩时在两个数据传送方向的任何一个上,包的压缩部分(类型域+数据域)被构造得象是它连在一起,形成一个连续的数据流。在两个数据传送方向上,压缩是独立进行的。

数据包加密:

现时支持的数据加密方法有这样几种:

SSH_CIPHER_NONE 0 不进行加密

SSH_CIPHER_IDEA 1 IDEA 加密法(CFB模式)

SSH_CIPHER_DES 2 DES 加密法(CBC模式)

SSH_CIPHER_3DES 3 3DES 加密法(CBC模式)

SSH_CIPHER_ARCFOUR 5 Arcfour加密法)

SSH_CIPHER_BLOWFISH 6 Blowfish 加密法

协议的所有具体实现都要求支持3DES。

DES 加密:

从会话密钥中取前8个字节,每个字只用高7位,忽略最低位,这样构成56位的密钥供加密使用。加密时使用CBC 模式,初使矢量被初始化为全零。

3DES 加密:

3DES 是 DES 的变体,它三次独立地使用 CBC 模式的DES 加密法,每一次的初始矢量都是独立的。第一次用DES 加密法对数据进行加密;第二次对第一次加密的结果用 DES 加密法进行解密;第三次再对第二次解密的

结用 DES 加密法进行加密。注意:第二次解密的结果并不就是被加密的数据,因为三次使用的密钥和初始矢量都是分别不同的。与上面的 DES 加密采用的方法类似,第一次从会话密钥中取起始的前8个字节生成加密密钥,第二次取下一个紧跟着的8个字节,第三次取再下一个紧跟着的8个字节。三次使用的初始矢量都初始化为零。

IDEA 加密:

加密密钥取自会话密钥的前16个字节,使用 CFB 模式。初始矢量初始化为全零。

RC4 加密:

会话密钥的前16个字节被服务器用作加密密钥,紧接着的下一个16字节被客户端用作加密密钥。结果是两个数据流方向上有两个独立的129位密钥。这种加密算法非常快。

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

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

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[i] + difference >= small_primes[i])

moduli[i] -= small_primes[i];

if (moduli[i] + 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[i] = 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[i]^session_id[i]);

else

mpz_add_ui(&key,&key, session_key[i]);

}

把结果发给服务器。在用服务器发来主机公钥和服务公钥对这个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 suclearcase/" target="_blank" >ccess;

}

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 服务器有什么漏洞的话,他/她可以自由运用了。

原文转自:http://www.ltesting.net