使用者討論:不愛思考得豬/SNI
以下是火狐瀏覽器原始碼中關於SNI的ClientHello語句生成函數,是一個關鍵性函數,通過瀏覽器發送的任何SNI請求都必須經過此函數生成ClientHello。這段原始碼來自於火狐瀏覽器原始碼文件系統下的security/nss/lib/ssl/sslext3.c
的文件:
/* Format an SNI extension, using the name from the socket's URL,
* unless that name is a dotted decimal string.
* Used by client and server.
*/
PRInt32
ssl3_SendServerNameXtn(sslSocket * ss, PRBool append,
PRUint32 maxBytes)
{
SECStatus rv;
if (!ss)
return 0;
if (!ss->sec.isServer) {
PRUint32 len;
PRNetAddr netAddr;
/* must have a hostname */
if (!ss->url || !ss->url[0])
return 0;
/* must not be an IPv4 or IPv6 address */
if (PR_SUCCESS == PR_StringToNetAddr(ss->url, &netAddr)) {
/* is an IP address (v4 or v6) */
return 0;
}
len = PORT_Strlen(ss->url);
if (append && maxBytes >= len + 9) {
/* extension_type */
rv = ssl3_AppendHandshakeNumber(ss, ssl_server_name_xtn, 2);
if (rv != SECSuccess) return -1;
/* length of extension_data */
rv = ssl3_AppendHandshakeNumber(ss, len + 5, 2);
if (rv != SECSuccess) return -1;
/* length of server_name_list */
rv = ssl3_AppendHandshakeNumber(ss, len + 3, 2);
if (rv != SECSuccess) return -1;
/* Name Type (sni_host_name) */
rv = ssl3_AppendHandshake(ss, "\0", 1);
if (rv != SECSuccess) return -1;
/* HostName (length and value) */
rv = ssl3_AppendHandshakeVariable(ss, (PRUint8 *)ss->url, len, 2);
if (rv != SECSuccess) return -1;
if (!ss->sec.isServer) {
TLSExtensionData *xtnData = &ss->xtnData;
xtnData->advertised[xtnData->numAdvertised++] =
ssl_server_name_xtn;
}
}
return len + 9;
}
/* Server side */
if (append && maxBytes >= 4) {
rv = ssl3_AppendHandshakeNumber(ss, ssl_server_name_xtn, 2);
if (rv != SECSuccess) return -1;
/* length of extension_data */
rv = ssl3_AppendHandshakeNumber(ss, 0, 2);
if (rv != SECSuccess) return -1;
}
return 4;
}
其中關鍵性的代碼為如下兩行:
len = PORT_Strlen(ss->url);
以及:
rv = ssl3_AppendHandshakeVariable(ss, (PRUint8 *)ss->url, len, 2);
其中ss->url
是目標網站域名,也就是SNI的域名(也就是唯一可以被牆看見的那個域名)。為只讀變量,不能修改(而且也不應該被修改,因為後續收到安全證書以後必須要能對上安全證書裡的域名列表里的某一個域名,而且再後續進行HTTPS GET
操作時就必須要有正確的域名才能取得正確的網頁和內容)。
但是(我要說但是了!)我們可以把在以上兩行里的ss->url
完全替換成【另外】的一個string literal(也就是所謂的「hard-coding SNI」)。比如以下兩種修改:
len = PORT_Strlen("wikimedia.org\0");
…
rv = ssl3_AppendHandshakeVariable(ss, (PRUint8 *)"wikimedia.org\0", len, 2);
len = PORT_Strlen("\0");
…
rv = ssl3_AppendHandshakeVariable(ss, (PRUint8 *)"\0", len, 2);
都能通過編譯器編譯,生成可執行火狐瀏覽器。我對以上兩種情況分別進行了實驗,有以下發現:
- 如果hard-code空字符串
\0
,那麼所有HTTPS連接一律報錯,沒有例外,也就是說如此編譯出來的瀏覽器是完全廢掉了。 - 如果hard-code維基媒體總站域名,那麼在我測試的網站中,除了Cloudflare網站不能正常工作,其它網站都能正常工作。特別有趣的是對谷歌發送維基媒體總站域名SNI也能得到正確的谷歌證書,成功打開
google.com
,而瀏覽器不會報錯。
甚至可以做出如下修改:
char url[500];
scanf("%s", url);
…
len = PORT_Strlen(url);
…
rv = ssl3_AppendHandshakeVariable(ss, (PRUint8 *)url, len, 2);
當然,以上修改後的火狐瀏覽器需要從xterm
終端里啟動,否則沒法輸入字符串。我個人從未做出或者測試過以上修改。但是我相信以上的修改是最最靈活的,因為允許用戶在運行火狐瀏覽器的時候自行鍵入想要送出的明文SNI域名。
很可惜,我身在牆外,所以完全不知道這些修改能不能規避牆的SNI重置封鎖。但是如果牆內朋友證實這些修改是可行的話,那麼這將是非常powerful的修改。這些修改將允許牆內網友瀏覽維基百科直到牆SNI封殺【最後一個】維基媒體域名(現在除了維基百科和維基新聞以外基本上所有其它維基媒體域名都未被牆封殺)。而且牆內網友可以直接打開維基百科,而不需要先打開比如維基文庫,然後利用HTTPS信道餘熱來打開維基百科。