說明:如何訪問維基百科/Firefox瀏覽器補丁

修改火狐瀏覽器關於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);

都能通過編譯器編譯,生成火狐瀏覽器的目標文件(object files)以及可執行二進制文件(binary executable)。我對以上兩種情況分別進行了實驗,有以下發現:

  • 如果hard-code空字符串\0,那麼所有HTTPS連接一律報錯,沒有例外,也就是說如此編譯出來的瀏覽器是完全廢掉了。(這種情況對應於「SNI拔除」,也就是試圖把現代火狐瀏覽器恢復到火狐瀏覽器1.0時代不發送SNI信息,現在看來這種方法完全行不通了)
  • 如果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信道餘熱來打開維基百科。

不愛思考得豬留言2019年5月17日 (五) 20:44 (UTC)[回覆]

會編譯,看得懂代碼的人為什麼需要這個...--Fantasticfears留言2019年5月17日 (五) 21:34 (UTC)[回覆]
其實說實話這是一個比較針對維基媒體的特定修改,而且可能也用不了多久了。牆不知道為什麼沒有對維基媒體進行全面封殺,而是只封殺了維基新聞和維基百科兩類域名。以上的修改就是利用剩下的、未被封殺的維基媒體域名進行一種類似域名前置的操作,使得牆內用戶在不翻牆的情況下依舊可以使用維基百科。但是說實話我個人是不太看好這個hack的,因為我認為牆應該即將封殺所有維基媒體域名了,甚至可能會對維基媒體的伺服器群進行徹底IP封殺。不愛思考得豬留言2019年5月17日 (五) 23:20 (UTC)[回覆]
其實就是,一種是SNI拔除,一種是類似域前置的方法。曾經有討論過,不過需要定製化的客戶端,只能適合硬核玩法。至於域前置的做法,好像有幾家CDN不再支持了,為了防止Telegram等利用。——路過圍觀的Sakamotosan | 避免做作,免敬 2019年5月18日 (六) 00:52 (UTC)[回覆]

Success!!! This is 不愛思考得豬. I have tunneled back inside the Great Firewall of China using PureVPN's Shanghai server. I have tested and verified that the above changes I have made to Firefox's source code really worked (together with relevant changes to /etc/hosts). Right now I am accessing zh.wikipedia.org with SNI wikimedia.org. I do apologize for posting this exciting update in English as my Firefox testing environment is Ubuntu Linux, so I cannot input Chinese. Look at my signature and you can see that the IP address is located in Shanghai. I am so happy right now! 101.226.196.139留言2019年5月19日 (日) 19:58 (UTC)[回覆]

成功啦!成功啦!昨天我訂購了PureVPN服務,PureVPN有伺服器在上海,連上去了以後果然就是牆內的環境。我修改的火狐瀏覽器是在Ubuntu上運行的,所以剛才成功了以後留言記錄證明連接成功只能打洋文了。要注意的是我對於火狐瀏覽器的修改必須要搭配/etc/hosts修改才行。在修改了/etc/hosts以後,運行Ubuntu內置火狐瀏覽器,就會收到「SNI Reset」錯誤信息。而運行我修改的火狐瀏覽器,則可以在未打開wikimedia.org的情況下直接打開zh.wikipedia.org,而且速度良好。今天我真是太高興啦!這個修改應該可以一直使用到牆封殺維基媒體旗下的最後一個域名。不愛思考得豬留言2019年5月19日 (日) 20:27 (UTC)[回覆]

你很厲害嘛。但你如果不編譯出來的話,我們這些小白可是用不了呢。不過Firefox升級後就又不能用了吧。雖然我有其他方法啦。--1=0歡迎加入WP:維基百科維護專題 2019年5月19日 (日) 23:19 (UTC)[回覆]
多謝Misel兄誇獎!我下來琢磨琢磨如何編譯出視窗平台上的火狐目標文件、庫文件、可執行文件。不愛思考得豬留言2019年5月20日 (一) 13:37 (UTC)[回覆]
其實說實話我到現在也沒有在視窗平台上開發過任何正經程序。習慣了在Linux上碼農,Linux的確比視窗對碼農更加友好。不愛思考得豬留言2019年5月20日 (一) 14:51 (UTC)[回覆]
我修改的火狐瀏覽器和Firefox主線升級沒有任何關係。主線Firefox升級了以後,我修改的火狐瀏覽器還是能夠正常工作的,只不過就是版本老一些而已。不愛思考得豬留言2019年5月20日 (一) 14:53 (UTC)[回覆]
或者將這個做成一個可以外部配置讀取的配置文件,判斷域名是否需要SNI修正,不需要的話就照常,需要的話就修正。不過還是那句,硬核玩法。——路過圍觀的Sakamotosan | 避免做作,免敬 2019年5月20日 (一) 01:20 (UTC)[回覆]
多謝cwek兄的建議,我試着創造一個外部配置讀取的配置文件。同時我再實驗幾種其它的靈活改變SNI方法看看效果如何。不愛思考得豬留言2019年5月20日 (一) 13:37 (UTC)[回覆]

各位,我已經上了Mozilla-Dev瞄了幾眼,在視窗上編譯火狐瀏覽器需要40個G的空間。我現在使用的視窗筆記本電腦💻沒有這麼多空間(因為是固態硬盤SSD),所以明天我先要去Best Buy買一台新的筆記本電腦💻,以傳統旋轉磁硬盤(HDD)為主(這樣空間足夠大),然後再開始安裝Visual Studio等視窗編譯環境,然後開始修改視窗版火狐瀏覽器原始碼,然後開始編譯視窗版火狐瀏覽器。整個過程最長可能要花上幾個禮拜的時間。所以這段討論基本上可以存檔到「如何訪問維基百科」裏面了,等我以後有更新了再開一個新話題告知修改版視窗火狐瀏覽器的下載地址。不愛思考得豬留言2019年5月20日 (一) 17:23 (UTC)[回覆]

如何確保分發的二進制文件中僅有相關部分被改變?-Mys_721tx留言2019年5月20日 (一) 21:21 (UTC)[回覆]
哈哈,這位兄台問的問題很好,以下是我的一些想法:
  • 我以人格擔保,我修改的火狐瀏覽器里絕對不夾雜私貨!(當然這是最最薄弱的一種說辭)
  • 你可以把我分享的文件與官方發行版火狐瀏覽器文件做一個二進制diff,你會觀察到唯一的不同就是libssl3.so(視窗上應該是libssl3.dll),而且你把不同的那些字節調出來進行反匯編(disassembly)以後會發現反匯編出來的那些指令正是我所添加的C語句的x64匯編語言版本。(當然這麼做對於很多人來說是非常有難度的)
  • 最好的方法當然是你自己去下載火狐官方原始碼,自己去到ssl3ext.c里做出修改,然後自己為自己編譯一個火狐瀏覽器。這也就是為什麼我這麼詳細的把要具體修改的東西在本客棧里貼出。我的初衷是讓所有人自行去修改原始碼然後編譯,因為我真的不覺得這麼做有任何難度(特別是當我已經為你們摸清楚了原始碼里究竟是哪個函數在控制着SNI)。
  • 還有就是把這個功能要求Mozilla添加到Nightly裏面去。(我不認識Mozilla的人,而且這種修改基本上不可能被Mozilla接受。)
如果其它維基人還能想出什麼好的保證機制請自由的往以上列表裏添加。不愛思考得豬留言2019年5月21日 (二) 01:40 (UTC)[回覆]


這其實等價於掛個HAProxy反向代理,然後中間人篡改下SNI……寫幾行配置就成,還不用重新編譯。--菲菇維基食用菌協會 2019年5月20日 (一) 23:41 (UTC)[回覆]

哇!😍😍😍😍😍😍😍😍😍😍😍😍真的嗎?!那麼還煩請菲菇兄能在本樓貼出使用反向代理篡改SNI的詳細具體操作教程,以及兄所說的那幾行配置。就像我在本樓里具體精確的指出需要修改火狐瀏覽器原始碼的哪一個文件里的哪一個函數裏的哪幾行代碼,以及怎麼修改那幾行代碼。謝謝!不愛思考得豬留言2019年5月21日 (二) 01:46 (UTC)[回覆]
奇技淫巧,不如肉翻。--菲菇維基食用菌協會 2019年5月21日 (二) 05:54 (UTC)[回覆]
菲菇兄這話說的在理兒!不愛思考得豬留言2019年5月21日 (二) 13:09 (UTC)[回覆]

火狐瀏覽器域前置修改更新

各位維基人大家好:

最近有幸能夠在Ubuntu 19.10上修改【最新】的火狐瀏覽器代碼。所以更新一下2019年5月我發過的Help_talk:如何訪問維基百科#修改火狐瀏覽器關於SNI的部分。(在Ubuntu 19.10上build火狐瀏覽器的具體步驟請參考[1]

修改地方一共有兩處。第一處就是2019年5月我修改的SNI代碼,但是最新的火狐瀏覽器代碼里負責生成ClientHello的原始碼文件名換了(或者說是細化了),新的原始碼文件名是mozilla-unified/security/nss/lib/ssl/ssl3exthandle.c。具體負責生成ClientHello的函數也換了(或者說是細化了),新函數原始碼如下:

/* 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.
 */
SECStatus
ssl3_ClientFormatServerNameXtn(const sslSocket *ss, const char *url,
                               TLSExtensionData *xtnData,
                               sslBuffer *buf)
{
    unsigned int len;
    SECStatus rv;

    len = PORT_Strlen(url);
    /* length of server_name_list */
    rv = sslBuffer_AppendNumber(buf, len + 3, 2);
    if (rv != SECSuccess) {
        return SECFailure;
    }
    /* Name Type (sni_host_name) */
    rv = sslBuffer_AppendNumber(buf, 0, 1);
    if (rv != SECSuccess) {
        return SECFailure;
    }
    /* HostName (length and value) */
    rv = sslBuffer_AppendVariable(buf, (const PRUint8 *)url, len, 2);
    if (rv != SECSuccess) {
        return SECFailure;
    }

    return SECSuccess;
}

具體修改和2019年5月我公佈的修改一樣,修改如下兩處地方:

    len = PORT_Strlen(url);

修改成

    len = PORT_Strlen("upload.wikimedia.org\0");
    rv = sslBuffer_AppendVariable(buf, (const PRUint8 *)url, len, 2);

修改成

    rv = sslBuffer_AppendVariable(buf, (const PRUint8 *)"upload.wikimedia.org\0", len, 2);

注意,如果upload.wikimedia.org被SNI封殺的話,那就要更換成另外一個尚未被SNI封殺的維基基金會的SNI域名。

這一次的修改比起2019年5月的修改,多了一個要修改的原始碼文件。我想既然是域前置,那就乾脆做全套的域前置,包括DNS部分。所以我順藤摸瓜的摸到了火狐負責完成DNS查詢的原始碼。原始碼的文件名是mozilla-unified/netwerk/dns/nsHostResolver.cpp。具體負責DNS查詢的函數名叫nsHostResolver::ResolveHost,細節如下:

nsresult nsHostResolver::ResolveHost(const nsACString& aHost,
                                     const nsACString& aTrrServer,
                                     uint16_t type,
                                     const OriginAttributes& aOriginAttributes,
                                     uint16_t flags, uint16_t af,
                                     nsResolveHostCallback* aCallback) {
  nsAutoCString host(aHost);
  NS_ENSURE_TRUE(!host.IsEmpty(), NS_ERROR_UNEXPECTED);

  nsAutoCString originSuffix;
  aOriginAttributes.CreateSuffix(originSuffix);
  LOG(("Resolving host [%s]<%s>%s%s type %d. [this=%p]\n", host.get(),
       originSuffix.get(), flags & RES_BYPASS_CACHE ? " - bypassing cache" : "",
       flags & RES_REFRESH_CACHE ? " - refresh cache" : "", type, this));

  // ensure that we are working with a valid hostname before proceeding.  see
  // bug 304904 for details.
  if (!net_IsValidHostName(host)) {
    return NS_ERROR_UNKNOWN_HOST;
  }

  // By-Type requests use only TRR. If TRR is disabled we can return
  // immediately.
  if (IS_OTHER_TYPE(type) && Mode() == MODE_TRROFF) {

...

整個函數的篇幅巨長,所以我就不全部列出了。需要修改的是第一行:

  nsAutoCString host(aHost);

修改成

  nsAutoCString host("upload.wikimedia.org\0");

注意,如果upload.wikimedia.org被DNS污染的話,那就要更換成另外一個尚未被DNS污染的維基基金會的DNS域名。

祝牆內的各位維基人在魔改火狐瀏覽器以後,免翻牆域前置瀏覽維基百科快樂!

--不愛思考得豬留言2020年9月8日 (二) 02:31 (UTC)[回覆]