背景
在西弗吉尼亚州查尔斯顿举办的SecureWV 2019网络安全会议上,我和Peixue进行了一场关于“剖析洋葱路由Tor网桥和可插拔传输”的主题演讲。我们将全部研究成果分两篇文章发表。在本文中,我们将分享这项研究的更多细节。我们将主要讲解如何找到内置的Tor网桥,以及Tor浏览器如何与启用的网桥共同工作。
Tor浏览器与Tor网络
Tor浏览器是一个通过Tor网络加密层提供匿名互联网连接的工具。当用户使用Tor浏览器浏览网站时,用户的真实IP地址会被Tor网络隐藏,因此目标网站永远无法获取到真实的源IP地址。用户还可以在Tor网络中建立自己的网站,其域名以“.onion”结尾。这样一来,只有Tor浏览器可以访问该网站,而没有人知道网站所对应的真实IP地址。这也解释了为什么勒索软件制作者会要求受害者通过Tor浏览器访问.onion网站上的付款页面。Tor项目团队目前清楚这样的案例,并且在Tor项目的博客上明确指出“Tor现在正在被犯罪者滥用”。
Tor浏览器是一个开源项目,其设计基于Mozilla Firefox浏览器。用户可以从其官方网站下载源代码。Tor网络是一个覆盖全球的网络,由数千个主动运行的中继节点组成。其中,又可以分为两类中继节点——普通中继节点和网桥中继节点。普通中继节点会在Tor目录中列出,用户可以轻松通过检测的方式识别和阻止与中继节点的连接。
网桥信息是在Firefox的配置文件中定义的,因此用户可以在Tor浏览器的地址栏中输入“about:config”来进行查看,如下图所示。
在Tor浏览器中查看配置数据:
但是,网桥中继节点并没有在Tor主目录中列出,这也就意味着不能通过检测的方式阻断与这些节点的连接。接下来,我将讨论如何使用Tor浏览器中内置的功能查找这些网桥与中继节点。
要在Tor浏览器中使用网桥中继,可以选择两种方法。Tor浏览器有一些内置的网桥供用户选择。如果内置的网桥不起作用,则用户可以通过访问https://bridges.torproject.org/或发送电子邮件到[email protected]来获取其他网桥。
分析使用的平台环境
我们的分析过程,是在以下平台环境中进行的:
使用Windows 7 SP1 x32操作系统;
使用Tor 浏览器8.0版本;
使用TorLauncher 0.2.16.3浏览器扩展;
使用Torbutton 2.0.6浏览器扩展。
Tor浏览器的版本信息:
在我们进行分析的过程中,Tor浏览器在2019年10月22日推出了Tor浏览器9.0的新版本。有关该版本的更多信息,请参考本文的附录。
使用内置网桥启动Tor浏览器
我们所分析的版本中,Tor浏览器提供了四种网桥方式:obfs4、fte、meek-azure和obfs3。这些都可以称为可插拔传输(Pluggable Transports)。我们可以在下图中看到详细的配置。
在Tor网络设置中选择一个内置的网桥:
我们强烈建议选择Tor官方网站上的obfs4网桥,本文的全部分析过程均使用这种网桥。通过查看Tor浏览器建立obfs4连接时的通信量,我发现TCP会话是由obfs4proxy.exe创建的,obfs4proxy.exe是一个网桥的客户端进程。
下图展示了使用obfs4启动Tor浏览器时的进程树。如图所示,firefox.exe启动了tor.exe,然后启动了obfs4proxy.exe。进程obfs4proxy.exe位于“Tor_installation_folder\Browser\TorBrowser\Tor\PluggableTransports”目录下。最初,我认为内置的obfs4网桥应该在obfs4proxy.exe进程中进行硬编码。
使用obfs4网桥时的进程树:
在网桥进程obfs4proxy.exe中进行跟踪
我们启动调试器,将其附加到obfs4proxy.exe。随后,我在connect API上设置了一个断点,该断点通常用于建立TCP连接。通常情况下,使用逆向工程的方法就可以迅速从这个API发现IP地址和端口,但在建立与obfs4网桥的连接之前,这个API并没有被触发过。经过对进程obfs4proxy.exe的进一步分析,我了解到它换用了来自mswsock.dll的另一个API“MSAFD_ConnectEx”。
调用MSAFD_ConnectEx API:
上图展示了obfs4proxy.exe调用API mswsock.MSAFD_ConnectEx()以建立与内置obfs4网桥的TCP连接,该网桥的IP地址和端口号为192.95.36.142:443。该函数的第二个参数是指向struct sockaddr_in结构变量的指针,其中保存着IP地址和要连接的端口。随后将调用WSASend和WSARecv这两个API,以与obfs4网桥进行通信。我们注意到,OllyDbg无法识别这个API,因为它不是mswsock.dll的导出功能。在使用IDA Pro对mswsock.dll进行分析时,我们看到地址750A7842只是MSAFD_ConnectEx()的API。这里需要说明的是,“call dword ptr [ebx]”指令被用来调用obfs4proxy.exe所需的几乎所有系统API,这是一种隐藏API以防止被分析的方法。
根据我的分析,Tor使用的大多数PE文件(exe和dll文件,例如obfs4proxy.exe)似乎都是由“GCC MINGW-64w编译器”编译的,该编译器始终使用“mov [esp], …”将参数传递给函数,而并没有使用会影响静态分析的“push…”指令。通过跟踪MSAFD_ConnectEx()中的调用堆栈流,我意识到我最初的想法是错误的,内置的IP地址和端口并没有在obfs4proxy.exe中进行硬编码,而是通过本地回环TCP连接从父进程中获取的。
Obfs4proxy.exe接收到一个obfs4网桥的IP地址和端口:
通常,从tor.exe到obfs4proxy.exe的第三个数据包中,包含一个内置的obfs4网桥的IP地址和二进制端口,如上图所示。它是一个Socks5数据包,长度为0xA字节。05 01 00 01时Socks5协议的标头,其余的数据是IP地址和二进制端口。该数据包表明它要求obfs4proxy.exe与二进制表示IP地址和端口的网桥建立连接。随后,obfs4proxy.exe对数据包进行解析,并将二进制的IP和端口转换为字符串,在我们所分析的样本中IP和端口为154.35.22.13:16815。
转向Tor.exe
Tor.exe使用来自libevent(事件通知库)中名称为libevent.dll的第三方模块来驱动Tor执行任务。Tor将大多数套接字任务(connect()、send()、recv()等)放在由libevent自动调用的事件上。在Tor.exe中以网桥IP地址和端口为关键字跟踪数据包时,我们可以在调用堆栈上下文中看到模块libevent.dll由许多返回地址。在下图中,暂停在了Tor.exe调用API ws2_32.send()以发送包含网桥IP地址和端口的数据包这一步上,类似于上图展示的接收数据包。
下图是调用堆栈窗口展示的libevent.dll的返回地址:
通过跟踪发送网桥IP地址和端口的tor.exe,我们发现了一个地方,它使用回调函数启动一个新事件,然后该回调函数发送网桥的IP地址和端口。下面的ASM代码片段展示了在tor.exe中调用libevent.event_new()的上下文。其中第二个参数是套接字句柄,第三个参数是事件动作(在这里是14H,代表着EV_WRITE和EV_PERSIST),第四个参数是回调函数(这里是sub_2833EE),第五个参数是网桥的IP地址和端口(一旦被libevent调用,就会被传递给回调函数sub_2833EE)。
下面是tor.exe中截取的ASM代码段,当前基址是00280000h。
[…] .text:00281C84 mov edx, eax .text:00281C86 mov eax, [ebp+var_2C] ; .text:00281C89 mov [eax+14h], edx .text:00281C8C mov eax, [ebp+var_2C] ; .text:00281C8F mov ebx, [eax+0Ch] .text:00281C92 call sub_5133E0 .text:00281C97 mov edx, eax .text:00281C99 mov eax, [ebp+var_2C] .text:00281C9C mov [esp+10h], eax ; argument for callback function .text:00281CA0 mov [esp+0Ch], offset sub_2833EE ; the callback function .text:00281CA8 mov [esp+8], 14h ; #define EV_WRITE 0x04|#define EV_PERSIST 0x10 .text:00281CB0 mov [esp+4], ebx ; socket .text:00281CB4 mov [esp], edx .text:00281CB7 call event_new ; event_new(event_base, socket, event EV_READ/EV_WRITE, callback_fn, callback_args); .text:00281CBC mov edx, eax .text:00281CBE mov eax, [ebp+var_2C] .text:00281CC1 mov [eax+18h], edx […]
通过在tor.exe中连续进行反向跟踪,我发现了一些obfs4网桥,它们都位于命令SETCONF的数据结构中,如下图所示。
SETCONF命令中的部分数据:
这里展示了命令SETCONF的数据片段,其中SETCONF是结构开始的命令名称,后面紧接着内置的网桥信息。红色标出的数据是一个称为Obfs4 Bridge的块,称为网桥配置行。每个网桥的节点都必须保存在一个网桥配置行中。这里,总共有27个这样的网桥配置行。如我们所见,网桥类型obfs4、网桥的IP地址和端口都使用了字符串格式。
至此我们知道,网桥信息也同样不是在tor.exe进程中硬编码的。那么现在,我们就遇到了另一个问题——整个SETCONF数据是从哪里来的?事实上,它是来源于firefox.exe接收到的TCP数据包,firefox.exe是tor.exe的父进程。一旦tor.exe启动,它将会打开TCP控制端口9151以接收来自firefox.exe和TCP代理端口9150的命令。我们将在下文中详细说明firefox.exe如何向其发送命令。
继续分析tor.exe,我们注意到除了命令SETCONF之外,它还支持其他命令,例如GETCONF、SAVECONF、GETINFO、AUTHENTICATE、SETEVENTS、+LOADCONF、QUIT等。Tor.exe使用一个函数来处理这些命令并转向不同的代码分支。
Tor.exe根据具体的命令来执行不同的任务。例如,SAVECONF命令将使得tor.exe将网桥信息保存到“Tor_installation_folder\Browser\TorBrowser\Tor\Data\torrc”中的文件中,SETCONF负责通知tor.exe网桥信息,然后将其传递到网桥进程以建立网桥连接。
在Firefox.exe中寻找内置网桥
Tor使用了许多回环TCP连接在进程之间传递命令,以执行其具体任务。
自3.0.1版本开始,Wireshark开始支持本地回环适配器,从而允许我们捕获回环接口上的流量,例如本地回环接口上从firefox.exe通过9151 TCP控制端口传到tor.exe的SETCONF数据包。RawCap是另一个可以使用的工具。Firefox.exe以“一个字节一个数据包”的特殊方式将命令数据包发送到tor.exe。如下图所示,展示了许多长度为1的数据包。通过对它们进行组合,可以形成完整的SETCONF命令。
Firefox.exe发送命令包:
大家可能知道,Firefox浏览器可以使用第三方开发人员开发的扩展插件来执行扩展功能。由于Tor浏览器是基于Mozilla Firefox设计的,因此它具有与Firefox相同的功能。
在9.0版本之前,Tor浏览器带有两个扩展:Torbutton和TorLauncher。它们位于“Tor_installation_folder\Browser\TorBrowser\Data\Browser\profile.default\extensions”文件夹中。相应的扩展文件是[email protected]和[email protected],如下图所示,这些都是包含JS文件和模块的ZIP压缩包。
Tor扩展文件:
但是,自9.0版本Tor浏览器以来,这两个扩展已经被删除。相反,它们的代码和功能已经合并到Tor浏览器中。更多信息请参考最后的附录部分。
Torbutton用于设置和显示Tor设置,以及显示Tor信息,例如“关于Tor”。我们可以单击Tor浏览器工具栏上显示的Tor图标找到该功能。
TorLauncher负责根据Torbutton设置来控制Tor进程。
在Firefox的xul.dll模块中,将加载TorLauncher并执行JS代码,xul.dll是Mozilla Firefox的核心组件,也是实际上将SETCONF(在network-setting.js中实现)、GETCONF(在tl-protocol.js中实现)、GETINFO(在tl-protocol.js和torbutton.js中实现)等命令发送到tor.exe的模块。Xul.dll模块是一类JS引擎,用于解析和执行这些命令的JS代码。
现在,让我们看看启用网桥后的Tor浏览器的内部工作原理。Tor浏览器在启用网桥的状态启动后,将执行以下步骤:
1、firefox.exe加载基本配置文件,首选项定义和扩展(涉及的模块:firefox.exe、xul.dll);
2、扩展TorLauncher使用命令行中的设置运行tor.exe(涉及的模块:xul.dll);
3、用户可以随时使用Torbutton更改Tor设置(涉及的模块:xul.dll);
4、它将命令发送到tor.exe,并根据通过本地回环TCP连接来传递具体的工作方式(涉及的模块:xul.dll、tor.exe);
5、然后,tor.exe运行一个特定的网桥进程(obfs3和obfs4使用obfs4proxy.exe,fte使用fteproxy.exe,meek-azure使用terminateprocess-buffer.exe)来建立网桥通信(涉及的模块:tor.exe、网桥进程);
6、tor.exe通过本地回环TCP连接与这些网桥进程通信(涉及的模块:tor.exe、网桥进程);
7、网桥进程连接到网桥中继(涉及的模块:网桥进程)。
Tor浏览器通过本地回环接口发送命令,以控制Tor客户端的工作模式。然后,它从Tor客户端接收并解析执行命令的结果。
根据我的分析,所有obfs4网桥信息都是在大量命名的全局变量(在Firefox中称为首选项)中定义的,并存储在本地文件中。它们在firefox.exe启动时进行初始化,根据名称(由xul.dll派生)读取首选项,随后对其进行访问和使用。
出于安全考虑,我隐去了包含所有网桥类型的整个网桥信息定义集合的文件名。在我所分析的Tor浏览器版本中,共包含27个obfs4网桥、4个obfs3网桥、1个meek-azure网桥和4个fte网桥。
可插拔传输
在上述分析过程中,我们提到了一些网桥的类型,包括obfs4、obfs3、meek-azure和fte。所有这些,都统称为“可插拔传输”(Pluggable Transport,PT)。它们的主要任务是转换Tor流量,并在客户端与第一跳(Tor中继)之间进行传输。因此,PT的使用可能会使Tor流量更加难以通过审查来识别或组阻断。
Obfs4是一个功能强大且最受欢迎的PT。通常,Obfs4的第一跳都是网桥中继,没有在Tor主目录中列出。Obfs4(混淆器)是由Yawning Angel开发和维护的。这是一个使用Go语言编写的开源项目,可以在GitHub上找到。Obfs网桥有多个历史版本,包括Obfs2和Obfs3。实际上,Obfs4与历史版本差异较大,更接近于ScrambleSuit,因为Obfs4的概念就来源于Philipp Winter的ScrambleSuit协议。这也就是为什么Obfs4客户端进程还可以充当ScrambleSuit客户端的原因。
Obfs4的所有功能都由obfs4proxy.exe程序提供,该程序是Tor用户的Obfs4客户端进程。
我们已经知道,Tor浏览器是基于Firefox浏览器构建的,它使用Firefox的扩展程序为用户提供匿名互联网访问。在Tor浏览器运行后,实际上会启动firefox.exe,该文件将加载两个Tor扩展(TorLauncher和Torbutton)。随后,其中的一个扩展启动tor.exe,也就是Tor客户端进程。当在Tor网络设置中将Tor浏览器设置为启用Obfs4网桥时,tor.exe将运行喔bfs4proxy.exe。
接下来,我们将详细介绍firefox.exe、tor.exe和obfs4proxy.exe这三个组件如何相互配合。
Tor浏览器如何与Tor客户端通信
Firefox.exe进程(Tor浏览器)和tor.exe(Tor客户端)通过Tor的TCP端口侦听回环接口(127.0.0.1)以实现相互通信。
根据上文我们可以看到,firefox.exe启动了tor.exe。或者更准确地说,Firefox扩展TorLauncher启动了tor.exe,该扩展由模块xul.dll加载并解析。当它调用Windows本地API ShellExecuteExW()启动tor.exe时,会有许多命令行参数传递给tor.exe。下图展现了这些命令行参数:
可以看到,共有10个参数传递给tor.exe。为了使这一过程更加清晰,我列出了下面的表格,每行展现一个参数。其中,“…\”用来代表Tor浏览器的安装路径。
在上表中,特别标出了两个端口,分别是“+__ControlPort 9151”和“+__SocksPort "127.0.0.1:9150 IPv6Traffic PreferIPv6 KeepAliveIsolateSOCKSAuth"”。Tor.exe将侦听回环接口(127.0.0.1)的TCP 9151和9150端口,即tor.exe使用的默认端口号。
这些默认值定义在几个本地文件中,用户可以修改两个端口的默认值。下面是本地文件中的相关代码样例:
// Proxy and proxy security pref("network.proxy.socks", "127.0.0.1"); pref("network.proxy.socks_port", 9150);
TCP/9151端口是用于在firefox.exe和tor.exe之间交换控制命令和结果的控制端口,TCP/9150则被Tor用于提供SOCKS5代理服务,该服务在firefox.exe和tor.exe之间传递SOCKS5数据包。
下面是控制命令的示例。Firefox.exe通过TCP/9151端口发送命令GETINFO到tor.exe并询问一些信息,随后tor.exe解析该命令,并将结果从TCP/9151端口发送回firefox.exe。
GETINFO status/bootstrap-phase 250-status/bootstrap-phase=NOTICE BOOTSTRAP PROGRESS=0 TAG=starting SUMMARY="Starting" 250 OK
Tor在127.0.0.1:9150上执行代理功能,该功能负责在firefox.exe和tor.exe之间传输普通数据包(未经Tor加密)。下图展示了在WireShark中捕获的数据包,这里我将其分为两部分,上面是Tor SOCKS5握手,下面是数据包传输。
通过TCP/9150端口在firefox.exe和tor.exe之间的Tor SOCKS5代理数据:
上图展示了使用Tor浏览器访问https://www.google.com时的数据包。在握手过程中,firefox.exe告诉tor.exe访问的目标是SOCKS5数据包中的www.google.com。在完成SOCKS5握手后,firefox.exe开始将Google TLS客户端Hello数据包发送到tor.ee,然后在Tor客户端对数据包进行加密。随后,Google TLS服务器Hello数据包从Tor客户端被发送回去。在此之后,firefox.exe开始通过TCP/9150端口向tor.exe发送请求数据包,并从tor.exe接收响应数据包。
在Tor客户端,通过TCP/9150端口接收数据包,并完成SOCKS5握手。然后,它从firefox.exe接收正常的数据包并将其加密。将Tor加密的数据包传递到所选Tor电路中的Tor入口节点。最后,Tor出口节点对接收到的数据包进行解密,得到正常数据包(如同在firefox.exe和tor.exe之间传输的数据包),并将其发送到目标服务器。
反过来,tor.exe从Tor入口节点接收加密的数据包,然后对其解密,得到纯文本数据包,该数据包最终通过TCP/9150端口从tor.exe发送到firefox.exe。
Tor客户端如何与Obfs4客户端通信
一旦Tor客户端从Tor浏览器接收到SETCONF命令,Tor客户端(tor.exe)就会启动Obfs4客户端obfs4proxy.exe。obfs4proxy.exe在回环接口上打开一个随机的TCP端口为Tor提供Obfs4网桥服务,它通过进程间管道将TCP端口号通知其父进程tor.exe。
下面的OllyDbg截图中展示了Windows API WriteFile()上的断点是在obfs4proxy.exe将其TCP端口通知tor.exe时被触发的。我们在内存中看到,“CMETHOD obfs4 socks5 127.0.0.1:49496”是用于将TCP端口发送到Tor客户端的数据。此时随机产生的TCP端口是49496。WriteFile()的第一个参数是进程间管道的文件句柄,这次是00000100。
obfs4proxy.exe通过进程间管道将其TCP端口发送到tor.exe:
之后,tor.exe可以建立与这个TCP端口的连接,以与Obfs4客户端进行通信。Tor分别将各网桥节点发送到这个端口,每次仅发送一个网桥的信息。从下图TCPView的截图中可以看到详细过程。
Tor将Obfs4网桥发送到Obfs4proxy:
上述就是Tor浏览器(firefox.exe)通过Tor客户端(tor.exe)与Obfs4客户端(obfs4proxy.exe)通信的整个过程。在下一篇文章中,我们将说明Obfs4客户端(obfs4proxy.exe)是如何与Obfs4网桥建立连接,以及Obfs4是如何转换数据包以防止被检测的。
附录
在2019年10月下旬,发布了Tor浏览器9.0版本,该版本中删除了两个扩展(TorLauncher和Torbutton),改为由Tor浏览器执行这两个扩展原本执行的任务。实际上,在分析过程中,我发现新版本并没有删除这两个扩展的代码,而是将代码集成到了名为“omin.ja”的Firefox JAR文件中。该文件在启动时由Firefox加载并解析。后面,我们可以使用菜单“Options”-“Tor”找到Tor网络设置,如下图所示。
两个扩展的功能已经集成到Tor浏览器中:
新版本的这一更改,不会影响到Tor浏览器的工作原理。因此,即使我们的分析过程是基于旧版本8.0,但该分析针对新版本也仍然适用。
在下篇文章中,我们将重点说明Tor是如何利用Obfs4网桥逃避审查的。