1 引言
当时看到这篇对代理检测的论文,对它的中文讨论较少,整理了自己阅读和实验后的笔记(关注于tor的obfs4),方便有需要的同学一起学习讨论。
(现在obfs4都要过时啦,出了新的WebTunnels,但是嘛,升级迭代也要相当一段时间了)
2 论文阅读
2.1 探针选择
我们的攻击集中在这样一个观察上,即从不使用数据进行响应是互联网上的异常行为。通过发送由流行协议和随机数据组成的探针,我们可以从超过400k IP/端口对的Tap数据集中的几乎所有端点(94%)获得响应。
对于不响应的端点,代理特有的TCP行为(如超时和数据阈值)与其他服务器相比存在不同。
从6个基本探测开始:HTTP、TLS、MODBUS、S7、随机字节,以及连接后不发送数据的空探测。对于每个探测,我们都会记录服务器如何响应其响应的数据(如果有)、关闭连接的时间(如果有)以及如何关闭连接(TCP FIN或RST)。
-
HTTP:对于HTTP,我们发送一个简单的HTTP/1.1 GET请求,主机头为example.com。大部分服务器将响应重定向或错误页面进行响应。即使不是HTTP的服务器也可能使用其协议固有的错误消息对此探测作出响应。
-
TLS:我们发送一条TLS客户端Hello消息,该消息通常由Chromium版本71浏览器生成。这包括流行的密码套件和扩展,即使服务器不支持TLS,也应该响应TLS警报消息。
-
Modbus:Modbus通常用于监控和数据采集(SCADA)环境中的可编程逻辑控制器(PLC)和其他嵌入式设备。使用了ZGrab中定义的探针,它发送一个3字节的命令,从远程主机请求设备信息描述符。
-
S7:S7是西门子PLC设备使用的专有协议。再次使用ZGrab中定义的探针,该探针通过COTP/S7协议发送设备标识符请求。
-
随机字节:不同数量随机字节的探测,服务器将解析失败,并以错误消息响应,或者以区别于代理服务器的方式关闭连接。
-
空探针:在连接后不发送数据。一些协议(如SSH)让服务器首先(或同时)与客户端通信。对于其他协议,当客户端发送了一些初始数据时,与不发送数据时相比,实现可能具有不同的超时。
-
DNS AXFR:根据DNS规范构建了一个DNS AXFR(区域传输)查询探测。
-
STUN: 实现了一个基于ZGrab-Golang库的STUN探测,用于探测Cisco WebEx设备
2.2 实验数据
-
代理端点
从Tor的BridgeDB收集了20多个obfs4代理端点 -
非代理端点
使用ZMap的主动网络扫描和网络流数据的被动收集。
我们使用ZMap向每个TCP端口(0-65535)上的20000个主机发送一个SYN数据包,总共有13亿次探测。发现了150万个响应SYN ACK的端点,我们将其标记为ZMap数据集。
科罗拉多大学的10 Gbps网关中采样netflow数据来收集端点。在无网络审查国家中,绝大多数流量不包含代理,所以收集到的端点主要是其他服务。在3天的时间跨度内,从ISP收集了超过550000个服务器IP/端口 的节点。其中,433286(79%)个主机接受了连接(使用一个TCP SYN-ACK响应),其余的大多数在尝试连接时只是超时。
后续扫描是在观察到连接后的几天内进行的,一些服务器可能在这段时间内移动了IP或离线。其次,服务器可能配置了防火墙,只允许从某些IP进行访问,可能会阻塞ZMap扫描主机。最终在这个数据集中捕获超过40万个发送数据的服务器。
2.3 实验结果
我们向190万个节点发送13个探测(第III-A节中的7个探测和6个随机数据范围为4KB-40KB的探测),并记录服务器是否和何时响应数据或关闭连接。如果服务器关闭,我们将记录它是否使用FIN或RST。如果服务器没有响应或关闭连接,我们将在300秒后超时,并将结果标记为超时。记录他们的关闭阈值。
由于抗探测代理从不响应数据,因此可以将响应的端点丢弃。在被动Tap数据集中,这排除了94%的主机,只留下26021个潜在代理。在ZMap数据集中,绝大多数主机对任何探测都没有数据响应,仅丢弃1.16%的端点 。
原因是ZMap识别的节点中,防火墙使用了chaff策略,仅在特定的子网上对SYN做出响应。42%的节点行为相同,不发送数据也不关闭与探测器的连接。将响应数据聚类(响应类型、响应字节数、连接关闭时间或超时时间) 在Tap数据集中,最流行的响应组包含3%的端点,似乎是Cloudflare网络中的TLS服务器(99.9%在具有端口443的端点上)。图2显示了ZMap和Tap数据集的不同响应集(按流行程度排序)的CDF,说明Tap数据集中有更大的多样性。前10个响应集占ZMap数据集中超过80%的端点,但只有13%的端点是在Tap中收集的。
2.4 检测方法
-
缓冲阈值
服务器将在接收到一定数量的字节后使用FIN关闭,并可能在接收到更高数量的字节之后使用RST关闭。代理服务的关闭阈值大多数是唯一的。
考虑一个从客户端读取N个字节并尝试将其解析为协议头的服务器。如果解析失败(例如无效字段、校验和或MAC),服务器可以简单地关闭连接。
如果客户端仅发送N−1字节,服务器可能会保持连接打开,并在尝试解析之前等待其他数据。obfs4握手长度可达8192字节,因此服务器在确定客户端无效并进入closeAfterDelay函数之前读取了这么多字节。此函数在随机延迟(30-90秒)后关闭连接,或在服务器启动时读取额外的N个字节(在0和8192之间随机选择的N个)后关闭。然而,这些读取都是使用1448字节的缓冲区完成的。这意味着,如果发送了 8192 + N 8192+N 8192+N字节,obfs4服务器将发送FIN,如果发送 8192 + N − ( ( 8192 + N ) m o d 1448 ) + 1448 8192 + N − ((8192 + N)mod 1448) + 1448 8192+N−((8192+N)mod1448)+1448字节,则返回rst。是obfs4独有行为。
论文作者与torproject交流研究成果后,obfs4proxy已于0.0.11版本修复关闭阈值的行为。
-
超时时间
许多端点根据其接收的数据量具有不同的超时。
不确认数据的主机不是标准的TCP行为,我们会重传,代理不会有这种行为,不影响结果。修复关闭阈值,但仍能观察到超时行为。
-
不响应任何探测是罕见的。
3 obfs4proxy分析
3.1 obfs4协议验证逻辑
- 握手过程
-
data内容
涉及算法:curve25519客户端及服务端都必须有自己的Keypair实例。根据curve25519包中的定义,PrivateKey在一定范围内随机生成。根据ECC算法,PublicKey可以通过调用curve25519.ScalarBaseMult()从私钥生成。
Representative秘钥根据公钥生成,在需要的时候可以调用extra25519.RepresentativeToPublicKey()函数再次转换为公钥。Keypair的定义及初始化代码位于NewKeypair()函数中,该函数在ntor.go文件中定义。
用extra25519.go里的ScalarBaseMult(),根据privatekey生成public和representative。representative通过Elligator 2映射完成混淆,避免收集到相当数量的公钥后推断出加密算法。
算法详细说明客户端及服务端都应当保存好私钥,将公钥以Representative秘钥的形式发送给对方。Obfs4客户端的连接过程从握手报文开始,因此我们来看一下客户端的握手报文。该报文的整体结构如下所示:
- Keypair.representative,其长度为20h字节。在服务器端,它可以作为还原客户端的公钥。(obfs4 bridge配置行)
- 使用随机字节的填充数据,其数据大小范围在4Dh至1FC0h之间,该填充数据会混淆握手数据包的大小,从而使其更难识别。
- 第一部分Keypair.representative的HMAC值。Obfs4使用SHA-256生成HMAC,长度为20h字节。Obfs4仅将前10h字节保留为HMAC,其余10h字节将被丢弃。
- Obfs4使用当前系统时间来计算UNIX Epoch时间(从UTC时间1970年1月1日星期四00:00:00开始)的小时值。计算数据包中前三个部分的HMAC值,加上(指拼接起来)字符串中的小时值。同样,其结果长度为20h字节,前10h字节作为数据包的第四部分。(防重放)
3.2 obfs4proxy源代码部分结构
- common/ 密码学、socks5等组件
- internal/ 更新的混淆算法
- obfs4proxy/
obfs4proxy.go 主函数 - transports/
包含meeklite、obfs2、3、4、 scramblesuit传输协议的实现- obfs4/
- framing/ 数据帧的处理
- handshake_ntor.go 完成obfs4握手,包括验证
- obfs4.go 握手相关函数
- framing/ 数据帧的处理
- obfs4/
3.3 obfs4proxy源代码部分函数
-
obfs4.go
-
WrapConn()
调用了serverHandshake(),不管返回什么错,都closeAfterDelay()。 -
serverHandshake()
新建服务端握手,设置基准超时时间。
然后按能读取的最大长度(8192)设置缓冲区,读取收包,保存收包内容,接下来parseClientHandshake()解析内容。
如果解析错误是ErrMarkNotFoundYet,继续读包,其他错误则函数返回错误。
解析正常则清空缓冲区。
-
-
handshake_ntor.go
-
parseClientHandshake()
对于接收内容过少/接收内容长度未达上限时,或找不到MarkMAC,准备了ErrMarkNotFoundYet的错误处理,可以返回继续读。
其他错误都关闭连接。找不到markMAC/HMAC/包长度不合规(有握手中不需要的数据),返ErrInvalidHandshake。
重放,返ErrReplayedHandshake。
计算验证不通过,返ErrNtorFailed。 -
closeAfterDelay()
closeAfterDelay()被修改,修复了因阈值产生FIN和RST的漏洞。旧版本会在延迟30s/接收一定长度数据后终止连接,新版本都是延迟30s终止连接。 -
SetReadDeadline()
开始读数据30s后,再有数据进入也返回超时错误,并终止连接。(论文里说有随机延迟,我没找到)
-
-
obfs4proxy.go
- serverHandler()
调用WrapConn(),如果返回错误,在日志中记录握手失败。 - serverAcceptLoop()
让服务器循环开启与客户端的握手,调用serverHandler()。
- serverHandler()
4 分析与实验
4.1 论文简析
论文人工构造的obfs4决策树中,首先随机发送17k的数据,2s内返回RST则进入下一步,发送其他探针,30s–90s之内返回FIN,则为obfs4。17k数据是为了引发阈值问题,已被修复,但延时FIN的情况仍然存在。
论文中针对obfs4 bridge的实验数据过少,我决定自己实验验证。在测试了几个已知obfs4 bridge后,我发现巧合的,每一个bridge都有固定的超时时间。
猜想:每一个bridge都有固定的超时时间。(有可能bridge在重启服务后会改变?随机延时时间只生成一次?)
对6个已知obfs4 bridge进行测试,发送同样的data(重放tor浏览器发起的成功握手中的data),命中了验证不通过的错误,进入closeAfterDelay()。
对每个bridge发起两次握手。对于同一bridge,在两个不同ip客户端上,发起的两次握手均经过相同时间后返回FIN,时间均落在[30,90]s。
4.2 实验验证
- 测试集:对测试集进行验证过滤后,得到四百余存活服务。
- 判断条件:TLS和HTTP探针无响应,17k字节返回RST或超时时间(返回FIN)为[30,90]s。
- 实验结果:
① 80%服务判断为obfs4 bridge。其中,50%返回RST,50%超时时间在范围内。
② 6%服务判断不为obfs4 bridge。
③ 10%服务无法确定,发17k字节立刻收到FIN,TLS和HTTP均超时。
4.3 实验分析
- 80%bridge服务命中判断条件,验证了判断条件可行。
- 6%服务中,发送TLS和HTTP均有回应,可探测出其他服务,应是提供的服务发生了变化,符合预期。
- 剩下10%服务,未命中判断条件,但是挑了几个,在tor浏览器中设置配置行后,可以作为bridge使用。发送20k字节后,50%返回RST。重放obfs4握手data之后,全部在[30,90]s内FIN。
猜测这些是已升级至新版本的bridge服务,重放握手数据后,则检验失败,进入延迟FIN函数。但是20k已经超过了论文所观察到的RST阈值,后来再发了30k,只有少数个响应RST。
4.4 实验结论
- 发送足够大的随机数据,马上返回RST(obfs4proxy 0.0.11版本已修复)
- 发送符合握手长度数据,[30,90]s内返回FIN
5 后续
结合其他特征后,已实现主动收集方案。
相关资料
torproject对于ORPort的讨论(move to here)
论文精读
论文讨论issue
torproject会议记录
torproject聊天记录