帮助:如何访问维基百科/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)
- 奇技淫巧,不如肉翻。--菲菇@维基食用菌协会 2019年5月21日 (二) 05:54 (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域名。
祝墙内的各位维基人在魔改火狐浏览器以后,免翻墙域前置浏览维基百科快乐!