0x00 前言
在上篇文章中(原文,译文),我详细介绍了Tor浏览器的内置Bridge(网桥)信息如何通过3个进程进行传递(firefox.exe
、tor.exe
及obfs4proxy.exe
)、Tor浏览器如何与Obfs4
bridge客户端通信以及这3个进程之间的关系。在本文中,我将继续介绍Tor如何使用Obfs4
Bridge绕过网络传感器。
0x01 Tor数据流
在分析Obfs4
Bridge之前,我想先介绍下启用和未启用Obfs4
Bridge时Tor浏览器的不同点。为了让大家有更直观的理解,我画了关于Tor数据流的两张图,如图1及图2所示:
图1. 未启用Obfs4
Bridge时的正常Tor数据流
未启用Obfs4
Bridge时的正常Tor数据流如图1所示,主要过程为:Tor客户端从Tor主目录获取3个Tor中继节点,分别为入口中继、中间中继以及出口中继。当用户访问网站时,Tor客户端就会从Tor浏览器(firefox.exe
)收到请求报文,然后使用出口中继、中间中继及入口中继的会话密钥对报文进行3次加密。经过多重加密的报文随后会被发送到入口中继、中间中继以及出口中继。经中继节点解密后,出口中继还原出原始报文,将其发送到目的服务器(如www.google.com
)。这大致上就是Tor浏览器将原始报文投递至目标网站的方式。
然而,网络传感器可以轻松识别并阻止经过Tor加密的流量(图1中红色箭头)。
图2. 使用Obfs4
Bridge后的Tor数据流
使用Obfs4
Bridge后的数据流如图2所示。启用Obfs4
Bridge后,Tor流量会先经过Obfs4
客户端(obfs4proxy.exe
),然后发送给bridge中继节点。Bridge中继节点现在会取代原有的Tor入口中继节点,变成新的入口中继节点。因此,现在任何Tor电路(circuit)包含的中继节点数始终为3,以获得较好的通信性能。
Obfs4
Bridge客户端接收经过Tor加密的payload,然后使用Obfs4
函数对其再次封装。经过封装后的报文随后会发送到Obfs4
Bridge中继节点,经过解封装后,Bridge中继节点提取Tor加密报文,将其转发至Tor电路中的下一跳Tor中继。数据包的反向路径与此类似。
以上就是Obfs4
Bridge的简要工作流程。下面我们将探讨Obfs4
Bridge具体使用哪些技术来强化流量,使流量更难识别。
0x02 SETCONF命令及Bridge配置行
Tor浏览器(firefox.exe
)会通过TCP控制端口9151
向tor.exe
发送SETCONF
命令及内置的Obfs4
Bridge信息。命令格式如下所示:
SETCONF UseBridges=1 Bridge="obfs4 109.105.109.165:10527
8DFCD8FB3285E855F5A55EDDA35696C743ABFC4E
cert=Bvg/itxeL4TWKLP6N1MaQzSOC6tcRIBv6q57DYAZc3b2AzuM+/TfB7mqTFEf
XILCjEwzVA iat-mode=1" Bridge="obfs4 85.17.30.79:443 ……
命令开头处的SETCONF
为命令名称,UseBridges=1
代表用户已启用Bridge功能。后面的数据包括整个内置的Obfs4
Bridge数据块(Tor网站上称之为bridge配置行)。每个bridge节点都必须保存在一条bridge配置行中,这里为演示方便,我只列出其中一行。每个bridge配置行都以Bridge=
作为前缀,下面我们来分析一下具体内容。
配置行中包含如下内容:
- Bridge类型:
obfs4
。 - Bridge服务端IP及端口:
109.105.109.165:10527
。 - Bridge ID:
14H
长的十六进制值。 - Bridge证书(“cert”):经过base64编码的
Obfs4
中继的nodeID
及IdPublicKey
。 - Bridge IAT-mode:IAT模式标志位,可取
0
、1
或者2
。
Tor会处理该命令,然后运行obfs4proxy.exe
子进程,该进程为Obfs4
Bridge客户端。obfs4proxy.exe
采用了相同的过程,通过环回接口上的TCP端口,在Obfs4
Bridge客户端及Tor客户端之间传输报文。
0x03 Tor客户端与Obfs4客户端通信
Tor客户端会将Obfs4
Bridge信息分别发送给obfs4proxy.exe
,要求对方与Obfs4
Bridge中继节点建连。Tor使用SOCKS5协议与Obfs4
Bridge客户端传输数据。
图3. tor.exe
将某个Obfs4
Bridge信息发送给obfs4proxy.exe
WireShark抓取的tor.exe
与obfs4proxy.exe
之间的通信数据如图3所示。红色部分为tor.exe
发往obfs4proxy.exe
的数据包(通过环回接口上的随机TCP端口),响应报文为蓝色部分。从上图可知,该过程中还用到了SOCKS5协议。
首先,tor.exe
将包含base64编码的cert=
及IAT-mode的 Obfs4
Bridge信息发送给obfs4proxy.exe
。随后,tor.exe
通过SOCKS5 “Connect (1)”报文(以|05 01 00 01|
开头)发送Obfs4
Bridge中继的IP地址及端口二进制数据。obfs4proxy.exe
随后通过该IP地址及端口连接到相应的Obfs4
Bridge中继。连接成功建立后,obfs4proxy.exe
会向tor.exe
发送“Succeeded (0)”报文(|05 00 00 01 00 00 00 00 00 00|
)。这意味着Obfs4
握手成功完成(下文我会介绍该握手过程)。随后tor.exe
可以通过该连接,发送并接收由Obfs4
Bridge传输的所有数据包。
0x04 Obfs4客户端与Obfs4 Bridge握手过程
客户端与Obfs4
Bridge连接时需要完成握手,目的是传输公钥,互相验证身份。
在分析握手报文之前,我想先花点时间介绍下Obfs4
所使用的加密算法及“密钥对”(Keypair)结构。
Obfs4
Bridge使用了ECC(椭圆加密)算法来加密Tor payload,确保Obfs4
客户端与中继节点之间的通信安全。众所周知,ECC是基于椭圆曲线理论的一种公钥加密技术。Obfs4所使用的ECC采用Go语言包curve25519
来实现,该程序包提供了两个重要函数:ScalarBaseMult()
及ScalarMult()
。
Keypair
结构的定义如下所示,其主要功能是保存公私钥对:
// Keypair is a Curve25519 keypair with an optional Elligator representative.
// As only certain Curve25519 keys can be obfuscated with Elligator, the
// representative must be generated along with the keypair.
type Keypair struct {
public *PublicKey
private *PrivateKey
representative *Representative
}
客户端及服务端都必须有自己的Keypair
实例。根据curve25519包中的定义,PrivateKey
在一定范围内随机生成。根据ECC算法,PublicKey
可以通过调用curve25519.ScalarBaseMult()
从私钥生成。Representative
秘钥根据公钥生成,在需要的时候可以调用extra25519.RepresentativeToPublicKey()
函数再次转换为公钥。Keypair
的定义及初始化代码位于NewKeypair()
函数中,该函数在ntor.go
文件中定义。
客户端及服务端都应当保存好私钥,将公钥以Representative
秘钥的形式发送给对方。Obfs4
客户端的连接过程从握手报文开始,因此我们来看一下客户端的握手报文。该报文的整体结构如下所示:
图4. 客户端的握手报文结构
客户端握手报文的第1部分为Keypair.representative
,大小为20h
字节。该数据在服务端可以用来还原出客户端的公钥。
第2部分为随机字节填充数据,数据大小范围为4Dh
到1FC0h
。填充数据可以用来混淆握手报文大小,使其更难识别。
第3部分为Obfs4
源码中的mark
区域,是第1部分(Keypair.representative
)的HMAC值。Obfs4
使用SHA-256
来生成HMAC,大小为20h
字节。Obfs4
只将前10h
字节作为HMAC,丢弃剩余的10h
字节。
Obfs4
使用当前系统时间来计算UNIX Epoch时间(从UTC时间1970年1月1日星期四00:00:00开始)的小时值。在一定时间内,本地时间不同的客户端及服务端能够计算出相同的小时值。随后,Obfs4
计算前3部分的HMAC值,再加上字符串形式的小时值。所得结果大小为20h
字节,前10h
字节作为报文的第4部分。
图5. obfs4proxy.exe
发送客户端握手报文
Ollydbg中可以观察到客户端的握手报文,如上图所示。我将相应内存数据切分为4部分,每部分采用不同颜色。在上图中,填充数据长度为52h
字节,服务端可以使用第3部分(mark
)及最后一部分(整个数据的HMAC值)来验证客户端的握手报文。
即使我们正确使用了以上所有数据,如果用户系统使用的是不正确的系统时间,Obfs4
Bridge仍然无法完成握手过程。这种情况如图6所示,当时我的测试系统时间并没有更新,导致当服务端收到客户端握手报文后(序号4),会关闭TCP会话(序号6)。
图6. 系统时间错误导致服务端认证失败
一旦连接请求成功建立,Obfs4
中继开始启动一个新线程,处理与客户端的通信数据,其中就包括客户端的握手报文。服务端的处理过程与客户端相似,会根据客户端的Keypair
生成服务端的Keypair
实例,调用curve25519.ScalarBaseMult()
,根据随机选择的私钥计算出Representative
秘钥及公钥。
服务端随后会解析并验证客户端的握手报文。在确保报文一切正确后,服务端会调用extra25519.RepresentativeToPublicKey()
函数,从Representative
秘钥还原出客户端的公钥,Representative
值位于握手报文的第一部分。
下一步对ECC算法的“标量乘法”(即curve25519.ScalarMult()
函数)而言非常关键。标量乘法是一种单向函数,没有对应的标量除法,可以确保ECC算法的安全性。
服务端使用私钥以及客户端的公钥执行标量乘法。与此类似,客户端收到服务端的握手报文后也会执行相同操作。计算结束后,两次标量乘法的结果应保持一致,等式如图7所示。
图7. ECC算法等式
该等式是ECC算法的关键点,能确保两端相等。等式左侧计算在服务端完成,右侧计算在客户端完成。
此外,随机生成的公私钥只对一个TCP连接会话有效,因此被称为会话公钥/私钥。这与ID公钥/私钥要区分开来,对某个Obfs4
中继而言这两个秘钥保持不变。
前面我们提到过,bridge配置行中经过base64编码的cert
中可以提取出IdPublicKey
及nodeID
。这里的IdPublicKey
实际上始终保存在Obfs4
中继中。当Obfs4
中继安装并启动后,会生成IdPrivateKey
/IdPublicKey
对,然后保存好IdPrivateKey
,通过bridge配置行公布IdPublicKey
,以供Obfs4
客户端使用。这些秘钥对在Obfs4
中继上保持不变,也是ECC概念上的秘钥对,与会话秘钥类似。
接下来,客户端及客户端会两次调用ScalarMult()
来生成2个值。服务端会使用自己的会话私钥以及客户端的会话公钥来调用ScalarMult()
,然后再使用IdPrivateKey
及客户端的会话公钥再次调用该函数。现在我们得到了两个函数结果,这两个结果将与nodeID
及某些字符串常量拼接在一起,以生成keySeed
,该值作为服务端的auth
(认证数据)使用。
现在可以开始构造服务端的握手报文,报文中包含客户端握手报文的所有组件,并且服务端的auth
元素位于服务端Keypair.representative
及填充数据(数据大小可变)之间,如下图所示:
图8. 服务端握手报文结构
服务端的auth
元素用于客户端身份认证,填充数据大小范围为0h
~1F73h
,确保数据包大小有较大随机性。该报文随后会发送回客户端(obsf4proxy.exe
)进行验证。需要注意的是,服务端现在已经拥有了当前TCP连接会话的通用keySeed
。
当服务端的握手报文发送回客户端时,客户端会验证报文最后两部数据,确保报文数据有效。随后客户端从报文第一部分提取出服务端的会话公钥(也就是server.representative
)。
现在客户端会两次调用ScalarMult()
,过程与服务端类似。首先客户端使用自己的会话私钥及服务端的公钥调用ScalarMult()
,然后使用客户端的会话私钥及IdPublicKey
再次调用该函数。
得到两次函数结果后,客户端将两个结果拼在一起,加上nodID
及其他字符串常量,生成通用的keySeed
及auth
数据。客户端随后将生成的auth
数据与服务端握手报文中的数据对比,完成验证过程。
客户端生成的keySeed
应该与服务端生成的值相同。通过这个通用的keySeed
,客户端及服务端可以生成自己的加密解密秘钥,用来加密及解密Tor payload。客户端的加密秘钥与服务端的解密秘钥一样,而客户端的解密秘钥与服务端的加密秘钥一样。
以上就是Obfs4
客户端与bridge的握手过程,数据包大小在一个大的范围内随机,并且数据看上去也呈现随机化,因此网络传感器很难识别这种握手报文。
0x05 Obfs4封装Tor Payload
客户端及服务端会使用前面生成的公用keySeed
来初始化自己的Encoder
及Decoder
。Obfs4
随后使用这两个实例来封装(“Sealing”)和解封装(“Unsealing”)Tor payload。Encoder
实例用于加密,而Decoder
实例用于解密。
现在obfs4
客户端拥有2个连接:一个用来在Tor(tor.exe
)及Obfs4
客户端(obfs4proxy.exe
)之间交换数据,另一个用来在Obfs4
Bridge客户端及Bridge中继之间传输Tor payload。当完成握手过程后,Obfs4
客户端会将两个连接绑定在一起。绑定两个连接的copyLoop
函数(位于obfs4proxy.go
源文件中)代码如下图所示,其中a
和b
参数为两个连接对应的实例。
图9. copyLoop
函数源代码
io.Copy(destination, source)
函数用来将数据从源拷贝到目的地,具体操作过程为:函数首先调用source.Read()
,提取特定数据,然后调用destination.Write()
,将数据以参数形式传入。
Obfs4
客户端重写了Write()
以及Read()
函数。Write()
函数用来加密Tor payload报文(即封装),并将封装后的数据发送到Obfs4
中继。Read()
函数负责从Obfs4
中继接收报文,然后解密这些报文(即解封装)。io.Copy
随后将解密的Tor报文发送给Tor。
图10. 重写Write()
及Read()
函数
来详细分析这两个函数的内部原理。如上图所示,Write()
会调用makePacket()
,该函数会调用另一个函数Encoder.Encode()
来加密Tor payload。而Read()
会调用readPackets()
函数,后者会调用Decoder.Decode()
。
Encoder.Encode()
最终会调用secretbox.Seal()
来加密Tor payload,生成对应的MAC信息。与之对应的是,Decoder.Decode()
会调用secretbox.Open()
来解密从Obfs4
收到的payload。
经过加密的数据不单单包括Tor payload,payload会被拷贝到某个数据缓冲区的offset + 3
地址,而前3个字节为某种头部结构,包含报文类型及Tor payload的大小。这些数据为Encoder.Encode()
加密的所有数据。
图11. 待加密的Tor payload
如上图所示,其中Obfs4
客户端正准备调用Encoder.Encode()
,其中一个参数指向内存中的数据缓冲区,第1个字节为数据包类型(00
表示这是一个payload数据包),后面紧跟着的0x00BF
为Tor payload的大小(网络字节序)。从offset + 3
开始的数据为来自于tor.exe
的Tor payload。
此外,Obfs4
还会在经过加密的数据后附加一些大小随机的填充数据,进一步混淆报文大小,这也是对抗网络传感器的一种方式。
0x06 Obfs4使用IAT-Mode拆分报文
除了随机填充数据之外,Obfs4
还支持另一种反检测技术,以对抗网络传感器:IAT-mode。在前文中,我们在描述Obfs4
Bridge信息时提到了“IAT-mode”。IAT-mode的全称为“Inter-Arrival Timing”,可以将较大的报文(大小超过1448
字节的报文)按MTU(Maximum Transmission Unit,最大传输单元)大小拆分,或者拆分成更小报文。
MTU的定义为可用于传输数据的最大报文或者帧大小。网络驱动器会将大报文拆分成MTU大小的报文(在TCP握手过程中协商),以便再次重组(当然网络传感器也能轻松识别)。这也是为什么Obfs4
会引入IAT-mode的原因所在。
在obfs4
中,IAT-mode可取0
、1
或者2
这3个值。
0
表示IAT-mode处于禁用状态,大报文由网络驱动器拆分,因此网络传感器能探测到对应的网络指纹。
1
表示Tor自己会将大报文拆分成MTU大小的报文,不需要网络驱动器介入。Obfs4
Bridge使用的MTU值为1448
字节,这意味着更小的报文无法被网络传感器重组并分析。Obfs4
用来计算MTU大小的代码片段如下图所示。
2
表示将大报文拆分成大小可变的报文,具体大小在Obfs4
中定义。
图12. Obfs4
使用的MTU
拆出的小数据包会分别发送给Obfs4
中继。通过这种方式,Tor可以混淆网络指纹特征,使Obfs4
流量更难以识别。
典型的Obfs4
流量如下图所示,其中IAT-mode设置为1
。每个红色方框代表使用IAT-mode拆分的大报文,蓝色方框为被拆分出的小报文。第一个原始数据包的大小为2719
,根据Obfs4
的MTU值(1448
),该数据包会被拆分成2个更小的数据包。
图13. 启用IAT-mode的Obfs4
流量
0x07 通过其他方式获取Obfs4 Bridge信息
Tor用户可以通过3种方式获取Obfs4
Bridge信息:Tor网络设置、Tor网站以及邮件,每种方式都会获取到3个Obfs4
bridge配置行。用来获取Obfs4
bridge配置行的Tor网站截图如下所示:
图14. 用来获取Obfs4
bridge信息的Tor网站
一旦获取到Obfs4
Bridge信息,接下来我们需要拷贝并粘贴这些信息,如下图所示:
图15. 拷贝并粘贴3个Obfs4
bridge信息
0x08 总结
对Obfs4
Bridge进行分析后,我们发现Obfs4
Bridge可以用来保护Tor流量,避免被网络传感器识别和阻拦,主要原因包括如下3点:
1、Obfs4
会加密Tor流量,并添加填充数据来混淆数据包大小(握手报文同样采用该操作);
2、IAT-mode会将较大的Obfs4
报文拆分,以混淆网络指纹;
3、除了内置的Obfs4
Bridge之外,Tor还提供了3种方式,用来获取其他Obfs4
Bridge。