国产一级一区二区_segui88久久综合9999_97久久夜色精品国产_欧美色网一区二区

掃一掃
關注微信公眾號

SSH通信協議淺析
2005-11-25   

第一部分:協議概覽
整個通訊過程中,經過下面幾個階段協商實現認證連接。
第一階段:
由客戶端向服務器發出 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 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 服務器有什么漏洞的話,他/她可以自由運用了。

熱詞搜索:

上一篇:SSH:增強安全“免疫力”
下一篇:簡易 Telnet 與 SSH 主機設定(1)

分享到: 收藏
主站蜘蛛池模板: 上饶县| 潜江市| 湘潭市| 广南县| 新疆| 平乐县| 阜平县| 阳曲县| 汝城县| 新平| 宜兰市| 溧阳市| 靖宇县| 土默特右旗| 周口市| 喀喇沁旗| 桐庐县| 斗六市| 台州市| 六枝特区| 韩城市| 双桥区| 乌鲁木齐市| 海兴县| 海盐县| 台州市| 峨山| 寻甸| 沅江市| 和龙市| 保靖县| 凤凰县| 香河县| 山丹县| 牟定县| 深泽县| 黄大仙区| 潜江市| 青神县| 林甸县| 乌什县|