之前爲了自己做一套SSH,先自己實現了一套telnet。但經過這麼多天的苦逼,發現以前的工作都是徒勞。ssh的協議很繁雜,核心的內容在於密碼算法,而且自己很難在網上找到周全的細節講解與詳細的實現,只有靠自己刷RFC和問大神還有就是靠強X我的服務器艱難地完成。
現計算了下時間,自己做SSH耗費了進兩個月的時間,雖然期間也夾着其他的繁雜事物,但自己在這方面確是是耗費了非常大的精力。因爲這方面詳細資料的匱乏,自己以前也幾乎沒有接觸過密碼學方面的東西,很多隻有靠自己摸索,所以我得經常拿我自己的服務器來做黑盒測試,我現在服務器上的ss服務器日誌全是一些非法連接的記錄(—_—|||)。早知當初就不那麼作死非要自己實現他的加密算法和過程,用openssl就很快搞定了。但我還是覺得這次做SSH的精力是我受益匪淺,不僅熟悉了各種加密,並且能靠自己實現並熟練應用了。可能這些對自己幫助不大,但至少和信安的小夥伴也有點吹牛的談資了~
這篇文章希望能幫助到想了解ssh2.0協議或是親手實現ssh協議的小夥伴。
首先對數據包的格式進行說明:
數據包由包長度(Packet Length)、填充長讀(Padding Length)、信息代碼(Msg code)、信息內容與填充值(Padding String) 這5部分組成。信息內容中的一些字符串以4字節長度+該長度數量的字符組成,數值按照網絡序排列,例如:abc: 00 00 00 03 (char)a (char)b (char)c 。另外有一種大整數的情況,負數和字符串的表示方式一樣,正數需要前導0,例如 4b64: 00 00 00 03 00 4b 64 。
ssh頭的結構體:
struct sshhead { unsigned int tlen; unsigned char plen; unsigned char msgcode; sshhead(){tlen=6;} };
就拿通過ssh遠程控制的一個完整個過程來講,ssh的過程可分爲以下3部分:
一、版本協商
二、算法協商與密鑰交換
三、加密通信(可能含有2、3部分)
這其中第二部分是ssh最爲核心的過程,該過程決定了以後通信所要使用的密鑰,下面按順序對每個部分對比着數據包進行詳細的講解並給出實現的過程。
一、版本協商:
在建立連接後,客戶端與服務器分別向對方發送自己ssh的版本信息(這裏的數據格式不同於其他包,只有一行版本號),以\r\n結束。版本的格式如下:
SSH-ssh協議版本-詳細版本\r\n (幾乎只有ssh協議版本之前的信息有效)
比如我linux上的就是:SSH-2.0-OpenSSH_5.3\r\n
Putty的是: SSH-2.0-PuTTY_Release_0.63\r\n
一般來說,在建立連接後,是先由服務器發版本號過來,單線程處理版本協商的朋友需要注意下。
在雙方收到對方發來的版本號後,會根據兩者之中最小的版本來進行接下來的通訊。
二、算法協商與祕鑰交換:
這部分的內容將會佔該文章總篇幅的一半以上。
首先給大家看下整個過程的數據包大概:
整個部分是從第6條開始到第15條結束,除去中間的非協議部分,總共有7條數據包。看起來只有這麼幾條數據包,但其中包含了非常多的過程與隱祕的信息。
1、算法協商:
位第6、9數據,分別爲雙發向對法發送的自己在不同密碼需求上支持的算法。
該數據包的格式:
按順序分別是:
cookie(隨機的值,16byte)
kex_algorithms(祕鑰租交換算法)
server_host_key_algorithms(服務器主機祕鑰,正常情況用處不大,甚至可以不用)
encryption_algorithms_client_to_server(兩端通信使用的加密算法)
encryption_algorithms_server_to_client
mac_algorithms_client_to_server(數據校驗用的hash算法)
mac_algorithms_server_to_client
compression_algorithms_client_to_server(壓縮算法)
compression_algorithms_server_to_client
languages_client_to_server
languages_server_to_client
first_kex_packet_follows
0(4byte整數,擴展用的)
每個算法類型可能會有多個不同的算法,這些算法之間使用逗號隔開。
現在雙方知道對方支持的算法,但是應該怎樣決定每個類型實際所使用的算法呢?
每個算法類型列表的第一個算法必須是首選的算法,服務器應以客戶端的算法優先級作爲考慮,就拿交換算法舉例:
現在服務器有三個算法dh1,dh2,dh3
客戶端有兩個算法dh3,dh2
那麼服務器的首選算法是dh1,而客戶端是dh3,客戶端此時知道服務器有dh3算法,因此客戶端就確認使用dh3算法。服務器發現自己的首選算法與客戶端不同,而自己擁有客戶端的首選算法,因此服務器也確認使用dh3算法。
再看另一個情況
服務器:dh1,dh2,dh3
客戶端:dh4,dh3,dh1
這時服務器沒有客戶端的首選算法,客戶端會使用第二個算法dh3,此時服務器也支持第二個算法,雙方將確定使用dh3算法。
如果服務器和客戶端雙方沒有共同的算法,這次會話將會終止。
下面是代碼實現和服務器之間的版本協商
#define KEI (char)20 #define NK (char)21 //算法名 #define VER "SSH-2.0-WCHRT_1.0\r\n" #define COOKIE "0123456789ABCDEF" #define VKEX "diffie-hellman-group-exchange-sha256" #define VSHK "ssh-rsa" #define VECS "aes128-cbc" #define VESC "aes128-cbc" #define VMCS "hmac-sha1" #define VMSC "hmac-sha1" #define VCCS "none" #define VCSC "none" #define VLCS "" #define VLSC "" #define KFPF ""
bool key_exchange() { sshhead sshh; sshh.msgcode=KEI; sshh.tlen-=4; //將算法列表與信息分別寫入緩衝區 mstrin(COOKIE,sshh.tlen); mstrin(VKEX,sshh.tlen); mstrin(VSHK,sshh.tlen); mstrin(VECS,sshh.tlen); mstrin(VESC,sshh.tlen); mstrin(VMCS,sshh.tlen); mstrin(VMSC,sshh.tlen); mstrin(VCCS,sshh.tlen); mstrin(VCSC,sshh.tlen); mstrin(VLCS,sshh.tlen); mstrin(VLSC,sshh.tlen); //ed for(int i=0;i<5;sshh.tlen++,i++) { data[sshh.tlen]=(char)0; } //載荷的計算與總長度的寫入都放在最後 //count padding length count_padding(sshh); sshheadin(sshh); //沒有封裝socket len=send(sock,data,sshh.tlen+4,0); mrecv(10); //printf("(%d)",len); if(data[5]==KEI) { return true; } return false; }
用到的一些功能函數:
//向緩衝區填充字符串,長度使用網絡字節序 void mstrin(string s,unsigned int &tlen) { data[tlen]=(char)(s.length()/(256*256*256)); data[tlen+1]=(char)(s.length()/(256*256)); data[tlen+2]=(char)(s.length()/256); data[tlen+3]=(char)(s.length()); tlen+=4; for(int i=0;i<s.length();tlen++,i++) { data[tlen]=s[i]; } data[tlen]='\0'; } void sshheadin(sshhead &sshh) { sshh.tlen-=4; data[0]=(char)(sshh.tlen/(256*256*256)); data[1]=(char)(sshh.tlen/(256*256)); data[2]=(char)(sshh.tlen/256); data[3]=(char)(sshh.tlen); data[4]=(char)sshh.plen; data[5]=sshh.msgcode; } void count_padding(sshhead &sshh) { int k=2; if(sshh.tlen%8<4) { k=1; } sshh.plen=(sshh.tlen/8+k)*8-sshh.tlen; sshh.tlen=(sshh.tlen/8+k)*8; }
2、祕鑰交換
在算法協商成功過後,雙方便立馬進行祕鑰組的交換。ssh2.0版本所使用的祕鑰組交換協議算法主要使用diffie-hellman-group-exchange-sha算法。
鑑於該部分內容特別多,我特意在另一篇單獨的文章中予以詳細介紹,再閱讀下文前請先參考該文章:dh-gex-sha算法詳解
我們數據包的第10到15條都是該部分的內容
1、dh key exchange init (C)
密鑰交換初始化,由客戶端先向服務器發送祕鑰交換請求的數據包,告知開始祕鑰交換。
//<<<<<<<<<<DH KEX INIT<<<<<<<<< sshh.tlen=6; sshh.msgcode=DHKEI; //payload mintin(0x1000,sshh.tlen); count_padding(sshh); sshheadin(sshh); len=send(sock,data,sshh.tlen+4,0); //dh: set I_C dhdata.set_i_c(string(data+4,sshh.tlen)); //dhdata.set_i_c(string(data,len)); //>>>>>>>>>>>>>>>>>>>>>>>>
2、dh key exchange reply (S)
服務器收到客戶端發起交換的請求後,將自己用於dh算法的P、G發送給客戶端,用於客戶端生成dh公私鑰。這裏的P是一個大素數,而G是大於1的數,G不必過大,10位以內最後,因爲按冪運算G能輕易生成特別大的數。
//<<<<<<<<<<DH KEX REPLAY<<<<<<<<< mrecv(10); if(data[5]!=DHKER) { puts("DH KEX REPLAY error"); return false; } //dh: set I_S dhdata.set_i_s(string(data+4,len-4)); //dhdata.set_i_s(string(data,len)); //dh: read P pos=6; intlen=readstrint(data+pos); pos+=4; Integer p=readstrbigint(data+pos,intlen); pos+=intlen; //dh: read G intlen=readstrint(data+pos); pos+=4; Integer g=readstrbigint(data+pos,intlen); pos+=intlen; //dh: set G and P dhdata.set_g_and_p(g,p); //cout<<dhdata.dh_p<<" "<<dhdata.dh_g<<endl; //>>>>>>>>>>>>>>>>>>>>>>>>
3、dh gex init (C)
客戶端收到服務器發過來的P、G後,自己變成根據P、G生成並計算出自己的公鑰e。這一步也只需要客戶端將生成的e發送給服務器即可。
//<<<<<<<<<<DH GEX INIT<<<<<<<<< sshh.tlen=6; sshh.msgcode=DHGI; dhdata.comp_e(); string e=inttostr(dhdata.get_e(),256); mstrin(e,sshh.tlen); count_padding(sshh); sshheadin(sshh); len=send(sock,data,sshh.tlen+4,0); //debugstr(data,len); //>>>>>>>>>>>>>>>>>>>>>>>>
4、dhgex reply (S)
重要的來了,服務器收到了客戶端發來的e後,便能計算出共享祕鑰K,並根據現有信息計算出生成所需祕鑰的H。
這個數據包裏面含有如下信息:
KEX DH host key(K_S):
主機公鑰,一般爲rsa公鑰。完整的格式爲:總長度+算法名長度+算法名+證書(n)長度+證書(n)+公鑰長度+公鑰。
DH server f :
服務器的dh公鑰值,客戶端收到後便能用f計算出同樣的共享祕鑰K。
KEX DH H signature (簽名後的H):
服務器用主機私鑰對計算出的hash值H進行簽名的結果。格式爲:總長度+算法名長度+算法名+簽名數據長度+簽名值。
H的計算方法: H=hash(V_C||V_S||I_C||I_S||K_S||e||f||K);
按順序用到的值(注意類型):
類型 | 值 | 說明 |
string | V_C | 客戶端的初始報文(版本信息:SSH-2.0-xxx,不含結尾的CR和LF) |
string | V_S | 服務器的初始報文 |
string | I_C | 客戶端 SSH_MSG_KEX_INIT的有效載荷(不含開頭的數據長度值) |
string | I_S | 服務器的同上 |
string | K_S | 主機祕鑰(dh gex reply(33)過程服務器發送host key (RSA公鑰)) |
mpint | e | 客戶端DH公鑰 |
mpint | f | 服務器DH公鑰 |
mpint | K | 共同DH計算結果 |
//<<<<<<<<<<DH GEX REPLAY<<<<<<<<< mrecv(10); if(data[5]!=DHGR) { puts("DH GEX REPLAY error"); system("pause"); return false; } int padlen=data[4]; //dh: set server host key pos=6; intlen=readstrint(data+pos);//host key all length dhdata.set_k_s(string(data+pos+4,intlen)); pos+=4; intlen=readstrint(data+pos);//host key name pos+=4; pos+=intlen; intlen=readstrint(data+pos);//get rsa e and n pos+=4; Integer ee=readstrbigint(data+pos,intlen); pos+=intlen; intlen=readstrint(data+pos); pos+=4; Integer nn=readstrbigint(data+pos,intlen);//set rsa e and n pos+=intlen; dhdata.set_e_and_n(ee,nn); //dh: set dh server f intlen=readstrint(data+pos); pos+=4; dhdata.set_f(readstrbigint(data+pos,intlen)); pos+=intlen; //dh: set shka_name pos+=4;//h's total length intlen=readstrint(data+pos); pos+=4; dhdata.set_shka_name(string(data+pos,intlen)); pos+=intlen; //dh: set server h intlen=readstrint(data+pos); pos+=4; dhdata.set_s_h(string(data+pos,intlen)); pos+=intlen; pos+=padlen; //and other MAC// dhdata.comp_k(); dhdata.comp_h();
5、new keys (C)
客戶端收到服務器的信息後計算出K,並用同樣的方式計算出H(服務器和客戶端的H都是同一個值)。並使用服務器發過來的K_S驗證服務器發過來的簽名後的H,如果驗證一致,則說明此次祕鑰交換成功。客戶端向服務器發送new key,標誌祕鑰交換過程的結束。如果此次祕鑰交換是整個會話的第一次交換,則計算出的H也是整個會話的會話ID(session_id)。
祕鑰基本信息在網絡上的傳輸與交換,接下來就分別是服務器和客戶端各自使用現有信息計算出以後加解密所要使用的祕鑰。祕鑰計算:
這裏的加密祕鑰指的是以後數據通信所用的祕鑰,一般用aes算法。
計算方式:hash(K,H,單個字符,session_id);
單個字符指的是單個大寫的ASCII字母,根據不同的加密祕鑰選擇不同的字符來計算。
字母 | 祕鑰 |
'A' | 客戶端到服務器的初始IV(CBC) |
'B' | 服務器到客戶端的初始IV |
'C' | 客戶端到服務器的加密祕鑰(對稱祕鑰) |
'D' | 服務器到客戶端的加密祕鑰 |
'E' | 客戶端到服務器的完整性祕鑰(HMAC) |
'F' | 服務器到客戶端的完整性祕鑰 |
就以aes-cbc爲例子,aes對稱加解密所需要用到的值有初始IV與對稱祕鑰。這裏的初始IV指的是cbc模式中加解密的初始向量,第二次加解密需要IV的值,以後的每次的加解密都要依賴於上一次加解密的數據。
三、加密通信
此時雙方都擁有協商好的算法以及用於加解密的祕鑰,現在開始所有傳輸的全部數據都要進行加密(包含總長度),並使用同樣的。
在加密通信的過程中,雙方允許重新發送KEX祕鑰交換請求。這時整個祕鑰交換過程的數據將會使用現有密鑰加解密。在該次祕鑰交換的過程中也會生成一個H值,但該H值不會影響到此次會話的session_id,session_id只是會話第一次祕鑰交換生成的H值。在祕鑰交換最後客戶端發出new keys請求時。雙方會放棄當前使用的祕鑰,使用新協商的祕鑰繼續通信。
在遠程數據的通信過程中,雙方使用SSH_MSG_CHANNEL_DATA標誌消息類型進行數據傳輸。
在祕鑰交換完成後第一次對發送數據加密時,首先需要對AES向量進行初始化,即設置對應的IV。aes部分我使用的是CRYPTOPP的aes-cbc算法(在後文的有對該算法的封裝)。
en_c_to_s.set_iv(dhdata.comp_encry_key(IVCSF,32)); en_c_to_s.set_k(dhdata.comp_encry_key(ECSF,32)); en_c_to_s.init(); de_s_to_c.set_iv(dhdata.comp_encry_key(IVSCF,32)); de_s_to_c.set_k(dhdata.comp_encry_key(ESCF,32)); de_s_to_c.init();
整個協議用到的主要加密算法的實現與封裝:
//mycrypt.h #ifndef _MYCRYPT_H__ #define _MYCRYPT_H__ #include<cstring> #include<string> #include <iostream> #include<cmath> #include "integer.h" #include "files.h" #include "hex.h" #include "sha.h" #include "modes.h" #include "osrng.h" using namespace std; using namespace CryptoPP; Integer mkrandomnum(int len); string inttostr(Integer num,unsigned int radix); string inttostrnum(Integer num,unsigned int radix); //大整數轉mpint string inttompint(Integer num,unsigned int radix); string strtostrnum(string s); //大整數快速冪運算 Integer fastpower_comp(Integer a,Integer b,Integer c); //sha算法封裝 class m_sha { public: string encode_sha1(string data); string encode_sha256(string data); }; //dh算法實現 class m_dh { public: Integer dh_g,dh_p,dh_x,dh_e; Integer dh_y,dh_f; Integer dh_k; void set_g_and_p(const Integer g,const Integer p) { dh_g=g; dh_p=p; } void set_y(Integer y) { dh_y=y; } void set_f(Integer f) { dh_f=f; } void comp_e(); Integer get_e() { return dh_e; } void comp_k(); Integer get_k() { return dh_k; } }; //rsa算法實現 class m_rsa { public: Integer rsa_e; Integer rsa_n; void set_e_and_n(Integer e,Integer n) { rsa_e=e; rsa_n=n; } Integer comp_rsa_result(Integer num); }; //dh gex協議算法實現 class m_dh_gex_sha:public m_dh,public m_sha,public m_rsa { public: string v_c,v_s; string i_c,i_s; string k_s; string dh_h,s_h; string shka_name; void set_v_c(string x) { v_c=x; } void set_v_s(string x) { v_s=x; } void set_i_c(string x) { i_c=x; } void set_i_s(string x) { i_s=x; } void set_k_s(string x) { k_s=x; } void set_s_h(string x) { s_h=x; } void set_shka_name(string x) { shka_name=x; } void comp_h(); string get_h() { return dh_h; } string comp_encry_key(char c,const int len); }; //aes-cbc算法封裝 class m_aes_cbc { public: string aes_k; string aes_iv; void set_k(string x) { aes_k=x; } void set_iv(string x) { aes_iv=x; } CBC_Mode<AES>::Encryption *aes_Encryptor; CBC_Mode<AES>::Decryption aes_Decryptor; void init(); string encode(string data); string decode(string data); }; #endif //_MYCRYPT_H__
//mycrypt.cpp #include "mycrypt.h" Integer mkrandomnum(int len) { Integer re=0; for(int i=0;i<len;i++) { re*=10; re+=abs(rand())%10; } return re; } string inttostr(Integer num,unsigned int radix) { string s1=""; unsigned int k; while(num>0) { k=num%radix; num/=radix; //cout<<num<<" "; //printf("%d\n",k); s1+=(char)k; } string s2=""; for(int i=s1.length()-1;i>=0;i--) { s2+=s1[i]; } return s2; } string inttostrnum(Integer num,unsigned int radix) { string s1=""; unsigned int k; while(num>0) { k=num%radix; num/=radix; if(k<10) { s1+='0'+k; } else { s1+='a'+k-10; } } string s2=""; for(int i=s1.length()-1;i>=0;i--) { s2+=s1[i]; } return s2; } string inttompint(Integer num,unsigned int radix) { string s=""; s+=(char)0; s+=inttostr(num,radix); int len=s.length(); string k=""; while(len>0) { k+=(char)(len%256); len/=256; } while(k.length()<4) { k+=(char)0; } string re; re+=k[3]; re+=k[2]; re+=k[1]; re+=k[0]; re+=s; return re; } string strtostrnum(string s) { string re=""; int k,p; for(int i=0;i<s.length();i++) { p=s[i]; if(p<0) { p=(256+s[i]); } k=p/16; for(int j=0;j<2;j++) { // if(k<10) { re+=('0'+k); } else { re+=('a'+k-10); } k=p%16; } } return re; } int intlength(Integer num) { int re=0; while(num>0) { num/=10; re++; } return re; } Integer fastpower_comp(Integer a,Integer b,Integer c) { /*unused fast power Integer re=1; for(int i=0;i<b;i++) { re*=a; re%=c; } return re; */ //fast power Integer n=c; c=1; while(b!=0) { if(b%2!=0) { b=b-1; c=(c*a)%n; } else { b=b/2; a=(a*a)%n; } } return c; } void m_dh::comp_e() { dh_x=mkrandomnum(50)+1; dh_e=fastpower_comp(dh_g,dh_x,dh_p); /* cout<<"//"<<endl; cout<<dh_g<<endl; cout<<dh_x<<endl; cout<<dh_p<<endl; cout<<dh_e<<endl; cout<<"//"<<endl; */ } void m_dh::comp_k() { dh_k=fastpower_comp(dh_f,dh_x,dh_p); } Integer m_rsa::comp_rsa_result(Integer num) { return fastpower_comp(num,rsa_e,rsa_n); } void m_dh_gex_sha::comp_h() { string data=""; /* data+=strtostrnum(v_c); data+=strtostrnum(v_s); data+=strtostrnum(i_c); data+=strtostrnum(i_s); data+=strtostrnum(k_s); data+=inttostrnum(dh_e,16); data+=inttostrnum(dh_f,16); data+=inttostrnum(dh_k,16); */ data+=v_c; data+=v_s; data+=i_c; data+=i_s; data+=k_s; data+=inttompint(dh_e,16); data+=inttompint(dh_f,16); data+=inttompint(dh_k,16); //cout<<endl<<"|"<<data<<"||"<<endl; dh_h=encode_sha256(data); } string m_dh_gex_sha::comp_encry_key(char c,const int len) { string re; string data=""; data+=inttompint(dh_k,16); data+=dh_h; data+=c; data+=dh_h; re=encode_sha256(data); while(re.length()<len) { data=inttompint(dh_k,16); data+=dh_h; data+=re; re+=encode_sha256(data); } while(re.length()>len) { re.pop_back(); } return re; } string m_sha::encode_sha1(string data) { string hash; SHA1 sha1; HashFilter hash_filter (sha1); hash_filter.Attach(new HexEncoder(new StringSink(hash), false)); hash_filter.Put((byte *)data.c_str(),data.length()); hash_filter.MessageEnd(); return hash; } string m_sha::encode_sha256(string data) { string hash; SHA256 sha256; HashFilter hash_filter (sha256); hash_filter.Attach(new HexEncoder(new StringSink(hash), false)); hash_filter.Put((byte *)data.c_str(),data.length()); hash_filter.MessageEnd(); return hash; } void m_aes_cbc::init() { aes_Encryptor=new CBC_Mode<AES>::Encryption((unsigned char *)aes_k.c_str(), aes_k.length(), (unsigned char *)aes_iv.c_str()); aes_Decryptor=new CBC_Mode<AES>::Decryption ((unsigned char *)aes_k.c_str(), aes_k.length(), (unsigned char *)aes_iv.c_str()); } string m_aes_cbc::encode(string data) { string re; StringSource(data, true, new StreamTransformationFilter(*aes_Encryptor, new StringSink(re), BlockPaddingSchemeDef::BlockPaddingScheme::ONE_AND_ZEROS_PADDING, true) ); return re; } string m_aes_cbc::decode(string data) { string re; StringSource(data, true, new StreamTransformationFilter(*aes_Decryptor, new StringSink(re), BlockPaddingSchemeDef::BlockPaddingScheme::ONE_AND_ZEROS_PADDING, true) ); return re; }
ssh的實現就到此終於結束了,截圖留念。
筆者在之初就想使用crypto++來幫助實現ssh過程的密碼算法。而剛接觸這東西完全搞不懂怎麼用,什麼編碼器、生成器、過濾器、sink...這些概念根本就不懂,網上的使用文檔直接就拿這一堆概念加上一堆組合出來的代碼來實現一個加密算,沒有什麼密碼學知識,想要快速掌握crypto++幾乎是不可能的,當時研究了很久就只是會使用它的hash加密。而後自己硬着頭皮實現了整個dh-gex,到後面aes後,發現自己能很自然得理解crypto++的用法了,便自己封裝了crypto++的aes算法供使用。
總之都是好事,以後遇到其他的基於ssl的協議與應用就應能很輕鬆地理解與實現了。