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[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 服務器有什漏洞的話,他/她可以自由運用了。
(http://www.fanqiang.com)
進入【UNIX論壇】
|